Skip to main content

fcmaes_core/
mapelites.rs

1// Numeric kernels index parallel arrays by a shared counter.
2#![allow(clippy::needless_range_loop)]
3
4//! Quality-Diversity search with CVT-MAP-Elites and the Diversifier.
5//!
6//! A CVT (centroidal Voronoi tessellation) archive partitions the behavior
7//! space into `capacity` niches (via k-means over uniform samples); each niche
8//! keeps the best solution found for it. MAP-Elites fills the archive with an
9//! SBX / Iso+LineDD emitter driven by a quality-diversity fitness
10//! `x -> (fitness, behavior descriptor)`. The Diversifier generalizes CMA-ME:
11//! it drives an ask/tell optimizer (here Rust CMA-ES) whose objective is the
12//! per-niche *improvement*, so the search fills and improves niches.
13//!
14//! Archive mutation remains serial and deterministic. Objective evaluation can
15//! be supplied either point-by-point through [`QdFitness`] or in parallel
16//! batches through [`QdBatchFitness`].
17//!
18//! # References
19//!
20//! - J.-B. Mouret and J. Clune, [“Illuminating Search Spaces by Mapping
21//!   Elites”](https://arxiv.org/abs/1504.04909) (2015).
22//! - V. Vassiliades, K. Chatzilygeroudis, and J.-B. Mouret, [“Using
23//!   Centroidal Voronoi Tessellations to Scale Up the Multi-dimensional
24//!   Archive of Phenotypic Elites
25//!   Algorithm”](https://arxiv.org/abs/1610.05729) (2016).
26//!
27//! # Example
28//!
29//! ```
30//! use fcmaes_core::{map_elites, Archive, MapElitesParams, Rng};
31//!
32//! let mut rng = Rng::new(42);
33//! let mut archive = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 16, 0, &mut rng);
34//! archive.seed_uniform(&[0.0; 2], &[1.0; 2], &mut rng);
35//! let mut fitness = |x: &[f64]| {
36//!     let quality = x.iter().map(|v| (v - 0.5).powi(2)).sum();
37//!     (quality, x.to_vec())
38//! };
39//! let params = MapElitesParams {
40//!     generations: 4,
41//!     chunk_size: 8,
42//!     ..Default::default()
43//! };
44//! map_elites(
45//!     &mut archive,
46//!     &mut fitness,
47//!     &[0.0; 2],
48//!     &[1.0; 2],
49//!     &params,
50//!     &mut rng,
51//! );
52//! assert!(archive.occupied() > 0);
53//! ```
54
55use crate::cmaes::{Cmaes, CmaesParams};
56use crate::fitness::Fitness;
57use crate::rng::Rng;
58use rayon::prelude::*;
59
60/// Quality-diversity fitness: maps a solution to `(fitness, behavior)`.
61pub trait QdFitness {
62    /// Evaluate one decoded solution, returning minimized quality and behavior descriptors.
63    fn eval(&mut self, x: &[f64]) -> (f64, Vec<f64>);
64}
65
66impl<F> QdFitness for F
67where
68    F: FnMut(&[f64]) -> (f64, Vec<f64>),
69{
70    fn eval(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
71        self(x)
72    }
73}
74
75/// Batch quality-diversity fitness. Implementations may evaluate `xs` in
76/// parallel, but must return one result per input in the same order.
77pub trait QdBatchFitness {
78    /// Evaluate decoded solutions in order.
79    fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)>;
80}
81
82impl<F> QdBatchFitness for F
83where
84    F: FnMut(&[Vec<f64>]) -> Vec<(f64, Vec<f64>)>,
85{
86    fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)> {
87        self(xs)
88    }
89}
90
91struct SerialQdBatchFitness<'a> {
92    fitness: &'a mut dyn QdFitness,
93}
94
95impl QdBatchFitness for SerialQdBatchFitness<'_> {
96    fn eval_batch(&mut self, xs: &[Vec<f64>]) -> Vec<(f64, Vec<f64>)> {
97        xs.iter().map(|x| self.fitness.eval(x)).collect()
98    }
99}
100
101// ---------------------------------------------------------------------------
102// k-means++ (CVT niche centers)
103// ---------------------------------------------------------------------------
104
105fn dist2(a: &[f64], b: &[f64]) -> f64 {
106    a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
107}
108
109/// Uniform two-dimensional grid with exactly `k` centers. Rows differ by at
110/// most one column when `k` is not a square.
111fn grid_centers_2d(k: usize) -> Vec<Vec<f64>> {
112    let rows = (k as f64).sqrt().floor().max(1.0) as usize;
113    let base_columns = k / rows;
114    let extra_columns = k % rows;
115    let mut centers = Vec::with_capacity(k);
116    for row in 0..rows {
117        let columns = base_columns + usize::from(row < extra_columns);
118        for column in 0..columns {
119            centers.push(vec![
120                (column as f64 + 0.5) / columns as f64,
121                (row as f64 + 0.5) / rows as f64,
122            ]);
123        }
124    }
125    centers
126}
127
128fn grid_index_2d(k: usize, descriptor: &[f64]) -> usize {
129    let rows = (k as f64).sqrt().floor().max(1.0) as usize;
130    let base_columns = k / rows;
131    let extra_columns = k % rows;
132    let y = descriptor[1].clamp(0.0, 1.0 - f64::EPSILON);
133    let row = (y * rows as f64) as usize;
134    let columns = base_columns + usize::from(row < extra_columns);
135    let x = descriptor[0].clamp(0.0, 1.0 - f64::EPSILON);
136    let column = (x * columns as f64) as usize;
137    row * base_columns + row.min(extra_columns) + column
138}
139
140/// Compute `k` niche centers in `[0,1]^dim` via k-means++ over
141/// `k * samples_per_niche` uniform samples (Lloyd iterations).
142fn cvt_centers(k: usize, dim: usize, samples_per_niche: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
143    let n = (k * samples_per_niche).max(k);
144    let samples: Vec<Vec<f64>> = (0..n)
145        .map(|_| (0..dim).map(|_| rng.uniform01()).collect())
146        .collect();
147
148    // k-means++ init.
149    let mut centers: Vec<Vec<f64>> = Vec::with_capacity(k);
150    centers.push(samples[rng.int_below(n as i64) as usize].clone());
151    let mut d2: Vec<f64> = samples.iter().map(|s| dist2(s, &centers[0])).collect();
152    while centers.len() < k {
153        let sum: f64 = d2.iter().sum();
154        let mut target = rng.uniform01() * sum;
155        let mut idx = 0;
156        for (i, &d) in d2.iter().enumerate() {
157            target -= d;
158            if target <= 0.0 {
159                idx = i;
160                break;
161            }
162        }
163        centers.push(samples[idx].clone());
164        let c = centers.last().unwrap();
165        d2.par_iter_mut().zip(&samples).for_each(|(distance, s)| {
166            let candidate = dist2(s, c);
167            if candidate < *distance {
168                *distance = candidate;
169            }
170        });
171    }
172
173    // A few Lloyd iterations. Assignment is the dominant O(samples * k)
174    // kernel, so use thread-local accumulators and merge them in parallel.
175    for _ in 0..10 {
176        let (sums, counts) = samples
177            .par_iter()
178            .fold(
179                || (vec![0.0; k * dim], vec![0usize; k]),
180                |(mut sums, mut counts), s| {
181                    let mut best = 0;
182                    let mut best_distance = f64::MAX;
183                    for (index, center) in centers.iter().enumerate() {
184                        let distance = dist2(s, center);
185                        if distance < best_distance {
186                            best_distance = distance;
187                            best = index;
188                        }
189                    }
190                    counts[best] += 1;
191                    for j in 0..dim {
192                        sums[best * dim + j] += s[j];
193                    }
194                    (sums, counts)
195                },
196            )
197            .reduce(
198                || (vec![0.0; k * dim], vec![0usize; k]),
199                |(mut left_sums, mut left_counts), (right_sums, right_counts)| {
200                    for index in 0..k {
201                        left_counts[index] += right_counts[index];
202                    }
203                    for index in 0..k * dim {
204                        left_sums[index] += right_sums[index];
205                    }
206                    (left_sums, left_counts)
207                },
208            );
209        let mut max_shift = 0.0_f64;
210        for ci in 0..k {
211            if counts[ci] > 0 {
212                let old = centers[ci].clone();
213                for j in 0..dim {
214                    centers[ci][j] = sums[ci * dim + j] / counts[ci] as f64;
215                }
216                max_shift = max_shift.max(dist2(&old, &centers[ci]));
217            }
218        }
219        if max_shift <= 1e-12 {
220            break;
221        }
222    }
223    centers
224}
225
226// ---------------------------------------------------------------------------
227// Archive
228// ---------------------------------------------------------------------------
229
230/// Exact row layout of a regular two-dimensional MAP-Elites archive.
231///
232/// The first `extra_columns` rows contain `base_columns + 1` cells; remaining
233/// rows contain `base_columns`. This represents every capacity exactly,
234/// including ragged grids such as 60 cells arranged over seven rows.
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236pub struct GridLayout {
237    /// Number of descriptor rows.
238    pub rows: usize,
239    /// Columns present in every row.
240    pub base_columns: usize,
241    /// Number of leading rows containing one additional column.
242    pub extra_columns: usize,
243}
244
245impl GridLayout {
246    /// Exact number of cells described by this layout.
247    pub fn cells(&self) -> usize {
248        self.rows * self.base_columns + self.extra_columns
249    }
250
251    /// Number of columns in `row`, or `None` when the row is out of range.
252    pub fn columns_in_row(&self, row: usize) -> Option<usize> {
253        (row < self.rows).then(|| self.base_columns + usize::from(row < self.extra_columns))
254    }
255
256    /// Maximum number of columns in any row.
257    pub fn max_columns(&self) -> usize {
258        self.base_columns + usize::from(self.extra_columns > 0)
259    }
260}
261
262/// CVT quality-diversity archive: `capacity` niches, each holding the best
263/// solution found for it.
264pub struct Archive {
265    dim: usize,
266    qd_dim: usize,
267    capacity: usize,
268    desc_lb: Vec<f64>,
269    desc_scale: Vec<f64>,
270    centers: Vec<Vec<f64>>, // normalized [0,1]^qd_dim
271    grid_2d: bool,
272    xs: Vec<Vec<f64>>,
273    ds: Vec<Vec<f64>>,
274    ys: Vec<f64>,
275    counts: Vec<u64>,
276    occupied: usize,
277    si: Vec<usize>, // niche indices sorted ascending by fitness
278}
279
280impl Archive {
281    /// Construct a validated CVT archive.
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if `dim` or `capacity` is zero, if the descriptor
286    /// bound slices are empty or of unequal length, or if any bound pair is
287    /// non-finite or does not satisfy `lower < upper`.
288    pub fn try_new(
289        dim: usize,
290        qd_lb: &[f64],
291        qd_ub: &[f64],
292        capacity: usize,
293        samples_per_niche: usize,
294        rng: &mut Rng,
295    ) -> Result<Self, &'static str> {
296        if dim == 0 {
297            return Err("archive decision dimension must be positive");
298        }
299        if qd_lb.is_empty() || qd_lb.len() != qd_ub.len() {
300            return Err("descriptor bounds must be non-empty and have equal lengths");
301        }
302        if qd_lb
303            .iter()
304            .zip(qd_ub)
305            .any(|(&lo, &hi)| !lo.is_finite() || !hi.is_finite() || lo >= hi)
306        {
307            return Err("descriptor bounds must be finite and satisfy lower < upper");
308        }
309        if capacity == 0 {
310            return Err("archive capacity must be positive");
311        }
312        Ok(Self::new_unchecked(
313            dim,
314            qd_lb,
315            qd_ub,
316            capacity,
317            samples_per_niche,
318            rng,
319        ))
320    }
321
322    /// Construct a CVT archive, panicking on invalid configuration. Prefer
323    /// [`Archive::try_new`] for user-supplied inputs.
324    ///
325    /// # Panics
326    ///
327    /// Panics on any configuration [`Archive::try_new`] rejects.
328    pub fn new(
329        dim: usize,
330        qd_lb: &[f64],
331        qd_ub: &[f64],
332        capacity: usize,
333        samples_per_niche: usize,
334        rng: &mut Rng,
335    ) -> Self {
336        Self::try_new(dim, qd_lb, qd_ub, capacity, samples_per_niche, rng)
337            .expect("invalid archive configuration")
338    }
339
340    fn new_unchecked(
341        dim: usize,
342        qd_lb: &[f64],
343        qd_ub: &[f64],
344        capacity: usize,
345        samples_per_niche: usize,
346        rng: &mut Rng,
347    ) -> Self {
348        let qd_dim = qd_lb.len();
349        let desc_lb = qd_lb.to_vec();
350        let desc_scale: Vec<f64> = qd_ub.iter().zip(qd_lb).map(|(u, l)| u - l).collect();
351        let grid_2d = samples_per_niche == 0 && qd_dim == 2;
352        let centers = if grid_2d {
353            grid_centers_2d(capacity)
354        } else {
355            cvt_centers(capacity, qd_dim, samples_per_niche.max(1), rng)
356        };
357        Archive {
358            dim,
359            qd_dim,
360            capacity,
361            desc_lb,
362            desc_scale,
363            centers,
364            grid_2d,
365            xs: vec![vec![0.0; dim]; capacity],
366            ds: vec![vec![0.0; qd_dim]; capacity],
367            ys: vec![f64::INFINITY; capacity],
368            counts: vec![0; capacity],
369            occupied: 0,
370            si: (0..capacity).collect(),
371        }
372    }
373
374    /// Number of decision variables stored for each elite.
375    pub fn dim(&self) -> usize {
376        self.dim
377    }
378    /// Number of behavior-descriptor dimensions.
379    pub fn qd_dim(&self) -> usize {
380        self.qd_dim
381    }
382    /// Total number of niches.
383    pub fn capacity(&self) -> usize {
384        self.capacity
385    }
386    /// Number of niches containing an evaluated elite.
387    pub fn occupied(&self) -> usize {
388        self.occupied
389    }
390
391    /// Exact layout of a regular two-dimensional archive.
392    ///
393    /// Returns `None` for CVT archives and descriptor dimensions other than
394    /// two. Use [`capacity`](Self::capacity) as the coverage denominator and
395    /// this layout when mapping a niche index to a rendered row.
396    pub fn grid_layout(&self) -> Option<GridLayout> {
397        if !self.grid_2d {
398            return None;
399        }
400        let rows = (self.capacity as f64).sqrt().floor().max(1.0) as usize;
401        Some(GridLayout {
402            rows,
403            base_columns: self.capacity / rows,
404            extra_columns: self.capacity % rows,
405        })
406    }
407
408    /// Shape of a regular two-dimensional archive.
409    ///
410    /// The first component is the number of columns and the second the number
411    /// of rows. Returns `None` for CVT archives and for descriptor dimensions
412    /// other than two. Non-rectangular regular grids report the maximum column
413    /// count; early rows may contain one additional cell as documented by the
414    /// archive's exact-capacity construction.
415    pub fn grid_shape(&self) -> Option<(usize, usize)> {
416        self.grid_layout()
417            .map(|layout| (layout.max_columns(), layout.rows))
418    }
419
420    /// Seed all niche solutions with uniform random samples in `[lower, upper]`
421    /// (never evaluated — they serve as the initial SBX/Iso parent pool, as the
422    /// Python original documents).
423    ///
424    /// # Panics
425    ///
426    /// Panics if `lower.len()` or `upper.len()` differs from the archive's
427    /// decision dimension.
428    pub fn seed_uniform(&mut self, lower: &[f64], upper: &[f64], rng: &mut Rng) {
429        assert_eq!(lower.len(), self.dim, "lower bounds length must equal dim");
430        assert_eq!(upper.len(), self.dim, "upper bounds length must equal dim");
431        assert!(
432            lower
433                .iter()
434                .zip(upper)
435                .all(|(&lo, &hi)| lo.is_finite() && hi.is_finite() && lo < hi),
436            "decision bounds must be finite and satisfy lower < upper"
437        );
438        for x in self.xs.iter_mut() {
439            for i in 0..self.dim {
440                x[i] = lower[i] + (upper[i] - lower[i]) * rng.uniform01();
441            }
442        }
443    }
444
445    fn encode_d(&self, d: &[f64]) -> Vec<f64> {
446        (0..self.qd_dim)
447            .map(|i| (d[i] - self.desc_lb[i]) / self.desc_scale[i])
448            .collect()
449    }
450
451    /// Index of the niche whose center is nearest the (encoded) descriptor.
452    ///
453    /// # Panics
454    ///
455    /// Panics if `d.len()` differs from the archive's descriptor dimension.
456    pub fn index_of_niche(&self, d: &[f64]) -> usize {
457        assert_eq!(d.len(), self.qd_dim, "descriptor length must equal qd_dim");
458        let e = self.encode_d(d);
459        if self.grid_2d {
460            return grid_index_2d(self.capacity, &e);
461        }
462        let mut best = 0;
463        let mut bd = f64::MAX;
464        for (i, c) in self.centers.iter().enumerate() {
465            let dist = dist2(&e, c);
466            if dist < bd {
467                bd = dist;
468                best = i;
469            }
470        }
471        best
472    }
473
474    /// Add a solution to niche `i` if it improves it.
475    ///
476    /// # Panics
477    ///
478    /// Panics if `i` is not below the archive capacity, or if `d` or `x` do
479    /// not match the descriptor and decision dimensions.
480    pub fn set(&mut self, i: usize, y: f64, d: &[f64], x: &[f64]) {
481        assert!(i < self.capacity, "niche index out of bounds");
482        assert_eq!(d.len(), self.qd_dim, "descriptor length mismatch");
483        assert_eq!(x.len(), self.dim, "solution length mismatch");
484        self.counts[i] += 1;
485        if y.is_finite() && d.iter().all(|v| v.is_finite()) && y < self.ys[i] {
486            if self.ys[i].is_infinite() {
487                self.occupied += 1;
488            }
489            self.ys[i] = y;
490            self.xs[i].copy_from_slice(x);
491            self.ds[i].copy_from_slice(d);
492        }
493    }
494
495    /// Evaluate `xs`, add to the archive, and return `(improvements, real_ys)`
496    /// where `improvement = fitness - niche's previous fitness` (negative is an
497    /// improvement — the objective the Diversifier's optimizer minimizes).
498    ///
499    /// # Panics
500    ///
501    /// Panics if `fitness` returns a descriptor whose length differs from the
502    /// archive's descriptor dimension.
503    pub fn update(&mut self, xs: &[Vec<f64>], fitness: &mut dyn QdFitness) -> (Vec<f64>, Vec<f64>) {
504        let evaluations: Vec<(f64, Vec<f64>)> = xs.iter().map(|x| fitness.eval(x)).collect();
505        self.update_evaluated(xs, &evaluations)
506            .expect("serial QD evaluation preserves batch length")
507    }
508
509    /// Apply already evaluated `(fitness, descriptor)` values in input order.
510    /// Keeping this step separate lets callers parallelize expensive objective
511    /// functions without concurrently mutating the archive.
512    ///
513    /// # Errors
514    ///
515    /// Returns an error if `evaluated` does not have the same length as `xs`.
516    pub fn update_evaluated(
517        &mut self,
518        xs: &[Vec<f64>],
519        evaluations: &[(f64, Vec<f64>)],
520    ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
521        if xs.len() != evaluations.len() {
522            return Err("QD evaluation batch length must match candidate batch length");
523        }
524        let mut improvements = Vec::with_capacity(xs.len());
525        let mut real_ys = Vec::with_capacity(xs.len());
526        for (x, (y, desc)) in xs.iter().zip(evaluations) {
527            if x.len() != self.dim
528                || desc.len() != self.qd_dim
529                || !y.is_finite()
530                || desc.iter().any(|value| !value.is_finite())
531            {
532                improvements.push(f64::INFINITY);
533                real_ys.push(f64::INFINITY);
534                continue;
535            }
536            let niche = self.index_of_niche(desc);
537            let oldy = self.ys[niche];
538            let improvement = if oldy.is_infinite() { *y } else { *y - oldy };
539            self.set(niche, *y, desc, x);
540            improvements.push(improvement);
541            real_ys.push(*y);
542        }
543        Ok((improvements, real_ys))
544    }
545
546    /// Evaluate and apply a complete batch. Evaluation may be parallel inside
547    /// `fitness`; archive updates are deterministic and retain input order.
548    ///
549    /// # Errors
550    ///
551    /// Returns an error if `fitness` does not return exactly one
552    /// `(fitness, descriptor)` pair per candidate.
553    pub fn update_batch(
554        &mut self,
555        xs: &[Vec<f64>],
556        fitness: &mut dyn QdBatchFitness,
557    ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
558        let evaluations = fitness.eval_batch(xs);
559        self.update_evaluated(xs, &evaluations)
560    }
561
562    /// Re-sort niche indices ascending by fitness.
563    pub fn argsort(&mut self) {
564        let mut si: Vec<usize> = (0..self.capacity).collect();
565        si.sort_by(|&a, &b| self.ys[a].total_cmp(&self.ys[b]));
566        self.si = si;
567    }
568
569    /// Sample `chunk` solutions from the best `best_n` niches (by fitness).
570    pub fn random_xs(&self, best_n: usize, chunk: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
571        let bn = best_n.max(1).min(self.capacity);
572        (0..chunk)
573            .map(|_| {
574                let sel = rng.int_below(bn as i64) as usize;
575                let niche = if bn < self.capacity {
576                    self.si[sel]
577                } else {
578                    sel
579                };
580                self.xs[niche].clone()
581            })
582            .collect()
583    }
584
585    /// A random solution from the best `best_n` niches (with fitness).
586    pub fn random_x_one(&self, best_n: usize, rng: &mut Rng) -> (Vec<f64>, f64) {
587        let bn = best_n.max(1).min(self.capacity);
588        let selected = rng.int_below(bn as i64) as usize;
589        let niche = if bn < self.capacity {
590            self.si[selected]
591        } else {
592            selected
593        };
594        (self.xs[niche].clone(), self.ys[niche])
595    }
596
597    /// Lowest finite quality currently stored, or infinity for an empty archive.
598    pub fn best_y(&self) -> f64 {
599        self.ys.iter().cloned().fold(f64::INFINITY, f64::min)
600    }
601
602    /// QD-score matching Python `Archive.get_qd_score`: for an all-positive
603    /// archive, sum reciprocal fitness; otherwise sum the negated negative
604    /// fitness values. Higher is better in both cases.
605    pub fn qd_score(&self) -> f64 {
606        let finite: Vec<f64> = self
607            .ys
608            .iter()
609            .copied()
610            .filter(|value| value.is_finite())
611            .collect();
612        if finite.is_empty() {
613            return 0.0;
614        }
615        if finite.iter().copied().fold(f64::INFINITY, f64::min) > 0.0 {
616            finite
617                .iter()
618                .filter(|&&value| value != 0.0)
619                .map(|&value| value.recip())
620                .sum()
621        } else {
622            finite
623                .iter()
624                .filter(|&&value| value < 0.0)
625                .map(|&value| -value)
626                .sum()
627        }
628    }
629
630    /// Per-niche quality values; empty niches contain infinity.
631    pub fn ys(&self) -> &[f64] {
632        &self.ys
633    }
634    /// Per-niche decision vectors.
635    pub fn xs(&self) -> &[Vec<f64>] {
636        &self.xs
637    }
638    /// Per-niche behavior descriptors.
639    pub fn descriptors(&self) -> &[Vec<f64>] {
640        &self.ds
641    }
642    /// Number of evaluated candidates mapped to each niche.
643    pub fn counts(&self) -> &[u64] {
644        &self.counts
645    }
646
647    /// Occupied `(x, y, descriptor)` triples.
648    pub fn occupied_data(&self) -> Vec<(Vec<f64>, f64, Vec<f64>)> {
649        (0..self.capacity)
650            .filter(|&i| self.ys[i].is_finite())
651            .map(|i| (self.xs[i].clone(), self.ys[i], self.ds[i].clone()))
652            .collect()
653    }
654}
655
656// ---------------------------------------------------------------------------
657// Emitters
658// ---------------------------------------------------------------------------
659
660/// SBX (simulated binary crossover) + polynomial mutation (the Python
661/// `variation_`), clamped to `[lower, upper]`.
662pub fn variation(
663    pop: &[Vec<f64>],
664    lower: &[f64],
665    upper: &[f64],
666    rng: &mut Rng,
667    dis_c: f64,
668    dis_m: f64,
669) -> Vec<Vec<f64>> {
670    let dis_c = dis_c * (0.5 + 0.5 * rng.uniform01());
671    let dis_m = dis_m * (0.5 + 0.5 * rng.uniform01());
672    let n = (pop.len() / 2) * 2;
673    let d = lower.len();
674    let half = n / 2;
675    let mut offspring = vec![vec![0.0; d]; n];
676    for p in 0..half {
677        let p1 = &pop[p];
678        let p2 = &pop[half + p];
679        for i in 0..d {
680            let mu = rng.uniform01();
681            let mut beta = if mu <= 0.5 {
682                (2.0 * mu).powf(1.0 / (dis_c + 1.0))
683            } else {
684                (2.0 * mu).powf(-1.0 / (dis_c + 1.0))
685            };
686            if rng.int_below(2) == 1 {
687                beta = -beta;
688            }
689            if rng.uniform01() < 0.5 {
690                beta = 1.0;
691            }
692            let mean = (p1[i] + p2[i]) * 0.5;
693            let diff = (p1[i] - p2[i]) * 0.5;
694            offspring[p][i] = mean + beta * diff;
695            offspring[half + p][i] = mean - beta * diff;
696        }
697    }
698    // polynomial mutation
699    let site_p = 1.0 / d as f64;
700    for op in offspring.iter_mut() {
701        for i in 0..d {
702            if rng.uniform01() < site_p {
703                let mu = rng.uniform01();
704                let span = upper[i] - lower[i];
705                if mu <= 0.5 {
706                    let norm = (op[i] - lower[i]) / span;
707                    op[i] += span
708                        * ((2.0 * mu + (1.0 - 2.0 * mu) * (1.0 - norm).abs().powf(dis_m + 1.0))
709                            .powf(1.0 / (dis_m + 1.0))
710                            - 1.0);
711                } else {
712                    let norm = (upper[i] - op[i]) / span;
713                    op[i] += span
714                        * (1.0
715                            - (2.0 * (1.0 - mu)
716                                + 2.0 * (mu - 0.5) * (1.0 - norm).abs().powf(dis_m + 1.0))
717                            .powf(1.0 / (dis_m + 1.0)));
718                }
719            }
720            op[i] = op[i].clamp(lower[i], upper[i]);
721        }
722    }
723    offspring
724}
725
726/// Iso+LineDD emitter (the Python `iso_dd_`): `x1 + N(0,iso) + N(0,line)*(x1-x2)`.
727pub fn iso_dd(
728    x1: &[Vec<f64>],
729    x2: &[Vec<f64>],
730    lower: &[f64],
731    upper: &[f64],
732    rng: &mut Rng,
733    iso_sigma: f64,
734    line_sigma: f64,
735) -> Vec<Vec<f64>> {
736    let d = lower.len();
737    x1.iter()
738        .zip(x2)
739        .map(|(a, b)| {
740            (0..d)
741                .map(|i| {
742                    let z = a[i]
743                        + rng.normreal(0.0, iso_sigma)
744                        + rng.normreal(0.0, line_sigma) * (a[i] - b[i]);
745                    z.clamp(lower[i], upper[i])
746                })
747                .collect()
748        })
749        .collect()
750}
751
752// ---------------------------------------------------------------------------
753// Drivers
754// ---------------------------------------------------------------------------
755
756/// MAP-Elites parameters.
757#[derive(Clone, Debug)]
758pub struct MapElitesParams {
759    /// Number of ordinary emitter generations.
760    pub generations: usize,
761    /// Candidates requested per ordinary generation.
762    pub chunk_size: usize,
763    /// Use simulated binary crossover; false selects Iso+LineDD.
764    pub use_sbx: bool,
765    /// Simulated-binary-crossover distribution index.
766    pub dis_c: f64,
767    /// Polynomial-mutation distribution index.
768    pub dis_m: f64,
769    /// Isotropic noise standard deviation for Iso+LineDD.
770    pub iso_sigma: f64,
771    /// Directional noise standard deviation for Iso+LineDD.
772    pub line_sigma: f64,
773    /// Additional CMA-ES emitter generations after ordinary generations.
774    pub cma_generations: usize,
775}
776
777impl Default for MapElitesParams {
778    fn default() -> Self {
779        Self {
780            generations: 100,
781            chunk_size: 20,
782            use_sbx: true,
783            dis_c: 20.0,
784            dis_m: 20.0,
785            iso_sigma: 0.02,
786            line_sigma: 0.2,
787            cma_generations: 0,
788        }
789    }
790}
791
792/// Run CVT-MAP-Elites into `archive` using the SBX / Iso+LineDD emitter, with
793/// optional CMA-ES emitter generations.
794///
795/// # Panics
796///
797/// Panics if the serial adapter around `fitness` returns a batch of the wrong
798/// length, which indicates a bug in this crate rather than in caller code.
799/// Use [`map_elites_batch`] to handle batch-length mismatches as an error.
800pub fn map_elites(
801    archive: &mut Archive,
802    fitness: &mut dyn QdFitness,
803    lower: &[f64],
804    upper: &[f64],
805    p: &MapElitesParams,
806    rng: &mut Rng,
807) {
808    let mut batch_fitness = SerialQdBatchFitness { fitness };
809    map_elites_batch(archive, &mut batch_fitness, lower, upper, p, rng)
810        .expect("serial QD evaluation preserves batch length");
811}
812
813/// Batch-evaluation variant of [`map_elites`]. Candidate generation and
814/// archive updates remain deterministic; `fitness` controls evaluation
815/// parallelism.
816///
817/// # Errors
818///
819/// Returns an error if `fitness` does not return one result per requested
820/// candidate.
821pub fn map_elites_batch(
822    archive: &mut Archive,
823    fitness: &mut dyn QdBatchFitness,
824    lower: &[f64],
825    upper: &[f64],
826    p: &MapElitesParams,
827    rng: &mut Rng,
828) -> Result<(), &'static str> {
829    map_elites_batch_with_progress(archive, fitness, lower, upper, p, rng, &mut |_, _| {})
830}
831
832/// Batch MAP-Elites with an ordered callback after every archive update.
833///
834/// The callback runs after the evaluated generation has been committed and
835/// sorted. It is intended for convergence logging and must not mutate the
836/// archive. The generation index is one-based and continues through optional
837/// CMA-emitter generations.
838///
839/// # Errors
840///
841/// Returns an error if `fitness` does not return one result per requested
842/// candidate. The archive keeps every generation committed before the
843/// failure.
844pub fn map_elites_batch_with_progress(
845    archive: &mut Archive,
846    fitness: &mut dyn QdBatchFitness,
847    lower: &[f64],
848    upper: &[f64],
849    p: &MapElitesParams,
850    rng: &mut Rng,
851    progress: &mut dyn FnMut(usize, &Archive),
852) -> Result<(), &'static str> {
853    let mut select_n = archive.capacity();
854    for generation in 0..p.generations {
855        let xs = if p.use_sbx {
856            let pop = archive.random_xs(select_n, p.chunk_size, rng);
857            variation(&pop, lower, upper, rng, p.dis_c, p.dis_m)
858        } else {
859            let x1 = archive.random_xs(select_n, p.chunk_size, rng);
860            let x2 = archive.random_xs(select_n, p.chunk_size, rng);
861            iso_dd(&x1, &x2, lower, upper, rng, p.iso_sigma, p.line_sigma)
862        };
863        archive.update_batch(&xs, fitness)?;
864        archive.argsort();
865        select_n = archive.occupied().max(1);
866        progress(generation + 1, archive);
867    }
868    for generation in 0..p.cma_generations {
869        cma_emitter_batch(archive, fitness, lower, upper, rng)?;
870        archive.argsort();
871        progress(p.generations + generation + 1, archive);
872    }
873    Ok(())
874}
875
876/// One CMA-ES emitter run: seed CMA-ES at a random good niche and drive it by
877/// per-niche improvement (the Python `optimize_cma_`).
878fn cma_emitter_batch(
879    archive: &mut Archive,
880    fitness: &mut dyn QdBatchFitness,
881    lower: &[f64],
882    upper: &[f64],
883    rng: &mut Rng,
884) -> Result<(), &'static str> {
885    let best_n = 100.min(archive.capacity());
886    let (x0, _) = archive.random_x_one(best_n, rng);
887    let sigma = {
888        let u = 0.03 + rng.uniform01() * 0.27;
889        u * u
890    };
891    let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
892    fit.set_normalize(true);
893    let params = CmaesParams {
894        popsize: 31,
895        max_evaluations: 100_000,
896        seed: rng.int_below(i64::MAX) as u64,
897        ..Default::default()
898    };
899    let mut es = Cmaes::new(fit, &x0, &[sigma], &params);
900    let stall = 5;
901    let mut last_improve = 0i32;
902    let mut old_ys: Option<Vec<f64>> = None;
903    for iter in 0..100 {
904        let xs = es.ask();
905        let (improvement, _real) = archive.update_batch(&xs, fitness)?;
906        let mut sorted = improvement.clone();
907        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
908        if let Some(oy) = &old_ys
909            && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
910        {
911            last_improve = iter;
912        }
913        if last_improve + stall < iter {
914            break;
915        }
916        if es.tell(&improvement) != 0 {
917            break;
918        }
919        old_ys = Some(sorted);
920    }
921    Ok(())
922}
923
924/// Diversifier parameters.
925#[derive(Clone, Debug)]
926pub struct DiversifierParams {
927    /// Maximum number of candidate evaluations.
928    pub max_evaluations: u64,
929    /// CMA-ES population size used by each emitter.
930    pub popsize: i32,
931    /// Stop an emitter after this many generations without improvement.
932    pub stall_criterion: i32,
933}
934
935impl Default for DiversifierParams {
936    fn default() -> Self {
937        Self {
938            max_evaluations: 100_000,
939            popsize: 31,
940            stall_criterion: 20,
941        }
942    }
943}
944
945/// The Diversifier meta-algorithm (CMA-ME-style): drive a CMA-ES ask/tell loop
946/// whose objective is per-niche improvement, filling the archive. Returns the
947/// best real solution found.
948///
949/// # Panics
950///
951/// Panics if the serial adapter around `fitness` returns a batch of the wrong
952/// length, which indicates a bug in this crate rather than in caller code.
953/// Use [`diversify_batch`] to handle batch-length mismatches as an error.
954pub fn diversify(
955    archive: &mut Archive,
956    fitness: &mut dyn QdFitness,
957    lower: &[f64],
958    upper: &[f64],
959    p: &DiversifierParams,
960    rng: &mut Rng,
961) -> (Vec<f64>, f64) {
962    let mut batch_fitness = SerialQdBatchFitness { fitness };
963    diversify_batch(archive, &mut batch_fitness, lower, upper, p, rng)
964        .expect("serial QD evaluation preserves batch length")
965}
966
967/// Batch-evaluation variant of [`diversify`]. CMA-ES asks and tells remain
968/// serial while each requested population can be evaluated concurrently.
969///
970/// # Errors
971///
972/// Returns an error if `fitness` does not return one result per requested
973/// candidate.
974pub fn diversify_batch(
975    archive: &mut Archive,
976    fitness: &mut dyn QdBatchFitness,
977    lower: &[f64],
978    upper: &[f64],
979    p: &DiversifierParams,
980    rng: &mut Rng,
981) -> Result<(Vec<f64>, f64), &'static str> {
982    let mut best_x = vec![0.0; archive.dim()];
983    let mut best_y = f64::INFINITY;
984    let mut evals: u64 = 0;
985    while evals < p.max_evaluations {
986        let (x0, _) = archive.random_x_one(archive.occupied().max(1), rng);
987        let sigma = {
988            let u = 0.03 + rng.uniform01() * 0.27;
989            u * u
990        };
991        let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
992        fit.set_normalize(true);
993        let params = CmaesParams {
994            popsize: p.popsize,
995            max_evaluations: 100_000,
996            seed: rng.int_below(i64::MAX) as u64,
997            ..Default::default()
998        };
999        let mut es = Cmaes::new(fit, &x0, &[sigma], &params);
1000        let max_iters = 50_000 / p.popsize.max(1) as usize;
1001        let stall = p.stall_criterion;
1002        let mut last_improve = 0i32;
1003        let mut old_ys: Option<Vec<f64>> = None;
1004        for iter in 0..max_iters as i32 {
1005            let xs = es.ask();
1006            let (improvement, real_ys) = archive.update_batch(&xs, fitness)?;
1007            evals += xs.len() as u64;
1008            // track best real solution
1009            for (x, &ry) in xs.iter().zip(&real_ys) {
1010                if ry < best_y {
1011                    best_y = ry;
1012                    best_x.copy_from_slice(x);
1013                }
1014            }
1015            let mut sorted = improvement.clone();
1016            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1017            if let Some(oy) = &old_ys
1018                && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
1019            {
1020                last_improve = iter;
1021            }
1022            if last_improve + stall < iter {
1023                break;
1024            }
1025            if es.tell(&improvement) != 0 || evals >= p.max_evaluations {
1026                break;
1027            }
1028            old_ys = Some(sorted);
1029        }
1030        archive.argsort();
1031    }
1032    Ok((best_x, best_y))
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    use super::*;
1038
1039    // A QD problem: minimize sphere; behavior = first two coordinates.
1040    fn qd(x: &[f64]) -> (f64, Vec<f64>) {
1041        let f = x.iter().map(|v| v * v).sum();
1042        (f, vec![x[0], x[1]])
1043    }
1044
1045    #[test]
1046    fn cvt_centers_are_in_unit_box() {
1047        let mut rng = Rng::new(1);
1048        let c = cvt_centers(20, 2, 10, &mut rng);
1049        assert_eq!(c.len(), 20);
1050        for center in &c {
1051            for &v in center {
1052                assert!((0.0..=1.0).contains(&v));
1053            }
1054        }
1055    }
1056
1057    #[test]
1058    fn grid_centers_are_fast_exact_and_cover_the_box() {
1059        for capacity in [1, 10, 64, 1_000] {
1060            let centers = grid_centers_2d(capacity);
1061            assert_eq!(centers.len(), capacity);
1062            assert!(
1063                centers
1064                    .iter()
1065                    .flatten()
1066                    .all(|value| (0.0..=1.0).contains(value))
1067            );
1068        }
1069        let mut rng = Rng::new(1);
1070        let archive = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 100, 0, &mut rng);
1071        assert_eq!(archive.centers.len(), 100);
1072        assert_eq!(archive.index_of_niche(&[0.0, 0.0]), 0);
1073        assert_eq!(archive.index_of_niche(&[1.0, 1.0]), 99);
1074    }
1075
1076    #[test]
1077    fn grid_layout_represents_rectangular_and_ragged_capacities_exactly() {
1078        let mut rng = Rng::new(11);
1079        let rectangular = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 120, 0, &mut rng);
1080        let layout = rectangular.grid_layout().unwrap();
1081        assert_eq!(
1082            layout,
1083            GridLayout {
1084                rows: 10,
1085                base_columns: 12,
1086                extra_columns: 0,
1087            }
1088        );
1089        assert_eq!(layout.cells(), rectangular.capacity());
1090        assert_eq!(layout.columns_in_row(9), Some(12));
1091        assert_eq!(layout.columns_in_row(10), None);
1092        assert_eq!(rectangular.grid_shape(), Some((12, 10)));
1093
1094        let ragged = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 60, 0, &mut rng);
1095        let layout = ragged.grid_layout().unwrap();
1096        assert_eq!(
1097            layout,
1098            GridLayout {
1099                rows: 7,
1100                base_columns: 8,
1101                extra_columns: 4,
1102            }
1103        );
1104        assert_eq!(layout.cells(), ragged.capacity());
1105        assert_eq!(layout.columns_in_row(0), Some(9));
1106        assert_eq!(layout.columns_in_row(3), Some(9));
1107        assert_eq!(layout.columns_in_row(4), Some(8));
1108        assert_eq!(ragged.grid_shape(), Some((9, 7)));
1109    }
1110
1111    #[test]
1112    fn map_elites_fills_niches() {
1113        let mut rng = Rng::new(2);
1114        let lower = vec![-2.0; 4];
1115        let upper = vec![2.0; 4];
1116        let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
1117        // seed the archive with random samples so SBX has parents
1118        let seed_pop: Vec<Vec<f64>> = (0..64)
1119            .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
1120            .collect();
1121        archive.update(&seed_pop, &mut qd);
1122        archive.argsort();
1123        let params = MapElitesParams {
1124            generations: 200,
1125            chunk_size: 16,
1126            ..Default::default()
1127        };
1128        map_elites(&mut archive, &mut qd, &lower, &upper, &params, &mut rng);
1129        assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
1130        assert!(archive.best_y() < 0.5, "best_y={}", archive.best_y());
1131    }
1132
1133    #[test]
1134    fn evaluated_batches_match_serial_updates_and_validate_lengths() {
1135        let mut rng_a = Rng::new(7);
1136        let mut rng_b = Rng::new(7);
1137        let mut serial = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_a);
1138        let mut batch = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_b);
1139        let xs = vec![vec![-1.0, 0.5], vec![0.25, -0.75], vec![1.0, 1.0]];
1140        let expected = serial.update(&xs, &mut qd);
1141        let evaluations: Vec<_> = xs.iter().map(|x| qd(x)).collect();
1142        let actual = batch.update_evaluated(&xs, &evaluations).unwrap();
1143        assert_eq!(actual, expected);
1144        assert_eq!(batch.ys(), serial.ys());
1145        assert_eq!(batch.descriptors(), serial.descriptors());
1146        assert!(batch.update_evaluated(&xs, &evaluations[..2]).is_err());
1147    }
1148
1149    #[test]
1150    fn batch_map_elites_preserves_serial_results() {
1151        let lower = vec![-2.0; 4];
1152        let upper = vec![2.0; 4];
1153        let mut rng_a = Rng::new(19);
1154        let mut rng_b = Rng::new(19);
1155        let mut serial = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_a);
1156        let mut batch = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_b);
1157        serial.seed_uniform(&lower, &upper, &mut rng_a);
1158        batch.seed_uniform(&lower, &upper, &mut rng_b);
1159        let initial = serial.xs().to_vec();
1160        let initial_evaluations: Vec<_> = initial.iter().map(|x| qd(x)).collect();
1161        serial.update(&initial, &mut qd);
1162        batch
1163            .update_evaluated(&initial, &initial_evaluations)
1164            .unwrap();
1165        serial.argsort();
1166        batch.argsort();
1167        let params = MapElitesParams {
1168            generations: 10,
1169            chunk_size: 8,
1170            ..Default::default()
1171        };
1172        map_elites(&mut serial, &mut qd, &lower, &upper, &params, &mut rng_a);
1173        let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
1174        map_elites_batch(
1175            &mut batch,
1176            &mut batch_qd,
1177            &lower,
1178            &upper,
1179            &params,
1180            &mut rng_b,
1181        )
1182        .unwrap();
1183        assert_eq!(batch.ys(), serial.ys());
1184        assert_eq!(batch.xs(), serial.xs());
1185        assert_eq!(batch.descriptors(), serial.descriptors());
1186    }
1187
1188    #[test]
1189    fn batch_progress_callback_is_ordered_and_observes_committed_updates() {
1190        let lower = vec![-2.0; 4];
1191        let upper = vec![2.0; 4];
1192        let mut rng = Rng::new(23);
1193        let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng);
1194        archive.seed_uniform(&lower, &upper, &mut rng);
1195        let params = MapElitesParams {
1196            generations: 5,
1197            chunk_size: 8,
1198            ..Default::default()
1199        };
1200        let mut observed = Vec::new();
1201        let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
1202        map_elites_batch_with_progress(
1203            &mut archive,
1204            &mut batch_qd,
1205            &lower,
1206            &upper,
1207            &params,
1208            &mut rng,
1209            &mut |generation, committed| {
1210                observed.push((generation, committed.occupied(), committed.best_y()));
1211            },
1212        )
1213        .unwrap();
1214        assert_eq!(
1215            observed.iter().map(|sample| sample.0).collect::<Vec<_>>(),
1216            vec![1, 2, 3, 4, 5]
1217        );
1218        assert!(
1219            observed
1220                .iter()
1221                .all(|(_, occupied, best)| *occupied > 0 && best.is_finite())
1222        );
1223        assert!(observed.windows(2).all(|pair| pair[1].2 <= pair[0].2));
1224    }
1225
1226    #[test]
1227    fn diversify_improves_and_fills() {
1228        let mut rng = Rng::new(3);
1229        let lower = vec![-2.0; 4];
1230        let upper = vec![2.0; 4];
1231        let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
1232        let seed_pop: Vec<Vec<f64>> = (0..64)
1233            .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
1234            .collect();
1235        archive.update(&seed_pop, &mut qd);
1236        let params = DiversifierParams {
1237            max_evaluations: 20_000,
1238            ..Default::default()
1239        };
1240        let (bx, by) = diversify(&mut archive, &mut qd, &lower, &upper, &params, &mut rng);
1241        assert_eq!(bx.len(), 4);
1242        assert!(by < 1e-3, "diversifier best_y={by}");
1243        assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
1244    }
1245
1246    #[test]
1247    fn archive_validation_and_bad_evaluations() {
1248        let mut rng = Rng::new(4);
1249        assert!(Archive::try_new(0, &[0.0], &[1.0], 4, 2, &mut rng).is_err());
1250        assert!(Archive::try_new(2, &[0.0], &[0.0], 4, 2, &mut rng).is_err());
1251        assert!(Archive::try_new(2, &[0.0], &[1.0], 0, 2, &mut rng).is_err());
1252
1253        let mut archive = Archive::try_new(2, &[0.0], &[1.0], 4, 2, &mut rng).unwrap();
1254        let (improvements, values) =
1255            archive.update(&[vec![0.0, 0.0]], &mut |_: &[f64]| (f64::NAN, vec![0.5]));
1256        assert!(improvements[0].is_infinite());
1257        assert!(values[0].is_infinite());
1258        assert_eq!(archive.occupied(), 0);
1259    }
1260
1261    #[test]
1262    fn qd_score_matches_positive_and_negative_python_rules() {
1263        let mut rng = Rng::new(5);
1264        let mut positive = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1265        positive.set(0, 2.0, &[0.1], &[0.0]);
1266        positive.set(1, 4.0, &[0.5], &[0.0]);
1267        assert_eq!(positive.qd_score(), 0.75);
1268
1269        let mut mixed = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1270        mixed.set(0, -2.0, &[0.1], &[0.0]);
1271        mixed.set(1, 4.0, &[0.5], &[0.0]);
1272        assert_eq!(mixed.qd_score(), 2.0);
1273    }
1274
1275    #[test]
1276    fn random_x_one_uses_sorted_best_niches() {
1277        let mut rng = Rng::new(6);
1278        let mut archive = Archive::new(1, &[0.0], &[1.0], 4, 2, &mut rng);
1279        for (index, value) in [4.0, 1.0, 3.0, 2.0].into_iter().enumerate() {
1280            archive.set(index, value, &[index as f64 / 4.0], &[index as f64]);
1281        }
1282        archive.argsort();
1283        for _ in 0..10 {
1284            let (x, y) = archive.random_x_one(1, &mut rng);
1285            assert_eq!(x, vec![1.0]);
1286            assert_eq!(y, 1.0);
1287        }
1288    }
1289}