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

forge ⚙️

A deterministic metaheuristic optimization substrate in Rust.

forge is to optimization what surtgis-core is to raster data: a shared engine that several tools in the author's ecosystem consume rather than an application in itself. Every optimizer speaks one interface — the Problem trait — minimizes by convention, counts objective evaluations as its budget, rejects non-finite candidates, and is reproducible for a given seed.

Crate name: published on crates.io as metalforge (the name forge-core was taken); the library is imported as forge_core:

[dependencies]
forge-core = { version = "0.1", package = "metalforge" }
use forge_core::{Dds, Optimizer, Termination};

Why not just use argmin / optirustic?

Generic Rust optimization crates exist. forge's value is not to reimplement them but to provide:

  1. Geoscientific global optimizers the generic libraries omit — DDS and SCE-UA — central to hydrological calibration.
  2. Certified determinism via one seedable RNG (Rng, SplitMix64), the same generator used across anvil-core and rainflow-core.
  3. One unified Problem trait for the whole ecosystem.

The DDS and SCE-UA implementations are migrated from rainflow-core, where they were validated against airGR (GR4J calibration NSE 0.7956 vs 0.7957).

Status — v0.2 (audit release)

v0.2.0 is the result of a full fidelity/correctness audit against the source papers (and, for L-SRTDE, the author's public reference implementation): exact evaluation budgets everywhere, input validation at every entry point, NaN-robust adaptive memories, L-SRTDE rebuilt on its true two-population scheme, CMA-ES boundary handling made consistent (clamped steps feed the adaptation), SMS-EMOA's paper Reduce rule, NSGA-III's persistent ideal point, and a golden-value test + CI matrix that turn the "identical with or without rayon" claim into checked evidence. Additive API changes now bump the minor version (Algorithm/StopReason/BoundsError are #[non_exhaustive]).

Component State
Problem / bounds / Maximize adapter
Deterministic Rng (SplitMix64) + split streams
DDS (Tolson & Shoemaker 2007)
SCE-UA (Duan et al. 1992)
Differential Evolution (Storn & Price 1997)
PSO (Kennedy & Eberhart 1995)
L-SHADE adaptive DE (Tanabe & Fukunaga 2014; CEC-winning lineage)
L-SRTDE success-rate adaptive DE (Stanovov & Semenkin 2024; CEC 2024 winner)
CMA-ES (Hansen & Ostermeier 2001)
NSGA-II multi-objective (Deb et al. 2002)
NSGA-III reference-point many-objective (Deb & Jain 2014)
SMS-EMOA hypervolume MOEA, 2-3 objectives (Beume et al. 2007)
Simulated Annealing (combinatorial, via Anneal trait)
GLUE uncertainty estimation (Beven & Binley 1992)
IPOP/BIPOP-CMA-ES restarts (Auger & Hansen 2005; Hansen 2009) ✅ v0.3
PA-DDS multi-objective DDS, HV-contribution selection (Asadzadeh & Tolson 2013) ✅ v0.3
MOEA/D Tchebycheff decomposition (Zhang & Li 2007) ✅ v0.3
Constraints: Deb feasibility rules adapter + ε-constrained L-SHADE (Takahama & Sakai) ✅ v0.3
Quality indicators: HV, HV contributions, GD, IGD, GD⁺, IGD⁺ (indicators module) ✅ v0.3
Test functions (sphere, Rosenbrock, Rastrigin, ZDT1, DTLZ2)
Convergence + determinism tests
Contract tests (exact budgets, NaN robustness, input validation)
Golden-value determinism test + CI feature matrix (rayon on/off)
rainflow consumes forge (DDS/SCE-UA)
anvil consumes forge (combinatorial SA, byte-identical)
Uniform Optimizer trait + MultiProblem + Anneal
Rayon island/ensemble parallelism (deterministic)
Parallel tempering (replica exchange, via Anneal)
Published on crates.io as metalforge
Cross-checked vs pymoo (HV/IGD; see validation/)
COCO/BBOB-style benchmark (fixed-target ERT; see bench/)
COCO-format data logging (IOHanalyzer-loadable) + MA-BBOB-style affine instances ✅ v0.3
DREAM_ZS formal Bayesian MCMC (companion to GLUE) ⬜ v0.4
PyO3 / WASM targets ⬜ later

Quick start

use forge_core::{Dds, Sce, De, Pso, Termination};
use forge_core::testfn::Sphere;

let problem = Sphere::new(5);
let report = Dds::default().optimize(&problem, &Termination::budget(2000));
assert!(report.best_value() < 1e-2);

Maximization (e.g. NSE/KGE) wraps the problem so the minimizing core stays the single convention:

use forge_core::{Optimizer, Sce, Termination};
use forge_core::problem::{func, Maximize};

let p = Maximize(func(vec![(-10.0, 10.0)], |x| -(x[0] - 2.0).powi(2)));
let report = Sce::default().optimize(&p, &Termination::budget(3000));
assert!((report.best()[0] - 2.0).abs() < 0.1);

Robust restarts via independent islands — deterministic regardless of scheduling, and identical with or without the rayon feature:

use forge_core::{ensemble, De, Termination};
use forge_core::testfn::Rastrigin;

let problem = Rastrigin::new(3);
// 8 islands, base seed 42; island i is seeded mix_seed(42, i).
let report = ensemble(&De::default(), &problem, &Termination::budget(5000), 8, 42);
assert!(report.best_value() < 1.0);

Enable the thread pool with --features rayon (off by default to keep the core dependency-free). The result is byte-identical either way.

Combinatorial problems use simulated annealing through the Anneal trait, which supports incremental move evaluation (O(touched) per step):

use forge_core::{Anneal, Rng, Sa, Schedule};

// Number partitioning: split weights into two groups of equal sum.
struct Partition { weights: Vec<f64> }
impl Partition { fn diff(&self, x: &[bool]) -> f64 {
    self.weights.iter().zip(x).map(|(w, &b)| if b { *w } else { -*w }).sum()
}}
impl Anneal for Partition {
    type State = Vec<bool>;
    type Move = usize;
    fn initial(&self, _r: &mut Rng) -> Vec<bool> { vec![false; self.weights.len()] }
    fn energy(&self, x: &Vec<bool>) -> f64 { self.diff(x).powi(2) }
    fn propose(&self, x: &Vec<bool>, r: &mut Rng) -> usize { r.index(x.len()) }
    fn delta(&self, x: &Vec<bool>, &i: &usize) -> f64 {
        let d = self.diff(x);
        let step = if x[i] { -2.0 } else { 2.0 } * self.weights[i];
        (d + step).powi(2) - d.powi(2)
    }
    fn apply(&self, x: &mut Vec<bool>, i: usize) { x[i] = !x[i]; }
}

let p = Partition { weights: vec![8.0, 7.0, 6.0, 5.0, 4.0] };
let result = Sa { iterations: 4000, restarts: 8, schedule: Schedule::adaptive(), seed: 1 }
    .optimize(&p);
assert!(result.energy < 1e-9); // perfect partition found

Layout

  • forge-core — traits, algorithms, deterministic RNG, termination criteria.
  • (planned) targets: native (Rayon) + Python (PyO3) + CLI runner + WASM demo.

License

MIT OR Apache-2.0.