Skip to main content

henad_core/explore/
outcome.rs

1//! Runs that a plan requests, and the record of each finished run.
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::explore::measure::SeriesBuffer;
7
8/// One run of a plan.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct PlannedRun {
11    /// Position of the run in the plan.
12    ///
13    /// In a sweep it is `config_id * replicates + rep`. In a search it is `config_id * replicates` plus the run's
14    /// index within its candidate. A re-evaluation continues the `rep` numbering after the existing replicates of
15    /// the candidate it repeats.
16    pub run_id: u64,
17    /// Id of the config the run builds.
18    pub config_id: u64,
19    /// Replicate index within the config.
20    pub rep: u64,
21    /// Seed the model is built with.
22    pub seed: u64,
23}
24
25/// End state of a run.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum RunStatus {
28    /// The run ended without a fault, and every sampled value was finite.
29    Ok,
30    /// The run ended without a fault, and some sampled value was not finite.
31    NonFinite,
32    /// The model panicked while building or stepping.
33    Panicked,
34    /// The device reported an error.
35    GpuError,
36    /// The host refused to build the model.
37    Refused,
38    /// A sample no longer fit the stat layout the plan fixed.
39    ShapeError,
40    /// The run passed its wall-clock timeout.
41    TimedOut,
42}
43
44impl RunStatus {
45    /// Returns the status's name in `runs.csv`, as in `timed_out`.
46    pub fn as_str(self) -> &'static str {
47        match self {
48            Self::Ok => "ok",
49            Self::NonFinite => "non_finite",
50            Self::Panicked => "panicked",
51            Self::GpuError => "gpu_error",
52            Self::Refused => "refused",
53            Self::ShapeError => "shape_error",
54            Self::TimedOut => "timed_out",
55        }
56    }
57
58    /// Returns whether the run ended on a fault or a timeout, so its values cover part of the run at most.
59    pub fn is_failure(self) -> bool {
60        match self {
61            Self::Ok | Self::NonFinite => false,
62            Self::Panicked | Self::GpuError | Self::Refused | Self::ShapeError | Self::TimedOut => true,
63        }
64    }
65}
66
67impl FromStr for RunStatus {
68    type Err = RunStatusError;
69
70    /// Reads a status as [`RunStatus::as_str`] writes it.
71    fn from_str(raw: &str) -> Result<Self, Self::Err> {
72        match raw {
73            "ok" => Ok(Self::Ok),
74            "non_finite" => Ok(Self::NonFinite),
75            "panicked" => Ok(Self::Panicked),
76            "gpu_error" => Ok(Self::GpuError),
77            "refused" => Ok(Self::Refused),
78            "shape_error" => Ok(Self::ShapeError),
79            "timed_out" => Ok(Self::TimedOut),
80            _ => Err(RunStatusError { raw: raw.to_owned() }),
81        }
82    }
83}
84
85/// Text that matches no [`RunStatus`].
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct RunStatusError {
88    /// Text as given.
89    pub raw: String,
90}
91
92impl fmt::Display for RunStatusError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "unknown run status '{}'", self.raw)
95    }
96}
97
98impl std::error::Error for RunStatusError {}
99
100/// Cause of the end of a run.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum StopReason {
103    /// The run stepped to its total.
104    Steps,
105    /// The stop condition held at a sample.
106    Condition,
107    /// A fault ended the run early.
108    Fault,
109    /// The run passed its wall-clock timeout.
110    Timeout,
111}
112
113impl StopReason {
114    /// Returns the reason's name in `runs.csv`, as in `condition`.
115    pub fn as_str(self) -> &'static str {
116        match self {
117            Self::Steps => "steps",
118            Self::Condition => "condition",
119            Self::Fault => "fault",
120            Self::Timeout => "timeout",
121        }
122    }
123}
124
125impl FromStr for StopReason {
126    type Err = StopReasonError;
127
128    /// Reads a reason as [`StopReason::as_str`] writes it.
129    fn from_str(raw: &str) -> Result<Self, Self::Err> {
130        match raw {
131            "steps" => Ok(Self::Steps),
132            "condition" => Ok(Self::Condition),
133            "fault" => Ok(Self::Fault),
134            "timeout" => Ok(Self::Timeout),
135            _ => Err(StopReasonError { raw: raw.to_owned() }),
136        }
137    }
138}
139
140/// Text that matches no [`StopReason`].
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct StopReasonError {
143    /// Text as given.
144    pub raw: String,
145}
146
147impl fmt::Display for StopReasonError {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "unknown stop reason '{}'", self.raw)
150    }
151}
152
153impl std::error::Error for StopReasonError {}
154
155/// Record of one finished run.
156#[derive(Debug, Clone, PartialEq)]
157pub struct RunOutcome {
158    /// Run the record belongs to.
159    pub run: PlannedRun,
160    /// Hash that identifies the run's results, from [`crate::explore::fingerprint::run_key`].
161    pub run_key: u64,
162    /// State the run ended in.
163    pub status: RunStatus,
164    /// Cause of the run's end.
165    pub stop_reason: StopReason,
166    /// Tick the run ended on.
167    ///
168    /// A GPU run that faulted or timed out reports the tick of its latest completed sample, or 0 before any sample
169    /// completes.
170    pub ticks: u64,
171    /// Population at the run's last sample.
172    pub population: u64,
173    /// Time in milliseconds spent building the model.
174    pub build_ms: f64,
175    /// Time in milliseconds spent stepping and sampling.
176    pub wall_ms: f64,
177    /// One value per reducer, `None` for a reducer that saw no finite value.
178    pub reducers: Vec<Option<f64>>,
179    /// Series rows that the run kept.
180    pub series: SeriesBuffer,
181    /// Actions the model rejected, then the fault message, the timeout or the first value that was not finite.
182    pub note: Option<String>,
183}
184
185impl RunOutcome {
186    /// Steps per second of wall time. Note that the value is not finite for a run with no wall time.
187    pub fn steps_per_s(&self) -> f64 {
188        self.ticks as f64 * 1000.0 / self.wall_ms
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::{RunStatus, StopReason};
195
196    #[test]
197    fn statuses_are_written_in_snake_case() {
198        let statuses = [
199            RunStatus::Ok,
200            RunStatus::NonFinite,
201            RunStatus::Panicked,
202            RunStatus::GpuError,
203            RunStatus::Refused,
204            RunStatus::ShapeError,
205            RunStatus::TimedOut,
206        ];
207        let names: Vec<&str> = statuses.iter().map(|status| status.as_str()).collect();
208        assert_eq!(
209            names,
210            [
211                "ok",
212                "non_finite",
213                "panicked",
214                "gpu_error",
215                "refused",
216                "shape_error",
217                "timed_out"
218            ]
219        );
220        let failures: Vec<bool> = statuses.iter().map(|status| status.is_failure()).collect();
221        assert_eq!(failures, [false, false, true, true, true, true, true]);
222        for status in statuses {
223            assert_eq!(status.as_str().parse(), Ok(status));
224        }
225        assert!("timed out".parse::<RunStatus>().is_err());
226        let reasons = [
227            StopReason::Steps,
228            StopReason::Condition,
229            StopReason::Fault,
230            StopReason::Timeout,
231        ];
232        let names: Vec<&str> = reasons.iter().map(|reason| reason.as_str()).collect();
233        assert_eq!(names, ["steps", "condition", "fault", "timeout"]);
234        for reason in reasons {
235            assert_eq!(reason.as_str().parse(), Ok(reason));
236        }
237        assert!("stopped".parse::<StopReason>().is_err());
238    }
239}