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}