Skip to main content

hyperopt_core/
trial.rs

1use crate::{Distribution, Value};
2use serde::{Deserialize, Serialize};
3
4/// Lifecycle state of a single trial.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum TrialState {
7    /// The objective is currently being evaluated.
8    Running,
9    /// The objective returned a value successfully.
10    Complete,
11    /// A [`crate::Pruner`] stopped the trial early.
12    Pruned,
13    /// The objective panicked or returned an error.
14    Failed,
15}
16
17/// One recorded `(name, distribution, value)` triple, in suggestion order.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct ParamRecord {
20    pub name: String,
21    pub distribution: Distribution,
22    pub value: Value,
23}
24
25/// A single optimization trial: its suggested parameters, intermediate
26/// reports, and final objective value.
27///
28/// `params` is kept in suggestion order (an ordered list rather than a hash
29/// map) so that define-by-run search spaces — where the set of parameters can
30/// differ between trials — round-trip faithfully through storage and are
31/// reproducible for grid enumeration.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct Trial {
34    /// Zero-based trial index within its study.
35    pub number: usize,
36    /// Parameters suggested so far, in the order they were requested.
37    pub params: Vec<ParamRecord>,
38    /// Intermediate `(step, value)` reports, populated via
39    /// [`crate::TrialContext::report`]; consumed by pruners.
40    pub intermediate_values: Vec<(usize, f64)>,
41    /// Final objective value, once the trial completes.
42    pub value: Option<f64>,
43    /// Current lifecycle state.
44    pub state: TrialState,
45}
46
47impl Trial {
48    /// Creates a fresh `Running` trial with the given number and no params.
49    pub fn new(number: usize) -> Self {
50        Trial {
51            number,
52            params: Vec::new(),
53            intermediate_values: Vec::new(),
54            value: None,
55            state: TrialState::Running,
56        }
57    }
58
59    /// Records a suggested parameter. Re-suggesting the same name overwrites
60    /// the previous record (mirrors Optuna, where repeated `suggest_*` calls
61    /// for one name within a trial are idempotent).
62    pub fn record(&mut self, name: &str, distribution: Distribution, value: Value) {
63        if let Some(existing) = self.params.iter_mut().find(|p| p.name == name) {
64            existing.distribution = distribution;
65            existing.value = value;
66        } else {
67            self.params.push(ParamRecord {
68                name: name.to_string(),
69                distribution,
70                value,
71            });
72        }
73    }
74
75    /// Looks up a previously recorded parameter value by name.
76    pub fn param_value(&self, name: &str) -> Option<&Value> {
77        self.params.iter().find(|p| p.name == name).map(|p| &p.value)
78    }
79
80    /// The intermediate value reported at exactly `step`, if any.
81    pub fn value_at_step(&self, step: usize) -> Option<f64> {
82        self.intermediate_values
83            .iter()
84            .find(|(s, _)| *s == step)
85            .map(|(_, v)| *v)
86    }
87
88    /// The intermediate value at the smallest reported step `>= step`, if any.
89    /// Used by rung-based pruners that compare trials at a resource budget.
90    pub fn value_at_or_after(&self, step: usize) -> Option<f64> {
91        self.intermediate_values
92            .iter()
93            .filter(|(s, _)| *s >= step)
94            .min_by_key(|(s, _)| *s)
95            .map(|(_, v)| *v)
96    }
97
98    /// The most recent `(step, value)` report, if the trial has reported.
99    pub fn last_intermediate(&self) -> Option<(usize, f64)> {
100        self.intermediate_values.last().copied()
101    }
102}