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`:
> ```toml
> [dependencies]
> forge-core = { version = "0.1", package = "metalforge" }
> ```
> ```rust
> 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

```rust
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:

```rust
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:

```rust
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):

```rust
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.