Skip to main content

comnum_mapping/
source.rs

1//! Sources of bits and symbols (OptiCommPy `optic.comm.sources`).
2//!
3//! - [`bits`]: independent fair bits from a seeded generator, and [`qpsk`], one
4//!   unit-energy QPSK point (the pilots and preambles drawn from a seed);
5//! - [`prbs`]: maximal-length pseudo-random binary sequences with the polynomials of
6//!   ITU-T O.150 (orders 9, 11, 15, 20, 23, 29, 31, and 7), produced by a Fibonacci
7//!   linear-feedback shift register from any register state: the sequence of order `n`
8//!   has period `2ⁿ - 1` and contains every non-zero `n`-bit word exactly once per
9//!   period;
10//! - [`O150Sequence`]: the eight test sequences of ITU-T O.150 (05/96), Sec. 5, exactly:
11//!   the polynomial, the inversion of the output (Sec. 4), the zero suppression of the
12//!   second sequence of period $2^{20} - 1$ (Sec. 5.5) and the starting phase of the
13//!   511-bit sequence (Sec. 5.1), the sequences of the error-rate test sets;
14//! - [`symbols`]: random symbols of a constellation with a given probability mass
15//!   function, uniform by default, or Maxwell–Boltzmann shaped with
16//!   [`maxwell_boltzmann`] (probabilistic shaping, `p(x) ∝ exp(-λ|x|²)`).
17//!
18//! # Examples
19//! ```
20//! use comnum_mapping::source;
21//! use comnum_core::rng::Rng;
22//! let b = source::bits(1000, &mut Rng::new(3));
23//! assert!(b.iter().all(|&v| v <= 1));
24//! let p = source::prbs(7, 127 * 2, 1).unwrap();
25//! assert_eq!(&p[..127], &p[127..]);           // period 2⁷ - 1
26//! assert_eq!(p[..127].iter().filter(|&&v| v == 1).count(), 64);
27//! // the 2¹⁵ − 1 sequence of the O.151 test sets: inverted, it starts with 15 zeros
28//! let t = source::O150Sequence::Prbs15.bits(16);
29//! assert_eq!(&t[..15], &[0; 15]);
30//! assert_eq!(t[15], 1);
31//! ```
32//!
33//! # References
34//!
35//! 1. ITU-T Recommendation O.150 (05/96), *General requirements for instrumentation for
36//!    performance measurements on digital transmission equipment*, Sec. 3 to 5 and
37//!    Table 1.
38
39use crate::constellation::Constellation;
40use comnum_core::Result;
41use comnum_core::rng::Rng;
42use comnum_math::C64;
43
44/// `n` independent fair bits (`0` or `1`).
45pub fn bits(n: usize, rng: &mut Rng) -> Vec<u8> {
46    (0..n).map(|_| rng.bit()).collect()
47}
48
49/// One unit-energy QPSK point $e^{j\pi(2i + 1)/4}$, $i$ uniform in `0..4` (one draw of
50/// [`Rng::below`]): the pilots and preambles drawn from a seed.
51///
52/// # Examples
53/// ```
54/// use comnum_core::rng::Rng;
55/// let s = comnum_mapping::source::qpsk(&mut Rng::new(1));
56/// assert!((s.norm() - 1.0).abs() < 1e-15 && (s.re.abs() - s.im.abs()).abs() < 1e-15);
57/// ```
58#[inline]
59pub fn qpsk(rng: &mut Rng) -> C64 {
60    C64::from_polar(1.0, std::f64::consts::FRAC_PI_4 * (2 * rng.below(4) + 1) as f64)
61}
62
63/// Feedback taps `(n, k)` of the PRBS polynomials `xⁿ + xᵏ + 1`: those of ITU-T O.150,
64/// Sec. 5.1 to 5.4 and 5.6 to 5.8, and `x⁷ + x⁶ + 1`, not in O.150.
65const PRBS_TAPS: [(u32, u32); 8] = [(7, 6), (9, 5), (11, 9), (15, 14), (20, 3), (23, 18), (29, 27), (31, 28)];
66
67/// `length` bits of the PRBS of order `order`, from the register state `seed`.
68///
69/// The register is a Fibonacci LFSR ([`comnum_math::sequences::lfsr`]) with the polynomial
70/// `xⁿ + xᵏ + 1` of the order;
71/// `seed` gives its initial content (its `n` low bits; an all-zero register is replaced
72/// by 1, since it would lock the sequence at zero). The output bit is the feedback bit:
73/// the sequence of O.150 at another phase and never inverted; [`O150Sequence`] gives the
74/// test sequences of the Recommendation exactly.
75///
76/// # Errors
77/// [`comnum_core::Error::Config`] for an order outside `{7, 9, 11, 15, 20, 23, 29, 31}`.
78pub fn prbs(order: u32, length: usize, seed: u64) -> Result<Vec<u8>> {
79    let (n, k) =
80        PRBS_TAPS.iter().copied().find(|&(n, _)| n == order).ok_or_else(|| {
81            comnum_core::Error::Config(format!("PRBS order {order} not in 7, 9, 11, 15, 20, 23, 29, 31"))
82        })?;
83    let (n, k) = (n as usize, k as usize);
84    let reg = match seed & ((1u64 << n) - 1) {
85        0 => 1,
86        r => r,
87    };
88    // the register holds the last n outputs, the newest in its bit 0: the output bit
89    // b(t) = b(t - n) ⊕ b(t - k) is the LFSR x(i + n) = x(i) ⊕ x(i + n - k)
90    let init: Vec<u8> = (0..n).map(|i| ((reg >> (n - 1 - i)) & 1) as u8).collect();
91    Ok(comnum_math::sequences::lfsr(&[0, n - k], n, &init, n + length).split_off(n))
92}
93
94/// The pseudo-random test sequences of ITU-T O.150 (05/96), Sec. 5 and Table 1.
95///
96/// Each is produced by an $n$-stage shift register whose $k$-th and $n$-th stage outputs
97/// are added modulo two and fed back to the first stage,
98///
99/// $$
100/// Q_{i+1}(t+1) = Q_{i}(t), \quad i = 1, \dots, n - 1, \qquad Q_{1}(t+1) = Q_{k}(t) \oplus Q_{n}(t),
101/// $$
102///
103/// the polynomial $x^{n} + x^{k} + 1$, period $2^{n} - 1$. The output is the last stage
104/// $Q_{n}(t)$, negated for the inverted sequences, whose longest run of zeros is then $n$
105/// instead of $n - 1$ (Sec. 4). The second sequence of period $2^{20} - 1$ (Sec. 5.5)
106/// forces its output to one before 14 zeros,
107/// $RD(t) = Q_{20}(t) + \overline{Q_{6}(t) + \dots + Q_{19}(t)}$ ($+$ the logical OR).
108///
109/// | Sequence | Clause | $n$ | $k$ | Output | Longest run of zeros |
110/// | --- | --- | --- | --- | --- | --- |
111/// | `Prbs9` | 5.1 | 9 | 5 | non-inverted | 8 |
112/// | `Prbs11` | 5.2 | 11 | 9 | non-inverted | 10 |
113/// | `Prbs15` | 5.3 | 15 | 14 | inverted | 15 |
114/// | `Prbs20` | 5.4 | 20 | 3 | non-inverted | 19 |
115/// | `Prbs20Zs` | 5.5 | 20 | 17 | zero suppression | 14 |
116/// | `Prbs23` | 5.6 | 23 | 18 | inverted | 23 |
117/// | `Prbs29` | 5.7 | 29 | 27 | inverted | 29 |
118/// | `Prbs31` | 5.8 | 31 | 28 | inverted | 31 |
119///
120/// The Recommendation fixes the starting phase of the 511-bit sequence only, "the first
121/// ONE of 9 consecutive ONEs" (Sec. 5.1); [`O150Sequence::bits`] starts every register
122/// with its stages at one, which gives that phase and makes an inverted sequence begin
123/// with its $n$ zeros. A test set synchronises to the received sequence (Sec. 4.2), so
124/// the phase does not matter for a measurement.
125///
126/// The printed formula of Sec. 5.5 ends its OR with $Q_{6}(k)$ instead of $Q_{19}(k)$,
127/// and Sec. 5.8 calls the 31-stage register "twenty-nine-stage": two misprints that the
128/// text of the same clauses corrects (`validation/readings/o150.md`).
129///
130/// # Examples
131/// ```
132/// use comnum_mapping::source::O150Sequence;
133/// let s = O150Sequence::Prbs9;
134/// let b = s.bits(511 + 9);
135/// assert_eq!(&b[..9], &[1; 9]);              // Sec. 5.1: the first ONE of 9 ONEs
136/// assert_eq!(&b[511..], &b[..9]);            // period 2⁹ − 1
137/// assert_eq!(O150Sequence::from_name("prbs20z")?, O150Sequence::Prbs20Zs);
138/// # Ok::<(), comnum_core::Error>(())
139/// ```
140#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
141pub enum O150Sequence {
142    /// $2^{9} - 1$ bits, Sec. 5.1 (O.153, data circuits up to 14 400 bit/s).
143    Prbs9,
144    /// $2^{11} - 1$ bits, Sec. 5.2 (O.152, 64 kbit/s and N × 64 kbit/s).
145    Prbs11,
146    /// $2^{15} - 1$ bits, inverted, Sec. 5.3 (O.151, 1544 to 44 736 kbit/s).
147    Prbs15,
148    /// $2^{20} - 1$ bits, Sec. 5.4 (O.153, data circuits up to 72 kbit/s).
149    Prbs20,
150    /// $2^{20} - 1$ bits with zero suppression, Sec. 5.5 (O.151, 1544 to 44 736 kbit/s).
151    Prbs20Zs,
152    /// $2^{23} - 1$ bits, inverted, Sec. 5.6 (O.151, 34 368 and 139 264 kbit/s).
153    Prbs23,
154    /// $2^{29} - 1$ bits, inverted, Sec. 5.7 (specific measurement tasks).
155    Prbs29,
156    /// $2^{31} - 1$ bits, inverted, Sec. 5.8 (specific measurement tasks).
157    Prbs31,
158}
159
160impl O150Sequence {
161    /// The eight sequences, in the order of Sec. 5.
162    pub const ALL: [O150Sequence; 8] = [
163        O150Sequence::Prbs9,
164        O150Sequence::Prbs11,
165        O150Sequence::Prbs15,
166        O150Sequence::Prbs20,
167        O150Sequence::Prbs20Zs,
168        O150Sequence::Prbs23,
169        O150Sequence::Prbs29,
170        O150Sequence::Prbs31,
171    ];
172
173    /// The sequence of a name: `prbs9`, `prbs11`, `prbs15`, `prbs20`, `prbs20z` (the zero
174    /// suppression of Sec. 5.5), `prbs23`, `prbs29`, `prbs31`.
175    ///
176    /// # Errors
177    /// [`comnum_core::Error::Config`] for another name.
178    pub fn from_name(name: &str) -> Result<O150Sequence> {
179        O150Sequence::ALL.into_iter().find(|s| s.name() == name.to_ascii_lowercase()).ok_or_else(|| {
180            let known: Vec<&str> = O150Sequence::ALL.iter().map(|s| s.name()).collect();
181            comnum_core::Error::Config(format!("O.150 sequence {name:?} not in {}", known.join(", ")))
182        })
183    }
184
185    /// The name of [`O150Sequence::from_name`].
186    pub fn name(self) -> &'static str {
187        match self {
188            O150Sequence::Prbs9 => "prbs9",
189            O150Sequence::Prbs11 => "prbs11",
190            O150Sequence::Prbs15 => "prbs15",
191            O150Sequence::Prbs20 => "prbs20",
192            O150Sequence::Prbs20Zs => "prbs20z",
193            O150Sequence::Prbs23 => "prbs23",
194            O150Sequence::Prbs29 => "prbs29",
195            O150Sequence::Prbs31 => "prbs31",
196        }
197    }
198
199    /// The number of stages $n$.
200    pub fn stages(self) -> u32 {
201        match self {
202            O150Sequence::Prbs9 => 9,
203            O150Sequence::Prbs11 => 11,
204            O150Sequence::Prbs15 => 15,
205            O150Sequence::Prbs20 | O150Sequence::Prbs20Zs => 20,
206            O150Sequence::Prbs23 => 23,
207            O150Sequence::Prbs29 => 29,
208            O150Sequence::Prbs31 => 31,
209        }
210    }
211
212    /// The other feedback stage $k$: the polynomial is $x^{n} + x^{k} + 1$.
213    pub fn feedback(self) -> u32 {
214        match self {
215            O150Sequence::Prbs9 => 5,
216            O150Sequence::Prbs11 => 9,
217            O150Sequence::Prbs15 => 14,
218            O150Sequence::Prbs20 => 3,
219            O150Sequence::Prbs20Zs => 17,
220            O150Sequence::Prbs23 => 18,
221            O150Sequence::Prbs29 => 27,
222            O150Sequence::Prbs31 => 28,
223        }
224    }
225
226    /// Whether the output is the negated register output (Sec. 4).
227    pub fn inverted(self) -> bool {
228        matches!(self, O150Sequence::Prbs15 | O150Sequence::Prbs23 | O150Sequence::Prbs29 | O150Sequence::Prbs31)
229    }
230
231    /// The period $2^{n} - 1$.
232    pub fn period(self) -> u64 {
233        (1u64 << self.stages()) - 1
234    }
235
236    /// The longest run of zeros of the sequence, Table 1.
237    pub fn longest_zeros(self) -> u32 {
238        match self {
239            O150Sequence::Prbs20Zs => 14,
240            s if s.inverted() => s.stages(),
241            s => s.stages() - 1,
242        }
243    }
244
245    /// The first `length` bits of the sequence, from the register with every stage at
246    /// one (see the type).
247    pub fn bits(self, length: usize) -> Vec<u8> {
248        self.iter().take(length).collect()
249    }
250
251    /// The endless sequence, bit after bit, from the register with every stage at one: a
252    /// whole period of the long sequences without storing it.
253    pub fn iter(self) -> O150Bits {
254        O150Bits { sequence: self, register: (1u32 << self.stages()) - 1 }
255    }
256}
257
258/// The bits of an [`O150Sequence`] ([`O150Sequence::iter`]).
259#[derive(Clone, Debug)]
260pub struct O150Bits {
261    sequence: O150Sequence,
262    /// Bit $i - 1$ holds the stage $Q_{i}$.
263    register: u32,
264}
265
266impl Iterator for O150Bits {
267    type Item = u8;
268
269    fn next(&mut self) -> Option<u8> {
270        let (n, k) = (self.sequence.stages(), self.sequence.feedback());
271        let q = self.register;
272        let last = (q >> (n - 1)) & 1;
273        let out = match self.sequence {
274            // Q_6 to Q_19, the next 14 outputs of Sec. 5.5
275            O150Sequence::Prbs20Zs => last | u32::from(q & (((1u32 << 14) - 1) << 5) == 0),
276            _ => last,
277        };
278        let fb = last ^ ((q >> (k - 1)) & 1);
279        self.register = ((q << 1) | fb) & ((1u32 << n) - 1);
280        Some(out as u8 ^ u8::from(self.sequence.inverted()))
281    }
282}
283
284/// Maxwell–Boltzmann probabilities `p_i ∝ exp(-λ |x_i|²)` over the points of `c`.
285///
286/// `λ = 0` gives the uniform distribution; larger `λ` favour the low-energy points
287/// (probabilistic constellation shaping).
288pub fn maxwell_boltzmann(c: &Constellation, lambda: f64) -> Vec<f64> {
289    let w: Vec<f64> = c.points().iter().map(|p| (-lambda * p.norm_sqr()).exp()).collect();
290    let s: f64 = w.iter().sum();
291    w.iter().map(|v| v / s).collect()
292}
293
294/// `n` random symbols of `c` drawn with the probabilities `px` (uniform if `None`,
295/// normalised to a sum of 1 otherwise, as [`Constellation::priors`]).
296///
297/// Returns the indices (Gray labels) and the symbols.
298///
299/// # Errors
300/// As [`Constellation::priors`].
301pub fn symbols(c: &Constellation, n: usize, px: Option<&[f64]>, rng: &mut Rng) -> Result<(Vec<usize>, Vec<C64>)> {
302    let m = c.order();
303    let idx: Vec<usize> = if px.is_none() {
304        (0..n).map(|_| rng.below(m)).collect()
305    } else {
306        let mut cdf = Vec::with_capacity(m);
307        let mut acc = 0.0;
308        for v in c.priors(px)? {
309            acc += v;
310            cdf.push(acc);
311        }
312        (0..n)
313            .map(|_| {
314                let u = rng.uniform() * acc;
315                cdf.partition_point(|&c| c <= u).min(m - 1)
316            })
317            .collect()
318    };
319    let x = idx.iter().map(|&i| c.points()[i]).collect();
320    Ok((idx, x))
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn prbs_periods_and_balance() {
329        for &(n, _) in &PRBS_TAPS[..4] {
330            let period = (1usize << n) - 1;
331            let p = prbs(n, 2 * period, 0xACE1).unwrap();
332            assert_eq!(&p[..period], &p[period..], "order {n}");
333            let ones = p[..period].iter().filter(|&&v| v == 1).count();
334            assert_eq!(ones, period.div_ceil(2), "order {n}");
335        }
336        assert!(prbs(8, 10, 1).is_err());
337        assert_eq!(prbs(29, 10, 1).unwrap().len(), 10);
338    }
339
340    #[test]
341    fn the_511_bit_sequence_begins_with_its_nine_ones() {
342        // O.150 Sec. 5.1: "The sequence begins with the first ONE of 9 consecutive ONEs"
343        let b = O150Sequence::Prbs9.bits(2 * 511);
344        assert_eq!(&b[..9], &[1; 9]);
345        assert_eq!(b[510], 0, "the bit before the run (one period earlier) is a zero");
346        assert_eq!(&b[..511], &b[511..]);
347    }
348
349    #[test]
350    fn an_inverted_sequence_is_the_negated_register_output() {
351        // same register as prbs() with the polynomial x^15 + x^14 + 1: a shifted phase
352        let t = O150Sequence::Prbs15.bits(32_767);
353        assert_eq!(&t[..15], &[0; 15]);
354        assert_eq!(t.iter().filter(|&&v| v == 0).count(), 16_384, "the 2^14 ones of the m-sequence are zeros");
355        let p = prbs(15, 2 * 32_767, 1).unwrap();
356        let negated: Vec<u8> = t.iter().map(|v| v ^ 1).collect();
357        assert!((0..32_767).any(|d| p[d..d + 32_767] == negated[..]), "a phase of the same m-sequence");
358    }
359
360    #[test]
361    fn the_zero_suppression_differs_from_the_plain_sequence_of_the_same_length() {
362        // O.150, note of Sec. 5.4: the sequences of 5.4 and 5.5 are not identical
363        let (a, b) = (O150Sequence::Prbs20.bits(4096), O150Sequence::Prbs20Zs.bits(4096));
364        assert_ne!(a, b);
365        let longest = b.split(|&v| v == 1).map(<[u8]>::len).max().unwrap();
366        assert!(longest <= 14);
367    }
368
369    #[test]
370    fn the_names_round_trip() {
371        for s in O150Sequence::ALL {
372            assert_eq!(O150Sequence::from_name(s.name()).unwrap(), s);
373            assert_eq!(s.period(), (1u64 << s.stages()) - 1);
374        }
375        assert_eq!(O150Sequence::from_name("PRBS23").unwrap(), O150Sequence::Prbs23);
376        assert!(O150Sequence::from_name("prbs7").is_err());
377    }
378
379    #[test]
380    fn shaped_symbols_follow_the_distribution() {
381        let c = Constellation::qam(16).unwrap().normalized();
382        let px = maxwell_boltzmann(&c, 1.0);
383        let (idx, _) = symbols(&c, 200_000, Some(&px), &mut Rng::new(5)).unwrap();
384        for i in [0usize, 5, 15] {
385            let f = idx.iter().filter(|&&k| k == i).count() as f64 / idx.len() as f64;
386            assert!((f - px[i]).abs() < 0.005, "point {i}: {f} vs {}", px[i]);
387        }
388    }
389}