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}