Skip to main content

somatize_study/pruner/
patience.rs

1//! Judged against itself.
2
3use super::judging::{latest, not_a_number};
4use super::{Pruner, Reason, Verdict};
5use crate::Goal;
6use std::fmt;
7use std::num::NonZeroUsize;
8
9/// Prune what has stopped improving on its own best. Early stopping, and the
10/// third thing a verdict can be measured against: the others, a constant,
11/// **itself**. Unlike both of the others it can prune a run that is doing
12/// perfectly well in the field and simply is not going anywhere.
13#[derive(Debug, Clone, PartialEq)]
14pub struct Patience {
15    /// How many reports without an improvement before it goes.
16    ///
17    /// Non-zero because zero patience would prune every trial at its first
18    /// report, improvement or not. Made impossible rather than validated.
19    pub steps: NonZeroUsize,
20    /// How much counts as an improvement. `0.0` means any at all, which makes
21    /// noise look like progress.
22    pub min_delta: f64,
23    /// Which way is better.
24    pub goal: Goal,
25}
26
27impl Patience {
28    /// Continue, or the reason not to. It ignores the other trials on purpose.
29    pub fn verdict(&self, mine: &[f64], _others: &[Vec<f64>]) -> Verdict {
30        let Some((at, value)) = latest(mine) else {
31            return Verdict::Continue;
32        };
33        if let Some(why) = not_a_number(at, value) {
34            return Verdict::Prune(why);
35        }
36
37        let mut best = f64::NAN;
38        let mut since = 0;
39        for (step, &value) in mine.iter().enumerate() {
40            // The first report there is has nothing to beat, so it counts.
41            if best.is_nan() || self.goal.better(value, best + self.moved()) {
42                best = value;
43                since = step;
44            }
45        }
46        if at - since >= self.steps.get() {
47            Verdict::Prune(Reason::NotImproving {
48                since,
49                steps: self.steps.get(),
50            })
51        } else {
52            Verdict::Continue
53        }
54    }
55
56    /// How far the best has to move to count, in the direction that is better.
57    fn moved(&self) -> f64 {
58        match self.goal {
59            Goal::Minimize => -self.min_delta,
60            Goal::Maximize => self.min_delta,
61        }
62    }
63}
64
65impl fmt::Display for Patience {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(
68            f,
69            "patience:{}:delta:{}:{}",
70            self.steps, self.min_delta, self.goal
71        )
72    }
73}
74
75impl From<Patience> for Pruner {
76    fn from(rule: Patience) -> Self {
77        Self::Patience(rule)
78    }
79}