Skip to main content

optimizer/
objective.rs

1//! The [`Objective`] trait defines what gets optimized.
2//!
3//! # Closures work directly
4//!
5//! Any `Fn(&mut Trial) -> Result<V, E>` closure automatically implements
6//! [`Objective`], so you can pass closures straight to
7//! [`Study::optimize`](crate::Study::optimize):
8//!
9//! ```
10//! use optimizer::prelude::*;
11//!
12//! let study: Study<f64> = Study::new(Direction::Minimize);
13//! let x = FloatParam::new(-10.0, 10.0).name("x");
14//!
15//! study
16//!     .optimize(50, |trial: &mut optimizer::Trial| {
17//!         let v = x.suggest(trial)?;
18//!         Ok::<_, Error>((v - 3.0).powi(2))
19//!     })
20//!     .unwrap();
21//! ```
22//!
23//! # Structs for lifecycle hooks
24//!
25//! For richer control — early stopping or per-trial logging — implement
26//! [`Objective`] on a struct and pass it to the same
27//! [`Study::optimize`](crate::Study::optimize) method:
28//!
29//! ```
30//! use std::ops::ControlFlow;
31//!
32//! use optimizer::Objective;
33//! use optimizer::prelude::*;
34//!
35//! struct QuadraticWithEarlyStopping {
36//!     x: FloatParam,
37//!     target: f64,
38//! }
39//!
40//! impl Objective<f64> for QuadraticWithEarlyStopping {
41//!     type Error = Error;
42//!
43//!     fn evaluate(&self, trial: &mut Trial) -> Result<f64> {
44//!         let v = self.x.suggest(trial)?;
45//!         Ok((v - 3.0).powi(2))
46//!     }
47//!
48//!     fn after_trial(&self, _study: &Study<f64>, trial: &CompletedTrial<f64>) -> ControlFlow<()> {
49//!         if trial.value < self.target {
50//!             ControlFlow::Break(())
51//!         } else {
52//!             ControlFlow::Continue(())
53//!         }
54//!     }
55//! }
56//!
57//! let study: Study<f64> = Study::new(Direction::Minimize);
58//! let obj = QuadraticWithEarlyStopping {
59//!     x: FloatParam::new(-10.0, 10.0).name("x"),
60//!     target: 1.0,
61//! };
62//! study.optimize(200, obj).unwrap();
63//! assert!(study.best_value().unwrap() < 1.0);
64//! ```
65
66use core::ops::ControlFlow;
67
68use crate::sampler::CompletedTrial;
69use crate::study::Study;
70use crate::trial::Trial;
71
72/// Defines an objective function with lifecycle hooks for optimization.
73///
74/// The only required method is [`evaluate`](Objective::evaluate), which
75/// computes the objective value for a given trial. Optional hooks provide
76/// early stopping ([`before_trial`](Objective::before_trial),
77/// [`after_trial`](Objective::after_trial)).
78///
79/// # Closures implement `Objective` automatically
80///
81/// A blanket implementation covers all `Fn(&mut Trial) -> Result<V, E>`
82/// closures, so you can pass closures directly to
83/// [`Study::optimize`](crate::Study::optimize) without wrapping them.
84///
85/// # Thread safety
86///
87/// The async optimization methods (`optimize_async`, `optimize_parallel`)
88/// additionally require `Send + Sync + 'static` on the objective. The
89/// sync `optimize` method has no thread-safety requirements.
90pub trait Objective<V: PartialOrd = f64> {
91    /// The error type returned by [`evaluate`](Objective::evaluate).
92    type Error: ToString + 'static;
93
94    /// Evaluate the objective function for a single trial.
95    ///
96    /// Sample parameters from `trial` via
97    /// [`Parameter::suggest`](crate::parameter::Parameter::suggest) and
98    /// return the objective value. Return `Err(TrialPruned)` to prune a
99    /// trial early.
100    ///
101    /// # Errors
102    ///
103    /// Any error whose type implements `ToString`. Pruning errors
104    /// (`Error::TrialPruned` or `TrialPruned`) are handled specially —
105    /// the trial is recorded as pruned rather than failed.
106    fn evaluate(&self, trial: &mut Trial) -> Result<V, Self::Error>;
107
108    /// Called before each trial is created.
109    ///
110    /// Return `ControlFlow::Break(())` to stop the optimization loop
111    /// before the next trial starts.
112    ///
113    /// Default: always continues.
114    fn before_trial(&self, _study: &Study<V>) -> ControlFlow<()> {
115        ControlFlow::Continue(())
116    }
117
118    /// Called after each **completed** trial (not failed or pruned).
119    ///
120    /// The trial is passed directly as the argument *before* it is pushed
121    /// to storage, so `study.n_trials()` and `study.trials()` do not yet
122    /// include this trial. The trial is always pushed to storage after this
123    /// callback returns, regardless of the return value.
124    ///
125    /// Return `ControlFlow::Break(())` to stop the optimization loop.
126    ///
127    /// Default: always continues.
128    fn after_trial(&self, _study: &Study<V>, _trial: &CompletedTrial<V>) -> ControlFlow<()> {
129        ControlFlow::Continue(())
130    }
131}
132
133/// Blanket implementation: any `Fn(&mut Trial) -> Result<V, E>` is an
134/// `Objective` with no lifecycle hooks.
135impl<F, V, E> Objective<V> for F
136where
137    F: Fn(&mut Trial) -> Result<V, E>,
138    V: PartialOrd,
139    E: ToString + 'static,
140{
141    type Error = E;
142
143    fn evaluate(&self, trial: &mut Trial) -> Result<V, E> {
144        self(trial)
145    }
146}