///|
/// Hard decisions use zero -> 0. Nonfinite channel inputs are rejected.
pub fn decide(llr : Array[Double]) -> Array[Int32] raise CodeError {
  for value in llr {
    if !finite(value) {
      raise InvalidNumber("nonfinite decision LLR")
    }
  }
  hard_decision(llr)
}

///|
/// Hamming distance with strict binary and length validation.
pub fn bit_distance(
  left : Array[Int32],
  right : Array[Int32],
) -> Int32 raise CodeError {
  ensure_bits(left, left.length())
  ensure_bits(right, left.length())
  let mut distance = 0
  for i, bit in left {
    if bit != right[i] {
      distance += 1
    }
  }
  distance
}

///|
/// Stable logistic conversion; returns P(bit=1), including saturated finite LLRs.
pub fn bit_one_probabilities(
  llr : Array[Double],
) -> Array[Double] raise CodeError {
  let result : Array[Double] = []
  for value in llr {
    if !finite(value) {
      raise InvalidNumber("nonfinite probability LLR")
    }
    let e = @math.exp(-value.abs())
    result.push(if value >= 0.0 { e / (1.0 + e) } else { 1.0 / (1.0 + e) })
  }
  result
}

///|
/// Probabilities of bit one become log(P0/P1). Endpoints saturate at limit.
pub fn probabilities_to_llr(
  probabilities : Array[Double],
  limit~ : Double = 50.0,
) -> Array[Double] raise CodeError {
  validate_limit(limit)
  let result : Array[Double] = []
  for p in probabilities {
    if !finite(p) || p < 0.0 || p > 1.0 {
      raise InvalidNumber("probability in [0,1]")
    }
    result.push(
      if p == 0.0 {
        limit
      } else if p == 1.0 {
        -limit
      } else {
        clamp(@math.ln_1p(-p) - @math.ln(p), limit)
      },
    )
  }
  result
}