Skip to main content

forge_core/
testfn.rs

1//! Standard optimization test functions, as [`Problem`]s.
2//!
3//! These are the classic benchmarks used to validate global optimizers: the
4//! convex unimodal **sphere**, the narrow-valley **Rosenbrock**, and the highly
5//! multimodal **Rastrigin**. All have a known global minimum of `0`, which the
6//! integration tests check each algorithm can reach.
7
8use crate::problem::{Bound, MultiProblem, Problem};
9
10/// Sphere: `f(x) = Σ xᵢ²`. Convex, unimodal, minimum `0` at the origin.
11pub struct Sphere {
12    bounds: Vec<Bound>,
13}
14
15impl Sphere {
16    /// `dim`-dimensional sphere on `[-5.12, 5.12]^dim`.
17    pub fn new(dim: usize) -> Self {
18        Sphere {
19            bounds: vec![(-5.12, 5.12); dim],
20        }
21    }
22}
23
24impl Problem for Sphere {
25    fn dim(&self) -> usize {
26        self.bounds.len()
27    }
28    fn bounds(&self) -> &[Bound] {
29        &self.bounds
30    }
31    fn objective(&self, x: &[f64]) -> f64 {
32        x.iter().map(|v| v * v).sum()
33    }
34}
35
36/// Rosenbrock: `f(x) = Σ [100(x_{i+1} − xᵢ²)² + (1 − xᵢ)²]`. A long, curved,
37/// nearly-flat valley; minimum `0` at the all-ones vector.
38pub struct Rosenbrock {
39    bounds: Vec<Bound>,
40}
41
42impl Rosenbrock {
43    /// `dim`-dimensional Rosenbrock (`dim >= 2`) on `[-5, 10]^dim`.
44    pub fn new(dim: usize) -> Self {
45        assert!(dim >= 2, "Rosenbrock needs at least 2 dimensions");
46        Rosenbrock {
47            bounds: vec![(-5.0, 10.0); dim],
48        }
49    }
50}
51
52impl Problem for Rosenbrock {
53    fn dim(&self) -> usize {
54        self.bounds.len()
55    }
56    fn bounds(&self) -> &[Bound] {
57        &self.bounds
58    }
59    fn objective(&self, x: &[f64]) -> f64 {
60        x.windows(2)
61            .map(|w| 100.0 * (w[1] - w[0] * w[0]).powi(2) + (1.0 - w[0]).powi(2))
62            .sum()
63    }
64}
65
66/// Rastrigin: `f(x) = 10n + Σ [xᵢ² − 10 cos(2π xᵢ)]`. Highly multimodal with a
67/// regular lattice of local minima; global minimum `0` at the origin.
68pub struct Rastrigin {
69    bounds: Vec<Bound>,
70}
71
72impl Rastrigin {
73    /// `dim`-dimensional Rastrigin on `[-5.12, 5.12]^dim`.
74    pub fn new(dim: usize) -> Self {
75        Rastrigin {
76            bounds: vec![(-5.12, 5.12); dim],
77        }
78    }
79}
80
81impl Problem for Rastrigin {
82    fn dim(&self) -> usize {
83        self.bounds.len()
84    }
85    fn bounds(&self) -> &[Bound] {
86        &self.bounds
87    }
88    fn objective(&self, x: &[f64]) -> f64 {
89        let n = x.len() as f64;
90        10.0 * n
91            + x.iter()
92                .map(|v| v * v - 10.0 * (2.0 * std::f64::consts::PI * v).cos())
93                .sum::<f64>()
94    }
95}
96
97/// ZDT1 (Zitzler, Deb & Thiele 2000): a two-objective benchmark with a known,
98/// convex Pareto front. On `[0, 1]^n`,
99///
100/// ```text
101/// f1 = x1
102/// g  = 1 + 9 · (Σ_{i≥2} xi) / (n − 1)
103/// f2 = g · (1 − √(f1 / g))
104/// ```
105///
106/// The Pareto-optimal set has `x2 … xn = 0` (so `g = 1`), giving the analytical
107/// front `f2 = 1 − √f1` for `f1 ∈ [0, 1]` — the ground truth the NSGA-II test
108/// measures distance to.
109pub struct Zdt1 {
110    bounds: Vec<Bound>,
111}
112
113impl Zdt1 {
114    /// `dim`-dimensional ZDT1 (`dim >= 2`) on `[0, 1]^dim`.
115    pub fn new(dim: usize) -> Self {
116        assert!(dim >= 2, "ZDT1 needs at least 2 dimensions");
117        Zdt1 {
118            bounds: vec![(0.0, 1.0); dim],
119        }
120    }
121
122    /// The analytical Pareto front value `f2 = 1 − √f1` for a given `f1`.
123    pub fn front_f2(f1: f64) -> f64 {
124        1.0 - f1.sqrt()
125    }
126}
127
128impl MultiProblem for Zdt1 {
129    fn dim(&self) -> usize {
130        self.bounds.len()
131    }
132    fn bounds(&self) -> &[Bound] {
133        &self.bounds
134    }
135    fn n_objectives(&self) -> usize {
136        2
137    }
138    fn objectives(&self, x: &[f64]) -> Vec<f64> {
139        let n = x.len();
140        let f1 = x[0];
141        let g = 1.0 + 9.0 * x[1..].iter().sum::<f64>() / (n - 1) as f64;
142        let f2 = g * (1.0 - (f1 / g).sqrt());
143        vec![f1, f2]
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn known_minima_are_zero() {
153        assert_eq!(Sphere::new(3).objective(&[0.0, 0.0, 0.0]), 0.0);
154        assert!(Rosenbrock::new(3).objective(&[1.0, 1.0, 1.0]).abs() < 1e-12);
155        assert!(Rastrigin::new(4).objective(&[0.0; 4]).abs() < 1e-12);
156    }
157
158    #[test]
159    fn zdt1_pareto_set_lies_on_the_analytical_front() {
160        // x2..xn = 0 ⇒ g = 1 ⇒ f2 = 1 − √f1 for any f1 = x1.
161        let p = Zdt1::new(5);
162        for &f1 in &[0.0, 0.25, 0.5, 1.0] {
163            let mut x = vec![0.0; 5];
164            x[0] = f1;
165            let o = p.objectives(&x);
166            assert!((o[0] - f1).abs() < 1e-12);
167            assert!((o[1] - Zdt1::front_f2(f1)).abs() < 1e-12);
168        }
169    }
170}