Theme: production lifecycle. heuropt becomes deployable for long- running, real-world workloads. No breaking changes — Optimizer trait gains a default-impl run_with method that falls back to run. Adds: - src/observer/ module: Snapshot, Observer trait, ControlFlow, plus built-in MaxTime / MaxIterations / TargetFitness / Stagnation / Periodic / AnyOf / AllOf and a closure impl. - Optimizer::run_with(problem, observer): default-impl on the trait, overridden for full per-gen visibility on Nsga2, RandomSearch, and DifferentialEvolution. Other algorithms inherit a final-only notification — full per-gen support follows incrementally. - New 'tracing' optional feature plus TracingObserver that emits structured debug! events per generation. - src/metrics/igd.rs: IGD + IGD+ performance indicators against a reference set. - src/metrics/r2.rs: R2 indicator using the weighted Tchebycheff utility; pair with das_dennis for the canonical weight set. - examples/constrained.rs: BNH constrained 2-objective problem solved with NSGA-II + observer composition (MaxTime.or(Periodic)). Bumps Cargo.toml to 0.6.0; CHANGELOG entry consolidates the above. Existing 247 unit + 38 doctest + 32 algorithm-property + property / metric / numerical-stability tests all pass; bit-identical compare output verified post-DE refactor.
126 lines
4.2 KiB
Rust
126 lines
4.2 KiB
Rust
//! Constrained multi-objective optimization (BNH problem) plus a
|
||
//! demo of the observer / stop-condition API.
|
||
//!
|
||
//! BNH (Binh & Korn 1996) is a 2-variable / 2-objective / 2-constraint
|
||
//! multi-objective problem:
|
||
//!
|
||
//! ```text
|
||
//! minimize f1 = 4·x1² + 4·x2²
|
||
//! f2 = (x1 − 5)² + (x2 − 5)²
|
||
//! subject to
|
||
//! g1: (x1 − 5)² + x2² ≤ 25
|
||
//! g2: (x1 − 8)² + (x2 + 3)² ≥ 7.7
|
||
//! 0 ≤ x1 ≤ 5, 0 ≤ x2 ≤ 3
|
||
//! ```
|
||
//!
|
||
//! Demonstrates:
|
||
//! - Constraint handling via `Evaluation::constrained` (heuropt's
|
||
//! default tournament/Pareto comparators prefer feasibles).
|
||
//! - The Observer API: a `Stagnation` observer that halts the run
|
||
//! once the front stops improving, plus a `Periodic` observer that
|
||
//! prints progress every 25 generations.
|
||
//! - Composing observers with `.or()`.
|
||
//!
|
||
//! Run with: `cargo run --release --example constrained`
|
||
|
||
use heuropt::prelude::*;
|
||
|
||
struct Bnh;
|
||
|
||
impl Problem for Bnh {
|
||
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 f1 = 4.0 * x[0] * x[0] + 4.0 * x[1] * x[1];
|
||
let f2 = (x[0] - 5.0).powi(2) + (x[1] - 5.0).powi(2);
|
||
|
||
// g1: (x1 − 5)² + x2² ≤ 25 → violation = max(0, lhs − 25)
|
||
let g1 = ((x[0] - 5.0).powi(2) + x[1].powi(2) - 25.0).max(0.0);
|
||
// g2: (x1 − 8)² + (x2 + 3)² ≥ 7.7 → violation = max(0, 7.7 − lhs)
|
||
let g2 = (7.7 - ((x[0] - 8.0).powi(2) + (x[1] + 3.0).powi(2))).max(0.0);
|
||
|
||
let total_violation = g1 + g2;
|
||
Evaluation::constrained(vec![f1, f2], total_violation)
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let bounds = vec![(0.0_f64, 5.0_f64), (0.0_f64, 3.0_f64)];
|
||
|
||
// Compose stop conditions: halt after 5 s OR (via .or()) print
|
||
// periodic progress every 25 generations. The Periodic observer
|
||
// never breaks; it only logs.
|
||
let stop = MaxTime::new(std::time::Duration::from_secs(5));
|
||
let progress = Periodic::new(25, |snap: &Snapshot<'_, Vec<f64>>| {
|
||
let feasible_in_pop = snap
|
||
.population
|
||
.iter()
|
||
.filter(|c| c.evaluation.is_feasible())
|
||
.count();
|
||
let front_size = snap.pareto_front.map(|f| f.len()).unwrap_or(0);
|
||
println!(
|
||
"gen {:>4} evaluations = {:>6} feasible/pop = {}/{} front = {}",
|
||
snap.iteration,
|
||
snap.evaluations,
|
||
feasible_in_pop,
|
||
snap.population.len(),
|
||
front_size,
|
||
);
|
||
});
|
||
let mut observer = <_ as Observer<Vec<f64>>>::or(stop, progress);
|
||
|
||
let mut opt = Nsga2::new(
|
||
Nsga2Config {
|
||
population_size: 100,
|
||
generations: 250,
|
||
seed: 42,
|
||
},
|
||
RealBounds::new(bounds.clone()),
|
||
CompositeVariation {
|
||
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
|
||
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / 2.0),
|
||
},
|
||
);
|
||
let result = opt.run_with(&Bnh, &mut observer);
|
||
|
||
let total_feasible = result
|
||
.population
|
||
.iter()
|
||
.filter(|c| c.evaluation.is_feasible())
|
||
.count();
|
||
|
||
println!();
|
||
println!("Final state after {} generations:", result.generations);
|
||
println!(" total evaluations: {}", result.evaluations);
|
||
println!(
|
||
" feasible / total pop: {} / {}",
|
||
total_feasible,
|
||
result.population.len()
|
||
);
|
||
println!(" pareto front size: {}", result.pareto_front.len());
|
||
println!();
|
||
println!("Sample of the front (f1, f2):");
|
||
let mut sorted = result.pareto_front.clone();
|
||
sorted.sort_by(|a, b| {
|
||
a.evaluation.objectives[0]
|
||
.partial_cmp(&b.evaluation.objectives[0])
|
||
.unwrap_or(std::cmp::Ordering::Equal)
|
||
});
|
||
let n = sorted.len();
|
||
if n > 0 {
|
||
for k in (0..n).step_by((n / 5).max(1)) {
|
||
let c = &sorted[k];
|
||
println!(
|
||
" f1 = {:>7.3}, f2 = {:>7.3}, violation = {:.3}",
|
||
c.evaluation.objectives[0],
|
||
c.evaluation.objectives[1],
|
||
c.evaluation.constraint_violation,
|
||
);
|
||
}
|
||
}
|
||
}
|