Skip to main content

comnum_sim/
lib.rs

1//! # comnum-sim — Monte Carlo simulation campaigns
2//!
3//! A campaign evaluates a **scenario** (one random trial of a link) on every
4//! **condition** of a **grid** (an SNR, a launch power, a modulation order, …), as many
5//! times as a **stopping rule** requires, in parallel on every core, and reports every
6//! metric with a confidence interval. The organisation follows PyTorch Lightning, so
7//! that the experiment code stays short and the engineering (parallelism, seeds,
8//! stopping, logging, progress) is written once:
9//!
10//! | PyTorch Lightning | `comnum_sim` | role |
11//! |---|---|---|
12//! | `LightningModule` | [`scenario::Scenario`] | what one trial does: `setup` once per condition, `trial` many times |
13//! | `self.log(...)` | [`scenario::Record`] | the metrics a trial reports: error counts or values |
14//! | `Trainer` | [`runner::Runner`] | runs the trials, in batches, on a thread pool |
15//! | `seed_everything` | [`runner::Runner::seed`] | trial `t` of condition `c` uses `stream(seed, name, c, t)` |
16//! | `max_epochs`, `EarlyStopping` | [`stopping::Stop`] | when a condition has enough trials (errors, precision, time) |
17//! | `Callback` | [`callbacks::Callback`] | hooks at the start and end of the run, of every condition and batch |
18//! | `Logger` | [`loggers::Logger`] | CSV (one row per condition, written as it completes) and JSON reports |
19//! | `fast_dev_run` | [`runner::Runner::fast_dev_run`] | one batch per condition, to check a scenario quickly |
20//!
21//! | Module | Content |
22//! |---|---|
23//! | [`scenario`] | the [`Scenario`](scenario::Scenario) trait, [`from_fn`](scenario::from_fn), the [`Record`](scenario::Record) of a trial |
24//! | [`grid`] | the conditions of a campaign: axes, zips, fixed values, explicit lists |
25//! | [`runner`] | the [`Runner`](runner::Runner): batches of trials on a thread pool, seeds, results |
26//! | [`stopping`] | the stopping rules [`Stop`](stopping::Stop) and their combinations |
27//! | [`callbacks`] | the hooks of a run: progress bar, skipping the rest of a sweep |
28//! | [`loggers`] | the CSV and JSON reports |
29//! | [`stats`] | error rates with Wilson intervals, means with normal intervals |
30//! | [`json`] | a minimal JSON writer for the reports |
31//!
32//! # Reproducibility
33//!
34//! Every trial draws its random numbers from its own stream
35//! `stream(seed, scenario name, condition index, trial index)` ([`comnum_core::rng::stream`]),
36//! the trials of a batch run in parallel, their records are merged **in trial order**,
37//! and the stopping rule is evaluated after every batch. The results are therefore
38//! identical whatever the number of threads (only a wall-time rule,
39//! [`stopping::Stop::max_time`], depends on the machine).
40//!
41//! # Statistics
42//!
43//! An error count is an [`stats::ErrorRate`] (`errors / total`, Wilson score interval);
44//! any other value is a [`stats::Mean`] (Welford's running mean and variance, normal
45//! interval of the mean). Both merge exactly across trials.
46//!
47//! # Example: BER of QPSK over AWGN against the theory
48//!
49//! ```
50//! use comnum_sim::prelude::*;
51//! use comnum_mapping::{constellation::{Constellation, ConstellationKind}, source as sources};
52//! use comnum_channel::awgn as channels;
53//! use comnum_metrics as metrics;
54//! use comnum_sim::scenario::from_fn;
55//!
56//! // a scenario written as a closure: no per-condition state
57//! let qpsk = from_fn("qpsk", |cond, rng, rec| {
58//!     let c = Constellation::qam(4)?.normalized();
59//!     let bits = sources::bits(2000, rng);
60//!     let rx = channels::awgn_ebn0(&c.map_bits(&bits)?, cond.f64("ebn0_db")?, 2.0, 1.0, rng);
61//!     rec.errors("ber", metrics::bit_errors(&c.demap_bits(&rx), &bits)? as u64, 2000);
62//!     Ok(())
63//! });
64//! let grid = Grid::new().axis("ebn0_db", [0.0, 2.0, 4.0]);
65//! let res = Runner::new()
66//!     .seed(7)
67//!     .batch(16)
68//!     .stop(Stop::min_errors("ber", 500).or(Stop::max_trials(2000)))
69//!     .run(&qpsk, &grid)
70//!     .unwrap();
71//! for r in &res {
72//!     let eb = r.condition.f64("ebn0_db").unwrap();
73//!     let ber = r.metric("ber").unwrap();
74//!     let (lo, hi) = ber.interval(0.99);
75//!     let theory = metrics::theory_ber(ConstellationKind::Qam, 4, eb).unwrap();
76//!     assert!(lo <= theory && theory <= hi, "{eb} dB: {theory} not in [{lo}, {hi}]");
77//! }
78//! ```
79
80/// Version of the library, recorded in every run.
81pub const VERSION: &str = env!("CARGO_PKG_VERSION");
82
83/// Commit of the library at build time, recorded in every run: `<hash>-dirty` when the
84/// tracked sources of the library (`crates/`, `bindings/`, `Cargo.toml`, `Cargo.lock`)
85/// have uncommitted changes, staged or not, `unknown` outside a Git checkout. The build
86/// script reruns when `HEAD`, the branch it points to, the index or one of these sources
87/// changes, so that the value is never stale.
88pub const COMMIT: &str = env!("COM_GIT_COMMIT");
89
90pub mod callbacks;
91pub mod grid;
92pub mod json;
93pub mod loggers;
94pub mod runner;
95pub mod scenario;
96pub mod stats;
97pub mod stopping;
98
99/// The items used by most campaigns.
100pub mod prelude {
101    pub use crate::{
102        callbacks::{Callback, ProgressBar, SkipAfter},
103        grid::{Condition, Grid, Value},
104        loggers::{CsvLogger, JsonLogger, Logger},
105        runner::{ConditionResult, Runner},
106        scenario::{Record, Scenario},
107        stats::{ErrorRate, Estimate, Mean},
108        stopping::Stop,
109    };
110    pub use comnum_core::rng::{Rng, stream};
111    pub use comnum_core::{Error, Result};
112}
113
114#[cfg(test)]
115mod tests {
116    #[test]
117    fn the_commit_is_a_hash_possibly_dirty_or_unknown() {
118        let hash = super::COMMIT.strip_suffix("-dirty").unwrap_or(super::COMMIT);
119        assert!(
120            hash == "unknown" || (hash.len() >= 40 && hash.chars().all(|c| c.is_ascii_hexdigit())),
121            "{}",
122            super::COMMIT
123        );
124    }
125}