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}