Skip to main content

basin/problems/
rastrigin.rs

1//! N-dimensional Rastrigin function.
2//!
3//! `f(x) = A·n + Σᵢ [xᵢ² − A·cos(2π·xᵢ)]`  with `A = 10`.
4//!
5//! Highly multimodal: a parabolic bowl `Σ xᵢ²` modulated by a cosine
6//! ripple of amplitude `A` creates a dense lattice of local minima on
7//! integer offsets. The global minimum sits at `x = (0, …, 0)` with
8//! `f = 0`. Separable (the sum decomposes per coordinate), which is a
9//! useful diagnostic — solvers that exploit separability handle it far
10//! better than non-separable multimodal functions like Schwefel or
11//! Ackley.
12//!
13//! The canonical search domain is `[−5.12, 5.12]^n`, set by Mühlenbein
14//! et al. (1991) when they generalized Rastrigin's original 2D
15//! formulation to arbitrary `n`. This is what the GA / evolutionary
16//! optimization literature has used ever since (CEC competitions,
17//! Bergmeir's MA-LSCh-CMA paper, etc.).
18//!
19//! Gradient is intentionally omitted: the function exists in basin's
20//! corpus to exercise *global* solvers (CMA-ES variants, memetic
21//! algorithms) which use cost evaluations only. Local gradient methods
22//! stall in the nearest cosine pit and learn nothing about basin
23//! geometry.
24
25use core::marker::PhantomData;
26
27use super::spec::{Dimensionality, HasSpec, ProblemSpec, Properties, Reference};
28use crate::{BoxConstraints, CostFunction};
29
30/// Rastrigin amplitude constant. Fixed at 10 by Mühlenbein et al.
31/// (1991); essentially every paper that benchmarks on Rastrigin uses
32/// this value.
33const A: f64 = 10.0;
34
35/// Standard lower bound on each coordinate (Mühlenbein et al. 1991).
36pub const STANDARD_LOWER: f64 = -5.12;
37/// Standard upper bound on each coordinate (Mühlenbein et al. 1991).
38pub const STANDARD_UPPER: f64 = 5.12;
39
40/// Evaluates the Rastrigin function at `x`. Works for any `n >= 1`.
41pub fn rastrigin(x: &[f64]) -> f64 {
42    let n = x.len() as f64;
43    let two_pi = 2.0 * core::f64::consts::PI;
44    let mut s = A * n;
45    for &v in x.iter() {
46        s += v * v - A * (two_pi * v).cos();
47    }
48    s
49}
50
51/// Pre-wrapped Rastrigin problem. Generic over the parameter backend
52/// `P`; the default `P = Vec<f64>` lets you write `Rastrigin::default()`
53/// for the common case. Backend impls (`nalgebra::DVector<f64>`,
54/// `ndarray::Array1<f64>`, `faer::Col<f64>`) are gated behind their
55/// respective features.
56///
57/// Carries no constraint metadata. For solvers that need explicit box
58/// bounds (e.g. CMA-ES variants), use [`RastriginBoxed`].
59pub struct Rastrigin<P = Vec<f64>>(PhantomData<fn() -> P>);
60
61impl<P> Rastrigin<P> {
62    /// Build a freshly typed problem instance. Pair with one of the
63    /// backend-specific impl blocks (Vec, nalgebra, ndarray, faer).
64    pub const fn new() -> Self {
65        Self(PhantomData)
66    }
67}
68
69impl<P> Default for Rastrigin<P> {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75/// Catalogue entry for this problem.
76pub static RASTRIGIN_SPEC: ProblemSpec = ProblemSpec {
77    name: "Rastrigin",
78    dim: Dimensionality::NDimensional { min: 1 },
79    properties: Properties {
80        smooth: true,
81        differentiable: true,
82        // Non-convex: the cosine term creates many bumps.
83        convex: false,
84        // Highly multimodal — many local minima on an integer lattice.
85        unimodal: false,
86        // f(x) = Σᵢ gᵢ(xᵢ) with gᵢ(t) = A + t² − A·cos(2π·t); the
87        // additive constant A·n is shared across coordinates but the
88        // sum still decomposes per coordinate.
89        separable: true,
90        scalable: true,
91    },
92    references: &[
93        Reference {
94            citation: "Rastrigin (1974)",
95            title: "Systems of extremal control",
96            source: "Nauka, Moscow (in Russian)",
97            doi: None,
98            url: None,
99        },
100        Reference {
101            citation: "Mühlenbein, Schomisch & Born (1991)",
102            title: "The parallel genetic algorithm as function optimizer",
103            source: "Parallel Computing, 17(6–7), 619–632",
104            doi: Some("10.1016/S0167-8191(05)80052-3"),
105            url: None,
106        },
107    ],
108    description: "Highly multimodal: parabolic bowl Σ xᵢ² modulated by a \
109                  cosine ripple of amplitude 10, giving a dense lattice of \
110                  local minima. Global minimum at x = (0, …, 0), value 0. \
111                  Standard search domain is [−5.12, 5.12]ⁿ (Mühlenbein \
112                  et al. 1991). Separable, so coordinate-wise solvers \
113                  handle it far better than non-separable multimodal \
114                  functions like Schwefel.",
115};
116
117impl<P> HasSpec for Rastrigin<P> {
118    const SPEC: &'static ProblemSpec = &RASTRIGIN_SPEC;
119}
120
121impl CostFunction for Rastrigin<Vec<f64>> {
122    type Param = Vec<f64>;
123    type Output = f64;
124    type Error = std::convert::Infallible;
125    fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
126        Ok(rastrigin(x))
127    }
128}
129
130#[cfg(feature = "nalgebra")]
131mod nalgebra_impl {
132    use super::{Rastrigin, rastrigin};
133    use crate::CostFunction;
134    use nalgebra::DVector;
135
136    impl CostFunction for Rastrigin<DVector<f64>> {
137        type Param = DVector<f64>;
138        type Output = f64;
139        type Error = std::convert::Infallible;
140        fn cost(&self, x: &DVector<f64>) -> Result<f64, std::convert::Infallible> {
141            Ok(rastrigin(x.as_slice()))
142        }
143    }
144}
145
146#[cfg(feature = "ndarray")]
147mod ndarray_impl {
148    use super::{Rastrigin, rastrigin};
149    use crate::CostFunction;
150    use ndarray::Array1;
151
152    impl CostFunction for Rastrigin<Array1<f64>> {
153        type Param = Array1<f64>;
154        type Output = f64;
155        type Error = std::convert::Infallible;
156        fn cost(&self, x: &Array1<f64>) -> Result<f64, std::convert::Infallible> {
157            Ok(rastrigin(x.as_slice().expect("Array1 is contiguous")))
158        }
159    }
160}
161
162#[cfg(feature = "faer")]
163mod faer_impl {
164    use super::{A, Rastrigin};
165    use crate::CostFunction;
166    use faer::Col;
167
168    // faer's `Col` doesn't expose a `&[f64]` directly across all 0.24 APIs
169    // we care about, so we evaluate elementwise here rather than routing
170    // through the slice-based primitive.
171    impl CostFunction for Rastrigin<Col<f64>> {
172        type Param = Col<f64>;
173        type Output = f64;
174        type Error = std::convert::Infallible;
175        fn cost(&self, x: &Col<f64>) -> Result<f64, std::convert::Infallible> {
176            let n = x.nrows();
177            let two_pi = 2.0 * core::f64::consts::PI;
178            let mut s = A * n as f64;
179            for i in 0..n {
180                let v = x[i];
181                s += v * v - A * (two_pi * v).cos();
182            }
183            Ok(s)
184        }
185    }
186}
187
188// ----------------------------------------------------------------------
189// Boxed (constrained) form
190// ----------------------------------------------------------------------
191// Carries element-wise bounds on the struct so it can implement
192// `BoxConstraints` for solvers that require explicit box constraints
193// (CMA-ES variants, projected methods). The standard `[−5.12, 5.12]ⁿ`
194// search domain is the most common choice; `with_standard_bounds(n)`
195// is a shortcut for that case.
196
197/// Rastrigin function with explicit element-wise box bounds, suitable
198/// for solvers that require [`BoxConstraints`] (e.g. CMA-ES variants
199/// like MA-LSCh-CMA). Carries the bounds as data on the problem (tenet
200/// 4 in `crate::core` / `CONTRIBUTING.md`) and routes the cost through the
201/// same raw [`rastrigin`] free function as the unconstrained
202/// [`Rastrigin`].
203///
204/// The standard search domain `[−5.12, 5.12]ⁿ` from Mühlenbein et al.
205/// (1991) is the common case; build it with
206/// [`RastriginBoxed::with_standard_bounds`].
207pub struct RastriginBoxed<P> {
208    lower: P,
209    upper: P,
210}
211
212impl<P> RastriginBoxed<P> {
213    /// Build a Rastrigin problem with arbitrary element-wise bounds.
214    /// Caller must ensure `lower[i] ≤ upper[i]` per component.
215    pub fn new(lower: P, upper: P) -> Self {
216        Self { lower, upper }
217    }
218}
219
220impl<P> HasSpec for RastriginBoxed<P> {
221    const SPEC: &'static ProblemSpec = &RASTRIGIN_SPEC;
222}
223
224impl RastriginBoxed<Vec<f64>> {
225    /// Build the canonical Rastrigin instance on `[−5.12, 5.12]ⁿ` for
226    /// the requested dimension `n`.
227    pub fn with_standard_bounds(n: usize) -> Self {
228        Self {
229            lower: vec![STANDARD_LOWER; n],
230            upper: vec![STANDARD_UPPER; n],
231        }
232    }
233}
234
235impl CostFunction for RastriginBoxed<Vec<f64>> {
236    type Param = Vec<f64>;
237    type Output = f64;
238    type Error = std::convert::Infallible;
239    fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
240        Ok(rastrigin(x))
241    }
242}
243
244impl BoxConstraints for RastriginBoxed<Vec<f64>> {
245    fn lower(&self) -> &Vec<f64> {
246        &self.lower
247    }
248    fn upper(&self) -> &Vec<f64> {
249        &self.upper
250    }
251}
252
253#[cfg(feature = "nalgebra")]
254mod nalgebra_boxed_impl {
255    use super::{RastriginBoxed, STANDARD_LOWER, STANDARD_UPPER, rastrigin};
256    use crate::{BoxConstraints, CostFunction};
257    use nalgebra::DVector;
258
259    impl RastriginBoxed<DVector<f64>> {
260        /// Build the canonical Rastrigin instance on `[−5.12, 5.12]ⁿ`
261        /// for the requested dimension `n`.
262        pub fn with_standard_bounds(n: usize) -> Self {
263            Self {
264                lower: DVector::from_element(n, STANDARD_LOWER),
265                upper: DVector::from_element(n, STANDARD_UPPER),
266            }
267        }
268    }
269
270    impl CostFunction for RastriginBoxed<DVector<f64>> {
271        type Param = DVector<f64>;
272        type Output = f64;
273        type Error = std::convert::Infallible;
274        fn cost(&self, x: &DVector<f64>) -> Result<f64, std::convert::Infallible> {
275            Ok(rastrigin(x.as_slice()))
276        }
277    }
278
279    impl BoxConstraints for RastriginBoxed<DVector<f64>> {
280        fn lower(&self) -> &DVector<f64> {
281            &self.lower
282        }
283        fn upper(&self) -> &DVector<f64> {
284            &self.upper
285        }
286    }
287}
288
289#[cfg(feature = "ndarray")]
290mod ndarray_boxed_impl {
291    use super::{RastriginBoxed, STANDARD_LOWER, STANDARD_UPPER, rastrigin};
292    use crate::{BoxConstraints, CostFunction};
293    use ndarray::Array1;
294
295    impl RastriginBoxed<Array1<f64>> {
296        /// Build the canonical Rastrigin instance on `[−5.12, 5.12]ⁿ`
297        /// for the requested dimension `n`.
298        pub fn with_standard_bounds(n: usize) -> Self {
299            Self {
300                lower: Array1::from_elem(n, STANDARD_LOWER),
301                upper: Array1::from_elem(n, STANDARD_UPPER),
302            }
303        }
304    }
305
306    impl CostFunction for RastriginBoxed<Array1<f64>> {
307        type Param = Array1<f64>;
308        type Output = f64;
309        type Error = std::convert::Infallible;
310        fn cost(&self, x: &Array1<f64>) -> Result<f64, std::convert::Infallible> {
311            Ok(rastrigin(x.as_slice().expect("Array1 is contiguous")))
312        }
313    }
314
315    impl BoxConstraints for RastriginBoxed<Array1<f64>> {
316        fn lower(&self) -> &Array1<f64> {
317            &self.lower
318        }
319        fn upper(&self) -> &Array1<f64> {
320            &self.upper
321        }
322    }
323}
324
325#[cfg(feature = "faer")]
326mod faer_boxed_impl {
327    use super::{A, RastriginBoxed, STANDARD_LOWER, STANDARD_UPPER};
328    use crate::{BoxConstraints, CostFunction};
329    use faer::Col;
330
331    impl RastriginBoxed<Col<f64>> {
332        /// Build the canonical Rastrigin instance on `[−5.12, 5.12]ⁿ`
333        /// for the requested dimension `n`.
334        pub fn with_standard_bounds(n: usize) -> Self {
335            Self {
336                lower: Col::<f64>::from_fn(n, |_| STANDARD_LOWER),
337                upper: Col::<f64>::from_fn(n, |_| STANDARD_UPPER),
338            }
339        }
340    }
341
342    impl CostFunction for RastriginBoxed<Col<f64>> {
343        type Param = Col<f64>;
344        type Output = f64;
345        type Error = std::convert::Infallible;
346        fn cost(&self, x: &Col<f64>) -> Result<f64, std::convert::Infallible> {
347            let n = x.nrows();
348            let two_pi = 2.0 * core::f64::consts::PI;
349            let mut s = A * n as f64;
350            for i in 0..n {
351                let v = x[i];
352                s += v * v - A * (two_pi * v).cos();
353            }
354            Ok(s)
355        }
356    }
357
358    impl BoxConstraints for RastriginBoxed<Col<f64>> {
359        fn lower(&self) -> &Col<f64> {
360            &self.lower
361        }
362        fn upper(&self) -> &Col<f64> {
363            &self.upper
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn rastrigin_minimum_is_zero_at_origin() {
374        assert!(rastrigin(&[0.0]).abs() < 1e-12);
375        assert!(rastrigin(&[0.0, 0.0]).abs() < 1e-12);
376        assert!(rastrigin(&[0.0; 10]).abs() < 1e-12);
377        assert!(rastrigin(&[0.0; 30]).abs() < 1e-12);
378    }
379
380    #[test]
381    fn rastrigin_known_value_at_unit_offsets() {
382        // At integer offsets cos(2π·k) = 1, so each coordinate
383        // contributes A + k² − A = k². The constant A·n cancels with
384        // the per-coordinate −A·cos = −A. Total:
385        //   f(x) = A·n + Σᵢ (xᵢ² − A) = Σᵢ xᵢ²
386        // For x = (1, 1, ..., 1), f = n. For n = 3 that's 3.
387        assert!((rastrigin(&[1.0, 1.0, 1.0]) - 3.0).abs() < 1e-9);
388        assert!((rastrigin(&[2.0, 2.0]) - 8.0).abs() < 1e-9);
389    }
390
391    #[test]
392    fn rastrigin_local_minimum_at_half_integer_offset() {
393        // The nearest local minima of the 1D component
394        // g(t) = A + t² − A·cos(2π·t) lie near t ≈ ±1 (not exactly,
395        // because the parabola tilts the cosine pits). Just verify
396        // the value at t = 1: g(1) = A + 1 − A·1 = 1, so the local
397        // pit value is exactly 1 there.
398        assert!((rastrigin(&[1.0]) - 1.0).abs() < 1e-12);
399        assert!((rastrigin(&[-1.0]) - 1.0).abs() < 1e-12);
400    }
401
402    #[test]
403    fn rastrigin_matches_definition_at_irregular_point() {
404        // Hand-compute f(0.3, -0.7):
405        //   A·n = 20
406        //   0.3² + (-0.7)² = 0.09 + 0.49 = 0.58
407        //   cos(2π·0.3) + cos(2π·(-0.7)) = cos(0.6π) + cos(-1.4π)
408        //     = cos(0.6π) + cos(1.4π)         (cos is even)
409        //     ≈ -0.30901699 + -0.30901699
410        //     ≈ -0.61803398
411        //   f = 20 + 0.58 − 10·(−0.61803398) = 20.58 + 6.1803398
412        //     ≈ 26.7603398
413        let f = rastrigin(&[0.3, -0.7]);
414        assert!((f - 26.7603398874989).abs() < 1e-9, "got {f}");
415    }
416
417    #[test]
418    fn spec_is_wired_up_via_has_spec_trait() {
419        let spec = <Rastrigin<Vec<f64>> as HasSpec>::SPEC;
420        assert_eq!(spec.name, "Rastrigin");
421        assert!(spec.properties.smooth);
422        assert!(spec.properties.differentiable);
423        assert!(spec.properties.separable);
424        assert!(spec.properties.scalable);
425        assert!(!spec.properties.convex);
426        assert!(!spec.properties.unimodal);
427        assert!(matches!(spec.dim, Dimensionality::NDimensional { min: 1 }));
428        assert!(!spec.references.is_empty());
429    }
430
431    #[test]
432    fn boxed_form_exposes_standard_bounds() {
433        let p = RastriginBoxed::<Vec<f64>>::with_standard_bounds(10);
434        let lo = <RastriginBoxed<Vec<f64>> as BoxConstraints>::lower(&p);
435        let hi = <RastriginBoxed<Vec<f64>> as BoxConstraints>::upper(&p);
436        assert_eq!(lo.len(), 10);
437        assert_eq!(hi.len(), 10);
438        for &v in lo {
439            assert_eq!(v, STANDARD_LOWER);
440        }
441        for &v in hi {
442            assert_eq!(v, STANDARD_UPPER);
443        }
444    }
445
446    #[test]
447    fn boxed_form_shares_cost_with_unboxed() {
448        let unboxed: Rastrigin<Vec<f64>> = Rastrigin::default();
449        let boxed = RastriginBoxed::<Vec<f64>>::with_standard_bounds(3);
450        let x = vec![0.3, -0.7, 1.2];
451        assert!((unboxed.cost(&x).unwrap() - boxed.cost(&x).unwrap()).abs() < 1e-12);
452    }
453
454    #[test]
455    fn boxed_form_reuses_rastrigin_spec() {
456        let spec = <RastriginBoxed<Vec<f64>> as HasSpec>::SPEC;
457        // Same static — both wrappers point at the one Rastrigin entry.
458        assert!(core::ptr::eq(spec, &RASTRIGIN_SPEC));
459    }
460}