///|
/// BPSK mapping: 0 -> +1, 1 -> -1.
pub fn bpsk(word : Array[Int32]) -> Array[Double] raise CodeError {
ensure_bits(word, word.length())
word.map(fn(bit) { if bit == 0 { 1.0 } else { -1.0 } })
}
///|
/// AWGN LLR uses noise variance sigma^2 per real sample, NOT Eb/N0.
/// Caller supplies received real samples; this function does not generate noise.
pub fn awgn_llr(
samples : Array[Double],
variance : Double,
limit~ : Double = 50.0,
) -> Array[Double] raise CodeError {
if !finite(variance) || variance <= 0.0 {
raise InvalidNumber("positive finite noise variance")
}
validate_limit(limit)
let result : Array[Double] = []
for sample in samples {
if !finite(sample) {
raise InvalidNumber("nonfinite received sample")
}
// Divide before multiply; overflow simply saturates to the documented finite bound.
result.push(clamp(sample / variance * 2.0, limit))
}
result
}
///|
fn validate_limit(limit : Double) -> Unit raise CodeError {
if !finite(limit) || limit < 1.0 || limit > 100.0 {
raise InvalidNumber("LLR limit 1..100")
}
}
///|
/// BSC crossover probability must lie strictly between zero and one half.
pub fn bsc_llr(
received : Array[Int32],
crossover : Double,
limit~ : Double = 50.0,
) -> Array[Double] raise CodeError {
ensure_bits(received, received.length())
if !finite(crossover) || crossover <= 0.0 || crossover >= 0.5 {
raise InvalidNumber("BSC crossover in (0,0.5)")
}
validate_limit(limit)
let magnitude = clamp(@math.ln_1p(-crossover) - @math.ln(crossover), limit)
received.map(fn(bit) { if bit == 0 { magnitude } else { -magnitude } })
}
///|
/// BEC -> soft observations; an erasure has exactly zero LLR.
pub fn bec_llr(
observations : Array[Int32?],
limit~ : Double = 50.0,
) -> Array[Double] raise CodeError {
validate_limit(limit)
let result : Array[Double] = []
for i, value in observations {
match value {
None => result.push(0.0)
Some(0) => result.push(limit)
Some(1) => result.push(-limit)
Some(bit) => raise InvalidBit(i, bit)
}
}
result
}
///|
pub struct ErrorCounts {
priv frames : Int64
priv frame_errors : Int64
priv bits : Int64
priv bit_errors : Int64
priv nonconverged : Int64
} derive(Debug, ToJson)
///|
pub fn ErrorCounts::new() -> ErrorCounts {
{ frames: 0L, frame_errors: 0L, bits: 0L, bit_errors: 0L, nonconverged: 0L, }
}
///|
/// Pure aggregation: each result is a new value, no aliasing or partial mutation on error.
pub fn ErrorCounts::observe(
self : ErrorCounts,
transmitted : Array[Int32],
decoded : Array[Int32],
converged : Bool,
) -> ErrorCounts raise CodeError {
ensure_bits(transmitted, transmitted.length())
ensure_bits(decoded, transmitted.length())
if self.frames >= 1000000000000L ||
self.bits > 1000000000000000L - transmitted.length().to_int64() {
raise ResourceLimit("error counter capacity")
}
let mut errors = 0L
for i, bit in transmitted {
if bit != decoded[i] {
errors += 1L
}
}
{
frames: self.frames + 1L,
frame_errors: self.frame_errors + (if errors > 0L { 1L } else { 0L }),
bits: self.bits + transmitted.length().to_int64(),
bit_errors: self.bit_errors + errors,
nonconverged: self.nonconverged + (if converged { 0L } else { 1L }),
}
}
///|
pub fn ErrorCounts::bit_error_rate(self : ErrorCounts) -> Double? {
if self.bits == 0L {
None
} else {
Some(self.bit_errors.to_double() / self.bits.to_double())
}
}
///|
pub fn ErrorCounts::frame_error_rate(self : ErrorCounts) -> Double? {
if self.frames == 0L {
None
} else {
Some(self.frame_errors.to_double() / self.frames.to_double())
}
}
///|
pub extend ErrorCounts with @debug.Debug::{to_repr}
///|
pub extend ErrorCounts with ToJson::{to_json}
///|
/// Heteroscedastic real AWGN demapping. Each sample has its own noise variance.
pub fn awgn_llr_per_sample(
samples : Array[Double],
variances : Array[Double],
limit~ : Double = 50.0,
) -> Array[Double] raise CodeError {
if samples.length() != variances.length() {
raise LengthMismatch(samples.length(), variances.length())
}
validate_limit(limit)
let result : Array[Double] = []
for i, sample in samples {
let variance = variances[i]
if !finite(sample) || !finite(variance) || variance <= 0.0 {
raise InvalidNumber("finite sample and positive finite variance")
}
result.push(clamp(sample / variance * 2.0, limit))
}
result
}
///|
/// Merge independent simulation shards without sharing mutable counters.
pub fn ErrorCounts::merge(
self : ErrorCounts,
other : ErrorCounts,
) -> ErrorCounts raise CodeError {
if self.frames > 1000000000000L - other.frames ||
self.bits > 1000000000000000L - other.bits {
raise ResourceLimit("merged error counter capacity")
}
{
frames: self.frames + other.frames,
frame_errors: self.frame_errors + other.frame_errors,
bits: self.bits + other.bits,
bit_errors: self.bit_errors + other.bit_errors,
nonconverged: self.nonconverged + other.nonconverged,
}
}
///|
pub fn ErrorCounts::nonconvergence_rate(self : ErrorCounts) -> Double? {
if self.frames == 0L {
None
} else {
Some(self.nonconverged.to_double() / self.frames.to_double())
}
}