diff --git a/examples/custom_optimizer.rs b/examples/custom_optimizer.rs new file mode 100644 index 0000000..162fc20 --- /dev/null +++ b/examples/custom_optimizer.rs @@ -0,0 +1,89 @@ +//! Implement a custom optimizer by implementing `Optimizer

` directly. +//! +//! Demonstrates spec ยง2.3: a junior engineer can add a new algorithm by +//! implementing a single trait, with no framework gymnastics required. +//! +//! Run with: +//! +//! ```bash +//! cargo run --example custom_optimizer +//! ``` + +use heuropt::prelude::*; + +/// A trivial single-objective hill-climber: sample one point, then repeatedly +/// jitter it with `GaussianMutation` and keep the better feasible result. +struct HillClimber { + iterations: usize, + sigma: f64, + initializer: RealBounds, + seed: u64, +} + +impl

Optimizer

for HillClimber +where + P: Problem>, +{ + fn run(&mut self, problem: &P) -> OptimizationResult { + let objectives = problem.objectives(); + assert!(objectives.is_single_objective(), "HillClimber needs one objective"); + let mut rng = rng_from_seed(self.seed); + let mut variation = GaussianMutation { sigma: self.sigma }; + + let mut current = self.initializer.initialize(1, &mut rng).remove(0); + let mut current_eval = problem.evaluate(¤t); + let mut evaluations = 1; + + for _ in 0..self.iterations { + let children = variation.vary(&[current.clone()], &mut rng); + let candidate = children.into_iter().next().unwrap(); + let candidate_eval = problem.evaluate(&candidate); + evaluations += 1; + // Accept on direction-correct improvement (Sphere is minimize). + let accept = candidate_eval.objectives[0] < current_eval.objectives[0]; + if accept { + current = candidate; + current_eval = candidate_eval; + } + } + + let best = Candidate::new(current.clone(), current_eval.clone()); + let population = Population::new(vec![best.clone()]); + let pareto_front = vec![best.clone()]; + OptimizationResult::new( + population, + pareto_front, + Some(best), + evaluations, + self.iterations, + ) + } +} + +struct Sphere1D; + +impl Problem for Sphere1D { + type Decision = Vec; + + fn objectives(&self) -> ObjectiveSpace { + ObjectiveSpace::new(vec![Objective::minimize("f")]) + } + + fn evaluate(&self, x: &Vec) -> Evaluation { + Evaluation::new(vec![x[0] * x[0]]) + } +} + +fn main() { + let mut climber = HillClimber { + iterations: 500, + sigma: 0.5, + initializer: RealBounds::new(vec![(-5.0, 5.0)]), + seed: 11, + }; + + let result = climber.run(&Sphere1D); + let best = result.best.expect("single-objective always has a best"); + println!("Hill-climber best f = {:.6}", best.evaluation.objectives[0]); + println!("Total evaluations: {}", result.evaluations); +} diff --git a/examples/random_search.rs b/examples/random_search.rs new file mode 100644 index 0000000..1739e26 --- /dev/null +++ b/examples/random_search.rs @@ -0,0 +1,36 @@ +//! Run a 2D sphere problem under `RandomSearch`. +//! +//! Run with: +//! +//! ```bash +//! cargo run --example random_search +//! ``` + +use heuropt::prelude::*; + +struct Sphere2D; + +impl Problem for Sphere2D { + type Decision = Vec; + + fn objectives(&self) -> ObjectiveSpace { + ObjectiveSpace::new(vec![Objective::minimize("f")]) + } + + fn evaluate(&self, x: &Vec) -> Evaluation { + Evaluation::new(vec![x.iter().map(|v| v * v).sum()]) + } +} + +fn main() { + let initializer = RealBounds::new(vec![(-5.0, 5.0), (-5.0, 5.0)]); + let config = RandomSearchConfig { iterations: 500, batch_size: 1, seed: 7 }; + let mut optimizer = RandomSearch::new(config, initializer); + + let result = optimizer.run(&Sphere2D); + + let best = result.best.expect("single-objective always has a best"); + println!("Total evaluations: {}", result.evaluations); + println!("Best decision: {:?}", best.decision); + println!("Best f: {:.6}", best.evaluation.objectives[0]); +} diff --git a/examples/toy_nsga2.rs b/examples/toy_nsga2.rs new file mode 100644 index 0000000..795ff33 --- /dev/null +++ b/examples/toy_nsga2.rs @@ -0,0 +1,42 @@ +//! Solve the Schaffer N.1 two-objective problem with NSGA-II. +//! +//! Run with: +//! +//! ```bash +//! cargo run --example toy_nsga2 +//! ``` + +use heuropt::prelude::*; + +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)]) + } +} + +fn main() { + let initializer = RealBounds::new(vec![(-5.0, 5.0)]); + let variation = GaussianMutation { sigma: 0.2 }; + let config = Nsga2Config { population_size: 60, generations: 80, seed: 42 }; + let mut optimizer = Nsga2::new(config, initializer, variation); + + let result = optimizer.run(&SchafferN1); + + println!("Final population: {}", result.population.len()); + println!("Pareto front size: {}", result.pareto_front.len()); + println!("Total evaluations: {}", result.evaluations); + println!("First few front points (f1, f2):"); + for c in result.pareto_front.iter().take(8) { + let o = &c.evaluation.objectives; + println!(" ({:.4}, {:.4})", o[0], o[1]); + } +}