Skip to main content

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}