1use crate::{params, search, verify};
4use comnum_core::{Error, Result};
5use comnum_sim::callbacks::SkipAfter;
6use comnum_sim::grid::{Condition, Grid, Value};
7use comnum_sim::stopping::{Stop, check_confidence};
8use serde::Deserialize;
9use serde_json::{Map, Value as Json};
10use std::collections::BTreeMap;
11use std::path::{Path, PathBuf};
12use std::time::Duration;
13
14#[derive(Clone, Debug)]
16pub struct Spec {
17 pub name: String,
19 pub description: String,
21 pub seed: u64,
23 pub batch: u64,
25 pub threads: usize,
27 pub confidence: f64,
30 pub grid: Grid,
32 pub fiber: Option<Json>,
34 pub link: Vec<Json>,
36 pub metrics: Json,
38 pub stop: Stop,
40 pub progress: bool,
42 pub skip_below: Option<SkipAfter>,
44 pub profile: bool,
46 pub csv: Option<PathBuf>,
48 pub json: Option<PathBuf>,
50 pub base_dir: Option<PathBuf>,
52 pub expect: Vec<Json>,
55 pub search: Option<search::SearchSpec>,
57 pub source: Json,
59}
60
61#[derive(Deserialize)]
62#[serde(deny_unknown_fields)]
63struct RawSpec {
64 name: String,
65 #[serde(default)]
66 description: String,
67 #[serde(default)]
68 seed: u64,
69 #[serde(default = "d_batch")]
70 batch: u64,
71 #[serde(default)]
72 threads: usize,
73 #[serde(default = "d_confidence")]
74 confidence: f64,
75 #[serde(default)]
76 grid: Map<String, Json>,
77 #[serde(default)]
78 cases: Vec<Map<String, Json>>,
79 #[serde(default)]
80 fiber: Option<Json>,
81 link: Vec<Json>,
82 #[serde(default)]
83 metrics: Json,
84 #[serde(default)]
85 stop: StopSpec,
86 #[serde(default)]
87 callbacks: CallbacksSpec,
88 #[serde(default)]
89 profile: bool,
90 #[serde(default)]
91 output: OutputSpec,
92 #[serde(default)]
93 expect: Vec<Json>,
94 #[serde(default)]
95 search: Option<Json>,
96}
97
98fn d_batch() -> u64 {
99 32
100}
101fn d_confidence() -> f64 {
102 0.95
103}
104
105#[derive(Deserialize, Default)]
107#[serde(deny_unknown_fields)]
108struct StopSpec {
109 max_trials: Option<u64>,
110 min_trials: Option<u64>,
111 #[serde(default)]
112 min_errors: BTreeMap<String, u64>,
113 #[serde(default)]
114 precision: BTreeMap<String, f64>,
115 max_time_s: Option<f64>,
116}
117
118impl StopSpec {
119 fn rule(&self, confidence: f64) -> Result<Stop> {
121 for (m, r) in &self.precision {
122 if !(r.is_finite() && *r > 0.0) {
123 return Err(Error::Config(format!("stop: precision of {m:?} must be a positive number, got {r}")));
124 }
125 }
126 let mut targets: Vec<Stop> = self.min_errors.iter().map(|(m, k)| Stop::min_errors(m, *k)).collect();
127 targets.extend(self.precision.iter().map(|(m, r)| Stop::precision_at(m, *r, confidence)));
128 let mut caps: Vec<Stop> = Vec::new();
129 if let Some(n) = self.max_trials {
130 caps.push(Stop::max_trials(n));
131 }
132 if let Some(t) = self.max_time_s {
133 let d = Duration::try_from_secs_f64(t).ok().filter(|_| t.is_finite()).ok_or_else(|| {
134 Error::Config(format!("stop: max_time_s must be a finite number of seconds >= 0, got {t}"))
135 })?;
136 caps.push(Stop::max_time(d));
137 }
138 if targets.is_empty() && caps.is_empty() {
139 caps.push(Stop::max_trials(self.min_trials.unwrap_or(100).max(1)));
140 }
141 let mut any = Vec::new();
142 if !targets.is_empty() {
143 if let Some(n) = self.min_trials {
144 targets.push(Stop::min_trials(n));
145 }
146 any.push(if targets.len() == 1 { targets.remove(0) } else { Stop::All(targets) });
147 }
148 any.extend(caps);
149 Ok(if any.len() == 1 { any.remove(0) } else { Stop::Any(any) })
150 }
151}
152
153pub fn stop_mapping(stop: &Stop, confidence: f64) -> Result<Json> {
171 fn operands<'a>(s: &'a Stop, any: bool, out: &mut Vec<&'a Stop>) {
173 match s {
174 Stop::Any(v) if any => v.iter().for_each(|x| operands(x, any, out)),
175 Stop::All(v) if !any => v.iter().for_each(|x| operands(x, any, out)),
176 _ => out.push(s),
177 }
178 }
179 let none = || {
180 Error::Config(format!(
181 "the rule {stop} has no `stop` mapping: a specification stops when all its targets \
182 (with min_trials, at the confidence {confidence}) or any of its caps are met"
183 ))
184 };
185 let once = |m: &mut Map<String, Json>, k: &str, v: Json| match m.insert(k.to_string(), v) {
186 None => Ok(()),
187 Some(_) => Err(none()),
188 };
189 let (mut errors, mut precision, mut min_trials, mut caps) = (Map::new(), Map::new(), Map::new(), Map::new());
191 let (mut any, mut grouped) = (Vec::new(), false);
192 operands(stop, true, &mut any);
193 for s in any {
194 match s {
195 Stop::MaxTrials(n) => once(&mut caps, "max_trials", Json::from(*n))?,
196 Stop::MaxTime(d) => once(&mut caps, "max_time_s", Json::from(d.as_secs_f64()))?,
197 _ if !grouped => {
198 grouped = true;
199 let mut all = Vec::new();
200 operands(s, false, &mut all);
201 for t in all {
202 match t {
203 Stop::MinErrors { metric, errors: k } => once(&mut errors, metric, Json::from(*k))?,
204 Stop::Precision { metric, relative, confidence: c } if *c == confidence => {
205 once(&mut precision, metric, Json::from(*relative))?;
206 }
207 Stop::MinTrials(n) => once(&mut min_trials, "min_trials", Json::from(*n))?,
208 _ => return Err(none()),
209 }
210 }
211 if errors.is_empty() && precision.is_empty() {
212 return Err(none());
213 }
214 }
215 _ => return Err(none()),
216 }
217 }
218 let mut out = Map::new();
219 if !errors.is_empty() {
220 out.insert("min_errors".into(), Json::Object(errors));
221 }
222 if !precision.is_empty() {
223 out.insert("precision".into(), Json::Object(precision));
224 }
225 out.extend(min_trials);
226 out.extend(caps);
227 if out.is_empty() {
228 return Err(none());
229 }
230 Ok(Json::Object(out))
231}
232
233#[derive(Deserialize, Default)]
234#[serde(deny_unknown_fields)]
235struct CallbacksSpec {
236 #[serde(default)]
237 progress: bool,
238 #[serde(default)]
239 skip_below: Option<SkipBelowSpec>,
240}
241
242#[derive(Deserialize)]
243#[serde(deny_unknown_fields)]
244struct SkipBelowSpec {
245 metric: String,
246 threshold: f64,
247 along: String,
248}
249
250#[derive(Deserialize, Default)]
251#[serde(deny_unknown_fields)]
252struct OutputSpec {
253 csv: Option<PathBuf>,
254 json: Option<PathBuf>,
255}
256
257#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub enum Format {
260 Yaml,
262 Json,
264 Toml,
266}
267
268impl Format {
269 pub fn read(self, text: &str) -> Result<Json> {
285 match self {
286 Format::Yaml => {
287 serde_norway::from_str::<Unique>(text).map(|u| u.0).map_err(|e| Error::Config(format!("YAML: {e}")))
288 }
289 Format::Json => {
290 serde_json::from_str::<Unique>(text).map(|u| u.0).map_err(|e| Error::Config(format!("JSON: {e}")))
291 }
292 Format::Toml => toml::from_str(text).map_err(|e| Error::Config(format!("TOML: {e}"))),
293 }
294 }
295}
296
297struct Unique(Json);
299
300impl<'de> Deserialize<'de> for Unique {
301 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Unique, D::Error> {
302 d.deserialize_any(UniqueVisitor).map(Unique)
303 }
304}
305
306struct UniqueVisitor;
308
309impl<'de> serde::de::Visitor<'de> for UniqueVisitor {
310 type Value = Json;
311
312 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 f.write_str("a JSON value")
314 }
315
316 fn visit_bool<E>(self, v: bool) -> std::result::Result<Json, E> {
317 Ok(Json::Bool(v))
318 }
319
320 fn visit_i64<E>(self, v: i64) -> std::result::Result<Json, E> {
321 Ok(Json::from(v))
322 }
323
324 fn visit_u64<E>(self, v: u64) -> std::result::Result<Json, E> {
325 Ok(Json::from(v))
326 }
327
328 fn visit_f64<E>(self, v: f64) -> std::result::Result<Json, E> {
330 Ok(Json::from(v))
331 }
332
333 fn visit_str<E>(self, v: &str) -> std::result::Result<Json, E> {
334 Ok(Json::String(v.to_string()))
335 }
336
337 fn visit_string<E>(self, v: String) -> std::result::Result<Json, E> {
338 Ok(Json::String(v))
339 }
340
341 fn visit_unit<E>(self) -> std::result::Result<Json, E> {
342 Ok(Json::Null)
343 }
344
345 fn visit_none<E>(self) -> std::result::Result<Json, E> {
346 Ok(Json::Null)
347 }
348
349 fn visit_some<D: serde::Deserializer<'de>>(self, d: D) -> std::result::Result<Json, D::Error> {
350 Unique::deserialize(d).map(|u| u.0)
351 }
352
353 fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> std::result::Result<Json, A::Error> {
354 let mut out = Vec::new();
355 while let Some(Unique(v)) = seq.next_element()? {
356 out.push(v);
357 }
358 Ok(Json::Array(out))
359 }
360
361 fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> std::result::Result<Json, A::Error> {
362 let mut out = Map::new();
363 while let Some(k) = map.next_key::<String>()? {
364 let Unique(v) = map.next_value()?;
365 if out.insert(k.clone(), v).is_some() {
366 return Err(serde::de::Error::custom(format!("the key {k:?} is given twice in a mapping")));
367 }
368 }
369 Ok(Json::Object(out))
370 }
371}
372
373fn grid_axis(name: &str, v: &Json) -> Result<Vec<Value>> {
374 match v {
375 Json::Array(a) => a.iter().map(|x| params::from_json(name, x)).collect(),
376 Json::Object(o) => {
377 if let Some(Json::Array(l)) = o.get("linspace") {
378 let f: Vec<f64> = l.iter().filter_map(Json::as_f64).collect();
379 if f.len() != 3 || f[2] < 1.0 {
380 return Err(Error::Config(format!("grid {name:?}: linspace is [start, stop, count]")));
381 }
382 let n = f[2] as usize;
383 return Ok((0..n)
384 .map(|i| Value::F64(if n == 1 { f[0] } else { f[0] + (f[1] - f[0]) * i as f64 / (n - 1) as f64 }))
385 .collect());
386 }
387 let get = |k: &str| o.get(k).and_then(Json::as_f64);
388 match (get("from"), get("to"), get("step")) {
389 (Some(a), Some(b), Some(st)) if st != 0.0 && (b - a) / st >= 0.0 => {
390 let n = ((b - a) / st + 1e-9).floor() as usize + 1;
391 let ints =
392 [a, b, st].iter().all(|v| v.fract() == 0.0) && o.values().all(|v| v.is_i64() || v.is_u64());
393 Ok((0..n)
394 .map(|i| {
395 let x = a + st * i as f64;
396 if ints { Value::I64(x as i64) } else { Value::F64(x) }
397 })
398 .collect())
399 }
400 _ => Err(Error::Config(format!(
401 "grid {name:?}: an axis is a list, {{from, to, step}} or {{linspace: [start, stop, count]}}"
402 ))),
403 }
404 }
405 scalar => Ok(vec![params::from_json(name, scalar)?]),
406 }
407}
408
409fn with_axes(mut grid: Grid, axes: &Map<String, Json>) -> Result<Grid> {
411 for (k, v) in axes {
412 grid = grid.axis(k, grid_axis(k, v)?);
413 }
414 grid.check()?;
415 Ok(grid)
416}
417
418pub fn grid_conditions(axes: &Json) -> Result<Vec<Condition>> {
433 let Json::Object(axes) = axes else {
434 return Err(Error::Config(format!("a grid is a mapping of axes, got {axes}")));
435 };
436 Ok(with_axes(Grid::new(), axes)?.conditions())
437}
438
439impl Spec {
440 pub fn from_json(v: Json) -> Result<Spec> {
446 let source = v.clone();
447 if let Some((k, _)) = v.get("stop").and_then(Json::as_object).and_then(|m| m.iter().find(|(_, x)| x.is_null()))
450 {
451 return Err(Error::Config(format!("stop: {k} must be a finite number, got null or a non-finite value")));
452 }
453 let raw: RawSpec = serde_json::from_value(v).map_err(|e| Error::Config(format!("specification: {e}")))?;
454 check_confidence(raw.confidence, "confidence")?;
455 let grid = if raw.cases.is_empty() {
456 Grid::new()
457 } else {
458 let mut conds = Vec::new();
459 for case in &raw.cases {
460 let mut c = Condition::new();
461 for (k, v) in case {
462 c.set(k, params::from_json(k, v)?);
463 }
464 conds.push(c);
465 }
466 Grid::from_conditions(conds)
467 };
468 let grid = with_axes(grid, &raw.grid)?;
469 if raw.link.is_empty() {
470 return Err(Error::Config("the link has no block".into()));
471 }
472 let expect =
473 raw.expect.into_iter().map(|e| verify::resolve_fiber(e, raw.fiber.as_ref())).collect::<Result<Vec<_>>>()?;
474 for e in &expect {
475 verify::Expectation::from_json(e)?;
476 }
477 Ok(Spec {
478 source,
479 name: raw.name,
480 description: raw.description,
481 seed: raw.seed,
482 batch: raw.batch.max(1),
483 threads: raw.threads,
484 confidence: raw.confidence,
485 grid,
486 fiber: raw.fiber,
487 link: raw.link,
488 metrics: raw.metrics,
489 stop: raw.stop.rule(raw.confidence)?,
490 progress: raw.callbacks.progress,
491 skip_below: raw.callbacks.skip_below.map(|s| SkipAfter::below(&s.metric, s.threshold, &s.along)),
492 profile: raw.profile,
493 csv: raw.output.csv,
494 json: raw.output.json,
495 base_dir: None,
496 expect,
497 search: raw.search.as_ref().map(search::SearchSpec::from_json).transpose()?,
498 })
499 }
500
501 pub fn parse(text: &str, format: Format) -> Result<Spec> {
514 Spec::from_json(format.read(text)?)
515 }
516
517 pub fn from_yaml(text: &str) -> Result<Spec> {
522 Spec::parse(text, Format::Yaml)
523 }
524
525 pub fn from_path(path: impl AsRef<Path>) -> Result<Spec> {
530 let path = path.as_ref();
531 let text = std::fs::read_to_string(path)?;
532 let format = match path.extension().and_then(|e| e.to_str()) {
533 Some("json") => Format::Json,
534 Some("toml") => Format::Toml,
535 _ => Format::Yaml,
536 };
537 let mut spec = Spec::parse(&text, format).map_err(|e| e.context(path.display()))?;
538 spec.base_dir = path.parent().map(Path::to_path_buf);
539 Ok(spec)
540 }
541
542 pub fn conditions(&self) -> Vec<Condition> {
544 self.grid.conditions()
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn stopping_rules() {
554 let s = |y: &str| Spec::from_yaml(&format!("name: t\nlink: [source]\nstop: {y}")).unwrap().stop.to_string();
555 assert_eq!(s("{}"), "max_trials(100)");
556 assert_eq!(s("{max_trials: 5}"), "max_trials(5)");
557 assert_eq!(s("{min_errors: {ber: 10}, max_trials: 5}"), "(errors(ber) ≥ 10 or max_trials(5))");
558 assert_eq!(
559 s("{min_errors: {ber: 10}, precision: {snr_db: 0.1}, min_trials: 2, max_trials: 5}"),
560 "((errors(ber) ≥ 10 and precision(snr_db) ≤ 0.1 at 0.95 and min_trials(2)) or max_trials(5))"
561 );
562 }
563
564 #[test]
565 fn stop_mappings_parse_back_to_their_rules() {
566 let parse = |m: &Json| {
567 let v = serde_json::json!({"name": "t", "link": ["source"], "stop": m});
568 Spec::from_json(v).unwrap().stop
569 };
570 for y in [
571 "{max_trials: 5}",
572 "{min_errors: {ber: 10}, max_trials: 5}",
573 "{min_errors: {ber: 10, fer: 2}, precision: {snr_db: 0.1}, min_trials: 2, max_trials: 5, max_time_s: 1.5}",
574 ] {
575 let rule = Spec::from_yaml(&format!("name: t\nlink: [source]\nstop: {y}")).unwrap().stop;
576 assert_eq!(parse(&stop_mapping(&rule, 0.95).unwrap()), rule, "{y}");
577 }
578 let nested = Stop::min_errors("ber", 10).or(Stop::max_trials(5)).or(Stop::max_time(Duration::from_secs(2)));
579 assert_eq!(
580 stop_mapping(&nested, 0.95).unwrap(),
581 serde_json::json!({"min_errors": {"ber": 10}, "max_trials": 5, "max_time_s": 2.0})
582 );
583 for bad in [
584 Stop::min_errors("ber", 10).or(Stop::min_errors("fer", 1)).or(Stop::max_trials(5)),
585 Stop::min_errors("ber", 10).and(Stop::max_trials(5)),
586 Stop::precision_at("ber", 0.1, 0.99).or(Stop::max_trials(5)),
587 Stop::max_trials(5).or(Stop::max_trials(6)),
588 Stop::min_trials(5),
589 ] {
590 assert!(stop_mapping(&bad, 0.95).is_err(), "{bad}");
591 }
592 }
593
594 #[test]
595 fn non_finite_or_negative_stopping_values_are_errors() {
596 let s = |y: &str| Spec::from_yaml(&format!("name: t\nlink: [source]\nstop: {y}"));
597 for bad in ["{max_time_s: .inf}", "{max_time_s: -1}", "{max_time_s: .nan}", "{max_time_s: 1e300}"] {
598 let e = s(bad).unwrap_err().to_string();
599 assert!(e.contains("max_time_s"), "{bad}: {e}");
600 }
601 for bad in ["{precision: {snr_db: 0}, max_trials: 5}", "{precision: {snr_db: -0.1}, max_trials: 5}"] {
602 let e = s(bad).unwrap_err().to_string();
603 assert!(e.contains("precision of \"snr_db\""), "{bad}: {e}");
604 }
605 assert!(s("{precision: {snr_db: .inf}, max_trials: 5}").is_err());
606 assert_eq!(s("{max_time_s: 0.5}").unwrap().stop.to_string(), "time ≥ 0.5 s");
607 }
608
609 #[test]
610 fn the_confidence_is_a_probability_and_reaches_the_precision_targets() {
611 let s = |c: &str| Spec::from_yaml(&format!("name: t\nlink: [source]\nconfidence: {c}"));
612 for bad in ["95", "1", "0", "-0.5", "1.5"] {
613 let e = s(bad).unwrap_err().to_string();
614 assert!(e.contains("confidence") && e.contains("between 0 and 1"), "{bad}: {e}");
615 }
616 let spec = Spec::from_yaml(
617 "name: t\nlink: [source]\nconfidence: 0.99\nstop: {precision: {snr_db: 0.1}, max_trials: 5}",
618 )
619 .unwrap();
620 assert_eq!(spec.stop.to_string(), "(precision(snr_db) ≤ 0.1 at 0.99 or max_trials(5))");
621 let spec = Spec::from_yaml("name: t\nlink: [source]\nstop: {max_trials: 2}").unwrap();
623 let e = spec
624 .bare_runner(&crate::RunOptions::default())
625 .confidence(95.0)
626 .run(&crate::SpecScenario::new(&spec), &spec.grid)
627 .unwrap_err();
628 assert!(e.to_string().contains("between 0 and 1"), "{e}");
629 }
630
631 #[test]
632 fn grids_from_lists_ranges_and_cases() {
633 let spec = Spec::from_yaml(
634 "name: g\nlink: [source]\ncases: [{m: 4}, {m: 16}]\ngrid:\n snr_db: {from: 0, to: 4, step: 2}\n rate: {linspace: [1.0, 2.0, 3]}\n kind: qam\n",
635 )
636 .unwrap();
637 let c = spec.conditions();
638 assert_eq!(c.len(), 2 * 3 * 3);
639 assert_eq!(c[0].label(), "m=4, snr_db=0, rate=1, kind=qam");
640 assert_eq!(c[17].label(), "m=16, snr_db=4, rate=2, kind=qam");
641 assert!(matches!(c[1].get("rate"), Some(Value::F64(_))));
642 assert!(matches!(c[3].get("snr_db"), Some(Value::I64(2))));
643 }
644
645 #[test]
646 fn a_key_given_twice_is_an_error() {
647 let e = Spec::from_yaml("name: g\nlink: [source]\ncases: [{m: 4}, {m: 16}]\ngrid: {m: [64]}").unwrap_err();
648 assert!(e.to_string().contains("\"m\" twice"), "{e}");
649 for text in
651 ["name: g\nlink: [source]\ngrid: {m: [4], m: [16]}", "name: g\nlink: [{type: awgn, snr_db: 3, snr_db: 5}]"]
652 {
653 let e = Spec::from_yaml(text).unwrap_err().to_string();
654 assert!(e.contains("\"m\" is given twice") || e.contains("\"snr_db\" is given twice"), "{e}");
655 }
656 let e = Spec::parse(r#"{"name": "g", "grid": {"m": [4], "m": [16]}, "link": ["source"]}"#, Format::Json);
657 assert!(e.unwrap_err().to_string().contains("given twice"));
658 }
659
660 #[test]
661 fn the_three_formats_give_the_same_specification() {
662 let y = Spec::parse("name: t\ngrid: {x: [1, 2]}\nlink: [source]", Format::Yaml).unwrap();
663 let j = Spec::parse(r#"{"name": "t", "grid": {"x": [1, 2]}, "link": ["source"]}"#, Format::Json).unwrap();
664 let t = Spec::parse("name = \"t\"\nlink = [\"source\"]\n[grid]\nx = [1, 2]\n", Format::Toml).unwrap();
665 for s in [&j, &t] {
666 assert_eq!(s.conditions(), y.conditions());
667 assert_eq!(s.link, y.link);
668 }
669 }
670}