///|
pub(all) suberror CodaError {
Invalid(String)
Shape(String)
Budget(String)
Numerical(String)
} derive(@debug.Debug)
///|
pub extend CodaError with @debug.Debug::{to_repr}
///|
fn finite(x : Double) -> Bool {
x == x && x.abs() <= 1.0e300
}
///|
fn checked(x : Double) -> Double raise CodaError {
if !finite(x) {
raise Numerical("non-finite or out-of-range intermediate")
}
x
}
///|
fn vector_size(n : Int) -> Unit raise CodaError {
if n < 2 || n > 64 {
raise Budget("composition dimension must be 2..64")
}
}
///|
fn real_vector(x : Array[Double]) -> Unit raise CodaError {
for v in x {
if !finite(v) {
raise Invalid("expected finite numbers bounded by 1e300")
}
}
}
///|
/// Normalize nonnegative weights without summing large raw numbers.
/// Zeros are allowed here; log-ratio routines require strictly positive parts.
pub fn closure(x : Array[Double]) -> Array[Double] raise CodaError {
vector_size(x.length())
real_vector(x)
let mut scale = 0.0
for v in x {
if v < 0.0 {
raise Invalid("negative composition part")
}
if v > scale {
scale = v
}
}
if scale == 0.0 {
raise Invalid("all-zero composition")
}
let mut total = 0.0
for v in x {
total += v / scale
}
let result = x.map(v => checked(v / scale / total))
for i, v in x {
if v > 0.0 && result[i] == 0.0 {
raise Numerical("closure underflow")
}
}
result
}
///|
pub fn positive(x : Array[Double]) -> Unit raise CodaError {
vector_size(x.length())
real_vector(x)
for v in x {
if v <= 0.0 {
raise Invalid("log-ratios require strictly positive parts")
}
}
}