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}