use crate::scenario::Record;
use crate::stats::Estimate;
use comnum_core::{Error, Result};
use std::fmt;
use std::time::Duration;
#[derive(Clone, Copy, Debug)]
pub struct Progress<'a> {
pub condition: usize,
pub trials: u64,
pub record: &'a Record,
pub elapsed: Duration,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Stop {
MaxTrials(u64),
MinTrials(u64),
MinErrors {
metric: String,
errors: u64,
},
Precision {
metric: String,
relative: f64,
confidence: f64,
},
MaxTime(Duration),
Any(Vec<Stop>),
All(Vec<Stop>),
}
impl Stop {
pub fn max_trials(n: u64) -> Stop {
Stop::MaxTrials(n)
}
pub fn min_trials(n: u64) -> Stop {
Stop::MinTrials(n)
}
pub fn min_errors(metric: &str, errors: u64) -> Stop {
Stop::MinErrors { metric: metric.to_string(), errors }
}
pub fn precision(metric: &str, relative: f64) -> Stop {
Stop::precision_at(metric, relative, 0.95)
}
pub fn precision_at(metric: &str, relative: f64, confidence: f64) -> Stop {
Stop::Precision { metric: metric.to_string(), relative, confidence }
}
pub fn max_time(d: Duration) -> Stop {
Stop::MaxTime(d)
}
pub fn or(self, other: Stop) -> Stop {
Stop::Any(vec![self, other])
}
pub fn and(self, other: Stop) -> Stop {
Stop::All(vec![self, other])
}
pub fn is_met(&self, p: &Progress<'_>) -> bool {
match self {
Stop::MaxTrials(n) | Stop::MinTrials(n) => p.trials >= *n,
Stop::MinErrors { metric, errors } => {
matches!(p.record.get(metric), Some(Estimate::Rate(r)) if r.errors >= *errors)
}
Stop::Precision { metric, relative, confidence } => {
p.record.get(metric).is_some_and(|e| e.relative_precision(*confidence) <= *relative)
}
Stop::MaxTime(d) => p.elapsed >= *d,
Stop::Any(v) => v.iter().any(|s| s.is_met(p)),
Stop::All(v) => v.iter().all(|s| s.is_met(p)),
}
}
pub fn is_bounded(&self) -> bool {
match self {
Stop::MaxTrials(_) | Stop::MaxTime(_) => true,
Stop::MinTrials(_) | Stop::MinErrors { .. } | Stop::Precision { .. } => false,
Stop::Any(v) => v.iter().any(Stop::is_bounded),
Stop::All(v) => v.iter().all(Stop::is_bounded),
}
}
pub fn trial_limit(&self) -> Option<u64> {
match self {
Stop::MaxTrials(n) | Stop::MinTrials(n) => Some(*n),
Stop::MinErrors { .. } | Stop::Precision { .. } | Stop::MaxTime(_) => None,
Stop::Any(v) => v.iter().filter_map(Stop::trial_limit).min(),
Stop::All(v) => v.iter().map(Stop::trial_limit).collect::<Option<Vec<u64>>>()?.into_iter().max(),
}
}
fn targets<'a>(&'a self, out: &mut Vec<(&'a str, bool)>) {
match self {
Stop::MinErrors { metric, .. } => out.push((metric, true)),
Stop::Precision { metric, .. } => out.push((metric, false)),
Stop::MaxTrials(_) | Stop::MinTrials(_) | Stop::MaxTime(_) => {}
Stop::Any(v) | Stop::All(v) => v.iter().for_each(|s| s.targets(out)),
}
}
pub fn check_confidences(&self) -> Result<()> {
match self {
Stop::Precision { confidence, .. } => check_confidence(*confidence, &format!("the stopping rule `{self}`")),
Stop::Any(v) | Stop::All(v) => v.iter().try_for_each(Stop::check_confidences),
Stop::MaxTrials(_) | Stop::MinTrials(_) | Stop::MinErrors { .. } | Stop::MaxTime(_) => Ok(()),
}
}
pub fn check_metrics(&self, record: &Record) -> Result<()> {
let mut targets = Vec::new();
self.targets(&mut targets);
for (metric, needs_rate) in targets {
if !record.knows(metric) {
let names: Vec<&str> = record.iter().map(|(k, _)| k.as_str()).collect();
return Err(Error::Config(format!(
"the stopping rule `{self}` targets the metric `{metric}`, which is not recorded (recorded: {}): it would never be met",
if names.is_empty() { "none".to_string() } else { names.join(", ") }
)));
}
if needs_rate && matches!(record.get(metric), Some(Estimate::Mean(_))) {
return Err(Error::Config(format!(
"the stopping rule `{self}`: min_errors needs an error rate, and `{metric}` is a mean (use precision)"
)));
}
}
Ok(())
}
}
pub fn check_confidence(c: f64, what: &str) -> Result<()> {
if c > 0.0 && c < 1.0 {
Ok(())
} else {
Err(Error::Config(format!("{what}: the confidence level must be between 0 and 1 (0.95 for 95 %), got {c}")))
}
}
impl fmt::Display for Stop {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Stop::MaxTrials(n) => write!(f, "max_trials({n})"),
Stop::MinTrials(n) => write!(f, "min_trials({n})"),
Stop::MinErrors { metric, errors } => write!(f, "errors({metric}) ≥ {errors}"),
Stop::Precision { metric, relative, confidence } => {
write!(f, "precision({metric}) ≤ {relative} at {confidence}")
}
Stop::MaxTime(d) => write!(f, "time ≥ {:.1} s", d.as_secs_f64()),
Stop::Any(v) => {
write!(f, "({})", v.iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join(" or "))
}
Stop::All(v) => {
write!(f, "({})", v.iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join(" and "))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_target_on_a_metric_not_recorded_is_an_error() {
let mut rec = Record::new();
rec.errors("ber", 1, 100);
rec.value("snr_db", f64::INFINITY); let rule = Stop::min_errors("ber", 10).or(Stop::max_trials(5));
assert!(rule.check_metrics(&rec).is_ok());
assert!(Stop::precision("snr_db", 0.1).or(Stop::max_trials(5)).check_metrics(&rec).is_ok());
let e = Stop::precision("evm", 0.1).or(Stop::max_trials(5)).check_metrics(&rec).unwrap_err().to_string();
assert!(e.contains("`evm`") && e.contains("recorded: ber"), "{e}");
let e = Stop::min_errors("ber", 1).or(Stop::max_trials(5)).check_metrics(&Record::new()).unwrap_err();
assert!(e.to_string().contains("recorded: none"), "{e}");
assert!(Stop::max_trials(5).check_metrics(&Record::new()).is_ok());
}
#[test]
fn min_errors_on_a_mean_is_an_error_and_never_met() {
let mut rec = Record::new();
rec.values("snr_db", [1.0; 50]);
let rule = Stop::min_errors("snr_db", 10);
let e = rule.check_metrics(&rec).unwrap_err().to_string();
assert!(e.contains("is a mean"), "{e}");
let p = Progress { condition: 0, trials: 50, record: &rec, elapsed: Duration::ZERO };
assert!(!rule.is_met(&p));
}
#[test]
fn the_trial_limit_of_combined_rules() {
assert_eq!(Stop::max_trials(7).trial_limit(), Some(7));
assert_eq!(Stop::max_time(Duration::from_secs(1)).trial_limit(), None);
assert_eq!(Stop::max_trials(7).or(Stop::max_trials(3)).trial_limit(), Some(3));
assert_eq!(Stop::max_time(Duration::from_secs(1)).or(Stop::max_trials(3)).trial_limit(), Some(3));
assert_eq!(Stop::precision("x", 0.1).and(Stop::min_trials(4)).or(Stop::max_trials(9)).trial_limit(), Some(9));
assert_eq!(Stop::max_time(Duration::from_secs(1)).and(Stop::max_trials(3)).trial_limit(), None);
}
}