Skip to main content

hpr_analysis/optimize/
nsga2.rs

1//! NSGA-II, the non-dominated sorting genetic algorithm II: a search for the designs that trade
2//! two or more goals off against each other, the *Pareto front*.
3//!
4//! A design *dominates* another when it is no worse in any goal and better in at least one. The
5//! designs no other design dominates are the Pareto front: along it, one goal can only be
6//! improved by giving up another. NSGA-II keeps a population of designs, breeds a new generation
7//! from them, and keeps the best half of parents and children together. They are ranked first by
8//! how many fronts deep they lie, then, within a front, by how far they are from their
9//! neighbours, so the front it finds spreads along the true one instead of bunching at one place.
10//!
11//! The method is K. Deb, A. Pratap, S. Agarwal and T. Meyarivan, "A fast and elitist
12//! multiobjective genetic algorithm: NSGA-II", *IEEE Transactions on Evolutionary Computation*
13//! 6(2), 182–197 (2002), <https://doi.org/10.1109/4235.996017>:
14//!
15//! - **Fast non-dominated sorting** (§III-A, p. 184): each design's count of designs dominating
16//!   it, and the set it dominates; the designs with a count of zero are the first front, and
17//!   removing them gives the next. Rank 0 here is the paper's front 1.
18//! - **Crowding distance** (§III-B, p. 185): in each front and for each goal, the designs sorted
19//!   by that goal; the two ends get an infinite distance, and every other design adds the gap
20//!   between its two neighbours' values divided by the goal's range in the front,
21//!   `(f_m(i+1) − f_m(i−1)) / (f_m^max − f_m^min)`.
22//! - **Crowded comparison** (p. 185): a lower rank wins; in one rank, the larger distance wins.
23//! - **The main loop** (§III-C, p. 186): parents and children together, `2N` designs, are
24//!   sorted into fronts; whole fronts fill the next `N` in rank order, and the front that doesn't
25//!   fit is cut by crowding distance, largest first.
26//! - **Constraints** (§VI, p. 192): design `i` *constrained-dominates* `j` if `i` keeps every
27//!   constraint and `j` doesn't, if neither does and `i`'s overall violation is smaller, or if both
28//!   do and `i` dominates `j`.
29//!
30//! Children come from parents chosen by binary tournaments with the crowded comparison, two at a
31//! time, by the paper's real-coded operators (§IV, p. 187):
32//!
33//! - **Simulated binary crossover** (SBX; K. Deb and R. B. Agrawal, "Simulated binary crossover
34//!   for continuous search space", *Complex Systems* 9(2), 115–148 (1995)): a child's spread
35//!   factor `β = |c₂ − c₁| / |x₂ − x₁|` has density `½(η_c + 1) β^η_c` for `β ≤ 1` and
36//!   `½(η_c + 1) / β^(η_c+2)` above (eqs. 19–20, pp. 125–126). Each variable of a crossing pair is
37//!   crossed with probability ½ (K. Deb and H.-G. Beyer, "Self-adaptive genetic algorithms with
38//!   simulated binary crossover", report CI-61/99, Univ. Dortmund (1999), p. 8), and the two
39//!   children's values swap with probability ½.
40//! - **Polynomial mutation** (K. Deb and M. Goyal, "A combined genetic adaptive search (GeneAS)
41//!   for engineering design", *Computer Science and Informatics* 26(4), 30–45 (1996)).
42//!
43//! Both are cut at the variable's bounds, so no child is placed outside and none piles up on
44//! the bound. SBX drops the density beyond the bound and scales the rest up to a whole, as Deb
45//! and Agrawal propose (p. 143). Mutation cuts each side of the value at its own bound and keeps
46//! half the probability on each side. The equations of both cuts, given with each function
47//! below, are those of Deb's NSGA-II code as pymoo 0.6.2 (Apache-2.0) prints them, in its
48//! `operators/crossover/sbx.py` and `operators/mutation/pm.py`; no paper we could reach prints
49//! them, so each is derived again in its function's comment.
50//!
51//! The defaults are the paper's real-coded settings (p. 187): crossover probability 0.9,
52//! mutation probability `1/n` per variable, distribution indices `η_c = η_m = 20`; the
53//! population of 100 and 250 generations are its runs on the ZDT problems (p. 187).
54//!
55//! # Variables
56//!
57//! NSGA-II draws its first population uniformly between each variable's bounds, so every
58//! variable needs two finite bounds ([`Variable::within`]); its start and step are not used.
59//! Integer variables are not taken yet.
60//!
61//! # Reproducibility
62//!
63//! A generation's random numbers come from one stream keyed by the seed and the generation's
64//! number ([`SeededRng::for_stream`]), and are drawn before its children are evaluated, so a run
65//! is bit for bit the same every time on one platform, however its children are evaluated.
66//! Ties in a sort keep the designs' order, parents before children.
67
68use serde::{Deserialize, Serialize};
69
70use hpr_core::random::SeededRng;
71
72use super::{Variable, check_variables};
73use crate::error::AnalysisError;
74
75/// The largest population a run takes, 2¹². The sort of parents and children together keeps, for
76/// each design, the list of designs it dominates: up to about `(2N)²/2` indices, some 270 MB on a
77/// 64-bit machine at this size (more while the lists grow), and four times as much for each
78/// doubling.
79pub const MAX_POPULATION: usize = 1 << 12;
80
81/// The largest bound a variable may have, in size: `f64::MAX/4`, so that crossover's sums and
82/// spreads stay finite.
83pub const MAX_BOUND: f64 = f64::MAX / 4.0;
84
85/// The most goals a run takes.
86pub const MAX_OBJECTIVES: usize = 16;
87
88/// The optimizer's settings: the variables, the number of goals, the population, the number of
89/// generations, and the crossover's and mutation's parameters. It serializes as its fields, and
90/// reads back through the same checks as [`Nsga2::new`] and its `with_` methods.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(try_from = "Nsga2Data")]
93pub struct Nsga2 {
94    variables: Vec<Variable>,
95    objectives: usize,
96    population: usize,
97    generations: usize,
98    crossover_probability: f64,
99    crossover_index: f64,
100    mutation_probability: f64,
101    mutation_index: f64,
102}
103
104/// The serialized form of an [`Nsga2`].
105#[derive(Deserialize)]
106#[serde(deny_unknown_fields)]
107struct Nsga2Data {
108    variables: Vec<Variable>,
109    objectives: usize,
110    population: usize,
111    generations: usize,
112    crossover_probability: f64,
113    crossover_index: f64,
114    mutation_probability: f64,
115    mutation_index: f64,
116}
117
118impl TryFrom<Nsga2Data> for Nsga2 {
119    type Error = AnalysisError;
120
121    fn try_from(data: Nsga2Data) -> Result<Self, AnalysisError> {
122        Nsga2::new(data.variables, data.objectives)?
123            .with_population(data.population)?
124            .with_generations(data.generations)?
125            .with_crossover(data.crossover_probability, data.crossover_index)?
126            .with_mutation(data.mutation_probability, data.mutation_index)
127    }
128}
129
130/// What a model gives for one design: its goals, each to be made as small as it can be, and by
131/// how much it breaks the constraints, zero if it keeps them all.
132///
133/// A design the model can't evaluate (a flight that fails) is [`Goals::failed`]: its violation
134/// is `+∞`, so every design that could be evaluated dominates it. Infinite values serialize as
135/// none, a JSON `null`.
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[non_exhaustive]
138pub struct Goals {
139    /// The goals' values, in the order the model gives them.
140    #[serde(with = "infinities_as_none")]
141    pub objectives: Vec<f64>,
142    /// The total violation, `Σ max(0, gⱼ)` over constraints written `gⱼ ≤ 0`: zero if the design
143    /// keeps them all, `+∞` if it failed.
144    #[serde(with = "super::cmaes::infinity_as_none")]
145    pub violation: f64,
146}
147
148impl Goals {
149    /// Goals with no constraints to break.
150    pub fn feasible(objectives: Vec<f64>) -> Self {
151        Self {
152            objectives,
153            violation: 0.0,
154        }
155    }
156
157    /// Goals under constraints `gⱼ(x) ≤ 0`, given as the numbers `gⱼ`: the violation is
158    /// `Σ max(0, gⱼ)`, as for [`Evaluation::constrained`](super::Evaluation::constrained), whose
159    /// advice on scaling the constraints holds here too.
160    pub fn constrained(objectives: Vec<f64>, constraints: &[f64]) -> Self {
161        Self {
162            objectives,
163            violation: super::Evaluation::constrained(0.0, constraints).violation,
164        }
165    }
166
167    /// A design the model can't evaluate: no goals, and a violation of `+∞`, behind every other.
168    pub fn failed() -> Self {
169        Self {
170            objectives: Vec::new(),
171            violation: f64::INFINITY,
172        }
173    }
174
175    /// Whether the design keeps every constraint.
176    pub fn is_feasible(&self) -> bool {
177        self.violation == 0.0
178    }
179}
180
181/// One design of a population: where it is, what the model gave for it, and where it ranks.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183#[non_exhaustive]
184pub struct Member {
185    /// The design, one value per variable, in the variables' order.
186    pub point: Vec<f64>,
187    /// Its goals' values, `+∞` each for a failed design (serialized as none, a JSON `null`).
188    #[serde(with = "infinities_as_none")]
189    pub objectives: Vec<f64>,
190    /// Its constraint violation: zero if it keeps every constraint, `+∞` if it failed
191    /// (serialized as none).
192    #[serde(with = "super::cmaes::infinity_as_none")]
193    pub violation: f64,
194    /// Its front, counted from 0: rank 0 is the designs no other in the sort dominated.
195    pub rank: usize,
196    /// Its crowding distance in its front, `+∞` at a front's ends (serialized as none).
197    #[serde(with = "super::cmaes::infinity_as_none")]
198    pub crowding: f64,
199}
200
201/// What a run found: its last population's first front, and the whole population.
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[non_exhaustive]
204pub struct Front {
205    /// The designs of rank 0 in the last population, in the population's order: the run's
206    /// estimate of the Pareto front. If no design kept every constraint, these are the ones
207    /// that broke them least: check [`Front::is_feasible`].
208    pub members: Vec<Member>,
209    /// The last population, every rank, ranked as the run left it.
210    pub population: Vec<Member>,
211    /// How many evaluations the run made: the population times the generations.
212    pub evaluations: usize,
213    /// How many generations it ran, the first, random one included.
214    pub generations: usize,
215}
216
217impl Front {
218    /// Whether every design of the front keeps every constraint.
219    pub fn is_feasible(&self) -> bool {
220        self.members.iter().all(|m| m.violation == 0.0)
221    }
222}
223
224/// The serialized form of goals that are finite or `+∞`: options, none for `+∞`.
225mod infinities_as_none {
226    use serde::ser::Error as _;
227    use serde::{Deserialize, Deserializer, Serialize, Serializer};
228
229    pub(super) fn serialize<S: Serializer>(x: &[f64], serializer: S) -> Result<S::Ok, S::Error> {
230        if let Some(bad) = x.iter().find(|x| x.is_nan() || **x == f64::NEG_INFINITY) {
231            return Err(S::Error::custom(format!("{bad} is neither finite nor +∞")));
232        }
233        let values: Vec<Option<f64>> = x
234            .iter()
235            .map(|&x| (x != f64::INFINITY).then_some(x))
236            .collect();
237        values.serialize(serializer)
238    }
239
240    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
241        deserializer: D,
242    ) -> Result<Vec<f64>, D::Error> {
243        Ok(Vec::<Option<f64>>::deserialize(deserializer)?
244            .into_iter()
245            .map(|x| x.unwrap_or(f64::INFINITY))
246            .collect())
247    }
248}
249
250/// A distribution index: finite and not negative.
251fn check_index(what: &'static str, index: f64) -> Result<f64, AnalysisError> {
252    if index.is_finite() && index >= 0.0 {
253        Ok(index)
254    } else {
255        Err(AnalysisError::Domain { what, value: index })
256    }
257}
258
259/// A probability: within `[0, 1]`.
260fn check_probability(what: &'static str, p: f64) -> Result<f64, AnalysisError> {
261    if (0.0..=1.0).contains(&p) {
262        Ok(p)
263    } else {
264        Err(AnalysisError::Domain { what, value: p })
265    }
266}
267
268impl Nsga2 {
269    /// The optimizer over `variables` for `objectives` goals, with the paper's settings: a
270    /// population of 100, 250 generations, crossover probability 0.9 and `η_c = 20`, mutation
271    /// probability `1/n` and `η_m = 20`.
272    ///
273    /// # Errors
274    ///
275    /// [`AnalysisError::TooFew`] or [`AnalysisError::Count`] for no variables or more than
276    /// [`MAX_VARIABLES`](super::MAX_VARIABLES), or no goals or more than [`MAX_OBJECTIVES`];
277    /// [`AnalysisError::DuplicateVariable`] for two variables of one name;
278    /// [`AnalysisError::Domain`] for a variable without two finite bounds, or with one beyond
279    /// [`MAX_BOUND`]; and
280    /// [`AnalysisError::Unsupported`] for an integer variable.
281    pub fn new(variables: Vec<Variable>, objectives: usize) -> Result<Self, AnalysisError> {
282        check_variables(&variables)?;
283        if objectives == 0 {
284            return Err(AnalysisError::TooFew {
285                what: "objectives",
286                count: 0,
287                minimum: 1,
288            });
289        }
290        if objectives > MAX_OBJECTIVES {
291            return Err(AnalysisError::Count {
292                what: "objectives",
293                count: objectives,
294                limit: MAX_OBJECTIVES,
295            });
296        }
297        for v in &variables {
298            if !v.low().is_finite() {
299                return Err(AnalysisError::Domain {
300                    what: "variable's low bound (NSGA-II needs two finite bounds)",
301                    value: v.low(),
302                });
303            }
304            if !v.high().is_finite() {
305                return Err(AnalysisError::Domain {
306                    what: "variable's high bound (NSGA-II needs two finite bounds)",
307                    value: v.high(),
308                });
309            }
310            // Within ±f64::MAX/4, crossover's sums and spreads can't overflow.
311            for bound in [v.low(), v.high()] {
312                if bound.abs() > MAX_BOUND {
313                    return Err(AnalysisError::Domain {
314                        what: "variable's bound (NSGA-II takes bounds within ±f64::MAX/4)",
315                        value: bound,
316                    });
317                }
318            }
319            if v.is_integer() {
320                return Err(AnalysisError::Unsupported(format!(
321                    "integer variable {:?} in NSGA-II",
322                    v.name()
323                )));
324            }
325        }
326        // Cast: n ≤ 200, exact in f64.
327        let mutation_probability = 1.0 / variables.len() as f64;
328        Ok(Self {
329            variables,
330            objectives,
331            population: 100,
332            generations: 250,
333            crossover_probability: 0.9,
334            crossover_index: 20.0,
335            mutation_probability,
336            mutation_index: 20.0,
337        })
338    }
339
340    /// The same optimizer with a population of `size`: an even number, as children are bred two
341    /// at a time, from 4 to [`MAX_POPULATION`].
342    ///
343    /// # Errors
344    ///
345    /// [`AnalysisError::TooFew`] below 4, [`AnalysisError::Count`] above [`MAX_POPULATION`],
346    /// and [`AnalysisError::Domain`] for an odd size.
347    pub fn with_population(mut self, size: usize) -> Result<Self, AnalysisError> {
348        if size < 4 {
349            return Err(AnalysisError::TooFew {
350                what: "population",
351                count: size,
352                minimum: 4,
353            });
354        }
355        if size > MAX_POPULATION {
356            return Err(AnalysisError::Count {
357                what: "population",
358                count: size,
359                limit: MAX_POPULATION,
360            });
361        }
362        if !size.is_multiple_of(2) {
363            return Err(AnalysisError::Domain {
364                what: "population (an even number)",
365                // Cast: at most 2¹², exact in f64.
366                value: size as f64,
367            });
368        }
369        self.population = size;
370        Ok(self)
371    }
372
373    /// The same optimizer, run for `generations` generations, the first, random one included: it
374    /// evaluates the population that many times.
375    ///
376    /// # Errors
377    ///
378    /// [`AnalysisError::TooFew`] for none.
379    pub fn with_generations(mut self, generations: usize) -> Result<Self, AnalysisError> {
380        if generations == 0 {
381            return Err(AnalysisError::TooFew {
382                what: "generations",
383                count: 0,
384                minimum: 1,
385            });
386        }
387        self.generations = generations;
388        Ok(self)
389    }
390
391    /// The same optimizer with SBX crossing a pair of parents with `probability`, and
392    /// distribution index `index`: the larger, the closer children fall to their parents.
393    ///
394    /// # Errors
395    ///
396    /// [`AnalysisError::Domain`] for a probability outside `[0, 1]`, or an index that is negative
397    /// or not finite.
398    pub fn with_crossover(mut self, probability: f64, index: f64) -> Result<Self, AnalysisError> {
399        self.crossover_probability = check_probability("crossover probability", probability)?;
400        self.crossover_index = check_index("crossover distribution index", index)?;
401        Ok(self)
402    }
403
404    /// The same optimizer with polynomial mutation changing each variable of a child with
405    /// `probability`, and distribution index `index`: the larger, the smaller the changes.
406    ///
407    /// # Errors
408    ///
409    /// As [`Nsga2::with_crossover`].
410    pub fn with_mutation(mut self, probability: f64, index: f64) -> Result<Self, AnalysisError> {
411        self.mutation_probability = check_probability("mutation probability", probability)?;
412        self.mutation_index = check_index("mutation distribution index", index)?;
413        Ok(self)
414    }
415
416    /// The variables.
417    pub fn variables(&self) -> &[Variable] {
418        &self.variables
419    }
420
421    /// The number of goals.
422    pub fn objectives(&self) -> usize {
423        self.objectives
424    }
425
426    /// The population's size.
427    pub fn population(&self) -> usize {
428        self.population
429    }
430
431    /// The number of generations.
432    pub fn generations(&self) -> usize {
433        self.generations
434    }
435
436    /// Starts a run from `seed`: its first candidates are the random first population.
437    pub fn start(&self, seed: u64) -> Run {
438        let mut rng = SeededRng::for_stream(seed, &[0]);
439        let candidates = (0..self.population)
440            .map(|_| {
441                self.variables
442                    .iter()
443                    .map(|v| v.low() + rng.uniform() * (v.high() - v.low()))
444                    .collect()
445            })
446            .collect();
447        Run {
448            settings: self.clone(),
449            seed,
450            generation: 0,
451            members: Vec::new(),
452            candidates,
453        }
454    }
455
456    /// Runs NSGA-II on `model` under constraints from `seed`, evaluating each candidate in turn.
457    ///
458    /// # Errors
459    ///
460    /// [`Run::tell_constrained`]'s.
461    pub fn minimize_constrained(
462        &self,
463        seed: u64,
464        mut model: impl FnMut(&[f64]) -> Goals,
465    ) -> Result<Front, AnalysisError> {
466        let mut run = self.start(seed);
467        loop {
468            let goals: Vec<Goals> = run.candidates().iter().map(|x| model(x)).collect();
469            if let Some(front) = run.tell_constrained(&goals)? {
470                return Ok(front);
471            }
472        }
473    }
474
475    /// Runs NSGA-II on `model` from `seed`, evaluating each candidate in turn: the model gives
476    /// each candidate's goals, every one to be made as small as it can be.
477    ///
478    /// # Errors
479    ///
480    /// [`Run::tell`]'s.
481    pub fn minimize(
482        &self,
483        seed: u64,
484        mut model: impl FnMut(&[f64]) -> Vec<f64>,
485    ) -> Result<Front, AnalysisError> {
486        let mut run = self.start(seed);
487        loop {
488            let values: Vec<Vec<f64>> = run.candidates().iter().map(|x| model(x)).collect();
489            if let Some(front) = run.tell(&values)? {
490                return Ok(front);
491            }
492        }
493    }
494}
495
496/// A run in progress: its population, ranked, and the candidates waiting to be evaluated.
497#[derive(Debug, Clone)]
498pub struct Run {
499    settings: Nsga2,
500    seed: u64,
501    /// Generations evaluated so far.
502    generation: usize,
503    /// The population, ranked: empty before the first generation is told.
504    members: Vec<Member>,
505    /// The candidates waiting to be evaluated: empty once the run has finished.
506    candidates: Vec<Vec<f64>>,
507}
508
509impl Run {
510    /// The candidates to evaluate next: the first population, then each generation's children.
511    /// Empty once the run has finished.
512    pub fn candidates(&self) -> &[Vec<f64>] {
513        &self.candidates
514    }
515
516    /// How many generations have been evaluated.
517    pub fn generation(&self) -> usize {
518        self.generation
519    }
520
521    /// The population as it stands, ranked: empty before the first generation is told.
522    pub fn members(&self) -> &[Member] {
523        &self.members
524    }
525
526    /// Takes the goals of the candidates, in their order, with no constraints: the population
527    /// is updated, and once the last generation is told the run's [`Front`] is returned. A
528    /// design with a goal of `+∞` counts as failed ([`Goals::failed`]): every design whose goals
529    /// are all finite dominates it.
530    ///
531    /// # Errors
532    ///
533    /// [`Run::tell_constrained`]'s.
534    pub fn tell(&mut self, values: &[Vec<f64>]) -> Result<Option<Front>, AnalysisError> {
535        let goals: Vec<Goals> = values.iter().cloned().map(Goals::feasible).collect();
536        self.tell_constrained(&goals)
537    }
538
539    /// [`Run::tell`] with each candidate's constraint violation: designs are sorted by
540    /// constrained domination. A failed design ([`Goals::failed`]) may have any number of goals,
541    /// as they are set aside: each counts as `+∞`. A design with a goal of `+∞` fails too, its
542    /// violation taken as `+∞`.
543    ///
544    /// # Errors
545    ///
546    /// [`AnalysisError::Length`] for a number of goals other than the candidates' (telling a
547    /// finished run, which has none, too) or a design's number of goals other than the
548    /// optimizer's; [`AnalysisError::Output`] for a goal that is NaN or `−∞`, with the index of
549    /// its evaluation in the run; and [`AnalysisError::Domain`] for a violation that is negative
550    /// or NaN.
551    pub fn tell_constrained(&mut self, goals: &[Goals]) -> Result<Option<Front>, AnalysisError> {
552        let m = self.settings.objectives;
553        if goals.len() != self.candidates.len() || self.candidates.is_empty() {
554            return Err(AnalysisError::Length {
555                what: "goals, against the generation's candidates",
556                length: goals.len(),
557                expected: self.candidates.len(),
558            });
559        }
560        let evaluated = self.generation * self.settings.population;
561        let mut children = Vec::with_capacity(goals.len());
562        for (k, (point, g)) in self.candidates.iter().zip(goals).enumerate() {
563            if g.violation.is_nan() || g.violation < 0.0 {
564                return Err(AnalysisError::Domain {
565                    what: "constraint violation (must not be negative or NaN)",
566                    value: g.violation,
567                });
568            }
569            // A failed design's goals are set aside; any other's are checked, and one with a goal
570            // of +∞ fails.
571            let mut failed = g.violation == f64::INFINITY;
572            if !failed {
573                if g.objectives.len() != m {
574                    return Err(AnalysisError::Length {
575                        what: "a design's goals",
576                        length: g.objectives.len(),
577                        expected: m,
578                    });
579                }
580                if let Some(&value) = g
581                    .objectives
582                    .iter()
583                    .find(|v| v.is_nan() || **v == f64::NEG_INFINITY)
584                {
585                    return Err(AnalysisError::Output {
586                        index: evaluated + k,
587                        value,
588                    });
589                }
590                failed = g.objectives.contains(&f64::INFINITY);
591            }
592            let objectives = if failed {
593                vec![f64::INFINITY; m]
594            } else {
595                g.objectives.clone()
596            };
597            children.push(Member {
598                point: point.clone(),
599                objectives,
600                violation: if failed { f64::INFINITY } else { g.violation },
601                rank: 0,
602                crowding: 0.0,
603            });
604        }
605        let mut combined = std::mem::take(&mut self.members);
606        combined.extend(children);
607        self.members = survivors(combined, self.settings.population);
608        self.generation += 1;
609        if self.generation == self.settings.generations {
610            self.candidates.clear();
611            let members = self
612                .members
613                .iter()
614                .filter(|m| m.rank == 0)
615                .cloned()
616                .collect();
617            return Ok(Some(Front {
618                members,
619                population: self.members.clone(),
620                evaluations: self.generation * self.settings.population,
621                generations: self.generation,
622            }));
623        }
624        self.candidates = self.breed();
625        Ok(None)
626    }
627
628    /// The next generation's children: parents by binary tournaments, crossed in pairs by SBX,
629    /// then mutated.
630    ///
631    /// The tournaments' contestants are two shuffles of the population laid end to end and taken
632    /// two at a time, so every design plays exactly two tournaments, as in Deb's NSGA-II code and
633    /// pymoo; the paper gives no pairing. The population is even, so no tournament pits a design
634    /// against itself. Winners are paired in turn: the first two, the next two, and so on.
635    fn breed(&self) -> Vec<Vec<f64>> {
636        let s = &self.settings;
637        // Cast: a generation count fits in u64.
638        let mut rng = SeededRng::for_stream(self.seed, &[self.generation as u64]);
639        let contestants = contestants(&mut rng, self.members.len());
640        let parents: Vec<&[f64]> = contestants
641            .as_chunks::<2>()
642            .0
643            .iter()
644            .map(|&[i, j]| self.tournament(i, j, &mut rng))
645            .collect();
646        let mut children = Vec::with_capacity(s.population);
647        for &[a, b] in parents.as_chunks::<2>().0 {
648            let (mut c1, mut c2) = (a.to_vec(), b.to_vec());
649            if rng.uniform() < s.crossover_probability {
650                for (i, v) in s.variables.iter().enumerate() {
651                    if rng.uniform() < 0.5 {
652                        let (y1, y2) =
653                            sbx(a[i], b[i], v.low(), v.high(), s.crossover_index, &mut rng);
654                        (c1[i], c2[i]) = (y1, y2);
655                    }
656                }
657            }
658            for child in [&mut c1, &mut c2] {
659                for (i, v) in s.variables.iter().enumerate() {
660                    if rng.uniform() < s.mutation_probability {
661                        child[i] = polynomial_mutation(
662                            child[i],
663                            v.low(),
664                            v.high(),
665                            s.mutation_index,
666                            &mut rng,
667                        );
668                    }
669                }
670            }
671            children.push(c1);
672            children.push(c2);
673        }
674        children
675    }
676
677    /// A binary tournament between members `i` and `j`: the winner by the crowded comparison, a
678    /// tie to either with equal chance.
679    fn tournament(&self, i: usize, j: usize, rng: &mut SeededRng) -> &[f64] {
680        let (a, b) = (&self.members[i], &self.members[j]);
681        let winner = match crowded_comparison(a, b) {
682            std::cmp::Ordering::Less => a,
683            std::cmp::Ordering::Greater => b,
684            std::cmp::Ordering::Equal => {
685                if rng.uniform() < 0.5 {
686                    a
687                } else {
688                    b
689                }
690            }
691        };
692        &winner.point
693    }
694}
695
696/// The tournaments' contestants for a population of `n`: two shuffles of `0..n` (Fisher–Yates,
697/// from the end) laid end to end, to be taken two at a time.
698fn contestants(rng: &mut SeededRng, n: usize) -> Vec<usize> {
699    let mut contestants = Vec::with_capacity(2 * n);
700    for _ in 0..2 {
701        let mut shuffle: Vec<usize> = (0..n).collect();
702        for i in (1..n).rev() {
703            shuffle.swap(i, draw_index(rng, i + 1));
704        }
705        contestants.extend(shuffle);
706    }
707    contestants
708}
709
710/// A uniform index below `n` (`n ≥ 1`): `⌊u n⌋` for a uniform `u` on `[0, 1)`, held below `n`.
711/// Its bias is at most `n/2⁵³`.
712fn draw_index(rng: &mut SeededRng, n: usize) -> usize {
713    // Casts: n ≤ 2¹², exact in f64, and ⌊u n⌋ < n converts back exactly.
714    let k = (rng.uniform() * n as f64).floor() as usize;
715    k.min(n - 1)
716}
717
718/// The crowded comparison: [`Less`](std::cmp::Ordering::Less) if `a` ranks ahead of `b`.
719fn crowded_comparison(a: &Member, b: &Member) -> std::cmp::Ordering {
720    a.rank
721        .cmp(&b.rank)
722        .then_with(|| b.crowding.total_cmp(&a.crowding))
723}
724
725/// Whether `a`'s goals dominate `b`'s: no worse in any, better in at least one. The two should
726/// be the same length; only as many goals as the shorter has are compared.
727pub fn dominates(a: &[f64], b: &[f64]) -> bool {
728    a.iter().zip(b).all(|(x, y)| x <= y) && a.iter().zip(b).any(|(x, y)| x < y)
729}
730
731/// Constrained domination (Deb et al. 2002, §VI).
732fn constrained_dominates(a: &Member, b: &Member) -> bool {
733    let (fa, fb) = (a.violation == 0.0, b.violation == 0.0);
734    match (fa, fb) {
735        (true, false) => true,
736        (false, true) => false,
737        (false, false) => a.violation < b.violation,
738        (true, true) => dominates(&a.objectives, &b.objectives),
739    }
740}
741
742/// Fast non-dominated sorting: each member's front, counted from 0, as indices into `members`,
743/// each front in the members' order.
744fn fronts(members: &[Member]) -> Vec<Vec<usize>> {
745    let n = members.len();
746    let mut dominated: Vec<Vec<usize>> = vec![Vec::new(); n];
747    let mut count = vec![0usize; n];
748    for p in 0..n {
749        for q in p + 1..n {
750            if constrained_dominates(&members[p], &members[q]) {
751                dominated[p].push(q);
752                count[q] += 1;
753            } else if constrained_dominates(&members[q], &members[p]) {
754                dominated[q].push(p);
755                count[p] += 1;
756            }
757        }
758    }
759    let mut fronts = Vec::new();
760    let mut current: Vec<usize> = (0..n).filter(|&p| count[p] == 0).collect();
761    while !current.is_empty() {
762        let mut next = Vec::new();
763        for &p in &current {
764            for &q in &dominated[p] {
765                count[q] -= 1;
766                if count[q] == 0 {
767                    next.push(q);
768                }
769            }
770        }
771        next.sort_unstable();
772        fronts.push(current);
773        current = next;
774    }
775    fronts
776}
777
778/// The crowding distance of each design of a front, given each design's goals, in the front's
779/// order. A goal whose range in the front is zero or not finite adds nothing between the ends.
780fn crowding(front: &[&[f64]]) -> Vec<f64> {
781    let l = front.len();
782    let mut distance = vec![0.0; l];
783    let Some(first) = front.first() else {
784        return distance;
785    };
786    #[allow(
787        clippy::needless_range_loop,
788        reason = "`m` indexes each design's goals, not `front`"
789    )]
790    for m in 0..first.len() {
791        let value = |k: usize| front[k][m];
792        let mut order: Vec<usize> = (0..l).collect();
793        // A stable sort: ties keep the front's order.
794        order.sort_by(|&a, &b| value(a).total_cmp(&value(b)));
795        distance[order[0]] = f64::INFINITY;
796        distance[order[l - 1]] = f64::INFINITY;
797        let range = value(order[l - 1]) - value(order[0]);
798        if !(range.is_finite() && range > 0.0) {
799            continue;
800        }
801        for k in 1..l.saturating_sub(1) {
802            distance[order[k]] += (value(order[k + 1]) - value(order[k - 1])) / range;
803        }
804    }
805    distance
806}
807
808/// The `n` survivors of `combined` (parents then children): whole fronts in rank order, and the
809/// front that doesn't fit cut by crowding distance, largest first. Each keeps the rank and
810/// crowding distance of the sort of `combined`.
811fn survivors(combined: Vec<Member>, n: usize) -> Vec<Member> {
812    let fronts = fronts(&combined);
813    let mut ranked: Vec<Option<Member>> = combined.into_iter().map(Some).collect();
814    let mut next = Vec::with_capacity(n);
815    for (rank, front) in fronts.iter().enumerate() {
816        if next.len() == n {
817            break;
818        }
819        let goals: Vec<&[f64]> = front
820            .iter()
821            .filter_map(|&i| ranked[i].as_ref().map(|m| m.objectives.as_slice()))
822            .collect();
823        let distance = crowding(&goals);
824        let mut order: Vec<usize> = (0..front.len()).collect();
825        if next.len() + front.len() > n {
826            // A stable sort: equal distances keep the front's order.
827            order.sort_by(|&a, &b| distance[b].total_cmp(&distance[a]));
828            order.truncate(n - next.len());
829            order.sort_unstable();
830        }
831        for k in order {
832            if let Some(mut member) = ranked[front[k]].take() {
833                member.rank = rank;
834                member.crowding = distance[k];
835                next.push(member);
836            }
837        }
838    }
839    next
840}
841
842/// Bounded simulated binary crossover of one variable: the two children of parent values `x1`
843/// and `x2` within `[low, high]`, the children swapped with probability ½. Parents closer than
844/// 10⁻¹⁴ are copied.
845///
846/// With `y₁ < y₂` the parents and `u` uniform on `[0, 1)`, unbounded SBX's spread for a child is
847/// `β_q = (2u)^(1/(η+1))` for `u ≤ ½`, else `(1/(2(1 − u)))^(1/(η+1))`, the inverse of the spread's
848/// distribution (Deb and Beyer 1999, eq. 4). A child `c₁ = ½((y₁ + y₂) − β (y₂ − y₁))` stays
849/// above `low` while `β ≤ β_b = 1 + 2(y₁ − low)/(y₂ − y₁)`, a spread whose probability is
850/// `α/2` with `α = 2 − β_b^−(η+1)`. Drawing the spread with probability scaled to that, at
851/// `u′ = u α/2`, gives `β_q = (u α)^(1/(η+1))` for `u ≤ 1/α`, else `(1/(2 − u α))^(1/(η+1))`; the
852/// other child uses the same `u` with `β_b = 1 + 2(high − y₂)/(y₂ − y₁)`. The clamp only catches
853/// rounding.
854fn sbx(x1: f64, x2: f64, low: f64, high: f64, eta: f64, rng: &mut SeededRng) -> (f64, f64) {
855    if (x1 - x2).abs() <= 1e-14 {
856        return (x1, x2);
857    }
858    let (y1, y2) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
859    let u = rng.uniform();
860    let exponent = 1.0 / (eta + 1.0);
861    // β_q for a child whose spread is cut at `beta`, the ratio its bound allows.
862    let beta_q = |beta: f64| {
863        let alpha = 2.0 - beta.powf(-(eta + 1.0));
864        if u <= 1.0 / alpha {
865            (u * alpha).powf(exponent)
866        } else {
867            (1.0 / (2.0 - u * alpha)).powf(exponent)
868        }
869    };
870    let gap = y2 - y1;
871    let c1 = 0.5 * ((y1 + y2) - beta_q(1.0 + 2.0 * (y1 - low) / gap) * gap);
872    let c2 = 0.5 * ((y1 + y2) + beta_q(1.0 + 2.0 * (high - y2) / gap) * gap);
873    let (c1, c2) = (c1.clamp(low, high), c2.clamp(low, high));
874    if rng.uniform() < 0.5 {
875        (c2, c1)
876    } else {
877        (c1, c2)
878    }
879}
880
881/// Bounded polynomial mutation of one value `y` within `[low, high]`: `y + δ_q (high − low)`.
882///
883/// Unbounded, the step `δ` has density `½(η + 1)(1 − |δ|)^η` on `[−1, 1]`, half each way. Here
884/// each half is cut at its bound, `δ₁ = (y − low)/(high − low)` below and `δ₂ = (high − y)/(high −
885/// low)` above, and keeps its half of the probability: for `u ≤ ½`,
886/// `δ_q = (2u + (1 − 2u)(1 − δ₁)^(η+1))^(1/(η+1)) − 1`, which runs from `−δ₁` at `u = 0` to 0 at
887/// `u = ½`; for `u > ½`, `δ_q = 1 − (2(1 − u) + 2(u − ½)(1 − δ₂)^(η+1))^(1/(η+1))`, from 0 to
888/// `δ₂`. The clamp only catches rounding.
889fn polynomial_mutation(y: f64, low: f64, high: f64, eta: f64, rng: &mut SeededRng) -> f64 {
890    let range = high - low;
891    let delta1 = (y - low) / range;
892    let delta2 = (high - y) / range;
893    let u = rng.uniform();
894    let power = 1.0 / (eta + 1.0);
895    let delta_q = if u <= 0.5 {
896        let value = 2.0 * u + (1.0 - 2.0 * u) * (1.0 - delta1).powf(eta + 1.0);
897        value.powf(power) - 1.0
898    } else {
899        let value = 2.0 * (1.0 - u) + 2.0 * (u - 0.5) * (1.0 - delta2).powf(eta + 1.0);
900        1.0 - value.powf(power)
901    };
902    (y + delta_q * range).clamp(low, high)
903}
904
905/// The generational distance of `set` from `reference`: the mean, over the points of `set`, of
906/// the Euclidean distance to the nearest point of `reference`. It is Deb et al.'s (2002)
907/// convergence metric Υ (§IV-B, p. 188), and pymoo's `GD`: zero when every point lies on the
908/// reference. NaN for an empty `set` or `reference`.
909pub fn generational_distance(set: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
910    mean_nearest(set, reference)
911}
912
913/// The inverted generational distance: the mean, over the points of `reference`, of the
914/// Euclidean distance to the nearest point of `set`, as pymoo's `IGD`. Small only if `set` both
915/// lies near the reference and covers all of it.
916pub fn inverted_generational_distance(set: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
917    mean_nearest(reference, set)
918}
919
920/// The mean over `from` of the distance to the nearest point of `to`.
921fn mean_nearest(from: &[Vec<f64>], to: &[Vec<f64>]) -> f64 {
922    if from.is_empty() || to.is_empty() {
923        return f64::NAN;
924    }
925    let total = from
926        .iter()
927        .map(|p| {
928            to.iter()
929                .map(|q| {
930                    p.iter()
931                        .zip(q)
932                        .map(|(a, b)| (a - b) * (a - b))
933                        .sum::<f64>()
934                        .sqrt()
935                })
936                .fold(f64::INFINITY, f64::min)
937        })
938        .fold(0.0, |total, d| total + d);
939    // Cast: a count of points, exact in f64 below 2⁵³.
940    total / from.len() as f64
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946
947    fn member(objectives: &[f64], violation: f64) -> Member {
948        Member {
949            point: Vec::new(),
950            objectives: objectives.to_vec(),
951            violation,
952            rank: 0,
953            crowding: 0.0,
954        }
955    }
956
957    fn unit(name: &str) -> Variable {
958        Variable::new(name, 0.5, 0.1)
959            .unwrap()
960            .within(0.0, 1.0)
961            .unwrap()
962    }
963
964    #[test]
965    fn domination_needs_one_strictly_better() {
966        assert!(dominates(&[1.0, 2.0], &[1.0, 3.0]));
967        assert!(!dominates(&[1.0, 2.0], &[1.0, 2.0]), "equal");
968        assert!(!dominates(&[1.0, 3.0], &[2.0, 2.0]), "a trade-off");
969        assert!(dominates(&[1.0, 2.0], &[f64::INFINITY, 2.0]));
970        // Constrained: feasible beats infeasible whatever the goals; two infeasible go by violation.
971        assert!(constrained_dominates(
972            &member(&[9.0, 9.0], 0.0),
973            &member(&[0.0, 0.0], 0.1)
974        ));
975        assert!(constrained_dominates(
976            &member(&[9.0, 9.0], 0.1),
977            &member(&[0.0, 0.0], 0.2)
978        ));
979        assert!(!constrained_dominates(
980            &member(&[0.0, 0.0], 0.2),
981            &member(&[9.0, 9.0], 0.2)
982        ));
983    }
984
985    /// Fronts of a hand-made set: (1, 5), (2, 3) and (4, 1) trade off; (3, 4) is dominated by
986    /// (2, 3) only, and (5, 5) by every other.
987    #[test]
988    fn fronts_peel_in_order() {
989        let members: Vec<Member> = [[3.0, 4.0], [1.0, 5.0], [5.0, 5.0], [2.0, 3.0], [4.0, 1.0]]
990            .iter()
991            .map(|f| member(f, 0.0))
992            .collect();
993        assert_eq!(fronts(&members), vec![vec![1, 3, 4], vec![0], vec![2]]);
994        // Failed designs form the last front.
995        let failed = member(&[f64::INFINITY, f64::INFINITY], f64::INFINITY);
996        let mut with_failed = members.clone();
997        with_failed.push(failed.clone());
998        with_failed.push(failed);
999        assert_eq!(
1000            fronts(&with_failed),
1001            vec![vec![1, 3, 4], vec![0], vec![2], vec![5, 6]]
1002        );
1003    }
1004
1005    /// Crowding distances by hand: four points on a line, goal ranges 3 and 6.
1006    #[test]
1007    fn crowding_by_hand() {
1008        let goals: [&[f64]; 4] = [&[0.0, 6.0], &[1.0, 4.0], &[3.0, 0.0], &[2.0, 3.0]];
1009        let d = crowding(&goals);
1010        assert_eq!(d[0], f64::INFINITY);
1011        assert_eq!(d[2], f64::INFINITY);
1012        // (1, 4): neighbours (0, 6) and (2, 3): 2/3 + 3/6.
1013        assert!((d[1] - (2.0 / 3.0 + 3.0 / 6.0)).abs() <= 1e-15);
1014        // (2, 3): neighbours (1, 4) and (3, 0): 2/3 + 4/6.
1015        assert!((d[3] - (2.0 / 3.0 + 4.0 / 6.0)).abs() <= 1e-15);
1016        // Two or fewer points are all ends; a goal of zero range adds nothing.
1017        assert_eq!(
1018            crowding(&[&[1.0, 2.0], &[2.0, 1.0]]),
1019            vec![f64::INFINITY; 2]
1020        );
1021        let flat: [&[f64]; 3] = [&[1.0, 5.0], &[2.0, 5.0], &[3.0, 5.0]];
1022        assert_eq!(crowding(&flat)[1], 1.0);
1023        assert!(crowding(&[]).is_empty());
1024    }
1025
1026    /// The front that doesn't fit is cut by crowding distance, its ends kept.
1027    #[test]
1028    fn survivors_cut_the_last_front_by_crowding() {
1029        let combined: Vec<Member> = [
1030            [0.0, 1.0],
1031            [0.1, 0.9],
1032            [0.5, 0.5],
1033            [0.55, 0.45],
1034            [1.0, 0.0],
1035            [2.0, 2.0],
1036        ]
1037        .iter()
1038        .map(|f| member(f, 0.0))
1039        .collect();
1040        let next = survivors(combined, 3);
1041        let kept: Vec<Vec<f64>> = next.iter().map(|m| m.objectives.clone()).collect();
1042        // The two ends, and of the middle three the one with the widest gaps around it: (0.1, 0.9)
1043        // and (0.55, 0.45) have 0.5 + 0.5, (0.5, 0.5) 0.45 + 0.45. The tie goes to the earlier.
1044        assert_eq!(kept, vec![vec![0.0, 1.0], vec![0.1, 0.9], vec![1.0, 0.0]]);
1045        assert!(next.iter().all(|m| m.rank == 0));
1046    }
1047
1048    /// SBX's spread factor `β = |c₂ − c₁| / |x₂ − x₁|` far from the bounds follows Deb and
1049    /// Agrawal's density: `P(β ≤ b) = bⁿ⁺¹/2` for `b ≤ 1`, and `1 − b^−(η+1)/2` above.
1050    #[test]
1051    fn sbx_spread_follows_its_density() {
1052        let eta = 2.0;
1053        let draws = 100_000;
1054        let mut rng = SeededRng::seed_from_u64(1);
1055        let betas: Vec<f64> = (0..draws)
1056            .map(|_| {
1057                let (c1, c2) = sbx(-0.5, 0.5, -1e9, 1e9, eta, &mut rng);
1058                (c2 - c1).abs()
1059            })
1060            .collect();
1061        for b in [0.25_f64, 0.5, 0.8, 1.0, 1.5, 3.0] {
1062            let expected: f64 = if b <= 1.0 {
1063                0.5 * b.powf(eta + 1.0)
1064            } else {
1065                1.0 - 0.5 * b.powf(-(eta + 1.0))
1066            };
1067            let share = betas.iter().filter(|&&x| x <= b).count() as f64 / draws as f64;
1068            let sigma = (expected * (1.0 - expected) / draws as f64).sqrt();
1069            assert!(
1070                (share - expected).abs() <= 5.0 * sigma + 1e-6,
1071                "P(β ≤ {b}) {share} against {expected}"
1072            );
1073        }
1074    }
1075
1076    /// Near a bound, SBX's children and mutated values stay within it, and their spread is cut,
1077    /// not piled at it: none lands on the bound.
1078    #[test]
1079    fn sbx_and_mutation_stay_within_bounds() {
1080        let mut rng = SeededRng::seed_from_u64(2);
1081        let mut at_bound = 0;
1082        for _ in 0..20_000 {
1083            let (c1, c2) = sbx(0.001, 0.3, 0.0, 1.0, 20.0, &mut rng);
1084            assert!((0.0..=1.0).contains(&c1) && (0.0..=1.0).contains(&c2));
1085            at_bound += usize::from(c1 == 0.0 || c2 == 0.0);
1086            let (c1, c2) = sbx(0.7, 0.999, 0.0, 1.0, 20.0, &mut rng);
1087            assert!((0.0..=1.0).contains(&c1) && (0.0..=1.0).contains(&c2));
1088            at_bound += usize::from(c1 == 1.0 || c2 == 1.0);
1089            let y = polynomial_mutation(0.999, 0.0, 1.0, 20.0, &mut rng);
1090            assert!((0.0..=1.0).contains(&y));
1091            at_bound += usize::from(y == 1.0);
1092            let y = polynomial_mutation(0.001, 0.0, 1.0, 20.0, &mut rng);
1093            assert!((0.0..=1.0).contains(&y));
1094            at_bound += usize::from(y == 0.0);
1095        }
1096        assert_eq!(at_bound, 0);
1097        // Parents within 1e-14 are copied.
1098        assert_eq!(sbx(0.3, 0.3, 0.0, 1.0, 20.0, &mut rng), (0.3, 0.3));
1099    }
1100
1101    /// Polynomial mutation's step `δ_q`, cut at each side's own bound, follows its distribution:
1102    /// at `y = 0.2` in `[0, 1]`, `δ₁ = 0.2` below and `δ₂ = 0.8` above, and with
1103    /// `tᵢ = (1 − δᵢ)^(η+1)`, `P(δ_q ≤ −d) = ((1 − d)^(η+1) − t₁) / (2 (1 − t₁))` for `d ≤ δ₁`, and
1104    /// `P(δ_q ≥ d) = ((1 − d)^(η+1) − t₂) / (2 (1 − t₂))` for `d ≤ δ₂`; half the steps each way.
1105    #[test]
1106    fn mutation_step_follows_its_distribution() {
1107        let draws = 100_000;
1108        for eta in [5.0_f64, 20.0] {
1109            let mut rng = SeededRng::seed_from_u64(3);
1110            let steps: Vec<f64> = (0..draws)
1111                .map(|_| polynomial_mutation(0.2, 0.0, 1.0, eta, &mut rng) - 0.2)
1112                .collect();
1113            let share = |f: &dyn Fn(f64) -> bool| {
1114                steps.iter().filter(|&&x| f(x)).count() as f64 / draws as f64
1115            };
1116            let check = |measured: f64, expected: f64, what: &str| {
1117                let sigma = (expected * (1.0 - expected) / draws as f64).sqrt();
1118                assert!(
1119                    (measured - expected).abs() <= 5.0 * sigma + 1e-6,
1120                    "η = {eta}, {what}: {measured} against {expected}"
1121                );
1122            };
1123            let (t1, t2) = (0.8_f64.powf(eta + 1.0), 0.2_f64.powf(eta + 1.0));
1124            for d in [0.01_f64, 0.03, 0.1, 0.19] {
1125                let below = ((1.0 - d).powf(eta + 1.0) - t1) / (2.0 * (1.0 - t1));
1126                check(share(&|x| x <= -d), below, &format!("P(δ ≤ −{d})"));
1127                let above = ((1.0 - d).powf(eta + 1.0) - t2) / (2.0 * (1.0 - t2));
1128                check(share(&|x| x >= d), above, &format!("P(δ ≥ {d})"));
1129            }
1130            check(share(&|x| x < 0.0), 0.5, "P(δ < 0)");
1131        }
1132    }
1133
1134    /// Near both bounds, each SBX child's spread is cut at its own bound: the lower child's
1135    /// `β = (y₁ + y₂ − 2c)/(y₂ − y₁)` at `β_b = 1 + 2(y₁ − low)/(y₂ − y₁)`, the upper child's
1136    /// `β = (2c − y₁ − y₂)/(y₂ − y₁)` at `β_b = 1 + 2(high − y₂)/(y₂ − y₁)`, each scaled by `1/α`,
1137    /// `α = 2 − β_b^−(η+1)`: `P(β ≤ b) = b^(η+1)/α` for `b ≤ 1`, and `(2 − b^−(η+1))/α` from 1 to
1138    /// `β_b`. Here the two cuts differ, 1.4 below and 1.8 above. The children come out in either
1139    /// order with equal chance.
1140    #[test]
1141    fn sbx_spread_is_cut_at_each_bound() {
1142        let (eta, draws) = (2.0_f64, 100_000);
1143        let (y1, y2, low, high) = (0.05_f64, 0.3_f64, 0.0_f64, 0.4_f64);
1144        let gap = y2 - y1;
1145        let mut rng = SeededRng::seed_from_u64(4);
1146        let mut lower_first = 0;
1147        let (mut lower, mut upper) = (Vec::new(), Vec::new());
1148        for _ in 0..draws {
1149            let (c1, c2) = sbx(y1, y2, low, high, eta, &mut rng);
1150            lower_first += usize::from(c1 < c2);
1151            lower.push((y1 + y2 - 2.0 * c1.min(c2)) / gap);
1152            upper.push((2.0 * c1.max(c2) - y1 - y2) / gap);
1153        }
1154        for (betas, beta_b, side) in [
1155            (&lower, 1.0 + 2.0 * (y1 - low) / gap, "lower"),
1156            (&upper, 1.0 + 2.0 * (high - y2) / gap, "upper"),
1157        ] {
1158            let alpha = 2.0 - beta_b.powf(-(eta + 1.0));
1159            for b in [
1160                0.5, 0.9, 0.94, 0.945, 0.96, 0.98, 1.0, 1.1, 1.3, 1.39, 1.6, 1.79,
1161            ] {
1162                if b > beta_b {
1163                    continue;
1164                }
1165                let expected = if b <= 1.0 {
1166                    b.powf(eta + 1.0) / alpha
1167                } else {
1168                    (2.0 - b.powf(-(eta + 1.0))) / alpha
1169                };
1170                let share = betas.iter().filter(|&&x| x <= b).count() as f64 / draws as f64;
1171                let sigma = (expected * (1.0 - expected) / draws as f64).sqrt();
1172                assert!(
1173                    (share - expected).abs() <= 5.0 * sigma + 1e-6,
1174                    "{side} child: P(β ≤ {b}) {share} against {expected}"
1175                );
1176            }
1177            assert!(betas.iter().all(|&b| b <= beta_b * (1.0 + 1e-12)));
1178        }
1179        let half = lower_first as f64 / draws as f64;
1180        assert!(
1181            (half - 0.5).abs() <= 5.0 * (0.25 / draws as f64).sqrt(),
1182            "{half}"
1183        );
1184    }
1185
1186    /// Two shuffles end to end: every design plays exactly two tournaments, never against itself.
1187    #[test]
1188    fn every_design_plays_two_tournaments() {
1189        for n in [4, 6, 20, 100] {
1190            for seed in 0..20 {
1191                let mut rng = SeededRng::seed_from_u64(seed);
1192                let c = contestants(&mut rng, n);
1193                assert_eq!(c.len(), 2 * n);
1194                for i in 0..n {
1195                    assert_eq!(c.iter().filter(|&&k| k == i).count(), 2);
1196                }
1197                assert!(c.as_chunks::<2>().0.iter().all(|[a, b]| a != b));
1198            }
1199        }
1200        // Each shuffle starts with each design a quarter of the time (n = 4), and the two are
1201        // independent: the second starts where the first does a quarter of the time too, not
1202        // always (one shuffle twice) or never.
1203        let mut rng = SeededRng::seed_from_u64(9);
1204        let (mut first, mut second, mut same) = ([0_usize; 4], [0_usize; 4], 0_usize);
1205        let draws = 40_000;
1206        for _ in 0..draws {
1207            let c = contestants(&mut rng, 4);
1208            first[c[0]] += 1;
1209            second[c[4]] += 1;
1210            same += usize::from(c[0] == c[4]);
1211        }
1212        let near_quarter = |count: usize| {
1213            let share = count as f64 / draws as f64;
1214            (share - 0.25).abs() <= 5.0 * (0.1875 / draws as f64).sqrt()
1215        };
1216        assert!(
1217            first.into_iter().chain(second).all(near_quarter),
1218            "{first:?} {second:?}"
1219        );
1220        assert!(near_quarter(same), "{same}");
1221    }
1222
1223    /// A design with a goal of +∞ fails: violation +∞, behind even a design that breaks a limit;
1224    /// a generation of only such designs leaves no feasible front.
1225    #[test]
1226    fn infinite_goals_fail() {
1227        let n = Nsga2::new(vec![unit("x")], 2)
1228            .unwrap()
1229            .with_population(4)
1230            .unwrap()
1231            .with_generations(1)
1232            .unwrap();
1233        let mut run = n.start(1);
1234        let goals = vec![
1235            Goals::feasible(vec![f64::INFINITY, -100.0]),
1236            Goals::constrained(vec![1.0, 1.0], &[0.5]),
1237            Goals::feasible(vec![2.0, 2.0]),
1238            Goals::feasible(vec![3.0, 1.0]),
1239        ];
1240        let front = run.tell_constrained(&goals).unwrap().unwrap();
1241        let members = run.members();
1242        assert_eq!(members.len(), 4);
1243        let infinite = members
1244            .iter()
1245            .find(|m| m.violation == f64::INFINITY)
1246            .unwrap();
1247        let broken = members.iter().find(|m| m.violation == 0.5).unwrap();
1248        assert_eq!(infinite.objectives, vec![f64::INFINITY; 2]);
1249        assert!(infinite.rank > broken.rank);
1250        assert!(front.is_feasible() && front.members.len() == 2);
1251        let mut run = n.start(2);
1252        let all = vec![
1253            vec![f64::INFINITY, 0.0],
1254            vec![-100.0, f64::INFINITY],
1255            vec![f64::INFINITY, 0.0],
1256            vec![-100.0, f64::INFINITY],
1257        ];
1258        let front = run.tell(&all).unwrap().unwrap();
1259        assert!(!front.is_feasible());
1260    }
1261
1262    #[test]
1263    fn settings_are_checked() {
1264        let x = || vec![unit("x")];
1265        assert!(Nsga2::new(x(), 0).is_err());
1266        assert!(Nsga2::new(x(), MAX_OBJECTIVES + 1).is_err());
1267        assert!(Nsga2::new(vec![], 2).is_err());
1268        let open = Variable::new("x", 0.0, 1.0).unwrap();
1269        assert!(matches!(
1270            Nsga2::new(vec![open.clone()], 2),
1271            Err(AnalysisError::Domain { what, .. }) if what.contains("low bound")
1272        ));
1273        let half = open.within(0.0, f64::INFINITY).unwrap();
1274        assert!(matches!(
1275            Nsga2::new(vec![half], 2),
1276            Err(AnalysisError::Domain { what, .. }) if what.contains("high bound")
1277        ));
1278        let whole = Variable::new("k", 1.0, 1.0)
1279            .unwrap()
1280            .within(0.0, 4.0)
1281            .unwrap()
1282            .integer()
1283            .unwrap();
1284        assert!(matches!(
1285            Nsga2::new(vec![whole], 2),
1286            Err(AnalysisError::Unsupported(_))
1287        ));
1288        let n = Nsga2::new(x(), 2).unwrap();
1289        assert!(matches!(
1290            n.clone().with_population(5),
1291            Err(AnalysisError::Domain { .. })
1292        ));
1293        assert!(matches!(
1294            n.clone().with_population(2),
1295            Err(AnalysisError::TooFew { .. })
1296        ));
1297        assert!(matches!(
1298            n.clone().with_population(MAX_POPULATION + 2),
1299            Err(AnalysisError::Count { .. })
1300        ));
1301        assert!(n.clone().with_population(MAX_POPULATION).is_ok());
1302        let wide = Variable::new("x", 0.0, 1.0)
1303            .unwrap()
1304            .within(-f64::MAX, f64::MAX)
1305            .unwrap();
1306        assert!(matches!(
1307            Nsga2::new(vec![wide], 2),
1308            Err(AnalysisError::Domain { what, .. }) if what.contains("MAX/4")
1309        ));
1310        let edge = |high: f64| {
1311            Variable::new("x", 0.0, 1.0)
1312                .unwrap()
1313                .within(-MAX_BOUND, high)
1314        };
1315        assert!(Nsga2::new(vec![edge(MAX_BOUND).unwrap()], 2).is_ok());
1316        assert!(Nsga2::new(vec![edge(MAX_BOUND.next_up()).unwrap()], 2).is_err());
1317        // At the largest bounds, SBX's cuts still hold: nothing lands on a bound.
1318        let mut rng = SeededRng::seed_from_u64(6);
1319        for _ in 0..20_000 {
1320            let (c1, c2) = sbx(
1321                0.9 * MAX_BOUND,
1322                0.99 * MAX_BOUND,
1323                -MAX_BOUND,
1324                MAX_BOUND,
1325                20.0,
1326                &mut rng,
1327            );
1328            assert!(c1.abs() < MAX_BOUND && c2.abs() < MAX_BOUND);
1329        }
1330        assert!(n.clone().with_generations(0).is_err());
1331        assert!(n.clone().with_crossover(1.1, 20.0).is_err());
1332        assert!(n.clone().with_crossover(f64::NAN, 20.0).is_err());
1333        assert!(n.clone().with_mutation(0.5, -1.0).is_err());
1334        assert!(n.clone().with_mutation(0.5, f64::INFINITY).is_err());
1335        let n = n.with_population(4).unwrap().with_generations(3).unwrap();
1336        assert_eq!((n.population(), n.generations(), n.objectives()), (4, 3, 2));
1337        let json = serde_json::to_string(&n).unwrap();
1338        let back: Nsga2 = serde_json::from_str(&json).unwrap();
1339        assert_eq!(back, n);
1340        let bad = json.replace("\"population\":4", "\"population\":3");
1341        assert!(serde_json::from_str::<Nsga2>(&bad).is_err());
1342    }
1343
1344    #[test]
1345    fn telling_checks_the_goals() {
1346        let n = Nsga2::new(vec![unit("x")], 2)
1347            .unwrap()
1348            .with_population(4)
1349            .unwrap()
1350            .with_generations(2)
1351            .unwrap();
1352        let mut run = n.start(1);
1353        assert_eq!(run.candidates().len(), 4);
1354        assert!(
1355            run.candidates()
1356                .iter()
1357                .flatten()
1358                .all(|x| (0.0..1.0).contains(x))
1359        );
1360        let ok = vec![vec![1.0, 2.0]; 4];
1361        assert!(matches!(
1362            run.tell(&ok[..3]),
1363            Err(AnalysisError::Length { .. })
1364        ));
1365        let mut short = ok.clone();
1366        short[2] = vec![1.0];
1367        assert!(matches!(
1368            run.tell(&short),
1369            Err(AnalysisError::Length { .. })
1370        ));
1371        let mut nan = ok.clone();
1372        nan[3] = vec![1.0, f64::NAN];
1373        assert!(matches!(
1374            run.tell(&nan),
1375            Err(AnalysisError::Output { index: 3, .. })
1376        ));
1377        let mut minus = ok.clone();
1378        minus[1] = vec![f64::NEG_INFINITY, 0.0];
1379        assert!(matches!(
1380            run.tell(&minus),
1381            Err(AnalysisError::Output { index: 1, .. })
1382        ));
1383        let mut goals: Vec<Goals> = ok.iter().cloned().map(Goals::feasible).collect();
1384        goals[0].violation = -1.0;
1385        assert!(matches!(
1386            run.tell_constrained(&goals),
1387            Err(AnalysisError::Domain { .. })
1388        ));
1389        // A failed design may give no goals; the first generation's numbering starts at 0, the
1390        // second's at the population.
1391        goals[0] = Goals::failed();
1392        assert_eq!(run.tell_constrained(&goals).unwrap(), None);
1393        assert_eq!(run.generation(), 1);
1394        assert_eq!(run.members().last().unwrap().violation, f64::INFINITY);
1395        assert!(matches!(
1396            run.tell(&nan),
1397            Err(AnalysisError::Output { index: 7, .. })
1398        ));
1399        let front = run.tell(&ok).unwrap().unwrap();
1400        assert_eq!((front.evaluations, front.generations), (8, 2));
1401        assert!(run.candidates().is_empty());
1402        assert!(matches!(run.tell(&ok), Err(AnalysisError::Length { .. })));
1403        // The front round-trips through JSON, its infinite crowding distances as null.
1404        let json = serde_json::to_string(&front).unwrap();
1405        assert!(json.contains("null"));
1406        assert_eq!(serde_json::from_str::<Front>(&json).unwrap(), front);
1407    }
1408
1409    /// Under constraints, a run that can keep them ends with a feasible front; a failed design
1410    /// never survives while a feasible one can take its place.
1411    #[test]
1412    fn constraints_and_failures_rank_last() {
1413        // Two goals x and 1 − x on [0, 1], with x ≥ 0.6, and x below 0.1 failing.
1414        let n = Nsga2::new(vec![unit("x")], 2)
1415            .unwrap()
1416            .with_population(20)
1417            .unwrap()
1418            .with_generations(30)
1419            .unwrap();
1420        let front = n
1421            .minimize_constrained(5, |x| {
1422                if x[0] < 0.1 {
1423                    Goals::failed()
1424                } else {
1425                    Goals::constrained(vec![x[0], 1.0 - x[0]], &[0.6 - x[0]])
1426                }
1427            })
1428            .unwrap();
1429        assert!(front.is_feasible());
1430        assert_eq!(front.members.len(), 20);
1431        assert!(front.members.iter().all(|m| m.point[0] >= 0.6));
1432        // Every design on [0.6, 1] is on the front: the front's ends reach close to both.
1433        let low = front.members.iter().map(|m| m.point[0]).fold(1.0, f64::min);
1434        let high = front.members.iter().map(|m| m.point[0]).fold(0.0, f64::max);
1435        assert!(low < 0.61 && high > 0.99, "{low} to {high}");
1436    }
1437
1438    #[test]
1439    fn distances_by_hand() {
1440        let set = vec![vec![0.0, 0.0], vec![3.0, 4.0]];
1441        let reference = vec![vec![0.0, 1.0], vec![3.0, 0.0]];
1442        // Nearest: 1 and 4 → GD 2.5; from the reference: 1 and 3 → IGD 2.
1443        assert_eq!(generational_distance(&set, &reference), 2.5);
1444        assert_eq!(inverted_generational_distance(&set, &reference), 2.0);
1445        assert!(generational_distance(&[], &reference).is_nan());
1446    }
1447}