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
//! Exports the objective vectors of forge's multi-objective fronts to CSV, for
//! cross-validation against pymoo (hypervolume / IGD comparison).
//!
//! Usage: `cargo run --example export_fronts -- [out_dir]`
//! (default `out_dir`: the system temp directory)
//!
//! Writes, for each seed s in 1..=15:
//! - `forge_zdt1_s{s}.csv`  — NSGA-II on ZDT1 (30 variables), pop 100,
//!   20 000 evaluations — matches pymoo's default ZDT1.
//! - `forge_dtlz2_s{s}.csv` — NSGA-III on DTLZ2 (3 objectives, 12 variables),
//!   pop 92, Das-Dennis p=12 reference points, 40 000 evaluations.
//!
//! Multi-seed export enables a dispersion-aware comparison (median ± IQR)
//! instead of a single-seed point estimate.

use std::fs::File;
use std::io::Write;

use forge_core::testfn::{Dtlz2, Zdt1};
use forge_core::{NsgaII, NsgaIII, ParetoFront, Termination};

const SEEDS: std::ops::RangeInclusive<u64> = 1..=15;

fn write_csv(path: &str, front: &ParetoFront) {
    let mut f = File::create(path).expect("create csv");
    let m = front.solutions[0].objectives.len();
    let header: Vec<String> = (0..m).map(|j| format!("f{}", j + 1)).collect();
    writeln!(f, "{}", header.join(",")).unwrap();
    for o in front.objective_vectors() {
        let row: Vec<String> = o.iter().map(|v| format!("{v:.10}")).collect();
        writeln!(f, "{}", row.join(",")).unwrap();
    }
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let out_dir = args
        .get(1)
        .cloned()
        .unwrap_or_else(|| std::env::temp_dir().to_string_lossy().into_owned());

    for seed in SEEDS {
        // NSGA-II on ZDT1 (n=30), pop 100, 20 000 evals (~200 generations).
        let zdt1 = Zdt1::new(30);
        let nsga2 = NsgaII {
            pop_size: 100,
            seed,
            ..NsgaII::default()
        };
        let f1 = nsga2.optimize(&zdt1, &Termination::budget(20_000));
        let p1 = format!("{out_dir}/forge_zdt1_s{seed}.csv");
        write_csv(&p1, &f1);
        eprintln!(
            "NSGA-II/ZDT1  seed {seed:2}: {} solutions, {} evals -> {p1}",
            f1.len(),
            f1.evaluations
        );

        // NSGA-III on DTLZ2 (3 obj, n=12), pop 92, p=12 reference points.
        let dtlz2 = Dtlz2::new(3, 12);
        let nsga3 = NsgaIII {
            pop_size: 92,
            divisions: 12,
            seed,
            ..NsgaIII::default()
        };
        let f2 = nsga3.optimize(&dtlz2, &Termination::budget(40_000));
        let p2 = format!("{out_dir}/forge_dtlz2_s{seed}.csv");
        write_csv(&p2, &f2);
        eprintln!(
            "NSGA-III/DTLZ2 seed {seed:2}: {} solutions, {} evals -> {p2}",
            f2.len(),
            f2.evaluations
        );
    }
}