#![allow(clippy::unwrap_used, clippy::expect_used, reason = "test code: a failure is a test failure")]
use comnum::channel::optical::fiber::{Amplification, Ssfm};
use comnum::dsp::signal;
use comnum::hardware::optical::{Iqm, Photodiode, coherent_receiver};
use comnum::mapping::constellation::gray_code;
use comnum::math::Jacobian;
use comnum::math::{cis, power};
use comnum::prelude::*;
use comnum::sim::scenario::from_fn;
fn error_rates() -> impl Scenario {
from_fn("awgn-error-rates", |cond, rng, rec| {
let kind = cond.str("kind")?.parse::<ConstellationKind>()?;
let c = Constellation::new(kind, cond.usize("m")?)?.normalized();
let (idx, x) = source::symbols(&c, 2000, None, rng)?;
let y = awgn::awgn_ebn0(&x, cond.f64("ebn0_db")?, c.bits_per_symbol() as f64, 1.0, rng);
let bits = comnum::mapping::constellation::indices_to_bits(&idx, c.bits_per_symbol());
rec.errors("ber", metrics::bit_errors(&c.demap_bits(&y), &bits)? as u64, bits.len() as u64);
rec.errors("ser", metrics::symbol_errors(&c.decide(&y), &idx)? as u64, idx.len() as u64);
Ok(())
})
}
#[test]
fn monte_carlo_error_rates_match_the_theory() {
let grid = Grid::from_conditions(vec![
Condition::new().with("kind", "qam").with("m", 4usize).with("ebn0_db", 6.0),
Condition::new().with("kind", "qam").with("m", 16usize).with("ebn0_db", 10.0),
Condition::new().with("kind", "qam").with("m", 64usize).with("ebn0_db", 14.0),
Condition::new().with("kind", "psk").with("m", 2usize).with("ebn0_db", 6.0),
Condition::new().with("kind", "psk").with("m", 8usize).with("ebn0_db", 12.0),
Condition::new().with("kind", "pam").with("m", 4usize).with("ebn0_db", 12.0),
]);
let rule = Stop::min_errors("ber", 2000).or(Stop::max_trials(4000));
let results = Runner::new().seed(21).batch(64).stop(rule).run(&error_rates(), &grid).unwrap();
for r in &results {
let kind = r.condition.str("kind").unwrap().parse::<ConstellationKind>().unwrap();
let (m, ebn0) = (r.condition.usize("m").unwrap(), r.condition.f64("ebn0_db").unwrap());
let ber = r.metric("ber").unwrap().value();
let theory = metrics::theory_ber(kind, m, ebn0).unwrap();
assert!((ber / theory - 1.0).abs() < 0.08, "{}: BER {ber:.3e} against {theory:.3e}", r.condition.label());
let ser = r.metric("ser").unwrap().value();
let theory = metrics::theory_ser(kind, m, ebn0).unwrap();
let tol = if kind == ConstellationKind::Psk { 0.1 } else { 0.06 };
assert!((ser / theory - 1.0).abs() < tol, "{}: SER {ser:.3e} against {theory:.3e}", r.condition.label());
}
}
#[test]
fn information_rates_are_ordered_and_bounded() {
let mut rng = Rng::new(8);
let c = Constellation::qam(16).unwrap().normalized();
let mut previous = 0.0;
for snr_db in [0.0, 6.0, 12.0, 18.0] {
let (_, x) = source::symbols(&c, 40_000, None, &mut rng).unwrap();
let y = awgn::awgn(&x, snr_db, 1.0, 1.0, &mut rng);
let mi = metrics::monte_carlo_mi(&y, &x, &c, None).unwrap();
let gmi = metrics::monte_carlo_gmi(&y, &x, &c, None).unwrap().gmi;
let capacity = (1.0 + db2lin(snr_db)).log2();
assert!(gmi <= mi + 0.02, "GMI {gmi} above MI {mi} at {snr_db} dB");
assert!(mi <= capacity + 0.02 && mi <= 4.0 + 1e-9, "MI {mi} above the bounds at {snr_db} dB");
assert!(mi > previous);
previous = mi;
}
assert!(previous > 3.95);
}
#[test]
fn llr_signs_are_the_hard_decisions() {
let mut rng = Rng::new(3);
for c in [Constellation::qam(64).unwrap(), Constellation::psk(8).unwrap(), Constellation::pam(4).unwrap()] {
let c = c.normalized();
let (_, x) = source::symbols(&c, 500, None, &mut rng).unwrap();
let y = awgn::awgn(&x, 15.0, 1.0, 1.0, &mut rng);
let hard = c.demap_bits(&y);
for max_log in [true, false] {
let llr = c.llr(&y, 0.05, None, Jacobian::from_max_log(max_log)).unwrap();
let from_llr: Vec<u8> = comnum::core::signal::hard_decisions(&llr);
if max_log {
assert_eq!(from_llr, hard, "max-log LLRs are the nearest-point decisions");
} else {
let disagree = from_llr.iter().zip(&hard).filter(|(a, b)| a != b).count();
assert!(disagree * 200 < hard.len(), "{disagree} bits differ");
}
}
}
}
#[test]
fn gray_labels_differ_by_one_bit_between_neighbours() {
for (c, d_min) in [(Constellation::qam(16).unwrap(), 2.0), (Constellation::pam(8).unwrap(), 2.0)] {
let p = c.points();
let labels = gray_code(c.bits_per_symbol() as u32).unwrap();
assert_eq!(labels.len(), c.order());
for i in 0..p.len() {
for j in 0..i {
if ((p[i] - p[j]).norm() - d_min).abs() < 1e-9 {
assert_eq!((i ^ j).count_ones(), 1, "points {i} and {j} are neighbours");
}
}
}
}
}
#[test]
fn linear_split_step_is_the_linear_channel_with_ideal_gain() {
let fiber = FiberParams { gamma_w_km: 0.0, n_spans: 3, span_km: 50.0, ..FiberParams::default() };
let mut rng = Rng::new(2);
let x = rng.complex_noise(1024, 1e-3);
let fs = 64e9;
let ssfm = Ssfm {
fiber: fiber.clone(),
step_km: 5.0,
amplification: Amplification::Ideal,
raman: None,
rule: None,
check_sampling: false,
};
let mut y = x.clone();
ssfm.propagate(&mut y, fs, None).unwrap();
let reference = fiber::linear_fiber_channel(&x, &fiber, fiber.total_km(), fs);
let gain = Amplification::Ideal.field_gain(&fiber, fiber.total_km());
let err: f64 = y.iter().zip(&reference).map(|(a, b)| (a - b * gain).norm_sqr()).sum::<f64>();
assert!(err / (power(&x) * x.len() as f64) < 1e-20);
}
#[test]
fn ase_of_a_chain_of_amplifiers_adds_up() {
let fiber = FiberParams { gamma_w_km: 0.0, n_spans: 5, ..FiberParams::default() };
let fs = 64e9;
let ssfm = Ssfm::new(fiber.clone(), 10.0);
let mut rng = Rng::new(17);
let silence = vec![C64::new(0.0, 0.0); 1 << 16];
let mut y = silence;
ssfm.propagate(&mut y, fs, Some(&mut rng)).unwrap();
let amp = fiber.span_edfa();
let expected = fiber.n_spans as f64 * amp.noise_variance(fs);
assert!((power(&y) / expected - 1.0).abs() < 0.03, "{} against {expected}", power(&y));
}
#[test]
fn nonlinear_phase_of_a_constant_field_is_gamma_p_leff() {
let fiber = FiberParams { n_spans: 2, ..FiberParams::default() };
let p: f64 = 5e-3;
let x = vec![C64::new(p.sqrt(), 0.0); 256];
let ssfm = Ssfm {
fiber: fiber.clone(),
step_km: 0.1,
amplification: Amplification::Ideal,
raman: None,
rule: None,
check_sampling: false,
};
let mut y = x;
ssfm.propagate(&mut y, 32e9, None).unwrap();
let phase = y[0].arg();
let expected = fiber.n_spans as f64 * fiber.gamma_w_km * p * fiber.effective_length_km();
assert!((phase - expected).abs() < 1e-3 * expected, "{phase} against {expected}");
assert!((y[0].norm_sqr() / p - 1.0).abs() < 1e-9);
}
#[test]
fn optical_transmitter_and_coherent_receiver_recover_the_symbols() {
let c = Constellation::qam(16).unwrap().normalized();
let mut rng = Rng::new(6);
let (idx, x) = source::symbols(&c, 2000, None, &mut rng).unwrap();
let iqm = Iqm::default();
let drive: Vec<C64> = x.iter().map(|v| v * 0.05 * iqm.vpi).collect();
let laser = vec![C64::new(0.1, 0.0); x.len()];
let e = iqm.modulate(&laser, &drive).unwrap();
let lo = vec![C64::new(1.0, 0.0); x.len()];
let r = coherent_receiver(&e, &lo, &Photodiode::ideal(), 32e9, &mut rng).unwrap();
assert_eq!(c.decide(&signal::pnorm(&r)), idx);
let snr = metrics::snr_db(&signal::pnorm(&r), &x).unwrap();
assert!(snr > 25.0 && snr < 80.0, "{snr}");
}
#[test]
fn frequency_shift_and_derotation_cancel() {
let mut rng = Rng::new(1);
let x = rng.complex_noise(100, 1.0);
let y = signal::freq_shift(&x, 1e9, 32e9);
let theta: Vec<f64> = (0..100).map(|n| -2.0 * std::f64::consts::PI * 1e9 * n as f64 / 32e9).collect();
let z = comnum::dsp::sync::carrier::derotate(&y, &theta, 1.0).unwrap();
assert!(z.iter().zip(&x).all(|(a, b)| (a - b).norm() < 1e-12));
assert!((cis(0.5) * cis(-0.5) - C64::new(1.0, 0.0)).norm() < 1e-15);
}