use crate::constellation::{indices_to_bits, qam_side};
use comnum_core::{Error, Result, ensure};
use comnum_math::bits::bits_to_indices;
use comnum_math::{C64, cis, gray, gray_inverse};
use std::f64::consts::TAU;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Detection {
Noncoherent,
Coherent,
}
impl std::str::FromStr for Detection {
type Err = Error;
fn from_str(s: &str) -> Result<Detection> {
match s.to_ascii_lowercase().as_str() {
"noncoherent" => Ok(Detection::Noncoherent),
"coherent" => Ok(Detection::Coherent),
_ => Err(Error::Config(format!("detection must be noncoherent or coherent, got {s:?}"))),
}
}
}
fn check(indices: &[usize], m: usize) -> Result<()> {
ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
ensure(indices.iter().all(|&i| i < m), || format!("phase indices must be below {m}"))
}
pub fn encode(indices: &[usize], m: usize) -> Result<Vec<usize>> {
check(indices, m)?;
let mut out = Vec::with_capacity(indices.len() + 1);
let mut c = 0;
out.push(c);
for &a in indices {
c = (a + c) % m;
out.push(c);
}
Ok(out)
}
pub fn decode(coded: &[usize], m: usize) -> Result<Vec<usize>> {
check(coded, m)?;
ensure_reference(coded.len())?;
Ok(coded.windows(2).map(|w| (w[1] + m - w[0]) % m).collect())
}
fn ensure_reference(n: usize) -> Result<()> {
if n == 0 {
Err(Error::Shape("a differential sequence holds at least its reference symbol".into()))
} else {
Ok(())
}
}
fn phase_index(z: C64, m: usize) -> usize {
let k = (z.arg() / TAU * m as f64).round() as i64;
k.rem_euclid(m as i64) as usize
}
pub fn detect_coherent(r: &[C64], m: usize) -> Result<Vec<usize>> {
check(&[], m)?;
Ok(r.iter().map(|&z| phase_index(z, m)).collect())
}
pub fn detect(r: &[C64], m: usize) -> Result<Vec<usize>> {
check(&[], m)?;
ensure_reference(r.len())?;
Ok(r.windows(2).map(|w| phase_index(w[1] * w[0].conj(), m)).collect())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Dpsk {
m: usize,
bits: usize,
}
impl Dpsk {
pub fn new(m: usize) -> Result<Dpsk> {
check(&[], m)?;
Ok(Dpsk { m, bits: m.trailing_zeros() as usize })
}
pub fn order(&self) -> usize {
self.m
}
pub fn bits_per_symbol(&self) -> usize {
self.bits
}
pub fn indices(&self, bits: &[u8]) -> Result<Vec<usize>> {
Ok(bits_to_indices(bits, self.bits)?.into_iter().map(gray_inverse).collect())
}
pub fn bits(&self, indices: &[usize]) -> Result<Vec<u8>> {
check(indices, self.m)?;
Ok(indices_to_bits(&indices.iter().map(|&k| gray(k)).collect::<Vec<_>>(), self.bits))
}
pub fn map_bits(&self, bits: &[u8]) -> Result<Vec<C64>> {
let c = encode(&self.indices(bits)?, self.m)?;
Ok(c.into_iter().map(|k| cis(TAU * k as f64 / self.m as f64)).collect())
}
pub fn demap_bits(&self, r: &[C64]) -> Result<Vec<u8>> {
self.bits(&detect(r, self.m)?)
}
pub fn demodulate_coherent(&self, r: &[C64]) -> Result<Vec<u8>> {
self.bits(&decode(&detect_coherent(r, self.m)?, self.m)?)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct QuadrantQam {
m: usize,
side: usize,
axis_bits: usize,
scale: f64,
}
impl QuadrantQam {
pub fn new(m: usize) -> Result<QuadrantQam> {
let side = qam_side(m).filter(|s| m >= 4 && s.is_power_of_two());
let side = side.ok_or_else(|| Error::Config(format!("the order must be 4, 16, 64, …, got {m}")))?;
let axis_bits = (side / 2).trailing_zeros() as usize;
Ok(QuadrantQam { m, side, axis_bits, scale: (1.5 / (m as f64 - 1.0)).sqrt() })
}
pub fn order(&self) -> usize {
self.m
}
pub fn bits_per_symbol(&self) -> usize {
2 + 2 * self.axis_bits
}
fn point(&self, q: usize, inner: usize) -> C64 {
let i = gray_inverse(inner >> self.axis_bits);
let k = gray_inverse(inner & ((1 << self.axis_bits) - 1));
C64::new((2 * i + 1) as f64, (2 * k + 1) as f64) * C64::new(0.0, 1.0).powu(q as u32) * self.scale
}
pub fn points(&self) -> Vec<C64> {
let inner = self.m / 4;
(0..self.m).map(|n| self.point(n / inner, n % inner)).collect()
}
pub fn map_bits(&self, bits: &[u8]) -> Result<Vec<C64>> {
let labels = bits_to_indices(bits, self.bits_per_symbol())?;
let inner_mask = (1 << (2 * self.axis_bits)) - 1;
let shift = 2 * self.axis_bits;
let quadrants = encode(&labels.iter().map(|&l| gray_inverse(l >> shift)).collect::<Vec<_>>(), 4)?;
let inner = std::iter::once(0).chain(labels.iter().map(|&l| l & inner_mask));
Ok(quadrants.into_iter().zip(inner).map(|(q, i)| self.point(q, i)).collect())
}
fn decide(&self, r: C64) -> (usize, usize) {
let edge = (self.side - 1) as f64;
let level = |x: f64| (2.0 * (x / 2.0).floor() + 1.0).clamp(-edge, edge);
let z = C64::new(level(r.re / self.scale), level(r.im / self.scale));
let q = match (z.re > 0.0, z.im > 0.0) {
(true, true) => 0,
(false, true) => 1,
(false, false) => 2,
(true, false) => 3,
};
let w = z * C64::new(0.0, -1.0).powu(q as u32);
let (i, k) = (((w.re.round() as usize) - 1) / 2, ((w.im.round() as usize) - 1) / 2);
(q, (gray(i) << self.axis_bits) | gray(k))
}
pub fn demap_bits(&self, r: &[C64]) -> Result<Vec<u8>> {
ensure_reference(r.len())?;
let (quadrants, inner): (Vec<usize>, Vec<usize>) = r.iter().map(|&z| self.decide(z)).unzip();
let shift = 2 * self.axis_bits;
let labels: Vec<usize> =
decode(&quadrants, 4)?.into_iter().zip(&inner[1..]).map(|(a, &i)| (gray(a) << shift) | i).collect();
Ok(indices_to_bits(&labels, self.bits_per_symbol()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
#[test]
fn the_detections_are_read_by_name() {
assert_eq!("NonCoherent".parse::<Detection>().unwrap(), Detection::Noncoherent);
assert_eq!("coherent".parse::<Detection>().unwrap(), Detection::Coherent);
assert!("differential".parse::<Detection>().is_err());
}
#[test]
fn round_trips_and_errors() {
let mut rng = Rng::new(4);
for m in [2usize, 4, 8, 16] {
let a: Vec<usize> = (0..500).map(|_| rng.below(m)).collect();
let c = encode(&a, m).unwrap();
assert_eq!(c.len(), a.len() + 1);
assert_eq!(c[0], 0);
assert_eq!(decode(&c, m).unwrap(), a);
let d = Dpsk::new(m).unwrap();
let bits = crate::source::bits(500 * d.bits_per_symbol(), &mut rng);
let x = d.map_bits(&bits).unwrap();
assert!(x.iter().all(|v| (v.norm() - 1.0).abs() < 1e-12));
assert_eq!(d.demap_bits(&x).unwrap(), bits);
assert_eq!(d.demodulate_coherent(&x).unwrap(), bits);
for k in 0..m {
let (b1, b2) = (d.bits(&[k]).unwrap(), d.bits(&[(k + 1) % m]).unwrap());
assert_eq!(b1.iter().zip(&b2).filter(|(x, y)| x != y).count(), 1);
}
}
assert!(encode(&[0, 1], 3).is_err());
assert!(encode(&[0, 4], 4).is_err());
assert!(decode(&[], 4).is_err());
assert!(detect(&[], 4).is_err());
assert!(Dpsk::new(6).is_err());
assert!(Dpsk::new(4).unwrap().map_bits(&[1, 0, 1]).is_err());
}
#[test]
fn quadrant_qam_survives_every_rotation_by_a_multiple_of_pi_2() {
let mut rng = Rng::new(9);
for m in [4usize, 16, 64, 256] {
let qq = QuadrantQam::new(m).unwrap();
let pts = qq.points();
assert_eq!(pts.len(), m);
let energy: f64 = pts.iter().map(C64::norm_sqr).sum::<f64>() / m as f64;
assert!((energy - 1.0).abs() < 1e-12);
let qam = crate::constellation::Constellation::qam(m).unwrap().normalized();
for p in &pts {
assert!(qam.points().iter().any(|v| (v - p).norm() < 1e-12));
}
let bits = crate::source::bits(400 * qq.bits_per_symbol(), &mut rng);
let x = qq.map_bits(&bits).unwrap();
assert_eq!(x.len(), 401);
for q in 0..4u32 {
let rot: Vec<C64> = x.iter().map(|v| v * C64::new(0.0, 1.0).powu(q)).collect();
assert_eq!(qq.demap_bits(&rot).unwrap(), bits, "M = {m}, rotation {q}");
}
for n in 0..m / 4 {
let a = qq.point(0, n);
for d in [C64::new(2.0, 0.0), C64::new(0.0, 2.0)] {
let b = a + d * qq.scale;
if let Some(j) = (0..m / 4).find(|&j| (qq.point(0, j) - b).norm() < 1e-9) {
assert_eq!((n ^ j).count_ones(), 1, "M = {m}");
}
}
}
}
assert!(QuadrantQam::new(8).is_err());
assert!(QuadrantQam::new(2).is_err());
assert!(QuadrantQam::new(16).unwrap().map_bits(&[1, 0, 1]).is_err());
assert!(QuadrantQam::new(16).unwrap().demap_bits(&[]).is_err());
}
#[test]
fn a_constant_rotation_does_not_change_the_decoded_bits() {
let mut rng = Rng::new(8);
for m in [2usize, 4, 8] {
let d = Dpsk::new(m).unwrap();
let bits = crate::source::bits(300 * d.bits_per_symbol(), &mut rng);
let x = d.map_bits(&bits).unwrap();
for phi in [0.1, 1.0, 2.5, -3.0, 4.2] {
let r: Vec<C64> = x.iter().map(|v| v * cis(phi)).collect();
assert_eq!(d.demap_bits(&r).unwrap(), bits, "M = {m}, φ = {phi}");
}
}
}
}
#[cfg(test)]
mod literature {
use super::*;
use crate::theory::{ber_dbpsk_coherent, ber_dpsk, differential_phase_tail};
use comnum_core::rng::Rng;
use comnum_math::{db2lin, erfc};
use std::f64::consts::PI;
fn noisy(x: &[C64], bits: usize, ebn0_db: f64, rng: &mut Rng) -> Vec<C64> {
let sd = (1.0 / (bits as f64 * db2lin(ebn0_db))).sqrt();
x.iter().map(|v| v + rng.complex_gaussian() * sd).collect()
}
fn ber(a: &[u8], b: &[u8]) -> f64 {
a.iter().zip(b).filter(|(x, y)| x != y).count() as f64 / a.len() as f64
}
#[test]
fn proakis_dbpsk_noncoherent_ber() {
let d = Dpsk::new(2).unwrap();
let mut rng = Rng::new(31);
let n = 400_000;
for ebn0_db in [6.0, 8.0] {
let bits = crate::source::bits(n, &mut rng);
let r = noisy(&d.map_bits(&bits).unwrap(), 1, ebn0_db, &mut rng);
let got = ber(&d.demap_bits(&r).unwrap(), &bits);
let want = 0.5 * (-db2lin(ebn0_db)).exp();
assert!((ber_dpsk(2, ebn0_db).unwrap() - want).abs() < 1e-12);
let tol = 3.0 / (want * n as f64).sqrt();
assert!((got / want - 1.0).abs() < tol, "{ebn0_db} dB: {got:e} vs {want:e}");
}
}
#[test]
fn proakis_coherent_detection_with_differential_decoding() {
let d = Dpsk::new(2).unwrap();
let mut rng = Rng::new(32);
let n = 400_000;
for ebn0_db in [6.0, 8.0] {
let bits = crate::source::bits(n, &mut rng);
let r = noisy(&d.map_bits(&bits).unwrap(), 1, ebn0_db, &mut rng);
let got = ber(&d.demodulate_coherent(&r).unwrap(), &bits);
let want = ber_dbpsk_coherent(ebn0_db);
let tol = 3.0 / (want * n as f64).sqrt();
assert!((got / want - 1.0).abs() < tol, "{ebn0_db} dB: {got:e} vs {want:e}");
}
}
#[test]
fn pawula_rice_roberts_1982_mdpsk_ber() {
let mut rng = Rng::new(33);
let n = 399_996; for (m, ebn0_db) in [(4usize, 8.0), (8, 11.0)] {
let d = Dpsk::new(m).unwrap();
let bits = crate::source::bits(n, &mut rng);
let r = noisy(&d.map_bits(&bits).unwrap(), d.bits_per_symbol(), ebn0_db, &mut rng);
let got = ber(&d.demap_bits(&r).unwrap(), &bits);
let want = ber_dpsk(m, ebn0_db).unwrap();
let tol = 3.0 / (want * n as f64).sqrt();
assert!((got / want - 1.0).abs() < tol, "M = {m} at {ebn0_db} dB: {got:e} vs {want:e}");
}
}
#[test]
fn pawula_rice_roberts_1982_section_iv_a() {
let pe = |m: f64, rho: f64| differential_phase_tail(PI / m, rho);
for (m, q) in [(3.0, 0.9186), (4.0, 0.9768), (8.0, 0.9987), (16.0, 0.9999)] {
let c = (PI / m).cos();
let asymptote = |rho: f64| ((1.0 + c) / (2.0 * c)).sqrt() * erfc((rho * (1.0 - c)).sqrt());
let fleck_trabka = |rho: f64| {
let x = (rho * (1.0 - c)).sqrt();
erfc(x) + x * (-x * x).exp() / (4.0 * PI.sqrt() * (rho + 0.125))
};
for (rho, tol) in [(100.0, 3e-3), (1000.0, 3e-4)] {
let r = pe(m, rho) / asymptote(rho);
assert!((r - 1.0).abs() < tol, "M = {m}, rho = {rho}: (44)/(46) = {r}");
}
let ratio = fleck_trabka(1000.0) / pe(m, 1000.0);
assert!((ratio - q).abs() < 3e-4, "M = {m}: Q = {ratio} vs {q}");
}
for (rho_db, tol) in [(10.0, 0.08), (15.0, 0.04)] {
let rho = db2lin(rho_db);
let tail = 0.5 * differential_phase_tail(3.0 * PI / 4.0, rho);
let pb = ber_dpsk(4, rho_db - comnum_math::lin2db(2.0)).unwrap();
assert!((pb / (0.5 * pe(4.0, rho) + tail) - 1.0).abs() < 1e-10, "{rho_db} dB: {pb:e}");
let approx = (-rho).exp() / (2.0 * PI * rho);
assert!((tail / approx - 1.0).abs() < tol, "{rho_db} dB: {tail:e} vs {approx:e}");
}
}
}