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]);
+ }
+}