Skip to main content

comnum_spec/
spec.rs

1//! The specification: its parsing ([`Spec`]), its grid and its stopping rule.
2
3use 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/// A parsed specification (see the module documentation for the format).
15#[derive(Clone, Debug)]
16pub struct Spec {
17    /// Name of the campaign (it enters the seeds of the trials).
18    pub name: String,
19    /// One-line description.
20    pub description: String,
21    /// Base seed.
22    pub seed: u64,
23    /// Trials per batch.
24    pub batch: u64,
25    /// Worker threads (0: all cores).
26    pub threads: usize,
27    /// Confidence level of the reported intervals and of the precision targets of the
28    /// stopping rule, in $(0, 1)$.
29    pub confidence: f64,
30    /// The conditions.
31    pub grid: Grid,
32    /// Defaults of the fibre blocks.
33    pub fiber: Option<Json>,
34    /// The blocks of the link, before substitution.
35    pub link: Vec<Json>,
36    /// The metrics, before substitution.
37    pub metrics: Json,
38    /// The stopping rule.
39    pub stop: Stop,
40    /// Print one line per condition.
41    pub progress: bool,
42    /// Skip the rest of a sweep below an error floor (`callbacks: {skip_below: …}`).
43    pub skip_below: Option<SkipAfter>,
44    /// Time every block of every trial (see [`Link::trial`](crate::Link::trial)).
45    pub profile: bool,
46    /// CSV output.
47    pub csv: Option<PathBuf>,
48    /// JSON output.
49    pub json: Option<PathBuf>,
50    /// Directory of the specification file (relative paths of the blocks).
51    pub base_dir: Option<PathBuf>,
52    /// The expected results (see [`verify`]), the fibre of their `gn_snr_db` theories
53    /// merged over the `fiber` of the specification.
54    pub expect: Vec<Json>,
55    /// The search of an operating point (see [`search`]), instead of the sweep.
56    pub search: Option<search::SearchSpec>,
57    /// The specification as parsed, recorded in the JSON report of every run.
58    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/// The `stop` mapping.
106#[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    /// The rule, its precision targets at the confidence level of the specification.
120    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
153/// The `stop` mapping of a specification of confidence level `confidence` whose rule is
154/// `stop`: the inverse of the parser, for a rule the format can write. A specification
155/// stops when all its targets (`min_errors`, `precision`, with `min_trials`) are met, or
156/// any of its caps (`max_trials`, `max_time_s`).
157///
158/// ```
159/// use comnum_sim::stopping::Stop;
160/// let rule = Stop::min_errors("ber", 100).or(Stop::max_trials(1000));
161/// let m = comnum_spec::stop_mapping(&rule, 0.95)?;
162/// assert_eq!(m, serde_json::json!({"min_errors": {"ber": 100}, "max_trials": 1000}));
163/// # Ok::<(), comnum_core::Error>(())
164/// ```
165///
166/// # Errors
167/// [`Error::Config`] for a rule the format cannot write: targets combined by *or*, a cap
168/// inside an *and*, a cap or a target given twice, a precision target at another
169/// confidence level than `confidence`, or no target and no cap.
170pub fn stop_mapping(stop: &Stop, confidence: f64) -> Result<Json> {
171    /// The operands of nested `Any` (`any`) or `All` rules.
172    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    // the targets, then the caps, in the order of the format
190    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/// The format of a specification text.
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub enum Format {
260    /// YAML (a superset of JSON).
261    Yaml,
262    /// JSON.
263    Json,
264    /// TOML.
265    Toml,
266}
267
268impl Format {
269    /// Read a document in this format as JSON (the specifications, and the validation
270    /// cards of `comnum-validation`).
271    ///
272    /// A key repeated in a mapping is an error (the readers of YAML and JSON would keep
273    /// the last value without a word; TOML refuses it itself).
274    ///
275    /// # Errors
276    /// [`Error::Config`] for a syntax error or a repeated key.
277    ///
278    /// # Examples
279    /// ```
280    /// use comnum_spec::Format;
281    /// assert_eq!(Format::Yaml.read("a: [1, 2]")?["a"][1], 2);
282    /// # Ok::<(), comnum_core::Error>(())
283    /// ```
284    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
297/// A JSON tree read from a document, refusing a key repeated in a mapping.
298struct 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
306/// The visitor of [`Unique`]: the values of `serde_json::Value`, the mappings checked.
307struct 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    /// A non-finite number becomes `null`, as in `serde_json::Value`.
329    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
409/// `grid` with one axis per entry of `axes` (the last varying fastest), checked.
410fn 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
418/// The conditions of a grid as a specification writes it: the Cartesian product of its
419/// axes, the last one varying fastest. An axis is a list of values, `{from, to, step}`,
420/// `{linspace: [start, stop, count]}` or one scalar.
421///
422/// ```
423/// use comnum_spec::grid_conditions;
424/// let conds = grid_conditions(&serde_json::json!({"m": [4, 16], "snr_db": {"from": 0, "to": 10, "step": 10}}))?;
425/// assert_eq!(conds.len(), 4);
426/// assert_eq!(conds[1].f64("snr_db")?, 10.0);
427/// # Ok::<(), comnum_core::Error>(())
428/// ```
429///
430/// # Errors
431/// [`Error::Config`] if `axes` is not a mapping or an axis is invalid.
432pub 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    /// Parse a specification from a JSON tree.
441    ///
442    /// # Errors
443    /// [`Error::Config`] for a missing or unknown key, an invalid grid (a key given by
444    /// a case and by an axis, see [`Grid::check`]) or rule.
445    pub fn from_json(v: Json) -> Result<Spec> {
446        let source = v.clone();
447        // a non-finite number of YAML or TOML (`.inf`, `nan`) becomes a JSON null, which
448        // an optional field would take for an absent one
449        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    /// Parse a specification text.
502    ///
503    /// # Errors
504    /// [`Error::Config`] for a syntax error or an invalid specification.
505    ///
506    /// # Examples
507    /// ```
508    /// use comnum_spec::{Format, Spec};
509    /// let spec = Spec::parse(r#"{"name": "t", "grid": {"x": [1, 2]}, "link": ["source"]}"#, Format::Json)?;
510    /// assert_eq!(spec.conditions().len(), 2);
511    /// # Ok::<(), comnum_core::Error>(())
512    /// ```
513    pub fn parse(text: &str, format: Format) -> Result<Spec> {
514        Spec::from_json(format.read(text)?)
515    }
516
517    /// Parse a YAML specification.
518    ///
519    /// # Errors
520    /// See [`Spec::parse`].
521    pub fn from_yaml(text: &str) -> Result<Spec> {
522        Spec::parse(text, Format::Yaml)
523    }
524
525    /// Read a specification file (`.yaml`, `.yml`, `.json` or `.toml`).
526    ///
527    /// # Errors
528    /// [`Error::Io`] if the file cannot be read, [`Error::Config`] otherwise.
529    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    /// The conditions of the grid.
543    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        // the runner refuses it too, whoever builds it
622        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        // a key repeated in a mapping of the document, of the grid or of a block
650        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}