1use super::{Block, BuildContext, Extension, Frame};
5use crate::io::Datatype;
6use crate::metrics::{Metric, Metrics, MetricsWindow};
7use comnum_core::Error;
8use comnum_core::Result;
9use comnum_core::ensure;
10use comnum_core::rng::Rng;
11use std::path::PathBuf;
12
13pub(super) fn blocks() -> Vec<Extension> {
15 vec![
16 builtin!(
17 "probe",
18 "Measure metrics at this point of the link; reported as <label>.<metric> (label: probe by default).",
19 ProbeSpec,
20 needs [Samples],
21 gives [],
22 |q, label, _| Ok(Box::new(Probe::new(q, label)?)),
23 ),
24 builtin!(
25 "dump",
26 "Save the samples at this point of the link for the first trials (by index) of every condition, as SigMF recordings <dir>/<campaign>/<condition>/<label>_<trial>.sigmf-meta and -data (one per stream, suffixed _s<stream> after the first).",
27 DumpSpec,
28 needs [Samples],
29 gives [],
30 |q, label, ctx| Ok(Box::new(Dump::new(&q, label, ctx)?)),
31 ),
32 builtin!(
33 "noise_estimate",
34 "Declare the receiver's posterior: the noise variance of the aligned samples; records sigma2.",
35 NoiseEstimateSpec,
36 needs [Samples, Symbols, Constellation],
37 gives [Noise],
38 |q, label, _| Ok(Box::new(NoiseEstimate::new(&q, label)?)),
39 ),
40 builtin!(
41 "classify",
42 "Blind modulation classification at one sample per symbol, by fourth-order cumulants or by likelihood; records class (the index of the decided candidate), correct (1 if it is the transmitted alphabet) and snr_m2m4_db (M2M4 for the decided alphabet).",
43 ClassifySpec,
44 needs [Samples],
45 gives [],
46 |q, label, _| Ok(Box::new(Classify::new(&q, label)?)),
47 ),
48 ]
49}
50
51block_params! {
54 struct ProbeSpec {
55 metrics: Vec<String> [["snr_db"] => "[snr_db]"],
56 "the metrics of the specifications (on symbols aligned at 1 sample per symbol), and power_dbm and papr_db anywhere";
57 window: MetricsWindow [flatten], "the window and the scaling, as for the metrics of the specification";
58 }
59}
60
61const SIGNAL_METRICS: [&str; 2] = ["power_dbm", "papr_db"];
63
64struct Probe {
66 label: String,
67 signal: Vec<&'static str>,
68 symbols: Option<Metrics>,
69}
70
71impl Probe {
72 fn new(p: ProbeSpec, label: String) -> Result<Probe> {
73 let mut signal = Vec::new();
74 let mut symbols = Vec::new();
75 for m in &p.metrics {
76 match SIGNAL_METRICS.iter().find(|s| s.eq_ignore_ascii_case(m)) {
77 Some(s) => signal.push(*s),
78 None => symbols.push(m.parse::<Metric>().map_err(|e| e.context("probe"))?),
79 }
80 }
81 let symbols = (!symbols.is_empty()).then(|| Metrics::with_window(symbols, p.window));
82 Ok(Probe { label, signal, symbols })
83 }
84}
85
86impl Block for Probe {
87 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
88 let mut rec = comnum_sim::scenario::Record::new();
89 for &m in &self.signal {
90 if m == "power_dbm" {
91 rec.value(m, comnum_math::w2dbm(s.mean_power()));
92 } else {
93 let papr: Vec<f64> = s.streams().map(|x| comnum_math::papr_db(x)).collect();
94 rec.value(m, papr.iter().sum::<f64>() / papr.len() as f64);
95 }
96 }
97 if let Some(metrics) = &self.symbols {
98 metrics.record(s, &mut rec)?;
99 }
100 for (k, e) in rec.iter() {
101 let name = format!("{}.{k}", self.label);
102 match e {
103 comnum_sim::stats::Estimate::Rate(r) => s.probes.errors(&name, r.errors, r.total),
104 comnum_sim::stats::Estimate::Mean(m) => s.probes.value(&name, m.mean()),
105 }
106 }
107 Ok(())
108 }
109}
110
111block_params! {
114 struct DumpSpec {
115 dir: String ["results/signals"], "directory of the recordings (relative to the working directory)";
116 trials: usize [1] >= 1, "trials saved per condition: those of index 0 to trials - 1, whatever the order the threads run them in";
117 datatype: String ["cf64"] one_of ["cf32", "cf64", "ci16"], "sample format of the SigMF data file";
118 }
119}
120
121struct Dump {
131 base: PathBuf,
132 label: String,
133 trials: u64,
134 datatype: Datatype,
135}
136
137impl Dump {
138 fn new(p: &DumpSpec, label: String, ctx: &BuildContext) -> Result<Dump> {
139 let mut base = PathBuf::from(&p.dir);
140 if let Some(name) = &ctx.name {
141 base.push(name);
142 }
143 if let Some(cond) = &ctx.condition {
144 if !cond.is_empty() {
145 base.push(cond.replace(", ", "_").replace(' ', ""));
146 }
147 }
148 Ok(Dump { base, label, trials: p.trials as u64, datatype: Datatype::parse(&p.datatype)? })
149 }
150}
151
152impl Block for Dump {
153 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
154 let trial = s.trial;
155 if trial >= self.trials {
156 return Ok(());
157 }
158 std::fs::create_dir_all(&self.base)?;
159 for (i, x) in s.streams().enumerate() {
160 let name = if i == 0 { format!("{}_{trial}", self.label) } else { format!("{}_{trial}_s{i}", self.label) };
161 let global = serde_json::json!({
162 "comnum:block": self.label,
163 "comnum:trial": trial,
164 "comnum:stream": i,
165 "comnum:sps": s.sps,
166 "comnum:symbol_rate": s.symbol_rate,
167 });
168 crate::io::write_sigmf(self.base.join(name), x, self.datatype, s.symbol_rate * s.sps, Some(&global))?;
169 }
170 Ok(())
171 }
172}
173
174block_params! {
175 struct NoiseEstimateSpec {
176 mode: String ["decisions"] one_of ["decisions", "known"],
177 "decisions (blind, around the nearest points) or known (around the transmitted symbols)";
178 }
179}
180
181struct NoiseEstimate {
184 known: bool,
185 label: String,
186}
187
188impl NoiseEstimate {
189 fn new(p: &NoiseEstimateSpec, label: String) -> Result<NoiseEstimate> {
190 let known = match p.mode.as_str() {
191 "decisions" => false,
192 "known" => true,
193 other => {
194 return Err(Error::Config(format!("noise_estimate: mode must be decisions or known, got {other:?}")));
195 }
196 };
197 Ok(NoiseEstimate { known, label })
198 }
199}
200
201impl Block for NoiseEstimate {
202 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
203 s.require_symbols("noise_estimate")?;
204 let c = s.constellation("noise_estimate")?;
205 let mut e = 0.0;
206 let mut n = 0usize;
207 for i in (0..s.samples.len()).filter(|&i| !s.excluded[i]) {
208 let z = s.samples[i];
209 let x = if self.known { s.symbols[i] } else { c.points()[c.nearest(z)] };
210 e += (z - x).norm_sqr();
211 n += 1;
212 }
213 ensure(n > 0, || "noise_estimate: every symbol is excluded".into())?;
214 let sigma2 = (e / n as f64).max(f64::MIN_POSITIVE);
215 s.receiver.noise_var = Some(sigma2);
216 s.record(&self.label, "sigma2", sigma2);
217 Ok(())
218 }
219}
220
221block_params! {
224 struct ClassifySpec {
225 candidates: Vec<String> [["BPSK", "QPSK", "8-PSK", "16-QAM", "64-QAM"] => "[BPSK, QPSK, 8-PSK, 16-QAM, 64-QAM]"],
226 "the candidate alphabets";
227 method: String ["ml"] one_of ["cumulants", "ml"],
228 "cumulants (Swami & Sadler 2000, normalised by the received power: for high SNR) or ml (likelihood, the scale and the noise estimated by M2M4 under every candidate)";
229 }
230}
231
232struct Classify {
233 candidates: Vec<comnum_mapping::constellation::Constellation>,
234 ml: bool,
235 label: String,
236}
237
238impl Classify {
239 fn new(p: &ClassifySpec, label: String) -> Result<Classify> {
240 ensure(!p.candidates.is_empty(), || "classify: give at least one candidate".into())?;
241 let candidates = p
242 .candidates
243 .iter()
244 .map(|n| comnum_mapping::constellation::Constellation::from_name(n).map(|c| c.normalized()))
245 .collect::<Result<Vec<_>>>()
246 .map_err(|e| e.context("classify"))?;
247 Ok(Classify { candidates, ml: p.method == "ml", label })
248 }
249}
250
251impl Block for Classify {
252 fn apply(&self, s: &mut Frame, _: &mut Rng) -> Result<()> {
253 s.require_one_sps("classify")?;
254 let r: Vec<_> =
255 (0..s.samples.len()).filter(|&i| s.excluded.get(i).is_none_or(|e| !e)).map(|i| s.samples[i]).collect();
256 let d = if self.ml {
257 comnum_metrics::blind::classify_ml_blind(&r, &self.candidates)
258 } else {
259 comnum_metrics::blind::classify_cumulants(&r, &self.candidates, None)
260 }
261 .map_err(|e| e.context("classify"))?;
262 let decided = &self.candidates[d.index];
263 let snr = comnum_metrics::blind::snr_m2m4(&r, decided).map_err(|e| e.context("classify"))?;
264 s.record(&self.label, "class", d.index as f64);
265 if let Some(c) = &s.constellation {
266 let correct = c.kind() == decided.kind() && c.order() == decided.order();
267 s.record(&self.label, "correct", f64::from(u8::from(correct)));
268 }
269 s.record(&self.label, "snr_m2m4_db", comnum_math::lin2db(snr));
270 Ok(())
271 }
272}