1use 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
12pub(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
34block_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 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}