1use super::schema::ParamType;
4use super::{Block, Extension, Frame, NumberOrList, part, per_pol};
5use comnum_core::Error;
6use comnum_core::Result;
7use comnum_core::ensure;
8use comnum_core::rng::Rng;
9use comnum_dsp::adaptive::UpdateRule;
10use comnum_dsp::equalizer::Algorithm;
11use comnum_dsp::equalizer::Singularity;
12use comnum_dsp::equalizer::{AdaptiveEqualizer, EqualizerOutput};
13use comnum_dsp::filter;
14use comnum_dsp::filter::PulseShape;
15use comnum_dsp::resample;
16use comnum_dsp::signal;
17use comnum_dsp::sync::carrier;
18use comnum_dsp::sync::frame;
19use comnum_dsp::sync::timing::GardnerLoop;
20use comnum_math::C64;
21use comnum_math::dbm2w;
22use serde::Deserialize;
23use serde_json::Value as Json;
24use std::f64::consts::{FRAC_PI_2, PI};
25
26pub(super) fn blocks() -> Vec<Extension> {
28 vec![
29 builtin!(
30 per_polarization "pulse_shape",
31 "Upsample and filter with a transmit pulse, then normalise to unit power.",
32 PulseSpec,
33 needs [Samples],
34 gives [],
35 |q, _, _| Ok(Box::new(PulseShaping::new(&q)?)),
36 ),
37 builtin!(
38 "normalize", "Scale the samples to a mean power.", Normalize, needs [Samples], gives [],
39 |q, _, _| Ok(Box::new(q)),
40 ),
41 builtin!(
42 per_polarization "resample",
43 "Band-limited resampling of the block (FFT) to a number of samples per symbol.",
44 Resample,
45 needs [Samples],
46 gives [],
47 |q, _, _| Ok(Box::new(q)),
48 ),
49 builtin!(
50 per_polarization "matched_filter",
51 "Filter with a receive pulse at the current samples per symbol.",
52 MatchedSpec,
53 needs [Samples],
54 gives [],
55 |q, _, _| Ok(Box::new(MatchedFilter::new(q)?)),
56 ),
57 builtin!(
58 per_polarization "decimate", "Reduce the samples per symbol.", DecimateSpec, needs [Samples], gives [],
59 |q, _, _| Ok(Box::new(Decimate::new(&q))),
60 ),
61 builtin!(
62 per_polarization "gardner", "Gardner clock recovery at 2 samples per symbol.", Gardner, needs [Samples], gives [],
63 |q, _, _| Ok(Box::new(q)),
64 ),
65 builtin!(
66 "adaptive_equalizer",
67 "Adaptive FIR equaliser, output at 1 sample per symbol; the n × n butterfly over the polarisations or antennas.",
68 EqualizerSpec,
69 needs [Samples, Constellation],
70 gives [],
71 |q, label, _| Ok(Box::new(Equalizer::new(&q, label)?)),
72 ),
73 builtin!(
74 per_polarization "foe",
75 "Frequency-offset estimation by the M-th power and correction; records foe_hz.",
76 FoeSpec,
77 needs [Samples],
78 gives [],
79 |q, label, _| Ok(Box::new(Foe { p: q, label })),
80 ),
81 builtin!(
82 per_polarization "bps",
83 "Blind phase search, unwrapped with a period of π/2, and correction.",
84 Bps,
85 needs [Samples, Constellation],
86 gives [],
87 |q, _, _| Ok(Box::new(q)),
88 ),
89 builtin!(
90 per_polarization "viterbi", "Viterbi–Viterbi phase estimation and correction.", Viterbi, needs [Samples], gives [],
91 |q, _, _| Ok(Box::new(q)),
92 ),
93 builtin!(
94 per_polarization "ddpll", "Decision-directed phase-locked loop.", Ddpll, needs [Samples, Constellation], gives [],
95 |q, _, _| Ok(Box::new(q)),
96 ),
97 builtin!(
98 "frame_sync",
99 "Delay and rotation on known symbols, output aligned on the transmitted symbols; records delay and quality.",
100 FrameSyncSpec,
101 needs [Samples, Symbols],
102 gives [],
103 |q, label, _| Ok(Box::new(FrameSync { p: q, label })),
104 ),
105 ]
106}
107
108block_params! {
109 #[derive(Clone, Debug)]
111 pub(super) struct PulseSettings {
112 kind: String ["rrc"] one_of ["rect", "nrz", "rc", "rrc", "rz"], "the pulse";
113 rolloff: f64 [0.1] >= 0 <= 1, "roll-off of rc and rrc";
114 n_taps: usize [1025] >= 1, "taps of rc and rrc (odd: on the sampling instants)";
115 }
116}
117
118impl PulseSettings {
119 pub(super) fn taps(&self, sps: usize) -> Result<Vec<f64>> {
121 ensure(sps >= 1, || "sps must be positive".into())?;
122 filter::pulse_shape(PulseShape::from_name(&self.kind, self.rolloff)?, sps, self.n_taps)
123 }
124}
125
126block_params! {
127 struct PulseSpec {
128 pulse: PulseSettings [flatten], "the transmit pulse";
129 sps: usize [2] >= 1, "samples per symbol";
130 }
131}
132
133struct PulseShaping {
134 taps: Vec<f64>,
135 sps: usize,
136}
137
138impl PulseShaping {
139 fn new(p: &PulseSpec) -> Result<PulseShaping> {
140 Ok(PulseShaping { taps: p.pulse.taps(p.sps)?, sps: p.sps })
141 }
142}
143
144impl Block for PulseShaping {
145 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
146 let k = s.int_sps("pulse_shape")?;
147 ensure(k == 1, || "pulse_shape expects symbols (1 sample per symbol)".into())?;
148 s.samples = signal::pnorm(&filter::upfirdn(&self.taps, &s.samples, self.sps, 1));
149 s.sps = self.sps as f64;
150 Ok(())
151 }
152}
153
154block_params! {
155 struct Normalize {
156 power_dbm: Option<f64> [absent "unit power"] "dBm", "mean power";
157 }
158}
159
160impl Block for Normalize {
161 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
165 let a = self.power_dbm.map_or(1.0, |p| dbm2w(p).sqrt());
166 let rms = s.mean_power().sqrt();
167 s.streams_mut().flatten().for_each(|v| *v = if rms == 0.0 { *v * a } else { *v / rms * a });
168 Ok(())
169 }
170}
171
172block_params! {
173 struct MatchedSpec {
174 pulse: PulseSettings [flatten], "the receive pulse";
175 }
176}
177
178struct MatchedFilter(PulseSettings);
180
181impl MatchedFilter {
182 fn new(p: MatchedSpec) -> Result<MatchedFilter> {
183 p.pulse.taps(2)?;
184 Ok(MatchedFilter(p.pulse))
185 }
186}
187
188impl Block for MatchedFilter {
189 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
190 let sps = s.int_sps("matched_filter")?;
191 let h = self.0.taps(sps)?;
192 s.samples = filter::fir_filter(&h, &s.samples);
193 Ok(())
194 }
195}
196
197block_params! {
198 struct DecimateSpec {
199 sps_out: usize [1] >= 1, "output samples per symbol";
200 method: DecimationMethod ["max_variance"] one_of ["max_variance", "offset"],
201 "max_variance (OptiCommPy decimate) or offset";
202 offset: usize [0], "sampling phase of the offset method";
203 }
204}
205
206#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
208#[serde(rename_all = "snake_case")]
209enum DecimationMethod {
210 MaxVariance,
212 Offset,
214}
215
216impl ParamType for DecimationMethod {
217 fn schema() -> Json {
218 String::schema()
219 }
220}
221
222struct Decimate {
223 sps_out: usize,
224 offset: Option<usize>,
225}
226
227impl Decimate {
228 fn new(p: &DecimateSpec) -> Decimate {
229 let offset = match p.method {
230 DecimationMethod::MaxVariance => None,
231 DecimationMethod::Offset => Some(p.offset),
232 };
233 Decimate { sps_out: p.sps_out, offset }
234 }
235}
236
237impl Block for Decimate {
238 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
239 let sps = s.int_sps("decimate")?;
240 ensure(sps % self.sps_out == 0, || format!("decimate: {} does not divide {sps}", self.sps_out))?;
241 let n = s.samples.len() / sps * sps;
242 s.samples.truncate(n);
243 s.samples = match self.offset {
244 None => resample::decimate(&s.samples, sps, self.sps_out)?,
245 Some(o) => resample::downsample(&s.samples, sps / self.sps_out, o),
246 };
247 s.sps = self.sps_out as f64;
248 Ok(())
249 }
250}
251
252block_params! {
253 struct Gardner {
254 kp: f64 [1e-3], "proportional gain";
255 ki: f64 [1e-6], "integral gain";
256 nyquist: bool [true], "detector for Nyquist pulses";
257 }
258}
259
260impl Block for Gardner {
261 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
262 ensure(s.int_sps("gardner")? == 2, || "gardner needs 2 samples per symbol".into())?;
263 let g = GardnerLoop { kp: self.kp, ki: self.ki, nyquist: self.nyquist, max_ppm: 500.0 };
264 let mut y = g.run(&signal::pnorm(&s.samples)).signal;
265 y.truncate(y.len() / 2 * 2);
266 s.samples = y;
267 Ok(())
268 }
269}
270
271block_params! {
272 struct EqualizerSpec {
273 algorithm: Schedule ["cma"], "cma, rde, nlms, dd-lms, or a schedule [[algorithm, symbols], …]";
274 n_taps: usize [15] >= 1, "taps";
275 mu: NumberOrList [1e-3] > 0,
276 "step size, or one per stage of the schedule (the last one holds for the following stages)";
277 passes: usize [1] >= 1, "passes over the block";
278 update_rule: Option<String> [absent] one_of ["lms", "nlms", "sign"],
279 "the update rule of every stage instead of that of its algorithm: sign for the sign-error update";
280 kikuchi: Option<usize> [absent],
281 "2 × 2: Kikuchi's initialisation against the singularity, output 0 alone during this number of symbols, then output 1 as its unitary complement; records correlation and singular";
282 }
283}
284
285json_param! {
286 Schedule,
288 serde_json::json!({"anyOf": [
289 {"type": "string"},
290 {"type": "array", "items": {"type": "array", "prefixItems": [{"type": "string"}, {"type": "integer", "minimum": 0}], "minItems": 2, "maxItems": 2}}
291 ]})
292}
293
294struct Equalizer {
295 schedule: Vec<(Algorithm, usize)>,
296 n_taps: usize,
297 mu: Vec<f64>,
298 passes: usize,
299 singularity: Singularity,
300 update_rule: Option<UpdateRule>,
301 label: String,
302}
303
304impl Equalizer {
305 fn new(p: &EqualizerSpec, label: String) -> Result<Equalizer> {
306 let schedule = match &p.algorithm.0 {
307 Json::String(a) => vec![(a.parse()?, usize::MAX)],
308 Json::Array(steps) => steps
309 .iter()
310 .map(|st| match st {
311 Json::Array(pair) if pair.len() == 2 => match (pair[0].as_str(), pair[1].as_u64()) {
312 (Some(a), Some(n)) => Ok((a.parse()?, n as usize)),
313 _ => Err(Error::Config(format!("a schedule step is [algorithm, symbols], got {st}"))),
314 },
315 _ => Err(Error::Config(format!("a schedule step is [algorithm, symbols], got {st}"))),
316 })
317 .collect::<Result<_>>()?,
318 other => return Err(Error::Config(format!("algorithm must be a name or a schedule, got {other}"))),
319 };
320 let singularity = p.kikuchi.map_or(Singularity::None, Singularity::Kikuchi);
321 let mu: Vec<f64> = match &p.mu.0 {
322 Json::Array(v) => v
323 .iter()
324 .map(|x| x.as_f64().ok_or_else(|| Error::Config("adaptive_equalizer: mu must be numbers".into())))
325 .collect::<Result<_>>()?,
326 v => vec![v.as_f64().ok_or_else(|| Error::Config("adaptive_equalizer: mu must be a number".into()))?],
327 };
328 ensure(!mu.is_empty() && mu.iter().all(|m| *m > 0.0), || {
329 "adaptive_equalizer: mu must be positive step sizes".into()
330 })?;
331 let update_rule = p.update_rule.as_deref().map(str::parse).transpose()?;
332 Ok(Equalizer { schedule, n_taps: p.n_taps, mu, passes: p.passes, singularity, update_rule, label })
333 }
334}
335
336impl Block for Equalizer {
337 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
338 let sps = s.int_sps("adaptive_equalizer")?;
339 let eq = AdaptiveEqualizer {
340 n_taps: self.n_taps,
341 sps,
342 mu: self.mu[0],
343 stage_mu: self.mu.clone(),
344 schedule: self.schedule.clone(),
345 passes: self.passes,
346 singularity: self.singularity,
347 update_rule: self.update_rule,
348 };
349 let c = s.constellation("adaptive_equalizer")?.clone();
350 let n = s.n_streams();
352 let needs_ref = self.schedule.iter().any(|(a, _)| *a == Algorithm::Nlms);
353 let reference: Vec<Vec<C64>> = if needs_ref {
354 (0..n).map(|i| part(&s.symbols, i, n, "symbols")).collect::<Result<_>>()?
355 } else {
356 Vec::new()
357 };
358 let x: Vec<Vec<C64>> = s.streams().map(|v| signal::pnorm(v)).collect();
359 let out = eq.run(&x, if needs_ref { Some(&reference) } else { None }, c.points())?;
360 if let Some(rho) = out.output_correlation() {
361 s.record(&self.label, "correlation", rho);
362 s.record(&self.label, "singular", f64::from(u8::from(rho > EqualizerOutput::SINGULAR_CORRELATION)));
363 }
364 s.set_streams(out.symbols);
365 s.sps = 1.0;
366 Ok(())
367 }
368}
369
370block_params! {
371 struct FoeSpec {
372 m: u32 [4] >= 1, "rotational symmetry";
373 refined: bool [true], "refine the periodogram peak between the bins";
374 }
375}
376
377struct Foe {
378 p: FoeSpec,
379 label: String,
380}
381
382impl Block for Foe {
383 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
384 let fs = s.fs();
385 let fo = if self.p.refined {
386 carrier::fourth_power_foe_refined(&s.samples, fs, self.p.m)?
387 } else {
388 carrier::fourth_power_foe(&s.samples, fs, self.p.m)?
389 };
390 s.samples = signal::freq_shift(&s.samples, -fo, fs);
391 s.record(&self.label, "foe_hz", fo);
392 Ok(())
393 }
394}
395
396block_params! {
397 struct Bps {
398 n: usize [35] "symbols", "half window";
399 b: usize [64] >= 1, "test phases";
400 }
401}
402
403impl Block for Bps {
404 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
405 let pts = s.constellation("bps")?.points().to_vec();
406 let z = signal::pnorm(&s.samples);
407 let theta = carrier::unwrap(&carrier::bps(&z, self.n, &pts, self.b)?, FRAC_PI_2);
408 s.samples = carrier::derotate(&z, &theta, 1.0)?;
409 Ok(())
410 }
411}
412
413block_params! {
414 struct Viterbi {
415 n: usize [35] "symbols", "window";
416 m: u32 [4] >= 1, "power";
417 }
418}
419
420impl Block for Viterbi {
421 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
422 let theta = carrier::viterbi_viterbi(&s.samples, self.n, self.m)?;
423 s.samples = carrier::derotate(&s.samples, &theta, 1.0)?;
424 Ok(())
425 }
426}
427
428block_params! {
429 struct Ddpll {
430 kv: f64 [0.1], "VCO gain";
431 tau1_s: f64 [1.0 / (2.0 * PI * 10e6) => "1/(2π 10 MHz)"] "s" > 0, "loop-filter time constant";
432 tau2_s: f64 [1.0 / (2.0 * PI * 10e6) => "1/(2π 10 MHz)"] "s" > 0, "loop-filter time constant";
433 }
434}
435
436impl Block for Ddpll {
437 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
438 let pts = s.constellation("ddpll")?.points().to_vec();
439 let z = signal::pnorm(&s.samples);
440 let theta = carrier::ddpll(&z, 1.0 / s.symbol_rate, self.kv, self.tau1_s, self.tau2_s, &pts, None)?;
441 s.samples = carrier::derotate(&z, &theta, 1.0)?;
442 Ok(())
443 }
444}
445
446block_params! {
447 struct FrameSyncSpec {
448 start: usize [0], "first known symbol";
449 length: usize [512] >= 1, "number of known symbols";
450 max_delay: usize [64] "symbols", "largest delay searched";
451 symmetry: u32 [4], "rotational symmetry of the constellation; 0 removes the exact phase";
452 exclude: bool [true], "exclude the known symbols from the metrics";
453 }
454}
455
456struct FrameSync {
457 p: FrameSyncSpec,
458 label: String,
459}
460
461impl Block for FrameSync {
462 fn apply(&self, s: &mut Frame, rng: &mut Rng) -> Result<()> {
463 if !s.layout.polarisations || s.n_streams() != 2 {
464 return self.single(s);
465 }
466 s.require_one_sps("frame_sync")?;
469 let p = &self.p;
470 let n = s.symbols.len() / 2;
471 let symmetry = if p.symmetry == 0 { None } else { Some(p.symmetry) };
472 let q = |z: &[C64], pol: usize| {
473 frame::frame_sync(z, &s.symbols[pol * n..(pol + 1) * n], p.start..p.start + p.length, p.max_delay, symmetry)
474 .map_or(0.0, |r| r.quality)
475 };
476 let direct = q(&s.samples, 0) + q(&s.antennas[0], 1);
477 let swapped = q(&s.samples, 1) + q(&s.antennas[0], 0);
478 let swap = swapped > direct;
479 if swap {
480 std::mem::swap(&mut s.samples, &mut s.antennas[0]);
481 }
482 s.record(&self.label, "pol_swap", f64::from(u8::from(swap)));
483 per_pol(s, rng, &|sub, _| self.single(sub))
484 }
485}
486
487impl FrameSync {
488 fn single(&self, s: &mut Frame) -> Result<()> {
489 s.require_one_sps("frame_sync")?;
490 let p = &self.p;
491 let range = p.start..p.start + p.length;
492 let symmetry = if p.symmetry == 0 { None } else { Some(p.symmetry) };
493 let r = frame::frame_sync(&s.samples, &s.symbols, range.clone(), p.max_delay, symmetry)?;
494 let len_in = s.samples.len() as isize;
495 s.samples = frame::align(&s.samples, r.delay, r.rotation, s.symbols.len());
496 for (i, e) in s.excluded.iter_mut().enumerate() {
498 let k = i as isize + r.delay;
499 if k < 0 || k >= len_in {
500 *e = true;
501 }
502 }
503 if p.exclude {
504 let n = s.excluded.len();
505 for i in range.filter(|&i| i < n) {
506 s.excluded[i] = true;
507 }
508 }
509 s.record(&self.label, "delay", r.delay as f64);
510 s.record(&self.label, "quality", r.quality);
511 Ok(())
512 }
513}
514
515block_params! {
516 struct Resample {
517 sps: f64 [2.0] > 0, "output samples per symbol";
518 }
519}
520
521impl Block for Resample {
522 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
523 let n_out = (s.samples.len() as f64 * self.sps / s.sps).round() as usize;
524 s.samples = resample::resample_fft(&s.samples, n_out);
525 s.sps = self.sps;
526 Ok(())
527 }
528}