Skip to main content

comnum_mapping/
differential.rs

1//! Differential coding of M-PSK (DBPSK, DQPSK, D8PSK, …): encoding and decoding of the
2//! phase indices, noncoherent detection, and the Gray-labelled link [`Dpsk`].
3//!
4//! # Model
5//!
6//! The information is carried by the **phase transitions**. With $a_{k} \in \\{0, \dots,
7//! M - 1\\}$ the differential phase index of the symbol $k$ and $c_{-1} = 0$ the
8//! reference,
9//!
10//! $$
11//! c_{k} = (a_{k} + c_{k-1}) \bmod M, \qquad x_{k} = e^{j 2 \pi c_{k} / M},
12//! $$
13//!
14//! so a sequence of $n$ information symbols is transmitted as $n + 1$ PSK symbols
15//! ([`encode`]). Decoding is the difference $a_{k} = (c_{k} - c_{k-1}) \bmod M$
16//! ([`decode`]). The noncoherent detector ([`detect`]) needs no carrier phase: it decides
17//! the differential phase nearest to
18//!
19//! $$
20//! \arg \left(r_{k} \\, r_{k-1}^{\ast}\right),
21//! $$
22//!
23//! which a constant rotation $e^{j \phi}$ of the received symbols leaves unchanged. With
24//! coherent decisions on every symbol followed by [`decode`], one wrong symbol spoils two
25//! differences: the bit error rate of DBPSK becomes $2p(1 - p)$ with $p =
26//! Q(\sqrt{2 E_{b}/N_{0}})$ ([`crate::theory::ber_dbpsk_coherent`]); the noncoherent
27//! detector gives $\frac{1}{2} e^{-E_{b}/N_{0}}$ for DBPSK and the integral of Pawula,
28//! Rice & Roberts for M-DPSK ([`crate::theory::ber_dpsk`]).
29//!
30//! [`Dpsk`] maps bits to differential phase indices with the Gray labelling of
31//! [`crate::constellation`] (the bits of a symbol are the Gray label of its phase
32//! transition), so that the nearest wrong transitions differ by one bit.
33//!
34//! # Square QAM with a differential quadrant
35//!
36//! A carrier recovery of square QAM, such as the blind phase search, leaves an ambiguity
37//! of a multiple of $\pi/2$. [`QuadrantQam`] removes it as Pfau, Hoffmann and Noé (2009,
38//! Sec. III-A, Fig. 5): the two bits that select the quadrant are the Gray label of a
39//! differential quadrant index ([`encode`] with $M = 4$), and the other $\log_{2} M - 2$
40//! bits select the point inside the quadrant, Gray-labelled along each axis, the pattern
41//! of the first quadrant rotated with the quadrant. A rotation of the received symbols by
42//! $j$ moves every symbol to the next quadrant with the same inner label, so the
43//! differential decoding of the quadrants returns the same bits. The labelling is no
44//! longer Gray across the axes, which costs about 3 dB of SNR for QPSK at a given bit
45//! error rate (every symbol error spoils two quadrant differences), and less and less
46//! for larger constellations.
47//!
48//! # Examples
49//! ```
50//! use comnum_mapping::differential::{Dpsk, decode, detect, encode};
51//! use comnum_core::rng::Rng;
52//! let a = vec![0, 1, 3, 2, 1];
53//! let c = encode(&a, 4)?;
54//! assert_eq!(c, vec![0, 0, 1, 0, 2, 3]);
55//! assert_eq!(decode(&c, 4)?, a);
56//! let dqpsk = Dpsk::new(4)?;
57//! let bits = comnum_mapping::source::bits(200, &mut Rng::new(1));
58//! let x = dqpsk.map_bits(&bits)?;
59//! assert_eq!(x.len(), 101);
60//! // a constant phase rotation does not change the decoded bits
61//! let rotated: Vec<_> = x.iter().map(|v| v * comnum_math::cis(1.234)).collect();
62//! assert_eq!(dqpsk.demap_bits(&rotated)?, bits);
63//! assert_eq!(detect(&rotated, 4)?, detect(&x, 4)?);
64//! # Ok::<(), comnum_core::Error>(())
65//! ```
66//!
67//! # References
68//!
69//! 1. J. G. Proakis and M. Salehi, *Digital Communications*, 5th ed., McGraw-Hill, 2008,
70//!    §3.2-2 (differentially encoded PSK) and §4.5 (noncoherent detection, DPSK).
71//! 2. R. F. Pawula, S. O. Rice and J. H. Roberts, "Distribution of the phase angle
72//!    between two vectors perturbed by Gaussian noise," *IEEE Trans. Commun.*, 30(8),
73//!    1828–1841, 1982.
74//! 3. T. Pfau, S. Hoffmann and R. Noé, "Hardware-efficient coherent digital receiver
75//!    concept with feedforward carrier recovery for M-QAM constellations," *J. Lightw.
76//!    Technol.*, 27(8), 2009.
77
78use crate::constellation::{indices_to_bits, qam_side};
79use comnum_core::{Error, Result, ensure};
80use comnum_math::bits::bits_to_indices;
81use comnum_math::{C64, cis, gray, gray_inverse};
82use std::f64::consts::TAU;
83
84/// The detection of a differential PSK signal.
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum Detection {
87    /// Noncoherent: the phase difference of consecutive samples ([`detect`]).
88    Noncoherent,
89    /// Coherent detection, then differential decoding ([`detect_coherent`]).
90    Coherent,
91}
92
93impl std::str::FromStr for Detection {
94    type Err = Error;
95
96    /// `noncoherent` or `coherent` (case-insensitive).
97    ///
98    /// # Errors
99    /// [`Error::Config`] for another name.
100    fn from_str(s: &str) -> Result<Detection> {
101        match s.to_ascii_lowercase().as_str() {
102            "noncoherent" => Ok(Detection::Noncoherent),
103            "coherent" => Ok(Detection::Coherent),
104            _ => Err(Error::Config(format!("detection must be noncoherent or coherent, got {s:?}"))),
105        }
106    }
107}
108
109/// Check the order and the indices of a differential code.
110fn check(indices: &[usize], m: usize) -> Result<()> {
111    ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
112    ensure(indices.iter().all(|&i| i < m), || format!("phase indices must be below {m}"))
113}
114
115/// Differential encoding: the `n + 1` phase indices $c_{-1} = 0, c_{0}, \dots, c_{n-1}$
116/// with $c_{k} = (a_{k} + c_{k-1}) \bmod M$.
117///
118/// # Errors
119/// [`Error::Config`] unless `m` is a power of two ≥ 2 and every index is below `m`.
120///
121/// # Examples
122/// ```
123/// use comnum_mapping::differential::encode;
124/// assert_eq!(encode(&[1, 1, 1, 1], 2).unwrap(), vec![0, 1, 0, 1, 0]);
125/// ```
126pub fn encode(indices: &[usize], m: usize) -> Result<Vec<usize>> {
127    check(indices, m)?;
128    let mut out = Vec::with_capacity(indices.len() + 1);
129    let mut c = 0;
130    out.push(c);
131    for &a in indices {
132        c = (a + c) % m;
133        out.push(c);
134    }
135    Ok(out)
136}
137
138/// Differential decoding: the `n` differences $a_{k} = (c_{k} - c_{k-1}) \bmod M$ of
139/// `n + 1` phase indices.
140///
141/// # Errors
142/// [`Error::Config`] unless `m` is a power of two ≥ 2 and every index is below `m`;
143/// [`Error::Shape`] without a reference symbol (an empty input).
144pub fn decode(coded: &[usize], m: usize) -> Result<Vec<usize>> {
145    check(coded, m)?;
146    ensure_reference(coded.len())?;
147    Ok(coded.windows(2).map(|w| (w[1] + m - w[0]) % m).collect())
148}
149
150/// [`Error::Shape`] unless there is at least the reference symbol.
151fn ensure_reference(n: usize) -> Result<()> {
152    if n == 0 {
153        Err(Error::Shape("a differential sequence holds at least its reference symbol".into()))
154    } else {
155        Ok(())
156    }
157}
158
159/// The phase index nearest to the argument of `z`, $\lfloor M \arg z / 2 \pi \rceil \bmod M$.
160fn phase_index(z: C64, m: usize) -> usize {
161    let k = (z.arg() / TAU * m as f64).round() as i64;
162    k.rem_euclid(m as i64) as usize
163}
164
165/// Coherent decisions: the phase index nearest to every received symbol (the hard
166/// decision of M-PSK on the absolute phase); [`decode`] then removes the differential
167/// encoding.
168///
169/// # Errors
170/// [`Error::Config`] unless `m` is a power of two ≥ 2.
171pub fn detect_coherent(r: &[C64], m: usize) -> Result<Vec<usize>> {
172    check(&[], m)?;
173    Ok(r.iter().map(|&z| phase_index(z, m)).collect())
174}
175
176/// Noncoherent detection: the differential phase index nearest to $\arg(r_{k}
177/// r_{k-1}^{\ast})$ for every pair of consecutive received symbols (`n` decisions for
178/// `n + 1` symbols).
179///
180/// # Errors
181/// [`Error::Config`] unless `m` is a power of two ≥ 2; [`Error::Shape`] for an empty
182/// input.
183///
184/// # Examples
185/// ```
186/// use comnum_mapping::differential::detect;
187/// use comnum_math::cis;
188/// let r = [cis(0.3), cis(0.3 + std::f64::consts::FRAC_PI_2), cis(0.3 - std::f64::consts::FRAC_PI_2)];
189/// assert_eq!(detect(&r, 4).unwrap(), vec![1, 2]);
190/// ```
191pub fn detect(r: &[C64], m: usize) -> Result<Vec<usize>> {
192    check(&[], m)?;
193    ensure_reference(r.len())?;
194    Ok(r.windows(2).map(|w| phase_index(w[1] * w[0].conj(), m)).collect())
195}
196
197/// Differential M-PSK with Gray-labelled phase transitions: the differential phase
198/// index $k$ carries the label $g(k)$ ([`comnum_math::gray`]).
199#[derive(Clone, Debug, PartialEq, Eq)]
200pub struct Dpsk {
201    m: usize,
202    bits: usize,
203}
204
205impl Dpsk {
206    /// Differential PSK of order `m`.
207    ///
208    /// # Errors
209    /// [`Error::Config`] unless `m` is a power of two ≥ 2.
210    pub fn new(m: usize) -> Result<Dpsk> {
211        check(&[], m)?;
212        Ok(Dpsk { m, bits: m.trailing_zeros() as usize })
213    }
214
215    /// The order $M$.
216    pub fn order(&self) -> usize {
217        self.m
218    }
219
220    /// Bits per symbol, $\log_{2} M$.
221    pub fn bits_per_symbol(&self) -> usize {
222        self.bits
223    }
224
225    /// The differential phase indices of a bit stream (most significant bit first).
226    ///
227    /// # Errors
228    /// [`Error::Shape`] if the number of bits is not a multiple of $\log_{2} M$.
229    pub fn indices(&self, bits: &[u8]) -> Result<Vec<usize>> {
230        Ok(bits_to_indices(bits, self.bits)?.into_iter().map(gray_inverse).collect())
231    }
232
233    /// The bits of a sequence of differential phase indices.
234    ///
235    /// # Errors
236    /// [`Error::Config`] if an index is not below $M$.
237    pub fn bits(&self, indices: &[usize]) -> Result<Vec<u8>> {
238        check(indices, self.m)?;
239        Ok(indices_to_bits(&indices.iter().map(|&k| gray(k)).collect::<Vec<_>>(), self.bits))
240    }
241
242    /// The `n/log2 M + 1` unit-energy PSK symbols carrying `n` bits (the reference first).
243    ///
244    /// # Errors
245    /// [`Error::Shape`] if the number of bits is not a multiple of $\log_{2} M$.
246    pub fn map_bits(&self, bits: &[u8]) -> Result<Vec<C64>> {
247        let c = encode(&self.indices(bits)?, self.m)?;
248        Ok(c.into_iter().map(|k| cis(TAU * k as f64 / self.m as f64)).collect())
249    }
250
251    /// Noncoherent demodulation ([`detect`]) of `n + 1` received symbols into
252    /// `n log2 M` bits.
253    ///
254    /// # Errors
255    /// [`Error::Shape`] for an empty input.
256    pub fn demap_bits(&self, r: &[C64]) -> Result<Vec<u8>> {
257        self.bits(&detect(r, self.m)?)
258    }
259
260    /// Coherent demodulation with differential decoding ([`detect_coherent`] then
261    /// [`decode`]) of `n + 1` received symbols into `n log2 M` bits.
262    ///
263    /// # Errors
264    /// [`Error::Shape`] for an empty input.
265    pub fn demodulate_coherent(&self, r: &[C64]) -> Result<Vec<u8>> {
266        self.bits(&decode(&detect_coherent(r, self.m)?, self.m)?)
267    }
268}
269
270/// Square M-QAM whose quadrant is differentially encoded and whose points inside a
271/// quadrant are Gray-labelled (Pfau et al. 2009, Sec. III-A; see the module): the bits
272/// are read by a coherent receiver after a carrier recovery with an ambiguity of a
273/// multiple of $\pi/2$.
274#[derive(Clone, Debug, PartialEq)]
275pub struct QuadrantQam {
276    m: usize,
277    side: usize,
278    axis_bits: usize,
279    scale: f64,
280}
281
282impl QuadrantQam {
283    /// Square QAM of order `m` (4, 16, 64, …), of unit average energy.
284    ///
285    /// # Errors
286    /// [`Error::Config`] unless `m` is an even power of two ≥ 4.
287    pub fn new(m: usize) -> Result<QuadrantQam> {
288        let side = qam_side(m).filter(|s| m >= 4 && s.is_power_of_two());
289        let side = side.ok_or_else(|| Error::Config(format!("the order must be 4, 16, 64, …, got {m}")))?;
290        let axis_bits = (side / 2).trailing_zeros() as usize;
291        Ok(QuadrantQam { m, side, axis_bits, scale: (1.5 / (m as f64 - 1.0)).sqrt() })
292    }
293
294    /// The order $M$.
295    pub fn order(&self) -> usize {
296        self.m
297    }
298
299    /// Bits per symbol, $\log_{2} M$.
300    pub fn bits_per_symbol(&self) -> usize {
301        2 + 2 * self.axis_bits
302    }
303
304    /// The point of the quadrant `q` with the inner label `inner`: the point
305    /// $(2i + 1) + j (2k + 1)$ of the first quadrant, $i$ and $k$ the Gray-decoded halves
306    /// of the label, rotated by $j^{q}$.
307    fn point(&self, q: usize, inner: usize) -> C64 {
308        let i = gray_inverse(inner >> self.axis_bits);
309        let k = gray_inverse(inner & ((1 << self.axis_bits) - 1));
310        C64::new((2 * i + 1) as f64, (2 * k + 1) as f64) * C64::new(0.0, 1.0).powu(q as u32) * self.scale
311    }
312
313    /// The $M$ points (for the decisions of a carrier recovery).
314    pub fn points(&self) -> Vec<C64> {
315        let inner = self.m / 4;
316        (0..self.m).map(|n| self.point(n / inner, n % inner)).collect()
317    }
318
319    /// The `n / log2 M + 1` symbols carrying `n` bits, the reference first (quadrant 0,
320    /// inner label 0); the first two bits of every symbol select the quadrant transition.
321    ///
322    /// # Errors
323    /// [`Error::Shape`] if the number of bits is not a multiple of $\log_{2} M$.
324    pub fn map_bits(&self, bits: &[u8]) -> Result<Vec<C64>> {
325        let labels = bits_to_indices(bits, self.bits_per_symbol())?;
326        let inner_mask = (1 << (2 * self.axis_bits)) - 1;
327        let shift = 2 * self.axis_bits;
328        let quadrants = encode(&labels.iter().map(|&l| gray_inverse(l >> shift)).collect::<Vec<_>>(), 4)?;
329        let inner = std::iter::once(0).chain(labels.iter().map(|&l| l & inner_mask));
330        Ok(quadrants.into_iter().zip(inner).map(|(q, i)| self.point(q, i)).collect())
331    }
332
333    /// The hard decision of a received symbol: its quadrant and its inner label.
334    fn decide(&self, r: C64) -> (usize, usize) {
335        let edge = (self.side - 1) as f64;
336        let level = |x: f64| (2.0 * (x / 2.0).floor() + 1.0).clamp(-edge, edge);
337        let z = C64::new(level(r.re / self.scale), level(r.im / self.scale));
338        let q = match (z.re > 0.0, z.im > 0.0) {
339            (true, true) => 0,
340            (false, true) => 1,
341            (false, false) => 2,
342            (true, false) => 3,
343        };
344        // back to the first quadrant
345        let w = z * C64::new(0.0, -1.0).powu(q as u32);
346        let (i, k) = (((w.re.round() as usize) - 1) / 2, ((w.im.round() as usize) - 1) / 2);
347        (q, (gray(i) << self.axis_bits) | gray(k))
348    }
349
350    /// Hard decisions on `n + 1` received symbols and differential decoding of their
351    /// quadrants: `n log2 M` bits, whatever multiple of $\pi/2$ rotates the symbols.
352    ///
353    /// # Errors
354    /// [`Error::Shape`] for an empty input.
355    pub fn demap_bits(&self, r: &[C64]) -> Result<Vec<u8>> {
356        ensure_reference(r.len())?;
357        let (quadrants, inner): (Vec<usize>, Vec<usize>) = r.iter().map(|&z| self.decide(z)).unzip();
358        let shift = 2 * self.axis_bits;
359        let labels: Vec<usize> =
360            decode(&quadrants, 4)?.into_iter().zip(&inner[1..]).map(|(a, &i)| (gray(a) << shift) | i).collect();
361        Ok(indices_to_bits(&labels, self.bits_per_symbol()))
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use comnum_core::rng::Rng;
369
370    #[test]
371    fn the_detections_are_read_by_name() {
372        assert_eq!("NonCoherent".parse::<Detection>().unwrap(), Detection::Noncoherent);
373        assert_eq!("coherent".parse::<Detection>().unwrap(), Detection::Coherent);
374        assert!("differential".parse::<Detection>().is_err());
375    }
376
377    #[test]
378    fn round_trips_and_errors() {
379        let mut rng = Rng::new(4);
380        for m in [2usize, 4, 8, 16] {
381            let a: Vec<usize> = (0..500).map(|_| rng.below(m)).collect();
382            let c = encode(&a, m).unwrap();
383            assert_eq!(c.len(), a.len() + 1);
384            assert_eq!(c[0], 0);
385            assert_eq!(decode(&c, m).unwrap(), a);
386            let d = Dpsk::new(m).unwrap();
387            let bits = crate::source::bits(500 * d.bits_per_symbol(), &mut rng);
388            let x = d.map_bits(&bits).unwrap();
389            assert!(x.iter().all(|v| (v.norm() - 1.0).abs() < 1e-12));
390            assert_eq!(d.demap_bits(&x).unwrap(), bits);
391            assert_eq!(d.demodulate_coherent(&x).unwrap(), bits);
392            // Gray labelling of the transitions: neighbouring phases differ by one bit
393            for k in 0..m {
394                let (b1, b2) = (d.bits(&[k]).unwrap(), d.bits(&[(k + 1) % m]).unwrap());
395                assert_eq!(b1.iter().zip(&b2).filter(|(x, y)| x != y).count(), 1);
396            }
397        }
398        assert!(encode(&[0, 1], 3).is_err());
399        assert!(encode(&[0, 4], 4).is_err());
400        assert!(decode(&[], 4).is_err());
401        assert!(detect(&[], 4).is_err());
402        assert!(Dpsk::new(6).is_err());
403        assert!(Dpsk::new(4).unwrap().map_bits(&[1, 0, 1]).is_err());
404    }
405
406    #[test]
407    fn quadrant_qam_survives_every_rotation_by_a_multiple_of_pi_2() {
408        let mut rng = Rng::new(9);
409        for m in [4usize, 16, 64, 256] {
410            let qq = QuadrantQam::new(m).unwrap();
411            let pts = qq.points();
412            assert_eq!(pts.len(), m);
413            let energy: f64 = pts.iter().map(C64::norm_sqr).sum::<f64>() / m as f64;
414            assert!((energy - 1.0).abs() < 1e-12);
415            // the points are those of square QAM
416            let qam = crate::constellation::Constellation::qam(m).unwrap().normalized();
417            for p in &pts {
418                assert!(qam.points().iter().any(|v| (v - p).norm() < 1e-12));
419            }
420            let bits = crate::source::bits(400 * qq.bits_per_symbol(), &mut rng);
421            let x = qq.map_bits(&bits).unwrap();
422            assert_eq!(x.len(), 401);
423            for q in 0..4u32 {
424                let rot: Vec<C64> = x.iter().map(|v| v * C64::new(0.0, 1.0).powu(q)).collect();
425                assert_eq!(qq.demap_bits(&rot).unwrap(), bits, "M = {m}, rotation {q}");
426            }
427            // Gray inside a quadrant: horizontal and vertical neighbours differ by one bit
428            for n in 0..m / 4 {
429                let a = qq.point(0, n);
430                for d in [C64::new(2.0, 0.0), C64::new(0.0, 2.0)] {
431                    let b = a + d * qq.scale;
432                    if let Some(j) = (0..m / 4).find(|&j| (qq.point(0, j) - b).norm() < 1e-9) {
433                        assert_eq!((n ^ j).count_ones(), 1, "M = {m}");
434                    }
435                }
436            }
437        }
438        assert!(QuadrantQam::new(8).is_err());
439        assert!(QuadrantQam::new(2).is_err());
440        assert!(QuadrantQam::new(16).unwrap().map_bits(&[1, 0, 1]).is_err());
441        assert!(QuadrantQam::new(16).unwrap().demap_bits(&[]).is_err());
442    }
443
444    #[test]
445    fn a_constant_rotation_does_not_change_the_decoded_bits() {
446        let mut rng = Rng::new(8);
447        for m in [2usize, 4, 8] {
448            let d = Dpsk::new(m).unwrap();
449            let bits = crate::source::bits(300 * d.bits_per_symbol(), &mut rng);
450            let x = d.map_bits(&bits).unwrap();
451            for phi in [0.1, 1.0, 2.5, -3.0, 4.2] {
452                let r: Vec<C64> = x.iter().map(|v| v * cis(phi)).collect();
453                assert_eq!(d.demap_bits(&r).unwrap(), bits, "M = {m}, φ = {phi}");
454            }
455        }
456    }
457}
458
459#[cfg(test)]
460mod literature {
461    use super::*;
462    use crate::theory::{ber_dbpsk_coherent, ber_dpsk, differential_phase_tail};
463    use comnum_core::rng::Rng;
464    use comnum_math::{db2lin, erfc};
465    use std::f64::consts::PI;
466
467    /// Unit-energy symbols plus noise at `ebn0_db` for `bits` bits per symbol:
468    /// `N_0 = 1 / (bits · E_b/N_0)`.
469    fn noisy(x: &[C64], bits: usize, ebn0_db: f64, rng: &mut Rng) -> Vec<C64> {
470        let sd = (1.0 / (bits as f64 * db2lin(ebn0_db))).sqrt();
471        x.iter().map(|v| v + rng.complex_gaussian() * sd).collect()
472    }
473
474    fn ber(a: &[u8], b: &[u8]) -> f64 {
475        a.iter().zip(b).filter(|(x, y)| x != y).count() as f64 / a.len() as f64
476    }
477
478    /// J. G. Proakis and M. Salehi, *Digital Communications*, 5th ed., 2008, §4.5
479    /// (binary DPSK): the noncoherent detector of DBPSK has the bit error probability
480    /// `½ exp(-E_b/N_0)`. 400 000 bits: about 3 700 errors are expected at 6 dB
481    /// (`9.3e-3`) and 364 at 8 dB (`9.1e-4`), so the relative standard errors are 1.6 %
482    /// and 5.2 %; the test accepts three standard errors.
483    #[test]
484    fn proakis_dbpsk_noncoherent_ber() {
485        let d = Dpsk::new(2).unwrap();
486        let mut rng = Rng::new(31);
487        let n = 400_000;
488        for ebn0_db in [6.0, 8.0] {
489            let bits = crate::source::bits(n, &mut rng);
490            let r = noisy(&d.map_bits(&bits).unwrap(), 1, ebn0_db, &mut rng);
491            let got = ber(&d.demap_bits(&r).unwrap(), &bits);
492            let want = 0.5 * (-db2lin(ebn0_db)).exp();
493            assert!((ber_dpsk(2, ebn0_db).unwrap() - want).abs() < 1e-12);
494            let tol = 3.0 / (want * n as f64).sqrt();
495            assert!((got / want - 1.0).abs() < tol, "{ebn0_db} dB: {got:e} vs {want:e}");
496        }
497    }
498
499    /// Proakis & Salehi, 5th ed., §3.2-2 (differentially encoded PSK with coherent
500    /// detection): an isolated symbol error spoils two differences, so the bit error rate
501    /// of DBPSK is `2p(1 - p)` with `p = Q(√(2 E_b/N_0))`. 400 000 bits at 6 dB
502    /// (`4.8e-3`, about 1 900 errors, standard error 2.3 %) and 8 dB (`3.8e-4`, about 150
503    /// errors, 8 %); three standard errors accepted.
504    #[test]
505    fn proakis_coherent_detection_with_differential_decoding() {
506        let d = Dpsk::new(2).unwrap();
507        let mut rng = Rng::new(32);
508        let n = 400_000;
509        for ebn0_db in [6.0, 8.0] {
510            let bits = crate::source::bits(n, &mut rng);
511            let r = noisy(&d.map_bits(&bits).unwrap(), 1, ebn0_db, &mut rng);
512            let got = ber(&d.demodulate_coherent(&r).unwrap(), &bits);
513            let want = ber_dbpsk_coherent(ebn0_db);
514            let tol = 3.0 / (want * n as f64).sqrt();
515            assert!((got / want - 1.0).abs() < tol, "{ebn0_db} dB: {got:e} vs {want:e}");
516        }
517    }
518
519    /// R. F. Pawula, S. O. Rice and J. H. Roberts, "Distribution of the phase angle
520    /// between two vectors perturbed by Gaussian noise," IEEE Trans. Commun. 30(8), 1982
521    /// (the integral form of the distribution of the differential phase, Section IV, for
522    /// two vectors of the same SNR): the bit error rate of Gray-coded DQPSK and D8PSK
523    /// with noncoherent detection ([`ber_dpsk`]). 399 996 bits at 8 dB for DQPSK
524    /// (`3.6e-3`, about 1 460 errors, standard error 2.6 %) and 11 dB for D8PSK
525    /// (`5.6e-3`, about 2 250 errors, 2.1 %); three standard errors accepted.
526    #[test]
527    fn pawula_rice_roberts_1982_mdpsk_ber() {
528        let mut rng = Rng::new(33);
529        let n = 399_996; // a multiple of 2 and 3 bits per symbol
530        for (m, ebn0_db) in [(4usize, 8.0), (8, 11.0)] {
531            let d = Dpsk::new(m).unwrap();
532            let bits = crate::source::bits(n, &mut rng);
533            let r = noisy(&d.map_bits(&bits).unwrap(), d.bits_per_symbol(), ebn0_db, &mut rng);
534            let got = ber(&d.demap_bits(&r).unwrap(), &bits);
535            let want = ber_dpsk(m, ebn0_db).unwrap();
536            let tol = 3.0 / (want * n as f64).sqrt();
537            assert!((got / want - 1.0).abs() < tol, "M = {m} at {ebn0_db} dB: {got:e} vs {want:e}");
538        }
539    }
540
541    /// Pawula, Rice & Roberts (1982), Section IV-A: the symbol error rate of M-DPSK is
542    /// their eq. (44), which [`differential_phase_tail`] computes at $\psi = \pi/M$; it
543    /// tends to the asymptote (46), $\sqrt{(1 + \cos(\pi/M))/(2\cos(\pi/M))}
544    /// \operatorname{erfc}\sqrt{\rho(1 - \cos(\pi/M))}$ (ratio within 0.3 % at $\rho = 100$,
545    /// 0.03 % at 1000); the approximation (47) of Fleck & Trabka over it tends to the
546    /// ratio $Q$ tabulated after eq. (50), 0.9186, 0.9768, 0.9987 and 0.9999 for $M = 3$,
547    /// 4, 8 and 16 (within 3e-4 at $\rho = 1000$); and, footnote 11, the bit error rate of
548    /// DQPSK is $\frac{1}{2} P_{E}(4) + P\{3\pi/4 < \psi \le \pi\}$, the last term
549    /// $\sim e^{-\rho}/(2\pi\rho)$ (within 8 % at 10 dB, 4 % at 15 dB), so that
550    /// $P_{B}(4) \approx \frac{1}{2} P_{E}(4)$, not the $\frac{2}{3} P_{E}(4)$ of orthogonal
551    /// signals.
552    #[test]
553    fn pawula_rice_roberts_1982_section_iv_a() {
554        let pe = |m: f64, rho: f64| differential_phase_tail(PI / m, rho);
555        for (m, q) in [(3.0, 0.9186), (4.0, 0.9768), (8.0, 0.9987), (16.0, 0.9999)] {
556            let c = (PI / m).cos();
557            let asymptote = |rho: f64| ((1.0 + c) / (2.0 * c)).sqrt() * erfc((rho * (1.0 - c)).sqrt());
558            let fleck_trabka = |rho: f64| {
559                let x = (rho * (1.0 - c)).sqrt();
560                erfc(x) + x * (-x * x).exp() / (4.0 * PI.sqrt() * (rho + 0.125))
561            };
562            for (rho, tol) in [(100.0, 3e-3), (1000.0, 3e-4)] {
563                let r = pe(m, rho) / asymptote(rho);
564                assert!((r - 1.0).abs() < tol, "M = {m}, rho = {rho}: (44)/(46) = {r}");
565            }
566            let ratio = fleck_trabka(1000.0) / pe(m, 1000.0);
567            assert!((ratio - q).abs() < 3e-4, "M = {m}: Q = {ratio} vs {q}");
568        }
569        for (rho_db, tol) in [(10.0, 0.08), (15.0, 0.04)] {
570            let rho = db2lin(rho_db);
571            let tail = 0.5 * differential_phase_tail(3.0 * PI / 4.0, rho);
572            let pb = ber_dpsk(4, rho_db - comnum_math::lin2db(2.0)).unwrap();
573            assert!((pb / (0.5 * pe(4.0, rho) + tail) - 1.0).abs() < 1e-10, "{rho_db} dB: {pb:e}");
574            let approx = (-rho).exp() / (2.0 * PI * rho);
575            assert!((tail / approx - 1.0).abs() < tol, "{rho_db} dB: {tail:e} vs {approx:e}");
576        }
577    }
578}