comnum_spec/lib.rs
1//! Declarative simulation specifications.
2//!
3//! A specification describes a whole Monte Carlo campaign in a YAML, JSON or TOML file:
4//! the grid of conditions, the link as a list of blocks, the metrics, the stopping
5//! rule and the outputs. Every number of a study then comes from a file that can be
6//! read, versioned and rerun (`comnum run spec.yaml`, [`run`] in Rust,
7//! `comnum.run_spec` in Python), and a simulation that cannot be written as a
8//! specification shows that a block is missing from the [registry](blocks::registry).
9//!
10//! # Format
11//!
12//! ```yaml
13//! name: qam16_awgn # the name of the campaign (it enters the seeds)
14//! description: BER of 16-QAM over AWGN
15//! seed: 1 # base seed (default 0)
16//! batch: 64 # trials between two checks of the stopping rule
17//! threads: 0 # worker threads (0: all cores)
18//! confidence: 0.95 # level of the reported intervals
19//!
20//! grid: # axes, the last one varying fastest
21//! ebn0_db: {from: 0, to: 12, step: 2} # also: a list, or {linspace: [a, b, n]}
22//! cases: # optional explicit conditions, crossed with the axes
23//! - {m: 4}
24//! - {m: 16}
25//!
26//! fiber: {n_spans: 10} # defaults of every fibre block (FiberParams)
27//!
28//! link: # blocks, in order; `$name` is a grid variable
29//! - {type: source, constellation: {kind: qam, m: $m}, n_symbols: 4096}
30//! - {type: awgn_ebn0, ebn0_db: $ebn0_db}
31//!
32//! metrics: {report: [ber, ser, snr_db]}
33//!
34//! stop: # (targets and min_trials) or max_trials or max_time_s
35//! min_errors: {ber: 200}
36//! precision: {snr_db: 0.01} # relative half-width of the interval
37//! min_trials: 8
38//! max_trials: 10000
39//! max_time_s: 600
40//!
41//! callbacks:
42//! progress: true
43//! skip_below: {metric: ber, threshold: 1e-6, along: ebn0_db}
44//!
45//! profile: true # the time of every block, as the metrics <label>.time_ms
46//! output: {csv: results/qam16_awgn.csv, json: results/qam16_awgn.json}
47//!
48//! search: # instead of the sweep: the operating point
49//! {along: ebn0_db, range: [4, 16], metric: ber, target: 1.0e-3}
50//!
51//! expect: # expected results, checked by `comnum verify`
52//! - {metric: ber, theory: {function: ber, kind: qam, m: $m, ebn0_db: $ebn0_db}, rtol: 0.05}
53//! ```
54//!
55//! - The variables of the grid are substituted in the link and the metrics before the
56//! blocks are built: a string `$name` (or `${name}`) becomes the value with its type,
57//! and `${name}` inside a longer string is replaced by its text.
58//! - The stopping rule is `(all targets and min_trials) or max_trials or max_time_s`;
59//! without any field it is `max_trials: 100`, and a rule with targets but no cap is
60//! rejected (it could run forever). A target on a metric that the link does not
61//! record, or `min_errors` on a mean (`snr_db`: use `precision`), is an error at the
62//! first batch (`comnum check` reports it); `max_time_s` is a finite number of seconds
63//! at least 0, a `precision` a positive number. A run of `max_trials` runs exactly
64//! that many trials, whatever the batch.
65//! - The accepted values of a parameter (`reference: signal`, `kind: rrc`) are compared
66//! without case, and the block receives the declared spelling.
67//! - The blocks are listed with their parameters by [`blocks::registry`] (and
68//! `comnum blocks`); the metrics are those of [`metrics`].
69//! - `search` replaces the sweep by the search of the value of a variable at which a
70//! metric reaches a target, for every combination of the other axes ([`search`]).
71//! - `expect` lists results that the campaign must reproduce (closed forms, published
72//! curves), with their sources and tolerances; see [`verify`].
73//! - A block trained once per condition ([`Block::needs_training`](blocks::Block::needs_training),
74//! a learned receiver of another crate, say) is trained on an independent block drawn
75//! from the stream `"<name>/train"` before the trials.
76//! - Blocks defined in other crates join the registry by [`blocks::register`].
77//!
78//! # Modules
79//!
80//! | Module | Content |
81//! |---|---|
82//! | [`blocks`] | the blocks of a link, the frame they act on, their registry and its extension point |
83//! | [`metrics`] | the metrics of a declarative link |
84//! | [`verify`] | the expected results of a specification and their checks |
85//! | [`search`] | the search of operating points |
86//! | [`io`] | recordings: SigMF, raw IQ files, NPY arrays |
87//! | [`cli`] | the command line `comnum` (`run`, `check`, `verify`, `conditions`, `blocks`, `schema`) |
88//!
89//! The specification itself ([`Spec`]), the link ([`Link`]) and the runs ([`run`],
90//! [`profile`]) are at the root.
91//!
92//! # Examples
93//! ```
94//! use comnum_spec::{run, RunOptions, Spec};
95//! let spec = Spec::from_yaml(r#"
96//! name: qpsk_doc
97//! seed: 3
98//! grid: {ebn0_db: [2, 4]}
99//! link:
100//! - {type: source, constellation: QPSK, n_symbols: 2000}
101//! - {type: awgn_ebn0, ebn0_db: $ebn0_db}
102//! metrics: {report: [ber]}
103//! stop: {min_errors: {ber: 300}, max_trials: 500}
104//! "#).unwrap();
105//! let results = run(&spec, &RunOptions::default()).unwrap();
106//! let ber = results[1].metric("ber").unwrap();
107//! let theory = comnum_metrics::theory_ber(comnum_mapping::constellation::ConstellationKind::Qam, 4, 4.0).unwrap();
108//! let (lo, hi) = ber.interval(0.999);
109//! assert!(lo < theory && theory < hi);
110//! ```
111
112pub mod blocks;
113pub mod cli;
114pub mod io;
115mod link;
116pub mod metrics;
117mod params;
118mod run;
119pub mod search;
120mod spec;
121pub mod verify;
122
123pub use link::{Link, SpecScenario, link_labels, number_labels};
124pub use params::{substitute, variable};
125pub use run::{ProfileRow, RunOptions, profile, profile_of, run};
126pub use spec::{Format, Spec, grid_conditions, stop_mapping};
127
128/// The error of a name that is not among the known ones (a block, a code, a metric): one
129/// wording for every list of names.
130pub(crate) fn unknown(what: &str, name: &str, known: &[&str]) -> comnum_core::Error {
131 comnum_core::Error::Config(format!("unknown {what} {name:?}: one of {}", known.join(", ")))
132}
133
134/// The paths the exported macros expand to, so that a crate that uses them needs no
135/// dependency of its own on `comnum-core` or `serde_json`.
136#[doc(hidden)]
137pub mod __macro {
138 pub use comnum_core::Result;
139 pub use serde_json::{Map, Value as Json, json};
140}