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}