refactor(docs): Documentation Overhaul
This commit is contained in:
+43
-17
@@ -1,6 +1,21 @@
|
||||
//! Error types for the optimizer crate.
|
||||
//!
|
||||
//! All fallible operations in the crate return [`Result<T>`], which is an
|
||||
//! alias for `core::result::Result<T, Error>`. The [`Error`] enum covers
|
||||
//! parameter validation, sampling conflicts, pruning signals, and
|
||||
//! feature-gated I/O errors.
|
||||
|
||||
/// Errors returned by optimizer operations.
|
||||
///
|
||||
/// Most variants are returned during parameter validation or trial
|
||||
/// management. The [`TrialPruned`](Error::TrialPruned) variant has special
|
||||
/// significance — it signals early stopping and is typically raised via
|
||||
/// the [`TrialPruned`](super::TrialPruned) convenience type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Returned when the lower bound is greater than the upper bound.
|
||||
/// The lower bound exceeds the upper bound in a
|
||||
/// [`FloatParam`](crate::parameter::FloatParam) or
|
||||
/// [`IntParam`](crate::parameter::IntParam).
|
||||
#[error("invalid bounds: low ({low}) must be less than or equal to high ({high})")]
|
||||
InvalidBounds {
|
||||
/// The lower bound value.
|
||||
@@ -9,19 +24,22 @@ pub enum Error {
|
||||
high: f64,
|
||||
},
|
||||
|
||||
/// Returned when log scale is used with non-positive bounds.
|
||||
/// Log-scale is enabled but the lower bound is not positive (float) or
|
||||
/// is less than 1 (integer).
|
||||
#[error("invalid log bounds: low must be positive for log scale")]
|
||||
InvalidLogBounds,
|
||||
|
||||
/// Returned when step size is not positive.
|
||||
/// The step size provided to a parameter is not positive.
|
||||
#[error("invalid step: step must be positive")]
|
||||
InvalidStep,
|
||||
|
||||
/// Returned when categorical choices are empty.
|
||||
/// A [`CategoricalParam`](crate::parameter::CategoricalParam) was created
|
||||
/// with an empty choices vector.
|
||||
#[error("categorical choices cannot be empty")]
|
||||
EmptyChoices,
|
||||
|
||||
/// Returned when a parameter is suggested with a different configuration.
|
||||
/// The same [`ParamId`](crate::parameter::ParamId) was suggested twice
|
||||
/// with a different distribution configuration.
|
||||
#[error("parameter conflict for '{name}': {reason}")]
|
||||
ParameterConflict {
|
||||
/// The name of the conflicting parameter.
|
||||
@@ -30,27 +48,29 @@ pub enum Error {
|
||||
reason: String,
|
||||
},
|
||||
|
||||
/// Returned when requesting the best trial but no trials have completed.
|
||||
/// [`Study::best_trial`](crate::Study::best_trial) or similar was called
|
||||
/// before any trial completed successfully.
|
||||
#[error("no completed trials available")]
|
||||
NoCompletedTrials,
|
||||
|
||||
/// Returned when gamma is not in the valid range (0.0, 1.0).
|
||||
/// The gamma value for TPE sampling is outside the open interval (0, 1).
|
||||
#[error("invalid gamma: {0} must be in (0.0, 1.0)")]
|
||||
InvalidGamma(f64),
|
||||
|
||||
/// Returned when bandwidth is not positive.
|
||||
/// A KDE bandwidth value is not positive.
|
||||
#[error("invalid bandwidth: {0} must be positive")]
|
||||
InvalidBandwidth(f64),
|
||||
|
||||
/// Returned when KDE is created with empty samples.
|
||||
/// A kernel density estimator was constructed with no samples.
|
||||
#[error("KDE requires at least one sample")]
|
||||
EmptySamples,
|
||||
|
||||
/// Returned when multivariate KDE samples have zero dimensions.
|
||||
/// Multivariate KDE samples have zero dimensions.
|
||||
#[error("multivariate KDE samples must have at least one dimension")]
|
||||
ZeroDimensions,
|
||||
|
||||
/// Returned when multivariate KDE samples have inconsistent dimensions.
|
||||
/// A sample in the multivariate KDE has a different number of dimensions
|
||||
/// than the first sample.
|
||||
#[error(
|
||||
"dimension mismatch: expected {expected} dimensions but sample {sample_index} has {got}"
|
||||
)]
|
||||
@@ -63,7 +83,7 @@ pub enum Error {
|
||||
sample_index: usize,
|
||||
},
|
||||
|
||||
/// Returned when bandwidth vector length doesn't match the number of dimensions.
|
||||
/// The bandwidth vector length does not match the number of KDE dimensions.
|
||||
#[error("bandwidth dimension mismatch: expected {expected} bandwidths but got {got}")]
|
||||
BandwidthDimensionMismatch {
|
||||
/// The expected number of bandwidths.
|
||||
@@ -72,11 +92,14 @@ pub enum Error {
|
||||
got: usize,
|
||||
},
|
||||
|
||||
/// Returned when a trial is pruned (stopped early by the objective function).
|
||||
/// The objective signalled that this trial should be pruned (stopped
|
||||
/// early). Typically raised via `Err(TrialPruned)?` inside the
|
||||
/// objective closure.
|
||||
#[error("trial was pruned")]
|
||||
TrialPruned,
|
||||
|
||||
/// Returned when the objective returns the wrong number of values.
|
||||
/// The multi-objective closure returned a different number of values
|
||||
/// than the number of directions configured on the study.
|
||||
#[error("objective dimension mismatch: expected {expected} values, got {got}")]
|
||||
ObjectiveDimensionMismatch {
|
||||
/// The expected number of objective values.
|
||||
@@ -85,21 +108,24 @@ pub enum Error {
|
||||
got: usize,
|
||||
},
|
||||
|
||||
/// Returned when an internal invariant is violated.
|
||||
/// An internal invariant was violated. This indicates a bug in the
|
||||
/// library rather than a user error.
|
||||
#[error("internal error: {0}")]
|
||||
Internal(&'static str),
|
||||
|
||||
/// Returned when an async task fails.
|
||||
/// An async worker task failed. Only available with the `async` feature.
|
||||
#[cfg(feature = "async")]
|
||||
#[error("async task error: {0}")]
|
||||
TaskError(String),
|
||||
|
||||
/// Returned when a storage operation fails.
|
||||
/// A storage I/O operation failed. Only available with the `journal`
|
||||
/// feature.
|
||||
#[cfg(feature = "journal")]
|
||||
#[error("storage error: {0}")]
|
||||
Storage(String),
|
||||
}
|
||||
|
||||
/// A convenience alias for `core::result::Result<T, Error>`.
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
/// Convenience type for signalling a pruned trial from an objective function.
|
||||
|
||||
+66
-9
@@ -1,26 +1,83 @@
|
||||
//! fANOVA (functional ANOVA) parameter importance via random forest.
|
||||
//!
|
||||
//! Decomposes the variance of the objective function into contributions
|
||||
//! from individual parameters (main effects) and parameter interactions.
|
||||
//! fANOVA decomposes the variance of the objective function into
|
||||
//! contributions from individual parameters (**main effects**) and
|
||||
//! parameter pairs (**interaction effects**). This helps answer the
|
||||
//! question: *"Which parameters matter most, and do any parameters
|
||||
//! interact?"*
|
||||
//!
|
||||
//! The algorithm:
|
||||
//! 1. Fits a random forest to `(parameters) -> objective_value`
|
||||
//! 2. Applies functional ANOVA decomposition to the forest
|
||||
//! 3. Computes main effects (single-parameter importance)
|
||||
//! 4. Computes interaction effects (pairwise parameter importance)
|
||||
//! # Algorithm
|
||||
//!
|
||||
//! 1. Fit a random forest to the mapping `(parameters) → objective`
|
||||
//! 2. Apply functional ANOVA decomposition to the trained forest
|
||||
//! 3. Compute main effects: the variance explained by each parameter alone
|
||||
//! 4. Compute interaction effects: the additional variance explained by
|
||||
//! pairs of parameters beyond their individual contributions
|
||||
//! 5. Normalize so all importances sum to 1.0
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **After optimization**: call [`Study::fanova()`](crate::Study::fanova)
|
||||
//! or [`Study::fanova_with_config()`](crate::Study::fanova_with_config)
|
||||
//! to identify which parameters had the most impact
|
||||
//! - **Interaction detection**: unlike Spearman correlation
|
||||
//! ([`Study::param_importance()`](crate::Study::param_importance)),
|
||||
//! fANOVA can detect non-linear relationships and parameter interactions
|
||||
//! - **Hyperparameter tuning**: focus tuning effort on high-importance
|
||||
//! parameters and fix low-importance ones to reasonable defaults
|
||||
//!
|
||||
//! # Reference
|
||||
//!
|
||||
//! Hutter, F., Hoos, H. & Leyton-Brown, K. (2014). "An Efficient
|
||||
//! Approach for Assessing Hyperparameter Importance." ICML 2014.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::prelude::*;
|
||||
//!
|
||||
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
//! let x = FloatParam::new(0.0, 10.0).name("x");
|
||||
//! let y = FloatParam::new(0.0, 10.0).name("y");
|
||||
//!
|
||||
//! study
|
||||
//! .optimize(50, |trial| {
|
||||
//! let xv = x.suggest(trial)?;
|
||||
//! let yv = y.suggest(trial)?;
|
||||
//! // x matters much more than y
|
||||
//! Ok::<_, optimizer::Error>(3.0 * xv + 0.1 * yv)
|
||||
//! })
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let result = study.fanova().unwrap();
|
||||
//! // Main effects sorted by descending importance
|
||||
//! assert_eq!(result.main_effects[0].0, "x");
|
||||
//! ```
|
||||
|
||||
/// Result of fANOVA analysis.
|
||||
///
|
||||
/// All importance values are fractions of total variance and sum to 1.0
|
||||
/// across main effects and interactions combined.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FanovaResult {
|
||||
/// Per-parameter importance (fraction of total variance explained).
|
||||
/// Sorted by descending importance.
|
||||
///
|
||||
/// Sorted by descending importance. Each entry is
|
||||
/// `(parameter_name, importance)` where importance is in `[0.0, 1.0]`.
|
||||
pub main_effects: Vec<(String, f64)>,
|
||||
/// Pairwise interaction importance (fraction of total variance explained).
|
||||
/// Sorted by descending importance.
|
||||
///
|
||||
/// Sorted by descending importance. Each entry is
|
||||
/// `((param_a, param_b), importance)`. Only pairs with non-negligible
|
||||
/// interaction (> 1e-10) are included.
|
||||
pub interactions: Vec<((String, String), f64)>,
|
||||
}
|
||||
|
||||
/// Configuration for fANOVA analysis.
|
||||
///
|
||||
/// Use [`Default::default()`] for reasonable settings, or customize
|
||||
/// the random forest parameters for specific needs. Pass to
|
||||
/// [`Study::fanova_with_config()`](crate::Study::fanova_with_config).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FanovaConfig {
|
||||
/// Number of trees in the random forest (default: 64).
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
//! Parameter importance via Spearman rank correlation.
|
||||
//!
|
||||
//! Compute the absolute Spearman rank correlation between each parameter
|
||||
//! and the objective value to estimate which parameters most influence
|
||||
//! the outcome. This is a lightweight, non-parametric alternative to
|
||||
//! [`fANOVA`](crate::fanova) that works well for monotonic relationships.
|
||||
//!
|
||||
//! # How it works
|
||||
//!
|
||||
//! 1. Rank parameter values and objective values independently
|
||||
//! 2. Compute the Pearson correlation on the ranks (= Spearman ρ)
|
||||
//! 3. Take the absolute value (direction of correlation is not relevant
|
||||
//! for importance)
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Quick importance check**: call
|
||||
//! [`Study::param_importance()`](crate::Study::param_importance) after
|
||||
//! optimization for a fast, interpretable ranking
|
||||
//! - **Monotonic relationships**: Spearman captures monotonic (not just
|
||||
//! linear) correlations but may miss non-monotonic effects or interactions
|
||||
//! - For interaction detection or non-linear importance, use
|
||||
//! [`fANOVA`](crate::fanova) instead
|
||||
|
||||
/// Assign average ranks to a slice of `f64` values (handles ties).
|
||||
#[allow(clippy::cast_precision_loss, clippy::float_cmp)]
|
||||
|
||||
+49
-164
@@ -9,194 +9,79 @@
|
||||
#![deny(clippy::pedantic)]
|
||||
#![deny(clippy::std_instead_of_core)]
|
||||
|
||||
//! A black-box optimization library with multiple sampling strategies.
|
||||
//! Bayesian and population-based optimization library with an Optuna-like API
|
||||
//! for hyperparameter tuning and black-box optimization. It ships 12 samplers
|
||||
//! (from random search to CMA-ES and NSGA-III), 8 pruners, async/parallel
|
||||
//! evaluation, and optional journal-based persistence — all with zero required
|
||||
//! feature flags for the common case.
|
||||
//!
|
||||
//! This library provides an Optuna-like API for hyperparameter optimization
|
||||
//! with support for multiple sampling algorithms:
|
||||
//! # Getting Started
|
||||
//!
|
||||
//! - **Random Search** - Simple random sampling for baseline comparisons
|
||||
//! - **TPE (Tree-Parzen Estimator)** - Bayesian optimization for efficient search
|
||||
//! - **Grid Search** - Exhaustive search over a specified parameter grid
|
||||
//! - **Sobol (QMC)** - Quasi-random sampling for better space coverage (requires `sobol` feature)
|
||||
//! - **CMA-ES** - Covariance Matrix Adaptation Evolution Strategy for continuous optimization (requires `cma-es` feature)
|
||||
//! - **DE** - Differential Evolution for population-based global optimization
|
||||
//! - **GP** - Gaussian Process Bayesian optimization with Expected Improvement (requires `gp` feature)
|
||||
//! - **BOHB** - Bayesian Optimization + `HyperBand` for budget-aware TPE sampling
|
||||
//! - **NSGA-II** - Non-dominated Sorting Genetic Algorithm II for multi-objective optimization
|
||||
//! - **NSGA-III** - Reference-point-based NSGA for many-objective (3+) optimization
|
||||
//! - **MOEA/D** - Decomposition-based multi-objective with Tchebycheff, Weighted Sum, or PBI
|
||||
//! - **MOTPE** - Multi-Objective Tree-Parzen Estimator for Bayesian multi-objective optimization
|
||||
//!
|
||||
//! Additional features include:
|
||||
//!
|
||||
//! - Float, integer, and categorical parameter types
|
||||
//! - Log-scale and stepped parameter sampling
|
||||
//! - Synchronous and async optimization
|
||||
//! - Parallel trial evaluation with bounded concurrency
|
||||
//!
|
||||
//! # Quick Start
|
||||
//! Minimize a function in five lines — no feature flags needed:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::prelude::*;
|
||||
//!
|
||||
//! // Create a study with TPE sampler
|
||||
//! let sampler = TpeSampler::builder().seed(42).build().unwrap();
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//!
|
||||
//! // Define parameter search space
|
||||
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
//! let x = FloatParam::new(-10.0, 10.0).name("x");
|
||||
//!
|
||||
//! // Optimize x^2 for 20 trials
|
||||
//! study
|
||||
//! .optimize(20, |trial| {
|
||||
//! let x_val = x.suggest(trial)?;
|
||||
//! Ok::<_, Error>(x_val * x_val)
|
||||
//! .optimize(50, |trial| {
|
||||
//! let v = x.suggest(trial)?;
|
||||
//! Ok::<_, Error>((v - 3.0).powi(2))
|
||||
//! })
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! // Get the best result
|
||||
//! let best = study.best_trial().unwrap();
|
||||
//! println!("x = {}", best.get(&x).unwrap());
|
||||
//! println!("x = {:.4}, f(x) = {:.4}", best.get(&x).unwrap(), best.value);
|
||||
//! ```
|
||||
//!
|
||||
//! # Creating a Study
|
||||
//! # Core Concepts
|
||||
//!
|
||||
//! A [`Study`] manages optimization trials. Create one with an optimization direction:
|
||||
//! | Type | Role |
|
||||
//! |------|------|
|
||||
//! | [`Study`] | Drive an optimization loop: create trials, record results, track the best. |
|
||||
//! | [`Trial`] | A single evaluation of the objective function, carrying suggested parameter values. |
|
||||
//! | [`Parameter`] | Define the search space — [`FloatParam`], [`IntParam`], [`CategoricalParam`], [`BoolParam`], [`EnumParam`]. |
|
||||
//! | [`Sampler`](sampler::Sampler) | Strategy for choosing the next point to evaluate (TPE, CMA-ES, random, etc.). |
|
||||
//! | [`Direction`] | Whether the study minimizes or maximizes the objective value. |
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::random::RandomSampler;
|
||||
//! use optimizer::sampler::tpe::TpeSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//! # Sampler Guide
|
||||
//!
|
||||
//! // Minimize with default random sampler
|
||||
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
//! ## Single-objective samplers
|
||||
//!
|
||||
//! // Maximize with TPE sampler
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Maximize, TpeSampler::new());
|
||||
//! | Sampler | Algorithm | Best for | Feature flag |
|
||||
//! |---------|-----------|----------|--------------|
|
||||
//! | [`RandomSampler`] | Uniform random | Baselines, high-dimensional | — |
|
||||
//! | [`TpeSampler`] | Tree-Parzen Estimator | General-purpose Bayesian | — |
|
||||
//! | [`GridSearchSampler`] | Exhaustive grid | Small, discrete spaces | — |
|
||||
//! | [`SobolSampler`] | Sobol quasi-random sequence | Space-filling, low dimensions | `sobol` |
|
||||
//! | [`CmaEsSampler`] | CMA-ES | Continuous, moderate dimensions | `cma-es` |
|
||||
//! | [`GpSampler`] | Gaussian Process + EI | Expensive objectives, few trials | `gp` |
|
||||
//! | [`DifferentialEvolutionSampler`] | Differential Evolution | Non-convex, population-based | — |
|
||||
//! | [`BohbSampler`] | BOHB (TPE + `HyperBand`) | Budget-aware early stopping | — |
|
||||
//!
|
||||
//! // With seeded sampler for reproducibility
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
|
||||
//! ```
|
||||
//! ## Multi-objective samplers
|
||||
//!
|
||||
//! # Suggesting Parameters
|
||||
//!
|
||||
//! Within the objective function, use parameter types to suggest values:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::parameter::{BoolParam, CategoricalParam, FloatParam, IntParam, Parameter};
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
//!
|
||||
//! // Define parameter search spaces
|
||||
//! let x_param = FloatParam::new(0.0, 1.0);
|
||||
//! let lr_param = FloatParam::new(1e-5, 1e-1).log_scale();
|
||||
//! let step_param = FloatParam::new(0.0, 1.0).step(0.1);
|
||||
//! let n_param = IntParam::new(1, 10);
|
||||
//! let batch_param = IntParam::new(16, 256).log_scale();
|
||||
//! let units_param = IntParam::new(32, 512).step(32);
|
||||
//! let flag_param = BoolParam::new();
|
||||
//! let optimizer_param = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"]);
|
||||
//!
|
||||
//! study
|
||||
//! .optimize(10, |trial| {
|
||||
//! let x = x_param.suggest(trial)?;
|
||||
//! let lr = lr_param.suggest(trial)?;
|
||||
//! let step = step_param.suggest(trial)?;
|
||||
//! let n = n_param.suggest(trial)?;
|
||||
//! let batch = batch_param.suggest(trial)?;
|
||||
//! let units = units_param.suggest(trial)?;
|
||||
//! let flag = flag_param.suggest(trial)?;
|
||||
//! let optimizer = optimizer_param.suggest(trial)?;
|
||||
//!
|
||||
//! Ok::<_, optimizer::Error>(x * n as f64)
|
||||
//! })
|
||||
//! .unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! # Available Samplers
|
||||
//!
|
||||
//! ## Random Search
|
||||
//!
|
||||
//! The simplest sampling strategy, useful for baselines:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::random::RandomSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
|
||||
//! ```
|
||||
//!
|
||||
//! ## TPE (Tree-Parzen Estimator)
|
||||
//!
|
||||
//! Bayesian optimization that learns from previous trials:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::tpe::TpeSampler;
|
||||
//!
|
||||
//! let sampler = TpeSampler::builder()
|
||||
//! .gamma(0.15) // Quantile for good/bad split
|
||||
//! .n_startup_trials(20) // Random trials before TPE
|
||||
//! .n_ei_candidates(32) // Candidates to evaluate
|
||||
//! .seed(42) // Reproducibility
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Grid Search
|
||||
//!
|
||||
//! Exhaustive search over a discretized parameter space:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::grid::GridSearchSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let sampler = GridSearchSampler::builder()
|
||||
//! .n_points_per_param(10) // Points per parameter dimension
|
||||
//! .build();
|
||||
//!
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! ```
|
||||
//!
|
||||
//! # Async and Parallel Optimization
|
||||
//!
|
||||
//! With the `async` feature enabled, you can run trials asynchronously:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use optimizer::{Study, Direction};
|
||||
//! use optimizer::parameter::{FloatParam, Parameter};
|
||||
//!
|
||||
//! let x_param = FloatParam::new(0.0, 1.0);
|
||||
//!
|
||||
//! // Sequential async
|
||||
//! study.optimize_async(10, |mut trial| {
|
||||
//! let x_param = x_param.clone();
|
||||
//! async move {
|
||||
//! let x = x_param.suggest(&mut trial)?;
|
||||
//! Ok((trial, x * x))
|
||||
//! }
|
||||
//! }).await?;
|
||||
//!
|
||||
//! // Parallel with bounded concurrency
|
||||
//! study.optimize_parallel(10, 4, |mut trial| {
|
||||
//! let x_param = x_param.clone();
|
||||
//! async move {
|
||||
//! let x = x_param.suggest(&mut trial)?;
|
||||
//! Ok((trial, x * x))
|
||||
//! }
|
||||
//! }).await?;
|
||||
//! ```
|
||||
//! | Sampler | Algorithm | Best for | Feature flag |
|
||||
//! |---------|-----------|----------|--------------|
|
||||
//! | [`Nsga2Sampler`] | NSGA-II | 2-3 objectives | — |
|
||||
//! | [`Nsga3Sampler`] | NSGA-III (reference-point) | 3+ objectives | — |
|
||||
//! | [`MoeadSampler`] | MOEA/D (decomposition) | Many objectives, structured fronts | — |
|
||||
//! | [`MotpeSampler`] | Multi-Objective TPE | Bayesian multi-objective | — |
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//!
|
||||
//! - `async`: Enable async optimization methods (requires tokio)
|
||||
//! - `derive`: Enable `#[derive(Categorical)]` for enum parameters
|
||||
//! - `serde`: Enable `Serialize`/`Deserialize` on public types and `Study::save()`/`Study::load()`
|
||||
//! - `sobol`: Enable the Sobol quasi-random sampler for better space coverage
|
||||
//! - `cma-es`: Enable the CMA-ES sampler for continuous optimization
|
||||
//! - `gp`: Enable the Gaussian Process sampler for Bayesian optimization
|
||||
//! - `visualization`: Generate self-contained HTML reports with interactive Plotly.js charts
|
||||
//! - `tracing`: Emit structured log events via the [`tracing`](https://docs.rs/tracing) crate at key optimization points
|
||||
//! | Flag | What it enables | Default |
|
||||
//! |------|----------------|---------|
|
||||
//! | `async` | Async/parallel optimization via tokio ([`Study::optimize_async`], [`Study::optimize_parallel`]) | off |
|
||||
//! | `derive` | `#[derive(Categorical)]` for enum parameters | off |
|
||||
//! | `serde` | `Serialize`/`Deserialize` on public types, [`Study::save`]/[`Study::load`] | off |
|
||||
//! | `journal` | [`JournalStorage`] — JSONL persistence with file locking (enables `serde`) | off |
|
||||
//! | `sobol` | [`SobolSampler`] — quasi-random low-discrepancy sequences | off |
|
||||
//! | `cma-es` | [`CmaEsSampler`] — Covariance Matrix Adaptation Evolution Strategy | off |
|
||||
//! | `gp` | [`GpSampler`] — Gaussian Process surrogate with Expected Improvement | off |
|
||||
//! | `tracing` | Structured log events via [`tracing`](https://docs.rs/tracing) at key optimization points | off |
|
||||
|
||||
/// Emit a `tracing::info!` event when the `tracing` feature is enabled.
|
||||
/// No-op otherwise.
|
||||
|
||||
+46
-10
@@ -1,9 +1,28 @@
|
||||
//! Multi-objective optimization via a dedicated study type.
|
||||
//!
|
||||
//! [`MultiObjectiveStudy`] manages trials that return multiple objective
|
||||
//! values. It supports arbitrary numbers of objectives with per-objective
|
||||
//! directions (minimize or maximize). Use [`pareto_front()`](MultiObjectiveStudy::pareto_front)
|
||||
//! to retrieve the Pareto-optimal solutions.
|
||||
//! [`MultiObjectiveStudy`] manages trials that return **multiple** objective
|
||||
//! values simultaneously. It supports arbitrary numbers of objectives with
|
||||
//! per-objective directions (minimize or maximize).
|
||||
//!
|
||||
//! # Key concepts
|
||||
//!
|
||||
//! In multi-objective optimization there is usually no single best solution.
|
||||
//! Instead, there is a **Pareto front** — the set of solutions where no
|
||||
//! objective can be improved without worsening another. Use
|
||||
//! [`pareto_front()`](MultiObjectiveStudy::pareto_front) to retrieve these
|
||||
//! non-dominated solutions after optimization.
|
||||
//!
|
||||
//! A solution **dominates** another if it is at least as good in all
|
||||
//! objectives and strictly better in at least one. Solutions that are not
|
||||
//! dominated by any other are called **Pareto-optimal**.
|
||||
//!
|
||||
//! # Samplers
|
||||
//!
|
||||
//! By default a random sampler is used. For smarter search, pass a
|
||||
//! [`MultiObjectiveSampler`] such as [`Nsga2Sampler`](crate::Nsga2Sampler),
|
||||
//! [`Nsga3Sampler`](crate::Nsga3Sampler), or
|
||||
//! [`MoeadSampler`](crate::MoeadSampler) via
|
||||
//! [`MultiObjectiveStudy::with_sampler`].
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -46,6 +65,11 @@ use crate::types::{Direction, TrialState};
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A completed trial with multiple objective values.
|
||||
///
|
||||
/// Each trial stores its sampled parameter values, the vector of
|
||||
/// objective values (one per objective), and optional constraint values.
|
||||
/// Retrieve typed parameter values with [`get()`](Self::get) and check
|
||||
/// constraint feasibility with [`is_feasible()`](Self::is_feasible).
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct MultiObjectiveTrial {
|
||||
@@ -111,9 +135,14 @@ impl MultiObjectiveTrial {
|
||||
|
||||
/// Trait for samplers aware of multi-objective history.
|
||||
///
|
||||
/// Separate from [`Sampler`] because NSGA-II needs access to
|
||||
/// `&[MultiObjectiveTrial]` (with vector-valued objectives) and
|
||||
/// `&[Direction]` (one direction per objective).
|
||||
/// Separate from [`Sampler`] because multi-objective algorithms (e.g.,
|
||||
/// NSGA-II) need access to the full vector of objective values per trial
|
||||
/// (`&[MultiObjectiveTrial]`) and the per-objective directions
|
||||
/// (`&[Direction]`).
|
||||
///
|
||||
/// Implementations include [`Nsga2Sampler`](crate::Nsga2Sampler),
|
||||
/// [`Nsga3Sampler`](crate::Nsga3Sampler), and
|
||||
/// [`MoeadSampler`](crate::MoeadSampler).
|
||||
pub trait MultiObjectiveSampler: Send + Sync {
|
||||
/// Samples a parameter value from the given distribution.
|
||||
fn sample(
|
||||
@@ -181,9 +210,12 @@ impl Sampler for MoSamplerBridge {
|
||||
|
||||
/// A study for multi-objective optimization.
|
||||
///
|
||||
/// Manages trials that return multiple objective values. Supports
|
||||
/// Manage trials that return multiple objective values. Supports
|
||||
/// arbitrary numbers of objectives with independent minimize/maximize
|
||||
/// directions.
|
||||
/// directions. After optimization, call [`pareto_front()`](Self::pareto_front)
|
||||
/// to retrieve the non-dominated solutions.
|
||||
///
|
||||
/// For single-objective optimization, use [`Study`](crate::Study) instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -269,7 +301,11 @@ impl MultiObjectiveStudy {
|
||||
self.completed_trials.read().clone()
|
||||
}
|
||||
|
||||
/// Returns the Pareto-optimal trials (front 0).
|
||||
/// Return the Pareto-optimal trials (the non-dominated front).
|
||||
///
|
||||
/// Uses fast non-dominated sorting (Deb et al., 2002) from the
|
||||
/// [`pareto`](crate::pareto) module. Returns an empty vec if no
|
||||
/// trials have completed.
|
||||
#[must_use]
|
||||
pub fn pareto_front(&self) -> Vec<MultiObjectiveTrial> {
|
||||
let trials = self.completed_trials.read();
|
||||
|
||||
+25
-8
@@ -1,18 +1,35 @@
|
||||
//! Parameter value storage types.
|
||||
//! Raw parameter value storage.
|
||||
//!
|
||||
//! [`ParamValue`] is the type-erased representation of a sampled parameter.
|
||||
//! Users rarely construct `ParamValue` directly — the
|
||||
//! [`Parameter::suggest`](crate::parameter::Parameter::suggest) method returns
|
||||
//! the already-typed value (e.g., `f64` for [`FloatParam`](crate::parameter::FloatParam)).
|
||||
//!
|
||||
//! `ParamValue` is useful when inspecting raw trial data via
|
||||
//! [`Trial::params`](crate::Trial::params) or
|
||||
//! [`CompletedTrial::params`](crate::sampler::CompletedTrial).
|
||||
|
||||
/// Represents a sampled parameter value.
|
||||
/// A type-erased sampled parameter value.
|
||||
///
|
||||
/// This enum stores different parameter value types uniformly.
|
||||
/// For categorical parameters, the `Categorical` variant stores
|
||||
/// the index into the choices array.
|
||||
/// Stores float, integer, or categorical (index) values uniformly.
|
||||
/// For categorical parameters the `Categorical` variant stores the
|
||||
/// zero-based index into the choices array, not the choice itself.
|
||||
///
|
||||
/// # Display
|
||||
///
|
||||
/// `ParamValue` implements [`Display`](core::fmt::Display): floats and
|
||||
/// integers print their numeric value, and categoricals print `category(i)`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum ParamValue {
|
||||
/// A floating-point parameter value.
|
||||
/// A floating-point parameter value (from [`FloatParam`](crate::parameter::FloatParam)).
|
||||
Float(f64),
|
||||
/// An integer parameter value.
|
||||
/// An integer parameter value (from [`IntParam`](crate::parameter::IntParam)).
|
||||
Int(i64),
|
||||
/// A categorical parameter value, stored as an index into the choices array.
|
||||
/// A categorical index into the choices array (from
|
||||
/// [`CategoricalParam`](crate::parameter::CategoricalParam),
|
||||
/// [`BoolParam`](crate::parameter::BoolParam), or
|
||||
/// [`EnumParam`](crate::parameter::EnumParam)).
|
||||
Categorical(usize),
|
||||
}
|
||||
|
||||
|
||||
+102
-50
@@ -1,8 +1,19 @@
|
||||
//! Central parameter trait and built-in parameter types.
|
||||
//! Parameter trait and five built-in parameter types.
|
||||
//!
|
||||
//! The [`Parameter`] trait provides a unified way to define parameter types
|
||||
//! and suggest values from a [`Trial`]. Built-in implementations
|
||||
//! cover floats, integers, categoricals, booleans, and enum types.
|
||||
//! The [`Parameter`] trait provides a unified way to define search-space
|
||||
//! dimensions and sample values from a [`Trial`]. Five implementations
|
||||
//! cover the most common hyperparameter types:
|
||||
//!
|
||||
//! | Type | Sampled value | Typical use |
|
||||
//! |------|---------------|-------------|
|
||||
//! | [`FloatParam`] | `f64` | Learning rate, dropout probability |
|
||||
//! | [`IntParam`] | `i64` | Layer count, batch size |
|
||||
//! | [`CategoricalParam`] | `T: Clone` | Optimizer name, activation function |
|
||||
//! | [`BoolParam`] | `bool` | Feature toggle |
|
||||
//! | [`EnumParam`] | `T: Categorical` | Typed enum variant selection |
|
||||
//!
|
||||
//! All five types support `.name()` for a human-readable label and
|
||||
//! `.suggest(&mut trial)` as a shorthand for `trial.suggest_param(¶m)`.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
@@ -14,10 +25,17 @@
|
||||
//!
|
||||
//! let lr = FloatParam::new(1e-5, 1e-1)
|
||||
//! .log_scale()
|
||||
//! .name("learning_rate")
|
||||
//! .suggest(&mut trial)
|
||||
//! .unwrap();
|
||||
//! let layers = IntParam::new(1, 10)
|
||||
//! .name("n_layers")
|
||||
//! .suggest(&mut trial)
|
||||
//! .unwrap();
|
||||
//! let dropout = BoolParam::new()
|
||||
//! .name("use_dropout")
|
||||
//! .suggest(&mut trial)
|
||||
//! .unwrap();
|
||||
//! let layers = IntParam::new(1, 10).suggest(&mut trial).unwrap();
|
||||
//! let dropout = BoolParam::new().suggest(&mut trial).unwrap();
|
||||
//! ```
|
||||
|
||||
use core::fmt::Debug;
|
||||
@@ -42,7 +60,7 @@ static NEXT_PARAM_ID: AtomicU64 = AtomicU64::new(0);
|
||||
pub struct ParamId(u64);
|
||||
|
||||
impl ParamId {
|
||||
/// Creates a new unique `ParamId`.
|
||||
/// Create a new unique `ParamId`.
|
||||
pub fn new() -> Self {
|
||||
Self(NEXT_PARAM_ID.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
@@ -60,52 +78,67 @@ impl core::fmt::Display for ParamId {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for defining parameter types that can be suggested by a [`Trial`].
|
||||
/// Define a parameter type that can be suggested by a [`Trial`].
|
||||
///
|
||||
/// Implementors specify the distribution to sample from and how to convert
|
||||
/// the raw [`ParamValue`] back into a typed value.
|
||||
/// the raw [`ParamValue`] back into a typed value. See the five built-in
|
||||
/// implementations: [`FloatParam`], [`IntParam`], [`CategoricalParam`],
|
||||
/// [`BoolParam`], and [`EnumParam`].
|
||||
pub trait Parameter: Debug {
|
||||
/// The typed value returned after sampling.
|
||||
type Value;
|
||||
|
||||
/// Returns the unique identifier for this parameter.
|
||||
/// Return the unique identifier for this parameter.
|
||||
fn id(&self) -> ParamId;
|
||||
|
||||
/// Returns the distribution that this parameter samples from.
|
||||
/// Return the distribution that this parameter samples from.
|
||||
fn distribution(&self) -> Distribution;
|
||||
|
||||
/// Converts a raw [`ParamValue`] into the typed value.
|
||||
/// Convert a raw [`ParamValue`] into the typed value.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the `ParamValue` variant doesn't match what this parameter expects.
|
||||
/// Return an error if the `ParamValue` variant does not match what this parameter expects.
|
||||
fn cast_param_value(&self, param_value: &ParamValue) -> Result<Self::Value>;
|
||||
|
||||
/// Validates the parameter configuration.
|
||||
/// Validate the parameter configuration.
|
||||
///
|
||||
/// Called before sampling. The default implementation accepts all configurations.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the parameter configuration is invalid.
|
||||
/// Return an error if the parameter configuration is invalid.
|
||||
fn validate(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a human-readable label for this parameter.
|
||||
/// Return a human-readable label for this parameter.
|
||||
///
|
||||
/// Defaults to the `Debug` output of the parameter.
|
||||
/// Defaults to the `Debug` output of the parameter. Override with
|
||||
/// the `.name()` builder method on concrete types.
|
||||
fn label(&self) -> String {
|
||||
format!("{self:?}")
|
||||
}
|
||||
|
||||
/// Suggests a value for this parameter from the given trial.
|
||||
/// Suggest a value for this parameter from the given trial.
|
||||
///
|
||||
/// This is a convenience method that delegates to [`Trial::suggest_param`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// let param = FloatParam::new(-5.0, 5.0).name("x");
|
||||
/// let value: f64 = param.suggest(&mut trial).unwrap();
|
||||
/// assert!((-5.0..=5.0).contains(&value));
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if validation fails, the parameter conflicts with
|
||||
/// Return an error if validation fails, the parameter conflicts with
|
||||
/// a previously suggested parameter of the same id, or sampling fails.
|
||||
fn suggest(&self, trial: &mut Trial) -> Result<Self::Value>
|
||||
where
|
||||
@@ -117,7 +150,7 @@ pub trait Parameter: Debug {
|
||||
|
||||
/// A floating-point parameter with optional log-scale and step size.
|
||||
///
|
||||
/// # Example
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
@@ -128,13 +161,14 @@ pub trait Parameter: Debug {
|
||||
/// // Simple range
|
||||
/// let x = FloatParam::new(0.0, 1.0).suggest(&mut trial).unwrap();
|
||||
///
|
||||
/// // Log-scale
|
||||
/// // Log-scale with a human-readable name
|
||||
/// let lr = FloatParam::new(1e-5, 1e-1)
|
||||
/// .log_scale()
|
||||
/// .name("learning_rate")
|
||||
/// .suggest(&mut trial)
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Stepped
|
||||
/// // Stepped (values will be multiples of 0.25)
|
||||
/// let step = FloatParam::new(0.0, 1.0)
|
||||
/// .step(0.25)
|
||||
/// .suggest(&mut trial)
|
||||
@@ -151,7 +185,7 @@ pub struct FloatParam {
|
||||
}
|
||||
|
||||
impl FloatParam {
|
||||
/// Creates a new float parameter with the given bounds.
|
||||
/// Create a new float parameter sampling uniformly from `[low, high]`.
|
||||
#[must_use]
|
||||
pub fn new(low: f64, high: f64) -> Self {
|
||||
Self {
|
||||
@@ -164,21 +198,21 @@ impl FloatParam {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables log-scale sampling.
|
||||
/// Enable log-scale sampling (bounds must be positive).
|
||||
#[must_use]
|
||||
pub fn log_scale(mut self) -> Self {
|
||||
self.log_scale = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a step size for discretized sampling.
|
||||
/// Set a step size for discretized sampling.
|
||||
#[must_use]
|
||||
pub fn step(mut self, step: f64) -> Self {
|
||||
self.step = Some(step);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a human-readable name for this parameter.
|
||||
/// Set a human-readable name for this parameter.
|
||||
///
|
||||
/// When set, this name is used as the parameter's label instead of
|
||||
/// the default `Debug` output.
|
||||
@@ -245,7 +279,7 @@ impl Parameter for FloatParam {
|
||||
|
||||
/// An integer parameter with optional log-scale and step size.
|
||||
///
|
||||
/// # Example
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
@@ -254,15 +288,19 @@ impl Parameter for FloatParam {
|
||||
/// let mut trial = Trial::new(0);
|
||||
///
|
||||
/// // Simple range
|
||||
/// let n = IntParam::new(1, 10).suggest(&mut trial).unwrap();
|
||||
/// let n = IntParam::new(1, 10)
|
||||
/// .name("n_layers")
|
||||
/// .suggest(&mut trial)
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Log-scale
|
||||
/// let batch = IntParam::new(1, 1024)
|
||||
/// .log_scale()
|
||||
/// .name("batch_size")
|
||||
/// .suggest(&mut trial)
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Stepped
|
||||
/// // Stepped (multiples of 32)
|
||||
/// let units = IntParam::new(32, 512).step(32).suggest(&mut trial).unwrap();
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -276,7 +314,7 @@ pub struct IntParam {
|
||||
}
|
||||
|
||||
impl IntParam {
|
||||
/// Creates a new integer parameter with the given bounds.
|
||||
/// Create a new integer parameter sampling uniformly from `[low, high]`.
|
||||
#[must_use]
|
||||
pub fn new(low: i64, high: i64) -> Self {
|
||||
Self {
|
||||
@@ -289,21 +327,21 @@ impl IntParam {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables log-scale sampling.
|
||||
/// Enable log-scale sampling (bounds must be ≥ 1).
|
||||
#[must_use]
|
||||
pub fn log_scale(mut self) -> Self {
|
||||
self.log_scale = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a step size for discretized sampling.
|
||||
/// Set a step size for discretized sampling.
|
||||
#[must_use]
|
||||
pub fn step(mut self, step: i64) -> Self {
|
||||
self.step = Some(step);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a human-readable name for this parameter.
|
||||
/// Set a human-readable name for this parameter.
|
||||
///
|
||||
/// When set, this name is used as the parameter's label instead of
|
||||
/// the default `Debug` output.
|
||||
@@ -370,7 +408,10 @@ impl Parameter for IntParam {
|
||||
|
||||
/// A categorical parameter that selects from a list of choices.
|
||||
///
|
||||
/// # Example
|
||||
/// The generic type `T` is the element type of the choices vector.
|
||||
/// The sampler picks an index and the corresponding element is returned.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
@@ -378,6 +419,7 @@ impl Parameter for IntParam {
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// let opt = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"])
|
||||
/// .name("optimizer")
|
||||
/// .suggest(&mut trial)
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
@@ -389,7 +431,7 @@ pub struct CategoricalParam<T: Clone> {
|
||||
}
|
||||
|
||||
impl<T: Clone> CategoricalParam<T> {
|
||||
/// Creates a new categorical parameter with the given choices.
|
||||
/// Create a new categorical parameter with the given choices.
|
||||
#[must_use]
|
||||
pub fn new(choices: Vec<T>) -> Self {
|
||||
Self {
|
||||
@@ -399,7 +441,7 @@ impl<T: Clone> CategoricalParam<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a human-readable name for this parameter.
|
||||
/// Set a human-readable name for this parameter.
|
||||
///
|
||||
/// When set, this name is used as the parameter's label instead of
|
||||
/// the default `Debug` output.
|
||||
@@ -444,16 +486,19 @@ impl<T: Clone + Debug> Parameter for CategoricalParam<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A boolean parameter (equivalent to a categorical with `[false, true]`).
|
||||
/// A boolean parameter (equivalent to a two-choice categorical: `false` / `true`).
|
||||
///
|
||||
/// # Example
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
/// use optimizer::parameter::{BoolParam, Parameter};
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// let dropout = BoolParam::new().suggest(&mut trial).unwrap();
|
||||
/// let use_dropout = BoolParam::new()
|
||||
/// .name("use_dropout")
|
||||
/// .suggest(&mut trial)
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BoolParam {
|
||||
@@ -462,7 +507,7 @@ pub struct BoolParam {
|
||||
}
|
||||
|
||||
impl BoolParam {
|
||||
/// Creates a new boolean parameter.
|
||||
/// Create a new boolean parameter.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -471,7 +516,7 @@ impl BoolParam {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a human-readable name for this parameter.
|
||||
/// Set a human-readable name for this parameter.
|
||||
///
|
||||
/// When set, this name is used as the parameter's label instead of
|
||||
/// the default `Debug` output.
|
||||
@@ -513,10 +558,10 @@ impl Parameter for BoolParam {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for enum types that can be used as categorical parameters.
|
||||
/// Map an enum type to sequential indices for use as a categorical parameter.
|
||||
///
|
||||
/// This trait maps enum variants to sequential indices and back. It can be
|
||||
/// derived automatically for fieldless enums using `#[derive(Categorical)]`
|
||||
/// This trait converts enum variants to sequential indices and back. It can
|
||||
/// be derived automatically for fieldless enums using `#[derive(Categorical)]`
|
||||
/// when the `derive` feature is enabled.
|
||||
///
|
||||
/// # Example
|
||||
@@ -558,20 +603,24 @@ pub trait Categorical: Sized + Clone {
|
||||
/// The number of variants in the enum.
|
||||
const N_CHOICES: usize;
|
||||
|
||||
/// Creates an instance from a variant index.
|
||||
/// Create an instance from a variant index.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index >= N_CHOICES`.
|
||||
fn from_index(index: usize) -> Self;
|
||||
|
||||
/// Returns the index of this variant.
|
||||
/// Return the index of this variant.
|
||||
fn to_index(&self) -> usize;
|
||||
}
|
||||
|
||||
/// A parameter that selects from the variants of an enum implementing [`Categorical`].
|
||||
///
|
||||
/// # Example
|
||||
/// Prefer this over [`CategoricalParam`] when the choices map to a Rust enum,
|
||||
/// because the returned value is already the correct variant — no string
|
||||
/// matching required.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
@@ -604,7 +653,10 @@ pub trait Categorical: Sized + Clone {
|
||||
/// }
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// let opt = EnumParam::<Optimizer>::new().suggest(&mut trial).unwrap();
|
||||
/// let opt = EnumParam::<Optimizer>::new()
|
||||
/// .name("optimizer")
|
||||
/// .suggest(&mut trial)
|
||||
/// .unwrap();
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EnumParam<T: Categorical> {
|
||||
@@ -614,7 +666,7 @@ pub struct EnumParam<T: Categorical> {
|
||||
}
|
||||
|
||||
impl<T: Categorical> EnumParam<T> {
|
||||
/// Creates a new enum parameter.
|
||||
/// Create a new enum parameter over all variants of `T`.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -624,7 +676,7 @@ impl<T: Categorical> EnumParam<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a human-readable name for this parameter.
|
||||
/// Set a human-readable name for this parameter.
|
||||
///
|
||||
/// When set, this name is used as the parameter's label instead of
|
||||
/// the default `Debug` output.
|
||||
|
||||
+85
-18
@@ -1,15 +1,71 @@
|
||||
//! Pareto front analysis utilities for multi-objective optimization.
|
||||
//!
|
||||
//! Provides functions for analyzing and working with Pareto fronts:
|
||||
//! In multi-objective optimization there is generally no single best
|
||||
//! solution. Instead, the goal is to find the **Pareto front** — the set
|
||||
//! of solutions where no objective can be improved without worsening
|
||||
//! another. This module provides tools for computing and analyzing Pareto
|
||||
//! fronts.
|
||||
//!
|
||||
//! - [`hypervolume`] — measure the quality of a Pareto front
|
||||
//! - [`non_dominated_sort`] — rank solutions into successive fronts
|
||||
//! - [`pareto_front_indices`] — filter to non-dominated solutions only
|
||||
//! - [`crowding_distance`] — measure diversity within a front
|
||||
//! # Available functions
|
||||
//!
|
||||
//! Internally also provides fast non-dominated sorting (Deb et al., 2002)
|
||||
//! used by [`MultiObjectiveStudy::pareto_front()`](crate::MultiObjectiveStudy::pareto_front)
|
||||
//! | Function | Purpose |
|
||||
//! |---|---|
|
||||
//! | [`hypervolume`] | Measure the quality of a Pareto front (volume of dominated space) |
|
||||
//! | [`non_dominated_sort`] | Rank solutions into successive fronts (front 0, 1, …) |
|
||||
//! | [`pareto_front_indices`] | Filter to non-dominated (Pareto-optimal) solutions only |
|
||||
//! | [`crowding_distance`] | Measure diversity/spread within a single front |
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Evaluating front quality**: Use [`hypervolume`] to compare two
|
||||
//! Pareto fronts — a higher hypervolume indicates a better-quality front.
|
||||
//! - **Ranking all solutions**: Use [`non_dominated_sort`] to partition
|
||||
//! solutions into successive fronts, useful for selection in evolutionary
|
||||
//! algorithms.
|
||||
//! - **Extracting the best solutions**: Use [`pareto_front_indices`] to get
|
||||
//! only the non-dominated set.
|
||||
//! - **Diversity measurement**: Use [`crowding_distance`] to quantify how
|
||||
//! spread out solutions are within a front, which helps maintain diversity.
|
||||
//!
|
||||
//! Internally, this module also provides the fast non-dominated sorting
|
||||
//! algorithm (Deb et al., 2002) used by
|
||||
//! [`MultiObjectiveStudy::pareto_front()`](crate::MultiObjectiveStudy::pareto_front)
|
||||
//! and [`Nsga2Sampler`](crate::Nsga2Sampler).
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::Direction;
|
||||
//! use optimizer::pareto::{
|
||||
//! crowding_distance, hypervolume, non_dominated_sort, pareto_front_indices,
|
||||
//! };
|
||||
//!
|
||||
//! let solutions = vec![
|
||||
//! vec![1.0, 5.0], // Pareto-optimal
|
||||
//! vec![5.0, 1.0], // Pareto-optimal
|
||||
//! vec![3.0, 3.0], // Pareto-optimal
|
||||
//! vec![4.0, 4.0], // Dominated by (3, 3)
|
||||
//! ];
|
||||
//! let dirs = [Direction::Minimize, Direction::Minimize];
|
||||
//!
|
||||
//! // Non-dominated sorting: front 0 has indices {0, 1, 2}
|
||||
//! let fronts = non_dominated_sort(&solutions, &dirs);
|
||||
//! assert_eq!(fronts.len(), 2);
|
||||
//!
|
||||
//! // Pareto front indices (shortcut for fronts[0])
|
||||
//! let mut front = pareto_front_indices(&solutions, &dirs);
|
||||
//! front.sort();
|
||||
//! assert_eq!(front, vec![0, 1, 2]);
|
||||
//!
|
||||
//! // Hypervolume with reference point (6, 6)
|
||||
//! let front_values: Vec<_> = front.iter().map(|&i| solutions[i].clone()).collect();
|
||||
//! let hv = hypervolume(&front_values, &[6.0, 6.0], &dirs);
|
||||
//! assert!(hv > 0.0);
|
||||
//!
|
||||
//! // Crowding distance for diversity analysis
|
||||
//! let cd = crowding_distance(&front_values, &dirs);
|
||||
//! assert!(cd[0].is_infinite()); // boundary solution
|
||||
//! ```
|
||||
|
||||
use crate::types::Direction;
|
||||
|
||||
@@ -200,12 +256,17 @@ pub(crate) fn crowding_distance_indexed(front_indices: &[usize], values: &[Vec<f
|
||||
/// Compute the hypervolume indicator of a Pareto front.
|
||||
///
|
||||
/// The hypervolume is the volume of the objective space dominated by
|
||||
/// the Pareto front and bounded by a reference point. Higher values
|
||||
/// indicate a better front.
|
||||
/// the Pareto front and bounded by a reference point. A **higher**
|
||||
/// hypervolume indicates a better front (closer to the ideal and more
|
||||
/// spread out).
|
||||
///
|
||||
/// Each entry in `front` is one solution's objective values.
|
||||
/// `reference_point` should be worse than all front members in every
|
||||
/// objective (e.g., the worst acceptable values).
|
||||
/// objective (e.g., the worst acceptable values). Solutions that do
|
||||
/// not strictly dominate the reference point are ignored.
|
||||
///
|
||||
/// Uses recursive slicing for dimensions > 1. Complexity grows with
|
||||
/// the number of objectives and front size.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
@@ -345,12 +406,14 @@ fn non_dominated_minimize(points: &[Vec<f64>]) -> Vec<Vec<f64>> {
|
||||
|
||||
/// Compute non-dominated sorting of a set of solutions.
|
||||
///
|
||||
/// Returns a vec of fronts, where `fronts[0]` is the Pareto front,
|
||||
/// `fronts[1]` is the next best, etc. Each inner vec contains indices
|
||||
/// into the original `solutions` slice.
|
||||
/// Return a vec of fronts, where `fronts[0]` is the Pareto front
|
||||
/// (non-dominated solutions), `fronts[1]` is the next-best front
|
||||
/// (dominated only by front 0), and so on. Each inner vec contains
|
||||
/// indices into the original `solutions` slice.
|
||||
///
|
||||
/// Uses the fast non-dominated sorting algorithm from
|
||||
/// Deb et al. (2002) with O(M N²) complexity.
|
||||
/// Use the fast non-dominated sorting algorithm from
|
||||
/// Deb et al. (2002) with O(M × N²) complexity, where M is the
|
||||
/// number of objectives and N is the number of solutions.
|
||||
#[must_use]
|
||||
pub fn non_dominated_sort(solutions: &[Vec<f64>], directions: &[Direction]) -> Vec<Vec<usize>> {
|
||||
fast_non_dominated_sort(solutions, directions)
|
||||
@@ -359,7 +422,8 @@ pub fn non_dominated_sort(solutions: &[Vec<f64>], directions: &[Direction]) -> V
|
||||
/// Filter solutions to return only non-dominated (Pareto-optimal) indices.
|
||||
///
|
||||
/// Equivalent to `non_dominated_sort(solutions, directions)[0]` but
|
||||
/// communicates the intent more clearly.
|
||||
/// communicates the intent more clearly. Use this when you only need
|
||||
/// the Pareto front and not the full ranking.
|
||||
#[must_use]
|
||||
pub fn pareto_front_indices(solutions: &[Vec<f64>], directions: &[Direction]) -> Vec<usize> {
|
||||
let fronts = fast_non_dominated_sort(solutions, directions);
|
||||
@@ -368,10 +432,13 @@ pub fn pareto_front_indices(solutions: &[Vec<f64>], directions: &[Direction]) ->
|
||||
|
||||
/// Compute crowding distance for diversity measurement.
|
||||
///
|
||||
/// Returns one distance value per solution in `front` (same order).
|
||||
/// Return one distance value per solution in `front` (same order).
|
||||
/// Boundary solutions (best/worst in any objective) receive
|
||||
/// [`f64::INFINITY`]. Interior solutions get a finite positive value
|
||||
/// proportional to the gap between their neighbors.
|
||||
/// proportional to the gap between their neighbors in each objective.
|
||||
///
|
||||
/// Crowding distance is used by NSGA-II to prefer well-spread
|
||||
/// solutions when two solutions are in the same front.
|
||||
///
|
||||
/// `directions` is accepted for API consistency but does not affect
|
||||
/// the result, since crowding distance measures spacing regardless of
|
||||
|
||||
+45
-1
@@ -1,3 +1,47 @@
|
||||
//! `HyperBand` pruner — adaptive budget scheduling with multiple SHA brackets.
|
||||
//!
|
||||
//! `HyperBand` addresses the main weakness of
|
||||
//! [`SuccessiveHalvingPruner`](super::SuccessiveHalvingPruner): sensitivity to
|
||||
//! the `min_resource` setting. It runs multiple Successive Halving brackets in
|
||||
//! parallel, each with a different trade-off between the number of trials and
|
||||
//! the starting budget:
|
||||
//!
|
||||
//! - **Bracket 0**: many trials, small starting budget (aggressive early pruning)
|
||||
//! - **Bracket `s_max`**: few trials, full budget (no pruning)
|
||||
//!
|
||||
//! Trials are assigned to brackets in round-robin order. This ensures that
|
||||
//! the overall search is robust regardless of how informative early steps are.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - When you don't know how many epochs/steps are needed before performance
|
||||
//! becomes predictive
|
||||
//! - As a drop-in upgrade over [`SuccessiveHalvingPruner`](super::SuccessiveHalvingPruner)
|
||||
//! when you can afford more total trials
|
||||
//! - For large-scale hyperparameter searches where compute savings matter most
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `min_resource` | 1 | Smallest budget for the most aggressive bracket |
|
||||
//! | `max_resource` | 81 | Full budget (last rung in every bracket) |
|
||||
//! | `reduction_factor` | 3 | At each rung, keep top 1/η trials |
|
||||
//! | `direction` | `Minimize` | Optimization direction |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::Direction;
|
||||
//! use optimizer::pruner::HyperbandPruner;
|
||||
//!
|
||||
//! let pruner = HyperbandPruner::new()
|
||||
//! .min_resource(1)
|
||||
//! .max_resource(81)
|
||||
//! .reduction_factor(3)
|
||||
//! .direction(Direction::Minimize);
|
||||
//! ```
|
||||
|
||||
use core::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
@@ -6,7 +50,7 @@ use super::Pruner;
|
||||
use crate::sampler::CompletedTrial;
|
||||
use crate::types::{Direction, TrialState};
|
||||
|
||||
/// Hyperband pruner that manages multiple Successive Halving brackets.
|
||||
/// `HyperBand` pruner that manages multiple Successive Halving brackets.
|
||||
///
|
||||
/// Hyperband addresses SHA's sensitivity to the `min_resource` choice by
|
||||
/// running multiple brackets, each with a different tradeoff between the
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
//! Median pruner — the recommended default pruner for most use cases.
|
||||
//!
|
||||
//! At each step, the current trial's intermediate value is compared against
|
||||
//! the median of all completed trials' values at the same step. Trials
|
||||
//! performing worse than the median are pruned.
|
||||
//!
|
||||
//! This is a convenience wrapper around [`PercentilePruner`](super::PercentilePruner)
|
||||
//! with a fixed percentile of 50%.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Default choice** for any iterative objective (e.g., neural network training)
|
||||
//! - Works well when intermediate values are a reasonable proxy for final performance
|
||||
//! - Prunes roughly half of unpromising trials, giving a good speed/accuracy balance
|
||||
//!
|
||||
//! If your intermediate values are noisy, consider [`WilcoxonPruner`](super::WilcoxonPruner)
|
||||
//! or wrapping this pruner in a [`PatientPruner`](super::PatientPruner).
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `n_warmup_steps` | 0 | Skip pruning in the first N steps |
|
||||
//! | `n_min_trials` | 1 | Require at least N completed trials before pruning |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::Direction;
|
||||
//! use optimizer::pruner::MedianPruner;
|
||||
//!
|
||||
//! let pruner = MedianPruner::new(Direction::Minimize)
|
||||
//! .n_warmup_steps(5)
|
||||
//! .n_min_trials(3);
|
||||
//! ```
|
||||
|
||||
use super::Pruner;
|
||||
use super::percentile::compute_percentile;
|
||||
use crate::sampler::CompletedTrial;
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
//! Pruners decide whether to stop (prune) a trial early based on its
|
||||
//! intermediate values compared to other trials. This is useful for
|
||||
//! discarding unpromising trials before they complete, saving compute.
|
||||
//!
|
||||
//! # How pruning works
|
||||
//!
|
||||
//! During optimization, each trial reports intermediate values at discrete
|
||||
//! steps (e.g., validation loss after each training epoch). A pruner inspects
|
||||
//! these values and compares them against completed trials to decide whether
|
||||
//! the current trial should be stopped early.
|
||||
//!
|
||||
//! The typical flow is:
|
||||
//!
|
||||
//! 1. Call [`Trial::report`](crate::Trial::report) to record an intermediate value.
|
||||
//! 2. Call [`Trial::should_prune`](crate::Trial::should_prune) to check the pruner's decision.
|
||||
//! 3. If the pruner says prune, return [`TrialPruned`](crate::TrialPruned) from the objective.
|
||||
//!
|
||||
//! # Available pruners
|
||||
//!
|
||||
//! | Pruner | Algorithm | Best for |
|
||||
//! |--------|-----------|----------|
|
||||
//! | [`MedianPruner`] | Prune below median at each step | General-purpose default |
|
||||
//! | [`PercentilePruner`] | Prune below configurable percentile | Tunable aggressiveness |
|
||||
//! | [`ThresholdPruner`] | Prune outside fixed bounds | Known divergence limits |
|
||||
//! | [`PatientPruner`] | Require N consecutive prune signals | Noisy intermediate values |
|
||||
//! | [`SuccessiveHalvingPruner`] | Keep top 1/η fraction at each rung | Budget-aware pruning |
|
||||
//! | [`HyperbandPruner`] | Multiple SHA brackets with different budgets | Robust to budget choice |
|
||||
//! | [`WilcoxonPruner`] | Statistical signed-rank test vs. best trial | Rigorous noisy pruning |
|
||||
//! | [`NopPruner`] | Never prune | Disabling pruning explicitly |
|
||||
//!
|
||||
//! # When to use pruning
|
||||
//!
|
||||
//! Pruning is most beneficial when:
|
||||
//!
|
||||
//! - The objective function has a natural notion of "steps" (e.g., training epochs)
|
||||
//! - Early steps are informative about final performance
|
||||
//! - Trials are expensive enough that stopping bad ones early saves significant time
|
||||
//!
|
||||
//! Start with [`MedianPruner`] for most use cases. Switch to [`WilcoxonPruner`]
|
||||
//! if your intermediate values are noisy, or to [`HyperbandPruner`] if you want
|
||||
//! automatic budget scheduling.
|
||||
|
||||
mod hyperband;
|
||||
mod median;
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
//! No-op pruner — never prune any trial.
|
||||
//!
|
||||
//! This is the default pruner used when no pruner is configured on a
|
||||
//! [`Study`](crate::Study). It unconditionally returns `false` for every
|
||||
//! pruning decision, allowing all trials to run to completion.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - When you want to explicitly disable pruning
|
||||
//! - As a baseline to compare against other pruners
|
||||
//! - Already used by default — you rarely need to configure this manually
|
||||
|
||||
use super::Pruner;
|
||||
use crate::sampler::CompletedTrial;
|
||||
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
//! Patient pruner — require consecutive prune signals before actually pruning.
|
||||
//!
|
||||
//! Wraps any other pruner and adds a patience window: the inner pruner
|
||||
//! must recommend pruning for `patience` consecutive steps before the
|
||||
//! trial is actually pruned. This prevents premature pruning when
|
||||
//! intermediate values are noisy and a single bad step doesn't indicate
|
||||
//! a truly bad trial.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - When your intermediate values have high variance (e.g., mini-batch loss)
|
||||
//! - When the inner pruner is too aggressive on its own
|
||||
//! - To add robustness to any statistical pruner without changing its threshold
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `inner` | *(required)* | The underlying pruner to wrap |
|
||||
//! | `patience` | *(required)* | Number of consecutive prune signals required |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::pruner::{PatientPruner, ThresholdPruner};
|
||||
//!
|
||||
//! // Only prune after the threshold is exceeded 3 times in a row
|
||||
//! let inner = ThresholdPruner::new().upper(100.0);
|
||||
//! let pruner = PatientPruner::new(inner, 3);
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
//! Percentile pruner — prune trials outside the top N% at each step.
|
||||
//!
|
||||
//! A generalization of [`MedianPruner`](super::MedianPruner) that lets you
|
||||
//! control how aggressively to prune. At each step, the current trial's
|
||||
//! intermediate value is compared against the given percentile of all
|
||||
//! completed trials' values at the same step.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - When you want finer control over pruning aggressiveness than median pruning
|
||||
//! - Lower percentiles (e.g., 25%) are more aggressive — only keep the best quarter
|
||||
//! - Higher percentiles (e.g., 75%) are more lenient — keep the top three quarters
|
||||
//! - Percentile 50% is equivalent to [`MedianPruner`](super::MedianPruner)
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `percentile` | *(required)* | Keep trials in the top N% — range `(0, 100)` |
|
||||
//! | `n_warmup_steps` | 0 | Skip pruning in the first N steps |
|
||||
//! | `n_min_trials` | 1 | Require at least N completed trials before pruning |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::Direction;
|
||||
//! use optimizer::pruner::PercentilePruner;
|
||||
//!
|
||||
//! // Keep only the top 25% of trials (aggressive pruning)
|
||||
//! let pruner = PercentilePruner::new(25.0, Direction::Minimize)
|
||||
//! .n_warmup_steps(5)
|
||||
//! .n_min_trials(3);
|
||||
//! ```
|
||||
|
||||
use super::Pruner;
|
||||
use crate::sampler::CompletedTrial;
|
||||
use crate::types::{Direction, TrialState};
|
||||
|
||||
@@ -1,3 +1,53 @@
|
||||
//! Successive Halving (SHA) pruner — budget-aware pruning at exponential rungs.
|
||||
//!
|
||||
//! Trials are evaluated at exponentially-spaced "rungs" (checkpoints). At each
|
||||
//! rung, only the top 1/η fraction of trials survive to the next rung. This
|
||||
//! is a principled way to allocate compute budget: give many trials a small
|
||||
//! budget, then progressively invest more in the best ones.
|
||||
//!
|
||||
//! For example, with `min_resource=1`, `max_resource=81`, `reduction_factor=3`:
|
||||
//!
|
||||
//! | Rung | Step | Survivors |
|
||||
//! |------|------|-----------|
|
||||
//! | 0 | 1 | top 1/3 |
|
||||
//! | 1 | 3 | top 1/3 |
|
||||
//! | 2 | 9 | top 1/3 |
|
||||
//! | 3 | 27 | top 1/3 |
|
||||
//! | 4 | 81 | all (full budget) |
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - When your objective has a natural "budget" dimension (epochs, iterations)
|
||||
//! - When early performance is a reasonable predictor of final performance
|
||||
//! - When you want a principled alternative to median pruning
|
||||
//!
|
||||
//! If you're unsure about the right `min_resource`, consider
|
||||
//! [`HyperbandPruner`](super::HyperbandPruner) which runs multiple brackets
|
||||
//! to hedge against that choice.
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `min_resource` | 1 | Step at which the first rung is placed |
|
||||
//! | `max_resource` | 81 | Full budget (final rung, no pruning) |
|
||||
//! | `reduction_factor` | 3 | At each rung, keep top 1/η trials |
|
||||
//! | `min_early_stopping_rate` | 0 | Skip the first N rungs |
|
||||
//! | `direction` | `Minimize` | Optimization direction |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::Direction;
|
||||
//! use optimizer::pruner::SuccessiveHalvingPruner;
|
||||
//!
|
||||
//! let pruner = SuccessiveHalvingPruner::new()
|
||||
//! .min_resource(1)
|
||||
//! .max_resource(81)
|
||||
//! .reduction_factor(3)
|
||||
//! .direction(Direction::Minimize);
|
||||
//! ```
|
||||
|
||||
use super::Pruner;
|
||||
use crate::sampler::CompletedTrial;
|
||||
use crate::types::{Direction, TrialState};
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
//! Threshold pruner — prune trials whose values fall outside fixed bounds.
|
||||
//!
|
||||
//! Unlike statistical pruners that compare against other trials, the
|
||||
//! threshold pruner uses absolute bounds. Any trial whose latest
|
||||
//! intermediate value exceeds the upper bound or falls below the lower
|
||||
//! bound is pruned immediately.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - When you know hard limits for valid intermediate values (e.g., loss should
|
||||
//! never exceed 100.0)
|
||||
//! - To catch diverging or NaN-producing trials early
|
||||
//! - Often combined with other pruners via [`PatientPruner`](super::PatientPruner)
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `upper` | `None` | Prune if value exceeds this bound |
|
||||
//! | `lower` | `None` | Prune if value falls below this bound |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::pruner::ThresholdPruner;
|
||||
//!
|
||||
//! // Prune if loss exceeds 100.0 or accuracy drops below 0.0
|
||||
//! let pruner = ThresholdPruner::new().upper(100.0).lower(0.0);
|
||||
//! ```
|
||||
|
||||
use super::Pruner;
|
||||
use crate::sampler::CompletedTrial;
|
||||
|
||||
|
||||
@@ -1,3 +1,44 @@
|
||||
//! Wilcoxon pruner — statistically rigorous pruning for noisy objectives.
|
||||
//!
|
||||
//! Uses the Wilcoxon signed-rank test to compare the current trial's
|
||||
//! intermediate values against the best completed trial at matching steps.
|
||||
//! The test accounts for the paired, step-aligned nature of the comparison
|
||||
//! and only prunes when the difference is statistically significant.
|
||||
//!
|
||||
//! This is more principled than [`MedianPruner`](super::MedianPruner) for
|
||||
//! noisy objectives because a single bad step won't trigger pruning — the
|
||||
//! test considers the full distribution of paired differences.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - When intermediate values have high variance (e.g., mini-batch loss,
|
||||
//! stochastic reward signals)
|
||||
//! - When you want a statistical guarantee that pruned trials are truly worse
|
||||
//! - When you have enough steps (at least 6) for a meaningful test
|
||||
//!
|
||||
//! For less noisy objectives, [`MedianPruner`](super::MedianPruner) is simpler
|
||||
//! and often sufficient.
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `p_value_threshold` | 0.05 | Significance level — lower is more conservative |
|
||||
//! | `n_warmup_steps` | 0 | Skip pruning in the first N steps |
|
||||
//! | `n_min_trials` | 1 | Require at least N completed trials before pruning |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::Direction;
|
||||
//! use optimizer::pruner::WilcoxonPruner;
|
||||
//!
|
||||
//! let pruner = WilcoxonPruner::new(Direction::Minimize)
|
||||
//! .p_value_threshold(0.05)
|
||||
//! .n_warmup_steps(5)
|
||||
//! .n_min_trials(1);
|
||||
//! ```
|
||||
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use super::Pruner;
|
||||
|
||||
+52
-6
@@ -1,13 +1,13 @@
|
||||
//! BOHB (Bayesian Optimization + `HyperBand`) sampler.
|
||||
//!
|
||||
//! BOHB combines TPE's model-guided sampling with Hyperband's budget-aware
|
||||
//! BOHB combines TPE's model-guided sampling with `HyperBand`'s budget-aware
|
||||
//! evaluation. Instead of building one global TPE model, BOHB conditions
|
||||
//! its TPE model on trials evaluated at a specific budget level, giving
|
||||
//! better-calibrated proposals for each rung of the Hyperband schedule.
|
||||
//! better-calibrated proposals for each rung of the `HyperBand` schedule.
|
||||
//!
|
||||
//! # How it works
|
||||
//!
|
||||
//! 1. Compute all Hyperband rung steps (budget levels) from the config.
|
||||
//! 1. Compute all `HyperBand` rung steps (budget levels) from the config.
|
||||
//! 2. On each `sample()` call, scan the history's `intermediate_values`
|
||||
//! to find the **largest budget level** with enough observations
|
||||
//! (`>= min_points_in_model`).
|
||||
@@ -16,6 +16,26 @@
|
||||
//! 4. Delegate to an internal [`TpeSampler`] for the actual sampling.
|
||||
//! 5. Fall back to random sampling if no budget level has enough data.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - You are tuning hyperparameters for models that support early stopping
|
||||
//! (e.g., neural networks with configurable epoch counts).
|
||||
//! - You want to combine model-guided search with aggressive pruning of
|
||||
//! unpromising configurations.
|
||||
//! - Your objective has a natural "budget" axis (epochs, iterations, data
|
||||
//! fraction) reported via [`Trial::report`](crate::Trial::report).
|
||||
//!
|
||||
//! Pair `BohbSampler` with [`matching_pruner`](BohbSampler::matching_pruner)
|
||||
//! to get a `HyperBandPruner` whose budget schedule is consistent with
|
||||
//! the sampler's conditioning levels.
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! - `min_resource` / `max_resource` — budget range (default: 1 … 81)
|
||||
//! - `reduction_factor` (η) — successive halving factor (default: 3)
|
||||
//! - `min_points_in_model` — observations needed before TPE replaces random (default: 10)
|
||||
//! - All [`TpeSamplerBuilder`](super::tpe::TpeSamplerBuilder) options (gamma, seed, etc.)
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
@@ -27,7 +47,7 @@
|
||||
//! let study: Study<f64> = Study::with_sampler_and_pruner(Direction::Minimize, bohb, pruner);
|
||||
//! ```
|
||||
//!
|
||||
//! Using the builder for custom configuration:
|
||||
//! Custom configuration via builder:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::bohb::BohbSampler;
|
||||
@@ -50,7 +70,7 @@ use crate::sampler::tpe::TpeSampler;
|
||||
use crate::sampler::{CompletedTrial, Sampler};
|
||||
use crate::types::Direction;
|
||||
|
||||
/// A BOHB sampler that combines TPE with Hyperband budget awareness.
|
||||
/// A BOHB sampler that combines TPE with `HyperBand` budget awareness.
|
||||
///
|
||||
/// BOHB filters trial history by budget level before delegating to TPE,
|
||||
/// so the surrogate model is conditioned on trials evaluated at the same
|
||||
@@ -58,7 +78,25 @@ use crate::types::Direction;
|
||||
/// than using a single global model across all budgets.
|
||||
///
|
||||
/// Use [`BohbSampler::matching_pruner`] to create a [`HyperbandPruner`]
|
||||
/// with matching parameters.
|
||||
/// with matching `HyperBand` parameters.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::bohb::BohbSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let bohb = BohbSampler::builder()
|
||||
/// .min_resource(1)
|
||||
/// .max_resource(27)
|
||||
/// .reduction_factor(3)
|
||||
/// .seed(42)
|
||||
/// .build()
|
||||
/// .unwrap();
|
||||
/// let pruner = bohb.matching_pruner(Direction::Minimize);
|
||||
/// let study: Study<f64> = Study::with_sampler_and_pruner(Direction::Minimize, bohb, pruner);
|
||||
/// ```
|
||||
pub struct BohbSampler {
|
||||
min_resource: u64,
|
||||
max_resource: u64,
|
||||
@@ -225,6 +263,14 @@ impl Sampler for BohbSampler {
|
||||
|
||||
/// Builder for configuring a [`BohbSampler`].
|
||||
///
|
||||
/// # Defaults
|
||||
///
|
||||
/// - `min_resource`: 1
|
||||
/// - `max_resource`: 81
|
||||
/// - `reduction_factor`: 3 (η)
|
||||
/// - `min_points_in_model`: 10
|
||||
/// - TPE: default settings (gamma = 0.25, etc.)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
|
||||
+56
-10
@@ -1,15 +1,55 @@
|
||||
//! CMA-ES (Covariance Matrix Adaptation Evolution Strategy) sampler.
|
||||
//!
|
||||
//! CMA-ES maintains a multivariate Gaussian distribution over continuous
|
||||
//! parameters and adapts its mean, covariance matrix, and step-size based
|
||||
//! on trial rankings. It is one of the most effective derivative-free
|
||||
//! optimizers for continuous search spaces.
|
||||
//! CMA-ES is a stochastic, population-based optimizer that maintains a
|
||||
//! multivariate Gaussian distribution over continuous parameters and adapts
|
||||
//! its **mean**, **covariance matrix**, and **step-size** (σ) based on
|
||||
//! trial rankings. It is widely regarded as one of the most effective
|
||||
//! derivative-free optimizers for continuous search spaces.
|
||||
//!
|
||||
//! Categorical parameters are sampled uniformly at random (not part of
|
||||
//! the CMA-ES vector). If all parameters are categorical, the sampler
|
||||
//! falls back to pure random sampling.
|
||||
//! # Algorithm overview
|
||||
//!
|
||||
//! Requires the `cma-es` feature flag.
|
||||
//! Each generation:
|
||||
//! 1. **Sample** λ (population size) candidates from N(m, σ²C).
|
||||
//! 2. **Evaluate** and **rank** the candidates by objective value.
|
||||
//! 3. **Update** the mean toward the best μ candidates (weighted recombination).
|
||||
//! 4. **Adapt** the covariance matrix C via rank-one and rank-μ updates, and
|
||||
//! adapt σ via cumulative step-size adaptation (CSA).
|
||||
//!
|
||||
//! Over time the search distribution narrows and rotates to align with the
|
||||
//! landscape, efficiently exploiting structure in the objective function.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Continuous parameters only** (float/int). Categorical parameters are
|
||||
//! sampled uniformly at random and do not participate in the CMA-ES model.
|
||||
//! - **Moderate dimensionality** — works well up to ~100 continuous dimensions.
|
||||
//! Beyond that, the O(n²) covariance matrix becomes expensive to maintain.
|
||||
//! - **Non-separable objectives** — CMA-ES learns parameter correlations
|
||||
//! through the covariance matrix, making it especially effective on
|
||||
//! rotated or ill-conditioned landscapes.
|
||||
//! - **Moderate evaluation budgets** — typically needs ≈10×n to 100×n
|
||||
//! evaluations to converge, where n is the number of continuous dimensions.
|
||||
//!
|
||||
//! For very cheap evaluations in low dimensions (d ≤ 20), consider
|
||||
//! [`GpSampler`](super::gp::GpSampler) instead. For high-dimensional
|
||||
//! separable problems, TPE may be more efficient.
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `sigma0` | `avg_range / 4` | Initial step size — controls exploration breadth |
|
||||
//! | `population_size` | `4 + ⌊3 ln n⌋` | Candidates per generation (λ) |
|
||||
//! | `seed` | random | RNG seed for reproducibility |
|
||||
//!
|
||||
//! # Feature flag
|
||||
//!
|
||||
//! Requires the **`cma-es`** feature (adds the `nalgebra` dependency):
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! optimizer = { version = "...", features = ["cma-es"] }
|
||||
//! ```
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -17,8 +57,14 @@
|
||||
//! use optimizer::sampler::cma_es::CmaEsSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let sampler = CmaEsSampler::with_seed(42);
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! // Minimize a 2-D sphere function with CMA-ES
|
||||
//! let sampler = CmaEsSampler::builder()
|
||||
//! .sigma0(1.0)
|
||||
//! .population_size(10)
|
||||
//! .seed(42)
|
||||
//! .build();
|
||||
//!
|
||||
//! let mut study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -1,22 +1,66 @@
|
||||
//! Differential Evolution (DE) sampler.
|
||||
//!
|
||||
//! DE is a population-based metaheuristic that maintains a population of
|
||||
//! candidate solutions and creates new candidates by combining (mutating +
|
||||
//! crossing over) existing ones. It is competitive with CMA-ES on many
|
||||
//! problems and simpler to implement.
|
||||
//! DE is a population-based metaheuristic that maintains a pool of candidate
|
||||
//! solutions and creates new candidates through **mutation** (combining
|
||||
//! difference vectors of existing members) and **binomial crossover**. A
|
||||
//! trial vector replaces its parent only if it achieves a better objective
|
||||
//! value, guaranteeing monotonic improvement of the population.
|
||||
//!
|
||||
//! Categorical parameters are sampled uniformly at random (not part of the
|
||||
//! DE vector). If all parameters are categorical, the sampler falls back to
|
||||
//! pure random sampling.
|
||||
//! # Algorithm overview
|
||||
//!
|
||||
//! Each generation, for every population member *xᵢ*:
|
||||
//! 1. **Mutation** — create a mutant vector *v* from other population
|
||||
//! members using the selected [`DifferentialEvolutionStrategy`]:
|
||||
//! - `Rand1`: `v = x_r1 + F * (x_r2 - x_r3)`
|
||||
//! - `Best1`: `v = x_best + F * (x_r1 - x_r2)`
|
||||
//! - `CurrentToBest1`: `v = x_i + F * (x_best - x_i) + F * (x_r1 - x_r2)`
|
||||
//! 2. **Crossover** — create a trial vector *u* by mixing *v* and *xᵢ*
|
||||
//! dimension-by-dimension with probability CR.
|
||||
//! 3. **Selection** — replace *xᵢ* with *u* if `f(u) ≤ f(xᵢ)`.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Continuous parameters** (float/int). Categorical parameters are
|
||||
//! sampled uniformly at random and do not participate in DE.
|
||||
//! - **Moderate to large search spaces** — DE scales better than GP-based
|
||||
//! methods to higher dimensions, though it may need more evaluations.
|
||||
//! - **Multi-modal landscapes** — the `Rand1` strategy maintains diversity
|
||||
//! and avoids premature convergence.
|
||||
//! - **No feature flags required** — DE is available with default features.
|
||||
//!
|
||||
//! For non-separable problems in moderate dimensions, consider
|
||||
//! [`CmaEsSampler`](super::cma_es::CmaEsSampler) which learns parameter
|
||||
//! correlations. For expensive functions with few dimensions, consider
|
||||
//! [`GpSampler`](super::gp::GpSampler).
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `population_size` | `max(10n, 15)` | Candidates per generation |
|
||||
//! | `mutation_factor` (F) | 0.8 | Differential amplification — higher = more exploration |
|
||||
//! | `crossover_rate` (CR) | 0.9 | Probability of taking a dimension from the mutant |
|
||||
//! | `strategy` | `Rand1` | Mutation strategy (see [`DifferentialEvolutionStrategy`]) |
|
||||
//! | `seed` | random | RNG seed for reproducibility |
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::differential_evolution::DifferentialEvolutionSampler;
|
||||
//! use optimizer::sampler::differential_evolution::{
|
||||
//! DifferentialEvolutionSampler, DifferentialEvolutionStrategy,
|
||||
//! };
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! // Minimize with DE using the Best1 strategy for faster convergence
|
||||
//! let sampler = DifferentialEvolutionSampler::builder()
|
||||
//! .mutation_factor(0.7)
|
||||
//! .crossover_rate(0.9)
|
||||
//! .strategy(DifferentialEvolutionStrategy::Best1)
|
||||
//! .population_size(20)
|
||||
//! .seed(42)
|
||||
//! .build();
|
||||
//!
|
||||
//! let mut study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
+59
-8
@@ -1,15 +1,60 @@
|
||||
//! Gaussian Process (GP) sampler with Expected Improvement acquisition.
|
||||
//!
|
||||
//! A classical Bayesian optimization sampler that uses a Gaussian Process
|
||||
//! surrogate model with a Matérn 5/2 kernel and Expected Improvement (EI)
|
||||
//! A classical Bayesian optimization sampler that builds a Gaussian Process
|
||||
//! surrogate model with a **Matérn 5/2 kernel** (with ARD lengthscales) and
|
||||
//! selects the next trial by maximizing the **Expected Improvement (EI)**
|
||||
//! acquisition function. Best suited for small, expensive evaluations in
|
||||
//! low-dimensional continuous spaces (d ≤ 20).
|
||||
//!
|
||||
//! Categorical parameters are sampled uniformly at random (not part of
|
||||
//! the GP model). If all parameters are categorical, the sampler falls
|
||||
//! back to pure random sampling.
|
||||
//! # Algorithm overview
|
||||
//!
|
||||
//! Requires the `gp` feature flag.
|
||||
//! 1. **Startup phase** — the first `n_startup_trials` trials are sampled
|
||||
//! uniformly at random to build an initial dataset.
|
||||
//! 2. **Fit GP** — training observations are standardized (zero mean, unit
|
||||
//! variance) and a GP with Matérn 5/2 kernel is fitted via Cholesky
|
||||
//! decomposition. ARD lengthscales are set to the per-dimension standard
|
||||
//! deviation of the training inputs.
|
||||
//! 3. **Maximize EI** — `n_candidates` random points are evaluated under
|
||||
//! the GP posterior and the point with the highest Expected Improvement
|
||||
//! is returned as the next trial.
|
||||
//!
|
||||
//! The GP uses at most 100 training points (the most recent ones) to keep
|
||||
//! the O(n³) fitting cost manageable.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Expensive objective functions** where every evaluation is costly
|
||||
//! (e.g. physical experiments, large simulations). The GP surrogate
|
||||
//! amortizes this cost by making fewer evaluations.
|
||||
//! - **Low-dimensional continuous spaces** — typically d ≤ 20. Beyond that,
|
||||
//! the GP becomes unreliable and alternatives like
|
||||
//! [`CmaEsSampler`](super::cma_es::CmaEsSampler) or
|
||||
//! [`TpeSampler`](super::tpe::TpeSampler) are preferable.
|
||||
//! - **Smooth, low-noise objectives** — the GP assumes smoothness through
|
||||
//! the Matérn 5/2 kernel. Very noisy objectives require increasing
|
||||
//! `noise_variance`.
|
||||
//!
|
||||
//! Categorical parameters are sampled uniformly at random and do not
|
||||
//! participate in the GP model. If all parameters are categorical, the
|
||||
//! sampler falls back to pure random sampling.
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |--------|---------|-------------|
|
||||
//! | `n_startup_trials` | 10 | Random trials before GP-guided sampling begins |
|
||||
//! | `n_candidates` | 1000 | Random candidates for EI maximization |
|
||||
//! | `noise_variance` | 1e-6 | Observation noise added to kernel diagonal |
|
||||
//! | `seed` | random | RNG seed for reproducibility |
|
||||
//!
|
||||
//! # Feature flag
|
||||
//!
|
||||
//! Requires the **`gp`** feature (adds the `nalgebra` dependency):
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! optimizer = { version = "...", features = ["gp"] }
|
||||
//! ```
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -17,8 +62,14 @@
|
||||
//! use optimizer::sampler::gp::GpSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let sampler = GpSampler::with_seed(42);
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! // Minimize an expensive function with GP-based Bayesian optimization
|
||||
//! let sampler = GpSampler::builder()
|
||||
//! .n_startup_trials(5)
|
||||
//! .n_candidates(500)
|
||||
//! .seed(42)
|
||||
//! .build();
|
||||
//!
|
||||
//! let mut study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
+53
-21
@@ -1,7 +1,39 @@
|
||||
//! Grid search sampler implementation.
|
||||
//! Grid search sampler — exhaustive evaluation of discretized parameter spaces.
|
||||
//!
|
||||
//! `GridSearchSampler` performs exhaustive grid search over the parameter space,
|
||||
//! systematically evaluating all combinations of discretized parameter values.
|
||||
//! [`GridSearchSampler`] divides each parameter range into a fixed number of
|
||||
//! evenly spaced points (or uses the explicit step size when defined) and
|
||||
//! evaluates them sequentially. This guarantees complete coverage of the
|
||||
//! search grid at the cost of scaling exponentially with the number of
|
||||
//! parameters.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Small, discrete spaces** — when you have a handful of categorical or
|
||||
//! integer parameters and want to evaluate every combination.
|
||||
//! - **Reproducibility** — grid search is fully deterministic with no random
|
||||
//! component.
|
||||
//! - **Benchmarking** — compare grid search results against adaptive samplers
|
||||
//! to measure their benefit.
|
||||
//!
|
||||
//! Avoid grid search for high-dimensional or large continuous spaces;
|
||||
//! prefer [`TpeSampler`](super::tpe::TpeSampler) or
|
||||
//! [`RandomSampler`](super::random::RandomSampler) instead.
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Option | Default | Description |
|
||||
//! |---|---|---|
|
||||
//! | `n_points_per_param` | 10 | Points per continuous parameter (ignored when `step` is set) |
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::prelude::*;
|
||||
//! use optimizer::sampler::grid::GridSearchSampler;
|
||||
//!
|
||||
//! let sampler = GridSearchSampler::builder().n_points_per_param(5).build();
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -295,38 +327,38 @@ struct GridState {
|
||||
grids: HashMap<String, CachedGrid>,
|
||||
}
|
||||
|
||||
/// A grid search sampler that exhaustively evaluates all grid points.
|
||||
/// Exhaustive grid search sampler.
|
||||
///
|
||||
/// `GridSearchSampler` divides the parameter space into a grid and systematically
|
||||
/// samples each point. This is useful when you want to evaluate all combinations
|
||||
/// of parameter values, especially for discrete or small parameter spaces.
|
||||
/// Divide each parameter range into evenly spaced points and evaluate them
|
||||
/// sequentially. When a parameter has an explicit `step` size, the step grid
|
||||
/// is used instead of auto-discretization.
|
||||
///
|
||||
/// # Grid Exhaustion
|
||||
/// Grid state is tracked **per distribution key** (bounds + step + log-scale).
|
||||
/// Parameters with identical distributions share the same grid counter, so
|
||||
/// use distinct ranges when multiple parameters span the same domain.
|
||||
///
|
||||
/// The sampler tracks its position in the grid for each distribution independently.
|
||||
/// When all grid points for a distribution have been sampled, subsequent calls to
|
||||
/// `sample()` for that distribution will **panic** with the message:
|
||||
/// `"GridSearchSampler: all grid points exhausted"`.
|
||||
/// # Grid exhaustion
|
||||
///
|
||||
/// To avoid panics, use [`is_exhausted()`](Self::is_exhausted) to check if all
|
||||
/// points have been sampled before calling `sample()`. You can also use
|
||||
/// [`grid_size()`](Self::grid_size) to determine the total number of grid points
|
||||
/// that will be sampled.
|
||||
/// When all grid points for a distribution have been sampled, the next
|
||||
/// `sample()` call for that distribution **panics**. Use
|
||||
/// [`is_exhausted()`](Self::is_exhausted) to check before sampling, or
|
||||
/// set `n_points_per_param` high enough to cover the planned number of
|
||||
/// trials.
|
||||
///
|
||||
/// # Thread Safety
|
||||
/// # Thread safety
|
||||
///
|
||||
/// `GridSearchSampler` is thread-safe (`Send + Sync`) and uses internal locking
|
||||
/// to ensure safe concurrent access to grid state.
|
||||
/// `GridSearchSampler` is `Send + Sync` and uses internal locking for
|
||||
/// safe concurrent access.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::sampler::grid::GridSearchSampler;
|
||||
///
|
||||
/// // Create with default settings (10 points per parameter)
|
||||
/// // Default: 10 points per parameter
|
||||
/// let sampler = GridSearchSampler::new();
|
||||
///
|
||||
/// // Create with custom settings using the builder
|
||||
/// // Custom grid density
|
||||
/// let sampler = GridSearchSampler::builder().n_points_per_param(20).build();
|
||||
/// ```
|
||||
pub struct GridSearchSampler {
|
||||
|
||||
+79
-10
@@ -1,8 +1,58 @@
|
||||
//! MOEA/D (Multi-Objective Evolutionary Algorithm based on Decomposition) sampler.
|
||||
//!
|
||||
//! Decomposes a multi-objective problem into scalar subproblems using
|
||||
//! weight vectors and solves them collaboratively. Supports Weighted Sum,
|
||||
//! Tchebycheff, and Penalty-based Boundary Intersection (PBI) scalarization.
|
||||
//! MOEA/D takes a fundamentally different approach from Pareto-based
|
||||
//! algorithms like NSGA-II/III. It **decomposes** the multi-objective
|
||||
//! problem into a set of scalar subproblems using evenly distributed
|
||||
//! weight vectors (Das-Dennis points), then solves them collaboratively
|
||||
//! through **neighborhood-based mating and replacement**.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
//! 1. **Decompose** — generate weight vectors on the unit simplex and
|
||||
//! assign one scalar subproblem per weight vector.
|
||||
//! 2. **Build neighborhoods** — for each subproblem, find its T nearest
|
||||
//! neighbors by Euclidean distance between weight vectors.
|
||||
//! 3. **Mate from neighborhood** — select parents from the neighborhood
|
||||
//! of each subproblem and produce offspring via SBX crossover +
|
||||
//! polynomial mutation.
|
||||
//! 4. **Scalarize and update** — evaluate offspring using a scalarization
|
||||
//! function and update neighboring subproblems if the offspring improves
|
||||
//! their scalar value.
|
||||
//! 5. **Update ideal point** — track the best value seen per objective.
|
||||
//!
|
||||
//! # Scalarization methods
|
||||
//!
|
||||
//! | Method | Formula | Best for |
|
||||
//! |--------|---------|----------|
|
||||
//! | [`Tchebycheff`](Decomposition::Tchebycheff) (default) | `max(wᵢ * \|fᵢ - zᵢ*\|)` | General purpose, handles non-convex fronts |
|
||||
//! | [`WeightedSum`](Decomposition::WeightedSum) | `Σ(wᵢ * fᵢ)` | Convex Pareto fronts only |
|
||||
//! | [`Pbi`](Decomposition::Pbi) | `d₁ + θ * d₂` | Fine-grained convergence/diversity control |
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - Problems where you want **evenly distributed** solutions along the
|
||||
//! Pareto front (one solution per weight direction).
|
||||
//! - Many-objective optimization (3+ objectives) — scales well because
|
||||
//! each subproblem is a simple scalar optimization.
|
||||
//! - Problems with **non-convex** Pareto fronts (use Tchebycheff or PBI).
|
||||
//! - When you need explicit control over the trade-off distribution via
|
||||
//! weight vectors.
|
||||
//!
|
||||
//! For Pareto-based approaches, see
|
||||
//! [`Nsga2Sampler`](super::nsga2::Nsga2Sampler) (crowding distance) or
|
||||
//! [`Nsga3Sampler`](super::nsga3::Nsga3Sampler) (reference-point niching).
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Parameter | Builder method | Default |
|
||||
//! |-----------|---------------|---------|
|
||||
//! | Population size | [`population_size`](MoeadSamplerBuilder::population_size) | Number of Das-Dennis weight vectors |
|
||||
//! | Neighborhood size (T) | [`neighborhood_size`](MoeadSamplerBuilder::neighborhood_size) | `min(20, pop_size)` |
|
||||
//! | Decomposition method | [`decomposition`](MoeadSamplerBuilder::decomposition) | Tchebycheff |
|
||||
//! | Crossover probability | [`crossover_prob`](MoeadSamplerBuilder::crossover_prob) | 1.0 |
|
||||
//! | SBX distribution index | [`crossover_eta`](MoeadSamplerBuilder::crossover_eta) | 20.0 |
|
||||
//! | Mutation distribution index | [`mutation_eta`](MoeadSamplerBuilder::mutation_eta) | 20.0 |
|
||||
//! | Random seed | [`seed`](MoeadSamplerBuilder::seed) | random |
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -37,15 +87,29 @@ use crate::multi_objective::MultiObjectiveTrial;
|
||||
use crate::param::ParamValue;
|
||||
use crate::types::Direction;
|
||||
|
||||
/// Decomposition (scalarization) method for MOEA/D.
|
||||
/// Decomposition (scalarization) method for [`MoeadSampler`].
|
||||
///
|
||||
/// Control how multi-objective values are reduced to a single scalar
|
||||
/// for each subproblem. The default is [`Tchebycheff`](Self::Tchebycheff),
|
||||
/// which handles both convex and non-convex Pareto fronts.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum Decomposition {
|
||||
/// Weighted sum: `sum(w_i * f_i)`.
|
||||
/// Weighted sum: `Σ(wᵢ * fᵢ)`.
|
||||
///
|
||||
/// Simplest method but can only find solutions on convex regions
|
||||
/// of the Pareto front.
|
||||
WeightedSum,
|
||||
/// Tchebycheff: `max(w_i * |f_i - z_i*|)`.
|
||||
/// Tchebycheff: `max(wᵢ * |fᵢ - zᵢ*|)`.
|
||||
///
|
||||
/// Handles non-convex Pareto fronts. The most commonly used
|
||||
/// decomposition method (default).
|
||||
#[default]
|
||||
Tchebycheff,
|
||||
/// Penalty-based Boundary Intersection with parameter theta.
|
||||
/// Penalty-based Boundary Intersection: `d₁ + θ * d₂`.
|
||||
///
|
||||
/// Provides fine-grained control over the convergence/diversity
|
||||
/// balance via the penalty parameter `theta`. Higher `theta`
|
||||
/// favors solutions closer to the weight direction.
|
||||
Pbi {
|
||||
/// Penalty parameter controlling the balance between convergence
|
||||
/// and diversity. Default: 5.0.
|
||||
@@ -55,9 +119,14 @@ pub enum Decomposition {
|
||||
|
||||
/// MOEA/D sampler for multi-objective optimization.
|
||||
///
|
||||
/// Decomposes the multi-objective problem into scalar subproblems
|
||||
/// using weight vectors, solving them collaboratively via
|
||||
/// neighborhood-based mating and replacement.
|
||||
/// Decompose a multi-objective problem into scalar subproblems using
|
||||
/// weight vectors and solve them collaboratively via neighborhood-based
|
||||
/// mating. Supports [`Tchebycheff`](Decomposition::Tchebycheff),
|
||||
/// [`WeightedSum`](Decomposition::WeightedSum), and
|
||||
/// [`Pbi`](Decomposition::Pbi) scalarization.
|
||||
///
|
||||
/// Create with [`MoeadSampler::new`], [`MoeadSampler::with_seed`], or
|
||||
/// [`MoeadSampler::builder`] for full configuration.
|
||||
pub struct MoeadSampler {
|
||||
state: Mutex<MoeadState>,
|
||||
}
|
||||
|
||||
+58
-14
@@ -1,19 +1,38 @@
|
||||
//! Multi-Objective Tree-Parzen Estimator (MOTPE) sampler.
|
||||
//!
|
||||
//! Extends TPE to handle multi-objective optimization by using Pareto
|
||||
//! non-dominated sorting to define "good" vs "bad" trial regions for
|
||||
//! the KDE models, replacing the single-objective gamma-based split.
|
||||
//! MOTPE extends TPE to multi-objective optimization by replacing the gamma-based
|
||||
//! split with Pareto non-dominated sorting. This lets the sampler propose
|
||||
//! parameters that push the Pareto front forward across all objectives
|
||||
//! simultaneously.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
//! In single-objective TPE, trials are sorted by value and split at a
|
||||
//! gamma percentile into good/bad groups. MOTPE replaces this with:
|
||||
//! In single-objective TPE, trials are sorted by value and split at a gamma
|
||||
//! percentile into good/bad groups. MOTPE replaces this with:
|
||||
//!
|
||||
//! 1. Compute non-dominated sorting on all completed trials
|
||||
//! 2. Use the Pareto front (rank 0) as "good" trials
|
||||
//! 3. Use dominated trials as "bad" trials
|
||||
//! 4. Build KDE l(x) from good, g(x) from bad
|
||||
//! 5. Sample candidates and score by l(x)/g(x)
|
||||
//! 1. Compute non-dominated sorting on all completed trials.
|
||||
//! 2. Use the Pareto front (rank 0) as "good" trials.
|
||||
//! 3. Use dominated trials (rank 1+) as "bad" trials.
|
||||
//! 4. Build KDE l(x) from good, g(x) from bad.
|
||||
//! 5. Sample candidates and score by l(x)/g(x).
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - You have 2+ objectives and want model-guided search (not pure evolutionary).
|
||||
//! - Your objectives are relatively smooth and continuous.
|
||||
//! - You want a Pareto-aware version of TPE without the overhead of full
|
||||
//! population-based algorithms like NSGA-II or NSGA-III.
|
||||
//!
|
||||
//! For single-objective problems, use [`TpeSampler`](super::tpe::TpeSampler) instead.
|
||||
//! For many-objective (3+) problems with reference-point decomposition, consider
|
||||
//! NSGA-III or MOEA/D.
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! - `n_startup_trials` — number of random trials before MOTPE kicks in (default: 11)
|
||||
//! - `n_ei_candidates` — candidates evaluated per sample (default: 24)
|
||||
//! - `kde_bandwidth` — optional fixed KDE bandwidth; `None` uses Scott's rule
|
||||
//! - `seed` — optional seed for reproducibility
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -50,14 +69,21 @@ use crate::{pareto, rng_util};
|
||||
|
||||
/// Multi-Objective TPE (MOTPE) sampler for multi-objective Bayesian optimization.
|
||||
///
|
||||
/// Uses Pareto non-dominated sorting to split completed trials into
|
||||
/// "good" (non-dominated, rank 0) and "bad" (dominated) groups, then
|
||||
/// fits kernel density estimators to each group and samples new points
|
||||
/// that maximize l(x)/g(x).
|
||||
/// Use Pareto non-dominated sorting to split completed trials into "good"
|
||||
/// (non-dominated, rank 0) and "bad" (dominated) groups, then fit kernel
|
||||
/// density estimators to each group and sample new points that maximize
|
||||
/// l(x)/g(x).
|
||||
///
|
||||
/// During the startup phase (fewer than `n_startup_trials` completed),
|
||||
/// MOTPE falls back to random sampling.
|
||||
///
|
||||
/// # When to use
|
||||
///
|
||||
/// Use `MotpeSampler` when optimizing 2+ objectives and you want a
|
||||
/// model-guided sampler that adapts proposals based on the current
|
||||
/// Pareto front. For single-objective problems, use
|
||||
/// [`TpeSampler`](super::tpe::TpeSampler) instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
@@ -74,6 +100,17 @@ use crate::{pareto, rng_util};
|
||||
///
|
||||
/// let study =
|
||||
/// MultiObjectiveStudy::with_sampler(vec![Direction::Minimize, Direction::Minimize], sampler);
|
||||
///
|
||||
/// let x = FloatParam::new(0.0, 1.0);
|
||||
/// study
|
||||
/// .optimize(30, |trial| {
|
||||
/// let xv = x.suggest(trial)?;
|
||||
/// Ok::<_, optimizer::Error>(vec![xv, 1.0 - xv])
|
||||
/// })
|
||||
/// .unwrap();
|
||||
///
|
||||
/// let front = study.pareto_front();
|
||||
/// assert!(!front.is_empty());
|
||||
/// ```
|
||||
pub struct MotpeSampler {
|
||||
/// Number of trials before MOTPE kicks in (uses random sampling before this).
|
||||
@@ -520,6 +557,13 @@ impl MultiObjectiveSampler for MotpeSampler {
|
||||
|
||||
/// Builder for configuring a [`MotpeSampler`].
|
||||
///
|
||||
/// # Defaults
|
||||
///
|
||||
/// - `n_startup_trials`: 11
|
||||
/// - `n_ei_candidates`: 24
|
||||
/// - `kde_bandwidth`: None (Scott's rule)
|
||||
/// - `seed`: None (OS entropy)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
|
||||
+46
-4
@@ -1,7 +1,45 @@
|
||||
//! NSGA-II (Non-dominated Sorting Genetic Algorithm II) sampler.
|
||||
//!
|
||||
//! Implements multi-objective optimization using non-dominated sorting,
|
||||
//! crowding distance, SBX crossover, and polynomial mutation.
|
||||
//! NSGA-II is one of the most widely used evolutionary multi-objective
|
||||
//! optimization algorithms. It ranks the population using **non-dominated
|
||||
//! sorting** (fast O(MN²) algorithm) and breaks ties within the same
|
||||
//! Pareto front using **crowding distance**, which favors solutions in
|
||||
//! less-crowded regions of the objective space.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
//! Each generation proceeds as follows:
|
||||
//!
|
||||
//! 1. **Non-dominated sorting** — partition the combined parent+offspring
|
||||
//! population into Pareto fronts F₁, F₂, …
|
||||
//! 2. **Crowding distance** — for each front, compute per-solution crowding
|
||||
//! distance (sum of normalized neighbor gaps in each objective).
|
||||
//! 3. **Selection** — fill the next population front-by-front. When a front
|
||||
//! only partially fits, prefer solutions with higher crowding distance.
|
||||
//! 4. **Binary tournament** — select parents using (rank, crowding distance)
|
||||
//! comparisons.
|
||||
//! 5. **SBX crossover + polynomial mutation** — generate offspring.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - Two-objective problems where you want a well-spread Pareto front.
|
||||
//! - General-purpose multi-objective optimization with moderate population
|
||||
//! sizes.
|
||||
//! - Problems that benefit from diversity preservation via crowding distance.
|
||||
//!
|
||||
//! For problems with **three or more objectives**, consider
|
||||
//! [`Nsga3Sampler`](super::nsga3::Nsga3Sampler) (reference-point niching)
|
||||
//! or [`MoeadSampler`](super::moead::MoeadSampler) (decomposition).
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Parameter | Builder method | Default |
|
||||
//! |-----------|---------------|---------|
|
||||
//! | Population size | [`population_size`](Nsga2SamplerBuilder::population_size) | `4 + floor(3 * ln(n_params))`, min 4 |
|
||||
//! | Crossover probability | [`crossover_prob`](Nsga2SamplerBuilder::crossover_prob) | 0.9 |
|
||||
//! | SBX distribution index | [`crossover_eta`](Nsga2SamplerBuilder::crossover_eta) | 20.0 |
|
||||
//! | Mutation distribution index | [`mutation_eta`](Nsga2SamplerBuilder::mutation_eta) | 20.0 |
|
||||
//! | Random seed | [`seed`](Nsga2SamplerBuilder::seed) | random |
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -39,8 +77,12 @@ use crate::types::Direction;
|
||||
|
||||
/// NSGA-II sampler for multi-objective optimization.
|
||||
///
|
||||
/// Provides non-dominated sorting, crowding distance selection,
|
||||
/// SBX crossover, and polynomial mutation.
|
||||
/// Use non-dominated sorting with crowding-distance tie-breaking to
|
||||
/// evolve a well-spread Pareto front. Best suited for bi-objective
|
||||
/// problems; for 3+ objectives prefer [`Nsga3Sampler`](super::nsga3::Nsga3Sampler).
|
||||
///
|
||||
/// Create with [`Nsga2Sampler::new`], [`Nsga2Sampler::with_seed`], or
|
||||
/// [`Nsga2Sampler::builder`] for full configuration.
|
||||
pub struct Nsga2Sampler {
|
||||
state: Mutex<Nsga2State>,
|
||||
}
|
||||
|
||||
+50
-6
@@ -1,9 +1,49 @@
|
||||
//! NSGA-III (Non-dominated Sorting Genetic Algorithm III) sampler.
|
||||
//!
|
||||
//! Uses reference-point-based niching for better diversity in
|
||||
//! many-objective (3+) optimization problems. Das-Dennis structured
|
||||
//! reference points guide the search toward a well-distributed
|
||||
//! Pareto front.
|
||||
//! NSGA-III extends NSGA-II to handle **many-objective** (3+) problems
|
||||
//! where crowding distance loses effectiveness. Instead of crowding
|
||||
//! distance, it uses **reference-point-based niching** with structured
|
||||
//! Das-Dennis reference points distributed on the unit simplex to guide
|
||||
//! the population toward a well-diversified Pareto front.
|
||||
//!
|
||||
//! # Algorithm
|
||||
//!
|
||||
//! Each generation proceeds as follows:
|
||||
//!
|
||||
//! 1. **Non-dominated sorting** — same as NSGA-II, partition the
|
||||
//! combined population into Pareto fronts F₁, F₂, …
|
||||
//! 2. **Normalize objectives** — translate by ideal point and scale by
|
||||
//! intercepts so all objectives lie in roughly \[0, 1\].
|
||||
//! 3. **Associate with reference points** — assign each solution to the
|
||||
//! closest Das-Dennis reference direction by perpendicular distance.
|
||||
//! 4. **Niching selection** — when the last front only partially fits,
|
||||
//! prefer solutions associated with under-represented reference points
|
||||
//! (lowest niche count first, closest distance second).
|
||||
//! 5. **SBX crossover + polynomial mutation** — generate offspring via
|
||||
//! rank-based tournament selection.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Three or more objectives** — NSGA-III maintains diversity far
|
||||
//! better than NSGA-II as the number of objectives grows.
|
||||
//! - Problems where you want a **uniformly distributed** Pareto front
|
||||
//! guided by structured reference points.
|
||||
//! - Scales well up to ~10 objectives with appropriate division settings.
|
||||
//!
|
||||
//! For bi-objective problems, [`Nsga2Sampler`](super::nsga2::Nsga2Sampler)
|
||||
//! is simpler and equally effective. For decomposition-based optimization,
|
||||
//! see [`MoeadSampler`](super::moead::MoeadSampler).
|
||||
//!
|
||||
//! # Configuration
|
||||
//!
|
||||
//! | Parameter | Builder method | Default |
|
||||
//! |-----------|---------------|---------|
|
||||
//! | Population size | [`population_size`](Nsga3SamplerBuilder::population_size) | Number of Das-Dennis reference points |
|
||||
//! | Das-Dennis divisions (H) | [`n_divisions`](Nsga3SamplerBuilder::n_divisions) | Auto-chosen from population size and objectives |
|
||||
//! | Crossover probability | [`crossover_prob`](Nsga3SamplerBuilder::crossover_prob) | 1.0 |
|
||||
//! | SBX distribution index | [`crossover_eta`](Nsga3SamplerBuilder::crossover_eta) | 30.0 |
|
||||
//! | Mutation distribution index | [`mutation_eta`](Nsga3SamplerBuilder::mutation_eta) | 20.0 |
|
||||
//! | Random seed | [`seed`](Nsga3SamplerBuilder::seed) | random |
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -49,8 +89,12 @@ use crate::types::Direction;
|
||||
|
||||
/// NSGA-III sampler for multi-objective optimization.
|
||||
///
|
||||
/// Uses reference-point-based niching to maintain diversity,
|
||||
/// especially effective for problems with 3 or more objectives.
|
||||
/// Use reference-point niching with Das-Dennis structured points to
|
||||
/// maintain diversity in many-objective (3+) problems. For bi-objective
|
||||
/// problems, [`Nsga2Sampler`](super::nsga2::Nsga2Sampler) is simpler.
|
||||
///
|
||||
/// Create with [`Nsga3Sampler::new`], [`Nsga3Sampler::with_seed`], or
|
||||
/// [`Nsga3Sampler::builder`] for full configuration.
|
||||
pub struct Nsga3Sampler {
|
||||
state: Mutex<Nsga3State>,
|
||||
}
|
||||
|
||||
+36
-5
@@ -1,4 +1,31 @@
|
||||
//! Random sampler implementation.
|
||||
//! Random sampler — uniform independent sampling.
|
||||
//!
|
||||
//! [`RandomSampler`] draws each parameter value independently and uniformly
|
||||
//! at random, ignoring trial history entirely. It respects log-scale and
|
||||
//! step-size constraints defined by the parameter distribution.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Baseline comparison** — run Random alongside smarter samplers to
|
||||
//! quantify their benefit.
|
||||
//! - **Startup phase** — many model-based samplers (TPE, GP, CMA-ES) use
|
||||
//! random sampling for their first *n* trials before fitting a surrogate.
|
||||
//! - **Very high dimensions** — when the search space is too large for
|
||||
//! structured exploration, random search with enough budget can be
|
||||
//! surprisingly competitive.
|
||||
//!
|
||||
//! For better uniform coverage without model fitting, consider
|
||||
//! [`SobolSampler`](super::sobol::SobolSampler) (requires the `sobol`
|
||||
//! feature flag).
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::prelude::*;
|
||||
//! use optimizer::sampler::random::RandomSampler;
|
||||
//!
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
|
||||
//! ```
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
@@ -7,11 +34,15 @@ use crate::param::ParamValue;
|
||||
use crate::rng_util;
|
||||
use crate::sampler::{CompletedTrial, Sampler};
|
||||
|
||||
/// A simple random sampler that samples uniformly from distributions.
|
||||
/// Uniform independent random sampler.
|
||||
///
|
||||
/// This sampler ignores the trial history and samples uniformly at random,
|
||||
/// respecting log scale and step size constraints. It serves as a baseline
|
||||
/// sampler and is used during the startup phase of more sophisticated samplers.
|
||||
/// Sample each parameter value uniformly at random, respecting log-scale and
|
||||
/// step-size constraints. Trial history is ignored — every sample is drawn
|
||||
/// independently.
|
||||
///
|
||||
/// This is the default sampler used by [`Study::new`](crate::Study::new)
|
||||
/// and during the startup phase of model-based samplers such as
|
||||
/// [`TpeSampler`](super::tpe::TpeSampler).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
+50
-8
@@ -1,4 +1,48 @@
|
||||
//! Quasi-random sampler using Sobol low-discrepancy sequences.
|
||||
//!
|
||||
//! [`SobolSampler`] generates points from a Sobol sequence (scrambled via the
|
||||
//! Burley 2020 algorithm) to fill the parameter space more uniformly than
|
||||
//! pure random sampling. Where [`RandomSampler`](super::random::RandomSampler)
|
||||
//! may cluster points in some regions by chance, Sobol sequences are
|
||||
//! constructed to spread points evenly across all dimensions.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Better-than-random baseline** — when you want uniform coverage
|
||||
//! without the cost of model fitting (TPE, GP, etc.).
|
||||
//! - **Startup phase replacement** — use Sobol instead of random for the
|
||||
//! initial exploration phase of adaptive samplers.
|
||||
//! - **Moderate dimensionality** — Sobol uniformity is strongest up to
|
||||
//! ~20 dimensions; beyond that the advantage over random sampling
|
||||
//! diminishes.
|
||||
//! - **Deterministic exploration** — Sobol sequences are fully deterministic
|
||||
//! for a given seed, making experiments reproducible.
|
||||
//!
|
||||
//! # How it works
|
||||
//!
|
||||
//! Each trial maps to a Sobol sequence index, and each parameter within a
|
||||
//! trial maps to a separate Sobol dimension. The resulting quasi-random
|
||||
//! point in \[0, 1) is then scaled to the parameter's distribution (linear,
|
||||
//! log-scale, or step grid).
|
||||
//!
|
||||
//! **Important:** parameters must be suggested in the same order across
|
||||
//! trials for consistent dimension assignment.
|
||||
//!
|
||||
//! Requires the **`sobol`** feature flag:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! optimizer = { version = "...", features = ["sobol"] }
|
||||
//! ```
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::prelude::*;
|
||||
//! use optimizer::sampler::sobol::SobolSampler;
|
||||
//!
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, SobolSampler::with_seed(42));
|
||||
//! ```
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use sobol_burley::sample;
|
||||
@@ -17,13 +61,11 @@ struct SobolState {
|
||||
|
||||
/// Quasi-random sampler using Sobol low-discrepancy sequences.
|
||||
///
|
||||
/// Provides better uniform coverage of the parameter space than
|
||||
/// [`RandomSampler`](super::random::RandomSampler). Useful as a baseline or
|
||||
/// for the startup phase of model-based samplers.
|
||||
///
|
||||
/// Unlike random sampling, Sobol sequences are deterministic and fill the
|
||||
/// space more evenly, reducing the number of trials needed to adequately
|
||||
/// cover the search space.
|
||||
/// Produce better uniform coverage of the parameter space than
|
||||
/// [`RandomSampler`](super::random::RandomSampler) by using a
|
||||
/// scrambled Sobol sequence (Burley 2020). Useful as a standalone
|
||||
/// baseline or as a drop-in replacement for the random startup
|
||||
/// phase of model-based samplers.
|
||||
///
|
||||
/// Each trial uses a different Sobol sequence index, and each parameter
|
||||
/// within a trial maps to a different Sobol dimension. Parameters must be
|
||||
@@ -33,7 +75,7 @@ struct SobolState {
|
||||
/// Sobol sequences are most effective in moderate dimensions (up to ~20).
|
||||
/// For very high dimensions, the uniformity advantage diminishes.
|
||||
///
|
||||
/// Requires the `sobol` feature flag.
|
||||
/// Requires the **`sobol`** feature flag.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
+59
-3
@@ -1,7 +1,63 @@
|
||||
//! Tree-Parzen Estimator (TPE) sampler implementation and utilities.
|
||||
//! Tree-Parzen Estimator (TPE) sampler family for Bayesian optimization.
|
||||
//!
|
||||
//! This module provides TPE-based sampling for Bayesian optimization,
|
||||
//! including support for intersection search space calculation.
|
||||
//! TPE is a sequential model-based optimization algorithm that models P(x|y) instead
|
||||
//! of P(y|x). It splits completed trials into "good" (below the gamma quantile) and
|
||||
//! "bad" groups, fits a kernel density estimator (KDE) to each, and proposes new
|
||||
//! points by maximizing the l(x)/g(x) ratio — an approximation of Expected Improvement.
|
||||
//!
|
||||
//! # Samplers
|
||||
//!
|
||||
//! | Sampler | Models parameters | Best for |
|
||||
//! |---------|-------------------|----------|
|
||||
//! | [`TpeSampler`] | Independently | General-purpose single-objective optimization |
|
||||
//! | [`MultivariateTpeSampler`] | Jointly | Problems with correlated parameters |
|
||||
//!
|
||||
//! # Gamma strategies
|
||||
//!
|
||||
//! The gamma quantile controls how many trials are considered "good". This module
|
||||
//! provides four built-in strategies via the [`GammaStrategy`] trait:
|
||||
//!
|
||||
//! | Strategy | Formula | Default |
|
||||
//! |----------|---------|---------|
|
||||
//! | [`FixedGamma`] | Constant value | gamma = 0.25 |
|
||||
//! | [`LinearGamma`] | Linear ramp from min to max | 0.10 → 0.25 over 100 trials |
|
||||
//! | [`SqrtGamma`] | 1/√n decay (Optuna-style) | factor = 1.0, max = 0.25 |
|
||||
//! | [`HyperoptGamma`] | (base+1)/n (Hyperopt-style) | base = 24, max = 0.25 |
|
||||
//!
|
||||
//! You can also implement [`GammaStrategy`] for a custom splitting rule.
|
||||
//!
|
||||
//! # Search-space utilities
|
||||
//!
|
||||
//! The [`search_space`] submodule provides [`IntersectionSearchSpace`] for computing
|
||||
//! the common parameter set across trials, and [`GroupDecomposedSearchSpace`] for
|
||||
//! splitting parameters into independent groups based on co-occurrence.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! Basic TPE with default settings:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::tpe::TpeSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, TpeSampler::new());
|
||||
//! ```
|
||||
//!
|
||||
//! Multivariate TPE for correlated parameters:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::sampler::tpe::MultivariateTpeSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
//! let sampler = MultivariateTpeSampler::builder()
|
||||
//! .gamma(0.15)
|
||||
//! .n_startup_trials(20)
|
||||
//! .group(true)
|
||||
//! .seed(42)
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
//! ```
|
||||
|
||||
mod gamma;
|
||||
mod multivariate;
|
||||
|
||||
@@ -167,15 +167,53 @@ pub enum ConstantLiarStrategy {
|
||||
|
||||
/// A Multivariate Tree-Parzen Estimator (TPE) sampler for Bayesian optimization.
|
||||
///
|
||||
/// This sampler extends the standard TPE approach by modeling joint distributions
|
||||
/// over all parameters, allowing it to capture parameter correlations.
|
||||
/// Unlike the standard [`super::TpeSampler`], which samples each parameter
|
||||
/// independently, this sampler models joint distributions over all parameters
|
||||
/// using multivariate KDE. This captures correlations between parameters and
|
||||
/// can significantly improve optimization on problems where parameters interact
|
||||
/// (e.g., Rosenbrock, coupled hyperparameters).
|
||||
///
|
||||
/// # Fields
|
||||
/// When the search space varies between trials (conditional parameters), the
|
||||
/// sampler automatically falls back to independent TPE or uniform sampling for
|
||||
/// parameters outside the intersection search space.
|
||||
///
|
||||
/// - `gamma_strategy`: Strategy for computing the gamma quantile
|
||||
/// - `n_startup_trials`: Number of random trials before TPE sampling begins
|
||||
/// - `n_ei_candidates`: Number of candidates to evaluate per joint sample
|
||||
/// - `group`: Whether to decompose search space into independent groups
|
||||
/// # When to use
|
||||
///
|
||||
/// - Parameters are correlated or interact with each other.
|
||||
/// - The search space is mostly fixed across trials.
|
||||
/// - You need parallel optimization (enable [`ConstantLiarStrategy`]).
|
||||
///
|
||||
/// Prefer [`super::TpeSampler`] when parameters are independent or the search space changes
|
||||
/// dynamically.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::tpe::MultivariateTpeSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let sampler = MultivariateTpeSampler::builder()
|
||||
/// .gamma(0.15)
|
||||
/// .n_startup_trials(20)
|
||||
/// .seed(42)
|
||||
/// .build()
|
||||
/// .unwrap();
|
||||
///
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
/// let x = FloatParam::new(-5.0, 5.0);
|
||||
/// let y = FloatParam::new(-5.0, 5.0);
|
||||
///
|
||||
/// study
|
||||
/// .optimize(30, |trial| {
|
||||
/// let xv = x.suggest(trial)?;
|
||||
/// let yv = y.suggest(trial)?;
|
||||
/// Ok::<_, optimizer::Error>(xv * xv + yv * yv)
|
||||
/// })
|
||||
/// .unwrap();
|
||||
///
|
||||
/// assert!(study.best_value().unwrap() < 1.0);
|
||||
/// ```
|
||||
pub struct MultivariateTpeSampler {
|
||||
/// Strategy for computing the gamma quantile.
|
||||
gamma_strategy: Arc<dyn GammaStrategy>,
|
||||
|
||||
@@ -102,7 +102,7 @@ use crate::sampler::{CompletedTrial, Sampler};
|
||||
///
|
||||
/// // Create with custom settings using the builder
|
||||
/// let sampler = TpeSampler::builder()
|
||||
/// .gamma(0.15) // Shorthand for Fixednew(0.15)
|
||||
/// .gamma(0.15) // Shorthand for FixedGamma::new(0.15)
|
||||
/// .n_startup_trials(20)
|
||||
/// .n_ei_candidates(32)
|
||||
/// .seed(42)
|
||||
|
||||
+92
-14
@@ -1,4 +1,72 @@
|
||||
//! JSONL-based journal storage backend.
|
||||
//!
|
||||
//! [`JournalStorage`] persists completed trials as one JSON object per
|
||||
//! line ([JSONL / JSON Lines](https://jsonlines.org/)) while keeping a
|
||||
//! full copy in memory for fast read access.
|
||||
//!
|
||||
//! # File format
|
||||
//!
|
||||
//! Each line is a self-contained JSON serialization of a
|
||||
//! [`CompletedTrial<V>`](crate::sampler::CompletedTrial). The file
|
||||
//! is append-only — no existing lines are ever modified or deleted.
|
||||
//!
|
||||
//! ```text
|
||||
//! {"id":0,"params":{...},"value":1.23,"state":"Completed",...}
|
||||
//! {"id":1,"params":{...},"value":0.87,"state":"Completed",...}
|
||||
//! ```
|
||||
//!
|
||||
//! # File locking
|
||||
//!
|
||||
//! Concurrent access is coordinated with `fs2` file locks:
|
||||
//!
|
||||
//! - **Writes** acquire an *exclusive* lock so only one process
|
||||
//! appends at a time.
|
||||
//! - **Reads** ([`refresh`](super::Storage::refresh)) acquire a
|
||||
//! *shared* lock so readers never see a partially written line.
|
||||
//!
|
||||
//! This makes it safe for multiple processes to share the same JSONL
|
||||
//! file — for example, distributed workers each running their own
|
||||
//! [`Study`](crate::Study) with a `JournalStorage` pointing to a
|
||||
//! shared path.
|
||||
//!
|
||||
//! # Resuming a study
|
||||
//!
|
||||
//! Use [`JournalStorage::open`] to reload previously persisted trials
|
||||
//! and continue optimization from where you left off:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use optimizer::prelude::*;
|
||||
//! use optimizer::storage::JournalStorage;
|
||||
//!
|
||||
//! // First run — creates the file.
|
||||
//! let storage = JournalStorage::<f64>::new("trials.jsonl");
|
||||
//! let mut study = Study::builder().minimize().storage(storage).build();
|
||||
//! study
|
||||
//! .optimize(50, |trial| {
|
||||
//! let x = FloatParam::new(-5.0, 5.0).suggest(trial)?;
|
||||
//! Ok::<_, optimizer::Error>(x * x)
|
||||
//! })
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! // Later run — reloads previous 50 trials, then adds 50 more.
|
||||
//! let storage = JournalStorage::<f64>::open("trials.jsonl").unwrap();
|
||||
//! let mut study = Study::builder().minimize().storage(storage).build();
|
||||
//! study
|
||||
//! .optimize(50, |trial| {
|
||||
//! let x = FloatParam::new(-5.0, 5.0).suggest(trial)?;
|
||||
//! Ok::<_, optimizer::Error>(x * x)
|
||||
//! })
|
||||
//! .unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Persistence** — survive process crashes or intentional restarts.
|
||||
//! - **Multi-process** — several workers collaborating on a single study.
|
||||
//! - **Inspection** — `cat trials.jsonl | jq .` for quick debugging.
|
||||
//!
|
||||
//! For pure in-memory usage without disk I/O, use
|
||||
//! [`MemoryStorage`](super::MemoryStorage) instead (the default).
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use std::fs::{File, OpenOptions};
|
||||
@@ -14,22 +82,30 @@ use serde::de::DeserializeOwned;
|
||||
use super::{MemoryStorage, Storage};
|
||||
use crate::sampler::CompletedTrial;
|
||||
|
||||
/// A storage backend that appends completed trials as JSON lines to a file.
|
||||
/// Append-only JSONL storage backend with file locking.
|
||||
///
|
||||
/// Trials are kept in memory for fast read access and simultaneously
|
||||
/// persisted to a JSONL file. Multiple processes can safely share
|
||||
/// the same file: writes use an exclusive file lock, reads use a
|
||||
/// shared file lock.
|
||||
/// Trials are kept in memory (via an inner [`MemoryStorage`]) for fast
|
||||
/// read access and simultaneously appended to a JSONL file on disk.
|
||||
/// Multiple processes can safely share the same file thanks to
|
||||
/// `fs2` file locks — writes use an exclusive lock, reads use a
|
||||
/// shared lock.
|
||||
///
|
||||
/// The type parameter `V` is the objective value type (typically `f64`).
|
||||
/// It must be serializable so that trials can be written to disk.
|
||||
/// The type parameter `V` is the objective value type (typically
|
||||
/// `f64`). It must implement [`Serialize`](serde::Serialize) and
|
||||
/// [`DeserializeOwned`](serde::de::DeserializeOwned) so trials can be
|
||||
/// written to and read from disk.
|
||||
///
|
||||
/// # Examples
|
||||
/// See the [`storage`](super) module docs for file format details
|
||||
/// and a resumption example.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use optimizer::prelude::*;
|
||||
/// use optimizer::storage::JournalStorage;
|
||||
///
|
||||
/// let storage: JournalStorage<f64> = JournalStorage::new("trials.jsonl");
|
||||
/// let storage = JournalStorage::<f64>::new("trials.jsonl");
|
||||
/// let mut study = Study::builder().minimize().storage(storage).build();
|
||||
/// ```
|
||||
pub struct JournalStorage<V = f64> {
|
||||
memory: MemoryStorage<V>,
|
||||
@@ -40,14 +116,15 @@ pub struct JournalStorage<V = f64> {
|
||||
}
|
||||
|
||||
impl<V: Serialize + DeserializeOwned + Send + Sync> JournalStorage<V> {
|
||||
/// Creates a new journal storage that writes to the given path.
|
||||
/// Create a new journal storage that writes to the given path.
|
||||
///
|
||||
/// The file does not need to exist yet — it will be created on the
|
||||
/// first write. Existing trials in the file are **not** loaded
|
||||
/// until [`refresh`](Storage::refresh) is called (which happens
|
||||
/// automatically at the start of each trial via the [`Study`](crate::Study)).
|
||||
///
|
||||
/// To pre-load existing trials, use [`JournalStorage::open`].
|
||||
/// To pre-load existing trials at construction time, use
|
||||
/// [`JournalStorage::open`] instead.
|
||||
#[must_use]
|
||||
pub fn new(path: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
@@ -58,13 +135,14 @@ impl<V: Serialize + DeserializeOwned + Send + Sync> JournalStorage<V> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens an existing journal file and loads all stored trials.
|
||||
/// Open an existing journal file and load all stored trials.
|
||||
///
|
||||
/// If the file does not exist, returns an empty storage (no error).
|
||||
/// If the file does not exist, return an empty storage (no error).
|
||||
/// This is the primary way to **resume** a study after a restart.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a [`Storage`](crate::Error::Storage) error if the file
|
||||
/// Return a [`Storage`](crate::Error::Storage) error if the file
|
||||
/// exists but cannot be read or parsed.
|
||||
pub fn open(path: impl AsRef<Path>) -> crate::Result<Self> {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
|
||||
+51
-4
@@ -1,3 +1,32 @@
|
||||
//! In-memory storage backend.
|
||||
//!
|
||||
//! [`MemoryStorage`] is the default backend used by every
|
||||
//! [`Study`](crate::Study). Trials are stored in a
|
||||
//! `Vec<CompletedTrial<V>>` behind a [`parking_lot::RwLock`] for
|
||||
//! thread-safe access.
|
||||
//!
|
||||
//! # When to use
|
||||
//!
|
||||
//! - **Single-process** studies where persistence is not needed.
|
||||
//! - **Testing** or **prototyping** — zero configuration required.
|
||||
//! - When you want the **fastest** possible read/write performance
|
||||
//! (no disk I/O).
|
||||
//!
|
||||
//! For persistent storage that survives process restarts, see
|
||||
//! [`JournalStorage`](super::JournalStorage) (requires the `journal`
|
||||
//! feature).
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::prelude::*;
|
||||
//! use optimizer::storage::MemoryStorage;
|
||||
//!
|
||||
//! // Explicit memory storage (equivalent to the default)
|
||||
//! let storage = MemoryStorage::<f64>::new();
|
||||
//! let study = Study::builder().minimize().storage(storage).build();
|
||||
//! ```
|
||||
|
||||
use core::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -8,14 +37,28 @@ use crate::sampler::CompletedTrial;
|
||||
|
||||
/// In-memory trial storage (the default).
|
||||
///
|
||||
/// This is a thin wrapper around `Arc<RwLock<Vec<CompletedTrial<V>>>>`.
|
||||
/// Wrap a `Vec<CompletedTrial<V>>` behind a read-write lock so that
|
||||
/// trials can be appended from any thread. This is the backend that
|
||||
/// [`Study`](crate::Study) uses when no explicit storage is provided.
|
||||
///
|
||||
/// Use [`with_trials`](Self::with_trials) to seed a study with
|
||||
/// previously collected data.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::storage::{MemoryStorage, Storage};
|
||||
///
|
||||
/// let storage = MemoryStorage::<f64>::new();
|
||||
/// assert_eq!(storage.trials_arc().read().len(), 0);
|
||||
/// ```
|
||||
pub struct MemoryStorage<V> {
|
||||
trials: Arc<RwLock<Vec<CompletedTrial<V>>>>,
|
||||
next_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl<V> MemoryStorage<V> {
|
||||
/// Creates a new, empty in-memory store.
|
||||
/// Create a new, empty in-memory store.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -24,7 +67,10 @@ impl<V> MemoryStorage<V> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an in-memory store pre-populated with `trials`.
|
||||
/// Create an in-memory store pre-populated with `trials`.
|
||||
///
|
||||
/// The internal ID counter is set to one past the highest trial ID
|
||||
/// so that subsequent trials receive unique IDs.
|
||||
#[must_use]
|
||||
pub fn with_trials(trials: Vec<CompletedTrial<V>>) -> Self {
|
||||
let next_id = trials.iter().map(|t| t.id).max().map_or(0, |id| id + 1);
|
||||
@@ -34,7 +80,8 @@ impl<V> MemoryStorage<V> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures the ID counter is at least `min_value`.
|
||||
/// Ensure the ID counter is at least `min_value`.
|
||||
#[cfg(feature = "journal")]
|
||||
pub(crate) fn bump_next_id(&self, min_value: u64) {
|
||||
self.next_id.fetch_max(min_value, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
+41
-8
@@ -1,10 +1,42 @@
|
||||
//! Trial storage backends.
|
||||
//!
|
||||
//! The [`Storage`] trait defines how completed trials are stored and
|
||||
//! accessed. [`MemoryStorage`] keeps trials in memory (the default).
|
||||
//! With the `journal` feature enabled, [`JournalStorage`] appends
|
||||
//! trials to a JSONL file with file-level locking so multiple
|
||||
//! processes can safely share state.
|
||||
//! The [`Storage`] trait defines how completed trials are persisted and
|
||||
//! retrieved. Every [`Study`](crate::Study) owns an `Arc<dyn Storage<V>>`
|
||||
//! so storage is transparently shared across threads.
|
||||
//!
|
||||
//! # Available backends
|
||||
//!
|
||||
//! | Backend | Description | Feature flag |
|
||||
//! |---------|-------------|-------------|
|
||||
//! | [`MemoryStorage`] | In-memory `Vec` behind a read-write lock (the default) | — |
|
||||
//! | [`JournalStorage`] | JSONL file with `fs2` file locking for multi-process sharing | `journal` |
|
||||
//!
|
||||
//! # When to swap backends
|
||||
//!
|
||||
//! The default [`MemoryStorage`] is sufficient for single-process studies
|
||||
//! where persistence is not needed. Switch to [`JournalStorage`] when you
|
||||
//! want to:
|
||||
//!
|
||||
//! - **Resume** a study after a process restart.
|
||||
//! - **Share state** across multiple processes writing to the same file.
|
||||
//! - **Inspect** trial history in a human-readable JSONL file.
|
||||
//!
|
||||
//! # Implementing a custom backend
|
||||
//!
|
||||
//! Implement the [`Storage`] trait to plug in your own backend (e.g. a
|
||||
//! database). The trait requires four methods: [`push`](Storage::push),
|
||||
//! [`trials_arc`](Storage::trials_arc), [`next_trial_id`](Storage::next_trial_id),
|
||||
//! and optionally [`refresh`](Storage::refresh) for external data sources.
|
||||
//!
|
||||
//! Inject your storage into a study via the builder:
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::prelude::*;
|
||||
//! use optimizer::storage::MemoryStorage;
|
||||
//!
|
||||
//! let storage = MemoryStorage::<f64>::new();
|
||||
//! let study = Study::builder().minimize().storage(storage).build();
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "journal")]
|
||||
mod journal;
|
||||
@@ -26,7 +58,8 @@ use crate::sampler::CompletedTrial;
|
||||
/// default implementation is [`MemoryStorage`], which keeps trials in
|
||||
/// a plain `Vec` behind a read-write lock.
|
||||
///
|
||||
/// Implementations must be safe to use from multiple threads.
|
||||
/// Implementations must be `Send + Sync` because a study may be shared
|
||||
/// across threads (e.g. via [`optimize_parallel`](crate::Study::optimize_parallel)).
|
||||
pub trait Storage<V>: Send + Sync {
|
||||
/// Append a completed trial to the store.
|
||||
fn push(&self, trial: CompletedTrial<V>);
|
||||
@@ -38,14 +71,14 @@ pub trait Storage<V>: Send + Sync {
|
||||
/// lock for efficient, allocation-free access.
|
||||
fn trials_arc(&self) -> &Arc<RwLock<Vec<CompletedTrial<V>>>>;
|
||||
|
||||
/// Atomically returns the next unique trial ID.
|
||||
/// Atomically return the next unique trial ID.
|
||||
///
|
||||
/// Each call increments an internal counter so that consecutive
|
||||
/// calls always produce distinct IDs.
|
||||
fn next_trial_id(&self) -> u64;
|
||||
|
||||
/// Reload from an external source (e.g. a file written by another
|
||||
/// process). Returns `true` if the in-memory buffer was updated.
|
||||
/// process). Return `true` if the in-memory buffer was updated.
|
||||
///
|
||||
/// The default implementation is a no-op that returns `false`.
|
||||
fn refresh(&self) -> bool {
|
||||
|
||||
+238
-78
@@ -63,7 +63,7 @@ impl<V> Study<V>
|
||||
where
|
||||
V: PartialOrd,
|
||||
{
|
||||
/// Creates a new study with the given optimization direction.
|
||||
/// Create a new study with the given optimization direction.
|
||||
///
|
||||
/// Uses the default `RandomSampler` for parameter sampling.
|
||||
///
|
||||
@@ -87,7 +87,7 @@ where
|
||||
Self::with_sampler(direction, RandomSampler::new())
|
||||
}
|
||||
|
||||
/// Returns a [`StudyBuilder`] for constructing a study with a fluent API.
|
||||
/// Return a [`StudyBuilder`] for constructing a study with a fluent API.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -111,7 +111,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a study that minimizes the objective value.
|
||||
/// Create a study that minimizes the objective value.
|
||||
///
|
||||
/// This is a shorthand for `Study::with_sampler(Direction::Minimize, sampler)`.
|
||||
///
|
||||
@@ -136,7 +136,7 @@ where
|
||||
Self::with_sampler(Direction::Minimize, sampler)
|
||||
}
|
||||
|
||||
/// Creates a study that maximizes the objective value.
|
||||
/// Create a study that maximizes the objective value.
|
||||
///
|
||||
/// This is a shorthand for `Study::with_sampler(Direction::Maximize, sampler)`.
|
||||
///
|
||||
@@ -161,7 +161,7 @@ where
|
||||
Self::with_sampler(Direction::Maximize, sampler)
|
||||
}
|
||||
|
||||
/// Creates a new study with a custom sampler.
|
||||
/// Create a new study with a custom sampler.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -189,7 +189,7 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds a trial factory for sampler integration when `V = f64`.
|
||||
/// Build a trial factory for sampler integration when `V = f64`.
|
||||
fn make_trial_factory(
|
||||
sampler: &Arc<dyn Sampler>,
|
||||
storage: &Arc<dyn crate::storage::Storage<V>>,
|
||||
@@ -220,10 +220,28 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a study with a custom sampler and storage backend.
|
||||
/// Create a study with a custom sampler and storage backend.
|
||||
///
|
||||
/// This is the most general constructor — all other constructors
|
||||
/// delegate to this one.
|
||||
/// delegate to this one. Use it when you need a non-default storage
|
||||
/// backend (e.g., [`JournalStorage`](crate::storage::JournalStorage)).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `direction` - Whether to minimize or maximize the objective function.
|
||||
/// * `sampler` - The sampler to use for parameter sampling.
|
||||
/// * `storage` - The storage backend for completed trials.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::storage::MemoryStorage;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let storage = MemoryStorage::<f64>::new();
|
||||
/// let study = Study::with_sampler_and_storage(Direction::Minimize, RandomSampler::new(), storage);
|
||||
/// ```
|
||||
pub fn with_sampler_and_storage(
|
||||
direction: Direction,
|
||||
sampler: impl Sampler + 'static,
|
||||
@@ -247,28 +265,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the optimization direction.
|
||||
/// Return the optimization direction.
|
||||
#[must_use]
|
||||
pub fn direction(&self) -> Direction {
|
||||
self.direction
|
||||
}
|
||||
|
||||
/// Sets a new sampler for the study.
|
||||
/// Creates a study with a custom sampler and pruner.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sampler` - The sampler to use for parameter sampling.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::sampler::tpe::TpeSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let mut study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// study.set_sampler(TpeSampler::new());
|
||||
/// ```
|
||||
/// Creates a new study with a custom sampler and pruner.
|
||||
/// Uses the default [`MemoryStorage`](crate::storage::MemoryStorage) backend.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -310,6 +315,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the sampler used for future parameter suggestions.
|
||||
///
|
||||
/// The new sampler takes effect for all subsequent calls to
|
||||
/// [`create_trial`](Self::create_trial), [`ask`](Self::ask), and the
|
||||
/// `optimize*` family. Already-completed trials are unaffected.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::sampler::tpe::TpeSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let mut study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// study.set_sampler(TpeSampler::new());
|
||||
/// ```
|
||||
pub fn set_sampler(&mut self, sampler: impl Sampler + 'static)
|
||||
where
|
||||
V: 'static,
|
||||
@@ -318,11 +338,18 @@ where
|
||||
self.trial_factory = Self::make_trial_factory(&self.sampler, &self.storage, &self.pruner);
|
||||
}
|
||||
|
||||
/// Sets a new pruner for the study.
|
||||
/// Replace the pruner used for future trials.
|
||||
///
|
||||
/// # Arguments
|
||||
/// The new pruner takes effect for all trials created after this call.
|
||||
///
|
||||
/// * `pruner` - The pruner to use for trial pruning.
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::prelude::*;
|
||||
///
|
||||
/// let mut study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// study.set_pruner(MedianPruner::new(Direction::Minimize));
|
||||
/// ```
|
||||
pub fn set_pruner(&mut self, pruner: impl Pruner + 'static)
|
||||
where
|
||||
V: 'static,
|
||||
@@ -331,13 +358,13 @@ where
|
||||
self.trial_factory = Self::make_trial_factory(&self.sampler, &self.storage, &self.pruner);
|
||||
}
|
||||
|
||||
/// Returns a reference to the study's pruner.
|
||||
/// Return a reference to the study's current pruner.
|
||||
#[must_use]
|
||||
pub fn pruner(&self) -> &dyn Pruner {
|
||||
&*self.pruner
|
||||
}
|
||||
|
||||
/// Enqueues a specific parameter configuration to be evaluated next.
|
||||
/// Enqueue a specific parameter configuration to be evaluated next.
|
||||
///
|
||||
/// The next call to [`ask()`](Self::ask) or the next trial in [`optimize()`](Self::optimize)
|
||||
/// will use these exact parameters instead of sampling from the sampler.
|
||||
@@ -377,7 +404,7 @@ where
|
||||
self.enqueued_params.lock().push_back(params);
|
||||
}
|
||||
|
||||
/// Returns the trial ID of the current best trial from the given slice.
|
||||
/// Return the trial ID of the current best trial from the given slice.
|
||||
#[cfg(feature = "tracing")]
|
||||
fn best_id(&self, trials: &[CompletedTrial<V>]) -> Option<u64> {
|
||||
let direction = self.direction;
|
||||
@@ -388,7 +415,7 @@ where
|
||||
.map(|t| t.id)
|
||||
}
|
||||
|
||||
/// Creates a new trial with pre-set parameter values.
|
||||
/// Create a new trial with pre-set parameter values.
|
||||
///
|
||||
/// The trial gets a new unique ID but reuses the given parameters. When
|
||||
/// `suggest_param` is called on the resulting trial, fixed values are
|
||||
@@ -404,18 +431,20 @@ where
|
||||
trial
|
||||
}
|
||||
|
||||
/// Returns the number of enqueued parameter configurations.
|
||||
/// Return the number of enqueued parameter configurations.
|
||||
///
|
||||
/// See [`enqueue`](Self::enqueue) for how to add configurations.
|
||||
#[must_use]
|
||||
pub fn n_enqueued(&self) -> usize {
|
||||
self.enqueued_params.lock().len()
|
||||
}
|
||||
|
||||
/// Generates the next unique trial ID.
|
||||
/// Generate the next unique trial ID.
|
||||
pub(crate) fn next_trial_id(&self) -> u64 {
|
||||
self.storage.next_trial_id()
|
||||
}
|
||||
|
||||
/// Creates a new trial with a unique ID.
|
||||
/// Create a new trial with a unique ID.
|
||||
///
|
||||
/// The trial starts in the `Running` state and can be used to suggest
|
||||
/// parameter values. After the objective function is evaluated, call
|
||||
@@ -456,7 +485,7 @@ where
|
||||
trial
|
||||
}
|
||||
|
||||
/// Records a completed trial with its objective value.
|
||||
/// Record a completed trial with its objective value.
|
||||
///
|
||||
/// This method stores the trial's parameters, distributions, and objective
|
||||
/// value in the study's history. The stored data is used by samplers to
|
||||
@@ -499,7 +528,7 @@ where
|
||||
self.storage.push(completed);
|
||||
}
|
||||
|
||||
/// Records a failed trial with an error message.
|
||||
/// Record a failed trial with an error message.
|
||||
///
|
||||
/// Failed trials are not stored in the study's history and do not
|
||||
/// contribute to future sampling decisions. This method is useful
|
||||
@@ -582,11 +611,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a pruned trial, preserving its intermediate values.
|
||||
/// Record a pruned trial, preserving its intermediate values.
|
||||
///
|
||||
/// Pruned trials are stored alongside completed trials so that samplers
|
||||
/// can optionally learn from partial evaluations. The trial's state is
|
||||
/// set to `Pruned`.
|
||||
/// set to [`Pruned`](crate::TrialState::Pruned).
|
||||
///
|
||||
/// In practice you rarely call this directly — returning
|
||||
/// `Err(TrialPruned)` from an objective function handles pruning
|
||||
/// automatically.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -611,9 +644,9 @@ where
|
||||
self.storage.push(completed);
|
||||
}
|
||||
|
||||
/// Returns an iterator over all completed trials.
|
||||
/// Return all completed trials as a `Vec`.
|
||||
///
|
||||
/// The iterator yields references to `CompletedTrial` values, which contain
|
||||
/// The returned vector contains clones of `CompletedTrial` values, which contain
|
||||
/// the trial's parameters, distributions, and objective value.
|
||||
///
|
||||
/// Note: This method acquires a read lock on the completed trials, so the
|
||||
@@ -643,7 +676,7 @@ where
|
||||
self.storage.trials_arc().read().clone()
|
||||
}
|
||||
|
||||
/// Returns the number of completed trials.
|
||||
/// Return the number of completed trials.
|
||||
///
|
||||
/// Failed trials are not counted.
|
||||
///
|
||||
@@ -667,7 +700,9 @@ where
|
||||
self.storage.trials_arc().read().len()
|
||||
}
|
||||
|
||||
/// Returns the number of pruned trials.
|
||||
/// Return the number of pruned trials.
|
||||
///
|
||||
/// Pruned trials are those that were stopped early by the pruner.
|
||||
#[must_use]
|
||||
pub fn n_pruned_trials(&self) -> usize {
|
||||
self.storage
|
||||
@@ -678,7 +713,7 @@ where
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Compares two completed trials using constraint-aware ranking.
|
||||
/// Compare two completed trials using constraint-aware ranking.
|
||||
///
|
||||
/// 1. Feasible trials always rank above infeasible trials.
|
||||
/// 2. Among feasible trials, rank by objective value (respecting direction).
|
||||
@@ -708,7 +743,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the trial with the best objective value.
|
||||
/// Return the trial with the best objective value.
|
||||
///
|
||||
/// The "best" trial depends on the optimization direction:
|
||||
/// - `Direction::Minimize`: Returns the trial with the lowest objective value.
|
||||
@@ -762,7 +797,7 @@ where
|
||||
Ok(best.clone())
|
||||
}
|
||||
|
||||
/// Returns the best objective value found so far.
|
||||
/// Return the best objective value found so far.
|
||||
///
|
||||
/// The "best" value depends on the optimization direction:
|
||||
/// - `Direction::Minimize`: Returns the lowest objective value.
|
||||
@@ -803,13 +838,33 @@ where
|
||||
self.best_trial().map(|trial| trial.value)
|
||||
}
|
||||
|
||||
/// Returns the top `n` trials sorted by objective value.
|
||||
/// Return the top `n` trials sorted by objective value.
|
||||
///
|
||||
/// For `Direction::Minimize`, returns trials with the lowest values.
|
||||
/// For `Direction::Maximize`, returns trials with the highest values.
|
||||
/// Only includes completed trials (not failed or pruned).
|
||||
///
|
||||
/// If fewer than `n` completed trials exist, returns all of them.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// let x = FloatParam::new(0.0, 10.0);
|
||||
///
|
||||
/// for val in [5.0, 1.0, 3.0] {
|
||||
/// let mut t = study.create_trial();
|
||||
/// let _ = x.suggest(&mut t);
|
||||
/// study.complete_trial(t, val);
|
||||
/// }
|
||||
///
|
||||
/// let top2 = study.top_trials(2);
|
||||
/// assert_eq!(top2.len(), 2);
|
||||
/// assert!(top2[0].value <= top2[1].value);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn top_trials(&self, n: usize) -> Vec<CompletedTrial<V>>
|
||||
where
|
||||
@@ -828,7 +883,7 @@ where
|
||||
completed
|
||||
}
|
||||
|
||||
/// Runs optimization with the given objective function.
|
||||
/// Run optimization with the given objective function.
|
||||
///
|
||||
/// This method runs `n_trials` evaluations sequentially. For each trial:
|
||||
/// 1. A new trial is created
|
||||
@@ -936,7 +991,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs optimization asynchronously with the given objective function.
|
||||
/// Run optimization asynchronously with the given objective function.
|
||||
///
|
||||
/// This method runs `n_trials` evaluations sequentially, but the objective
|
||||
/// function can be async (e.g., for I/O-bound operations like network requests
|
||||
@@ -1036,7 +1091,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs optimization with bounded parallelism for concurrent trial evaluation.
|
||||
/// Run optimization with bounded parallelism for concurrent trial evaluation.
|
||||
///
|
||||
/// This method runs up to `concurrency` trials simultaneously, allowing
|
||||
/// efficient use of async I/O-bound objective functions. A semaphore limits
|
||||
@@ -1163,7 +1218,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs optimization with a callback for monitoring progress.
|
||||
/// Run optimization with a callback for monitoring progress.
|
||||
///
|
||||
/// This method is similar to `optimize`, but calls a callback function after
|
||||
/// each completed trial. The callback can inspect the study state and the
|
||||
@@ -1305,7 +1360,7 @@ where
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/// Runs optimization until the given duration has elapsed.
|
||||
/// Run optimization until the given duration has elapsed.
|
||||
///
|
||||
/// Trials that are already running when the timeout is reached will
|
||||
/// complete — we never interrupt mid-trial. The actual elapsed time
|
||||
@@ -1392,7 +1447,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs optimization until the given duration has elapsed, with a callback.
|
||||
/// Run optimization until the given duration has elapsed, with a callback.
|
||||
///
|
||||
/// Like [`optimize_until`](Self::optimize_until), but calls a callback after
|
||||
/// each completed trial. The callback can stop optimization early by returning
|
||||
@@ -1526,16 +1581,17 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs optimization asynchronously until the given duration has elapsed.
|
||||
/// Run optimization asynchronously until the given duration has elapsed.
|
||||
///
|
||||
/// The async variant of [`optimize_until`](Self::optimize_until). Trials are
|
||||
/// run sequentially, but the objective function can be async.
|
||||
/// run sequentially, but the objective function can be async (useful for
|
||||
/// I/O-bound evaluations).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - The maximum wall-clock time to spend on optimization.
|
||||
/// * `objective` - A function that takes a `Trial` and returns a `Future`
|
||||
/// that resolves to a tuple of `(Trial, Result<V, E>)`.
|
||||
/// that resolves to a tuple of `(Trial, V)` or an error.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -1585,7 +1641,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs optimization with bounded parallelism until the given duration has elapsed.
|
||||
/// Run optimization with bounded parallelism until the given duration has elapsed.
|
||||
///
|
||||
/// The parallel variant of [`optimize_until`](Self::optimize_until). Runs up to
|
||||
/// `concurrency` trials simultaneously using async tasks. New trials are spawned
|
||||
@@ -1675,7 +1731,7 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs optimization with automatic retry for failed trials.
|
||||
/// Run optimization with automatic retry for failed trials.
|
||||
///
|
||||
/// If the objective function returns an error, the same parameter
|
||||
/// configuration is retried up to `max_retries` times. Only after all
|
||||
@@ -1789,7 +1845,7 @@ impl<V> Study<V>
|
||||
where
|
||||
V: PartialOrd + Clone + fmt::Display,
|
||||
{
|
||||
/// Export completed trials to CSV format.
|
||||
/// Write completed trials to a writer in CSV format.
|
||||
///
|
||||
/// Columns: `trial_id`, `value`, `state`, then one column per unique
|
||||
/// parameter label, then one column per unique user-attribute key.
|
||||
@@ -1800,6 +1856,25 @@ where
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an I/O error if writing fails.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// let x = FloatParam::new(0.0, 10.0).name("x");
|
||||
///
|
||||
/// let mut trial = study.create_trial();
|
||||
/// let _ = x.suggest(&mut trial);
|
||||
/// study.complete_trial(trial, 0.42);
|
||||
///
|
||||
/// let mut buf = Vec::new();
|
||||
/// study.to_csv(&mut buf).unwrap();
|
||||
/// let csv = String::from_utf8(buf).unwrap();
|
||||
/// assert!(csv.contains("trial_id"));
|
||||
/// ```
|
||||
pub fn to_csv(&self, mut writer: impl std::io::Write) -> std::io::Result<()> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -1892,7 +1967,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export completed trials to a CSV file.
|
||||
/// Export completed trials to a CSV file at the given path.
|
||||
///
|
||||
/// Convenience wrapper around [`to_csv`](Self::to_csv) that creates a
|
||||
/// buffered file writer.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -1902,7 +1980,7 @@ where
|
||||
self.to_csv(std::io::BufWriter::new(file))
|
||||
}
|
||||
|
||||
/// Returns a human-readable summary of the study.
|
||||
/// Return a human-readable summary of the study.
|
||||
///
|
||||
/// The summary includes:
|
||||
/// - Optimization direction and total trial count
|
||||
@@ -1973,10 +2051,24 @@ impl<V> Study<V>
|
||||
where
|
||||
V: PartialOrd + Clone,
|
||||
{
|
||||
/// Returns an iterator over all completed trials.
|
||||
/// Return an iterator over all completed trials.
|
||||
///
|
||||
/// This clones the internal trial list, so it is suitable for
|
||||
/// analysis and iteration but not for hot paths.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// let trial = study.create_trial();
|
||||
/// study.complete_trial(trial, 1.0);
|
||||
///
|
||||
/// for t in study.iter() {
|
||||
/// println!("Trial {} → {}", t.id, t.value);
|
||||
/// }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn iter(&self) -> std::vec::IntoIter<CompletedTrial<V>> {
|
||||
self.trials().into_iter()
|
||||
@@ -1987,7 +2079,7 @@ impl<V> Study<V>
|
||||
where
|
||||
V: PartialOrd + Clone + Into<f64>,
|
||||
{
|
||||
/// Computes parameter importance scores using Spearman rank correlation.
|
||||
/// Compute parameter importance scores using Spearman rank correlation.
|
||||
///
|
||||
/// For each parameter, the absolute Spearman correlation between its values
|
||||
/// and the objective values is computed across all completed trials. Scores
|
||||
@@ -2086,7 +2178,7 @@ where
|
||||
scores
|
||||
}
|
||||
|
||||
/// Computes parameter importance using fANOVA (functional ANOVA) with
|
||||
/// Compute parameter importance using fANOVA (functional ANOVA) with
|
||||
/// default configuration.
|
||||
///
|
||||
/// Fits a random forest to the trial data and decomposes variance into
|
||||
@@ -2097,11 +2189,33 @@ where
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::Error::NoCompletedTrials`] if fewer than 2 trials have completed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// let x = FloatParam::new(0.0, 10.0).name("x");
|
||||
/// let y = FloatParam::new(0.0, 10.0).name("y");
|
||||
///
|
||||
/// study
|
||||
/// .optimize(30, |trial| {
|
||||
/// let xv = x.suggest(trial)?;
|
||||
/// let yv = y.suggest(trial)?;
|
||||
/// Ok::<_, optimizer::Error>(xv * xv + 0.1 * yv)
|
||||
/// })
|
||||
/// .unwrap();
|
||||
///
|
||||
/// let result = study.fanova().unwrap();
|
||||
/// assert!(!result.main_effects.is_empty());
|
||||
/// ```
|
||||
pub fn fanova(&self) -> crate::Result<crate::fanova::FanovaResult> {
|
||||
self.fanova_with_config(&crate::fanova::FanovaConfig::default())
|
||||
}
|
||||
|
||||
/// Computes parameter importance using fANOVA with custom configuration.
|
||||
/// Compute parameter importance using fANOVA with custom configuration.
|
||||
///
|
||||
/// See [`Self::fanova`] for details. The [`FanovaConfig`](crate::fanova::FanovaConfig)
|
||||
/// allows tuning the number of trees, tree depth, and random seed.
|
||||
@@ -2316,7 +2430,30 @@ impl Study<f64> {
|
||||
}
|
||||
|
||||
impl<V: PartialOrd + Send + Sync + 'static> Study<V> {
|
||||
/// Creates a study with a custom sampler, pruner, and storage backend.
|
||||
/// Create a study with a custom sampler, pruner, and storage backend.
|
||||
///
|
||||
/// The most flexible constructor, allowing full control over all components.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `direction` - Whether to minimize or maximize the objective function.
|
||||
/// * `sampler` - The sampler to use for parameter sampling.
|
||||
/// * `pruner` - The pruner to use for trial pruning.
|
||||
/// * `storage` - The storage backend for completed trials.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::prelude::*;
|
||||
/// use optimizer::storage::MemoryStorage;
|
||||
///
|
||||
/// let study = Study::with_sampler_pruner_and_storage(
|
||||
/// Direction::Minimize,
|
||||
/// TpeSampler::new(),
|
||||
/// MedianPruner::new(Direction::Minimize),
|
||||
/// MemoryStorage::<f64>::new(),
|
||||
/// );
|
||||
/// ```
|
||||
pub fn with_sampler_pruner_and_storage(
|
||||
direction: Direction,
|
||||
sampler: impl Sampler + 'static,
|
||||
@@ -2373,49 +2510,55 @@ pub struct StudyBuilder<V: PartialOrd = f64> {
|
||||
}
|
||||
|
||||
impl<V: PartialOrd> StudyBuilder<V> {
|
||||
/// Sets the optimization direction to minimize.
|
||||
/// Set the optimization direction to minimize (the default).
|
||||
#[must_use]
|
||||
pub fn minimize(mut self) -> Self {
|
||||
self.direction = Direction::Minimize;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the optimization direction to maximize.
|
||||
/// Set the optimization direction to maximize.
|
||||
#[must_use]
|
||||
pub fn maximize(mut self) -> Self {
|
||||
self.direction = Direction::Maximize;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the optimization direction.
|
||||
/// Set the optimization direction explicitly.
|
||||
#[must_use]
|
||||
pub fn direction(mut self, direction: Direction) -> Self {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the sampler used for parameter suggestions.
|
||||
/// Set the sampler used for parameter suggestions.
|
||||
///
|
||||
/// Defaults to [`RandomSampler`] if not specified.
|
||||
#[must_use]
|
||||
pub fn sampler(mut self, sampler: impl Sampler + 'static) -> Self {
|
||||
self.sampler = Some(Box::new(sampler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the pruner used for early stopping of trials.
|
||||
/// Set the pruner used for early stopping of trials.
|
||||
///
|
||||
/// Defaults to [`NopPruner`] (no pruning) if not specified.
|
||||
#[must_use]
|
||||
pub fn pruner(mut self, pruner: impl Pruner + 'static) -> Self {
|
||||
self.pruner = Some(Box::new(pruner));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a custom storage backend.
|
||||
/// Set a custom storage backend.
|
||||
///
|
||||
/// Defaults to [`MemoryStorage`](crate::storage::MemoryStorage) if not specified.
|
||||
#[must_use]
|
||||
pub fn storage(mut self, storage: impl crate::storage::Storage<V> + 'static) -> Self {
|
||||
self.storage = Some(Box::new(storage));
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the [`Study`] with the configured options.
|
||||
/// Build the [`Study`] with the configured options.
|
||||
#[must_use]
|
||||
pub fn build(self) -> Study<V>
|
||||
where
|
||||
@@ -2450,15 +2593,31 @@ impl<V> Study<V>
|
||||
where
|
||||
V: PartialOrd + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
|
||||
{
|
||||
/// Creates a study backed by a JSONL journal file.
|
||||
/// Create a study backed by a JSONL journal file.
|
||||
///
|
||||
/// Any existing trials in the file are loaded into memory and the
|
||||
/// trial ID counter is set to one past the highest stored ID. New
|
||||
/// trials are written through to the file on completion.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `direction` - Whether to minimize or maximize the objective function.
|
||||
/// * `sampler` - The sampler to use for parameter sampling.
|
||||
/// * `path` - Path to the JSONL journal file (created if absent).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a [`Storage`](crate::Error::Storage) error if loading fails.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use optimizer::sampler::tpe::TpeSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> =
|
||||
/// Study::with_journal(Direction::Minimize, TpeSampler::new(), "trials.jsonl").unwrap();
|
||||
/// ```
|
||||
pub fn with_journal(
|
||||
direction: Direction,
|
||||
sampler: impl Sampler + 'static,
|
||||
@@ -2470,9 +2629,9 @@ where
|
||||
}
|
||||
|
||||
impl Study<f64> {
|
||||
/// Generates an HTML report with interactive Plotly.js charts.
|
||||
/// Generate an HTML report with interactive Plotly.js charts.
|
||||
///
|
||||
/// Creates a self-contained HTML file that can be opened in any browser.
|
||||
/// Create a self-contained HTML file that can be opened in any browser.
|
||||
/// See [`generate_html_report`](crate::visualization::generate_html_report)
|
||||
/// for details on the included charts.
|
||||
///
|
||||
@@ -2519,6 +2678,7 @@ impl<V: PartialOrd + Clone + serde::Serialize> Study<V> {
|
||||
/// Export trials as a pretty-printed JSON array to a file.
|
||||
///
|
||||
/// Each element in the array is a serialized [`CompletedTrial`].
|
||||
/// Requires the `serde` feature.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -2529,7 +2689,7 @@ impl<V: PartialOrd + Clone + serde::Serialize> Study<V> {
|
||||
serde_json::to_writer_pretty(file, &trials).map_err(std::io::Error::other)
|
||||
}
|
||||
|
||||
/// Saves the study state to a JSON file.
|
||||
/// Save the study state to a JSON file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -2561,7 +2721,7 @@ impl<V: PartialOrd + Clone + serde::Serialize> Study<V> {
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<V: PartialOrd + Clone + Default + serde::Serialize> Study<V> {
|
||||
/// Runs optimization with automatic checkpointing every `interval` trials.
|
||||
/// Run optimization with automatic checkpointing every `interval` trials.
|
||||
///
|
||||
/// This is convenience sugar over [`optimize_with_callback`](Self::optimize_with_callback)
|
||||
/// combined with [`save`](Self::save). The checkpoint is written atomically so
|
||||
@@ -2595,7 +2755,7 @@ impl<V: PartialOrd + Clone + Default + serde::Serialize> Study<V> {
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<V: PartialOrd + Send + Sync + Clone + serde::de::DeserializeOwned + 'static> Study<V> {
|
||||
/// Loads a study from a JSON file.
|
||||
/// Load a study from a JSON file.
|
||||
///
|
||||
/// The loaded study uses a `RandomSampler` by default. Call
|
||||
/// [`set_sampler()`](Self::set_sampler) to restore the original sampler
|
||||
|
||||
+120
-42
@@ -1,4 +1,23 @@
|
||||
//! Trial implementation for tracking sampled parameters and trial state.
|
||||
//! Trial lifecycle management for optimization runs.
|
||||
//!
|
||||
//! A [`Trial`] represents a single evaluation of the objective function. The study
|
||||
//! creates trials, the objective function samples parameters from them via
|
||||
//! [`Parameter::suggest`](crate::parameter::Parameter::suggest), and reports
|
||||
//! intermediate values for pruning decisions.
|
||||
//!
|
||||
//! # Lifecycle
|
||||
//!
|
||||
//! 1. **Created** — `Study` creates a trial with [`Trial::new`] or internally via
|
||||
//! `Trial::with_sampler`.
|
||||
//! 2. **Running** — The objective calls [`Trial::suggest_param`] to sample parameters
|
||||
//! and optionally [`Trial::report`] / [`Trial::should_prune`] for early stopping.
|
||||
//! 3. **Completed / Failed / Pruned** — The study marks the trial's final state.
|
||||
//!
|
||||
//! # User Attributes
|
||||
//!
|
||||
//! Trials support arbitrary key-value metadata via [`Trial::set_user_attr`] and
|
||||
//! [`Trial::user_attr`], useful for logging hyperparameters, hardware info, or
|
||||
//! debug notes alongside the optimization results.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -57,14 +76,26 @@ impl From<bool> for AttrValue {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trial represents a single evaluation of the objective function.
|
||||
/// A single evaluation of the objective function.
|
||||
///
|
||||
/// Each trial has a unique ID and stores the sampled parameters along with
|
||||
/// their distributions. The trial progresses through states: Running -> Complete/Failed.
|
||||
/// their distributions. The trial progresses through states:
|
||||
/// `Running` → `Complete` / `Failed` / `Pruned`.
|
||||
///
|
||||
/// Trials use a sampler to generate parameter values. When created through
|
||||
/// `Study::create_trial()`, the trial receives the study's sampler and access
|
||||
/// to the history of completed trials for informed sampling.
|
||||
/// Trials use a [`Sampler`](crate::sampler::Sampler) to generate parameter
|
||||
/// values. When created through [`Study::create_trial`](crate::Study::create_trial),
|
||||
/// the trial receives the study's sampler and access to the history of
|
||||
/// completed trials for informed sampling.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// let x = FloatParam::new(-5.0, 5.0).suggest(&mut trial).unwrap();
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Trial {
|
||||
/// Unique identifier for this trial.
|
||||
@@ -113,13 +144,14 @@ impl core::fmt::Debug for Trial {
|
||||
}
|
||||
|
||||
impl Trial {
|
||||
/// Creates a new trial with the given ID.
|
||||
/// Create a new trial with the given ID.
|
||||
///
|
||||
/// The trial starts in the `Running` state with no parameters sampled.
|
||||
/// This constructor creates a trial without a sampler, which will use
|
||||
/// local random sampling for suggest methods.
|
||||
/// This constructor creates a trial without a sampler, which will fall
|
||||
/// back to random sampling for [`suggest_param`](Self::suggest_param) calls.
|
||||
///
|
||||
/// For trials that use the study's sampler, use `Trial::with_sampler` instead.
|
||||
/// For trials that use the study's sampler, the study creates them
|
||||
/// internally via `Trial::with_sampler`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -151,10 +183,10 @@ impl Trial {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new trial with a sampler and access to trial history.
|
||||
/// Create a new trial with a sampler and access to trial history.
|
||||
///
|
||||
/// This constructor is used by `Study::create_trial()` to create trials
|
||||
/// that use the study's sampler for informed parameter suggestions.
|
||||
/// Used internally by `Study::create_trial()` to create trials that use
|
||||
/// the study's sampler for informed parameter suggestions.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -183,19 +215,19 @@ impl Trial {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets pre-filled parameters on this trial.
|
||||
/// Set pre-filled parameters on this trial.
|
||||
///
|
||||
/// When `suggest_param` is called for a parameter that has a fixed value,
|
||||
/// the fixed value is used instead of sampling.
|
||||
/// When [`suggest_param`](Self::suggest_param) is called for a parameter
|
||||
/// that has a fixed value, the fixed value is used instead of sampling.
|
||||
pub(crate) fn set_fixed_params(&mut self, params: HashMap<ParamId, ParamValue>) {
|
||||
self.fixed_params = params;
|
||||
}
|
||||
|
||||
/// Samples a value from the given distribution using the sampler.
|
||||
/// Sample a value from the given distribution using the sampler.
|
||||
///
|
||||
/// If the trial has a sampler, it delegates to the sampler's sample method
|
||||
/// with the history of completed trials. Otherwise, it uses the `RandomSampler`
|
||||
/// as a fallback.
|
||||
/// If the trial has a sampler, delegates to the sampler's sample method
|
||||
/// with the history of completed trials. Otherwise, falls back to
|
||||
/// [`RandomSampler`](crate::sampler::random::RandomSampler).
|
||||
fn sample_value(&self, distribution: &Distribution) -> ParamValue {
|
||||
if let (Some(sampler), Some(history)) = (&self.sampler, &self.history) {
|
||||
let history_guard = history.read();
|
||||
@@ -208,40 +240,55 @@ impl Trial {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unique ID of this trial.
|
||||
/// Return the unique ID of this trial.
|
||||
#[must_use]
|
||||
pub fn id(&self) -> u64 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns the current state of this trial.
|
||||
/// Return the current state of this trial.
|
||||
#[must_use]
|
||||
pub fn state(&self) -> TrialState {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Returns a reference to the sampled parameters.
|
||||
/// Return a reference to the sampled parameters, keyed by [`ParamId`](crate::parameter::ParamId).
|
||||
#[must_use]
|
||||
pub fn params(&self) -> &HashMap<ParamId, ParamValue> {
|
||||
&self.params
|
||||
}
|
||||
|
||||
/// Returns a reference to the parameter distributions.
|
||||
/// Return a reference to the parameter distributions, keyed by [`ParamId`](crate::parameter::ParamId).
|
||||
#[must_use]
|
||||
pub fn distributions(&self) -> &HashMap<ParamId, Distribution> {
|
||||
&self.distributions
|
||||
}
|
||||
|
||||
/// Returns a reference to the parameter labels.
|
||||
/// Return a reference to the parameter labels, keyed by [`ParamId`](crate::parameter::ParamId).
|
||||
#[must_use]
|
||||
pub fn param_labels(&self) -> &HashMap<ParamId, String> {
|
||||
&self.param_labels
|
||||
}
|
||||
|
||||
/// Reports an intermediate objective value at a given step.
|
||||
/// Report an intermediate objective value at a given step.
|
||||
///
|
||||
/// Steps should be monotonically increasing (e.g., epoch number).
|
||||
/// Duplicate steps overwrite the previous value.
|
||||
/// Call this during iterative training (e.g., once per epoch) so the
|
||||
/// [`Pruner`](crate::pruner::Pruner) can decide whether to stop the trial
|
||||
/// early. Steps should be monotonically increasing; duplicate steps
|
||||
/// overwrite the previous value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// for epoch in 0..10 {
|
||||
/// let loss = 1.0 / (epoch as f64 + 1.0);
|
||||
/// trial.report(epoch, loss);
|
||||
/// }
|
||||
/// assert_eq!(trial.intermediate_values().len(), 10);
|
||||
/// ```
|
||||
pub fn report(&mut self, step: u64, value: f64) {
|
||||
if let Some(entry) = self
|
||||
.intermediate_values
|
||||
@@ -256,8 +303,11 @@ impl Trial {
|
||||
|
||||
/// Ask whether this trial should be pruned at the current step.
|
||||
///
|
||||
/// Returns `true` if the pruner recommends stopping this trial.
|
||||
/// The caller should return `Err(TrialPruned)` from the objective.
|
||||
/// Return `true` if the pruner recommends stopping this trial based on
|
||||
/// the intermediate values reported so far. When `true`, the objective
|
||||
/// should return early with `Err(TrialPruned)?`.
|
||||
///
|
||||
/// Always returns `false` when no pruner is configured.
|
||||
#[must_use]
|
||||
pub fn should_prune(&self) -> bool {
|
||||
let (Some(pruner), Some(history)) = (&self.pruner, &self.history) else {
|
||||
@@ -274,59 +324,87 @@ impl Trial {
|
||||
prune
|
||||
}
|
||||
|
||||
/// Returns all intermediate values reported so far.
|
||||
/// Return all intermediate values reported so far as `(step, value)` pairs.
|
||||
#[must_use]
|
||||
pub fn intermediate_values(&self) -> &[(u64, f64)] {
|
||||
&self.intermediate_values
|
||||
}
|
||||
|
||||
/// Sets a user attribute on this trial.
|
||||
/// Set a user attribute on this trial.
|
||||
///
|
||||
/// User attributes are arbitrary key-value pairs for logging, debugging,
|
||||
/// or analysis. Values can be `f64`, `i64`, `String`, `&str`, or `bool`
|
||||
/// (anything implementing `Into<AttrValue>`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// trial.set_user_attr("gpu", "A100");
|
||||
/// trial.set_user_attr("batch_size", 64_i64);
|
||||
/// trial.set_user_attr("accuracy", 0.95);
|
||||
/// ```
|
||||
pub fn set_user_attr(&mut self, key: impl Into<String>, value: impl Into<AttrValue>) {
|
||||
self.user_attrs.insert(key.into(), value.into());
|
||||
}
|
||||
|
||||
/// Gets a user attribute by key.
|
||||
/// Return a user attribute by key, or `None` if it does not exist.
|
||||
#[must_use]
|
||||
pub fn user_attr(&self, key: &str) -> Option<&AttrValue> {
|
||||
self.user_attrs.get(key)
|
||||
}
|
||||
|
||||
/// Returns all user attributes.
|
||||
/// Return all user attributes as a map.
|
||||
#[must_use]
|
||||
pub fn user_attrs(&self) -> &HashMap<String, AttrValue> {
|
||||
&self.user_attrs
|
||||
}
|
||||
|
||||
/// Sets constraint values for this trial.
|
||||
/// Set constraint values for this trial.
|
||||
///
|
||||
/// Each value represents a constraint; a value <= 0.0 means the constraint
|
||||
/// is satisfied (feasible). A value > 0.0 means the constraint is violated.
|
||||
/// Each element represents one constraint. A value ≤ 0.0 means the
|
||||
/// constraint is satisfied (feasible); a value > 0.0 means violated.
|
||||
/// Constrained samplers (e.g., NSGA-II with constraints) use these values
|
||||
/// to prefer feasible solutions.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::Trial;
|
||||
///
|
||||
/// let mut trial = Trial::new(0);
|
||||
/// // Two constraints: first satisfied, second violated
|
||||
/// trial.set_constraints(vec![-0.5, 0.3]);
|
||||
/// assert_eq!(trial.constraint_values(), &[-0.5, 0.3]);
|
||||
/// ```
|
||||
pub fn set_constraints(&mut self, values: Vec<f64>) {
|
||||
self.constraint_values = values;
|
||||
}
|
||||
|
||||
/// Returns the constraint values for this trial.
|
||||
/// Return the constraint values for this trial.
|
||||
#[must_use]
|
||||
pub fn constraint_values(&self) -> &[f64] {
|
||||
&self.constraint_values
|
||||
}
|
||||
|
||||
/// Sets the trial state to Complete.
|
||||
/// Set the trial state to `Complete`.
|
||||
pub(crate) fn set_complete(&mut self) {
|
||||
self.state = TrialState::Complete;
|
||||
}
|
||||
|
||||
/// Sets the trial state to Failed.
|
||||
/// Set the trial state to `Failed`.
|
||||
pub(crate) fn set_failed(&mut self) {
|
||||
self.state = TrialState::Failed;
|
||||
}
|
||||
|
||||
/// Sets the trial state to Pruned.
|
||||
/// Set the trial state to `Pruned`.
|
||||
pub(crate) fn set_pruned(&mut self) {
|
||||
self.state = TrialState::Pruned;
|
||||
}
|
||||
|
||||
/// Suggests a parameter value using a [`Parameter`] definition.
|
||||
/// Suggest a parameter value using a [`Parameter`] definition.
|
||||
///
|
||||
/// This is the primary entry point for sampling parameters. It handles
|
||||
/// validation, caching, conflict detection, sampling, and conversion.
|
||||
|
||||
+46
-10
@@ -1,7 +1,41 @@
|
||||
//! HTML report generation for optimization visualization.
|
||||
//!
|
||||
//! Generates self-contained HTML files with embedded Plotly.js charts
|
||||
//! for offline visualization of optimization results.
|
||||
//! Generate self-contained HTML files with embedded
|
||||
//! [Plotly.js](https://plotly.com/javascript/) charts for offline
|
||||
//! visualization of optimization results. No feature flag is required —
|
||||
//! this module is always available.
|
||||
//!
|
||||
//! # Charts included
|
||||
//!
|
||||
//! | Chart | Description |
|
||||
//! |---|---|
|
||||
//! | **Optimization history** | Objective value vs trial number with best-so-far line |
|
||||
//! | **Slice plots** | Objective value vs each parameter (1D scatter per param) |
|
||||
//! | **Parallel coordinates** | Multi-parameter relationship view (color = objective) |
|
||||
//! | **Parameter importance** | Horizontal bar chart of Spearman-based importance |
|
||||
//! | **Trial timeline** | Duration/index of each trial, color-coded by state |
|
||||
//! | **Intermediate values** | Per-trial learning curves (if pruning data available) |
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Call [`Study::export_html()`](crate::Study::export_html) or
|
||||
//! [`generate_html_report()`] directly:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use optimizer::prelude::*;
|
||||
//!
|
||||
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
//! # let x = FloatParam::new(0.0, 1.0);
|
||||
//! # study.optimize(10, |trial| {
|
||||
//! # let v = x.suggest(trial)?;
|
||||
//! # Ok::<_, optimizer::Error>(v * v)
|
||||
//! # }).unwrap();
|
||||
//! study.export_html("report.html").unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! The output is a single HTML file that can be opened in any browser.
|
||||
//! An internet connection is needed on first load to fetch `Plotly.js`
|
||||
//! from a CDN.
|
||||
|
||||
use core::fmt::Write as _;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -15,17 +49,19 @@ use crate::types::{Direction, TrialState};
|
||||
|
||||
/// Generate an HTML report with interactive Plotly.js charts.
|
||||
///
|
||||
/// Creates a self-contained HTML file at `path` containing:
|
||||
/// - **Optimization history**: Objective value vs trial number with best-so-far line
|
||||
/// - **Slice plots**: Objective value vs each parameter (1D scatter)
|
||||
/// - **Parallel coordinates**: Multi-parameter relationship view
|
||||
/// - **Trial timeline**: Duration index of each trial (horizontal bar)
|
||||
/// - **Intermediate values**: Learning curves per trial (if pruning data available)
|
||||
/// - **Parameter importance**: Bar chart (if enough completed trials)
|
||||
/// Create a self-contained HTML file at `path` containing up to six
|
||||
/// interactive charts. Charts that require data not present in the study
|
||||
/// (e.g., intermediate values) are automatically omitted.
|
||||
///
|
||||
/// The report includes: optimization history, slice plots, parallel
|
||||
/// coordinates, parameter importance, trial timeline, and intermediate
|
||||
/// values (when available).
|
||||
///
|
||||
/// This is also available as [`Study::export_html()`](crate::Study::export_html).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an I/O error if the file cannot be created or written.
|
||||
/// Return an I/O error if the file cannot be created or written.
|
||||
pub fn generate_html_report(
|
||||
study: &crate::Study<f64>,
|
||||
path: impl AsRef<Path>,
|
||||
|
||||
Reference in New Issue
Block a user