Skip to main content

comnum_spec/blocks/
ce_ofdm.rs

1//! Constant-envelope OFDM (Thompson et al. 2008): the modulator and the phase
2//! demodulator, which records the error rates of the linear regime.
3
4use super::{Block, Extension, Frame};
5use comnum_core::Result;
6use comnum_core::rng::Rng;
7use comnum_waveform::ce_ofdm::CeOfdm;
8use comnum_waveform::ce_ofdm::gamp::Gamp;
9use comnum_waveform::ce_ofdm::theory as ce_theory;
10use comnum_waveform::ce_ofdm::{Receiver, Unwrap};
11
12/// The blocks of this module: their documentation and their builders.
13pub(super) fn blocks() -> Vec<Extension> {
14    vec![
15        builtin!(
16            "ce_ofdm_tx",
17            "Constant-envelope OFDM modulator (Thompson et al. 2008): K symbols per block on the bins 1..K of an N = 2J(K + 1) DFT, exp(j2πh x).",
18            CeOfdmSpec,
19            needs [Samples],
20            gives [],
21            |q, _, _| Ok(Box::new(CeOfdmTx(q.link()))),
22        ),
23        builtin!(
24            "ce_ofdm_rx",
25            "CE-OFDM receiver back to the symbols: the arctangent phase demodulator, the linear and enhanced receivers of Ahmed and Zeidler (2015) or GAMP (López Morales et al. 2022); records ser_theory and ber_theory when esn0_db is given.",
26            CeOfdmRxSpec,
27            needs [Samples, Constellation],
28            gives [],
29            |q, label, _| Ok(Box::new(CeOfdmRx::new(&q, label)?)),
30        ),
31    ]
32}
33
34// --------------------------------------------------------------------------- CE-OFDM
35
36block_params! {
37    struct CeOfdmSpec {
38        n_subcarriers: usize [64] >= 1, "data subcarriers K per block";
39        oversampling: usize [4] >= 1, "oversampling factor J";
40        h: f64 [required] > 0, "modulation index (phase standard deviation 2πh)";
41        n_cp: usize [0] "samples", "cyclic prefix";
42    }
43}
44
45impl CeOfdmSpec {
46    fn link(&self) -> CeOfdm {
47        CeOfdm {
48            n_subcarriers: self.n_subcarriers,
49            oversampling: self.oversampling,
50            h: self.h,
51            n_cp: self.n_cp,
52            receiver: Receiver::default(),
53            sample_rate: comnum_core::units::Hertz(1.0),
54        }
55    }
56}
57
58struct CeOfdmTx(CeOfdm);
59
60impl Block for CeOfdmTx {
61    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
62        s.require_one_sps("ce_ofdm_tx")?;
63        s.samples = self.0.modulate(&s.samples)?;
64        s.sps = self.0.sps();
65        Ok(())
66    }
67}
68
69block_params! {
70    struct CeOfdmRxSpec {
71        n_subcarriers: usize [64] >= 1, "data subcarriers K (as the modulator)";
72        oversampling: usize [4] >= 1, "oversampling factor J (as the modulator)";
73        h: f64 [required] > 0, "modulation index (as the modulator)";
74        n_cp: usize [0] "samples", "cyclic prefix (as the modulator)";
75        unwrap: String ["none"] one_of ["none", "stream"],
76            "none (principal phase) or stream (unwrapped along the stream, the phase unwrapper of the paper)";
77        receiver: String ["arctangent"] one_of ["arctangent", "linear", "enhanced", "gamp"],
78            "arctangent (phase demodulator), linear (Im r) or enhanced (Im r (2 - Re r)) of Ahmed and Zeidler 2015, or gamp (near-optimum message passing, needs esn0_db)";
79        esn0_db: Option<f64> [absent] "dB",
80            "E_s/N_0 of the channel per data symbol: records ser_theory and ber_theory (the linear regime for arctangent and gamp, where it is the optimum asymptotic rate of QPSK; Bussgang's analysis for linear and enhanced), and sets the noise variance of gamp";
81    }
82}
83
84struct CeOfdmRx {
85    ce: CeOfdm,
86    /// `None` for GAMP.
87    receiver: Option<Receiver>,
88    label: String,
89    esn0_db: Option<f64>,
90}
91
92impl CeOfdmRx {
93    fn new(p: &CeOfdmRxSpec, label: String) -> Result<CeOfdmRx> {
94        let ce =
95            CeOfdmSpec { n_subcarriers: p.n_subcarriers, oversampling: p.oversampling, h: p.h, n_cp: p.n_cp }.link();
96        let receiver = match p.receiver.as_str() {
97            "gamp" => {
98                if p.esn0_db.is_none() {
99                    return Err(comnum_core::Error::Config("ce_ofdm_rx: the gamp receiver needs esn0_db".into()));
100                }
101                None
102            }
103            "arctangent" => Some(Receiver::Arctangent(p.unwrap.parse::<Unwrap>()?)),
104            other => Some(other.parse::<Receiver>()?),
105        };
106        Ok(CeOfdmRx { ce, receiver, label, esn0_db: p.esn0_db })
107    }
108}
109
110impl Block for CeOfdmRx {
111    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
112        let c = s.constellation("ce_ofdm_rx")?.clone();
113        if let Some(es) = self.esn0_db {
114            let (m, kind) = (c.order(), c.kind());
115            let ce =
116                CeOfdm { receiver: self.receiver.unwrap_or(Receiver::Arctangent(Unwrap::None)), ..self.ce.clone() };
117            s.record(&self.label, "ser_theory", ce_theory::ser_receiver(&ce, kind, m, es)?);
118            s.record(&self.label, "ber_theory", ce_theory::ber_receiver(&ce, kind, m, es)?);
119        }
120        s.samples = match (self.receiver, self.esn0_db) {
121            (Some(r), _) => self.ce.demodulate_with_receiver(&s.samples, r)?,
122            (None, Some(es)) => Gamp::default().detect(&self.ce, &s.samples, &c, self.ce.noise_variance(es))?,
123            (None, None) => unreachable!("checked by new"),
124        };
125        s.sps = 1.0;
126        Ok(())
127    }
128}