Skip to main content

comnum_spec/blocks/
channel.rs

1//! Channels and impairments of one or several streams: noise, phase noise, frequency offset, linear channels, IQ imbalance, converters.
2
3use super::schema::ParamType;
4use super::{Block, Extension, Frame};
5use comnum_channel::awgn::{ebn0_noise_variance, snr_noise_variance};
6use comnum_core::Error;
7use comnum_core::Result;
8use comnum_core::ensure;
9use comnum_core::rng::Rng;
10use comnum_dsp::filter;
11use comnum_hardware::converter::{Adc, Dac, Quantizer};
12use comnum_hardware::iq;
13use comnum_hardware::oscillator::{PhaseOrigin, frequency_offset, increment_sd, sampling_offset, wiener_phase};
14use comnum_math::C64;
15use comnum_math::cis;
16use serde::Deserialize;
17use serde_json::Value as Json;
18use std::f64::consts::PI;
19
20/// The blocks of this module: their documentation and their builders.
21pub(super) fn blocks() -> Vec<Extension> {
22    vec![
23        builtin!(
24            "awgn",
25            "Add white Gaussian noise for an SNR in a bandwidth.",
26            AwgnSpec,
27            needs[Samples],
28            gives[Noise],
29            |q, _, _| Ok(Box::new(Awgn::new(&q))),
30        ),
31        builtin!(
32            "awgn_ebn0",
33            "Add white Gaussian noise for an E_b/N_0 (matched-filter definition).",
34            AwgnEbn0,
35            needs [Samples, Constellation],
36            gives [Noise],
37            |q, _, _| Ok(Box::new(q)),
38        ),
39        builtin!(
40            "phase_noise", "Wiener phase noise.", PhaseNoiseSpec, needs [Samples], gives [],
41            |q, _, _| Ok(Box::new(PhaseNoise::new(&q)?)),
42        ),
43        builtin!(
44            "cfo", "Carrier frequency offset.", CfoSpec, needs [Samples], gives [],
45            |q, _, _| Ok(Box::new(Cfo::new(&q)?)),
46        ),
47        builtin!(
48            "clock_offset", "Sampling clock offset: the samples at m (1 + ζ) T (cubic interpolation).", ClockOffsetSpec,
49            needs [Samples], gives [],
50            |q, _, _| Ok(Box::new(q)),
51        ),
52        builtin!(
53            per_polarization "fir", "Linear channel with complex taps.", FirSpec, needs [Samples], gives [],
54            |q, _, _| Ok(Box::new(Fir::new(&q)?)),
55        ),
56        builtin!(
57            per_polarization "rx_iq", "Receiver IQ imbalance [Re y; Im y] = T [Re x; Im x].", RxIq, needs [Samples], gives [],
58            |q, _, _| Ok(Box::new(q)),
59        ),
60        builtin!(
61            per_polarization "tx_iq", "Transmitter IQ imbalance y = x + β x*.", TxIq, needs [Samples], gives [],
62            |q, _, _| Ok(Box::new(q)),
63        ),
64        builtin!(
65            per_polarization "adc",
66            "Analogue-to-digital converter: front-end bandwidth, clock jitter, ENOB noise and uniform quantisation with clipping.",
67            AdcSpec,
68            needs[Samples],
69            gives[],
70            |q, _, _| Ok(Box::new(q)),
71        ),
72        builtin!(
73            per_polarization "dac",
74            "Digital-to-analogue converter: uniform quantisation with clipping and output bandwidth.",
75            DacSpec,
76            needs[Samples],
77            gives[],
78            |q, _, _| Ok(Box::new(q)),
79        ),
80    ]
81}
82
83// ----------------------------------------------------------------------- channels
84
85block_params! {
86    struct AwgnSpec {
87        snr_db: f64 [required] "dB", "signal-to-noise ratio";
88        reference: NoiseReference ["signal"] one_of ["signal", "unit"],
89            "signal: measured power (OptiCommPy); unit: E_s = 1 whatever the samples, σ² = 10^(-SNR/10) per symbol";
90        bandwidth_hz: Option<f64> [absent "symbol rate"] "Hz" > 0, "noise bandwidth B: σ² = (f_s/B) P / SNR";
91    }
92}
93
94/// The power the SNR of `awgn` refers to.
95#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
96#[serde(rename_all = "lowercase")]
97enum NoiseReference {
98    /// The measured power of the signal.
99    Signal,
100    /// Unit-energy symbols.
101    Unit,
102}
103
104impl ParamType for NoiseReference {
105    fn schema() -> Json {
106        String::schema()
107    }
108}
109
110struct Awgn {
111    snr_db: f64,
112    unit: bool,
113    bandwidth: Option<f64>,
114}
115
116impl Awgn {
117    fn new(p: &AwgnSpec) -> Awgn {
118        Awgn { snr_db: p.snr_db, unit: p.reference == NoiseReference::Unit, bandwidth: p.bandwidth_hz }
119    }
120}
121
122impl Block for Awgn {
123    fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
124        let b = self.bandwidth.unwrap_or(s.symbol_rate);
125        // the power of the samples (that of the channel output before fading, if any)
126        let p = if self.unit { 1.0 / s.sps } else { s.reference_power() };
127        add_noise(s, snr_noise_variance(p, self.snr_db, s.fs(), b), rng);
128        Ok(())
129    }
130}
131
132/// Add complex white noise of variance `noise_var` to every antenna and record it.
133fn add_noise(s: &mut Frame, noise_var: f64, rng: &mut Rng) {
134    for v in s.streams_mut() {
135        comnum_channel::awgn::add_noise(v, noise_var, rng);
136    }
137    s.channel.noise_var = Some(noise_var);
138}
139
140block_params! {
141    struct AwgnEbn0 {
142        ebn0_db: f64 [required] "dB", "E_b/N_0";
143    }
144}
145
146impl Block for AwgnEbn0 {
147    fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
148        // information bits per symbol: log2 M times the code rate, or the entropy of a
149        // shaped source
150        let b = match (s.coding.code_rate, &s.pmf) {
151            (Some(r), _) => s.constellation("awgn_ebn0")?.bits_per_symbol() as f64 * r,
152            (None, Some(p)) => comnum_metrics::entropy(p),
153            (None, None) => s.constellation("awgn_ebn0")?.bits_per_symbol() as f64,
154        };
155        // the power of the samples over every antenna, before fading if any (that of
156        // comnum_channel::awgn::awgn_ebn0 for a single stream)
157        let p = s.reference_power();
158        add_noise(s, ebn0_noise_variance(p, self.ebn0_db, b, s.sps), rng);
159        Ok(())
160    }
161}
162
163block_params! {
164    struct PhaseNoiseSpec {
165        linewidth_hz: Option<f64> [absent] "Hz" >= 0, "laser linewidth, increments of variance 2πΔν/f_s";
166        sigma_rad: Option<f64> [absent] "rad" >= 0, "increment standard deviation per sample, instead of the linewidth";
167        origin: String ["zero"] one_of ["zero", "step"],
168            "zero: φ[0] = 0, the first sample is not rotated (OptiCommPy); step: φ[0] = Δ[0], the first sample is rotated too";
169    }
170}
171
172struct PhaseNoise {
173    linewidth_hz: Option<f64>,
174    sigma_rad: Option<f64>,
175    origin: PhaseOrigin,
176}
177
178impl PhaseNoise {
179    fn new(p: &PhaseNoiseSpec) -> Result<PhaseNoise> {
180        ensure(p.linewidth_hz.is_some() != p.sigma_rad.is_some(), || {
181            "phase_noise: give linewidth_hz or sigma_rad".into()
182        })?;
183        let origin = p.origin.parse().map_err(|e: Error| e.context("phase_noise"))?;
184        Ok(PhaseNoise { linewidth_hz: p.linewidth_hz, sigma_rad: p.sigma_rad, origin })
185    }
186}
187
188impl Block for PhaseNoise {
189    fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
190        let sd = match (self.linewidth_hz, self.sigma_rad) {
191            (Some(lw), _) => increment_sd(lw, s.fs())?,
192            (_, Some(sg)) => sg,
193            _ => 0.0,
194        };
195        let rot: Vec<C64> = wiener_phase(sd, s.samples.len(), self.origin, rng).into_iter().map(cis).collect();
196        // one oscillator for every antenna
197        for x in s.streams_mut() {
198            for (v, r) in x.iter_mut().zip(&rot) {
199                *v *= r;
200            }
201        }
202        Ok(())
203    }
204}
205
206block_params! {
207    struct CfoSpec {
208        offset_hz: Option<f64> [absent] "Hz", "offset";
209        omega_rad: Option<f64> [absent] "rad/sample", "offset per sample, instead of offset_hz";
210        centred: bool [false], "phase zero at the middle of the block, e^{jω(n - N/2)}, instead of the first sample";
211    }
212}
213
214struct Cfo {
215    offset_hz: Option<f64>,
216    omega_rad: Option<f64>,
217    centred: bool,
218}
219
220impl Cfo {
221    fn new(p: &CfoSpec) -> Result<Cfo> {
222        ensure(p.offset_hz.is_some() != p.omega_rad.is_some(), || "cfo: give offset_hz or omega_rad".into())?;
223        Ok(Cfo { offset_hz: p.offset_hz, omega_rad: p.omega_rad, centred: p.centred })
224    }
225}
226
227impl Block for Cfo {
228    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
229        let w = self.omega_rad.unwrap_or_else(|| 2.0 * PI * self.offset_hz.unwrap_or(0.0) / s.fs());
230        for x in s.streams_mut() {
231            frequency_offset(x, w, self.centred);
232        }
233        Ok(())
234    }
235}
236
237block_params! {
238    struct ClockOffsetSpec {
239        ppm: f64 [required] "ppm", "the relative offset ζ = (T' - T)/T of the sampling period of the receiver, positive for a slower clock";
240    }
241}
242
243impl Block for ClockOffsetSpec {
244    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
245        for x in s.streams_mut() {
246            let y = sampling_offset(x, self.ppm * 1e-6);
247            x.copy_from_slice(&y);
248        }
249        Ok(())
250    }
251}
252
253block_params! {
254    struct FirSpec {
255        h: Vec<ComplexNumber> [required], "taps, [[re, im], …] or real numbers";
256        alignment: Alignment ["causal"] one_of ["causal", "centred", "centered"],
257            "causal: y[n] = Σ h[k] x[n-k], the delay of the taps kept; centred: the taps centred on the samples (OptiCommPy firFilter)";
258    }
259}
260
261/// The alignment of the output of `fir` on its input.
262#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
263#[serde(rename_all = "lowercase")]
264enum Alignment {
265    /// $y\[n\] = \sum_{k} h\[k\] x\[n-k\]$.
266    Causal,
267    /// Centred on the middle tap (OptiCommPy `firFilter`).
268    #[serde(alias = "centered")]
269    Centred,
270}
271
272impl ParamType for Alignment {
273    fn schema() -> Json {
274        String::schema()
275    }
276}
277
278json_param! {
279    /// A complex number: `[re, im]` or a real number.
280    ComplexNumber,
281    serde_json::json!({"anyOf": [{"type": "number"}, {"type": "array", "items": {"type": "number"}, "minItems": 2, "maxItems": 2}]})
282}
283
284struct Fir {
285    h: Vec<C64>,
286    causal: bool,
287}
288
289/// Parse a complex number written as `[re, im]` or a real number.
290pub(crate) fn complex(v: &Json) -> Result<C64> {
291    match v {
292        Json::Number(n) => Ok(C64::new(n.as_f64().unwrap_or(f64::NAN), 0.0)),
293        Json::Array(a) if a.len() == 2 => match (a[0].as_f64(), a[1].as_f64()) {
294            (Some(r), Some(i)) => Ok(C64::new(r, i)),
295            _ => Err(Error::Config(format!("a complex number is [re, im], got {v}"))),
296        },
297        other => Err(Error::Config(format!("a complex number is [re, im] or a real number, got {other}"))),
298    }
299}
300
301impl Fir {
302    fn new(p: &FirSpec) -> Result<Fir> {
303        ensure(!p.h.is_empty(), || "fir: h needs at least one tap".into())?;
304        let h = p.h.iter().map(|v| complex(&v.0)).collect::<Result<Vec<_>>>()?;
305        Ok(Fir { h, causal: p.alignment == Alignment::Causal })
306    }
307}
308
309impl Block for Fir {
310    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
311        s.samples = if self.causal {
312            let mut y = filter::convolve_full(&self.h, &s.samples);
313            y.truncate(s.samples.len());
314            y
315        } else {
316            filter::fir_filter_complex(&self.h, &s.samples)
317        };
318        Ok(())
319    }
320}
321
322block_params! {
323    struct RxIq {
324        t: [f64; 4] [required], "row-major real 2×2 matrix [t00, t01, t10, t11]";
325    }
326}
327
328impl Block for RxIq {
329    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
330        let t = self.t;
331        iq::RxIq { t: [[t[0], t[1]], [t[2], t[3]]] }.apply(&mut s.samples);
332        Ok(())
333    }
334}
335
336block_params! {
337    struct TxIq {
338        beta: [f64; 2] [required], "[re, im] of β";
339    }
340}
341
342impl Block for TxIq {
343    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
344        iq::TxIq { beta: C64::new(self.beta[0], self.beta[1]) }.apply(&mut s.samples);
345        Ok(())
346    }
347}
348
349// ---------------------------------------------------------------------- converters
350
351block_params! {
352    struct AdcSpec {
353        bits: u32 [required] "bits" >= 1, "resolution N (1 to 24)";
354        full_scale: Option<f64> [absent "loading × RMS"] > 0, "full scale A of each dimension (levels ±(A - Δ/2), Δ = 2A/2^N)";
355        loading: Option<f64> [absent "optimal for a Gaussian input"] > 0,
356            "full scale in RMS values of one dimension of the samples (an automatic gain control)";
357        enob: Option<f64> [absent "bits"] > 0, "effective number of bits (≤ bits)";
358        jitter_s: f64 [0.0] "s" >= 0, "RMS sampling-clock jitter";
359        bandwidth_hz: Option<f64> [absent "unlimited"] "Hz" > 0, "3 dB bandwidth of the front end (Gaussian)";
360    }
361}
362
363impl Block for AdcSpec {
364    fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
365        let quantizer = Quantizer::for_signal(self.bits, self.full_scale, self.loading, &s.samples)
366            .map_err(|e| e.context("adc"))?;
367        let adc = Adc { quantizer, enob: self.enob, jitter_s: self.jitter_s, bandwidth_hz: self.bandwidth_hz };
368        s.samples = adc.convert(&s.samples, s.fs(), rng).map_err(|e| e.context("adc"))?;
369        Ok(())
370    }
371}
372
373block_params! {
374    struct DacSpec {
375        bits: u32 [required] "bits" >= 1, "resolution N (1 to 24)";
376        full_scale: Option<f64> [absent "loading × RMS"] > 0, "full scale A of each dimension";
377        loading: Option<f64> [absent "optimal for a Gaussian input"] > 0,
378            "full scale in RMS values of one dimension of the samples";
379        bandwidth_hz: Option<f64> [absent "unlimited"] "Hz" > 0, "3 dB bandwidth of the output (Gaussian)";
380    }
381}
382
383impl Block for DacSpec {
384    fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
385        let quantizer = Quantizer::for_signal(self.bits, self.full_scale, self.loading, &s.samples)
386            .map_err(|e| e.context("dac"))?;
387        let dac = Dac { quantizer, hold: 1, bandwidth_hz: self.bandwidth_hz };
388        s.samples = dac.convert(&s.samples, s.fs()).map_err(|e| e.context("dac"))?;
389        Ok(())
390    }
391}