Skip to main content

comnum_fec/
puncture.rs

1//! Puncturing of the convolutional and turbo codes: rate-compatible patterns, the
2//! punctured block codes and the free distance of a punctured code.
3//!
4//! # Model
5//!
6//! A rate-$1/n$ code emits $n$ bits per trellis step; a [`Puncturer`] of period $P$
7//! keeps the bit $i$ of step $t$ when the entry $(i, t \bmod P)$ of its pattern, an
8//! $n \times P$ matrix of 0 and 1, is 1. With $w$ ones in the pattern, the rate becomes
9//! $P / w$ (Cain, Clark & Geist 1979). The receiver puts back a zero LLR at every
10//! punctured position: in the Viterbi and BCJR branch metric $\sum_{i} (1 - 2c_{i})
11//! L_{i} / 2$ of [`crate::conv`] a zero contributes nothing, which is the standard
12//! treatment of a dummy ("stolen") bit (IEEE 802.11-2020, Sec. 17.3.5.6).
13//!
14//! The patterns of the standards are provided as constructors:
15//!
16//! | Constructor | Source | Pattern (row $i$ = output $i$) |
17//! |---|---|---|
18//! | [`Puncturer::ieee80211_2_3`] | IEEE 802.11-2020, Sec. 17.3.5.6, Figure 17-9 | A: 1 1, B: 1 0 |
19//! | [`Puncturer::ieee80211_3_4`] | IEEE 802.11-2020, Sec. 17.3.5.6, Figure 17-9 | A: 1 1 0, B: 1 0 1 |
20//! | [`Puncturer::ieee80211_5_6`] | IEEE 802.11-2020, Sec. 19.3.11.6, Figure 19-11 | A: 1 1 0 1 0, B: 1 0 1 0 1 |
21//! | [`Puncturer::dvbs_2_3`] | ETSI EN 300 421 V1.1.2, Table 2 | Y: 1 1, X: 1 0 |
22//! | [`Puncturer::dvbs_3_4`] | ETSI EN 300 421 V1.1.2, Table 2 | Y: 1 1 0, X: 1 0 1 |
23//! | [`Puncturer::dvbs_5_6`] | ETSI EN 300 421 V1.1.2, Table 2 | Y: 1 1 0 1 0, X: 1 0 1 0 1 |
24//! | [`Puncturer::dvbs_7_8`] | ETSI EN 300 421 V1.1.2, Table 2 | Y: 1 1 1 1 0 1 0, X: 1 0 0 0 1 0 1 |
25//! | [`Puncturer::turbo_1_2`] | Berrou, Glavieux & Thitimajshima 1993, Sec. II | systematic: 1 1, parity 1: 1 0, parity 2: 0 1 |
26//!
27//! all for the $(133, 171)_{8}$ code of constraint length 7 in the output order of
28//! [`ConvolutionalCode::k7`](crate::conv::ConvolutionalCode::k7) ($133_{8}$ first), but
29//! the last, for the three streams of a turbo code. IEEE 802.11 emits A from $133_{8}$
30//! and B from $171_{8}$, so its rows are in the printed order; DVB-S emits X from
31//! $G_{1} = 171_{8}$ and Y from $G_{2} = 133_{8}$ (EN 300 421, Sec. 4.4.3), so the rows of
32//! its Table 2 are given Y first. The two standards then puncture the same codes: the
33//! patterns of rates 2/3, 3/4 and 5/6 are defined once, by IEEE 802.11, and the DVB-S
34//! constructors of these rates are aliases kept under the name of their standard. They
35//! keep the bits of DVB-S but send $Y$ before $X$ at every step; the transmitted order of
36//! the standard, $X$ first, is that of `comnum_systems::dvb::InnerRate`.
37//!
38//! A rate matching that transmits some bits of a word, once or several times, is a
39//! [`BitSelection`]: output bit $t$ carries bit $\pi(t)$ of the word, and the receiver
40//! sums the LLRs of every bit over its copies (zero for a bit never sent). A puncturer
41//! is one ([`Puncturer::selection`]), as are the circular buffers of 5G NR
42//! ([`crate::nr`]).
43//!
44//! [`Punctured`] wraps a [`BlockCode`] whose coded bits are in step-major order (the
45//! [`ConvolutionalCode`](crate::conv::ConvolutionalCode), tail included), and
46//! [`PuncturedTurbo`] a [`TurboCode`] whose codeword is the
47//! systematic, first-parity and second-parity streams (the pattern is applied across
48//! the three streams at every information index; the tail bits are kept). The
49//! rate matching of 3GPP (TS 36.212, Sec. 5.1.4: sub-block interleaving and a circular
50//! buffer) is not a periodic pattern and is not provided.
51//!
52//! [`free_distance`] computes the free distance of a punctured code from its trellis, a
53//! shortest path on the $2^{K-1} \times P$ states of the periodically time-varying
54//! trellis; it reproduces the tables of Yasuda, Kashiki & Hirata (1984).
55//!
56//! # Examples
57//! ```
58//! use comnum_fec::block::BlockCode;
59//! use comnum_fec::conv::ConvolutionalCode;
60//! use comnum_fec::puncture::{Punctured, Puncturer, free_distance};
61//! let code = Punctured::new(ConvolutionalCode::k7(100)?, Puncturer::ieee80211_3_4())?;
62//! assert_eq!(code.n(), 35 * 4 + 2);                 // 4 bits kept of every 6, 2 of the last 2
63//! let info: Vec<u8> = (0..100).map(|i| ((i * 5) % 7 < 3) as u8).collect();
64//! let c = code.encode_block(&info)?;
65//! let llr: Vec<f64> = c.iter().map(|&b| if b == 0 { 4.0 } else { -4.0 }).collect(); // noiseless
66//! assert_eq!(code.decode_block(&llr)?, info);
67//! assert_eq!(free_distance(code.code().trellis(), code.puncturer())?, 5);
68//! # Ok::<(), comnum_core::Error>(())
69//! ```
70//!
71//! # References
72//!
73//! 1. J. B. Cain, G. C. Clark and J. M. Geist, "Punctured convolutional codes of rate
74//!    $(n-1)/n$ and simplified maximum likelihood decoding," *IEEE Trans. Inf. Theory*,
75//!    25(1), 97–100, 1979.
76//! 2. Y. Yasuda, K. Kashiki and Y. Hirata, "High-rate punctured convolutional codes for
77//!    soft decision Viterbi decoding," *IEEE Trans. Commun.*, 32(3), 315–319, 1984,
78//!    Table I.
79//! 3. IEEE Std 802.11-2020, Sec. 17.3.5.6 (Figure 17-9) and Sec. 19.3.11.6
80//!    (Figure 19-11).
81//! 4. ETSI EN 300 421 V1.1.2, *Digital Video Broadcasting (DVB); Framing structure,
82//!    channel coding and modulation for 11/12 GHz satellite services*, 1997, Sec. 4.4.3,
83//!    Table 2.
84//! 5. C. Berrou, A. Glavieux and P. Thitimajshima, "Near Shannon limit error-correcting
85//!    coding and decoding: turbo-codes," *Proc. IEEE ICC*, 1064–1070, 1993, Sec. II.
86
87use crate::block::{BlockCode, SoftDecode, container_traits, expect_len};
88use crate::conv::Trellis;
89use crate::turbo::{TurboCode, TurboDecoder};
90use comnum_core::{Error, Result, ensure, ensure_shape};
91
92/// The bits of a word of `full_len` bits that a rate matching transmits: output bit $t$
93/// carries bit `positions()[t]` of the word (a bit may be sent several times, or never).
94///
95/// # Examples
96/// ```
97/// use comnum_fec::puncture::BitSelection;
98/// // a word of 4 bits, bit 3 dropped, bit 0 repeated
99/// let sel = BitSelection::new(vec![0, 1, 2, 0], 4)?;
100/// assert_eq!(sel.select(&[1u8, 0, 1, 1])?, [1, 0, 1, 1]);
101/// let mut full = [0.0; 4];
102/// sel.accumulate_into(&[2.0, -1.0, 0.5, 3.0], &mut full)?;
103/// assert_eq!(full, [5.0, -1.0, 0.5, 0.0]);
104/// # Ok::<(), comnum_core::Error>(())
105/// ```
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct BitSelection {
108    positions: Vec<usize>,
109    full_len: usize,
110}
111
112impl BitSelection {
113    /// The selection of the bits `positions` of a word of `full_len` bits.
114    ///
115    /// # Errors
116    /// [`comnum_core::Error::Config`] for a position beyond the word.
117    pub fn new(positions: Vec<usize>, full_len: usize) -> Result<BitSelection> {
118        ensure(positions.iter().all(|&p| p < full_len), || {
119            format!("a selected bit lies beyond the word of {full_len} bits")
120        })?;
121        Ok(BitSelection { positions, full_len })
122    }
123
124    /// The number of transmitted bits.
125    pub fn len(&self) -> usize {
126        self.positions.len()
127    }
128
129    /// Whether nothing is transmitted.
130    pub fn is_empty(&self) -> bool {
131        self.positions.is_empty()
132    }
133
134    /// The length of the word.
135    pub fn full_len(&self) -> usize {
136        self.full_len
137    }
138
139    /// The bit of the word carried by every transmitted bit.
140    pub fn positions(&self) -> &[usize] {
141        &self.positions
142    }
143
144    /// The transmitted bits of a word.
145    ///
146    /// # Errors
147    /// [`comnum_core::Error::Shape`] if `word` does not have [`BitSelection::full_len`] values.
148    pub fn select<T: Copy>(&self, word: &[T]) -> Result<Vec<T>> {
149        expect_len(word.len(), self.full_len, "bits of the word")?;
150        Ok(self.positions.iter().map(|&p| word[p]).collect())
151    }
152
153    /// The LLRs of the bits of the word from the LLRs of the transmitted bits into
154    /// `out`: the sum over the copies of every bit, zero for a bit never sent.
155    ///
156    /// # Errors
157    /// [`comnum_core::Error::Shape`] if `llr` does not have [`BitSelection::len`] values or
158    /// `out` [`BitSelection::full_len`].
159    pub fn accumulate_into(&self, llr: &[f64], out: &mut [f64]) -> Result<()> {
160        expect_len(llr.len(), self.positions.len(), "LLRs")?;
161        expect_len(out.len(), self.full_len, "LLRs of the word")?;
162        out.fill(0.0);
163        for (&p, &l) in self.positions.iter().zip(llr) {
164            out[p] += l;
165        }
166        Ok(())
167    }
168
169    /// [`BitSelection::accumulate_into`] into a new vector.
170    ///
171    /// # Errors
172    /// As [`BitSelection::accumulate_into`].
173    pub fn word_llr(&self, llr: &[f64]) -> Result<Vec<f64>> {
174        let mut out = vec![0.0; self.full_len];
175        self.accumulate_into(llr, &mut out)?;
176        Ok(out)
177    }
178}
179
180/// A periodic puncturing pattern over the $n$ outputs of a code (see the module).
181#[derive(Clone, Debug, PartialEq, Eq)]
182pub struct Puncturer {
183    n_out: usize,
184    period: usize,
185    /// `keep[p * n_out + i]`: output `i` of the step of phase `p` is transmitted.
186    keep: Vec<bool>,
187}
188
189impl Puncturer {
190    /// The puncturer of a pattern given by rows: `pattern[i][p]` is 1 when the output
191    /// `i` is kept at the steps of phase `p`.
192    ///
193    /// # Errors
194    /// [`comnum_core::Error::Config`] for no row, rows of different or zero length, an entry
195    /// other than 0 or 1, or a pattern that keeps nothing.
196    pub fn new(pattern: &[&[u8]]) -> Result<Puncturer> {
197        let n_out = pattern.len();
198        ensure(n_out >= 1, || "puncturing: the pattern needs at least one row".into())?;
199        let period = pattern[0].len();
200        ensure(period >= 1, || "puncturing: the pattern needs at least one column".into())?;
201        ensure(pattern.iter().all(|r| r.len() == period), || "puncturing: the rows must have the same length".into())?;
202        ensure(pattern.iter().flat_map(|r| r.iter()).all(|&b| b <= 1), || {
203            "puncturing: the pattern entries must be 0 or 1".into()
204        })?;
205        let keep: Vec<bool> = (0..period).flat_map(|p| pattern.iter().map(move |r| r[p] == 1)).collect();
206        ensure(keep.iter().any(|&k| k), || "puncturing: the pattern keeps no bit".into())?;
207        Ok(Puncturer { n_out, period, keep })
208    }
209
210    /// The pattern that keeps every bit of a rate-$1/n$ code.
211    ///
212    /// # Errors
213    /// [`comnum_core::Error::Config`] for $n = 0$.
214    pub fn identity(n_out: usize) -> Result<Puncturer> {
215        let row = [1u8];
216        let rows: Vec<&[u8]> = vec![&row; n_out];
217        Puncturer::new(&rows)
218    }
219
220    fn of(pattern: &[&[u8]]) -> Puncturer {
221        Puncturer::new(pattern).unwrap_or_else(|_| unreachable!("a valid pattern"))
222    }
223
224    /// Rate 2/3 of IEEE 802.11-2020, Sec. 17.3.5.6, Figure 17-9: A 1 1, B 1 0.
225    pub fn ieee80211_2_3() -> Puncturer {
226        Puncturer::of(&[&[1, 1], &[1, 0]])
227    }
228
229    /// Rate 3/4 of IEEE 802.11-2020, Sec. 17.3.5.6, Figure 17-9: A 1 1 0, B 1 0 1.
230    pub fn ieee80211_3_4() -> Puncturer {
231        Puncturer::of(&[&[1, 1, 0], &[1, 0, 1]])
232    }
233
234    /// Rate 5/6 of IEEE 802.11-2020, Sec. 19.3.11.6, Figure 19-11: A 1 1 0 1 0,
235    /// B 1 0 1 0 1.
236    pub fn ieee80211_5_6() -> Puncturer {
237        Puncturer::of(&[&[1, 1, 0, 1, 0], &[1, 0, 1, 0, 1]])
238    }
239
240    /// Rate 2/3 of ETSI EN 300 421 V1.1.2, Table 2: X 1 0, Y 1 1 (rows Y, X: see the module),
241    /// the pattern of [`Puncturer::ieee80211_2_3`] (an alias, under the name of the
242    /// standard).
243    pub fn dvbs_2_3() -> Puncturer {
244        Puncturer::ieee80211_2_3()
245    }
246
247    /// Rate 3/4 of ETSI EN 300 421 V1.1.2, Table 2: X 1 0 1, Y 1 1 0 (rows Y, X), the pattern
248    /// of [`Puncturer::ieee80211_3_4`] (an alias).
249    pub fn dvbs_3_4() -> Puncturer {
250        Puncturer::ieee80211_3_4()
251    }
252
253    /// Rate 5/6 of ETSI EN 300 421 V1.1.2, Table 2: X 1 0 1 0 1, Y 1 1 0 1 0 (rows Y, X), the
254    /// pattern of [`Puncturer::ieee80211_5_6`] (an alias).
255    pub fn dvbs_5_6() -> Puncturer {
256        Puncturer::ieee80211_5_6()
257    }
258
259    /// Rate 7/8 of ETSI EN 300 421 V1.1.2, Table 2: X 1 0 0 0 1 0 1, Y 1 1 1 1 0 1 0 (rows Y, X).
260    pub fn dvbs_7_8() -> Puncturer {
261        Puncturer::of(&[&[1, 1, 1, 1, 0, 1, 0], &[1, 0, 0, 0, 1, 0, 1]])
262    }
263
264    /// The rate-1/2 turbo code of Berrou, Glavieux & Thitimajshima (1993), Sec. II: the
265    /// systematic bits and the parity bits of the two encoders alternately
266    /// (systematic 1 1, parity 1: 1 0, parity 2: 0 1), for [`PuncturedTurbo`].
267    pub fn turbo_1_2() -> Puncturer {
268        Puncturer::of(&[&[1, 1], &[1, 0], &[0, 1]])
269    }
270
271    /// The pattern of a name: `ieee80211_2_3`, `ieee80211_3_4`, `ieee80211_5_6`,
272    /// `dvbs_2_3`, `dvbs_3_4`, `dvbs_5_6`, `dvbs_7_8` (the convolutional codes) or
273    /// `turbo_1_2` (the turbo codes), case-insensitive: the constructors of the same
274    /// names.
275    ///
276    /// # Errors
277    /// [`comnum_core::Error::Config`] for another name.
278    ///
279    /// # Examples
280    /// ```
281    /// use comnum_fec::puncture::Puncturer;
282    /// assert_eq!(Puncturer::from_name("dvbs_7_8")?, Puncturer::dvbs_7_8());
283    /// assert!(Puncturer::from_name("dvbs_9_10").is_err());
284    /// # Ok::<(), comnum_core::Error>(())
285    /// ```
286    pub fn from_name(name: &str) -> Result<Puncturer> {
287        Ok(match name.to_ascii_lowercase().as_str() {
288            "ieee80211_2_3" => Puncturer::ieee80211_2_3(),
289            "ieee80211_3_4" => Puncturer::ieee80211_3_4(),
290            "ieee80211_5_6" => Puncturer::ieee80211_5_6(),
291            "dvbs_2_3" => Puncturer::dvbs_2_3(),
292            "dvbs_3_4" => Puncturer::dvbs_3_4(),
293            "dvbs_5_6" => Puncturer::dvbs_5_6(),
294            "dvbs_7_8" => Puncturer::dvbs_7_8(),
295            "turbo_1_2" => Puncturer::turbo_1_2(),
296            other => {
297                return Err(Error::Config(format!(
298                    "unknown puncturing pattern {other:?} (ieee80211_2_3, ieee80211_3_4, ieee80211_5_6, \
299                     dvbs_2_3, dvbs_3_4, dvbs_5_6, dvbs_7_8, turbo_1_2)"
300                )));
301            }
302        })
303    }
304
305    /// Outputs per step $n$ (the rows of the pattern).
306    pub fn n_out(&self) -> usize {
307        self.n_out
308    }
309
310    /// The period $P$ (the columns of the pattern).
311    pub fn period(&self) -> usize {
312        self.period
313    }
314
315    /// The bits kept per period $w$.
316    pub fn kept_per_period(&self) -> usize {
317        self.keep.iter().filter(|&&k| k).count()
318    }
319
320    /// Whether output `i` of step `step` is transmitted.
321    pub fn keeps(&self, step: usize, i: usize) -> bool {
322        self.keep[(step % self.period) * self.n_out + i]
323    }
324
325    /// The number of transmitted bits of `n_coded` coded bits.
326    ///
327    /// # Errors
328    /// [`comnum_core::Error::Shape`] if `n_coded` is not a multiple of $n$.
329    pub fn punctured_len(&self, n_coded: usize) -> Result<usize> {
330        self.check_coded(n_coded)?;
331        Ok((0..n_coded).filter(|&j| self.keep[j % (self.period * self.n_out)]).count())
332    }
333
334    /// The transmitted bits of `n_coded` coded bits in step-major order, as a
335    /// [`BitSelection`].
336    ///
337    /// # Errors
338    /// [`comnum_core::Error::Shape`] if `n_coded` is not a multiple of $n$.
339    pub fn selection(&self, n_coded: usize) -> Result<BitSelection> {
340        self.check_coded(n_coded)?;
341        let cycle = self.period * self.n_out;
342        Ok(BitSelection { positions: (0..n_coded).filter(|&j| self.keep[j % cycle]).collect(), full_len: n_coded })
343    }
344
345    fn check_coded(&self, n_coded: usize) -> Result<()> {
346        if n_coded % self.n_out == 0 {
347            Ok(())
348        } else {
349            Err(Error::Shape(format!("{n_coded} coded bits is not a multiple of the {} outputs", self.n_out)))
350        }
351    }
352
353    /// The transmitted bits of coded bits in step-major order (the $n$ outputs of step
354    /// 0, then of step 1, …).
355    ///
356    /// # Errors
357    /// [`comnum_core::Error::Shape`] if the number of bits is not a multiple of $n$.
358    pub fn puncture(&self, bits: &[u8]) -> Result<Vec<u8>> {
359        self.selection(bits.len())?.select(bits)
360    }
361
362    /// The LLRs of the `n_coded` coded bits from the LLRs of the transmitted ones: a
363    /// zero at every punctured position.
364    ///
365    /// # Errors
366    /// [`comnum_core::Error::Shape`] if `n_coded` is not a multiple of $n$ or `llr` does
367    /// not have [`Puncturer::punctured_len`] values.
368    pub fn depuncture(&self, llr: &[f64], n_coded: usize) -> Result<Vec<f64>> {
369        self.selection(n_coded)?.word_llr(llr)
370    }
371}
372
373/// A block code whose coded bits (in step-major order) are punctured: the
374/// [`ConvolutionalCode`](crate::conv::ConvolutionalCode).
375#[derive(Clone, Debug, PartialEq, Eq)]
376pub struct Punctured<C> {
377    /// The mother code.
378    code: C,
379    /// The pattern.
380    puncturer: Puncturer,
381    /// The transmitted coded bits of a block.
382    selection: BitSelection,
383}
384
385impl<C: BlockCode> Punctured<C> {
386    /// The code `code` punctured by `puncturer`.
387    ///
388    /// # Errors
389    /// [`comnum_core::Error::Shape`] if the coded length of the mother code is not a
390    /// multiple of the outputs of the pattern.
391    pub fn new(code: C, puncturer: Puncturer) -> Result<Punctured<C>> {
392        let selection = puncturer.selection(code.n())?;
393        Ok(Punctured { code, puncturer, selection })
394    }
395
396    /// The mother code (its settings are those it had when punctured).
397    pub fn code(&self) -> &C {
398        &self.code
399    }
400
401    /// The pattern.
402    pub fn puncturer(&self) -> &Puncturer {
403        &self.puncturer
404    }
405
406    /// The transmitted coded bits of a block.
407    pub fn selection(&self) -> &BitSelection {
408        &self.selection
409    }
410}
411
412impl<C: BlockCode> BlockCode for Punctured<C> {
413    fn k(&self) -> usize {
414        self.code.k()
415    }
416
417    fn n(&self) -> usize {
418        self.selection.len()
419    }
420
421    fn encode_block(&self, info: &[u8]) -> Result<Vec<u8>> {
422        self.selection.select(&self.code.encode_block(info)?)
423    }
424
425    fn decode_block(&self, llr: &[f64]) -> Result<Vec<u8>> {
426        self.code.decode_block(&self.selection.word_llr(llr)?)
427    }
428}
429
430impl<C: SoftDecode> SoftDecode for Punctured<C> {
431    /// The soft output of the mother code, a zero LLR at every punctured position.
432    fn app_into(&self, llr: &[f64], apriori: Option<&[f64]>, out: &mut [f64]) -> Result<usize> {
433        self.code.app_into(&self.selection.word_llr(llr)?, apriori, out)
434    }
435}
436
437/// A turbo code whose systematic and parity streams are punctured by a pattern of three
438/// rows (systematic, parity 1, parity 2) applied at every information index; the tail
439/// bits are kept.
440#[derive(Clone, Debug, PartialEq)]
441pub struct PuncturedTurbo {
442    /// The mother code.
443    code: TurboCode,
444    /// The pattern (three rows).
445    puncturer: Puncturer,
446    /// The transmitted bits of the codeword of the mother code (systematic, parity 1,
447    /// parity 2, tail).
448    selection: BitSelection,
449}
450
451impl PuncturedTurbo {
452    /// The turbo code `code` punctured by the three-row pattern `puncturer`.
453    ///
454    /// # Errors
455    /// [`comnum_core::Error::Config`] if the pattern does not have three rows.
456    pub fn new(code: TurboCode, puncturer: Puncturer) -> Result<PuncturedTurbo> {
457        ensure(puncturer.n_out() == 3, || {
458            format!("puncturing a turbo code needs a pattern of 3 rows, got {}", puncturer.n_out())
459        })?;
460        let k = code.k();
461        // the pattern runs over the steps (systematic, parity 1, parity 2) of every
462        // index: step-major bit j is bit (j mod 3) k + j / 3 of the three streams
463        let mut positions: Vec<usize> =
464            puncturer.selection(3 * k)?.positions().iter().map(|&j| (j % 3) * k + j / 3).collect();
465        positions.extend(3 * k..code.n());
466        let selection = BitSelection { positions, full_len: code.n() };
467        Ok(PuncturedTurbo { code, puncturer, selection })
468    }
469
470    /// The mother code.
471    pub fn code(&self) -> &TurboCode {
472        &self.code
473    }
474
475    /// The pattern.
476    pub fn puncturer(&self) -> &Puncturer {
477        &self.puncturer
478    }
479
480    /// The settings of the decoder of the mother code, which can change without
481    /// changing the code.
482    pub fn decoder_mut(&mut self) -> &mut TurboDecoder {
483        &mut self.code.decoder
484    }
485
486    /// The transmitted bits of the codeword of the mother code.
487    pub fn selection(&self) -> &BitSelection {
488        &self.selection
489    }
490}
491
492impl BlockCode for PuncturedTurbo {
493    fn k(&self) -> usize {
494        self.code.k()
495    }
496
497    fn n(&self) -> usize {
498        self.selection.len()
499    }
500
501    fn encode_block(&self, info: &[u8]) -> Result<Vec<u8>> {
502        self.selection.select(&self.code.encode_block(info)?)
503    }
504
505    fn decode_block(&self, llr: &[f64]) -> Result<Vec<u8>> {
506        self.code.decode_block(&self.selection.word_llr(llr)?)
507    }
508}
509
510impl SoftDecode for PuncturedTurbo {
511    /// The soft output of the turbo code, a zero LLR at every punctured position.
512    fn app_into(&self, llr: &[f64], apriori: Option<&[f64]>, out: &mut [f64]) -> Result<usize> {
513        self.code.app_into(&self.selection.word_llr(llr)?, apriori, out)
514    }
515}
516
517container_traits!(Punctured<C>, C: BlockCode);
518container_traits!(PuncturedTurbo);
519
520/// The free distance of the code of `trellis` punctured by `puncturer`: the smallest
521/// weight of a transmitted path that leaves the zero state and returns to it, over
522/// every phase of the pattern. A shortest path over the $2^{K-1} P$ (state, phase)
523/// nodes of the time-varying trellis (Bellman–Ford, the weights being the numbers of
524/// kept ones of the branches).
525///
526/// # Errors
527/// [`comnum_core::Error::Shape`] if the pattern does not have the $n$ rows of the trellis.
528pub fn free_distance(trellis: &Trellis, puncturer: &Puncturer) -> Result<u32> {
529    let n = trellis.n_out;
530    ensure_shape(puncturer.n_out() == n, || {
531        format!("the pattern has {} rows for a code of {n} outputs", puncturer.n_out())
532    })?;
533    let (ns, p) = (trellis.n_states(), puncturer.period());
534    let weight = |o: u32, phase: usize| {
535        (0..n).filter(|&i| puncturer.keeps(phase, i) && (o >> (n - 1 - i)) & 1 == 1).count() as u32
536    };
537    let mut best = u32::MAX;
538    for start in 0..p {
539        // the diverging branch: input 1 from state 0 at phase `start`
540        let mut dist = vec![u32::MAX; ns * p];
541        let s1 = trellis.next[0][1];
542        let w1 = weight(trellis.out[0][1], start);
543        let ph1 = (start + 1) % p;
544        dist[s1 * p + ph1] = dist[s1 * p + ph1].min(w1);
545        if s1 == 0 {
546            best = best.min(w1);
547        }
548        // relax until stable (every shortest path is simple: at most ns·p steps)
549        for _ in 0..ns * p {
550            let mut changed = false;
551            for s in 0..ns {
552                for ph in 0..p {
553                    let d = dist[s * p + ph];
554                    if d == u32::MAX || s == 0 {
555                        continue;
556                    }
557                    for u in 0..2 {
558                        let s2 = trellis.next[s][u];
559                        let d2 = d + weight(trellis.out[s][u], ph);
560                        let ph2 = (ph + 1) % p;
561                        if s2 == 0 {
562                            best = best.min(d2);
563                        } else if d2 < dist[s2 * p + ph2] {
564                            dist[s2 * p + ph2] = d2;
565                            changed = true;
566                        }
567                    }
568                }
569            }
570            if !changed {
571                break;
572            }
573        }
574    }
575    Ok(best)
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use crate::block::testing::awgn_llr;
582    use crate::bounds::bpsk_noise_variance;
583    use crate::conv::ConvolutionalCode;
584    use comnum_core::rng::Rng;
585
586    #[test]
587    fn puncture_and_depuncture_restore_the_positions() {
588        let p = Puncturer::ieee80211_3_4();
589        assert_eq!((p.n_out(), p.period(), p.kept_per_period()), (2, 3, 4));
590        let bits: Vec<u8> = (0..12).map(|i| (i % 3 == 0) as u8).collect();
591        // steps (A, B): kept A0 B0 A1 B2 A3 B3 A4 B5
592        let punctured = p.puncture(&bits).unwrap();
593        assert_eq!(punctured, [bits[0], bits[1], bits[2], bits[5], bits[6], bits[7], bits[8], bits[11]]);
594        let llr: Vec<f64> = punctured
595            .iter()
596            .enumerate()
597            .map(|(i, &b)| if b == 1 { -(i as f64 + 1.0) } else { i as f64 + 1.0 })
598            .collect();
599        let full = p.depuncture(&llr, 12).unwrap();
600        assert_eq!(full.len(), 12);
601        for (j, v) in full.iter().enumerate() {
602            if p.keeps(j / 2, j % 2) {
603                assert_eq!(u8::from(*v < 0.0), bits[j]);
604            } else {
605                assert_eq!(*v, 0.0, "a punctured position carries a zero LLR");
606            }
607        }
608        assert!(p.puncture(&bits[..5]).is_err());
609        assert!(p.depuncture(&llr[..7], 12).is_err());
610        assert!(Puncturer::new(&[&[1, 1], &[1]]).is_err());
611        assert!(Puncturer::new(&[&[0, 0], &[0, 0]]).is_err());
612        assert!(Puncturer::new(&[&[2]]).is_err());
613        assert!(Puncturer::new(&[]).is_err());
614        // a partial period at the end
615        assert_eq!(p.punctured_len(8).unwrap(), 6);
616    }
617
618    /// The streams of a turbo codeword in step-major order, (systematic, parity 1,
619    /// parity 2) per index, as `PuncturedTurbo` wrote them before its selection.
620    fn former_streams_to_steps<T: Copy>(k: usize, v: &[T]) -> Vec<T> {
621        (0..k).flat_map(|i| [v[i], v[k + i], v[2 * k + i]]).collect()
622    }
623
624    /// The depuncturing written position by position before the selections.
625    fn former_depuncture(p: &Puncturer, llr: &[f64], n_coded: usize) -> Vec<f64> {
626        let cycle = p.period * p.n_out;
627        let mut src = llr.iter();
628        (0..n_coded).map(|j| if p.keep[j % cycle] { src.next().copied().unwrap_or(0.0) } else { 0.0 }).collect()
629    }
630
631    #[test]
632    fn the_selections_give_the_former_punctured_words_and_llrs() {
633        let mut rng = Rng::new(12);
634        for p in [Puncturer::ieee80211_3_4(), Puncturer::dvbs_7_8(), Puncturer::turbo_1_2()] {
635            for n_coded in [0, p.n_out(), 7 * p.n_out(), 120] {
636                if n_coded % p.n_out() != 0 {
637                    continue;
638                }
639                let bits: Vec<u8> = (0..n_coded).map(|_| rng.bit()).collect();
640                let cycle = p.period * p.n_out;
641                let former: Vec<u8> =
642                    bits.iter().enumerate().filter(|(j, _)| p.keep[j % cycle]).map(|(_, &b)| b).collect();
643                assert_eq!(p.puncture(&bits).unwrap(), former);
644                let llr: Vec<f64> = (0..former.len()).map(|_| rng.gaussian()).collect();
645                let (new, old) = (p.depuncture(&llr, n_coded).unwrap(), former_depuncture(&p, &llr, n_coded));
646                assert!(new.len() == old.len() && new.iter().zip(&old).all(|(a, b)| a.to_bits() == b.to_bits()));
647            }
648        }
649        // the punctured turbo code: the former decoder input, the streams of the
650        // depunctured body and the tail
651        let turbo = PuncturedTurbo::new(TurboCode::lte(40).unwrap(), Puncturer::turbo_1_2()).unwrap();
652        let llr: Vec<f64> = (0..turbo.n()).map(|_| rng.gaussian()).collect();
653        let body = former_depuncture(&Puncturer::turbo_1_2(), &llr[..80], 120);
654        let mut former: Vec<f64> =
655            (0..3).flat_map(|s| (0..40).map(move |i| (s, i))).map(|(s, i)| body[3 * i + s]).collect();
656        former.extend_from_slice(&llr[80..]);
657        let new = turbo.selection().word_llr(&llr).unwrap();
658        assert!(new.iter().zip(&former).all(|(a, b)| a.to_bits() == b.to_bits()));
659        assert!(BitSelection::new(vec![0, 4], 4).is_err());
660    }
661
662    #[test]
663    fn the_soft_output_passes_through_the_puncturing() {
664        let mut rng = Rng::new(13);
665        let code = Punctured::new(ConvolutionalCode::k7(50).unwrap(), Puncturer::ieee80211_2_3()).unwrap();
666        let llr: Vec<f64> = (0..code.n()).map(|_| rng.gaussian()).collect();
667        let app = code.app(&llr, None).unwrap();
668        let mother = code.code().app(&code.puncturer().depuncture(&llr, code.code().n()).unwrap(), None).unwrap();
669        assert!(app.iter().zip(&mother).all(|(a, b)| a.to_bits() == b.to_bits()));
670        let mut turbo = PuncturedTurbo::new(TurboCode::lte(40).unwrap(), Puncturer::turbo_1_2()).unwrap();
671        turbo.decoder_mut().iterations = 3;
672        let llr: Vec<f64> = (0..turbo.n()).map(|_| rng.gaussian()).collect();
673        assert_eq!(turbo.code().decoder.iterations, 3);
674        assert_eq!(
675            comnum_core::signal::hard_decisions(&turbo.app(&llr, None).unwrap()),
676            turbo.decode_block(&llr).unwrap()
677        );
678    }
679
680    #[test]
681    fn punctured_codes_decode_without_error_at_high_snr() {
682        let mut rng = Rng::new(7);
683        for puncturer in [
684            Puncturer::ieee80211_2_3(),
685            Puncturer::ieee80211_3_4(),
686            Puncturer::ieee80211_5_6(),
687            Puncturer::dvbs_2_3(),
688            Puncturer::dvbs_3_4(),
689            Puncturer::dvbs_5_6(),
690            Puncturer::dvbs_7_8(),
691        ] {
692            let code = Punctured::new(ConvolutionalCode::k7(300).unwrap(), puncturer).unwrap();
693            let info: Vec<u8> = (0..300).map(|_| rng.bit()).collect();
694            let c = code.encode_block(&info).unwrap();
695            assert_eq!(c.len(), code.n());
696            // Eb/N0 = 10 dB
697            let y = awgn_llr(&c, bpsk_noise_variance(10.0, code.code_rate()), &mut rng);
698            assert_eq!(code.decode_block(&y).unwrap(), info, "{:?}", code.puncturer());
699            // the hard-decision decoder also treats the dummy bits as zero metrics
700            let mut hard = code.code().clone();
701            hard.hard_decision = true;
702            let hard_code = Punctured::new(hard, code.puncturer().clone()).unwrap();
703            assert_eq!(hard_code.decode_block(&y).unwrap(), info);
704        }
705        let turbo = PuncturedTurbo::new(TurboCode::lte(40).unwrap(), Puncturer::turbo_1_2()).unwrap();
706        assert_eq!(turbo.n(), 2 * 40 + 12);
707        assert!((turbo.code_rate() - 40.0 / 92.0).abs() < 1e-12);
708        let info: Vec<u8> = (0..40).map(|_| rng.bit()).collect();
709        let c = turbo.encode_block(&info).unwrap();
710        let mother = turbo.code().encode_block(&info).unwrap();
711        assert_eq!(
712            &c[..80],
713            &former_streams_to_steps(40, &mother[..120])
714                .iter()
715                .enumerate()
716                .filter(|(j, _)| Puncturer::turbo_1_2().keep[j % 6])
717                .map(|(_, &b)| b)
718                .collect::<Vec<u8>>()[..]
719        );
720        assert_eq!(c[80..], mother[120..]);
721        let y = awgn_llr(&c, bpsk_noise_variance(6.0, turbo.code_rate()), &mut rng);
722        assert_eq!(turbo.decode_block(&y).unwrap(), info);
723        assert!(PuncturedTurbo::new(TurboCode::lte(40).unwrap(), Puncturer::ieee80211_2_3()).is_err());
724    }
725
726    #[test]
727    fn the_rate_3_4_code_lies_between_rate_1_2_and_uncoded_bpsk() {
728        // Eb/N0 = 4 dB: uncoded BPSK has P_b = Q(√(2·2.51)) = 0.0125; soft-decision Viterbi
729        // decoding of the K = 7 rate-1/2 code is below 1e-4 and of the rate-3/4 code
730        // about 5e-4
731        let ebn0 = 4.0;
732        let (k, blocks) = (2000, 25);
733        let half = ConvolutionalCode::k7(k).unwrap();
734        let three_quarters = Punctured::new(ConvolutionalCode::k7(k).unwrap(), Puncturer::ieee80211_3_4()).unwrap();
735        let mut rng = Rng::new(34);
736        let (mut e_half, mut e_34, mut e_uncoded) = (0usize, 0usize, 0usize);
737        for _ in 0..blocks {
738            let info: Vec<u8> = (0..k).map(|_| rng.bit()).collect();
739            let errors = |decoded: &[u8]| decoded.iter().zip(&info).filter(|(a, b)| a != b).count();
740            let c = half.encode_block(&info).unwrap();
741            let y = awgn_llr(&c, bpsk_noise_variance(ebn0, half.code_rate()), &mut rng);
742            e_half += errors(&half.decode_block(&y).unwrap());
743            let c = three_quarters.encode_block(&info).unwrap();
744            let y = awgn_llr(&c, bpsk_noise_variance(ebn0, three_quarters.code_rate()), &mut rng);
745            e_34 += errors(&three_quarters.decode_block(&y).unwrap());
746            let y = awgn_llr(&info, bpsk_noise_variance(ebn0, 1.0), &mut rng);
747            e_uncoded += errors(&comnum_core::signal::hard_decisions(&y));
748        }
749        // 50 000 bits: about 625 uncoded errors, 25 at rate 3/4 and a few at rate 1/2
750        // (this seed: 644, 24 and 0); the counts are Poisson-like with standard
751        // deviations of 25, 5 and 2, so the factors of two below hold with margins of
752        // many standard deviations
753        assert!(e_half < e_34 && e_34 < e_uncoded, "rate 1/2: {e_half}, rate 3/4: {e_34}, uncoded: {e_uncoded}");
754        assert!(
755            e_34 * 2 < e_uncoded && e_half * 2 < e_34,
756            "rate 1/2: {e_half}, rate 3/4: {e_34}, uncoded: {e_uncoded}"
757        );
758    }
759}
760
761#[cfg(test)]
762mod literature {
763    use super::*;
764
765    /// Y. Yasuda, K. Kashiki and Y. Hirata, "High-rate punctured convolutional codes for
766    /// soft decision Viterbi decoding," IEEE Trans. Commun. 32(3), 1984, Table I: the
767    /// free distances of the codes punctured from the K = 7 (133, 171) code are 6 at
768    /// rate 2/3, 5 at 3/4, 4 at 5/6 and 3 at 7/8 (the mother code: 10). The patterns of
769    /// DVB-S (ETSI EN 300 421 V1.1.2, Table 2) and of 802.11 are those of the paper; the swapped
770    /// rows at rate 2/3 (keeping both bits of the 171 output instead of the 133 one)
771    /// give 5, not 6, which checks the output order of the constructors.
772    #[test]
773    fn yasuda_1984_table_i_free_distances_of_the_k7_punctured_codes() {
774        let t = Trellis::feedforward(7, &[0o133, 0o171]).unwrap();
775        assert_eq!(free_distance(&t, &Puncturer::identity(2).unwrap()).unwrap(), 10);
776        assert_eq!(free_distance(&t, &Puncturer::dvbs_2_3()).unwrap(), 6);
777        assert_eq!(free_distance(&t, &Puncturer::dvbs_3_4()).unwrap(), 5);
778        assert_eq!(free_distance(&t, &Puncturer::dvbs_5_6()).unwrap(), 4);
779        assert_eq!(free_distance(&t, &Puncturer::dvbs_7_8()).unwrap(), 3);
780        assert_eq!(free_distance(&t, &Puncturer::ieee80211_2_3()).unwrap(), 6);
781        assert_eq!(free_distance(&t, &Puncturer::ieee80211_3_4()).unwrap(), 5);
782        assert_eq!(free_distance(&t, &Puncturer::ieee80211_5_6()).unwrap(), 4);
783        assert!(free_distance(&t, &Puncturer::turbo_1_2()).is_err());
784        assert_eq!(free_distance(&t, &Puncturer::new(&[&[1, 0], &[1, 1]]).unwrap()).unwrap(), 5);
785    }
786
787    /// ETSI EN 300 421 V1.1.2, Table 2, read with the rows Y (from $G_{2} = 133_{8}$) then X
788    /// (from $G_{1} = 171_{8}$): the patterns of rates 2/3, 3/4 and 5/6 are those of IEEE
789    /// 802.11-2020, Figures 17-9 and 19-11, which the DVB-S constructors alias.
790    #[test]
791    fn etsi_en_300_421_table_2_is_the_80211_pattern_at_the_shared_rates() {
792        // (X, Y) as printed in Table 2
793        let table: [(&[u8], &[u8], Puncturer); 3] = [
794            (&[1, 0], &[1, 1], Puncturer::dvbs_2_3()),
795            (&[1, 0, 1], &[1, 1, 0], Puncturer::dvbs_3_4()),
796            (&[1, 0, 1, 0, 1], &[1, 1, 0, 1, 0], Puncturer::dvbs_5_6()),
797        ];
798        for (x, y, alias) in table {
799            assert_eq!(Puncturer::new(&[y, x]).unwrap(), alias);
800        }
801    }
802
803    /// Yasuda, Kashiki & Hirata (1984), Tables I and II in full: the 84 deleting maps of
804    /// the codes of rates 2/3 to 13/14 punctured from the rate-1/2 codes of maximal free
805    /// distance with $\nu = 2$ to 8 (generators (5, 7) to (561, 753), the map of the first
806    /// generator first) have the free distances of Table II, and the mother codes 5, 6, 7,
807    /// 8, 10, 10 and 12.
808    #[test]
809    fn yasuda_1984_tables_i_ii_every_punctured_code() {
810        let generators =
811            [(0o5, 0o7), (0o15, 0o17), (0o23, 0o35), (0o53, 0o75), (0o133, 0o171), (0o247, 0o371), (0o561, 0o753)];
812        let mother: [u32; 7] = [5, 6, 7, 8, 10, 10, 12];
813        // (map of the first generator, map of the second, d), nu = 2 to 8
814        let table: [[(&str, &str, u32); 7]; 12] = [
815            // rate 2/3, nu = 2 to 8
816            [
817                ("10", "11", 3),
818                ("11", "10", 4),
819                ("11", "10", 4),
820                ("10", "11", 6),
821                ("11", "10", 6),
822                ("10", "11", 7),
823                ("11", "10", 7),
824            ],
825            // rate 3/4, nu = 2 to 8
826            [
827                ("101", "110", 3),
828                ("110", "101", 4),
829                ("101", "110", 3),
830                ("100", "111", 4),
831                ("110", "101", 5),
832                ("110", "101", 6),
833                ("111", "100", 6),
834            ],
835            // rate 4/5, nu = 2 to 8
836            [
837                ("1011", "1100", 2),
838                ("1011", "1100", 3),
839                ("1010", "1101", 3),
840                ("1000", "1111", 4),
841                ("1111", "1000", 4),
842                ("1010", "1101", 5),
843                ("1101", "1010", 5),
844            ],
845            // rate 5/6, nu = 2 to 8
846            [
847                ("10111", "11000", 2),
848                ("10100", "11011", 3),
849                ("10111", "11000", 3),
850                ("10000", "11111", 4),
851                ("11010", "10101", 4),
852                ("11100", "10011", 4),
853                ("10110", "11001", 5),
854            ],
855            // rate 6/7, nu = 2 to 8
856            [
857                ("101111", "110000", 2),
858                ("100011", "111100", 2),
859                ("101010", "110101", 3),
860                ("110110", "101001", 3),
861                ("111010", "100101", 3),
862                ("101001", "110110", 4),
863                ("110110", "101001", 4),
864            ],
865            // rate 7/8, nu = 2 to 8
866            [
867                ("1011111", "1100000", 2),
868                ("1000010", "1111101", 2),
869                ("1010011", "1101100", 3),
870                ("1011101", "1100010", 3),
871                ("1111010", "1000101", 3),
872                ("1010100", "1101011", 4),
873                ("1101011", "1010100", 4),
874            ],
875            // rate 8/9, nu = 2 to 8
876            [
877                ("10111111", "11000000", 2),
878                ("10000011", "11111100", 2),
879                ("10100011", "11011100", 3),
880                ("11100010", "10011101", 3),
881                ("11110100", "10001011", 3),
882                ("10110110", "11001001", 3),
883                ("11100000", "10011111", 4),
884            ],
885            // rate 9/10, nu = 2 to 8
886            [
887                ("101111111", "110000000", 2),
888                ("101000000", "110111111", 2),
889                ("111110011", "100001100", 2),
890                ("100001111", "111110000", 3),
891                ("111101110", "100010001", 3),
892                ("101100110", "110011001", 4),
893                ("111000101", "100111010", 4),
894            ],
895            // rate 10/11, nu = 2 to 8
896            [
897                ("1011111111", "1100000000", 2),
898                ("1000000011", "1111111100", 2),
899                ("1000000101", "1111111010", 2),
900                ("1001110100", "1110001011", 3),
901                ("1110110111", "1001001000", 3),
902                ("1001000011", "1110111100", 3),
903                ("1000101100", "1111010011", 4),
904            ],
905            // rate 11/12, nu = 2 to 8
906            [
907                ("10111111111", "11000000000", 2),
908                ("10000000010", "11111111101", 2),
909                ("10101101101", "11010010010", 2),
910                ("10001110100", "11110001011", 3),
911                ("11110111110", "10001000001", 3),
912                ("10110000110", "11001111001", 3),
913                ("11000010001", "10111101110", 4),
914            ],
915            // rate 12/13, nu = 2 to 8
916            [
917                ("101111111111", "110000000000", 2),
918                ("100000000011", "111111111100", 2),
919                ("101101111011", "110010000100", 2),
920                ("110100110110", "101011001001", 3),
921                ("111111110101", "100000001010", 3),
922                ("100100001100", "111011110011", 3),
923                ("110000011010", "101111100101", 4),
924            ],
925            // rate 13/14, nu = 2 to 8
926            [
927                ("1011111111111", "1100000000000", 2),
928                ("1010000000000", "1101111111111", 2),
929                ("1110110110111", "1001001001000", 2),
930                ("1100011000100", "1011100111011", 3),
931                ("1101000001111", "1010111110000", 3),
932                ("1010100100000", "1101011011111", 3),
933                ("1100000100001", "1011111011110", 4),
934            ],
935        ];
936        let bits = |s: &str| s.bytes().map(|b| b - b'0').collect::<Vec<u8>>();
937        for (j, &(g1, g2)) in generators.iter().enumerate() {
938            let t = Trellis::feedforward(j as u32 + 3, &[g1, g2]).unwrap();
939            assert_eq!(free_distance(&t, &Puncturer::identity(2).unwrap()).unwrap(), mother[j], "nu = {}", j + 2);
940            for row in &table {
941                let (a, b, d) = row[j];
942                let (a, b) = (bits(a), bits(b));
943                let p = Puncturer::new(&[&a, &b]).unwrap();
944                assert_eq!(free_distance(&t, &p).unwrap(), d, "nu = {}, rate {}/{}", j + 2, a.len(), a.len() + 1);
945            }
946        }
947    }
948}