2026-02-12 12:25:20 +01:00
|
|
|
//! The [`Objective`] trait defines what gets optimized.
|
|
|
|
|
//!
|
2026-02-12 13:09:14 +01:00
|
|
|
//! # Closures work directly
|
|
|
|
|
//!
|
|
|
|
|
//! Any `Fn(&mut Trial) -> Result<V, E>` closure automatically implements
|
|
|
|
|
//! [`Objective`], so you can pass closures straight to
|
2026-02-12 12:25:20 +01:00
|
|
|
//! [`Study::optimize`](crate::Study::optimize):
|
|
|
|
|
//!
|
|
|
|
|
//! ```
|
|
|
|
|
//! use optimizer::prelude::*;
|
|
|
|
|
//!
|
|
|
|
|
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
|
|
|
|
//! let x = FloatParam::new(-10.0, 10.0).name("x");
|
|
|
|
|
//!
|
|
|
|
|
//! study
|
2026-02-12 13:09:14 +01:00
|
|
|
//! .optimize(50, |trial: &mut optimizer::Trial| {
|
2026-02-12 12:25:20 +01:00
|
|
|
//! let v = x.suggest(trial)?;
|
|
|
|
|
//! Ok::<_, Error>((v - 3.0).powi(2))
|
|
|
|
|
//! })
|
|
|
|
|
//! .unwrap();
|
|
|
|
|
//! ```
|
|
|
|
|
//!
|
2026-02-12 13:09:14 +01:00
|
|
|
//! # Structs for lifecycle hooks
|
|
|
|
|
//!
|
|
|
|
|
//! For richer control — early stopping or per-trial logging — implement
|
|
|
|
|
//! [`Objective`] on a struct and pass it to the same
|
|
|
|
|
//! [`Study::optimize`](crate::Study::optimize) method:
|
2026-02-12 12:25:20 +01:00
|
|
|
//!
|
|
|
|
|
//! ```
|
|
|
|
|
//! use std::ops::ControlFlow;
|
|
|
|
|
//!
|
|
|
|
|
//! use optimizer::Objective;
|
|
|
|
|
//! use optimizer::prelude::*;
|
|
|
|
|
//!
|
|
|
|
|
//! struct QuadraticWithEarlyStopping {
|
|
|
|
|
//! x: FloatParam,
|
|
|
|
|
//! target: f64,
|
|
|
|
|
//! }
|
|
|
|
|
//!
|
|
|
|
|
//! impl Objective<f64> for QuadraticWithEarlyStopping {
|
|
|
|
|
//! type Error = Error;
|
|
|
|
|
//!
|
|
|
|
|
//! fn evaluate(&self, trial: &mut Trial) -> Result<f64> {
|
|
|
|
|
//! let v = self.x.suggest(trial)?;
|
|
|
|
|
//! Ok((v - 3.0).powi(2))
|
|
|
|
|
//! }
|
|
|
|
|
//!
|
|
|
|
|
//! fn after_trial(&self, _study: &Study<f64>, trial: &CompletedTrial<f64>) -> ControlFlow<()> {
|
|
|
|
|
//! if trial.value < self.target {
|
|
|
|
|
//! ControlFlow::Break(())
|
|
|
|
|
//! } else {
|
|
|
|
|
//! ControlFlow::Continue(())
|
|
|
|
|
//! }
|
|
|
|
|
//! }
|
|
|
|
|
//! }
|
|
|
|
|
//!
|
|
|
|
|
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
|
|
|
|
//! let obj = QuadraticWithEarlyStopping {
|
|
|
|
|
//! x: FloatParam::new(-10.0, 10.0).name("x"),
|
|
|
|
|
//! target: 1.0,
|
|
|
|
|
//! };
|
2026-02-12 13:09:14 +01:00
|
|
|
//! study.optimize(200, obj).unwrap();
|
2026-02-12 12:25:20 +01:00
|
|
|
//! assert!(study.best_value().unwrap() < 1.0);
|
|
|
|
|
//! ```
|
|
|
|
|
|
|
|
|
|
use core::ops::ControlFlow;
|
|
|
|
|
|
|
|
|
|
use crate::sampler::CompletedTrial;
|
|
|
|
|
use crate::study::Study;
|
|
|
|
|
use crate::trial::Trial;
|
|
|
|
|
|
|
|
|
|
/// Defines an objective function with lifecycle hooks for optimization.
|
|
|
|
|
///
|
|
|
|
|
/// The only required method is [`evaluate`](Objective::evaluate), which
|
|
|
|
|
/// computes the objective value for a given trial. Optional hooks provide
|
|
|
|
|
/// early stopping ([`before_trial`](Objective::before_trial),
|
2026-02-12 13:09:14 +01:00
|
|
|
/// [`after_trial`](Objective::after_trial)).
|
2026-02-12 12:25:20 +01:00
|
|
|
///
|
2026-02-12 13:09:14 +01:00
|
|
|
/// # Closures implement `Objective` automatically
|
2026-02-12 12:25:20 +01:00
|
|
|
///
|
2026-02-12 13:09:14 +01:00
|
|
|
/// A blanket implementation covers all `Fn(&mut Trial) -> Result<V, E>`
|
|
|
|
|
/// closures, so you can pass closures directly to
|
|
|
|
|
/// [`Study::optimize`](crate::Study::optimize) without wrapping them.
|
2026-02-12 12:25:20 +01:00
|
|
|
///
|
|
|
|
|
/// # Thread safety
|
|
|
|
|
///
|
|
|
|
|
/// The async optimization methods (`optimize_async`, `optimize_parallel`)
|
|
|
|
|
/// additionally require `Send + Sync + 'static` on the objective. The
|
|
|
|
|
/// sync `optimize` method has no thread-safety requirements.
|
|
|
|
|
pub trait Objective<V: PartialOrd = f64> {
|
|
|
|
|
/// The error type returned by [`evaluate`](Objective::evaluate).
|
|
|
|
|
type Error: ToString + 'static;
|
|
|
|
|
|
|
|
|
|
/// Evaluate the objective function for a single trial.
|
|
|
|
|
///
|
|
|
|
|
/// Sample parameters from `trial` via
|
|
|
|
|
/// [`Parameter::suggest`](crate::parameter::Parameter::suggest) and
|
|
|
|
|
/// return the objective value. Return `Err(TrialPruned)` to prune a
|
|
|
|
|
/// trial early.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
///
|
|
|
|
|
/// Any error whose type implements `ToString`. Pruning errors
|
|
|
|
|
/// (`Error::TrialPruned` or `TrialPruned`) are handled specially —
|
|
|
|
|
/// the trial is recorded as pruned rather than failed.
|
|
|
|
|
fn evaluate(&self, trial: &mut Trial) -> Result<V, Self::Error>;
|
|
|
|
|
|
|
|
|
|
/// Called before each trial is created.
|
|
|
|
|
///
|
|
|
|
|
/// Return `ControlFlow::Break(())` to stop the optimization loop
|
|
|
|
|
/// before the next trial starts.
|
|
|
|
|
///
|
|
|
|
|
/// Default: always continues.
|
|
|
|
|
fn before_trial(&self, _study: &Study<V>) -> ControlFlow<()> {
|
|
|
|
|
ControlFlow::Continue(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Called after each **completed** trial (not failed or pruned).
|
|
|
|
|
///
|
2026-02-12 14:39:28 +01:00
|
|
|
/// The trial is passed directly as the argument *before* it is pushed
|
|
|
|
|
/// to storage, so `study.n_trials()` and `study.trials()` do not yet
|
|
|
|
|
/// include this trial. The trial is always pushed to storage after this
|
|
|
|
|
/// callback returns, regardless of the return value.
|
|
|
|
|
///
|
2026-02-12 12:25:20 +01:00
|
|
|
/// Return `ControlFlow::Break(())` to stop the optimization loop.
|
|
|
|
|
///
|
|
|
|
|
/// Default: always continues.
|
|
|
|
|
fn after_trial(&self, _study: &Study<V>, _trial: &CompletedTrial<V>) -> ControlFlow<()> {
|
|
|
|
|
ControlFlow::Continue(())
|
|
|
|
|
}
|
2026-02-12 13:09:14 +01:00
|
|
|
}
|
2026-02-12 12:25:20 +01:00
|
|
|
|
2026-02-12 13:09:14 +01:00
|
|
|
/// Blanket implementation: any `Fn(&mut Trial) -> Result<V, E>` is an
|
|
|
|
|
/// `Objective` with no lifecycle hooks.
|
|
|
|
|
impl<F, V, E> Objective<V> for F
|
|
|
|
|
where
|
|
|
|
|
F: Fn(&mut Trial) -> Result<V, E>,
|
|
|
|
|
V: PartialOrd,
|
|
|
|
|
E: ToString + 'static,
|
|
|
|
|
{
|
|
|
|
|
type Error = E;
|
|
|
|
|
|
|
|
|
|
fn evaluate(&self, trial: &mut Trial) -> Result<V, E> {
|
|
|
|
|
self(trial)
|
2026-02-12 12:25:20 +01:00
|
|
|
}
|
|
|
|
|
}
|