Skip to main content

comnum_fec/nr/
polar.rs

1//! The polar code of the control and broadcast channels of 5G NR (TS 38.212, Sec.
2//! 5.3.1, 5.4.1, 6.3.1, 7.1, 7.3).
3//!
4//! # Model
5//!
6//! $A$ payload bits receive a CRC ($L$ bits) and become $K = A + L$ bits, interleaved by
7//! the pattern of Table 5.3.1.1-1 on the downlink ($I_{\mathrm{IL}} = 1$). The mother
8//! code length is $N = 2^{n}$ with $n = \max(\min(n_{1}, n_{2}, n_{\max}), 5)$,
9//! $n_{1} = \lceil \log_{2} E \rceil - 1$ when $E \le \frac{9}{8} 2^{\lceil \log_{2} E
10//! \rceil - 1}$ and $K/E < 9/16$ (else $\lceil \log_{2} E \rceil$), $n_{2} = \lceil
11//! \log_{2}(8K) \rceil$ (Sec. 5.3.1). The sub-block interleaver of Sec. 5.4.1.1 cuts the
12//! codeword into 32 blocks permuted by Table 5.4.1.1-1, $J(n) = P(\lfloor 32 n / N
13//! \rfloor) N/32 + n \bmod N/32$; the $E$ transmitted bits are, when $E < N$, the last
14//! $E$ (puncturing, $K/E \le 7/16$) or the first $E$ (shortening) of the interleaved
15//! word, and when $E \ge N$ the word repeated. The frozen set holds the positions
16//! $J(n)$ that are not transmitted — and, with puncturing, the first $\lceil 3N/4 - E/2
17//! \rceil$ (or $\lceil 9N/16 - E/4 \rceil$) positions — and the least reliable of the
18//! others in the order of the polar sequence of Table 5.3.1.2-1; the $K + n_{\mathrm{PC}}$
19//! most reliable carry the bits and the parity-check bits ([`crate::polar::PolarCode::with_positions`]).
20//! On the uplink, the triangular interleaver of Sec. 5.4.1.3 ($I_{\mathrm{BIL}} = 1$)
21//! writes the $E$ bits row by row into rows of $T, T - 1, \dots$ cells, $T(T+1)/2 \ge E$,
22//! and reads them column by column.
23//!
24//! - [`NrPolar::uplink`]: the uplink control information (Sec. 6.3.1.2–6.3.1.4): CRC6
25//!   and 3 parity-check bits for $12 \le A \le 19$ (one of minimum row weight when
26//!   $E - K + 3 > 192$), CRC11 for $A \ge 20$, $n_{\max} = 10$, $I_{\mathrm{BIL}} = 1$;
27//!   one code block ($A < 360$ or $E < 1088$, and $A < 1013$);
28//! - [`NrPolar::downlink`]: the downlink control information and the broadcast channel
29//!   (Sec. 7.3, 7.1): CRC24C, $I_{\mathrm{IL}} = 1$, $n_{\max} = 9$. The CRC is that of
30//!   the payload: the 24 ones prepended to a DCI and its masking by an RNTI are left to
31//!   the caller.
32//!
33//! The decoder undoes the interleavers and the rate matching on the LLRs (summing the
34//! repetitions; zero on the punctured bits, a large positive value on the shortened
35//! ones, which are zeros), runs the SC list decoder of [`crate::polar`] and keeps the
36//! first path whose deinterleaved bits pass the CRC.
37//!
38//! # Examples
39//! ```
40//! use comnum_fec::block::BlockCode;
41//! use comnum_fec::nr::NrPolar;
42//! let code = NrPolar::uplink(30, 100)?.with_list(8)?;     // A = 30 bits, E = 100
43//! assert_eq!(code.mother_length(), 128);
44//! let info: Vec<u8> = (0..30).map(|i| (i % 4 == 1) as u8).collect();
45//! let c = code.encode_block(&info)?;
46//! let llr: Vec<f64> = c.iter().map(|b| if *b == 0 { 3.0 } else { -3.0 }).collect();
47//! assert_eq!(code.decode_block(&llr)?, info);
48//! # Ok::<(), comnum_core::Error>(())
49//! ```
50//!
51//! # References
52//!
53//! 1. 3GPP TS 38.212 V18.5.0, *NR; Multiplexing and channel coding*, Sec. 5.3.1, 5.4.1,
54//!    6.3.1, 7.1, 7.3 (ETSI TS 138 212, 2025).
55//! 2. V. Bioglio, C. Condo, I. Land, "Design of polar codes in 5G New Radio," *IEEE
56//!    Commun. Surveys Tuts.* 23(1), 29–40, 2021.
57
58use super::tables::{POLAR_INTERLEAVER, POLAR_SEQUENCE, SUBBLOCK_PATTERN};
59use crate::block::{BlockCode, container_traits, expect_len};
60use crate::crc::Crc;
61use crate::polar::PolarCode;
62use crate::puncture::BitSelection;
63use comnum_core::signal::KNOWN_BIT_LLR;
64use comnum_core::{Error, Result, ensure};
65
66/// The polar code of 5G NR for one code block (see the module).
67#[derive(Clone, Debug)]
68pub struct NrPolar {
69    a: usize,
70    e: usize,
71    crc: Crc,
72    /// $\Pi$: interleaved bit $k$ is bit $\Pi(k)$ of the payload and CRC (identity
73    /// without input interleaving).
74    il: Vec<usize>,
75    /// The bit of the mother codeword carried by every output bit.
76    map: BitSelection,
77    /// The mother codeword bits that shortening sets to zero.
78    shortened: Vec<usize>,
79    /// The mother code, its positions and its list size.
80    code: PolarCode,
81}
82
83/// The options of a link direction (Sec. 6.3.1 and 7.3): the parity-check bits, the
84/// interleavers and the largest mother code.
85struct Direction {
86    /// Parity-check bits $n_{\mathrm{PC}}$, and $n_{\mathrm{PC}}^{\mathrm{wm}}$ of them
87    /// of minimum row weight (0 or 1).
88    n_pc: usize,
89    n_pc_wm: usize,
90    /// The input interleaver $I_{\mathrm{IL}}$ (downlink) and the triangular interleaver
91    /// $I_{\mathrm{BIL}}$ (uplink).
92    i_il: bool,
93    i_bil: bool,
94    /// $n_{\max}$.
95    n_max: u32,
96}
97
98/// $\lceil \log_{2} x \rceil$.
99fn ceil_log2(x: usize) -> u32 {
100    x.next_power_of_two().trailing_zeros()
101}
102
103/// The mother code length $N = 2^{n}$ of Sec. 5.3.1 for $K$ bits sent on $E$.
104fn mother_length(k: usize, e: usize, n_max: u32) -> usize {
105    let ce = ceil_log2(e);
106    let n1 = if ce >= 1 && 8 * e <= 9 * (1 << (ce - 1)) && 16 * k < 9 * e { ce - 1 } else { ce };
107    let n2 = ceil_log2(8 * k);
108    1usize << n1.min(n2).min(n_max).max(5)
109}
110
111/// The rate matching of Sec. 5.4.1.1 on the sub-block interleaver `j`: whether the code
112/// punctures, the frozen positions it implies, and the positions it shortens.
113fn rate_matching_frozen(j: &[usize], k: usize, e: usize) -> (bool, Vec<bool>, Vec<usize>) {
114    let n = j.len();
115    let mut frozen = vec![false; n];
116    let puncturing = e < n && 16 * k <= 7 * e;
117    let mut shortened = Vec::new();
118    if e < n {
119        if puncturing {
120            for &p in &j[..n - e] {
121                frozen[p] = true;
122            }
123            let t = if 4 * e >= 3 * n {
124                // ceil(3N/4 - E/2)
125                (6 * n - 4 * e).div_ceil(8)
126            } else {
127                // ceil(9N/16 - E/4)
128                (9 * n - 4 * e).div_ceil(16)
129            };
130            for f in frozen.iter_mut().take(t) {
131                *f = true;
132            }
133        } else {
134            for &p in &j[e..] {
135                frozen[p] = true;
136                shortened.push(p);
137            }
138        }
139    }
140    (puncturing, frozen, shortened)
141}
142
143/// The information and parity-check positions (Sec. 5.3.1.2): the $K + n_{\mathrm{PC}}$
144/// most reliable positions not frozen, in the order of the polar sequence; the
145/// parity checks on the least reliable of them, and one of minimum row weight among the
146/// $K$ most reliable.
147fn info_and_pc(frozen: &[bool], k: usize, d: &Direction) -> Result<(Vec<usize>, Vec<usize>)> {
148    let n = frozen.len();
149    let reliable: Vec<usize> = POLAR_SEQUENCE.iter().map(|&q| q as usize).filter(|&q| q < n && !frozen[q]).collect();
150    let used: Vec<usize> = reliable[reliable.len() - (k + d.n_pc)..].to_vec();
151    let mut pc = Vec::new();
152    if d.n_pc > 0 {
153        // n_PC - n_PC^wm parity checks on the least reliable positions of Q_I
154        pc.extend_from_slice(&used[..d.n_pc - d.n_pc_wm]);
155        if d.n_pc_wm > 0 {
156            // and one of minimum row weight 2^popcount among the K most reliable,
157            // the most reliable on ties
158            let best = used[d.n_pc..]
159                .iter()
160                .enumerate()
161                .min_by_key(|&(r, &p)| (p.count_ones(), std::cmp::Reverse(r)))
162                .map(|(_, &p)| p)
163                .ok_or_else(|| comnum_core::Error::Config("no position for the parity check".into()))?;
164            pc.push(best);
165        }
166    }
167    let info: Vec<usize> = used.iter().copied().filter(|p| !pc.contains(p)).collect();
168    Ok((info, pc))
169}
170
171/// The triangular interleaver of Sec. 5.4.1.3: `bits` written row by row into rows of
172/// $T, T - 1, \dots$ cells, $T(T+1)/2 \ge E$, and read column by column.
173fn triangular<T: Copy>(bits: &[T]) -> Vec<T> {
174    let e = bits.len();
175    let mut t = 0;
176    while t * (t + 1) / 2 < e {
177        t += 1;
178    }
179    // row i holds T - i cells, written row by row
180    let mut cell = vec![vec![None; t]; t];
181    let mut idx = 0;
182    for (i, row) in cell.iter_mut().enumerate() {
183        for c in row.iter_mut().take(t - i) {
184            if idx < e {
185                *c = Some(bits[idx]);
186            }
187            idx += 1;
188        }
189    }
190    let mut out = Vec::with_capacity(e);
191    for col in 0..t {
192        for row in cell.iter().take(t - col) {
193            if let Some(v) = row[col] {
194                out.push(v);
195            }
196        }
197    }
198    out
199}
200
201impl NrPolar {
202    /// The code of a link by name: `uplink` ([`NrPolar::uplink`]) or `downlink`
203    /// ([`NrPolar::downlink`]), case-insensitive.
204    ///
205    /// # Errors
206    /// [`comnum_core::Error::Config`] for another name, or as the code of the link.
207    pub fn from_name(link: &str, a: usize, e: usize) -> Result<NrPolar> {
208        match link.to_ascii_lowercase().as_str() {
209            "uplink" => NrPolar::uplink(a, e),
210            "downlink" => NrPolar::downlink(a, e),
211            _ => Err(Error::Config(format!("the link is uplink or downlink, got {link:?}"))),
212        }
213    }
214
215    /// The code of the uplink control information: `a` bits, `e` transmitted bits.
216    ///
217    /// # Errors
218    /// [`comnum_core::Error::Config`] for $A < 12$ (the small block codes of Sec. 5.3.3),
219    /// a payload that needs two code blocks, or $E$ too small for the code.
220    pub fn uplink(a: usize, e: usize) -> Result<NrPolar> {
221        ensure(a >= 12, || format!("the uplink polar code takes A ≥ 12 bits, got {a}"))?;
222        ensure(a < 1013 && !(a >= 360 && e >= 1088), || {
223            format!("A = {a}, E = {e} needs two code blocks (Sec. 6.3.1.2.1), not supported")
224        })?;
225        let (crc, n_pc) = if a <= 19 { (Crc::crc6(), 3) } else { (Crc::crc11(), 0) };
226        let k = a + crc.width() as usize;
227        let n_pc_wm = usize::from(n_pc > 0 && e + n_pc > k + 192);
228        NrPolar::build(a, e, crc, &Direction { n_pc, n_pc_wm, i_il: false, i_bil: true, n_max: 10 })
229    }
230
231    /// The code of the downlink control information and of the broadcast channel: `a`
232    /// bits, `e` transmitted bits.
233    ///
234    /// # Errors
235    /// [`comnum_core::Error::Config`] for $A = 0$, $A + 24 > 164$ (the input interleaver
236    /// of Table 5.3.1.1-1), or $E$ too small for the code.
237    pub fn downlink(a: usize, e: usize) -> Result<NrPolar> {
238        ensure(a >= 1 && a + 24 <= 164, || format!("the downlink polar code takes 1 ≤ A ≤ 140 bits, got {a}"))?;
239        NrPolar::build(a, e, Crc::crc24c(), &Direction { n_pc: 0, n_pc_wm: 0, i_il: true, i_bil: false, n_max: 9 })
240    }
241
242    fn build(a: usize, e: usize, crc: Crc, d: &Direction) -> Result<NrPolar> {
243        let k = a + crc.width() as usize;
244        ensure(e >= 1, || "E must be positive".to_string())?;
245        let n = mother_length(k, e, d.n_max);
246        ensure(k + d.n_pc <= e && k + d.n_pc <= n, || format!("K + n_PC = {} exceeds E = {e} or N = {n}", k + d.n_pc))?;
247        // the sub-block interleaver J(n)
248        let b = n / 32;
249        let j: Vec<usize> = (0..n).map(|m| SUBBLOCK_PATTERN[32 * m / n] as usize * b + m % b).collect();
250        let (puncturing, frozen, shortened) = rate_matching_frozen(&j, k, e);
251        let (info, pc) = info_and_pc(&frozen, k, d)?;
252        let code = PolarCode::with_positions(n, &info, &pc)?;
253        // the input interleaver of Sec. 5.3.1.1
254        let il: Vec<usize> = if d.i_il {
255            POLAR_INTERLEAVER.iter().map(|&m| m as usize).filter(|&m| m >= 164 - k).map(|m| m - (164 - k)).collect()
256        } else {
257            (0..k).collect()
258        };
259        // bit selection, Sec. 5.4.1.2: indices into y, then into d through J
260        let sel: Vec<usize> = (0..e)
261            .map(|t| {
262                if e >= n {
263                    t % n
264                } else if puncturing {
265                    t + n - e
266                } else {
267                    t
268                }
269            })
270            .map(|y| j[y])
271            .collect();
272        let map = BitSelection::new(if d.i_bil { triangular(&sel) } else { sel }, n)?;
273        Ok(NrPolar { a, e, crc, il, map, shortened, code })
274    }
275
276    /// The same code decoded by SC list with `list` paths.
277    ///
278    /// # Errors
279    /// [`comnum_core::Error::Config`] for `list = 0`.
280    pub fn with_list(mut self, list: usize) -> Result<NrPolar> {
281        self.code = self.code.with_list(list)?;
282        Ok(self)
283    }
284
285    /// The list size $L$ of the decoder.
286    pub fn list(&self) -> usize {
287        self.code.list()
288    }
289
290    /// The mother code length $N$.
291    pub fn mother_length(&self) -> usize {
292        self.code.n()
293    }
294
295    /// The mother code, its positions and its list size.
296    pub fn code(&self) -> &PolarCode {
297        &self.code
298    }
299
300    /// The bit of the mother codeword carried by every output bit.
301    pub fn bit_map(&self) -> &[usize] {
302        self.map.positions()
303    }
304}
305
306impl BlockCode for NrPolar {
307    fn k(&self) -> usize {
308        self.a
309    }
310
311    fn n(&self) -> usize {
312        self.e
313    }
314
315    fn encode_block(&self, info: &[u8]) -> Result<Vec<u8>> {
316        expect_len(info.len(), self.a, "bits")?;
317        let c = self.crc.attach(info);
318        let interleaved: Vec<u8> = self.il.iter().map(|&p| c[p]).collect();
319        self.map.select(&self.code.encode_block(&interleaved)?)
320    }
321
322    fn decode_block(&self, llr: &[f64]) -> Result<Vec<u8>> {
323        expect_len(llr.len(), self.e, "LLRs")?;
324        let mut d = self.map.word_llr(llr)?;
325        for &p in &self.shortened {
326            d[p] = KNOWN_BIT_LLR;
327        }
328        let k = self.il.len();
329        let mut first = None;
330        for cand in self.code.decode_list(&d)? {
331            let mut c = vec![0u8; k];
332            for (&p, &b) in self.il.iter().zip(&cand) {
333                c[p] = b;
334            }
335            if self.crc.check(&c) {
336                return Ok(c[..self.a].to_vec());
337            }
338            first.get_or_insert(c);
339        }
340        Ok(first.map_or_else(|| vec![0; self.a], |c| c[..self.a].to_vec()))
341    }
342}
343
344container_traits!(NrPolar);
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn the_links_are_read_by_name() {
352        let same = |a: NrPolar, b: NrPolar| a.k() == b.k() && a.n() == b.n() && a.bit_map() == b.bit_map();
353        assert!(same(NrPolar::from_name("Uplink", 30, 100).unwrap(), NrPolar::uplink(30, 100).unwrap()));
354        assert!(same(NrPolar::from_name("downlink", 30, 100).unwrap(), NrPolar::downlink(30, 100).unwrap()));
355        assert!(!same(NrPolar::uplink(30, 100).unwrap(), NrPolar::downlink(30, 100).unwrap()));
356        assert!(NrPolar::from_name("sidelink", 30, 100).is_err());
357    }
358
359    #[test]
360    fn the_tables_are_permutations() {
361        let mut q: Vec<u16> = POLAR_SEQUENCE.to_vec();
362        q.sort_unstable();
363        assert!(q.iter().enumerate().all(|(i, &v)| v as usize == i));
364        let mut il: Vec<u8> = POLAR_INTERLEAVER.to_vec();
365        il.sort_unstable();
366        assert!(il.iter().enumerate().all(|(i, &v)| v as usize == i));
367        let mut p: Vec<u8> = SUBBLOCK_PATTERN.to_vec();
368        p.sort_unstable();
369        assert!(p.iter().enumerate().all(|(i, &v)| v as usize == i));
370    }
371
372    #[test]
373    fn puncturing_shortening_repetition_and_parity_checks_round_trip() {
374        // (A, E): shortening, puncturing, repetition, PC bits with and without the
375        // minimum-weight one, downlink
376        for (a, e, down) in
377            [(30, 45, false), (30, 200, false), (40, 600, false), (14, 100, false), (14, 300, false), (39, 108, true)]
378        {
379            let code =
380                if down { NrPolar::downlink(a, e) } else { NrPolar::uplink(a, e) }.unwrap().with_list(8).unwrap();
381            let info: Vec<u8> = (0..a).map(|i| ((i * 5 + a) % 3 == 0) as u8).collect();
382            let c = code.encode_block(&info).unwrap();
383            assert_eq!(c.len(), e);
384            let llr: Vec<f64> = c.iter().map(|b| if *b == 0 { 2.0 } else { -2.0 }).collect();
385            assert_eq!(code.decode_block(&llr).unwrap(), info, "A {a}, E {e}");
386        }
387        assert!(NrPolar::uplink(11, 100).is_err() && NrPolar::uplink(400, 2000).is_err());
388        assert!(NrPolar::uplink(30, 100).unwrap().with_list(0).is_err());
389    }
390
391    /// TS 38.212, Sec. 5.4.1.3: for E = 10 (T = 4), the rows 0 1 2 3 / 4 5 6 / 7 8 / 9
392    /// read column by column.
393    #[test]
394    fn the_triangular_interleaver_reads_the_columns() {
395        let e: Vec<usize> = (0..10).collect();
396        assert_eq!(triangular(&e), [0, 4, 7, 9, 1, 5, 8, 2, 6, 3]);
397        // E = 8 leaves the last two cells of T = 4 empty
398        assert_eq!(triangular(&e[..8]), [0, 4, 7, 1, 5, 2, 6, 3]);
399    }
400}