use crate::bounds::bpsk_noise_variance;
use crate::conv::{Trellis, bcjr};
use comnum_core::rng::Rng;
use comnum_core::{Error, Result};
use comnum_math::{Jacobian, bisect, gaussian_nodes, ln_1p_exp};
use std::f64::consts::LN_2;
pub fn j_function(sigma: f64) -> f64 {
if sigma <= 0.0 {
return 0.0;
}
let mu = sigma * sigma / 2.0;
let loss: f64 =
gaussian_nodes(sigma * sigma, false, 64).iter().map(|(w, p)| p * ln_1p_exp(-(mu + w.re)) / LN_2).sum();
(1.0 - loss).clamp(0.0, 1.0)
}
pub fn j_inverse(mi: f64) -> f64 {
if mi <= 0.0 {
return 0.0;
}
bisect(|s| j_function(s) < mi, 0.0, 60.0, 80)
}
pub fn llr_mutual_information(bits: &[u8], llr: &[f64]) -> Result<f64> {
if bits.len() != llr.len() || bits.is_empty() {
return Err(Error::Shape(format!("{} bits for {} LLRs", bits.len(), llr.len())));
}
let loss: f64 = bits
.iter()
.zip(llr)
.map(|(&b, &l)| {
let x = if b == 0 { 1.0 } else { -1.0 };
ln_1p_exp(-x * l) / LN_2
})
.sum();
Ok(1.0 - loss / bits.len() as f64)
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExitCurve {
pub apriori: Vec<f64>,
pub extrinsic: Vec<f64>,
}
impl ExitCurve {
pub fn measure(
trellis: &Trellis,
parity_period: usize,
ebn0_db: f64,
rate: f64,
n_bits: usize,
apriori: &[f64],
seed: u64,
) -> Result<ExitCurve> {
if trellis.n_out() != 2 || parity_period == 0 || n_bits == 0 || !(rate > 0.0 && rate <= 1.0) {
return Err(Error::Config(format!(
"an EXIT curve needs a rate-1/2 recursive code, a period >= 1, bits and a rate in (0, 1], got n_out {}, period {parity_period}, {n_bits} bits, rate {rate}",
trellis.n_out()
)));
}
let mut rng = Rng::new(seed);
let bits: Vec<u8> = (0..n_bits).map(|_| rng.bit()).collect();
let coded = trellis.encode(&bits, false);
let sd = bpsk_noise_variance(ebn0_db, rate).sqrt();
let llr: Vec<f64> = coded
.iter()
.enumerate()
.map(|(i, &b)| {
let x = if b == 0 { 1.0 } else { -1.0 };
let y = x + sd * rng.gaussian();
if i % 2 == 1 && (i / 2) % parity_period != 0 { 0.0 } else { 2.0 * y / (sd * sd) }
})
.collect();
let noise: Vec<f64> = (0..n_bits).map(|_| rng.gaussian()).collect();
let extrinsic = apriori
.iter()
.map(|&ia| {
let sa = j_inverse(ia);
let a: Vec<f64> = bits
.iter()
.zip(&noise)
.map(|(&b, &n)| {
let x = if b == 0 { 1.0 } else { -1.0 };
sa * sa / 2.0 * x + sa * n
})
.collect();
let d = bcjr(trellis, &llr, &a, false, Jacobian::Exact)?;
let e: Vec<f64> = (0..n_bits).map(|k| d[k] - a[k] - llr[2 * k]).collect();
llr_mutual_information(&bits, &e)
})
.collect::<Result<Vec<f64>>>()?;
Ok(ExitCurve { apriori: apriori.to_vec(), extrinsic })
}
pub fn transfer(&self, i: f64) -> f64 {
let x = &self.apriori;
if i <= x[0] {
return self.extrinsic[0];
}
for k in 1..x.len() {
if i <= x[k] {
let t = (i - x[k - 1]) / (x[k] - x[k - 1]);
return self.extrinsic[k - 1] + t * (self.extrinsic[k] - self.extrinsic[k - 1]);
}
}
self.extrinsic[x.len() - 1]
}
pub fn tunnel_open(&self, second: &ExitCurve, target: f64) -> bool {
let mut i = 0.0;
for _ in 0..10_000 {
let next = second.transfer(self.transfer(i));
if next >= target {
return true;
}
if next <= i + 1e-9 {
return false;
}
i = next;
}
false
}
}
#[allow(clippy::too_many_arguments, reason = "the parameters of the measurement and of the search")]
pub fn pinch_off_db(
first: &Trellis,
second: &Trellis,
parity_period: usize,
rate: f64,
n_bits: usize,
seed: u64,
(lo, hi): (f64, f64),
steps: usize,
end: f64,
) -> Result<f64> {
let grid: Vec<f64> = (0..50).map(|k| f64::from(k) * 0.02).chain(std::iter::once(0.995)).collect();
let mut err = None;
let t = bisect(
|db| {
let curves = ExitCurve::measure(first, parity_period, db, rate, n_bits, &grid, seed)
.and_then(|c1| Ok((c1, ExitCurve::measure(second, parity_period, db, rate, n_bits, &grid, seed)?)));
match curves {
Ok((c1, c2)) => !c1.tunnel_open(&c2, end),
Err(e) => {
err = Some(e);
false
}
}
},
lo,
hi,
steps,
);
match err {
Some(e) => Err(e),
None => Ok(t),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_estimator_recovers_j_on_gaussian_llrs() {
let mut rng = Rng::new(9);
for sigma in [0.5, 1.5, 3.0] {
let bits: Vec<u8> = (0..200_000).map(|_| rng.bit()).collect();
let llr: Vec<f64> = bits
.iter()
.map(|&b| {
let x = if b == 0 { 1.0 } else { -1.0 };
sigma * sigma / 2.0 * x + sigma * rng.gaussian()
})
.collect();
let mi = llr_mutual_information(&bits, &llr).unwrap();
assert!((mi - j_function(sigma)).abs() < 0.005, "{sigma}: {mi} {}", j_function(sigma));
}
assert!(llr_mutual_information(&[0, 1], &[0.0, 0.0]).unwrap().abs() < 1e-15);
assert!(llr_mutual_information(&[0], &[]).is_err());
}
#[test]
fn transfer_curves_rise_with_the_apriori_and_the_channel() {
let t = Trellis::recursive(3, 0o7, 0o5).unwrap();
let grid = [0.0, 0.5, 0.99];
let low = ExitCurve::measure(&t, 2, 0.5, 0.5, 20_000, &grid, 1).unwrap();
let high = ExitCurve::measure(&t, 2, 2.0, 0.5, 20_000, &grid, 1).unwrap();
assert!(low.extrinsic.windows(2).all(|w| w[1] > w[0]), "{low:?}");
assert!(high.extrinsic.iter().zip(&low.extrinsic).all(|(h, l)| h > l));
assert!(high.extrinsic[2] > 0.95);
assert!(ExitCurve::measure(&t, 0, 1.0, 0.5, 100, &grid, 1).is_err());
}
}