1use crate::{Spec, SpecScenario};
4use comnum_core::Result;
5use comnum_sim::callbacks::ProgressBar;
6use comnum_sim::loggers::{CsvLogger, JsonLogger};
7use comnum_sim::runner::{ConditionResult, Runner};
8use std::path::PathBuf;
9
10#[derive(Clone, Debug, Default)]
12pub struct RunOptions {
13 pub threads: Option<usize>,
15 pub batch: Option<u64>,
17 pub fast_dev_run: bool,
19 pub progress: Option<bool>,
21 pub profile: Option<bool>,
23 pub csv: Option<PathBuf>,
25 pub json: Option<PathBuf>,
27}
28
29impl Spec {
30 pub(crate) fn bare_runner(&self, opts: &RunOptions) -> Runner {
35 Runner::new()
36 .seed(self.seed)
37 .batch(opts.batch.unwrap_or(self.batch))
38 .threads(opts.threads.unwrap_or(self.threads))
39 .stop(self.stop.clone())
40 .confidence(self.confidence)
41 .config(self.source.to_string())
42 .fast_dev_run(opts.fast_dev_run)
43 }
44
45 pub fn runner(&self, opts: &RunOptions) -> Runner {
62 let mut r = self.bare_runner(opts);
63 if opts.progress.unwrap_or(self.progress) {
64 let mut bar = ProgressBar::default();
65 bar.confidence = self.confidence;
66 r = r.callback(bar);
67 }
68 if let Some(skip) = &self.skip_below {
69 r = r.callback(skip.clone());
70 }
71 if let Some(p) = opts.csv.as_ref().or(self.csv.as_ref()) {
72 r = r.logger(CsvLogger::new(p));
73 }
74 if let Some(p) = opts.json.as_ref().or(self.json.as_ref()) {
75 r = r.logger(JsonLogger::new(p));
76 }
77 r
78 }
79}
80
81pub fn run(spec: &Spec, opts: &RunOptions) -> Result<Vec<ConditionResult>> {
88 spec.runner(opts).run(&SpecScenario::new(spec).profile(opts.profile.unwrap_or(spec.profile)), &spec.grid)
89}
90
91#[derive(Clone, Debug, PartialEq)]
93pub struct ProfileRow {
94 pub label: String,
96 pub time_ms: f64,
98 pub share: f64,
100}
101
102pub fn profile(results: &[ConditionResult], labels: &[String]) -> Vec<ProfileRow> {
107 let mean = |name: &str| {
108 let v: Vec<f64> =
109 results.iter().filter_map(|r| r.record.get(name).map(comnum_sim::stats::Estimate::value)).collect();
110 (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
111 };
112 let Some(total) = mean("link.time_ms") else { return Vec::new() };
113 let mut rows: Vec<ProfileRow> = labels
114 .iter()
115 .filter_map(|l| {
116 let time_ms = mean(&format!("{l}.time_ms"))?;
117 Some(ProfileRow { label: l.clone(), time_ms, share: if total > 0.0 { time_ms / total } else { 0.0 } })
118 })
119 .collect();
120 rows.push(ProfileRow { label: "link".into(), time_ms: total, share: 1.0 });
121 rows
122}
123
124pub fn profile_of(spec: &Spec, results: &[ConditionResult]) -> Result<Vec<ProfileRow>> {
131 let Some(cond) = spec.conditions().into_iter().next() else { return Ok(Vec::new()) };
132 let link = spec.link.iter().map(|b| crate::substitute(b, &cond)).collect::<Result<Vec<_>>>()?;
133 Ok(profile(results, &crate::link_labels(&link)?))
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::blocks::Frame;
140 use crate::io;
141 use comnum_core::rng::stream;
142
143 #[test]
144 fn profile_times_every_block_in_the_order_of_the_link() {
145 let spec = Spec::from_yaml(
146 "name: prof\ngrid: {ebn0_db: [4, 8]}\nlink: [{type: source, constellation: QPSK, n_symbols: 512}, {type: pulse_shape, sps: 2}, {type: awgn_ebn0, ebn0_db: $ebn0_db}, {type: matched_filter}, {type: decimate}]\nmetrics: {report: [ber], skip_start: 16, skip_end: 16}\nstop: {max_trials: 4}\nprofile: true",
147 )
148 .unwrap();
149 let results = run(&spec, &RunOptions::default()).unwrap();
150 let names: Vec<&String> =
151 results[0].record.iter().map(|(k, _)| k).filter(|k| k.ends_with(".time_ms")).collect();
152 assert_eq!(names.len(), 6, "{names:?}");
153 let rows = profile_of(&spec, &results).unwrap();
154 let labels: Vec<&str> = rows.iter().map(|r| r.label.as_str()).collect();
155 assert_eq!(labels, ["source", "pulse_shape", "awgn_ebn0", "matched_filter", "decimate", "link"]);
156 assert!(rows.iter().all(|r| r.time_ms >= 0.0 && r.share >= 0.0 && r.share <= 1.0));
157 assert!((rows.last().unwrap().share - 1.0).abs() < 1e-12);
158 let spec =
160 Spec::from_yaml("name: plain\nlink: [{type: source, n_symbols: 64}]\nstop: {max_trials: 1}").unwrap();
161 let results = run(&spec, &RunOptions::default()).unwrap();
162 assert!(results[0].record.iter().all(|(k, _)| !k.ends_with(".time_ms")));
163 assert_eq!(profile_of(&spec, &results).unwrap(), Vec::<ProfileRow>::new());
164 let results = run(&spec, &RunOptions { profile: Some(true), ..RunOptions::default() }).unwrap();
166 assert!(results[0].record.iter().any(|(k, _)| k == "link.time_ms"));
167 }
168
169 #[test]
170 fn dump_saves_the_first_trials_of_every_condition() {
171 let dir = std::env::temp_dir().join(format!("comnum-dump-{}", std::process::id()));
172 let _ = std::fs::remove_dir_all(&dir);
173 let spec = Spec::from_yaml(&format!(
174 "name: dumps\ngrid: {{snr_db: [5, 10]}}\nbatch: 8\nlink: [{{type: source, constellation: QPSK, n_symbols: 128, symbol_rate: 1e6}}, {{type: pulse_shape, sps: 4}}, {{type: dump, dir: {}, trials: 2, label: tap}}, {{type: awgn, snr_db: $snr_db}}, {{type: matched_filter}}, {{type: decimate}}]\nstop: {{max_trials: 8}}",
175 dir.display()
176 ))
177 .unwrap();
178 run(&spec, &RunOptions::default()).unwrap();
179 let cond = dir.join("dumps").join("snr_db=10");
180 let mut files: Vec<String> =
181 std::fs::read_dir(&cond).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect();
182 files.sort();
183 assert_eq!(files, ["tap_0.sigmf-data", "tap_0.sigmf-meta", "tap_1.sigmf-data", "tap_1.sigmf-meta"]);
184 let rec = io::read_sigmf(cond.join("tap_0.sigmf-meta")).unwrap();
185 assert_eq!(rec.samples.len(), 512);
186 assert_eq!(rec.sample_rate, Some(4e6));
187 assert_eq!(rec.meta["global"]["comnum:sps"], 4.0);
188 assert_eq!(rec.meta["global"]["comnum:block"], "tap");
189 let link = spec.link_for(&spec.conditions()[1]).unwrap();
192 let mut rng = stream(spec.seed, &spec.name, 1, 0);
193 let mut s = Frame::default();
194 for b in &link.blocks[..2] {
195 b.apply(&mut s, &mut rng).unwrap();
196 }
197 assert_eq!(s.samples, rec.samples);
198 std::fs::remove_dir_all(&dir).unwrap();
199 }
200}