Skip to main content

comnum_sim/
callbacks.rs

1//! Callbacks: hooks into the run.
2//!
3//! A [`Callback`] is called at the start and the end of the run, of every condition and
4//! after every batch of trials. It observes (progress display, monitoring) or steers
5//! the run through the [`Action`] it returns: skip a condition before it starts, stop a
6//! condition early, or stop the whole run after a condition. Every method has a default
7//! that does nothing, so a callback implements only the hooks it needs.
8//!
9//! Built-in callbacks:
10//!
11//! - [`ProgressBar`]: one line per condition on the standard error (trials, wall time,
12//!   the estimate and its interval for every metric), and a periodic progress line on
13//!   long conditions;
14//! - [`SkipAfter`]: the classical shortcut of error-rate curves: once an error rate
15//!   falls below a floor along an axis (for instance the BER along an SNR sweep), the
16//!   following points of the same sweep are skipped, since they would need too many
17//!   trials and bring no information.
18//!
19//! # Examples
20//! ```
21//! use comnum_sim::prelude::*;
22//! use comnum_mapping::{constellation::{Constellation, ConstellationKind}, source as sources};
23//! use comnum_channel::awgn as channels;
24//! use comnum_metrics as metrics;
25//! use comnum_sim::scenario::from_fn;
26//! // error probability 1, 1e-2, 1e-4, 1e-6: the point at 30 dB is skipped once the
27//! // estimate at 20 dB falls below 1e-3
28//! let s = from_fn("decay", |cond, rng, rec| {
29//!     let p = 10f64.powf(-cond.f64("snr_db")? / 5.0);
30//!     rec.errors("ber", (rng.uniform() < p) as u64, 1);
31//!     Ok(())
32//! });
33//! let grid = Grid::new().axis("snr_db", [0.0, 10.0, 20.0, 30.0]);
34//! let r = Runner::new()
35//!     .stop(Stop::max_trials(2000))
36//!     .callback(SkipAfter::below("ber", 1e-3, "snr_db"))
37//!     .run(&s, &grid)
38//!     .unwrap();
39//! let status: Vec<&str> = r.iter().map(|c| c.status.as_str()).collect();
40//! assert_eq!(status, ["completed", "completed", "completed", "skipped"]);
41//! ```
42
43use crate::grid::{Condition, Value};
44use crate::runner::{ConditionResult, RunInfo, secs};
45use crate::stopping::Progress;
46use std::io::Write;
47use std::time::Instant;
48
49/// What a callback asks the runner to do.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Action {
52    /// Go on.
53    Continue,
54    /// At the start of a condition: do not run it (after a batch, it stops the
55    /// condition as [`Action::StopCondition`] does).
56    Skip,
57    /// After a batch: stop the condition (its status is `stopped`).
58    StopCondition,
59    /// After a condition: run no further condition.
60    StopRun,
61}
62
63/// Hooks into a run (all optional).
64pub trait Callback: Send {
65    /// Before the first condition.
66    fn on_run_start(&mut self, _info: &RunInfo) {}
67    /// Before a condition: [`Action::Skip`] does not run it.
68    fn on_condition_start(&mut self, _index: usize, _condition: &Condition) -> Action {
69        Action::Continue
70    }
71    /// After every batch of trials: [`Action::StopCondition`] stops the condition.
72    fn on_batch_end(&mut self, _progress: &Progress<'_>) -> Action {
73        Action::Continue
74    }
75    /// After a condition (run or skipped): [`Action::StopRun`] ends the run.
76    fn on_condition_end(&mut self, _result: &ConditionResult) -> Action {
77        Action::Continue
78    }
79    /// After the last condition.
80    fn on_run_end(&mut self, _results: &[ConditionResult]) {}
81}
82
83/// Progress lines on the standard error.
84#[derive(Clone, Debug)]
85pub struct ProgressBar {
86    /// Seconds between two progress lines within a long condition.
87    pub every_s: f64,
88    /// Confidence of the printed intervals.
89    pub confidence: f64,
90    n: usize,
91    last: Option<Instant>,
92}
93
94impl Default for ProgressBar {
95    fn default() -> Self {
96        ProgressBar { every_s: 5.0, confidence: 0.95, n: 0, last: None }
97    }
98}
99
100impl ProgressBar {
101    /// A progress display with a line every `every_s` seconds within a condition.
102    pub fn new(every_s: f64) -> ProgressBar {
103        ProgressBar { every_s, ..ProgressBar::default() }
104    }
105}
106
107impl Callback for ProgressBar {
108    fn on_run_start(&mut self, info: &RunInfo) {
109        self.n = info.n_conditions;
110        eprintln!(
111            "[{}] {} conditions, seed {}, batches of {}, {} threads, stop: {}",
112            info.scenario, info.n_conditions, info.seed, info.batch, info.threads, info.stop
113        );
114    }
115
116    fn on_condition_start(&mut self, _: usize, _: &Condition) -> crate::callbacks::Action {
117        self.last = Some(Instant::now());
118        Action::Continue
119    }
120
121    fn on_batch_end(&mut self, p: &Progress<'_>) -> Action {
122        let now = Instant::now();
123        if self.last.is_some_and(|t| now.duration_since(t) >= secs(self.every_s)) {
124            self.last = Some(now);
125            eprint!(
126                "\r  condition {}/{}: {} trials, {:.0} s",
127                p.condition + 1,
128                self.n,
129                p.trials,
130                p.elapsed.as_secs_f64()
131            );
132            let _ = std::io::stderr().flush();
133        }
134        Action::Continue
135    }
136
137    fn on_condition_end(&mut self, r: &ConditionResult) -> Action {
138        let metrics: Vec<String> = r
139            .record
140            .iter()
141            .map(|(k, e)| {
142                let (lo, hi) = e.interval(self.confidence);
143                format!("{k} = {:.4e} [{:.2e}, {:.2e}]", e.value(), lo, hi)
144            })
145            .collect();
146        eprintln!(
147            "\r[{}/{}] ({}) {}: {} trials, {:.2} s{}{}",
148            r.index + 1,
149            self.n,
150            r.condition.label(),
151            r.status.as_str(),
152            r.trials,
153            r.wall_s,
154            if metrics.is_empty() { "" } else { "  " },
155            metrics.join(", ")
156        );
157        Action::Continue
158    }
159}
160
161/// Skip the rest of a sweep once an error rate is below a floor.
162///
163/// After a condition whose metric is below `threshold` (with at least one trial), the
164/// later conditions that have the same values on every key except `along` are skipped.
165/// The grid should list the values of `along` in the direction of decreasing error
166/// rates (increasing SNR), which is the case when `along` is its last axis.
167#[derive(Clone, Debug)]
168pub struct SkipAfter {
169    /// The error-rate metric.
170    pub metric: String,
171    /// The floor.
172    pub threshold: f64,
173    /// The swept key.
174    pub along: String,
175    done: Vec<Vec<(String, Value)>>,
176}
177
178impl SkipAfter {
179    /// Skip along `along` once `metric` is below `threshold`.
180    pub fn below(metric: &str, threshold: f64, along: &str) -> SkipAfter {
181        SkipAfter { metric: metric.to_string(), threshold, along: along.to_string(), done: Vec::new() }
182    }
183
184    fn others(&self, c: &Condition) -> Vec<(String, Value)> {
185        c.entries().iter().filter(|(k, _)| *k != self.along).cloned().collect()
186    }
187}
188
189impl Callback for SkipAfter {
190    fn on_condition_start(&mut self, _: usize, c: &Condition) -> Action {
191        if self.done.contains(&self.others(c)) { Action::Skip } else { Action::Continue }
192    }
193
194    fn on_condition_end(&mut self, r: &ConditionResult) -> Action {
195        if r.trials > 0 && r.metric(&self.metric).is_some_and(|e| e.value() < self.threshold) {
196            let key = self.others(&r.condition);
197            if !self.done.contains(&key) {
198                self.done.push(key);
199            }
200        }
201        Action::Continue
202    }
203}