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:
@@ -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(¤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<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);
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user