1use 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
20pub(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
83block_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#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
96#[serde(rename_all = "lowercase")]
97enum NoiseReference {
98 Signal,
100 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 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
132fn 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 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 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 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#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
263#[serde(rename_all = "lowercase")]
264enum Alignment {
265 Causal,
267 #[serde(alias = "centered")]
269 Centred,
270}
271
272impl ParamType for Alignment {
273 fn schema() -> Json {
274 String::schema()
275 }
276}
277
278json_param! {
279 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
289pub(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
349block_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}