Skip to main content

comnum_fec/
exit.rs

1//! Extrinsic information transfer (EXIT) charts of convolutional constituent codes (ten
2//! Brink 2001): the mutual information between the bits and their LLRs, the transfer
3//! curve $I_{E} = T(I_{A})$ of a BCJR decoder fed with Gaussian a priori LLRs, and the
4//! pinch-off $E_{b}/N_{0}$ of a parallel concatenation, below which the decoding tunnel
5//! between the two curves closes.
6//!
7//! The a priori LLRs are modelled as $A = \mu_{A} x + n_{A}$ with $x = \pm 1$,
8//! $n_{A} \sim \mathcal{N}(0, \sigma_{A}^{2})$ and $\mu_{A} = \sigma_{A}^{2}/2$ (eqs. (9)–(11));
9//! their mutual information with the bits is $J(\sigma_{A})$ (eq. (14)),
10//! $$J(\sigma) = 1 - \int \frac{e^{-(\xi - \sigma^{2}/2)^{2}/(2\sigma^{2})}}{\sqrt{2\pi\sigma^{2}}}\log_{2}(1 + e^{-\xi})\,d\xi$$
11//! ([`j_function`], Gauss–Hermite with 64 nodes; [`j_inverse`] by bisection). The
12//! extrinsic LLRs of the decoder are $E = D - A - Z$ (eq. (19)), $D$ the a posteriori,
13//! $Z$ the channel LLRs of the systematic bits; their mutual information is estimated by
14//! the time average $1 - \frac1N\sum_{k}\log_{2}(1 + e^{-x_{k}E_{k}})$
15//! ([`llr_mutual_information`]), exact in the mean for consistent LLRs (the paper uses
16//! histograms of $E$). [`ExitCurve::measure`] simulates a recursive systematic code on the
17//! BPSK AWGN channel with its parity punctured by a period, and [`pinch_off_db`]
18//! bisects the $E_{b}/N_{0}$ at which $T_{2}(T_{1}(I)) > I$ stops holding on
19//! $[0, I_{\max}]$, the curves measured on common random numbers.
20//!
21//! # Examples
22//! ```
23//! use comnum_fec::exit::{j_function, j_inverse};
24//! // J(0) = 0, J grows to 1; J and its inverse agree
25//! assert!(j_function(0.0).abs() < 1e-12 && j_function(20.0) > 0.999_99);
26//! assert!((j_inverse(j_function(1.7)) - 1.7).abs() < 1e-9);
27//! ```
28//!
29//! # References
30//! 1. S. ten Brink, "Convergence behavior of iteratively decoded parallel concatenated
31//!    codes," *IEEE Trans. Commun.* 49(10), 1727–1737, 2001: eqs. (9)–(19), Tables II, IV.
32
33use crate::bounds::bpsk_noise_variance;
34use crate::conv::{Trellis, bcjr};
35use comnum_core::rng::Rng;
36use comnum_core::{Error, Result};
37use comnum_math::{Jacobian, bisect, gaussian_nodes, ln_1p_exp};
38use std::f64::consts::LN_2;
39
40/// $J(\sigma)$, the mutual information between a bit and a consistent Gaussian LLR of
41/// standard deviation $\sigma$ and mean $\sigma^{2}/2$ (ten Brink 2001, eq. (14)).
42pub fn j_function(sigma: f64) -> f64 {
43    if sigma <= 0.0 {
44        return 0.0;
45    }
46    let mu = sigma * sigma / 2.0;
47    let loss: f64 =
48        gaussian_nodes(sigma * sigma, false, 64).iter().map(|(w, p)| p * ln_1p_exp(-(mu + w.re)) / LN_2).sum();
49    (1.0 - loss).clamp(0.0, 1.0)
50}
51
52/// The inverse of [`j_function`] on $[0, 1)$, by bisection of $\sigma$ on $[0, 60]$.
53pub fn j_inverse(mi: f64) -> f64 {
54    if mi <= 0.0 {
55        return 0.0;
56    }
57    bisect(|s| j_function(s) < mi, 0.0, 60.0, 80)
58}
59
60/// The mutual information between the bits `bits` (0 or 1) and their LLRs `llr`
61/// (positive for 0), by the time average $1 - \frac1N\sum\log_{2}(1 + e^{-x_{k}L_{k}})$,
62/// $x_{k} = 1 - 2b_{k}$.
63///
64/// # Errors
65/// [`Error::Shape`] for lengths that differ or none.
66pub fn llr_mutual_information(bits: &[u8], llr: &[f64]) -> Result<f64> {
67    if bits.len() != llr.len() || bits.is_empty() {
68        return Err(Error::Shape(format!("{} bits for {} LLRs", bits.len(), llr.len())));
69    }
70    let loss: f64 = bits
71        .iter()
72        .zip(llr)
73        .map(|(&b, &l)| {
74            let x = if b == 0 { 1.0 } else { -1.0 };
75            ln_1p_exp(-x * l) / LN_2
76        })
77        .sum();
78    Ok(1.0 - loss / bits.len() as f64)
79}
80
81/// The transfer curve of a constituent decoder on a grid of a priori informations.
82#[derive(Clone, Debug, PartialEq)]
83pub struct ExitCurve {
84    /// The a priori informations $I_{A}$, increasing.
85    pub apriori: Vec<f64>,
86    /// The extrinsic informations $I_{E} = T(I_{A})$.
87    pub extrinsic: Vec<f64>,
88}
89
90impl ExitCurve {
91    /// Measure the transfer curve of the recursive systematic code `trellis` (rate $1/2$:
92    /// systematic and parity bits) whose parity is kept once every `parity_period` bits
93    /// (2: a rate-2/3 constituent), at the $E_{b}/N_{0}$ `ebn0_db` of the concatenated
94    /// code of rate `rate`, on a block of `n_bits` information bits (unterminated,
95    /// log-MAP BCJR), at the a priori informations `apriori`. The bits, the channel and
96    /// the a priori noise are drawn from `seed`: two curves of the same seed share them.
97    ///
98    /// # Errors
99    /// [`Error::Config`] for a trellis that is not of rate 1/2, a period of 0, no bit, or
100    /// a rate outside $(0, 1]$; as [`bcjr`].
101    pub fn measure(
102        trellis: &Trellis,
103        parity_period: usize,
104        ebn0_db: f64,
105        rate: f64,
106        n_bits: usize,
107        apriori: &[f64],
108        seed: u64,
109    ) -> Result<ExitCurve> {
110        if trellis.n_out() != 2 || parity_period == 0 || n_bits == 0 || !(rate > 0.0 && rate <= 1.0) {
111            return Err(Error::Config(format!(
112                "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}",
113                trellis.n_out()
114            )));
115        }
116        let mut rng = Rng::new(seed);
117        let bits: Vec<u8> = (0..n_bits).map(|_| rng.bit()).collect();
118        let coded = trellis.encode(&bits, false);
119        let sd = bpsk_noise_variance(ebn0_db, rate).sqrt();
120        let llr: Vec<f64> = coded
121            .iter()
122            .enumerate()
123            .map(|(i, &b)| {
124                let x = if b == 0 { 1.0 } else { -1.0 };
125                let y = x + sd * rng.gaussian();
126                // the parity of step k = i / 2 is punctured unless k is a multiple of the period
127                if i % 2 == 1 && (i / 2) % parity_period != 0 { 0.0 } else { 2.0 * y / (sd * sd) }
128            })
129            .collect();
130        let noise: Vec<f64> = (0..n_bits).map(|_| rng.gaussian()).collect();
131        let extrinsic = apriori
132            .iter()
133            .map(|&ia| {
134                let sa = j_inverse(ia);
135                let a: Vec<f64> = bits
136                    .iter()
137                    .zip(&noise)
138                    .map(|(&b, &n)| {
139                        let x = if b == 0 { 1.0 } else { -1.0 };
140                        sa * sa / 2.0 * x + sa * n
141                    })
142                    .collect();
143                let d = bcjr(trellis, &llr, &a, false, Jacobian::Exact)?;
144                let e: Vec<f64> = (0..n_bits).map(|k| d[k] - a[k] - llr[2 * k]).collect();
145                llr_mutual_information(&bits, &e)
146            })
147            .collect::<Result<Vec<f64>>>()?;
148        Ok(ExitCurve { apriori: apriori.to_vec(), extrinsic })
149    }
150
151    /// $T(I)$ by linear interpolation on the grid (clamped at its ends).
152    pub fn transfer(&self, i: f64) -> f64 {
153        let x = &self.apriori;
154        if i <= x[0] {
155            return self.extrinsic[0];
156        }
157        for k in 1..x.len() {
158            if i <= x[k] {
159                let t = (i - x[k - 1]) / (x[k] - x[k - 1]);
160                return self.extrinsic[k - 1] + t * (self.extrinsic[k] - self.extrinsic[k - 1]);
161            }
162        }
163        self.extrinsic[x.len() - 1]
164    }
165
166    /// Whether the tunnel between this curve (first decoder) and `second` is open: the
167    /// alternating passes $I \to T_{2}(T_{1}(I))$ from $I = 0$ reach `target`.
168    pub fn tunnel_open(&self, second: &ExitCurve, target: f64) -> bool {
169        let mut i = 0.0;
170        for _ in 0..10_000 {
171            let next = second.transfer(self.transfer(i));
172            if next >= target {
173                return true;
174            }
175            if next <= i + 1e-9 {
176                return false;
177            }
178            i = next;
179        }
180        false
181    }
182}
183
184/// The pinch-off $E_{b}/N_{0}$ (dB) of the parallel concatenation of `first` and
185/// `second` (rate `rate`, parities punctured by `parity_period`): the bisection, `steps`
186/// steps on $[$`lo`$,$ `hi`$]$, of the $E_{b}/N_{0}$ at which the passes reach $I$ = `end`
187/// (no intersection of the curves before the end of the chart; the curve of a code of
188/// small memory reaches 1 only at $I_{A} = 1$, so `end` stays below 1: 0.97), every curve measured with [`ExitCurve::measure`] on `n_bits` bits and the
189/// same `seed`, on the grid $I_{A} = 0, 0.02, \dots, 0.98$ and 0.995.
190///
191/// # Errors
192/// As [`ExitCurve::measure`].
193#[allow(clippy::too_many_arguments, reason = "the parameters of the measurement and of the search")]
194pub fn pinch_off_db(
195    first: &Trellis,
196    second: &Trellis,
197    parity_period: usize,
198    rate: f64,
199    n_bits: usize,
200    seed: u64,
201    (lo, hi): (f64, f64),
202    steps: usize,
203    end: f64,
204) -> Result<f64> {
205    let grid: Vec<f64> = (0..50).map(|k| f64::from(k) * 0.02).chain(std::iter::once(0.995)).collect();
206    let mut err = None;
207    let t = bisect(
208        |db| {
209            let curves = ExitCurve::measure(first, parity_period, db, rate, n_bits, &grid, seed)
210                .and_then(|c1| Ok((c1, ExitCurve::measure(second, parity_period, db, rate, n_bits, &grid, seed)?)));
211            match curves {
212                // below: the tunnel is closed
213                Ok((c1, c2)) => !c1.tunnel_open(&c2, end),
214                Err(e) => {
215                    err = Some(e);
216                    false
217                }
218            }
219        },
220        lo,
221        hi,
222        steps,
223    );
224    match err {
225        Some(e) => Err(e),
226        None => Ok(t),
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    /// The estimator gives J(sigma) on LLRs drawn from the a priori model, and 0 on
235    /// LLRs that carry no information.
236    #[test]
237    fn the_estimator_recovers_j_on_gaussian_llrs() {
238        let mut rng = Rng::new(9);
239        for sigma in [0.5, 1.5, 3.0] {
240            let bits: Vec<u8> = (0..200_000).map(|_| rng.bit()).collect();
241            let llr: Vec<f64> = bits
242                .iter()
243                .map(|&b| {
244                    let x = if b == 0 { 1.0 } else { -1.0 };
245                    sigma * sigma / 2.0 * x + sigma * rng.gaussian()
246                })
247                .collect();
248            let mi = llr_mutual_information(&bits, &llr).unwrap();
249            assert!((mi - j_function(sigma)).abs() < 0.005, "{sigma}: {mi} {}", j_function(sigma));
250        }
251        assert!(llr_mutual_information(&[0, 1], &[0.0, 0.0]).unwrap().abs() < 1e-15);
252        assert!(llr_mutual_information(&[0], &[]).is_err());
253    }
254
255    /// A transfer curve rises with the a priori information, and a stronger channel lifts
256    /// it; with full a priori information the extrinsic information is close to 1.
257    #[test]
258    fn transfer_curves_rise_with_the_apriori_and_the_channel() {
259        let t = Trellis::recursive(3, 0o7, 0o5).unwrap();
260        let grid = [0.0, 0.5, 0.99];
261        let low = ExitCurve::measure(&t, 2, 0.5, 0.5, 20_000, &grid, 1).unwrap();
262        let high = ExitCurve::measure(&t, 2, 2.0, 0.5, 20_000, &grid, 1).unwrap();
263        assert!(low.extrinsic.windows(2).all(|w| w[1] > w[0]), "{low:?}");
264        assert!(high.extrinsic.iter().zip(&low.extrinsic).all(|(h, l)| h > l));
265        assert!(high.extrinsic[2] > 0.95);
266        assert!(ExitCurve::measure(&t, 0, 1.0, 0.5, 100, &grid, 1).is_err());
267    }
268}