diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs new file mode 100644 index 0000000..373701c --- /dev/null +++ b/src/algorithms/mod.rs @@ -0,0 +1,5 @@ +//! Built-in reference optimizers. + +pub mod random_search; + +pub use random_search::*; diff --git a/src/algorithms/random_search.rs b/src/algorithms/random_search.rs new file mode 100644 index 0000000..0b07627 --- /dev/null +++ b/src/algorithms/random_search.rs @@ -0,0 +1,122 @@ +//! Baseline `RandomSearch` optimizer. +//! +//! This is the reference example for spec §2.4 / §12.1: read this file before +//! writing your own optimizer. + +use crate::core::candidate::Candidate; +use crate::core::population::Population; +use crate::core::problem::Problem; +use crate::core::result::OptimizationResult; +use crate::core::rng::rng_from_seed; +use crate::pareto::{best_candidate, pareto_front}; +use crate::traits::{Initializer, Optimizer}; + +/// Configuration for [`RandomSearch`]. +#[derive(Debug, Clone)] +pub struct RandomSearchConfig { + /// Number of iterations (equals `generations` in the result). + pub iterations: usize, + /// Decisions sampled per iteration. + pub batch_size: usize, + /// Seed for the deterministic RNG. + pub seed: u64, +} + +impl Default for RandomSearchConfig { + fn default() -> Self { + Self { iterations: 100, batch_size: 1, seed: 42 } + } +} + +/// Sample-evaluate-keep baseline optimizer. +/// +/// Each iteration the configured `Initializer` produces `batch_size` decisions +/// which are evaluated and pushed into the population. Cheap, parallelism-free, +/// and useful as a sanity-check baseline. +#[derive(Debug, Clone)] +pub struct RandomSearch { + /// Algorithm configuration. + pub config: RandomSearchConfig, + /// Decision-sampling strategy. + pub initializer: I, +} + +impl RandomSearch { + /// Construct a `RandomSearch` from its config and initializer. + pub fn new(config: RandomSearchConfig, initializer: I) -> Self { + Self { config, initializer } + } +} + +impl Optimizer

for RandomSearch +where + P: Problem, + I: Initializer, +{ + fn run(&mut self, problem: &P) -> OptimizationResult { + let objectives = problem.objectives(); + let mut rng = rng_from_seed(self.config.seed); + let mut all: Vec> = Vec::new(); + let mut evaluations = 0usize; + + for _ in 0..self.config.iterations { + let decisions = self.initializer.initialize(self.config.batch_size, &mut rng); + for decision in decisions { + let eval = problem.evaluate(&decision); + evaluations += 1; + all.push(Candidate::new(decision, eval)); + } + } + + let front = pareto_front(&all, &objectives); + let best = best_candidate(&all, &objectives); + OptimizationResult::new( + Population::new(all), + front, + best, + evaluations, + self.config.iterations, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::operators::RealBounds; + use crate::tests_support::{SchafferN1, Sphere1D}; + + #[test] + fn evaluation_count_matches_iterations_times_batch() { + let mut opt = RandomSearch::new( + RandomSearchConfig { iterations: 30, batch_size: 4, seed: 1 }, + RealBounds::new(vec![(-2.0, 2.0)]), + ); + let r = opt.run(&Sphere1D); + assert_eq!(r.evaluations, 30 * 4); + assert_eq!(r.population.len(), 30 * 4); + assert_eq!(r.generations, 30); + } + + #[test] + fn pareto_front_non_empty_for_multi_objective() { + let mut opt = RandomSearch::new( + RandomSearchConfig { iterations: 50, batch_size: 1, seed: 42 }, + RealBounds::new(vec![(-5.0, 5.0)]), + ); + let r = opt.run(&SchafferN1); + assert!(!r.pareto_front.is_empty()); + // Multi-objective ⇒ best is None. + assert!(r.best.is_none()); + } + + #[test] + fn single_objective_returns_best() { + let mut opt = RandomSearch::new( + RandomSearchConfig { iterations: 100, batch_size: 1, seed: 7 }, + RealBounds::new(vec![(-1.0, 1.0)]), + ); + let r = opt.run(&Sphere1D); + assert!(r.best.is_some()); + } +} diff --git a/src/lib.rs b/src/lib.rs index bc00fd2..ce754aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,13 @@ //! many-objective optimization. See `docs/heuropt_tech_design_spec.md` for the //! full design. +pub mod algorithms; pub mod core; pub mod operators; pub mod pareto; pub mod prelude; pub mod selection; pub mod traits; + +#[cfg(test)] +pub(crate) mod tests_support; diff --git a/src/prelude.rs b/src/prelude.rs index 1be8d1f..51ad9e1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -17,3 +17,5 @@ pub use crate::pareto::{ }; pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds, SwapMutation}; + +pub use crate::algorithms::{RandomSearch, RandomSearchConfig}; diff --git a/src/tests_support/mod.rs b/src/tests_support/mod.rs new file mode 100644 index 0000000..f44d8fb --- /dev/null +++ b/src/tests_support/mod.rs @@ -0,0 +1,40 @@ +//! Shared, deliberately tiny test problems used by algorithm unit tests. +//! +//! Not part of the public API. + +use crate::core::evaluation::Evaluation; +use crate::core::objective::{Objective, ObjectiveSpace}; +use crate::core::problem::Problem; + +/// 1-D minimization sphere `f(x) = x^2`. Single objective, always feasible. +pub struct Sphere1D; + +impl Problem for Sphere1D { + type Decision = Vec; + + fn objectives(&self) -> ObjectiveSpace { + ObjectiveSpace::new(vec![Objective::minimize("f")]) + } + + fn evaluate(&self, decision: &Vec) -> Evaluation { + Evaluation::new(vec![decision[0] * decision[0]]) + } +} + +/// Schaffer N.1 — the textbook two-objective minimization warm-up: +/// +/// `f1(x) = x^2`, `f2(x) = (x - 2)^2`. Always feasible. +pub struct SchafferN1; + +impl Problem for SchafferN1 { + type Decision = Vec; + + fn objectives(&self) -> ObjectiveSpace { + ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]) + } + + fn evaluate(&self, x: &Vec) -> Evaluation { + let v = x[0]; + Evaluation::new(vec![v * v, (v - 2.0).powi(2)]) + } +}