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}