Skip to main content

u_metaheur/
stop.rs

1//! Why a run stopped.
2//!
3//! A best-so-far solution cut short by the time limit and one the search
4//! finished look alike; only the run knows which it is. Every runner in this
5//! crate reports one [`StopReason`], set where it stops.
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10/// Why a run stopped: the run's own account of the solution it returns.
11///
12/// The names follow the termination statuses of mathematical-programming
13/// solvers (Gurobi `OPTIMAL` · `ITERATION_LIMIT` · `TIME_LIMIT` ·
14/// `USER_OBJ_LIMIT` · `INTERRUPTED`) and MATLAB `ga`'s stall generations.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
18pub enum StopReason {
19    /// The method ran to its own end: a schedule that cooled to its final
20    /// temperature, a construction that tried every element.
21    Completed,
22    /// An exact method proved the solution optimal.
23    Optimal,
24    /// The solution met the target the caller set, so the search stopped
25    /// there.
26    TargetReached,
27    /// The search spent its generation or iteration budget.
28    IterationLimit,
29    /// The search went its stall budget without improving its best
30    /// solution, and stopped there.
31    NoImprovement,
32    /// The time limit ran out; the solution is the best found by then.
33    TimeLimit,
34    /// The caller cancelled the run; the solution is the best found by then.
35    Cancelled,
36}
37
38impl StopReason {
39    /// Every reason, in declaration order.
40    pub const ALL: [Self; 7] = [
41        Self::Completed,
42        Self::Optimal,
43        Self::TargetReached,
44        Self::IterationLimit,
45        Self::NoImprovement,
46        Self::TimeLimit,
47        Self::Cancelled,
48    ];
49
50    /// The wire name (`"time_limit"`, `"no_improvement"`, ...).
51    pub fn name(self) -> &'static str {
52        match self {
53            Self::Completed => "completed",
54            Self::Optimal => "optimal",
55            Self::TargetReached => "target_reached",
56            Self::IterationLimit => "iteration_limit",
57            Self::NoImprovement => "no_improvement",
58            Self::TimeLimit => "time_limit",
59            Self::Cancelled => "cancelled",
60        }
61    }
62
63    /// Whether the run ended on something other than its own course: the
64    /// caller cancelled it or its time limit ran out.
65    pub fn cut_short(self) -> bool {
66        matches!(self, Self::TimeLimit | Self::Cancelled)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn names_are_distinct_snake_case() {
76        let names: std::collections::HashSet<&str> =
77            StopReason::ALL.iter().map(|r| r.name()).collect();
78        assert_eq!(names.len(), StopReason::ALL.len());
79        for name in names {
80            assert!(name.chars().all(|c| c.is_ascii_lowercase() || c == '_'));
81        }
82    }
83}
84
85/// A wall-clock budget counted from the start of a run: `time_limit_ms` of
86/// the runner's config, `None` for no limit.
87///
88/// Read through `web_time`, which is `std::time` natively and
89/// `performance.now()` in WebAssembly -- `std::time::Instant` panics there, so
90/// a limit read through it could only be honoured natively.
91#[derive(Debug, Clone, Copy)]
92pub(crate) struct Deadline {
93    start: web_time::Instant,
94    limit_ms: Option<u64>,
95}
96
97impl Deadline {
98    /// The budget `limit_ms`, starting now.
99    pub(crate) fn start(limit_ms: Option<u64>) -> Self {
100        Self {
101            start: web_time::Instant::now(),
102            limit_ms,
103        }
104    }
105
106    /// Whether the budget is spent.
107    pub(crate) fn passed(&self) -> bool {
108        self.limit_ms
109            .is_some_and(|ms| self.start.elapsed().as_millis() >= u128::from(ms))
110    }
111}