///|
/// Center logarithms directly, avoiding closure underflow for wide ratios.
pub fn clr(x : Array[Double]) -> Array[Double] raise CodaError {
  positive(x)
  let logs = x.map(v => @math.ln(v))
  let mut mean = 0.0
  for v in logs {
    mean += v / logs.length().to_double()
  }
  logs.map(v => v - mean)
}

///|
/// Stable softmax. Reject an unrepresentable positive composition.
pub fn clr_inverse(z : Array[Double]) -> Array[Double] raise CodaError {
  vector_size(z.length())
  real_vector(z)
  let mut high = z[0]
  for v in z {
    if v > high {
      high = v
    }
  }
  let weights = z.map(v => @math.exp(v - high))
  for v in weights {
    if v == 0.0 {
      raise Numerical("inverse log-ratio underflow")
    }
  }
  closure(weights)
}

///|
/// Geometric mean may overflow/underflow only when the result is unrepresentable.
pub fn geometric_mean(x : Array[Double]) -> Double raise CodaError {
  positive(x)
  let mut mean = 0.0
  for v in x {
    mean += @math.ln(v) / x.length().to_double()
  }
  let g = checked(@math.exp(mean))
  if g <= 0.0 {
    raise Numerical("geometric mean underflow")
  }
  g
}