feat(algorithms): add OnePlusOneEs (1+1)-ES with Rechenberg's one-fifth rule
Rechenberg 1973's elemental evolution strategy: one parent, one child
each generation, accept the child if it is no worse, and adapt the
mutation step size by tracking the success rate. If more than 1/5 of
recent moves were accepted the search is too cautious — multiply σ by
`step_increase` (typical 1.22). Below 1/5 — divide by the same factor.
At 1/5 — leave it alone. The success window has length `adaptation_period`.
Single-objective only. Vec<f64> only. Generic Gaussian step bounded by
the embedded `RealBounds`.
Why ship it: it's the smallest possible self-adapting evolution strategy
and a useful pedagogical / baseline endpoint. Pairs well as the budget
floor ("give me anything cheaper than CMA-ES").
This commit is contained in:
@@ -15,6 +15,7 @@ pub mod moead;
|
|||||||
pub mod mopso;
|
pub mod mopso;
|
||||||
pub mod nsga2;
|
pub mod nsga2;
|
||||||
pub mod nsga3;
|
pub mod nsga3;
|
||||||
|
pub mod one_plus_one_es;
|
||||||
pub mod paes;
|
pub mod paes;
|
||||||
pub(crate) mod parallel_eval;
|
pub(crate) mod parallel_eval;
|
||||||
pub mod pesa2;
|
pub mod pesa2;
|
||||||
@@ -43,6 +44,7 @@ pub use moead::*;
|
|||||||
pub use mopso::*;
|
pub use mopso::*;
|
||||||
pub use nsga2::*;
|
pub use nsga2::*;
|
||||||
pub use nsga3::*;
|
pub use nsga3::*;
|
||||||
|
pub use one_plus_one_es::*;
|
||||||
pub use paes::*;
|
pub use paes::*;
|
||||||
pub use particle_swarm::*;
|
pub use particle_swarm::*;
|
||||||
pub use pesa2::*;
|
pub use pesa2::*;
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
//! `OnePlusOneEs` — the (1+1) evolution strategy with Rechenberg's
|
||||||
|
//! one-fifth success rule for σ adaptation.
|
||||||
|
|
||||||
|
use rand_distr::{Distribution, Normal};
|
||||||
|
|
||||||
|
use crate::core::candidate::Candidate;
|
||||||
|
use crate::core::evaluation::Evaluation;
|
||||||
|
use crate::core::objective::Direction;
|
||||||
|
use crate::core::population::Population;
|
||||||
|
use crate::core::problem::Problem;
|
||||||
|
use crate::core::result::OptimizationResult;
|
||||||
|
use crate::core::rng::rng_from_seed;
|
||||||
|
use crate::operators::real::RealBounds;
|
||||||
|
use crate::traits::Optimizer;
|
||||||
|
|
||||||
|
/// Configuration for [`OnePlusOneEs`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OnePlusOneEsConfig {
|
||||||
|
/// Number of mutation iterations.
|
||||||
|
pub iterations: usize,
|
||||||
|
/// Initial mutation step size (`σ_0`).
|
||||||
|
pub initial_sigma: f64,
|
||||||
|
/// Number of recent iterations the success-rate is computed over.
|
||||||
|
/// The classic value is 10·dim; 50 is a fine default for low-dim
|
||||||
|
/// problems.
|
||||||
|
pub adaptation_period: usize,
|
||||||
|
/// Step-size multiplier when the success rate exceeds 1/5. Reciprocal
|
||||||
|
/// is applied when the rate is below 1/5. Rechenberg's analytical
|
||||||
|
/// derivation gives ≈ `0.817^(-1/n)` for dim n; 1.22 is a popular
|
||||||
|
/// dimension-agnostic value.
|
||||||
|
pub step_increase: f64,
|
||||||
|
/// Seed for the deterministic RNG.
|
||||||
|
pub seed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OnePlusOneEsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
iterations: 5_000,
|
||||||
|
initial_sigma: 0.5,
|
||||||
|
adaptation_period: 50,
|
||||||
|
step_increase: 1.22,
|
||||||
|
seed: 42,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (1+1)-ES with the one-fifth rule: tiny, parameter-light continuous
|
||||||
|
/// optimizer. `Vec<f64>` decisions only; single-objective only.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OnePlusOneEs {
|
||||||
|
/// Algorithm configuration.
|
||||||
|
pub config: OnePlusOneEsConfig,
|
||||||
|
/// Per-variable bounds — used to seed the parent at the box midpoint
|
||||||
|
/// and clamp every mutated child.
|
||||||
|
pub bounds: RealBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OnePlusOneEs {
|
||||||
|
/// Construct a `OnePlusOneEs`.
|
||||||
|
pub fn new(config: OnePlusOneEsConfig, bounds: RealBounds) -> Self {
|
||||||
|
Self { config, bounds }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> Optimizer<P> for OnePlusOneEs
|
||||||
|
where
|
||||||
|
P: Problem<Decision = Vec<f64>> + Sync,
|
||||||
|
{
|
||||||
|
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
|
||||||
|
assert!(self.config.initial_sigma > 0.0, "OnePlusOneEs initial_sigma must be > 0");
|
||||||
|
assert!(
|
||||||
|
self.config.step_increase > 1.0,
|
||||||
|
"OnePlusOneEs step_increase must be > 1",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
self.config.adaptation_period >= 1,
|
||||||
|
"OnePlusOneEs adaptation_period must be >= 1",
|
||||||
|
);
|
||||||
|
let objectives = problem.objectives();
|
||||||
|
assert!(
|
||||||
|
objectives.is_single_objective(),
|
||||||
|
"OnePlusOneEs requires exactly one objective",
|
||||||
|
);
|
||||||
|
let direction = objectives.objectives[0].direction;
|
||||||
|
let mut rng = rng_from_seed(self.config.seed);
|
||||||
|
|
||||||
|
// Seed parent at midpoint of bounds.
|
||||||
|
let mut parent: Vec<f64> = self
|
||||||
|
.bounds
|
||||||
|
.bounds
|
||||||
|
.iter()
|
||||||
|
.map(|&(lo, hi)| 0.5 * (lo + hi))
|
||||||
|
.collect();
|
||||||
|
let mut parent_eval = problem.evaluate(&parent);
|
||||||
|
let mut evaluations = 1usize;
|
||||||
|
|
||||||
|
let mut sigma = self.config.initial_sigma;
|
||||||
|
let mut window = std::collections::VecDeque::with_capacity(self.config.adaptation_period);
|
||||||
|
let n = parent.len();
|
||||||
|
|
||||||
|
for _ in 0..self.config.iterations {
|
||||||
|
let normal = Normal::new(0.0, sigma).expect("Normal::new(0, sigma)");
|
||||||
|
let mut child = parent.clone();
|
||||||
|
for j in 0..n {
|
||||||
|
let (lo, hi) = self.bounds.bounds[j];
|
||||||
|
child[j] = (child[j] + normal.sample(&mut rng)).clamp(lo, hi);
|
||||||
|
}
|
||||||
|
let child_eval = problem.evaluate(&child);
|
||||||
|
evaluations += 1;
|
||||||
|
|
||||||
|
// Accept if not strictly worse (so neutral moves are kept and
|
||||||
|
// can drive σ up when on a plateau).
|
||||||
|
let accepted = !worse_than(&child_eval, &parent_eval, direction);
|
||||||
|
if accepted {
|
||||||
|
parent = child;
|
||||||
|
parent_eval = child_eval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update success window.
|
||||||
|
window.push_back(if accepted { 1u8 } else { 0u8 });
|
||||||
|
if window.len() > self.config.adaptation_period {
|
||||||
|
window.pop_front();
|
||||||
|
}
|
||||||
|
// Apply one-fifth rule once we have a full window.
|
||||||
|
if window.len() == self.config.adaptation_period {
|
||||||
|
let success_count: usize =
|
||||||
|
window.iter().map(|&b| b as usize).sum();
|
||||||
|
let rate = success_count as f64 / window.len() as f64;
|
||||||
|
if rate > 0.2 {
|
||||||
|
sigma *= self.config.step_increase;
|
||||||
|
} else if rate < 0.2 {
|
||||||
|
sigma /= self.config.step_increase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let best = Candidate::new(parent, parent_eval);
|
||||||
|
let population = Population::new(vec![best.clone()]);
|
||||||
|
let front = vec![best.clone()];
|
||||||
|
OptimizationResult::new(
|
||||||
|
population,
|
||||||
|
front,
|
||||||
|
Some(best),
|
||||||
|
evaluations,
|
||||||
|
self.config.iterations,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worse_than(a: &Evaluation, b: &Evaluation, direction: Direction) -> bool {
|
||||||
|
match (a.is_feasible(), b.is_feasible()) {
|
||||||
|
(false, true) => true,
|
||||||
|
(true, false) => false,
|
||||||
|
(false, false) => a.constraint_violation > b.constraint_violation,
|
||||||
|
(true, true) => match direction {
|
||||||
|
Direction::Minimize => a.objectives[0] > b.objectives[0],
|
||||||
|
Direction::Maximize => a.objectives[0] < b.objectives[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::tests_support::{SchafferN1, Sphere1D};
|
||||||
|
|
||||||
|
fn make_optimizer(seed: u64) -> OnePlusOneEs {
|
||||||
|
OnePlusOneEs::new(
|
||||||
|
OnePlusOneEsConfig {
|
||||||
|
iterations: 2_000,
|
||||||
|
initial_sigma: 1.0,
|
||||||
|
adaptation_period: 30,
|
||||||
|
step_increase: 1.22,
|
||||||
|
seed,
|
||||||
|
},
|
||||||
|
RealBounds::new(vec![(-5.0, 5.0)]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finds_minimum_of_sphere() {
|
||||||
|
let mut opt = make_optimizer(1);
|
||||||
|
let r = opt.run(&Sphere1D);
|
||||||
|
let best = r.best.unwrap();
|
||||||
|
assert!(
|
||||||
|
best.evaluation.objectives[0] < 1e-6,
|
||||||
|
"got f = {}",
|
||||||
|
best.evaluation.objectives[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deterministic_with_same_seed() {
|
||||||
|
let mut a = make_optimizer(99);
|
||||||
|
let mut b = make_optimizer(99);
|
||||||
|
let ra = a.run(&Sphere1D);
|
||||||
|
let rb = b.run(&Sphere1D);
|
||||||
|
assert_eq!(
|
||||||
|
ra.best.unwrap().evaluation.objectives,
|
||||||
|
rb.best.unwrap().evaluation.objectives,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "exactly one objective")]
|
||||||
|
fn multi_objective_panics() {
|
||||||
|
let mut opt = make_optimizer(0);
|
||||||
|
let _ = opt.run(&SchafferN1);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -26,7 +26,7 @@ pub use crate::algorithms::{
|
|||||||
DifferentialEvolutionConfig, EpsilonMoea, EpsilonMoeaConfig,
|
DifferentialEvolutionConfig, EpsilonMoea, EpsilonMoeaConfig,
|
||||||
GeneticAlgorithm, GeneticAlgorithmConfig, Grea, GreaConfig, HillClimber, HillClimberConfig, Hype,
|
GeneticAlgorithm, GeneticAlgorithmConfig, Grea, GreaConfig, HillClimber, HillClimberConfig, Hype,
|
||||||
HypeConfig, Ibea, IbeaConfig, Knea, KneaConfig, Moead, MoeadConfig, Mopso, MopsoConfig, Nsga2,
|
HypeConfig, Ibea, IbeaConfig, Knea, KneaConfig, Moead, MoeadConfig, Mopso, MopsoConfig, Nsga2,
|
||||||
Nsga2Config, Nsga3, Nsga3Config, Paes, PaesConfig, ParticleSwarm, PesaII, PesaIIConfig,
|
Nsga2Config, Nsga3, Nsga3Config, OnePlusOneEs, OnePlusOneEsConfig, Paes, PaesConfig, ParticleSwarm, PesaII, PesaIIConfig,
|
||||||
ParticleSwarmConfig, RandomSearch, RandomSearchConfig, Rvea, RveaConfig,
|
ParticleSwarmConfig, RandomSearch, RandomSearchConfig, Rvea, RveaConfig,
|
||||||
SimulatedAnnealing,
|
SimulatedAnnealing,
|
||||||
SimulatedAnnealingConfig, SmsEmoa, SmsEmoaConfig, Spea2, Spea2Config, TabuSearch,
|
SimulatedAnnealingConfig, SmsEmoa, SmsEmoaConfig, Spea2, Spea2Config, TabuSearch,
|
||||||
|
|||||||
Reference in New Issue
Block a user