feat: add Differential Evolution sampler
Population-based optimizer with mutation + crossover. Supports three strategies (Rand1, Best1, CurrentToBest1), configurable F and CR, and auto-sized populations. No extra dependencies needed.
This commit is contained in:
+6
-2
@@ -257,7 +257,9 @@ pub use sampler::CompletedTrial;
|
|||||||
pub use sampler::bohb::BohbSampler;
|
pub use sampler::bohb::BohbSampler;
|
||||||
#[cfg(feature = "cma-es")]
|
#[cfg(feature = "cma-es")]
|
||||||
pub use sampler::cma_es::CmaEsSampler;
|
pub use sampler::cma_es::CmaEsSampler;
|
||||||
pub use sampler::de::{DeSampler, DeStrategy};
|
pub use sampler::differential_evolution::{
|
||||||
|
DifferentialEvolutionSampler, DifferentialEvolutionStrategy,
|
||||||
|
};
|
||||||
#[cfg(feature = "gp")]
|
#[cfg(feature = "gp")]
|
||||||
pub use sampler::gp::GpSampler;
|
pub use sampler::gp::GpSampler;
|
||||||
pub use sampler::grid::GridSearchSampler;
|
pub use sampler::grid::GridSearchSampler;
|
||||||
@@ -300,7 +302,9 @@ pub mod prelude {
|
|||||||
pub use crate::sampler::bohb::BohbSampler;
|
pub use crate::sampler::bohb::BohbSampler;
|
||||||
#[cfg(feature = "cma-es")]
|
#[cfg(feature = "cma-es")]
|
||||||
pub use crate::sampler::cma_es::CmaEsSampler;
|
pub use crate::sampler::cma_es::CmaEsSampler;
|
||||||
pub use crate::sampler::de::{DeSampler, DeStrategy};
|
pub use crate::sampler::differential_evolution::{
|
||||||
|
DifferentialEvolutionSampler, DifferentialEvolutionStrategy,
|
||||||
|
};
|
||||||
#[cfg(feature = "gp")]
|
#[cfg(feature = "gp")]
|
||||||
pub use crate::sampler::gp::GpSampler;
|
pub use crate::sampler::gp::GpSampler;
|
||||||
pub use crate::sampler::grid::GridSearchSampler;
|
pub use crate::sampler::grid::GridSearchSampler;
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
//! # Examples
|
//! # Examples
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use optimizer::sampler::de::DeSampler;
|
//! use optimizer::sampler::differential_evolution::DifferentialEvolutionSampler;
|
||||||
//! use optimizer::{Direction, Study};
|
//! use optimizer::{Direction, Study};
|
||||||
//!
|
//!
|
||||||
//! let sampler = DeSampler::with_seed(42);
|
//! let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ use crate::sampler::{CompletedTrial, Sampler};
|
|||||||
///
|
///
|
||||||
/// Controls how mutant vectors are created from the current population.
|
/// Controls how mutant vectors are created from the current population.
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub enum DeStrategy {
|
pub enum DifferentialEvolutionStrategy {
|
||||||
/// DE/rand/1: `v = x_r1 + F * (x_r2 - x_r3)`
|
/// DE/rand/1: `v = x_r1 + F * (x_r2 - x_r3)`
|
||||||
///
|
///
|
||||||
/// The most robust strategy. Uses three random population members.
|
/// The most robust strategy. Uses three random population members.
|
||||||
@@ -57,36 +57,46 @@ pub enum DeStrategy {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use optimizer::sampler::de::DeSampler;
|
/// use optimizer::sampler::differential_evolution::DifferentialEvolutionSampler;
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
/// // Default configuration
|
/// // Default configuration
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, DeSampler::new());
|
/// let study: Study<f64> =
|
||||||
|
/// Study::with_sampler(Direction::Minimize, DifferentialEvolutionSampler::new());
|
||||||
///
|
///
|
||||||
/// // With seed for reproducibility
|
/// // With seed for reproducibility
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, DeSampler::with_seed(42));
|
/// let study: Study<f64> = Study::with_sampler(
|
||||||
|
/// Direction::Minimize,
|
||||||
|
/// DifferentialEvolutionSampler::with_seed(42),
|
||||||
|
/// );
|
||||||
///
|
///
|
||||||
/// // Custom configuration via builder
|
/// // Custom configuration via builder
|
||||||
/// use optimizer::sampler::de::DeStrategy;
|
/// use optimizer::sampler::differential_evolution::DifferentialEvolutionStrategy;
|
||||||
/// let sampler = DeSampler::builder()
|
/// let sampler = DifferentialEvolutionSampler::builder()
|
||||||
/// .mutation_factor(0.8)
|
/// .mutation_factor(0.8)
|
||||||
/// .crossover_rate(0.9)
|
/// .crossover_rate(0.9)
|
||||||
/// .strategy(DeStrategy::Best1)
|
/// .strategy(DifferentialEvolutionStrategy::Best1)
|
||||||
/// .population_size(30)
|
/// .population_size(30)
|
||||||
/// .seed(42)
|
/// .seed(42)
|
||||||
/// .build();
|
/// .build();
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
/// ```
|
/// ```
|
||||||
pub struct DeSampler {
|
pub struct DifferentialEvolutionSampler {
|
||||||
state: Mutex<DeState>,
|
state: Mutex<State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeSampler {
|
impl DifferentialEvolutionSampler {
|
||||||
/// Creates a new DE sampler with default settings and a random seed.
|
/// Creates a new DE sampler with default settings and a random seed.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: Mutex::new(DeState::new(None, 0.8, 0.9, DeStrategy::Rand1, None)),
|
state: Mutex::new(State::new(
|
||||||
|
None,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
DifferentialEvolutionStrategy::Rand1,
|
||||||
|
None,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,24 +104,30 @@ impl DeSampler {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_seed(seed: u64) -> Self {
|
pub fn with_seed(seed: u64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: Mutex::new(DeState::new(None, 0.8, 0.9, DeStrategy::Rand1, Some(seed))),
|
state: Mutex::new(State::new(
|
||||||
|
None,
|
||||||
|
0.8,
|
||||||
|
0.9,
|
||||||
|
DifferentialEvolutionStrategy::Rand1,
|
||||||
|
Some(seed),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a builder for configuring a `DeSampler`.
|
/// Creates a builder for configuring a `DifferentialEvolutionSampler`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn builder() -> DeSamplerBuilder {
|
pub fn builder() -> DifferentialEvolutionSamplerBuilder {
|
||||||
DeSamplerBuilder::new()
|
DifferentialEvolutionSamplerBuilder::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DeSampler {
|
impl Default for DifferentialEvolutionSampler {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for configuring a [`DeSampler`].
|
/// Builder for configuring a [`DifferentialEvolutionSampler`].
|
||||||
///
|
///
|
||||||
/// All options have sensible defaults:
|
/// All options have sensible defaults:
|
||||||
/// - `population_size`: `max(10 * n_dims, 15)` (auto-computed from parameter count)
|
/// - `population_size`: `max(10 * n_dims, 15)` (auto-computed from parameter count)
|
||||||
@@ -123,32 +139,34 @@ impl Default for DeSampler {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use optimizer::sampler::de::{DeSamplerBuilder, DeStrategy};
|
/// use optimizer::sampler::differential_evolution::{
|
||||||
|
/// DifferentialEvolutionSamplerBuilder, DifferentialEvolutionStrategy,
|
||||||
|
/// };
|
||||||
///
|
///
|
||||||
/// let sampler = DeSamplerBuilder::new()
|
/// let sampler = DifferentialEvolutionSamplerBuilder::new()
|
||||||
/// .mutation_factor(0.5)
|
/// .mutation_factor(0.5)
|
||||||
/// .crossover_rate(0.7)
|
/// .crossover_rate(0.7)
|
||||||
/// .strategy(DeStrategy::CurrentToBest1)
|
/// .strategy(DifferentialEvolutionStrategy::CurrentToBest1)
|
||||||
/// .population_size(20)
|
/// .population_size(20)
|
||||||
/// .seed(42)
|
/// .seed(42)
|
||||||
/// .build();
|
/// .build();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DeSamplerBuilder {
|
pub struct DifferentialEvolutionSamplerBuilder {
|
||||||
population_size: Option<usize>,
|
population_size: Option<usize>,
|
||||||
mutation_factor: f64,
|
mutation_factor: f64,
|
||||||
crossover_rate: f64,
|
crossover_rate: f64,
|
||||||
strategy: DeStrategy,
|
strategy: DifferentialEvolutionStrategy,
|
||||||
seed: Option<u64>,
|
seed: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DeSamplerBuilder {
|
impl Default for DifferentialEvolutionSamplerBuilder {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeSamplerBuilder {
|
impl DifferentialEvolutionSamplerBuilder {
|
||||||
/// Creates a new builder with default settings.
|
/// Creates a new builder with default settings.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -156,7 +174,7 @@ impl DeSamplerBuilder {
|
|||||||
population_size: None,
|
population_size: None,
|
||||||
mutation_factor: 0.8,
|
mutation_factor: 0.8,
|
||||||
crossover_rate: 0.9,
|
crossover_rate: 0.9,
|
||||||
strategy: DeStrategy::Rand1,
|
strategy: DifferentialEvolutionStrategy::Rand1,
|
||||||
seed: None,
|
seed: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,9 +219,9 @@ impl DeSamplerBuilder {
|
|||||||
|
|
||||||
/// Sets the mutation strategy.
|
/// Sets the mutation strategy.
|
||||||
///
|
///
|
||||||
/// Default: [`DeStrategy::Rand1`].
|
/// Default: [`DifferentialEvolutionStrategy::Rand1`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn strategy(mut self, strategy: DeStrategy) -> Self {
|
pub fn strategy(mut self, strategy: DifferentialEvolutionStrategy) -> Self {
|
||||||
self.strategy = strategy;
|
self.strategy = strategy;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -215,11 +233,11 @@ impl DeSamplerBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the configured [`DeSampler`].
|
/// Builds the configured [`DifferentialEvolutionSampler`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build(self) -> DeSampler {
|
pub fn build(self) -> DifferentialEvolutionSampler {
|
||||||
DeSampler {
|
DifferentialEvolutionSampler {
|
||||||
state: Mutex::new(DeState::new(
|
state: Mutex::new(State::new(
|
||||||
self.population_size,
|
self.population_size,
|
||||||
self.mutation_factor,
|
self.mutation_factor,
|
||||||
self.crossover_rate,
|
self.crossover_rate,
|
||||||
@@ -248,7 +266,7 @@ struct DimensionInfo {
|
|||||||
|
|
||||||
/// A candidate solution produced by mutation + crossover.
|
/// A candidate solution produced by mutation + crossover.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct DeCandidate {
|
struct Candidate {
|
||||||
/// Internal-space vector (only continuous dimensions).
|
/// Internal-space vector (only continuous dimensions).
|
||||||
x: Vec<f64>,
|
x: Vec<f64>,
|
||||||
/// Values for categorical dimensions (index in `dimensions` -> categorical index).
|
/// Values for categorical dimensions (index in `dimensions` -> categorical index).
|
||||||
@@ -267,7 +285,7 @@ struct TrialProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Phase of the DE state machine.
|
/// Phase of the DE state machine.
|
||||||
enum DePhase {
|
enum Phase {
|
||||||
/// Discovering the search space structure (first trial).
|
/// Discovering the search space structure (first trial).
|
||||||
Discovery,
|
Discovery,
|
||||||
/// Active sampling and evolving.
|
/// Active sampling and evolving.
|
||||||
@@ -275,7 +293,7 @@ enum DePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Top-level mutable state behind the `Mutex`.
|
/// Top-level mutable state behind the `Mutex`.
|
||||||
struct DeState {
|
struct State {
|
||||||
/// The RNG used for sampling.
|
/// The RNG used for sampling.
|
||||||
rng: fastrand::Rng,
|
rng: fastrand::Rng,
|
||||||
/// User-provided population size (None = auto).
|
/// User-provided population size (None = auto).
|
||||||
@@ -285,9 +303,9 @@ struct DeState {
|
|||||||
/// Crossover rate (CR).
|
/// Crossover rate (CR).
|
||||||
crossover_rate: f64,
|
crossover_rate: f64,
|
||||||
/// Mutation strategy.
|
/// Mutation strategy.
|
||||||
strategy: DeStrategy,
|
strategy: DifferentialEvolutionStrategy,
|
||||||
/// Current phase.
|
/// Current phase.
|
||||||
phase: DePhase,
|
phase: Phase,
|
||||||
/// Discovered dimension info (populated during discovery).
|
/// Discovered dimension info (populated during discovery).
|
||||||
dimensions: Vec<DimensionInfo>,
|
dimensions: Vec<DimensionInfo>,
|
||||||
/// Last `trial_id` seen during discovery.
|
/// Last `trial_id` seen during discovery.
|
||||||
@@ -309,7 +327,7 @@ struct DeState {
|
|||||||
|
|
||||||
// --- Current generation ---
|
// --- Current generation ---
|
||||||
/// Current generation's candidates.
|
/// Current generation's candidates.
|
||||||
candidates: Vec<DeCandidate>,
|
candidates: Vec<Candidate>,
|
||||||
/// Mapping from `trial_id` to its progress.
|
/// Mapping from `trial_id` to its progress.
|
||||||
trial_progress: HashMap<u64, TrialProgress>,
|
trial_progress: HashMap<u64, TrialProgress>,
|
||||||
/// Number of candidates assigned so far in the current generation.
|
/// Number of candidates assigned so far in the current generation.
|
||||||
@@ -318,12 +336,12 @@ struct DeState {
|
|||||||
generation_trial_ids: Vec<u64>,
|
generation_trial_ids: Vec<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DeState {
|
impl State {
|
||||||
fn new(
|
fn new(
|
||||||
user_population_size: Option<usize>,
|
user_population_size: Option<usize>,
|
||||||
mutation_factor: f64,
|
mutation_factor: f64,
|
||||||
crossover_rate: f64,
|
crossover_rate: f64,
|
||||||
strategy: DeStrategy,
|
strategy: DifferentialEvolutionStrategy,
|
||||||
seed: Option<u64>,
|
seed: Option<u64>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let rng = seed.map_or_else(fastrand::Rng::new, fastrand::Rng::with_seed);
|
let rng = seed.map_or_else(fastrand::Rng::new, fastrand::Rng::with_seed);
|
||||||
@@ -333,7 +351,7 @@ impl DeState {
|
|||||||
mutation_factor,
|
mutation_factor,
|
||||||
crossover_rate,
|
crossover_rate,
|
||||||
strategy,
|
strategy,
|
||||||
phase: DePhase::Discovery,
|
phase: Phase::Discovery,
|
||||||
dimensions: Vec::new(),
|
dimensions: Vec::new(),
|
||||||
discovery_trial_id: None,
|
discovery_trial_id: None,
|
||||||
population: Vec::new(),
|
population: Vec::new(),
|
||||||
@@ -457,28 +475,6 @@ fn clamp_to_bounds(value: f64, bounds: Option<(f64, f64)>) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a `ParamValue` to its internal-space representation.
|
|
||||||
#[allow(dead_code, clippy::cast_precision_loss)]
|
|
||||||
fn to_internal(value: &ParamValue, distribution: &Distribution) -> f64 {
|
|
||||||
match (value, distribution) {
|
|
||||||
(ParamValue::Float(v), Distribution::Float(d)) => {
|
|
||||||
if d.log_scale {
|
|
||||||
v.ln()
|
|
||||||
} else {
|
|
||||||
*v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(ParamValue::Int(v), Distribution::Int(d)) => {
|
|
||||||
if d.log_scale {
|
|
||||||
(*v as f64).ln()
|
|
||||||
} else {
|
|
||||||
*v as f64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!("to_internal: mismatched value and distribution"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// DE algorithm
|
// DE algorithm
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -500,62 +496,8 @@ fn select_random_indices(
|
|||||||
selected
|
selected
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a mutant vector using the specified DE strategy.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn create_mutant(state: &DeState, target_idx: usize, n_continuous: usize) -> Vec<f64> {
|
|
||||||
// We need to work with a mutable reference to state for the rng,
|
|
||||||
// but this function is called from a context where state is already mutable.
|
|
||||||
// So we'll take the necessary data and return the result.
|
|
||||||
let pop = &state.population;
|
|
||||||
let best = &state.population[state.best_idx];
|
|
||||||
|
|
||||||
match state.strategy {
|
|
||||||
DeStrategy::Rand1 => {
|
|
||||||
let indices = select_random_indices(
|
|
||||||
&mut state.rng.clone(),
|
|
||||||
state.population_size,
|
|
||||||
3,
|
|
||||||
&[target_idx],
|
|
||||||
);
|
|
||||||
let (r1, r2, r3) = (indices[0], indices[1], indices[2]);
|
|
||||||
(0..n_continuous)
|
|
||||||
.map(|j| pop[r1][j] + state.mutation_factor * (pop[r2][j] - pop[r3][j]))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
DeStrategy::Best1 => {
|
|
||||||
let indices = select_random_indices(
|
|
||||||
&mut state.rng.clone(),
|
|
||||||
state.population_size,
|
|
||||||
2,
|
|
||||||
&[target_idx],
|
|
||||||
);
|
|
||||||
let (r1, r2) = (indices[0], indices[1]);
|
|
||||||
(0..n_continuous)
|
|
||||||
.map(|j| best[j] + state.mutation_factor * (pop[r1][j] - pop[r2][j]))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
DeStrategy::CurrentToBest1 => {
|
|
||||||
let indices = select_random_indices(
|
|
||||||
&mut state.rng.clone(),
|
|
||||||
state.population_size,
|
|
||||||
2,
|
|
||||||
&[target_idx],
|
|
||||||
);
|
|
||||||
let (r1, r2) = (indices[0], indices[1]);
|
|
||||||
let target = &pop[target_idx];
|
|
||||||
(0..n_continuous)
|
|
||||||
.map(|j| {
|
|
||||||
target[j]
|
|
||||||
+ state.mutation_factor * (best[j] - target[j])
|
|
||||||
+ state.mutation_factor * (pop[r1][j] - pop[r2][j])
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate trial vectors (mutation + crossover) for the current population.
|
/// Generate trial vectors (mutation + crossover) for the current population.
|
||||||
fn generate_trial_vectors(state: &mut DeState) -> Vec<DeCandidate> {
|
fn generate_trial_vectors(state: &mut State) -> Vec<Candidate> {
|
||||||
let n_continuous = state.dimensions.iter().filter(|d| d.is_continuous).count();
|
let n_continuous = state.dimensions.iter().filter(|d| d.is_continuous).count();
|
||||||
let pop_size = state.population_size;
|
let pop_size = state.population_size;
|
||||||
|
|
||||||
@@ -595,7 +537,7 @@ fn generate_trial_vectors(state: &mut DeState) -> Vec<DeCandidate> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates.push(DeCandidate {
|
candidates.push(Candidate {
|
||||||
x: trial_x,
|
x: trial_x,
|
||||||
categorical_values,
|
categorical_values,
|
||||||
target_idx: i,
|
target_idx: i,
|
||||||
@@ -606,7 +548,7 @@ fn generate_trial_vectors(state: &mut DeState) -> Vec<DeCandidate> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a mutant vector, consuming RNG from state.
|
/// Create a mutant vector, consuming RNG from state.
|
||||||
fn create_mutant_with_rng(state: &mut DeState, target_idx: usize, n_continuous: usize) -> Vec<f64> {
|
fn create_mutant_with_rng(state: &mut State, target_idx: usize, n_continuous: usize) -> Vec<f64> {
|
||||||
if n_continuous == 0 {
|
if n_continuous == 0 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
@@ -617,21 +559,21 @@ fn create_mutant_with_rng(state: &mut DeState, target_idx: usize, n_continuous:
|
|||||||
let pop_size = state.population_size;
|
let pop_size = state.population_size;
|
||||||
|
|
||||||
match state.strategy {
|
match state.strategy {
|
||||||
DeStrategy::Rand1 => {
|
DifferentialEvolutionStrategy::Rand1 => {
|
||||||
let indices = select_random_indices(&mut state.rng, pop_size, 3, &[target_idx]);
|
let indices = select_random_indices(&mut state.rng, pop_size, 3, &[target_idx]);
|
||||||
let (r1, r2, r3) = (indices[0], indices[1], indices[2]);
|
let (r1, r2, r3) = (indices[0], indices[1], indices[2]);
|
||||||
(0..n_continuous)
|
(0..n_continuous)
|
||||||
.map(|j| pop[r1][j] + f * (pop[r2][j] - pop[r3][j]))
|
.map(|j| pop[r1][j] + f * (pop[r2][j] - pop[r3][j]))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
DeStrategy::Best1 => {
|
DifferentialEvolutionStrategy::Best1 => {
|
||||||
let indices = select_random_indices(&mut state.rng, pop_size, 2, &[target_idx]);
|
let indices = select_random_indices(&mut state.rng, pop_size, 2, &[target_idx]);
|
||||||
let (r1, r2) = (indices[0], indices[1]);
|
let (r1, r2) = (indices[0], indices[1]);
|
||||||
(0..n_continuous)
|
(0..n_continuous)
|
||||||
.map(|j| pop[best_idx][j] + f * (pop[r1][j] - pop[r2][j]))
|
.map(|j| pop[best_idx][j] + f * (pop[r1][j] - pop[r2][j]))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
DeStrategy::CurrentToBest1 => {
|
DifferentialEvolutionStrategy::CurrentToBest1 => {
|
||||||
let indices = select_random_indices(&mut state.rng, pop_size, 2, &[target_idx]);
|
let indices = select_random_indices(&mut state.rng, pop_size, 2, &[target_idx]);
|
||||||
let (r1, r2) = (indices[0], indices[1]);
|
let (r1, r2) = (indices[0], indices[1]);
|
||||||
(0..n_continuous)
|
(0..n_continuous)
|
||||||
@@ -663,7 +605,7 @@ fn continuous_dim_bounds(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the initial random population.
|
/// Generate the initial random population.
|
||||||
fn generate_initial_population(state: &mut DeState) -> Vec<DeCandidate> {
|
fn generate_initial_population(state: &mut State) -> Vec<Candidate> {
|
||||||
let n_continuous = state.dimensions.iter().filter(|d| d.is_continuous).count();
|
let n_continuous = state.dimensions.iter().filter(|d| d.is_continuous).count();
|
||||||
|
|
||||||
let mut candidates = Vec::with_capacity(state.population_size);
|
let mut candidates = Vec::with_capacity(state.population_size);
|
||||||
@@ -671,7 +613,6 @@ fn generate_initial_population(state: &mut DeState) -> Vec<DeCandidate> {
|
|||||||
for i in 0..state.population_size {
|
for i in 0..state.population_size {
|
||||||
let x: Vec<f64> = if n_continuous > 0 {
|
let x: Vec<f64> = if n_continuous > 0 {
|
||||||
let mut v = Vec::with_capacity(n_continuous);
|
let mut v = Vec::with_capacity(n_continuous);
|
||||||
let mut ci = 0;
|
|
||||||
for dim in &state.dimensions {
|
for dim in &state.dimensions {
|
||||||
if dim.is_continuous {
|
if dim.is_continuous {
|
||||||
let val = if let Some(bounds) = dim.bounds {
|
let val = if let Some(bounds) = dim.bounds {
|
||||||
@@ -680,10 +621,8 @@ fn generate_initial_population(state: &mut DeState) -> Vec<DeCandidate> {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
v.push(val);
|
v.push(val);
|
||||||
ci += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = ci;
|
|
||||||
v
|
v
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
@@ -698,7 +637,7 @@ fn generate_initial_population(state: &mut DeState) -> Vec<DeCandidate> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates.push(DeCandidate {
|
candidates.push(Candidate {
|
||||||
x,
|
x,
|
||||||
categorical_values,
|
categorical_values,
|
||||||
target_idx: i,
|
target_idx: i,
|
||||||
@@ -712,7 +651,7 @@ fn generate_initial_population(state: &mut DeState) -> Vec<DeCandidate> {
|
|||||||
// Sampler trait implementation
|
// Sampler trait implementation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
impl Sampler for DeSampler {
|
impl Sampler for DifferentialEvolutionSampler {
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
fn sample(
|
fn sample(
|
||||||
&self,
|
&self,
|
||||||
@@ -723,14 +662,14 @@ impl Sampler for DeSampler {
|
|||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
|
|
||||||
match &state.phase {
|
match &state.phase {
|
||||||
DePhase::Discovery => sample_discovery(&mut state, distribution, trial_id),
|
Phase::Discovery => sample_discovery(&mut state, distribution, trial_id),
|
||||||
DePhase::Active => sample_active(&mut state, distribution, trial_id, history),
|
Phase::Active => sample_active(&mut state, distribution, trial_id, history),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle sampling during the discovery phase.
|
/// Handle sampling during the discovery phase.
|
||||||
fn sample_discovery(state: &mut DeState, distribution: &Distribution, trial_id: u64) -> ParamValue {
|
fn sample_discovery(state: &mut State, distribution: &Distribution, trial_id: u64) -> ParamValue {
|
||||||
// Check if this is a new trial (discovery phase ended for previous trial)
|
// Check if this is a new trial (discovery phase ended for previous trial)
|
||||||
if let Some(prev_id) = state.discovery_trial_id
|
if let Some(prev_id) = state.discovery_trial_id
|
||||||
&& trial_id != prev_id
|
&& trial_id != prev_id
|
||||||
@@ -758,7 +697,7 @@ fn sample_discovery(state: &mut DeState, distribution: &Distribution, trial_id:
|
|||||||
|
|
||||||
/// Finalize discovery and transition to the active phase.
|
/// Finalize discovery and transition to the active phase.
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
fn finalize_discovery(state: &mut DeState) {
|
fn finalize_discovery(state: &mut State) {
|
||||||
let n_continuous = state.dimensions.iter().filter(|d| d.is_continuous).count();
|
let n_continuous = state.dimensions.iter().filter(|d| d.is_continuous).count();
|
||||||
|
|
||||||
// Resolve population size
|
// Resolve population size
|
||||||
@@ -774,12 +713,12 @@ fn finalize_discovery(state: &mut DeState) {
|
|||||||
state.assigned_count = 0;
|
state.assigned_count = 0;
|
||||||
state.generation_trial_ids.clear();
|
state.generation_trial_ids.clear();
|
||||||
state.trial_progress.clear();
|
state.trial_progress.clear();
|
||||||
state.phase = DePhase::Active;
|
state.phase = Phase::Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle sampling during the active phase.
|
/// Handle sampling during the active phase.
|
||||||
fn sample_active(
|
fn sample_active(
|
||||||
state: &mut DeState,
|
state: &mut State,
|
||||||
distribution: &Distribution,
|
distribution: &Distribution,
|
||||||
trial_id: u64,
|
trial_id: u64,
|
||||||
history: &[CompletedTrial],
|
history: &[CompletedTrial],
|
||||||
@@ -826,7 +765,7 @@ fn sample_active(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Assign a candidate to a trial.
|
/// Assign a candidate to a trial.
|
||||||
fn assign_candidate(state: &mut DeState, trial_id: u64) {
|
fn assign_candidate(state: &mut State, trial_id: u64) {
|
||||||
let candidate_idx = if state.assigned_count < state.candidates.len() {
|
let candidate_idx = if state.assigned_count < state.candidates.len() {
|
||||||
let idx = state.assigned_count;
|
let idx = state.assigned_count;
|
||||||
state.assigned_count += 1;
|
state.assigned_count += 1;
|
||||||
@@ -852,7 +791,7 @@ fn assign_candidate(state: &mut DeState, trial_id: u64) {
|
|||||||
categorical_values.insert(dim_idx, state.rng.usize(0..cat.n_choices));
|
categorical_values.insert(dim_idx, state.rng.usize(0..cat.n_choices));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.candidates.push(DeCandidate {
|
state.candidates.push(Candidate {
|
||||||
x,
|
x,
|
||||||
categorical_values,
|
categorical_values,
|
||||||
target_idx: 0, // overflow candidates don't compete
|
target_idx: 0, // overflow candidates don't compete
|
||||||
@@ -873,7 +812,7 @@ fn assign_candidate(state: &mut DeState, trial_id: u64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if we should process completed trials and start a new generation.
|
/// Check if we should process completed trials and start a new generation.
|
||||||
fn maybe_update_generation(state: &mut DeState, history: &[CompletedTrial]) {
|
fn maybe_update_generation(state: &mut State, history: &[CompletedTrial]) {
|
||||||
let pop_size = state.population_size;
|
let pop_size = state.population_size;
|
||||||
|
|
||||||
// Only update when at least pop_size candidates have been assigned
|
// Only update when at least pop_size candidates have been assigned
|
||||||
@@ -918,7 +857,7 @@ fn maybe_update_generation(state: &mut DeState, history: &[CompletedTrial]) {
|
|||||||
|
|
||||||
/// Initialize the population from the first generation's results.
|
/// Initialize the population from the first generation's results.
|
||||||
fn initialize_population(
|
fn initialize_population(
|
||||||
state: &mut DeState,
|
state: &mut State,
|
||||||
trial_ids: &[u64],
|
trial_ids: &[u64],
|
||||||
history_map: &HashMap<u64, f64>,
|
history_map: &HashMap<u64, f64>,
|
||||||
_n_continuous: usize,
|
_n_continuous: usize,
|
||||||
@@ -952,7 +891,7 @@ fn initialize_population(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Perform DE selection: replace parent if trial vector is better.
|
/// Perform DE selection: replace parent if trial vector is better.
|
||||||
fn perform_selection(state: &mut DeState, trial_ids: &[u64], history_map: &HashMap<u64, f64>) {
|
fn perform_selection(state: &mut State, trial_ids: &[u64], history_map: &HashMap<u64, f64>) {
|
||||||
for &trial_id in trial_ids {
|
for &trial_id in trial_ids {
|
||||||
let progress = &state.trial_progress[&trial_id];
|
let progress = &state.trial_progress[&trial_id];
|
||||||
let candidate = &state.candidates[progress.candidate_idx];
|
let candidate = &state.candidates[progress.candidate_idx];
|
||||||
@@ -987,7 +926,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_de_sampler_basic_float() {
|
fn test_de_sampler_basic_float() {
|
||||||
let sampler = DeSampler::with_seed(42);
|
let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||||
let dist = Distribution::Float(FloatDistribution {
|
let dist = Distribution::Float(FloatDistribution {
|
||||||
low: -5.0,
|
low: -5.0,
|
||||||
high: 5.0,
|
high: 5.0,
|
||||||
@@ -1019,7 +958,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let sample_values = |seed: u64| {
|
let sample_values = |seed: u64| {
|
||||||
let sampler = DeSampler::with_seed(seed);
|
let sampler = DifferentialEvolutionSampler::with_seed(seed);
|
||||||
(0..20)
|
(0..20)
|
||||||
.map(|i| sampler.sample(&dist, i, &[]))
|
.map(|i| sampler.sample(&dist, i, &[]))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -1035,16 +974,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_de_strategy_default() {
|
fn test_de_strategy_default() {
|
||||||
assert!(matches!(DeStrategy::default(), DeStrategy::Rand1));
|
assert!(matches!(
|
||||||
|
DifferentialEvolutionStrategy::default(),
|
||||||
|
DifferentialEvolutionStrategy::Rand1
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_builder_defaults() {
|
fn test_builder_defaults() {
|
||||||
let builder = DeSamplerBuilder::new();
|
let builder = DifferentialEvolutionSamplerBuilder::new();
|
||||||
assert!(builder.population_size.is_none());
|
assert!(builder.population_size.is_none());
|
||||||
assert!((builder.mutation_factor - 0.8).abs() < f64::EPSILON);
|
assert!((builder.mutation_factor - 0.8).abs() < f64::EPSILON);
|
||||||
assert!((builder.crossover_rate - 0.9).abs() < f64::EPSILON);
|
assert!((builder.crossover_rate - 0.9).abs() < f64::EPSILON);
|
||||||
assert!(matches!(builder.strategy, DeStrategy::Rand1));
|
assert!(matches!(
|
||||||
|
builder.strategy,
|
||||||
|
DifferentialEvolutionStrategy::Rand1
|
||||||
|
));
|
||||||
assert!(builder.seed.is_none());
|
assert!(builder.seed.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
pub mod bohb;
|
pub mod bohb;
|
||||||
#[cfg(feature = "cma-es")]
|
#[cfg(feature = "cma-es")]
|
||||||
pub mod cma_es;
|
pub mod cma_es;
|
||||||
pub mod de;
|
pub mod differential_evolution;
|
||||||
#[cfg(feature = "gp")]
|
#[cfg(feature = "gp")]
|
||||||
pub mod gp;
|
pub mod gp;
|
||||||
pub mod grid;
|
pub mod grid;
|
||||||
|
|||||||
@@ -0,0 +1,350 @@
|
|||||||
|
use optimizer::prelude::*;
|
||||||
|
use optimizer::sampler::differential_evolution::{
|
||||||
|
DifferentialEvolutionSampler, DifferentialEvolutionStrategy,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sphere_function() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(200, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
Ok::<_, Error>(xv * xv + yv * yv)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(
|
||||||
|
best.value < 1.0,
|
||||||
|
"sphere best value should be < 1.0, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rosenbrock_function() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::builder()
|
||||||
|
.population_size(20)
|
||||||
|
.seed(42)
|
||||||
|
.build();
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(400, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
let val = (1.0 - xv).powi(2) + 100.0 * (yv - xv * xv).powi(2);
|
||||||
|
Ok::<_, Error>(val)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(
|
||||||
|
best.value < 50.0,
|
||||||
|
"rosenbrock best value should be < 50.0, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rastrigin_function() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::builder()
|
||||||
|
.population_size(30)
|
||||||
|
.mutation_factor(0.7)
|
||||||
|
.crossover_rate(0.9)
|
||||||
|
.seed(42)
|
||||||
|
.build();
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-5.12, 5.12).name("x");
|
||||||
|
let y = FloatParam::new(-5.12, 5.12).name("y");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(500, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
let val = 20.0
|
||||||
|
+ (xv * xv - 10.0 * (2.0 * std::f64::consts::PI * xv).cos())
|
||||||
|
+ (yv * yv - 10.0 * (2.0 * std::f64::consts::PI * yv).cos());
|
||||||
|
Ok::<_, Error>(val)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
// Rastrigin is multimodal; just check reasonable convergence
|
||||||
|
assert!(
|
||||||
|
best.value < 10.0,
|
||||||
|
"rastrigin best value should be < 10.0, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounds_respected() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(123);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-2.0, 3.0).name("x");
|
||||||
|
let y = FloatParam::new(0.0, 10.0).name("y");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(100, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
Ok::<_, Error>(xv + yv)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for trial in study.trials() {
|
||||||
|
let xv: f64 = trial.get(&x).unwrap();
|
||||||
|
let yv: f64 = trial.get(&y).unwrap();
|
||||||
|
assert!((-2.0..=3.0).contains(&xv), "x = {xv} out of bounds [-2, 3]");
|
||||||
|
assert!((0.0..=10.0).contains(&yv), "y = {yv} out of bounds [0, 10]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strategy_best1() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::builder()
|
||||||
|
.strategy(DifferentialEvolutionStrategy::Best1)
|
||||||
|
.population_size(15)
|
||||||
|
.seed(42)
|
||||||
|
.build();
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(200, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
Ok::<_, Error>(xv * xv + yv * yv)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(
|
||||||
|
best.value < 5.0,
|
||||||
|
"Best1 strategy should converge, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strategy_current_to_best1() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::builder()
|
||||||
|
.strategy(DifferentialEvolutionStrategy::CurrentToBest1)
|
||||||
|
.population_size(15)
|
||||||
|
.seed(42)
|
||||||
|
.build();
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(200, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
Ok::<_, Error>(xv * xv + yv * yv)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(
|
||||||
|
best.value < 5.0,
|
||||||
|
"CurrentToBest1 strategy should converge, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_params_float_and_categorical() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let cat = CategoricalParam::new(vec!["a", "b", "c"]).name("cat");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(100, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let cv = cat.suggest(trial)?;
|
||||||
|
let penalty = match cv {
|
||||||
|
"a" => 0.0,
|
||||||
|
"b" => 1.0,
|
||||||
|
_ => 2.0,
|
||||||
|
};
|
||||||
|
Ok::<_, Error>(xv * xv + penalty)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(
|
||||||
|
best.value < 10.0,
|
||||||
|
"best value should be < 10.0, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seeded_reproducibility() {
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||||
|
|
||||||
|
let run = |seed: u64| {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(seed);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
study
|
||||||
|
.optimize(50, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
Ok::<_, Error>(xv * xv + yv * yv)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
study.trials().iter().map(|t| t.value).collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let results1 = run(42);
|
||||||
|
let results2 = run(42);
|
||||||
|
assert_eq!(results1, results2, "same seed should produce same results");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_seeds_different_results() {
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||||
|
|
||||||
|
let run = |seed: u64| {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(seed);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
study
|
||||||
|
.optimize(20, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
Ok::<_, Error>(xv * xv + yv * yv)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
study.trials().iter().map(|t| t.value).collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let results1 = run(42);
|
||||||
|
let results2 = run(99);
|
||||||
|
assert_ne!(
|
||||||
|
results1, results2,
|
||||||
|
"different seeds should produce different results"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_dimension() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-10.0, 10.0).name("x");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(100, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
Ok::<_, Error>((xv - 3.0).powi(2))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(
|
||||||
|
best.value < 1.0,
|
||||||
|
"1-D optimization should converge, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn integer_params() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let n = IntParam::new(1, 20).name("n");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(100, |trial| {
|
||||||
|
let nv = n.suggest(trial)?;
|
||||||
|
// Minimum at n = 10
|
||||||
|
Ok::<_, Error>(((nv - 10) * (nv - 10)) as f64)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
let best_n: i64 = best.get(&n).unwrap();
|
||||||
|
assert!(
|
||||||
|
(1..=20).contains(&best_n),
|
||||||
|
"integer value {best_n} out of bounds"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
best.value < 10.0,
|
||||||
|
"integer optimization should converge, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn log_scale_params() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::with_seed(42);
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let lr = FloatParam::new(1e-5, 1.0).log_scale().name("lr");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(100, |trial| {
|
||||||
|
let lrv = lr.suggest(trial)?;
|
||||||
|
// Minimum at lr = 0.01
|
||||||
|
Ok::<_, Error>((lrv.ln() - 0.01_f64.ln()).powi(2))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for trial in study.trials() {
|
||||||
|
let lrv: f64 = trial.get(&lr).unwrap();
|
||||||
|
assert!(
|
||||||
|
(1e-5..=1.0).contains(&lrv),
|
||||||
|
"log-scale value {lrv} out of bounds"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_mutation_and_crossover() {
|
||||||
|
let sampler = DifferentialEvolutionSampler::builder()
|
||||||
|
.mutation_factor(0.5)
|
||||||
|
.crossover_rate(0.7)
|
||||||
|
.population_size(10)
|
||||||
|
.seed(42)
|
||||||
|
.build();
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||||
|
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(100, |trial| {
|
||||||
|
let xv = x.suggest(trial)?;
|
||||||
|
let yv = y.suggest(trial)?;
|
||||||
|
Ok::<_, Error>(xv * xv + yv * yv)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(
|
||||||
|
best.value < 5.0,
|
||||||
|
"custom F/CR optimization should work, got {}",
|
||||||
|
best.value
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user