docs(examples): add toy_nsga2, random_search, and custom_optimizer

The three runnable examples called out in spec §18.5 / §19. All open
with `use heuropt::prelude::*;` so they double as a check that the
prelude is sufficient on its own:

- toy_nsga2.rs: Schaffer N.1 solved with NSGA-II.
- random_search.rs: 2D sphere solved with RandomSearch.
- custom_optimizer.rs: a minimal hill-climber implementing
  `Optimizer<P>` directly, demonstrating spec §2.3.
This commit is contained in:
2026-05-04 19:26:54 -06:00
parent 5672e21c87
commit c58d0241f9
3 changed files with 167 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
//! Implement a custom optimizer by implementing `Optimizer<P>` 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<P> Optimizer<P> for HillClimber
where
P: Problem<Decision = Vec<f64>>,
{
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
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(&current);
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<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate(&self, x: &Vec<f64>) -> 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);
}
+36
View File
@@ -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<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate(&self, x: &Vec<f64>) -> 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]);
}
+42
View File
@@ -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<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
fn evaluate(&self, x: &Vec<f64>) -> 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]);
}
}