Skip to main content

comnum_fec/nr/
ldpc.rs

1//! The LDPC code of the data channels of 5G NR (TS 38.212, Sec. 5.2.2, 5.3.2, 5.4.2).
2//!
3//! # Model
4//!
5//! A code block of $K'$ information bits is encoded by base graph 1 ($46 \times 68$,
6//! $K_{b} = 22$) or 2 ($42 \times 52$, $K_{b} \le 10$), chosen as the shared channels do
7//! (Sec. 7.2.2): base graph 2 when $K' \le 292$, or $K' \le 3824$ and $R \le 0.67$, or
8//! $R \le 0.25$, with $R = K'/E$; base graph 1 otherwise. The lifting size $Z_{c}$ is the
9//! smallest of Table 5.3.2-1 with $K_{b} Z_{c} \ge K'$ ($K_{b} = 22$ for base graph 1;
10//! for base graph 2, 10, 9, 8 or 6 for $K'$ above 640, 560, 192 or below), and every
11//! nonzero entry $V_{i,j}$ of the base graph becomes the $Z_{c} \times Z_{c}$ identity
12//! cyclically shifted by $V_{i,j} \bmod Z_{c}$ (the column of the set $i_{\mathrm{LS}}$
13//! that contains $Z_{c}$). The $K = 22 Z_{c}$ (or $10 Z_{c}$) systematic bits are the
14//! information bits followed by $K - K'$ filler bits (zeros); the encoder of
15//! [`crate::ldpc::LdpcCode::from_core_rows`] computes the $N = 66 Z_{c}$ (or
16//! $50 Z_{c}$) other bits of $d$ — the first $2 Z_{c}$ systematic bits are never
17//! transmitted.
18//!
19//! Rate matching (Sec. 5.4.2.1, without limited buffer, $N_{\mathrm{cb}} = N$): the $E$
20//! output bits are read from the circular buffer $d$ from $k_{0}$, skipping the filler
21//! bits, where $k_{0} = 0$, $\lfloor 17 N_{\mathrm{cb}} / (66 Z_{c}) \rfloor Z_{c}$,
22//! $\lfloor 33 N_{\mathrm{cb}} / (66 Z_{c}) \rfloor Z_{c}$,
23//! $\lfloor 56 N_{\mathrm{cb}} / (66 Z_{c}) \rfloor Z_{c}$ for the redundancy versions 0
24//! to 3 (13, 25, 43 over $50 Z_{c}$ for base graph 2); then the bit interleaver of
25//! Sec. 5.4.2.2 writes them row by row in $Q_{m}$ rows and reads them column by column
26//! ($Q_{m}$ the bits per modulation symbol, 1 for none).
27//!
28//! The decoder places the received LLRs on the bits they carry (summed where the
29//! circular buffer wraps), zero on the punctured and unsent bits and a large positive
30//! value on the filler bits, and runs the belief propagation of [`crate::ldpc`].
31//!
32//! # Examples
33//! ```
34//! use comnum_fec::block::BlockCode;
35//! use comnum_fec::nr::NrLdpc;
36//! let code = NrLdpc::new(100, 300)?;                       // K' = 100, E = 300
37//! assert_eq!((code.base_graph(), code.lifting_size()), (2, 18));
38//! let info: Vec<u8> = (0..100).map(|i| (i % 3 == 0) as u8).collect();
39//! let c = code.encode_block(&info)?;
40//! let llr: Vec<f64> = c.iter().map(|b| if *b == 0 { 4.0 } else { -4.0 }).collect();
41//! assert_eq!(code.decode_block(&llr)?, info);
42//! # Ok::<(), comnum_core::Error>(())
43//! ```
44//!
45//! # References
46//!
47//! 1. 3GPP TS 38.212 V18.5.0, *NR; Multiplexing and channel coding*, Sec. 5.2.2, 5.3.2,
48//!    5.4.2, 7.2.2 (ETSI TS 138 212, 2025).
49//! 2. T. Richardson, S. Kudekar, "Design of low-density parity check codes for 5G new
50//!    radio," *IEEE Commun. Mag.* 56(3), 28–34, 2018.
51
52use super::tables::{BG1, BG2, LIFTING_SETS};
53use crate::block::{BlockCode, SoftDecode, container_traits, expect_len};
54use crate::interleaving::Interleaver;
55use crate::ldpc::{LdpcCode, LdpcDecoder};
56use crate::puncture::BitSelection;
57use comnum_core::signal::KNOWN_BIT_LLR;
58use comnum_core::{Result, ensure};
59
60/// A base graph: its nonzero entries $(i, j, V_{i,j})$.
61type BaseGraph = [(u8, u8, [u16; 8])];
62
63/// The LDPC code of 5G NR for one code block (see the module).
64#[derive(Clone, Debug)]
65pub struct NrLdpc {
66    bg: u8,
67    z: usize,
68    k_info: usize,
69    e: usize,
70    /// The bit of the full codeword (all $K + N$ columns) carried by every output bit,
71    /// after the bit interleaver.
72    map: BitSelection,
73    /// The code of the lifted parity-check matrix and its decoder settings.
74    code: LdpcCode,
75}
76
77/// The base graph of 5G NR for $K'$ bits at the rate $K'/E$ (Sec. 7.2.2).
78fn base_graph(k_info: usize, e: usize) -> u8 {
79    let r = k_info as f64 / e as f64;
80    if k_info <= 292 || (k_info <= 3824 && r <= 0.67) || r <= 0.25 { 2 } else { 1 }
81}
82
83impl NrLdpc {
84    /// The code of `k_info` information bits rate-matched to `e` bits, redundancy
85    /// version 0, without bit interleaving.
86    ///
87    /// # Errors
88    /// As [`NrLdpc::with_options`].
89    pub fn new(k_info: usize, e: usize) -> Result<NrLdpc> {
90        NrLdpc::with_options(k_info, e, None, 0, 1)
91    }
92
93    /// The code of `k_info` information bits rate-matched to `e` bits, the base graph
94    /// chosen as in Sec. 7.2.2 or forced by `base_graph`, the redundancy version `rv`
95    /// and the bit interleaver of `qm` bits per symbol (1: none).
96    ///
97    /// # Errors
98    /// [`comnum_core::Error::Config`] for a block larger than one code block (8448 bits for
99    /// base graph 1, 3840 for base graph 2), `e` not a multiple of `qm`, an `rv` above
100    /// 3, or a base graph other than 1 or 2.
101    pub fn with_options(k_info: usize, e: usize, base_graph_override: Option<u8>, rv: u8, qm: usize) -> Result<NrLdpc> {
102        ensure(k_info >= 1 && e >= 1, || format!("need K' ≥ 1 and E ≥ 1, got {k_info} and {e}"))?;
103        ensure(rv <= 3 && qm >= 1 && e % qm == 0, || format!("rv ≤ 3 and Qm | E, got rv {rv}, Qm {qm}, E {e}"))?;
104        let bg = base_graph_override.unwrap_or_else(|| base_graph(k_info, e));
105        ensure(bg == 1 || bg == 2, || format!("the base graph is 1 or 2, got {bg}"))?;
106        let k_cb = if bg == 1 { 8448 } else { 3840 };
107        ensure(k_info <= k_cb, || {
108            format!("{k_info} bits exceed one code block of base graph {bg} ({k_cb}); segment the block first")
109        })?;
110        let kb = match (bg, k_info) {
111            (1, _) => 22,
112            (_, b) if b > 640 => 10,
113            (_, b) if b > 560 => 9,
114            (_, b) if b > 192 => 8,
115            _ => 6,
116        };
117        let (z, ils) = LIFTING_SETS
118            .iter()
119            .enumerate()
120            .flat_map(|(i, set)| set.iter().map(move |&z| (z, i)))
121            .filter(|&(z, _)| kb * z >= k_info)
122            .min()
123            .ok_or_else(|| comnum_core::Error::Config(format!("no lifting size for K' = {k_info}")))?;
124        let (table, cols_k, rows_n): (&BaseGraph, usize, usize) = if bg == 1 { (&BG1, 22, 46) } else { (&BG2, 10, 42) };
125        let k = cols_k * z;
126        let n_full = (cols_k + rows_n) * z;
127        let mut rows: Vec<Vec<usize>> = vec![Vec::new(); rows_n * z];
128        for &(i, j, v) in table {
129            let shift = v[ils] as usize % z;
130            for r in 0..z {
131                rows[i as usize * z + r].push(j as usize * z + (r + shift) % z);
132            }
133        }
134        let code = LdpcCode::from_core_rows(n_full, rows, k, 4 * z)?;
135        // the circular buffer d: the full codeword without its first 2Z bits
136        let ncb = n_full - 2 * z;
137        let num = match (bg, rv) {
138            (_, 0) => 0,
139            (1, 1) => 17,
140            (1, 2) => 33,
141            (1, _) => 56,
142            (_, 1) => 13,
143            (_, 2) => 25,
144            _ => 43,
145        };
146        let den = if bg == 1 { 66 } else { 50 };
147        let k0 = num * ncb / (den * z) * z;
148        let filler = |col: usize| col >= k_info && col < k;
149        let mut sel = Vec::with_capacity(e);
150        let mut j = 0;
151        while sel.len() < e {
152            let col = 2 * z + (k0 + j) % ncb;
153            if !filler(col) {
154                sel.push(col);
155            }
156            j += 1;
157        }
158        // the bit interleaver: f[i + j Qm] = e[i E/Qm + j], written row by row into Qm
159        // rows and read column by column
160        let map = BitSelection::new(Interleaver::block(qm, e / qm).interleave(&sel), code.n())?;
161        Ok(NrLdpc { bg, z, k_info, e, map, code })
162    }
163
164    /// The base graph, 1 or 2.
165    pub fn base_graph(&self) -> u8 {
166        self.bg
167    }
168
169    /// The lifting size $Z_{c}$.
170    pub fn lifting_size(&self) -> usize {
171        self.z
172    }
173
174    /// The bit of the full codeword carried by every output bit.
175    pub fn bit_map(&self) -> &[usize] {
176        self.map.positions()
177    }
178
179    /// The code of the lifted parity-check matrix.
180    pub fn code(&self) -> &LdpcCode {
181        &self.code
182    }
183
184    /// The settings of the decoder, which can change without changing the code.
185    pub fn decoder_mut(&mut self) -> &mut LdpcDecoder {
186        &mut self.code.decoder
187    }
188
189    /// Add the `e` LLRs of one transmission to the soft buffer `buffer` of the full
190    /// codeword (`code().n()` values, zeros before the first transmission): the soft
191    /// combining of the transmissions of a HARQ process (chase combining of the same
192    /// redundancy version, incremental redundancy of the others), every redundancy
193    /// version of the same $K'$, $E$ and base graph sharing the buffer.
194    ///
195    /// # Errors
196    /// [`comnum_core::Error::Shape`] unless `llr` has $E$ values and `buffer` `code().n()`.
197    pub fn combine(&self, llr: &[f64], buffer: &mut [f64]) -> Result<()> {
198        expect_len(buffer.len(), self.code.n(), "buffer LLRs")?;
199        for (b, v) in buffer.iter_mut().zip(self.map.word_llr(llr)?) {
200            *b += v;
201        }
202        Ok(())
203    }
204
205    /// Decode the soft buffer of [`NrLdpc::combine`]: the $K'$ information bits.
206    ///
207    /// # Errors
208    /// [`comnum_core::Error::Shape`] unless `buffer` has `code().n()` values.
209    pub fn decode_buffer(&self, buffer: &[f64]) -> Result<Vec<u8>> {
210        expect_len(buffer.len(), self.code.n(), "buffer LLRs")?;
211        let mut full = buffer.to_vec();
212        for v in full.iter_mut().take(self.code.k()).skip(self.k_info) {
213            *v = KNOWN_BIT_LLR;
214        }
215        let (post, _) = self.code.decode_llr(&full)?;
216        Ok(comnum_core::signal::hard_decisions(&post[..self.k_info]))
217    }
218
219    /// The LLRs of the full codeword: the received LLRs summed over the copies of every
220    /// bit, zero on the unsent bits, [`KNOWN_BIT_LLR`] on the filler bits.
221    fn full_llr(&self, llr: &[f64]) -> Result<Vec<f64>> {
222        let mut full = self.map.word_llr(llr)?;
223        for v in full.iter_mut().take(self.code.k()).skip(self.k_info) {
224            *v = KNOWN_BIT_LLR;
225        }
226        Ok(full)
227    }
228}
229
230impl BlockCode for NrLdpc {
231    fn k(&self) -> usize {
232        self.k_info
233    }
234
235    fn n(&self) -> usize {
236        self.e
237    }
238
239    fn encode_block(&self, info: &[u8]) -> Result<Vec<u8>> {
240        expect_len(info.len(), self.k_info, "bits")?;
241        let mut u = info.to_vec();
242        u.resize(self.code.k(), 0);
243        self.map.select(&self.code.encode_block(&u)?)
244    }
245
246    fn decode_block(&self, llr: &[f64]) -> Result<Vec<u8>> {
247        expect_len(llr.len(), self.e, "LLRs")?;
248        let (post, _) = self.code.decode_llr(&self.full_llr(llr)?)?;
249        Ok(comnum_core::signal::hard_decisions(&post[..self.k_info]))
250    }
251}
252
253impl SoftDecode for NrLdpc {
254    /// The a posteriori LLRs of the $K'$ information bits after the belief propagation
255    /// of the full codeword (the a priori LLRs added to theirs).
256    fn app_into(&self, llr: &[f64], apriori: Option<&[f64]>, out: &mut [f64]) -> Result<usize> {
257        expect_len(llr.len(), self.e, "LLRs")?;
258        expect_len(out.len(), self.k_info, "a posteriori LLRs")?;
259        let mut full = self.full_llr(llr)?;
260        if let Some(a) = apriori {
261            expect_len(a.len(), self.k_info, "a priori LLRs")?;
262            for (v, a) in full.iter_mut().zip(a) {
263                *v += a;
264            }
265        }
266        let (post, it) = self.code.decode_llr(&full)?;
267        out.copy_from_slice(&post[..self.k_info]);
268        Ok(it)
269    }
270}
271
272container_traits!(NrLdpc);
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn soft_combining_of_redundancy_versions_recovers_what_one_cannot() {
280        use comnum_core::rng::Rng;
281        let (k, e) = (480, 528);
282        let codes: Vec<NrLdpc> =
283            [0u8, 2, 3, 1].iter().map(|&rv| NrLdpc::with_options(k, e, Some(2), rv, 2).unwrap()).collect();
284        let mut rng = Rng::new(5);
285        let info: Vec<u8> = (0..k).map(|_| u8::from(rng.uniform() < 0.5)).collect();
286        // BPSK LLRs 2y/σ² at an SNR where one transmission of rate 0.91 fails
287        let sigma2: f64 = 1.6;
288        let mut buffer = vec![0.0; codes[0].code().n()];
289        let mut first = None;
290        for code in &codes {
291            let c = code.encode_block(&info).unwrap();
292            let llr: Vec<f64> = c
293                .iter()
294                .map(|b| {
295                    let y = if *b == 0 { 1.0 } else { -1.0 } + rng.gaussian() * sigma2.sqrt();
296                    2.0 * y / sigma2
297                })
298                .collect();
299            code.combine(&llr, &mut buffer).unwrap();
300            first.get_or_insert_with(|| code.decode_block(&llr).unwrap() == info);
301        }
302        assert_eq!(first, Some(false));
303        assert_eq!(codes[0].decode_buffer(&buffer).unwrap(), info);
304        // one transmission in the buffer decodes as decode_block
305        let c = codes[0].encode_block(&info).unwrap();
306        let llr: Vec<f64> = c.iter().map(|b| if *b == 0 { 3.0 } else { -3.0 }).collect();
307        let mut one = vec![0.0; codes[0].code().n()];
308        codes[0].combine(&llr, &mut one).unwrap();
309        assert_eq!(codes[0].decode_buffer(&one).unwrap(), codes[0].decode_block(&llr).unwrap());
310        assert!(codes[0].combine(&llr[1..], &mut one).is_err());
311    }
312
313    #[test]
314    fn the_base_graphs_have_their_dimensions() {
315        assert_eq!((BG1.len(), BG2.len()), (316, 197));
316        assert_eq!(BG1.iter().map(|e| e.0).max(), Some(45));
317        assert_eq!(BG1.iter().map(|e| e.1).max(), Some(67));
318        assert_eq!(BG2.iter().map(|e| e.0).max(), Some(41));
319        assert_eq!(BG2.iter().map(|e| e.1).max(), Some(51));
320        // the extension: row i ≥ 4 closes on column K_b + i
321        for (t, kb) in [(&BG1[..], 22u8), (&BG2[..], 10)] {
322            for i in 4..=t.iter().map(|e| e.0).max().unwrap() {
323                let last = t.iter().filter(|e| e.0 == i).map(|e| e.1).max().unwrap();
324                assert_eq!(last, kb + i);
325            }
326        }
327    }
328
329    #[test]
330    fn every_lifting_size_encodes_codewords() {
331        for set in LIFTING_SETS {
332            for &z in set.iter().filter(|&&z| z <= 64) {
333                for bg in [1u8, 2] {
334                    let kb = if bg == 1 { 22 } else { 10 };
335                    let k_info = kb * z - 3;
336                    let code = NrLdpc::with_options(k_info, 3 * k_info, Some(bg), 0, 1).unwrap();
337                    assert_eq!(code.lifting_size(), z.max(code.lifting_size()));
338                    let info: Vec<u8> = (0..k_info).map(|i| ((i * 7 + z) % 5 < 2) as u8).collect();
339                    let mut u = info.clone();
340                    u.resize(code.code().k(), 0);
341                    assert!(code.code().is_codeword(&code.code().encode_block(&u).unwrap()));
342                    assert_eq!(code.encode_block(&info).unwrap().len(), 3 * k_info);
343                }
344            }
345        }
346        assert!(NrLdpc::new(9000, 10000).is_err());
347    }
348
349    /// The rate matching as it was written before the bit selection: the bit
350    /// interleaver by its index formula, the LLRs summed position by position.
351    #[test]
352    fn the_selection_gives_the_former_rate_matching() {
353        let mut rng = comnum_core::rng::Rng::new(21);
354        for (k_info, e, rv, qm) in [(100, 300, 0, 1), (500, 1200, 2, 4), (1000, 1500, 3, 6), (60, 1000, 1, 2)] {
355            let code = NrLdpc::with_options(k_info, e, None, rv, qm).unwrap();
356            let k = code.code.k();
357            let z = code.z;
358            let ncb = code.code.n() - 2 * z;
359            let (num, den) = match (code.bg, rv) {
360                (_, 0) => (0, 1),
361                (1, 1) => (17, 66),
362                (1, 2) => (33, 66),
363                (1, _) => (56, 66),
364                (_, 1) => (13, 50),
365                (_, 2) => (25, 50),
366                _ => (43, 50),
367            };
368            let k0 = num * ncb / (den * z) * z;
369            let mut sel = Vec::new();
370            let mut j = 0;
371            while sel.len() < e {
372                let col = 2 * z + (k0 + j) % ncb;
373                if !(col >= k_info && col < k) {
374                    sel.push(col);
375                }
376                j += 1;
377            }
378            let map: Vec<usize> = (0..e).map(|t| sel[(t % qm) * (e / qm) + t / qm]).collect();
379            assert_eq!(code.bit_map(), map);
380            let llr: Vec<f64> = (0..e).map(|_| rng.gaussian()).collect();
381            let mut full = vec![0.0; code.code.n()];
382            for (&i, &l) in map.iter().zip(&llr) {
383                full[i] += l;
384            }
385            for v in full.iter_mut().take(k).skip(k_info) {
386                *v = 30.0;
387            }
388            let new = code.full_llr(&llr).unwrap();
389            assert!(new.iter().zip(&full).all(|(a, b)| a.to_bits() == b.to_bits()));
390            let (post, _) = code.code.decode_llr(&full).unwrap();
391            let app = code.app(&llr, None).unwrap();
392            assert!(app.iter().zip(&post[..k_info]).all(|(a, b)| a.to_bits() == b.to_bits()));
393            assert_eq!(code.decode_block(&llr).unwrap(), comnum_core::signal::hard_decisions(&post[..k_info]));
394        }
395    }
396}