Skip to main content

hpr_analysis/optimize/
cmaes.rs

1//! The covariance matrix adaptation evolution strategy (CMA-ES).
2//!
3//! Each generation draws `λ` candidates from a normal distribution about a mean `m`, with an
4//! overall step size `σ` and a covariance `C` that sets the steps' shape. The model ranks them;
5//! the mean moves to a weighted average of the best `μ = ⌊λ/2⌋`. The step size grows when
6//! successive moves point the same way and shrinks when they cancel (*cumulative step-size
7//! adaptation*), and the covariance learns the directions the good steps took (the *rank-one* and
8//! *rank-μ* updates). Only the ranking matters, so the strategy is unchanged by any increasing
9//! transformation of the output, and by any rotation of the variables once `C` has adapted.
10//!
11//! The algorithm and its default parameters are those of N. Hansen, "The CMA Evolution Strategy:
12//! A Tutorial", arXiv:1604.00772v2 (2023), <https://arxiv.org/abs/1604.00772>: Appendix A's
13//! summary (Figure 6, eqs. (38) to (47), pp. 28–29) with Table 1's default parameters (eqs. (48)
14//! to (58), p. 31), and with the weights after the `μ`-th zero, as in the tutorial's own code
15//! ("This code does not implement negative weights, that is, wᵢ = 0 for i > µ in Table 1",
16//! p. 36): the original strategy, not the *active* one. Equations, for a generation `g` counted
17//! from 0:
18//!
19//! ```text
20//! yₖ = B D zₖ,  zₖ ~ N(0, I),  xₖ = m + σ yₖ                        (sampling, k = 1 … λ)
21//! ⟨y⟩ = Σᵢ wᵢ yᵢ:λ                                                  (the μ best, ranked)
22//! m ← m + c_m σ ⟨y⟩
23//! p_σ ← (1 − c_σ) p_σ + √(c_σ (2 − c_σ) μ_eff) C^(−1/2) ⟨y⟩
24//! σ ← σ exp((c_σ/d_σ) (‖p_σ‖/E‖N(0, I)‖ − 1))
25//! h_σ = 1 if ‖p_σ‖/√(1 − (1 − c_σ)^(2(g+1))) < (1.4 + 2/(n + 1)) E‖N(0, I)‖, else 0
26//! p_c ← (1 − c_c) p_c + h_σ √(c_c (2 − c_c) μ_eff) ⟨y⟩
27//! C ← (1 + c₁ δ(h_σ) − c₁ − c_μ Σwⱼ) C + c₁ p_c p_cᵀ + c_μ Σᵢ wᵢ yᵢ:λ yᵢ:λᵀ
28//! ```
29//!
30//! with `δ(h_σ) = (1 − h_σ) c_c (2 − c_c)` and `E‖N(0, I)‖ ≈ √n (1 − 1/(4n) + 1/(21n²))`.
31//! `C = B D² Bᵀ` is decomposed every generation, by Jacobi's method; the tutorial allows putting
32//! it off for up to `1/(10 n (c₁ + c_μ))` generations (B.2, p. 33), which is under one for up to
33//! about 85 variables at the default population.
34//!
35//! # Starting point and scaling
36//!
37//! The strategy works in each [`Variable`] divided by its step. There it starts as the tutorial's
38//! Figure 6 does, with `σ = 1`, `C = I` and both paths zero, at the variables' starts. So a
39//! variable in meters and another in kilograms each start with the steps the caller gave them,
40//! and the condition number the run stops at is the tutorial's. This is the tutorial's advice for
41//! variables whose search intervals differ: "a scaling of the variables should be applied"
42//! (Figure 6's footnote, p. 29). [`Run::covariance`] is `C` in these scaled variables.
43//!
44//! # Bounds
45//!
46//! A candidate outside its variables' bounds is drawn again, from its own stream, until it falls
47//! inside: the second of the two methods the tutorial gives for a best point strictly inside the
48//! feasible region ("re-sampling any infeasible solution x until it become feasible", B.5,
49//! p. 34). No candidate is repaired onto a bound, which the tutorial advises against. If any one
50//! candidate of a generation is still outside after [`MAX_DRAWS`] tries, the run ends
51//! ([`Stop::Bounds`]). The chance that a draw falls inside halves with each variable whose mean
52//! sits on a bound, so with many variables near their bounds this comes soon. A best point *on* a
53//! bound is reached only slowly this way; for a bound that binds, leave the variable unbounded on
54//! that side and write the bound as a constraint.
55//!
56//! # Integer variables
57//!
58//! An integer variable ([`Variable::integer`]) is drawn as a real number like the others, and the
59//! model is given the whole number nearest the draw, clamped to its bounds
60//! ([`Variable::encode`]); the update learns from the real draws. Left at that, the spread in an
61//! integer variable would shrink until every draw gave the same whole number and that variable
62//! stopped moving, wherever it was. CMA-ES with margin (R. Hamano, S. Saito, M. Nomura and
63//! S. Shirakawa, "CMA-ES with Margin: Lower-Bounding Marginal Probability for Mixed-Integer
64//! Black-Box Optimization", GECCO 2022, <https://arxiv.org/abs/2205.13482>, §4 and Algorithm 1,
65//! pp. 5–6 and 10) prevents it: after each update it keeps at least a chance `α = 1/(n λ)` that a
66//! draw lands on another value, by moving the mean towards a threshold or by stretching that
67//! variable's draws by a factor `A` ([`Run::margin_scale`]):
68//!
69//! ```text
70//! xₖ = m + σ S A yₖ                                    (S the steps, A diagonal, 1 if continuous)
71//! s = σ A S √C_jj                                       (variable j's spread)
72//! at an end value, threshold ℓ:  m ← ℓ + sign(m − ℓ) min(|m − ℓ|, Φ⁻¹(1 − α) s)      eq. (13)
73//! inside, thresholds ℓ₋ < m ≤ ℓ₊:
74//!   p₋ = Φ((ℓ₋ − m)/s),  p₊ = Φ((m − ℓ₊)/s),  p₀ = 1 − p₋ − p₊                  eqs. (17)–(19)
75//!   p′ = max(α/2, p),  p″ = p′ + (1 − p′₋ − p′₊ − p₀)(p′ − α/2)/(p′₋ + p′₊ + p₀ − 3α/2)
76//!   χ = Φ⁻¹(1 − p″):  m ← (ℓ₋ χ₊ + ℓ₊ χ₋)/(χ₋ + χ₊),  A ← (ℓ₊ − ℓ₋)/((χ₋ + χ₊) σ S √C_jj)   (24)
77//! ```
78//!
79//! The thresholds lie halfway between neighbouring whole numbers; the mean, `σ` and `C` are the
80//! updated ones and `A` the old one in `s`. The paths and `C` never see the correction. An
81//! integer variable's draws are never redrawn for its bounds, which its encoding enforces. The
82//! paper's α is the default here; its Figure 4 (p. 7) finds the method works across a range
83//! about it. `Φ` is the standard normal distribution function.
84//!
85//! # Constraints
86//!
87//! Other constraints go through [`Run::tell_constrained`], which ranks candidates by Deb's
88//! feasibility rules ([`Evaluation`]): any candidate that keeps every constraint ranks ahead of
89//! any that doesn't, and those that don't rank by how far they break them. Infeasible candidates
90//! are evaluated and ranked, not drawn again, so the distribution can sit across a constraint's
91//! edge and close in on a minimum that lies on it.
92//!
93//! # Stopping
94//!
95//! A run stops at the first of: a target value reached ([`Stop::Target`]), the evaluations used
96//! up ([`Stop::Evaluations`]), the distribution's spread and its evolution path below a tolerance
97//! in every variable ([`Stop::TolX`]), the best values of the last `10 + ⌈30 n/λ⌉` generations
98//! and every value of the last one all within a tolerance ([`Stop::TolFun`]), the covariance's
99//! condition number above 10¹⁴ or a step or candidate that has overflowed ([`Stop::Condition`]), or a
100//! candidate that can't be drawn inside the bounds ([`Stop::Bounds`]). These are the tutorial's
101//! TolX, TolFun and ConditionCov (B.3, pp. 33–34), with its suggested 10⁻¹² for both tolerances,
102//! TolX's taken in the scaled variables, so as a fraction of each one's step. Its NoEffectAxis,
103//! NoEffectCoord, Stagnation and TolXUp tests are left out: a run that diverges ends at
104//! [`Stop::Condition`], or at the evaluation cap.
105//!
106//! # A run with no finite value
107//!
108//! If every candidate so far has given `+∞` (every flight failed, say), the run goes on, ranking
109//! them in their order, and its [`Optimum`]'s value is `+∞`. Check [`Optimum::value`] before
110//! using the point; under constraints, check [`Optimum::violation`] too, which is above zero if
111//! no candidate kept them all.
112
113use serde::{Deserialize, Serialize};
114
115use hpr_core::random::SeededRng;
116
117use super::eigen::symmetric_eigen;
118use super::{Evaluation, Variable, check_variables, normal};
119use crate::error::AnalysisError;
120
121/// The most times one candidate is drawn again to fall inside the bounds.
122pub const MAX_DRAWS: usize = 1000;
123
124/// The largest population a run takes.
125pub const MAX_POPULATION: usize = 1 << 16;
126
127/// The covariance's largest condition number before a run stops: its axes' lengths then differ
128/// by 10⁷, about where rounding in `f64` starts to blur the shortest.
129pub const MAX_CONDITION: f64 = 1e14;
130
131/// The optimizer's settings: the variables, the population, and when to stop. It serializes as
132/// its fields, and reads back through the same checks as [`Cmaes::new`] and its `with_` methods.
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[serde(try_from = "CmaesData")]
135pub struct Cmaes {
136    variables: Vec<Variable>,
137    population: usize,
138    max_evaluations: usize,
139    target: Option<f64>,
140    tolerance_x: f64,
141    tolerance_value: f64,
142}
143
144/// The serialized form of a [`Cmaes`].
145#[derive(Deserialize)]
146#[serde(deny_unknown_fields)]
147struct CmaesData {
148    variables: Vec<Variable>,
149    population: usize,
150    max_evaluations: usize,
151    target: Option<f64>,
152    tolerance_x: f64,
153    tolerance_value: f64,
154}
155
156impl TryFrom<CmaesData> for Cmaes {
157    type Error = AnalysisError;
158
159    fn try_from(data: CmaesData) -> Result<Self, AnalysisError> {
160        let cmaes = Cmaes::new(data.variables)?
161            .with_population(data.population)?
162            .with_max_evaluations(data.max_evaluations)?
163            .with_tolerance_x(data.tolerance_x)?
164            .with_tolerance_value(data.tolerance_value)?;
165        match data.target {
166            Some(target) => cmaes.with_target(target),
167            None => Ok(cmaes),
168        }
169    }
170}
171
172/// Why a run stopped.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
174#[non_exhaustive]
175pub enum Stop {
176    /// A value at or below the target was found.
177    Target,
178    /// The evaluations allowed were used up.
179    Evaluations,
180    /// The distribution's spread, and its evolution path, fell below the tolerance in every
181    /// variable: the run has converged in the variables.
182    TolX,
183    /// The values stopped changing by more than the tolerance: converged in the output.
184    TolFun,
185    /// The covariance's condition number passed [`MAX_CONDITION`], or the step size or a
186    /// candidate overflowed: the run has diverged, or its distribution has degenerated.
187    Condition,
188    /// A candidate of a generation didn't fall inside the bounds in [`MAX_DRAWS`] tries.
189    Bounds,
190}
191
192/// What a run found.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194#[non_exhaustive]
195pub struct Optimum {
196    /// The best point evaluated, one value per variable, in the variables' order, as the model
197    /// was given it (an integer variable's value encoded).
198    pub point: Vec<f64>,
199    /// The model's value there: `+∞` if no candidate gave a finite value (serialized as none, a
200    /// JSON `null`).
201    #[serde(with = "infinity_as_none")]
202    pub value: f64,
203    /// Its constraint violation ([`Evaluation::violation`]): zero if it keeps every constraint,
204    /// and always zero for a run told plain values; `+∞` if every candidate failed (serialized as
205    /// none).
206    #[serde(default, with = "infinity_as_none")]
207    pub violation: f64,
208    /// Which evaluation found it, counted from 1.
209    pub evaluation: usize,
210    /// How many evaluations the run made.
211    pub evaluations: usize,
212    /// How many generations it ran.
213    pub generations: usize,
214    /// The distribution's final mean, not encoded: an integer variable's may lie between whole
215    /// numbers or outside its bounds ([`Variable::encode`] gives the value it stands for).
216    pub mean: Vec<f64>,
217    /// The distribution's largest standard deviation in any one variable, in that variable's
218    /// units: the largest `σ √Cᵢᵢ` times the variable's step and its margin scale `Aᵢ`; `+∞`
219    /// once the step size has overflowed (serialized as none, a JSON `null`). An integer variable
220    /// at an inner value keeps it large enough for the margin (its `Aᵢ` grows), so then it
221    /// doesn't shrink to zero; at an end value only the mean moves, and it may.
222    #[serde(with = "infinity_as_none")]
223    pub spread: f64,
224    /// Why it stopped.
225    pub stop: Stop,
226}
227
228/// The serialized form of a number that is finite or `+∞`, the only infinity an [`Optimum`]
229/// holds: an option, none for `+∞`, as JSON has no infinity. A NaN or `−∞` is refused, as
230/// it would read back as `+∞`.
231pub(crate) mod infinity_as_none {
232    use serde::ser::Error as _;
233    use serde::{Deserialize, Deserializer, Serialize, Serializer};
234
235    pub(crate) fn serialize<S: Serializer>(x: &f64, serializer: S) -> Result<S::Ok, S::Error> {
236        if x.is_nan() || *x == f64::NEG_INFINITY {
237            return Err(S::Error::custom(format!("{x} is neither finite nor +∞")));
238        }
239        let value = (*x != f64::INFINITY).then_some(*x);
240        value.serialize(serializer)
241    }
242
243    pub(crate) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
244        Ok(Option::<f64>::deserialize(deserializer)?.unwrap_or(f64::INFINITY))
245    }
246}
247
248impl Cmaes {
249    /// The optimizer over `variables`, with the tutorial's default population
250    /// `λ = 4 + ⌊3 ln n⌋`, at most 10,000 evaluations, no target, and tolerances of 10⁻¹² (in
251    /// the variables, as a fraction of each one's step; and in the output, in its own units).
252    ///
253    /// # Errors
254    ///
255    /// [`AnalysisError::TooFew`] for no variables, [`AnalysisError::Count`] for more than
256    /// [`MAX_VARIABLES`](super::MAX_VARIABLES), and [`AnalysisError::DuplicateVariable`] for two
257    /// with one name.
258    pub fn new(variables: Vec<Variable>) -> Result<Self, AnalysisError> {
259        check_variables(&variables)?;
260        let n = variables.len() as f64;
261        // Cast: 4 + ⌊3 ln n⌋ is at most 19 for n ≤ 200.
262        let population = 4 + (3.0 * n.ln()).floor() as usize;
263        Ok(Self {
264            variables,
265            population,
266            max_evaluations: 10_000,
267            target: None,
268            tolerance_x: 1e-12,
269            tolerance_value: 1e-12,
270        })
271    }
272
273    /// The same, with `λ` candidates a generation. A larger population searches more widely,
274    /// which helps on outputs with many local minima, at more evaluations a generation.
275    ///
276    /// # Errors
277    ///
278    /// [`AnalysisError::TooFew`] for fewer than 2, [`AnalysisError::Count`] for more than
279    /// [`MAX_POPULATION`].
280    pub fn with_population(mut self, lambda: usize) -> Result<Self, AnalysisError> {
281        if lambda < 2 {
282            return Err(AnalysisError::TooFew {
283                what: "population",
284                count: lambda,
285                minimum: 2,
286            });
287        }
288        if lambda > MAX_POPULATION {
289            return Err(AnalysisError::Count {
290                what: "population",
291                count: lambda,
292                limit: MAX_POPULATION,
293            });
294        }
295        self.population = lambda;
296        Ok(self)
297    }
298
299    /// The same, stopping once `max` evaluations have been made (it finishes the generation, so
300    /// it may make up to `λ − 1` more).
301    ///
302    /// # Errors
303    ///
304    /// [`AnalysisError::TooFew`] for none.
305    pub fn with_max_evaluations(mut self, max: usize) -> Result<Self, AnalysisError> {
306        if max == 0 {
307            return Err(AnalysisError::TooFew {
308                what: "evaluations",
309                count: 0,
310                minimum: 1,
311            });
312        }
313        self.max_evaluations = max;
314        Ok(self)
315    }
316
317    /// The same, stopping once a value at or below `target` is found.
318    ///
319    /// # Errors
320    ///
321    /// [`AnalysisError::Domain`] for a target that isn't finite: an infinite one would be met by
322    /// a generation whose every candidate failed.
323    pub fn with_target(mut self, target: f64) -> Result<Self, AnalysisError> {
324        if !target.is_finite() {
325            return Err(AnalysisError::Domain {
326                what: "target (finite)",
327                value: target,
328            });
329        }
330        self.target = Some(target);
331        Ok(self)
332    }
333
334    /// The same, ending a run once the distribution's spread, and its evolution path, are below
335    /// `tolerance` times each variable's step in every variable ([`Stop::TolX`]). Zero turns the
336    /// test off.
337    ///
338    /// # Errors
339    ///
340    /// [`AnalysisError::Domain`] for a tolerance that is negative or not finite.
341    pub fn with_tolerance_x(mut self, tolerance: f64) -> Result<Self, AnalysisError> {
342        self.tolerance_x = check_tolerance("x tolerance", tolerance)?;
343        Ok(self)
344    }
345
346    /// The same, ending a run once its recent values all lie within `tolerance` of each other, in
347    /// the output's units ([`Stop::TolFun`]). Zero turns the test off.
348    ///
349    /// # Errors
350    ///
351    /// [`AnalysisError::Domain`] for a tolerance that is negative or not finite.
352    pub fn with_tolerance_value(mut self, tolerance: f64) -> Result<Self, AnalysisError> {
353        self.tolerance_value = check_tolerance("value tolerance", tolerance)?;
354        Ok(self)
355    }
356
357    /// The variables.
358    pub fn variables(&self) -> &[Variable] {
359        &self.variables
360    }
361
362    /// The population, `λ`.
363    pub fn population(&self) -> usize {
364        self.population
365    }
366
367    /// Starts a run from `seed`, with its first generation drawn. Evaluate the candidates, give
368    /// their values to [`Run::tell`], and repeat until it returns why it stopped.
369    ///
370    /// # Errors
371    ///
372    /// [`AnalysisError::OutOfBounds`] if a first candidate can't be drawn inside the bounds, and
373    /// [`AnalysisError::Domain`] if one overflows (steps near `f64::MAX`).
374    pub fn start(&self, seed: u64) -> Result<Run, AnalysisError> {
375        let mut run = Run::new(self, seed);
376        match run.draw() {
377            None => Ok(run),
378            Some(Stop::Bounds) => Err(AnalysisError::OutOfBounds { draws: MAX_DRAWS }),
379            Some(_) => Err(AnalysisError::Domain {
380                what: "first candidate (not finite: the steps are too large)",
381                value: f64::INFINITY,
382            }),
383        }
384    }
385
386    /// Minimizes `model` under constraints from `seed`, evaluating each candidate in turn: the
387    /// model gives each candidate's [`Evaluation`], and [`Run::tell_constrained`] ranks them.
388    /// If no candidate keeps every constraint, the optimum's [`violation`](Optimum::violation)
389    /// is above zero: check it before using the point.
390    ///
391    /// # Errors
392    ///
393    /// [`Cmaes::start`]'s and [`Run::tell_constrained`]'s.
394    pub fn minimize_constrained(
395        &self,
396        seed: u64,
397        mut model: impl FnMut(&[f64]) -> Evaluation,
398    ) -> Result<Optimum, AnalysisError> {
399        let mut run = self.start(seed)?;
400        loop {
401            let evaluations: Vec<Evaluation> = run.candidates().iter().map(|x| model(x)).collect();
402            if let Some(optimum) = run.tell_constrained(&evaluations)? {
403                return Ok(optimum);
404            }
405        }
406    }
407
408    /// Minimizes `model` from `seed`, evaluating each candidate in turn.
409    ///
410    /// # Errors
411    ///
412    /// [`Cmaes::start`]'s and [`Run::tell`]'s.
413    pub fn minimize(
414        &self,
415        seed: u64,
416        mut model: impl FnMut(&[f64]) -> f64,
417    ) -> Result<Optimum, AnalysisError> {
418        let mut run = self.start(seed)?;
419        loop {
420            let values: Vec<f64> = run.candidates().iter().map(|x| model(x)).collect();
421            if let Some(optimum) = run.tell(&values)? {
422                return Ok(optimum);
423            }
424        }
425    }
426}
427
428/// A tolerance, checked to be finite and not negative.
429fn check_tolerance(what: &'static str, tolerance: f64) -> Result<f64, AnalysisError> {
430    if tolerance.is_finite() && tolerance >= 0.0 {
431        Ok(tolerance)
432    } else {
433        Err(AnalysisError::Domain {
434            what,
435            value: tolerance,
436        })
437    }
438}
439
440/// The strategy's parameters for `n` variables and population `λ`: the tutorial's Table 1 with
441/// the negative weights set to zero. [`Run::parameters`] gives a run's; they serialize, but
442/// aren't read back, as nothing takes them in.
443#[derive(Debug, Clone, PartialEq, Serialize)]
444#[non_exhaustive]
445pub struct Parameters {
446    /// `μ = ⌊λ/2⌋`, the candidates that move the mean.
447    pub mu: usize,
448    /// `wᵢ ∝ ln((λ + 1)/2) − ln i`, `i = 1 … μ`, summing to 1.
449    pub weights: Vec<f64>,
450    /// `μ_eff = 1/Σwᵢ²`, the selection's variance-effective size.
451    pub mu_eff: f64,
452    /// `c_σ = (μ_eff + 2)/(n + μ_eff + 5)`.
453    pub c_sigma: f64,
454    /// `d_σ = 1 + 2 max(0, √((μ_eff − 1)/(n + 1)) − 1) + c_σ`.
455    pub d_sigma: f64,
456    /// `c_c = (4 + μ_eff/n)/(n + 4 + 2 μ_eff/n)`.
457    pub c_c: f64,
458    /// `c₁ = 2/((n + 1.3)² + μ_eff)`.
459    pub c_1: f64,
460    /// `c_μ = min(1 − c₁, 2 (1/4 + μ_eff + 1/μ_eff − 2)/((n + 2)² + μ_eff))`.
461    pub c_mu: f64,
462    /// `E‖N(0, I)‖ ≈ √n (1 − 1/(4n) + 1/(21n²))`.
463    pub chi_n: f64,
464}
465
466impl Parameters {
467    /// The parameters for `n ≥ 1` variables and `lambda ≥ 2` candidates a generation, as
468    /// [`Cmaes`] checks them.
469    pub(crate) fn new(n: usize, lambda: usize) -> Self {
470        let nf = n as f64;
471        let mu = lambda / 2;
472        let half = (lambda as f64 + 1.0) / 2.0;
473        let raw: Vec<f64> = (1..=mu).map(|i| half.ln() - (i as f64).ln()).collect();
474        let sum: f64 = raw.iter().sum();
475        let weights: Vec<f64> = raw.iter().map(|w| w / sum).collect();
476        let mu_eff = 1.0 / weights.iter().map(|w| w * w).sum::<f64>();
477        let c_sigma = (mu_eff + 2.0) / (nf + mu_eff + 5.0);
478        let d_sigma = 1.0 + 2.0 * (((mu_eff - 1.0) / (nf + 1.0)).sqrt() - 1.0).max(0.0) + c_sigma;
479        let c_c = (4.0 + mu_eff / nf) / (nf + 4.0 + 2.0 * mu_eff / nf);
480        // α_cov = 2.
481        let c_1 = 2.0 / ((nf + 1.3).powi(2) + mu_eff);
482        let c_mu = (1.0 - c_1).min(
483            2.0 * (0.25 + mu_eff + 1.0 / mu_eff - 2.0) / ((nf + 2.0).powi(2) + 2.0 * mu_eff / 2.0),
484        );
485        let chi_n = nf.sqrt() * (1.0 - 1.0 / (4.0 * nf) + 1.0 / (21.0 * nf * nf));
486        Self {
487            mu,
488            weights,
489            mu_eff,
490            c_sigma,
491            d_sigma,
492            c_c,
493            c_1,
494            c_mu,
495            chi_n,
496        }
497    }
498}
499
500/// A run in progress: its distribution, the current generation's candidates, and the best point
501/// so far.
502#[derive(Debug, Clone)]
503pub struct Run {
504    variables: Vec<Variable>,
505    seed: u64,
506    lambda: usize,
507    max_evaluations: usize,
508    target: Option<f64>,
509    tolerance_x: f64,
510    tolerance_value: f64,
511    p: Parameters,
512    n: usize,
513    /// Each variable's step: the strategy works in the variables divided by these.
514    scale: Vec<f64>,
515    /// The mean, in the variables' own units.
516    mean: Vec<f64>,
517    sigma: f64,
518    p_sigma: Vec<f64>,
519    p_c: Vec<f64>,
520    /// `C`, row-major, in the scaled variables.
521    c: Vec<f64>,
522    /// `B`, the eigenvectors of `C` as columns, row-major.
523    b: Vec<f64>,
524    /// `D`, the square roots of `C`'s eigenvalues.
525    d: Vec<f64>,
526    /// The margin's diagonal `A`: each candidate's draw is `m + σ S A y`. 1 for a continuous
527    /// variable, always.
528    a: Vec<f64>,
529    /// The margin `α = 1/(n λ)`: the least chance an integer variable's draw has of falling on a
530    /// value next to its mean's.
531    alpha: f64,
532    generation: usize,
533    evaluations: usize,
534    candidates: Vec<Vec<f64>>,
535    /// Each candidate's step `y = B D z`, in the scaled variables.
536    steps: Vec<Vec<f64>>,
537    best: Option<(Vec<f64>, Evaluation, usize)>,
538    /// The best evaluation of each generation, oldest first.
539    history: Vec<Evaluation>,
540    stop: Option<Stop>,
541}
542
543impl Run {
544    fn new(cmaes: &Cmaes, seed: u64) -> Self {
545        let n = cmaes.variables.len();
546        let mut identity = vec![0.0; n * n];
547        for i in 0..n {
548            identity[i * n + i] = 1.0;
549        }
550        Self {
551            seed,
552            lambda: cmaes.population,
553            max_evaluations: cmaes.max_evaluations,
554            target: cmaes.target,
555            tolerance_x: cmaes.tolerance_x,
556            tolerance_value: cmaes.tolerance_value,
557            p: Parameters::new(n, cmaes.population),
558            n,
559            scale: cmaes.variables.iter().map(Variable::step).collect(),
560            mean: cmaes.variables.iter().map(Variable::start).collect(),
561            sigma: 1.0,
562            p_sigma: vec![0.0; n],
563            p_c: vec![0.0; n],
564            c: identity.clone(),
565            b: identity,
566            d: vec![1.0; n],
567            a: vec![1.0; n],
568            // Casts: n ≤ 200 and λ ≤ 2¹⁶, exact in f64.
569            alpha: 1.0 / (n as f64 * cmaes.population as f64),
570            generation: 0,
571            evaluations: 0,
572            candidates: Vec::new(),
573            steps: Vec::new(),
574            best: None,
575            history: Vec::new(),
576            stop: None,
577            variables: cmaes.variables.clone(),
578        }
579    }
580
581    /// The strategy's parameters.
582    pub fn parameters(&self) -> &Parameters {
583        &self.p
584    }
585
586    /// The current generation's candidates, `λ` points, each one value per variable, as the model
587    /// is to be given them: an integer variable's value is encoded, a whole number within its
588    /// bounds. Empty once the run has stopped.
589    pub fn candidates(&self) -> &[Vec<f64>] {
590        &self.candidates
591    }
592
593    /// The generations finished so far.
594    pub fn generation(&self) -> usize {
595        self.generation
596    }
597
598    /// The distribution's mean. An integer variable's is a real number, not encoded, and may lie
599    /// outside its bounds.
600    pub fn mean(&self) -> &[f64] {
601        &self.mean
602    }
603
604    /// The step size `σ`.
605    pub fn sigma(&self) -> f64 {
606        self.sigma
607    }
608
609    /// The covariance `C`, row-major, of the variables divided by their steps: the distribution
610    /// of draws about the mean has covariance `σ² S A C A S`, with `S` the steps and `A`
611    /// [`Run::margin_scale`] on diagonals.
612    pub fn covariance(&self) -> &[f64] {
613        &self.c
614    }
615
616    /// The margin's diagonal `A`, one entry per variable: an integer variable's draws spread
617    /// `A` times as far as `σ` and `C` alone would spread them ([`Variable::integer`]). 1 for
618    /// every continuous variable.
619    pub fn margin_scale(&self) -> &[f64] {
620        &self.a
621    }
622
623    /// The margin `α = 1/(n λ)`: the least chance, each generation, that an integer variable's
624    /// draw falls on a value other than the one its mean is nearest (at least `α/2` on each side
625    /// when the mean's value has neighbours on both).
626    pub fn integer_margin(&self) -> f64 {
627        self.alpha
628    }
629
630    /// Why the run stopped, or `None` while it runs.
631    pub fn stopped(&self) -> Option<Stop> {
632        self.stop
633    }
634
635    /// Takes the model's values at [`Run::candidates`], in their order, and moves the
636    /// distribution. Returns what the run found once it stops, and `None` while it goes on, with
637    /// the next generation's candidates drawn.
638    ///
639    /// A value may be `+∞`, for a candidate the model can't evaluate (a flight that fails, say):
640    /// it ranks below every finite one.
641    ///
642    /// # Errors
643    ///
644    /// [`AnalysisError::Length`] for a number of values other than the candidates', and
645    /// [`AnalysisError::Output`] for a NaN or `−∞`, with the index of its evaluation in the run.
646    /// Telling a stopped run is [`AnalysisError::Length`] too, as it has no candidates.
647    pub fn tell(&mut self, values: &[f64]) -> Result<Option<Optimum>, AnalysisError> {
648        let evaluations: Vec<Evaluation> =
649            values.iter().map(|&v| Evaluation::feasible(v)).collect();
650        self.tell_constrained(&evaluations)
651    }
652
653    /// [`Run::tell`] with each candidate's constraint violation: candidates are ranked by Deb's
654    /// feasibility rules ([`Evaluation::rank`]), the best point is the best by the same rules,
655    /// and a target counts only for a point that keeps every constraint. The stop on values
656    /// ([`Stop::TolFun`]) looks only at generations whose best keeps them, and at the values of
657    /// this generation's candidates that keep them.
658    ///
659    /// Told only violations of zero, a run is bit for bit [`Run::tell`]'s.
660    ///
661    /// # Errors
662    ///
663    /// [`Run::tell`]'s, and [`AnalysisError::Domain`] for a violation that is negative or NaN
664    /// (`+∞` is a failed candidate's, [`Evaluation::failed`]).
665    pub fn tell_constrained(
666        &mut self,
667        evaluations: &[Evaluation],
668    ) -> Result<Option<Optimum>, AnalysisError> {
669        let values: Vec<f64> = evaluations.iter().map(|e| e.value).collect();
670        if values.len() != self.candidates.len() || self.candidates.is_empty() {
671            return Err(AnalysisError::Length {
672                what: "values, against the generation's candidates",
673                length: values.len(),
674                expected: self.candidates.len(),
675            });
676        }
677        if let Some(e) = evaluations
678            .iter()
679            .find(|e| e.violation.is_nan() || e.violation < 0.0)
680        {
681            return Err(AnalysisError::Domain {
682                what: "constraint violation (must not be negative or NaN)",
683                value: e.violation,
684            });
685        }
686        if let Some((k, &value)) = values
687            .iter()
688            .enumerate()
689            .find(|(_, v)| v.is_nan() || **v == f64::NEG_INFINITY)
690        {
691            return Err(AnalysisError::Output {
692                index: self.evaluations + k,
693                value,
694            });
695        }
696        // Rank: a stable sort, so ties keep the candidates' order.
697        let mut order: Vec<usize> = (0..self.lambda).collect();
698        order.sort_by(|&i, &j| evaluations[i].rank(&evaluations[j]));
699        let first = order[0];
700        if self
701            .best
702            .as_ref()
703            .is_none_or(|(_, e, _)| evaluations[first].beats(e))
704        {
705            self.best = Some((
706                self.candidates[first].clone(),
707                evaluations[first],
708                self.evaluations + first + 1,
709            ));
710        }
711        self.evaluations += self.lambda;
712        self.update(&order);
713        self.generation += 1;
714        self.history.push(evaluations[first]);
715        let feasible: Vec<f64> = evaluations
716            .iter()
717            .filter(|e| e.is_feasible())
718            .map(|e| e.value)
719            .collect();
720        self.stop = self.check_stop(&feasible);
721        if self.stop.is_none() {
722            self.stop = self.draw();
723        }
724        Ok(self.stop.map(|stop| self.optimum(stop)))
725    }
726
727    /// Moves the mean, the paths, the covariance and the step size: the tutorial's update.
728    fn update(&mut self, order: &[usize]) {
729        let n = self.n;
730        let p = &self.p;
731        let selected: Vec<&Vec<f64>> = order[..p.mu].iter().map(|&k| &self.steps[k]).collect();
732        let mut y_w = vec![0.0; n];
733        for (w, y) in p.weights.iter().zip(&selected) {
734            for (a, yi) in y_w.iter_mut().zip(y.iter()) {
735                *a += w * yi;
736            }
737        }
738        // c_m = 1; the step is scaled back to the variables' units.
739        for ((m, y), s) in self.mean.iter_mut().zip(&y_w).zip(&self.scale) {
740            *m += self.sigma * s * y;
741        }
742        // C^(−1/2) ⟨y⟩ = B D⁻¹ Bᵀ ⟨y⟩.
743        let bt_y: Vec<f64> = (0..n)
744            .map(|k| (0..n).map(|i| self.b[i * n + k] * y_w[i]).sum::<f64>() / self.d[k])
745            .collect();
746        let whitened: Vec<f64> = (0..n)
747            .map(|i| (0..n).map(|k| self.b[i * n + k] * bt_y[k]).sum())
748            .collect();
749        let cs = p.c_sigma;
750        let scale_sigma = (cs * (2.0 - cs) * p.mu_eff).sqrt();
751        for (ps, w) in self.p_sigma.iter_mut().zip(&whitened) {
752            *ps = (1.0 - cs) * *ps + scale_sigma * w;
753        }
754        let norm_ps = self.p_sigma.iter().map(|x| x * x).sum::<f64>().sqrt();
755        // The generation just finished is g + 1, counting from 1.
756        let exponent = 2.0 * (self.generation as f64 + 1.0);
757        let h_sigma = norm_ps / (1.0 - (1.0 - cs).powf(exponent)).sqrt()
758            < (1.4 + 2.0 / (n as f64 + 1.0)) * p.chi_n;
759        let cc = p.c_c;
760        let scale_c = (cc * (2.0 - cc) * p.mu_eff).sqrt();
761        for (pc, y) in self.p_c.iter_mut().zip(&y_w) {
762            *pc = (1.0 - cc) * *pc + if h_sigma { scale_c * y } else { 0.0 };
763        }
764        let delta = if h_sigma { 0.0 } else { cc * (2.0 - cc) };
765        // Σwⱼ = 1 with the negative weights zero.
766        let keep = 1.0 + p.c_1 * delta - p.c_1 - p.c_mu;
767        for i in 0..n {
768            for j in 0..=i {
769                let rank_mu: f64 = p
770                    .weights
771                    .iter()
772                    .zip(&selected)
773                    .map(|(w, y)| w * y[i] * y[j])
774                    .sum();
775                let value =
776                    keep * self.c[i * n + j] + p.c_1 * self.p_c[i] * self.p_c[j] + p.c_mu * rank_mu;
777                self.c[i * n + j] = value;
778                self.c[j * n + i] = value;
779            }
780        }
781        self.sigma *= ((cs / p.d_sigma) * (norm_ps / p.chi_n - 1.0)).exp();
782        let (values, vectors) = symmetric_eigen(&self.c, n);
783        self.b = vectors;
784        self.d = values.iter().map(|v| v.max(0.0).sqrt()).collect();
785        self.correct_margin();
786    }
787
788    /// The margin of CMA-ES with margin (R. Hamano, S. Saito, M. Nomura, S. Shirakawa, GECCO
789    /// 2022, arXiv:2205.13482, §4.2 to 4.4, eqs. (12) to (24), pp. 5–6, and Algorithm 1, p. 10):
790    /// for each integer variable, moves the mean and stretches `A` so that a draw keeps at least
791    /// a chance `α` of leaving the mean's value, after the update of `m`, `σ` and `C`, and before
792    /// the next draw. The paths and `C` never see it.
793    fn correct_margin(&mut self) {
794        let n = self.n;
795        let alpha = self.alpha;
796        for j in 0..n {
797            let variable = &self.variables[j];
798            if !variable.is_integer() {
799                continue;
800            }
801            // The draw's spread in this variable, in its own units.
802            let spread_unit = self.sigma * self.scale[j] * self.c[j * n + j].sqrt();
803            let spread = self.a[j] * spread_unit;
804            if !(spread > 0.0 && spread.is_finite()) {
805                // σ or C has collapsed or overflowed: the run stops at `Stop::Condition`.
806                continue;
807            }
808            let m = self.mean[j];
809            // The thresholds sit halfway between neighbouring values.
810            let first = variable.low() + 0.5;
811            let last = variable.high() - 0.5;
812            if m <= first || m > last {
813                // At an end value (and always for two values), eq. (13): keep the mean within
814                // Φ⁻¹(1 − α) spreads of the threshold, so the far side keeps a chance α. A is
815                // unchanged, eq. (14).
816                let threshold = if m <= first { first } else { last };
817                // `−Φ⁻¹(α)`: `1 − α` would round first.
818                let reach = -normal::quantile(alpha) * spread;
819                if (m - threshold).abs() <= reach {
820                    continue;
821                }
822                let mut mean = threshold + reach.copysign(m - threshold);
823                // The sum rounds; never land farther from the threshold than `reach`.
824                if (mean - threshold).abs() > reach {
825                    mean = if mean > threshold {
826                        mean.next_down()
827                    } else {
828                        mean.next_up()
829                    };
830                }
831                self.mean[j] = mean;
832                continue;
833            }
834            // Inside, eqs. (15) to (24): the thresholds either side of the mean.
835            let value = super::nearest_whole(m);
836            let (low, up) = (value - 0.5, value + 0.5);
837            let p_low = normal::cdf((low - m) / spread);
838            let p_up = normal::cdf((m - up) / spread);
839            let half = alpha / 2.0;
840            if p_low >= half && p_up >= half {
841                // Eqs. (20) to (24) would give back the same mean and A.
842                continue;
843            }
844            let p_mid = 1.0 - p_low - p_up;
845            // Eqs. (20) to (23): each side at least α/2, the excess taken back in proportion.
846            let (q_low, q_up) = (p_low.max(half), p_up.max(half));
847            let excess = 1.0 - q_low - q_up - p_mid;
848            let share = q_low + q_up + p_mid - 3.0 * half;
849            let r_low = q_low + excess * (q_low - half) / share;
850            let r_up = q_up + excess * (q_up - half) / share;
851            // Eq. (24): the mean and A that give the sides these chances.
852            // `Φ⁻¹(1 − r) = −Φ⁻¹(r)`, without rounding `1 − r` first.
853            let chi_low = -normal::quantile(r_low);
854            let chi_up = -normal::quantile(r_up);
855            let mean = (low * chi_up + up * chi_low) / (chi_low + chi_up);
856            let a = (up - low) / ((chi_low + chi_up) * spread_unit);
857            if mean.is_finite() && a.is_finite() && a > 0.0 {
858                self.mean[j] = mean;
859                self.a[j] = a;
860            }
861        }
862    }
863
864    /// Why the run should stop now, if it should; `values` are this generation's feasible ones.
865    fn check_stop(&self, values: &[f64]) -> Option<Stop> {
866        let n = self.n;
867        if let (Some(target), Some((_, best, _))) = (self.target, &self.best)
868            && best.is_feasible()
869            && best.value <= target
870        {
871            return Some(Stop::Target);
872        }
873        if self.evaluations >= self.max_evaluations {
874            return Some(Stop::Evaluations);
875        }
876        let d_max = self.d.iter().copied().fold(0.0, f64::max);
877        let d_min = self.d.iter().copied().fold(f64::INFINITY, f64::min);
878        // Written so that a NaN in D or σ stops the run too.
879        // And in the variables' own units, where a step above 1 overflows first.
880        let finite = (0..n).all(|i| {
881            (self.sigma * self.c[i * n + i].sqrt() * self.scale[i] * self.a[i]).is_finite()
882                && self.mean[i].is_finite()
883        });
884        if !(d_min > 0.0
885            && d_max / d_min <= MAX_CONDITION.sqrt()
886            && (self.sigma * d_max).is_finite()
887            && finite)
888        {
889            return Some(Stop::Condition);
890        }
891        // In the scaled variables, so the tolerance is a fraction of each variable's step.
892        let converged_x = (0..n).all(|i| {
893            self.sigma * self.c[i * n + i].sqrt() < self.tolerance_x
894                && self.sigma * self.p_c[i].abs() < self.tolerance_x
895        });
896        if converged_x {
897            return Some(Stop::TolX);
898        }
899        // Cast: ⌈30 n/λ⌉ is small; n ≤ 200 and λ ≥ 2.
900        let window = 10 + (30.0 * n as f64 / self.lambda as f64).ceil() as usize;
901        if self.history.len() >= window && !values.is_empty() {
902            let bests = &self.history[self.history.len() - window..];
903            if !bests.iter().all(Evaluation::is_feasible) {
904                return None;
905            }
906            let recent = bests.iter().map(|e| &e.value).chain(values);
907            let (low, high) = recent.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
908                (lo.min(v), hi.max(v))
909            });
910            if high - low < self.tolerance_value {
911                return Some(Stop::TolFun);
912            }
913        }
914        None
915    }
916
917    /// Draws the next generation's candidates, each from its own stream. Returns why the run must
918    /// stop instead: [`Stop::Bounds`] if one can't be drawn inside the bounds, [`Stop::Condition`]
919    /// if one overflows.
920    fn draw(&mut self) -> Option<Stop> {
921        let n = self.n;
922        self.candidates.clear();
923        self.steps.clear();
924        // B D, once a generation.
925        let mut bd = self.b.clone();
926        for row in bd.chunks_exact_mut(n) {
927            for (x, d) in row.iter_mut().zip(&self.d) {
928                *x *= d;
929            }
930        }
931        let mut z = vec![0.0; n];
932        let mut y = vec![0.0; n];
933        let mut x = vec![0.0; n];
934        for k in 0..self.lambda {
935            // Casts: a generation and a candidate's place are far below 2⁶⁴.
936            let mut rng = SeededRng::for_stream(self.seed, &[self.generation as u64, k as u64]);
937            let mut drawn = None;
938            for _ in 0..MAX_DRAWS {
939                z.iter_mut().for_each(|zi| *zi = rng.standard_normal());
940                for (yi, row) in y.iter_mut().zip(bd.chunks_exact(n)) {
941                    *yi = row.iter().zip(&z).map(|(a, b)| a * b).sum();
942                }
943                for i in 0..n {
944                    x[i] = self.mean[i] + self.sigma * self.scale[i] * self.a[i] * y[i];
945                }
946                if !x.iter().all(|xi| xi.is_finite()) {
947                    drawn = Some(Stop::Condition);
948                    break;
949                }
950                // An integer variable's draw is never outside: its encoding clamps it.
951                if x.iter()
952                    .zip(&self.variables)
953                    .all(|(xi, v)| v.is_integer() || v.contains(*xi))
954                {
955                    drawn = None;
956                    self.candidates.push(
957                        x.iter()
958                            .zip(&self.variables)
959                            .map(|(xi, v)| v.encode(*xi))
960                            .collect(),
961                    );
962                    self.steps.push(y.clone());
963                    break;
964                }
965                drawn = Some(Stop::Bounds);
966            }
967            if drawn.is_some() {
968                self.candidates.clear();
969                self.steps.clear();
970                return drawn;
971            }
972        }
973        None
974    }
975
976    /// What the run has found, stopped for `stop`.
977    fn optimum(&mut self, stop: Stop) -> Optimum {
978        self.candidates.clear();
979        self.steps.clear();
980        let n = self.n;
981        // A NaN here can only come of an overflowed σ times a zero: count it as infinite, as
982        // `f64::max` would drop it.
983        let spread = (0..n)
984            .map(|i| self.sigma * self.c[i * n + i].sqrt() * self.scale[i] * self.a[i])
985            .map(|t| if t.is_nan() { f64::INFINITY } else { t })
986            .fold(0.0, f64::max);
987        // A run is only told after a generation is evaluated, so there is a best point; the
988        // default is never used.
989        let (point, best, evaluation) =
990            self.best
991                .clone()
992                .unwrap_or((Vec::new(), Evaluation::feasible(f64::INFINITY), 0));
993        Optimum {
994            point,
995            value: best.value,
996            violation: best.violation,
997            evaluation,
998            evaluations: self.evaluations,
999            generations: self.generation,
1000            mean: self.mean.clone(),
1001            spread,
1002            stop,
1003        }
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010    use crate::optimize::benchmark::constrained::sphere_above;
1011    use crate::optimize::benchmark::sphere;
1012
1013    /// Told violations of zero, a constrained run is the plain one, bit for bit. (`tell` goes
1014    /// through `tell_constrained`; `tests/optimize.rs` pins that path to pycma and to a dense
1015    /// recomputation.)
1016    #[test]
1017    fn zero_violations_change_nothing() {
1018        let cmaes = Cmaes::new(variables(5, 1.0, 0.5)).unwrap();
1019        let plain = cmaes.minimize(7, sphere).unwrap();
1020        let constrained = cmaes
1021            .minimize_constrained(7, |x| Evaluation::feasible(sphere(x)))
1022            .unwrap();
1023        assert_eq!(plain, constrained);
1024    }
1025
1026    #[test]
1027    fn violations_must_not_be_negative_or_nan() {
1028        let cmaes = Cmaes::new(variables(2, 1.0, 0.5)).unwrap();
1029        for bad in [-1e-300, f64::NAN, f64::NEG_INFINITY] {
1030            let mut run = cmaes.start(1).unwrap();
1031            let told: Vec<Evaluation> = run
1032                .candidates()
1033                .iter()
1034                .map(|_| Evaluation {
1035                    value: 1.0,
1036                    violation: bad,
1037                })
1038                .collect();
1039            match run.tell_constrained(&told) {
1040                Err(AnalysisError::Domain { what, value }) => {
1041                    assert!(what.starts_with("constraint violation"));
1042                    assert!(value.total_cmp(&bad).is_eq());
1043                }
1044                other => panic!("{bad}: {other:?}"),
1045            }
1046        }
1047    }
1048
1049    /// A NaN or −∞ isn't written as JSON's `null`, which reads back as a failure's `+∞`.
1050    #[test]
1051    fn json_refuses_what_would_read_back_as_a_failure() {
1052        for bad in [f64::NAN, f64::NEG_INFINITY] {
1053            let e = Evaluation {
1054                value: 1.0,
1055                violation: bad,
1056            };
1057            assert!(serde_json::to_string(&e).is_err(), "{bad}");
1058            assert!(
1059                serde_json::to_string(&Evaluation::feasible(bad)).is_err(),
1060                "{bad}"
1061            );
1062        }
1063    }
1064
1065    /// Failed candidates rank last: a run where every one fails reports `+∞` for both, and
1066    /// survives JSON; a run where some fail finds the minimum among the rest.
1067    #[test]
1068    fn failed_candidates_rank_last() {
1069        let cmaes = Cmaes::new(variables(3, 3.0, 1.0))
1070            .unwrap()
1071            .with_max_evaluations(300)
1072            .unwrap();
1073        let none = cmaes
1074            .minimize_constrained(5, |_| Evaluation::failed())
1075            .unwrap();
1076        assert_eq!((none.value, none.violation), (f64::INFINITY, f64::INFINITY));
1077        let json = serde_json::to_string(&none).unwrap();
1078        assert_eq!(serde_json::from_str::<Optimum>(&json).unwrap(), none);
1079        // Every candidate with x₁ < 0 fails; the minimum (1, 0, 0) is on that edge.
1080        let cmaes = Cmaes::new(variables(3, 3.0, 1.0))
1081            .unwrap()
1082            .with_max_evaluations(20_000)
1083            .unwrap();
1084        let some = cmaes
1085            .minimize_constrained(5, |x| {
1086                if x[1] < 0.0 {
1087                    Evaluation::failed()
1088                } else {
1089                    sphere_above(x)
1090                }
1091            })
1092            .unwrap();
1093        assert_eq!(some.violation, 0.0);
1094        assert!(some.point[1] >= 0.0);
1095        assert!((some.value - 1.0).abs() < 1e-8, "{}", some.value);
1096    }
1097
1098    /// A target counts only for a point that keeps the constraints: an infeasible candidate
1099    /// far below the target doesn't stop the run.
1100    #[test]
1101    fn a_target_needs_a_feasible_point() {
1102        let cmaes = Cmaes::new(variables(3, 0.0, 1.0))
1103            .unwrap()
1104            .with_target(1.5)
1105            .unwrap();
1106        let optimum = cmaes.minimize_constrained(3, sphere_above).unwrap();
1107        assert_eq!(optimum.stop, Stop::Target);
1108        assert_eq!(optimum.violation, 0.0);
1109        assert!(optimum.value <= 1.5 && optimum.point[0] >= 1.0);
1110    }
1111
1112    fn variables(n: usize, start: f64, step: f64) -> Vec<Variable> {
1113        (0..n)
1114            .map(|i| Variable::new(format!("x{i}"), start, step).unwrap())
1115            .collect()
1116    }
1117
1118    /// Table 1 at n = 10, λ = 10, worked by hand: μ = 5, w' = ln 5.5 − ln i.
1119    #[test]
1120    fn parameters_follow_table_1() {
1121        let p = Parameters::new(10, 10);
1122        assert_eq!(p.mu, 5);
1123        let raw: Vec<f64> = (1..=5).map(|i| 5.5f64.ln() - (i as f64).ln()).collect();
1124        let sum: f64 = raw.iter().sum();
1125        for (w, r) in p.weights.iter().zip(&raw) {
1126            assert!((w - r / sum).abs() <= 1e-16);
1127        }
1128        assert!((p.weights.iter().sum::<f64>() - 1.0).abs() <= 1e-15);
1129        let mu_eff = sum * sum / raw.iter().map(|r| r * r).sum::<f64>();
1130        assert!((p.mu_eff - mu_eff).abs() <= 1e-13);
1131        assert!((p.c_sigma - (mu_eff + 2.0) / (15.0 + mu_eff)).abs() <= 1e-16);
1132        assert!((p.c_1 - 2.0 / (11.3f64.powi(2) + mu_eff)).abs() <= 1e-16);
1133        // μ_eff < 11 here, so d_σ's max term is zero.
1134        assert!((p.d_sigma - (1.0 + p.c_sigma)).abs() <= 1e-16);
1135    }
1136
1137    /// Ranking is all that matters: a run on `f` and on `exp(f)` take the same steps.
1138    #[test]
1139    fn invariant_under_an_increasing_transformation() {
1140        let cmaes = Cmaes::new(variables(4, 1.0, 0.5))
1141            .unwrap()
1142            .with_max_evaluations(400)
1143            .unwrap();
1144        let a = cmaes.minimize(3, sphere).unwrap();
1145        let b = cmaes.minimize(3, |x| sphere(x).sqrt().exp()).unwrap();
1146        assert_eq!(a.point, b.point);
1147        assert_eq!(a.mean, b.mean);
1148        assert_eq!(a.evaluations, b.evaluations);
1149    }
1150
1151    /// The same seed gives the same run bit for bit; another seed another run.
1152    #[test]
1153    fn a_seed_fixes_the_run() {
1154        let cmaes = Cmaes::new(variables(3, 2.0, 1.0)).unwrap();
1155        let a = cmaes.minimize(11, sphere).unwrap();
1156        let b = cmaes.minimize(11, sphere).unwrap();
1157        let c = cmaes.minimize(12, sphere).unwrap();
1158        assert_eq!(a, b);
1159        assert_ne!(a.point, c.point);
1160    }
1161
1162    /// Bounds hold every candidate, and a run whose best point is inside still finds it.
1163    #[test]
1164    fn bounds_hold_every_candidate() {
1165        let vars: Vec<Variable> = (0..3)
1166            .map(|i| {
1167                Variable::new(format!("x{i}"), 1.5, 1.0)
1168                    .unwrap()
1169                    .within(-0.5, 2.0)
1170                    .unwrap()
1171            })
1172            .collect();
1173        let cmaes = Cmaes::new(vars).unwrap().with_target(1e-12).unwrap();
1174        let mut run = cmaes.start(5).unwrap();
1175        let optimum = loop {
1176            for x in run.candidates() {
1177                assert!(x.iter().all(|xi| (-0.5..=2.0).contains(xi)), "{x:?}");
1178            }
1179            let values: Vec<f64> = run.candidates().iter().map(|x| sphere(x)).collect();
1180            if let Some(optimum) = run.tell(&values).unwrap() {
1181                break optimum;
1182            }
1183        };
1184        assert_eq!(optimum.stop, Stop::Target);
1185        assert!(optimum.value <= 1e-12);
1186    }
1187
1188    /// Steps far larger than the bounds can't draw a first candidate: an error, not a hang.
1189    #[test]
1190    fn steps_too_large_for_the_bounds_are_refused() {
1191        let vars: Vec<Variable> = (0..4)
1192            .map(|i| {
1193                Variable::new(format!("x{i}"), 0.0, 1e6)
1194                    .unwrap()
1195                    .within(-1e-6, 1e-6)
1196                    .unwrap()
1197            })
1198            .collect();
1199        let error = Cmaes::new(vars).unwrap().start(1).unwrap_err();
1200        assert!(matches!(error, AnalysisError::OutOfBounds { draws } if draws == MAX_DRAWS));
1201    }
1202
1203    /// A NaN is refused with its evaluation's index; `+∞` ranks last; a wrong count is refused.
1204    #[test]
1205    fn values_are_checked() {
1206        let cmaes = Cmaes::new(variables(2, 1.0, 0.5)).unwrap();
1207        let mut run = cmaes.start(1).unwrap();
1208        let lambda = run.candidates().len();
1209        assert_eq!(lambda, 6);
1210        let mut values = vec![1.0; lambda];
1211        values[4] = f64::NAN;
1212        assert!(matches!(
1213            run.tell(&values),
1214            Err(AnalysisError::Output { index: 4, .. })
1215        ));
1216        values[4] = f64::NEG_INFINITY;
1217        assert!(run.tell(&values).is_err());
1218        assert!(matches!(
1219            run.tell(&values[..3]),
1220            Err(AnalysisError::Length { length: 3, .. })
1221        ));
1222        values[4] = f64::INFINITY;
1223        assert!(run.tell(&values).unwrap().is_none());
1224        assert_eq!(run.generation(), 1);
1225        let values = vec![1.0; lambda];
1226        assert!(matches!(run.tell(&values), Ok(None)));
1227        // The second generation's evaluations are counted from the first's end.
1228        let mut values = vec![1.0; lambda];
1229        values[0] = f64::NAN;
1230        assert!(matches!(
1231            run.tell(&values),
1232            Err(AnalysisError::Output { index, .. }) if index == 2 * lambda
1233        ));
1234    }
1235
1236    /// A flat output ends the run by the value tolerance; a run out of evaluations says so.
1237    #[test]
1238    fn stops_are_reported() {
1239        let cmaes = Cmaes::new(variables(2, 1.0, 0.5)).unwrap();
1240        let flat = cmaes.minimize(1, |_| 7.0).unwrap();
1241        assert_eq!(flat.stop, Stop::TolFun);
1242        // Window 10 + ⌈60/6⌉ = 20 generations of history, checked from the 20th.
1243        assert_eq!(flat.generations, 20);
1244        let short = cmaes
1245            .clone()
1246            .with_max_evaluations(30)
1247            .unwrap()
1248            .minimize(1, sphere)
1249            .unwrap();
1250        assert_eq!(short.stop, Stop::Evaluations);
1251        assert_eq!(short.evaluations, 30);
1252        let converged = cmaes
1253            .with_tolerance_x(1e-6)
1254            .unwrap()
1255            .with_tolerance_value(0.0)
1256            .unwrap()
1257            .minimize(1, sphere)
1258            .unwrap();
1259        assert_eq!(converged.stop, Stop::TolX);
1260        // σ √Cᵢᵢ below 10⁻⁶ in the scaled variables: 10⁻⁶ times the step, 0.5.
1261        assert!(converged.spread < 1e-6 * 0.5);
1262    }
1263
1264    /// Settings are checked.
1265    #[test]
1266    fn settings_are_checked() {
1267        let cmaes = Cmaes::new(variables(2, 0.0, 1.0)).unwrap();
1268        assert_eq!(cmaes.population(), 6);
1269        assert!(cmaes.clone().with_population(1).is_err());
1270        assert!(cmaes.clone().with_population(MAX_POPULATION + 1).is_err());
1271        assert!(cmaes.clone().with_max_evaluations(0).is_err());
1272        assert!(cmaes.clone().with_target(f64::NAN).is_err());
1273        assert!(cmaes.clone().with_target(f64::INFINITY).is_err());
1274        assert!(cmaes.clone().with_tolerance_x(-1.0).is_err());
1275        assert!(cmaes.clone().with_tolerance_value(f64::INFINITY).is_err());
1276        assert!(Cmaes::new(Vec::new()).is_err());
1277    }
1278
1279    /// Settings read back through their checks: a round trip, and refusals.
1280    #[test]
1281    fn settings_serialize_and_are_checked_on_reading() {
1282        let cmaes = Cmaes::new(variables(2, 0.0, 1.0))
1283            .unwrap()
1284            .with_target(0.5)
1285            .unwrap();
1286        let json = serde_json::to_string(&cmaes).unwrap();
1287        assert_eq!(serde_json::from_str::<Cmaes>(&json).unwrap(), cmaes);
1288        for (field, bad) in [
1289            ("\"population\":6", "\"population\":1"),
1290            ("\"max_evaluations\":10000", "\"max_evaluations\":0"),
1291            ("\"tolerance_x\":1e-12", "\"tolerance_x\":-1.0"),
1292        ] {
1293            assert!(json.contains(field), "{json}");
1294            let edited = json.replace(field, bad);
1295            assert!(serde_json::from_str::<Cmaes>(&edited).is_err(), "{edited}");
1296        }
1297        let empty = json.replace(
1298            &json[json.find('[').unwrap()..=json.find(']').unwrap()],
1299            "[]",
1300        );
1301        assert!(serde_json::from_str::<Cmaes>(&empty).is_err(), "{empty}");
1302        let extra = json.replacen('{', "{\"extra\":1,", 1);
1303        assert!(serde_json::from_str::<Cmaes>(&extra).is_err());
1304    }
1305
1306    /// The smallest populations, `λ` = 2 and 3 (`μ` = 1), still converge on the sphere.
1307    #[test]
1308    fn smallest_populations_run() {
1309        for lambda in [2, 3] {
1310            let optimum = Cmaes::new(variables(2, 1.0, 0.5))
1311                .unwrap()
1312                .with_population(lambda)
1313                .unwrap()
1314                .with_target(1e-10)
1315                .unwrap()
1316                .minimize(1, sphere)
1317                .unwrap();
1318            assert_eq!(optimum.stop, Stop::Target, "λ = {lambda}");
1319        }
1320    }
1321
1322    /// A covariance stretched past 10¹⁴ ends the run: an ellipse whose curvatures differ by 10¹⁶.
1323    #[test]
1324    fn stops_at_the_condition_limit() {
1325        let optimum = Cmaes::new(variables(2, 1.0, 0.5))
1326            .unwrap()
1327            .with_tolerance_x(0.0)
1328            .unwrap()
1329            .with_tolerance_value(0.0)
1330            .unwrap()
1331            .minimize(1, |x| x[0] * x[0] + 1e16 * x[1] * x[1])
1332            .unwrap();
1333        assert_eq!(optimum.stop, Stop::Condition);
1334    }
1335
1336    /// Steps 10⁸ apart, on a sphere scaled to match: the run works in the scaled variables, so it
1337    /// converges (a run on the unscaled covariance stopped at its condition limit after one
1338    /// generation), and the x tolerance is a fraction of each variable's own step.
1339    #[test]
1340    fn steps_far_apart_converge_alike() {
1341        let steps = [1e-4, 1e4];
1342        let vars = vec![
1343            Variable::new("small", steps[0], steps[0]).unwrap(),
1344            Variable::new("large", steps[1], steps[1]).unwrap(),
1345        ];
1346        let model = |x: &[f64]| (x[0] / 1e-4).powi(2) + (x[1] / 1e4).powi(2);
1347        let cmaes = Cmaes::new(vars)
1348            .unwrap()
1349            .with_tolerance_x(1e-6)
1350            .unwrap()
1351            .with_tolerance_value(0.0)
1352            .unwrap();
1353        let mut run = cmaes.start(1).unwrap();
1354        let optimum = loop {
1355            let values: Vec<f64> = run.candidates().iter().map(|x| model(x)).collect();
1356            if let Some(optimum) = run.tell(&values).unwrap() {
1357                break optimum;
1358            }
1359        };
1360        assert_eq!(optimum.stop, Stop::TolX);
1361        assert!(optimum.value < 1e-10, "{}", optimum.value);
1362        for i in 0..2 {
1363            let sd = run.sigma() * run.covariance()[i * 2 + i].sqrt();
1364            assert!(sd < 1e-6, "variable {i}: {sd} of its step");
1365        }
1366        assert!(optimum.spread < 1e-6 * steps[1]);
1367    }
1368
1369    /// A run whose every value is `+∞` gives an infinity that reads back; so does an infinite
1370    /// spread. A run that diverges stops as `Condition`.
1371    #[test]
1372    fn infinite_results_read_back() {
1373        let cmaes = Cmaes::new(variables(1, 0.0, 1.0))
1374            .unwrap()
1375            .with_max_evaluations(50)
1376            .unwrap();
1377        let failed = cmaes.minimize(1, |_| f64::INFINITY).unwrap();
1378        assert_eq!(failed.value, f64::INFINITY);
1379        let json = serde_json::to_string(&failed).unwrap();
1380        assert!(json.contains("\"value\":null"), "{json}");
1381        assert_eq!(serde_json::from_str::<Optimum>(&json).unwrap(), failed);
1382        let overflowed = Optimum {
1383            spread: f64::INFINITY,
1384            ..failed.clone()
1385        };
1386        let json = serde_json::to_string(&overflowed).unwrap();
1387        assert!(json.contains("\"spread\":null"), "{json}");
1388        assert_eq!(serde_json::from_str::<Optimum>(&json).unwrap(), overflowed);
1389        // Unbounded below and a step above 1: a candidate overflows before σ does, and the run
1390        // says so as `Condition`, not `Bounds`.
1391        let vars = vec![Variable::new("x", 0.0, 1000.0).unwrap()];
1392        let diverged = Cmaes::new(vars)
1393            .unwrap()
1394            .with_max_evaluations(1_000_000)
1395            .unwrap()
1396            .minimize(7, |x| x[0])
1397            .unwrap();
1398        assert_eq!(diverged.stop, Stop::Condition);
1399    }
1400
1401    /// Steps so large that a first candidate overflows are refused.
1402    #[test]
1403    fn overflowing_first_candidates_are_refused() {
1404        let vars = vec![Variable::new("x", 0.0, 1e308).unwrap()];
1405        let error = Cmaes::new(vars).unwrap().start(1).unwrap_err();
1406        assert!(matches!(
1407            error,
1408            AnalysisError::Domain { what, .. } if what.starts_with("first candidate")
1409        ));
1410    }
1411
1412    /// After every generation, each integer variable's next draw keeps the margin: at least `α`
1413    /// of falling past the threshold nearest an end value's mean (eq. (13)), and at least `α/2`
1414    /// on each side of an inner value (eqs. (20), (21)), with the spread `σ A s √C_jj` the draw
1415    /// uses. The run mixes an end-bound binary, an integer from −10 to 10 driven to an end, one
1416    /// driven to the middle, and two continuous variables.
1417    #[test]
1418    fn integer_draws_keep_the_margin() {
1419        let variables = vec![
1420            Variable::new("x0", 1.0, 0.5).unwrap(),
1421            Variable::new("x1", -2.0, 0.5).unwrap(),
1422            Variable::new("binary", 0.5, 1.0)
1423                .unwrap()
1424                .within(0.0, 1.0)
1425                .unwrap()
1426                .integer()
1427                .unwrap(),
1428            Variable::new("to an end", 3.0, 1.0)
1429                .unwrap()
1430                .within(-10.0, 10.0)
1431                .unwrap()
1432                .integer()
1433                .unwrap(),
1434            Variable::new("inner", -4.0, 2.0)
1435                .unwrap()
1436                .within(-10.0, 10.0)
1437                .unwrap()
1438                .integer()
1439                .unwrap(),
1440        ];
1441        let f = |x: &[f64]| {
1442            x[0] * x[0] + x[1] * x[1] + (1.0 - x[2]) + (x[3] - 10.0).abs() + (x[4] - 2.0).powi(2)
1443        };
1444        let mut run = Cmaes::new(variables).unwrap().start(3).unwrap();
1445        let n = 5;
1446        let alpha = run.integer_margin();
1447        assert_eq!(alpha, 1.0 / (5.0 * 8.0));
1448        let mut generations = 0;
1449        let mut corrected = 0;
1450        loop {
1451            let values: Vec<f64> = run.candidates().iter().map(|x| f(x)).collect();
1452            for x in run.candidates() {
1453                assert!(x[2] == 0.0 || x[2] == 1.0);
1454                assert!(x[3].fract() == 0.0 && (-10.0..=10.0).contains(&x[3]));
1455            }
1456            let done = run.tell(&values).unwrap();
1457            if done.is_some() {
1458                break;
1459            }
1460            generations += 1;
1461            for (j, (low, high)) in [(2, (0.0, 1.0)), (3, (-10.0, 10.0)), (4, (-10.0, 10.0))] {
1462                let step = run.variables[j].step();
1463                let spread =
1464                    run.sigma() * run.margin_scale()[j] * step * run.covariance()[j * n + j].sqrt();
1465                let m = run.mean()[j];
1466                let (first, last) = (low + 0.5, high - 0.5);
1467                // Φ and Φ⁻¹ are good to a few parts in 10¹⁶, and a relative error `δ` in a
1468                // tail's `x` moves the tail by about `x² δ` of itself; `x` here is under 3.
1469                let floor = |p: f64| p * (1.0 - 1e-13);
1470                if m <= first || m > last {
1471                    let threshold = if m <= first { first } else { last };
1472                    let far = normal::cdf(-(m - threshold).abs() / spread);
1473                    assert!(
1474                        far >= floor(alpha),
1475                        "gen {generations} x{j}: {far} < {alpha}"
1476                    );
1477                } else {
1478                    let value = crate::optimize::nearest_whole(m);
1479                    let below = normal::cdf((value - 0.5 - m) / spread);
1480                    let above = normal::cdf((m - value - 0.5) / spread);
1481                    assert!(
1482                        below >= floor(alpha / 2.0),
1483                        "gen {generations} x{j}: {below}"
1484                    );
1485                    assert!(
1486                        above >= floor(alpha / 2.0),
1487                        "gen {generations} x{j}: {above}"
1488                    );
1489                }
1490                if run.margin_scale()[j] != 1.0 {
1491                    corrected += 1;
1492                }
1493            }
1494            assert_eq!(&run.margin_scale()[..2], &[1.0, 1.0]);
1495        }
1496        // The run went long enough for the margin to bind: σ shrank and A grew.
1497        assert!(generations > 50, "{generations}");
1498        assert!(corrected > 0);
1499    }
1500
1501    /// The correction at a set state against the paper's equations evaluated in 40-digit
1502    /// arithmetic (mpmath's `ncdf` and `erfinv`), with `n = 2`, `λ = 6`, so `α = 1/12`: an inner
1503    /// mean whose lower side has less than `α/2` and upper side more, an end-value mean too far
1504    /// from its threshold, and an inner mean with both sides above `α/2`, which is left alone.
1505    #[test]
1506    #[expect(
1507        clippy::excessive_precision,
1508        reason = "17 digits of each 40-digit reference, which round to the nearest f64"
1509    )]
1510    fn margin_correction_matches_the_equations() {
1511        let variables = vec![
1512            Variable::new("x", 0.0, 1.0).unwrap(),
1513            Variable::new("k", 0.0, 1.0)
1514                .unwrap()
1515                .within(-10.0, 10.0)
1516                .unwrap()
1517                .integer()
1518                .unwrap(),
1519        ];
1520        let fresh = || {
1521            let mut run = Cmaes::new(variables.clone()).unwrap().start(1).unwrap();
1522            run.c = vec![1.0, 0.0, 0.0, 1.0];
1523            run
1524        };
1525        assert_eq!(fresh().integer_margin(), 1.0 / 12.0);
1526        let close = |got: f64, want: f64| (got - want).abs() <= 1e-14 * want.abs();
1527
1528        // Inside, eqs. (17) to (24): spread 2 × 0.125, `P(below 1.5) = 6.9e-4`,
1529        // `P(above 2.5) = 0.212`; the lower side is lifted to α/2 and the upper gives way.
1530        let mut run = fresh();
1531        (run.mean[1], run.sigma, run.a[1]) = (2.3, 0.125, 2.0);
1532        run.correct_margin();
1533        assert!(
1534            close(run.mean[1], 2.176_899_187_754_804_1),
1535            "{}",
1536            run.mean[1]
1537        );
1538        assert!(close(run.a[1], 3.127_161_079_343_547_5), "{}", run.a[1]);
1539        assert_eq!((run.mean[0], run.a[0]), (0.0, 1.0));
1540
1541        // At the low end, eq. (13): the mean is drawn to `Φ⁻¹(1 − α)` spreads of −9.5.
1542        let mut run = fresh();
1543        (run.mean[1], run.sigma) = (-9.8, 0.1);
1544        run.correct_margin();
1545        assert!(
1546            close(run.mean[1], -9.638_299_412_710_063_8),
1547            "{}",
1548            run.mean[1]
1549        );
1550        assert_eq!(run.a[1], 1.0);
1551
1552        // An end mean never lands farther from its threshold than the reach, though
1553        // `threshold ± reach` rounds outward for some spreads: those are stepped back an ulp.
1554        let reach_per_spread = -normal::quantile(1.0 / 12.0);
1555        let mut stepped = 0;
1556        for k in 0..2000 {
1557            let sigma = 0.01 + 1e-4 * f64::from(k);
1558            for (m, threshold) in [(-9.9, -9.5), (9.9, 9.5)] {
1559                let mut run = fresh();
1560                (run.mean[1], run.sigma) = (m, sigma);
1561                run.correct_margin();
1562                let reach = reach_per_spread * sigma;
1563                let gap = (run.mean[1] - threshold).abs();
1564                assert!(gap <= reach, "σ = {sigma}: {gap} > {reach}");
1565                assert!(reach - gap <= 4.0 * f64::EPSILON * 10.0, "σ = {sigma}");
1566                if (threshold + reach.copysign(m - threshold) - threshold).abs() > reach {
1567                    stepped += 1;
1568                }
1569            }
1570        }
1571        assert!(stepped > 0, "no spread rounded outward");
1572
1573        // A mean just above −0.5 stands for 0: its lower side is lifted, and it stays above.
1574        let mut run = fresh();
1575        (run.mean[1], run.sigma) = (-0.499_999_999_999_999_94, 0.05);
1576        run.correct_margin();
1577        assert!(run.mean[1] > -0.5 && run.a[1] > 1.0, "{}", run.mean[1]);
1578
1579        // Both sides above α/2, or an end mean already near its threshold: nothing moves.
1580        for (m, sigma) in [(2.3, 1.0), (-9.6, 0.1)] {
1581            let mut run = fresh();
1582            (run.mean[1], run.sigma) = (m, sigma);
1583            run.correct_margin();
1584            assert_eq!((run.mean[1], run.a[1]), (m, 1.0));
1585        }
1586    }
1587
1588    /// An integer variable's draws are encoded before the model sees them; the best point is
1589    /// encoded too, and the run ends at the integer minimum exactly.
1590    #[test]
1591    fn integer_runs_reach_whole_minima() {
1592        let variables = vec![
1593            Variable::new("x", 2.5, 1.0).unwrap(),
1594            Variable::new("k", 2.3, 1.0)
1595                .unwrap()
1596                .within(-10.0, 10.0)
1597                .unwrap()
1598                .integer()
1599                .unwrap(),
1600        ];
1601        let optimum = Cmaes::new(variables)
1602            .unwrap()
1603            .with_target(1e-12)
1604            .unwrap()
1605            .minimize(5, |x| x[0] * x[0] + (x[1] - 3.0).powi(2))
1606            .unwrap();
1607        assert_eq!(optimum.stop, Stop::Target);
1608        assert_eq!(optimum.point[1], 3.0);
1609    }
1610}