metalforge 0.3.0

forge: a deterministic metaheuristic optimization substrate in Rust. Unified Problem/MultiProblem/Anneal traits; DDS, SCE-UA, DE, L-SHADE, L-SRTDE, PSO, CMA-ES, NSGA-II/III, SMS-EMOA, simulated annealing, parallel tempering and GLUE uncertainty; reproducible by seed; optional Rayon parallelism.
Documentation
//! Contract tests: the crate-wide guarantees every optimizer must honor —
//! exact evaluation budgets, graceful handling of non-finite objectives, and
//! input validation. These are the invariants the 2026-06 audit found gaps in;
//! each test here pins one of them.

use forge_core::problem::func;
use forge_core::testfn::Sphere;
use forge_core::{
    Algorithm, CmaEs, Dds, De, LShade, LSrtde, Moead, NsgaII, NsgaIII, Optimizer, Padds, Pso,
    RestartCmaEs, Sce, SmsEmoa, Termination,
};

fn all_single_objective() -> Vec<(&'static str, Algorithm)> {
    vec![
        ("dds", Algorithm::Dds(Dds::default())),
        ("sce", Algorithm::Sce(Sce::default())),
        ("de", Algorithm::De(De::default())),
        ("lshade", Algorithm::LShade(LShade::default())),
        ("lsrtde", Algorithm::LSrtde(LSrtde::default())),
        ("pso", Algorithm::Pso(Pso::default())),
        ("cmaes", Algorithm::CmaEs(CmaEs::default())),
        ("bipop", Algorithm::RestartCmaEs(RestartCmaEs::bipop())),
    ]
}

/// Every optimizer spends *exactly* its budget — including budgets smaller
/// than an initial population, mid-generation cutoffs, and budgets that are
/// not multiples of any population size.
#[test]
fn budget_is_exact_for_every_optimizer() {
    let problem = Sphere::new(5);
    for (name, algo) in all_single_objective() {
        for budget in [1usize, 5, 7, 37, 100, 1013] {
            let r = algo.optimize(&problem, &Termination::budget(budget));
            assert_eq!(
                r.evaluations, budget,
                "{name} spent {} evaluations on a budget of {budget}",
                r.evaluations
            );
        }
    }
}

/// The multi-objective optimizers never exceed the budget either (they may
/// stop mid-generation).
#[test]
fn budget_is_exact_for_multiobjective() {
    use forge_core::testfn::Zdt1;
    let problem = Zdt1::new(6);
    for budget in [1usize, 57, 250, 1003] {
        let a = NsgaII::default().optimize(&problem, &Termination::budget(budget));
        assert_eq!(a.evaluations, budget, "NsgaII spent {}", a.evaluations);
        let b = NsgaIII::default().optimize(&problem, &Termination::budget(budget));
        assert_eq!(b.evaluations, budget, "NsgaIII spent {}", b.evaluations);
        let c = SmsEmoa::default().optimize(&problem, &Termination::budget(budget));
        assert_eq!(c.evaluations, budget, "SmsEmoa spent {}", c.evaluations);
        let d = Moead::default().optimize(&problem, &Termination::budget(budget));
        assert_eq!(d.evaluations, budget, "Moead spent {}", d.evaluations);
        let e = Padds::default().optimize(&problem, &Termination::budget(budget));
        assert_eq!(e.evaluations, budget, "Padds spent {}", e.evaluations);
    }
}

/// An objective that always returns NaN must terminate cleanly (no panic, no
/// hang) with the budget fully spent and an infinite best value.
#[test]
fn all_nan_objective_terminates_cleanly() {
    let problem = func(vec![(-1.0, 1.0); 3], |_| f64::NAN);
    for (name, algo) in all_single_objective() {
        let r = algo.optimize(&problem, &Termination::budget(500));
        assert_eq!(r.evaluations, 500, "{name}");
        assert!(
            r.best_value().is_infinite(),
            "{name}: all-NaN objective must report +inf, got {}",
            r.best_value()
        );
    }
}

/// Regression for the audit's CRITICAL finding: an improvement from an
/// infinite-fitness parent to a finite trial used to push a `+inf` delta into
/// the L-SHADE success memory, NaN-poisoning `M_F` and hanging the Cauchy
/// resampling loop. The objective below is NaN outside a small feasible ball,
/// so early populations are almost entirely infinite and finite improvements
/// are common.
#[test]
fn mostly_nan_objective_does_not_hang_adaptive_de() {
    let problem = func(vec![(-5.0, 5.0); 3], |x| {
        let s: f64 = x.iter().map(|v| v * v).sum();
        if s < 1.0 {
            s
        } else {
            f64::NAN
        }
    });
    for algo in [
        Algorithm::LShade(LShade::default()),
        Algorithm::LSrtde(LSrtde::default()),
    ] {
        let r = algo.optimize(&problem, &Termination::budget(3000));
        assert_eq!(r.evaluations, 3000);
        // With 3000 evaluations something feasible is essentially certain.
        assert!(r.best_value().is_finite(), "got {}", r.best_value());
    }
}

/// Invalid problems are rejected loudly instead of producing silent garbage.
#[test]
#[should_panic(expected = "invalid problem")]
fn inverted_bounds_panic() {
    let bad = func(vec![(1.0, -1.0)], |x| x[0]);
    Dds::default().optimize(&bad, &Termination::budget(10));
}

#[test]
#[should_panic(expected = "invalid problem")]
fn zero_dimensional_problem_panics() {
    let bad = func(vec![], |_| 0.0);
    De::default().optimize(&bad, &Termination::budget(10));
}

#[test]
#[should_panic(expected = "init has length")]
fn dds_rejects_wrong_length_start() {
    let p = Sphere::new(3);
    Dds::default().optimize_from(&p, &Termination::budget(10), Some(&[0.0, 0.0]));
}

/// A closure that lies about its objective count is caught up front, not deep
/// inside the non-dominated sort.
#[test]
#[should_panic(expected = "invalid problem")]
fn short_objective_vector_panics() {
    let bad = forge_core::problem::multi_func(vec![(0.0, 1.0)], 3, |x| vec![x[0], 1.0 - x[0]]);
    NsgaII::default().optimize(&bad, &Termination::budget(100));
}