Skip to main content

turboswarm_core/benchmarks/
functions.rs

1//! Concrete test functions.
2
3/// Metadata for a test function, useful for validation.
4#[derive(Debug, Clone)]
5pub struct Benchmark {
6    /// Name of the function (the same one used to dispatch it by string).
7    pub name: &'static str,
8    /// Recommended [min, max] bound per dimension (symmetric).
9    pub bound: f64,
10    /// Value of the global optimum.
11    pub optimum_value: f64,
12}
13
14/// Sphere: f(x) = Σ xᵢ². Global minimum f(0) = 0. Convex, unimodal.
15pub fn sphere(x: &[f64]) -> f64 {
16    x.iter().map(|&xi| xi * xi).sum()
17}
18
19/// Rastrigin: highly multimodal, global minimum f(0) = 0.
20/// f(x) = 10n + Σ [xᵢ² − 10·cos(2π·xᵢ)]
21pub fn rastrigin(x: &[f64]) -> f64 {
22    let n = x.len() as f64;
23    10.0 * n
24        + x.iter()
25            .map(|&xi| xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos())
26            .sum::<f64>()
27}
28
29/// Rosenbrock ("banana valley"): global minimum f(1,…,1) = 0.
30/// f(x) = Σ [100·(xᵢ₊₁ − xᵢ²)² + (1 − xᵢ)²]
31pub fn rosenbrock(x: &[f64]) -> f64 {
32    x.windows(2)
33        .map(|w| {
34            let (xi, xj) = (w[0], w[1]);
35            100.0 * (xj - xi * xi).powi(2) + (1.0 - xi).powi(2)
36        })
37        .sum()
38}
39
40/// Ackley: multimodal, nearly flat away from the origin with a narrow well.
41/// Global minimum f(0) = 0.
42/// f(x) = −20·exp(−0.2·√(mean xᵢ²)) − exp(mean cos(2π·xᵢ)) + 20 + e
43pub fn ackley(x: &[f64]) -> f64 {
44    let n = x.len() as f64;
45    let sum_sq: f64 = x.iter().map(|&xi| xi * xi).sum();
46    let sum_cos: f64 = x
47        .iter()
48        .map(|&xi| (2.0 * std::f64::consts::PI * xi).cos())
49        .sum();
50    -20.0 * (-0.2 * (sum_sq / n).sqrt()).exp() - (sum_cos / n).exp() + 20.0 + std::f64::consts::E
51}
52
53/// Griewank: a product of cosines that creates many regular local minima.
54/// Global minimum f(0) = 0.
55/// f(x) = 1 + Σ xᵢ²/4000 − Π cos(xᵢ/√i)
56pub fn griewank(x: &[f64]) -> f64 {
57    let sum: f64 = x.iter().map(|&xi| xi * xi).sum::<f64>() / 4000.0;
58    let prod: f64 = x
59        .iter()
60        .enumerate()
61        .map(|(i, &xi)| (xi / ((i + 1) as f64).sqrt()).cos())
62        .product();
63    1.0 + sum - prod
64}
65
66/// Schwefel: multimodal and, unlike the others, with the optimum FAR from the
67/// origin (at ≈420.97 per dimension). A good example of why centering
68/// the search at 0 can be misleading. Global minimum f(420.9687…) = 0.
69/// f(x) = 418.9829·n − Σ xᵢ·sin(√|xᵢ|)
70pub fn schwefel(x: &[f64]) -> f64 {
71    let n = x.len() as f64;
72    418.982_887_272_433_8 * n - x.iter().map(|&xi| xi * xi.abs().sqrt().sin()).sum::<f64>()
73}
74
75// --- CEC-family functions ---
76//
77// These are the canonical (unshifted, unrotated) base functions used as the
78// building blocks of the CEC benchmark suites. The official suites compose them
79// with shift vectors and rotation matrices supplied as data files; those are
80// not bundled here, but a shift/rotation can be applied by the caller. All have
81// their global minimum equal to 0.
82
83/// Bent Cigar: unimodal and severely ill-conditioned — one cheap direction and
84/// the rest scaled by 10⁶. Global minimum f(0) = 0.
85/// f(x) = x₁² + 10⁶·Σ_{i>1} xᵢ²
86pub fn bent_cigar(x: &[f64]) -> f64 {
87    if x.is_empty() {
88        return 0.0;
89    }
90    x[0] * x[0] + 1e6 * x[1..].iter().map(|&xi| xi * xi).sum::<f64>()
91}
92
93/// Discus: the ill-conditioned counterpart of Bent Cigar — one expensive
94/// direction (10⁶) and the rest cheap. Global minimum f(0) = 0.
95/// f(x) = 10⁶·x₁² + Σ_{i>1} xᵢ²
96pub fn discus(x: &[f64]) -> f64 {
97    if x.is_empty() {
98        return 0.0;
99    }
100    1e6 * x[0] * x[0] + x[1..].iter().map(|&xi| xi * xi).sum::<f64>()
101}
102
103/// High Conditioned Elliptic: a smoothly increasing condition number across
104/// dimensions (from 1 to 10⁶). Unimodal. Global minimum f(0) = 0.
105/// f(x) = Σᵢ (10⁶)^((i−1)/(n−1))·xᵢ²
106pub fn elliptic(x: &[f64]) -> f64 {
107    let n = x.len();
108    if n <= 1 {
109        return x.first().map_or(0.0, |&v| v * v);
110    }
111    x.iter()
112        .enumerate()
113        .map(|(i, &xi)| 1e6_f64.powf(i as f64 / (n - 1) as f64) * xi * xi)
114        .sum()
115}
116
117/// Zakharov: unimodal with no local minima, coupling the dimensions through a
118/// weighted sum. Global minimum f(0) = 0.
119/// f(x) = Σxᵢ² + (Σ 0.5·i·xᵢ)² + (Σ 0.5·i·xᵢ)⁴
120pub fn zakharov(x: &[f64]) -> f64 {
121    let sum_sq: f64 = x.iter().map(|&xi| xi * xi).sum();
122    let sum_half: f64 = x
123        .iter()
124        .enumerate()
125        .map(|(i, &xi)| 0.5 * (i + 1) as f64 * xi)
126        .sum();
127    sum_sq + sum_half.powi(2) + sum_half.powi(4)
128}
129
130/// Levy: multimodal with many local minima. Global minimum f(1,…,1) = 0
131/// (the optimum is away from the origin).
132pub fn levy(x: &[f64]) -> f64 {
133    if x.is_empty() {
134        return 0.0;
135    }
136    let pi = std::f64::consts::PI;
137    let w: Vec<f64> = x.iter().map(|&xi| 1.0 + (xi - 1.0) / 4.0).collect();
138    let n = w.len();
139    let term1 = (pi * w[0]).sin().powi(2);
140    let term_mid: f64 = w[..n - 1]
141        .iter()
142        .map(|&wi| (wi - 1.0).powi(2) * (1.0 + 10.0 * (pi * wi + 1.0).sin().powi(2)))
143        .sum();
144    let term_last = (w[n - 1] - 1.0).powi(2) * (1.0 + (2.0 * pi * w[n - 1]).sin().powi(2));
145    term1 + term_mid + term_last
146}
147
148/// Expanded Schaffer F6: a deceptive, highly multimodal function built by
149/// chaining the 2-D Schaffer F6 over consecutive (cyclic) pairs. Global
150/// minimum f(0) = 0.
151pub fn expanded_schaffer(x: &[f64]) -> f64 {
152    fn g(a: f64, b: f64) -> f64 {
153        let s = a * a + b * b;
154        0.5 + (s.sqrt().sin().powi(2) - 0.5) / (1.0 + 0.001 * s).powi(2)
155    }
156    let n = x.len();
157    if n == 0 {
158        return 0.0;
159    }
160    if n == 1 {
161        return g(x[0], x[0]);
162    }
163    (0..n).map(|i| g(x[i], x[(i + 1) % n])).sum()
164}
165
166/// Metadata for the Phase 1 functions.
167pub const SPHERE: Benchmark = Benchmark {
168    name: "sphere",
169    bound: 5.12,
170    optimum_value: 0.0,
171};
172pub const RASTRIGIN: Benchmark = Benchmark {
173    name: "rastrigin",
174    bound: 5.12,
175    optimum_value: 0.0,
176};
177pub const ROSENBROCK: Benchmark = Benchmark {
178    name: "rosenbrock",
179    bound: 2.048,
180    optimum_value: 0.0,
181};
182
183/// Metadata for the Phase 2 functions.
184pub const ACKLEY: Benchmark = Benchmark {
185    name: "ackley",
186    bound: 32.768,
187    optimum_value: 0.0,
188};
189pub const GRIEWANK: Benchmark = Benchmark {
190    name: "griewank",
191    bound: 600.0,
192    optimum_value: 0.0,
193};
194pub const SCHWEFEL: Benchmark = Benchmark {
195    name: "schwefel",
196    bound: 500.0,
197    optimum_value: 0.0,
198};
199
200/// Metadata for the CEC-family functions.
201pub const BENT_CIGAR: Benchmark = Benchmark {
202    name: "bent_cigar",
203    bound: 100.0,
204    optimum_value: 0.0,
205};
206pub const DISCUS: Benchmark = Benchmark {
207    name: "discus",
208    bound: 100.0,
209    optimum_value: 0.0,
210};
211pub const ELLIPTIC: Benchmark = Benchmark {
212    name: "elliptic",
213    bound: 100.0,
214    optimum_value: 0.0,
215};
216pub const ZAKHAROV: Benchmark = Benchmark {
217    name: "zakharov",
218    bound: 10.0,
219    optimum_value: 0.0,
220};
221pub const LEVY: Benchmark = Benchmark {
222    name: "levy",
223    bound: 10.0,
224    optimum_value: 0.0,
225};
226pub const EXPANDED_SCHAFFER: Benchmark = Benchmark {
227    name: "expanded_schaffer",
228    bound: 100.0,
229    optimum_value: 0.0,
230};
231
232/// All registered benchmarks with their metadata. It lets the
233/// visualization layer choose bounds and know the optimum without
234/// hardcoding them by hand (e.g. auto-fitting the domain of a plot).
235pub const ALL: &[Benchmark] = &[
236    SPHERE,
237    RASTRIGIN,
238    ROSENBROCK,
239    ACKLEY,
240    GRIEWANK,
241    SCHWEFEL,
242    BENT_CIGAR,
243    DISCUS,
244    ELLIPTIC,
245    ZAKHAROV,
246    LEVY,
247    EXPANDED_SCHAFFER,
248];
249
250/// Looks up the metadata of a benchmark by name.
251pub fn meta(name: &str) -> Option<&'static Benchmark> {
252    ALL.iter().find(|b| b.name == name)
253}