# 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]`).
| `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};
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.