1#![allow(clippy::needless_range_loop)]
3
4use crate::cmaes::{Cmaes, CmaesParams};
56use crate::fitness::Fitness;
57use crate::rng::Rng;
58use rayon::prelude::*;
59
60pub trait QdFitness {
62 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
75pub trait QdBatchFitness {
78 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
101fn dist2(a: &[f64], b: &[f64]) -> f64 {
106 a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
107}
108
109fn 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
140fn 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 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, ¢ers[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 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, ¢ers[ci]));
217 }
218 }
219 if max_shift <= 1e-12 {
220 break;
221 }
222 }
223 centers
224}
225
226pub struct Archive {
233 dim: usize,
234 qd_dim: usize,
235 capacity: usize,
236 desc_lb: Vec<f64>,
237 desc_scale: Vec<f64>,
238 centers: Vec<Vec<f64>>, grid_2d: bool,
240 xs: Vec<Vec<f64>>,
241 ds: Vec<Vec<f64>>,
242 ys: Vec<f64>,
243 counts: Vec<u64>,
244 occupied: usize,
245 si: Vec<usize>, }
247
248impl Archive {
249 pub fn try_new(
257 dim: usize,
258 qd_lb: &[f64],
259 qd_ub: &[f64],
260 capacity: usize,
261 samples_per_niche: usize,
262 rng: &mut Rng,
263 ) -> Result<Self, &'static str> {
264 if dim == 0 {
265 return Err("archive decision dimension must be positive");
266 }
267 if qd_lb.is_empty() || qd_lb.len() != qd_ub.len() {
268 return Err("descriptor bounds must be non-empty and have equal lengths");
269 }
270 if qd_lb
271 .iter()
272 .zip(qd_ub)
273 .any(|(&lo, &hi)| !lo.is_finite() || !hi.is_finite() || lo >= hi)
274 {
275 return Err("descriptor bounds must be finite and satisfy lower < upper");
276 }
277 if capacity == 0 {
278 return Err("archive capacity must be positive");
279 }
280 Ok(Self::new_unchecked(
281 dim,
282 qd_lb,
283 qd_ub,
284 capacity,
285 samples_per_niche,
286 rng,
287 ))
288 }
289
290 pub fn new(
297 dim: usize,
298 qd_lb: &[f64],
299 qd_ub: &[f64],
300 capacity: usize,
301 samples_per_niche: usize,
302 rng: &mut Rng,
303 ) -> Self {
304 Self::try_new(dim, qd_lb, qd_ub, capacity, samples_per_niche, rng)
305 .expect("invalid archive configuration")
306 }
307
308 fn new_unchecked(
309 dim: usize,
310 qd_lb: &[f64],
311 qd_ub: &[f64],
312 capacity: usize,
313 samples_per_niche: usize,
314 rng: &mut Rng,
315 ) -> Self {
316 let qd_dim = qd_lb.len();
317 let desc_lb = qd_lb.to_vec();
318 let desc_scale: Vec<f64> = qd_ub.iter().zip(qd_lb).map(|(u, l)| u - l).collect();
319 let grid_2d = samples_per_niche == 0 && qd_dim == 2;
320 let centers = if grid_2d {
321 grid_centers_2d(capacity)
322 } else {
323 cvt_centers(capacity, qd_dim, samples_per_niche.max(1), rng)
324 };
325 Archive {
326 dim,
327 qd_dim,
328 capacity,
329 desc_lb,
330 desc_scale,
331 centers,
332 grid_2d,
333 xs: vec![vec![0.0; dim]; capacity],
334 ds: vec![vec![0.0; qd_dim]; capacity],
335 ys: vec![f64::INFINITY; capacity],
336 counts: vec![0; capacity],
337 occupied: 0,
338 si: (0..capacity).collect(),
339 }
340 }
341
342 pub fn dim(&self) -> usize {
344 self.dim
345 }
346 pub fn qd_dim(&self) -> usize {
348 self.qd_dim
349 }
350 pub fn capacity(&self) -> usize {
352 self.capacity
353 }
354 pub fn occupied(&self) -> usize {
356 self.occupied
357 }
358
359 pub fn seed_uniform(&mut self, lower: &[f64], upper: &[f64], rng: &mut Rng) {
368 assert_eq!(lower.len(), self.dim, "lower bounds length must equal dim");
369 assert_eq!(upper.len(), self.dim, "upper bounds length must equal dim");
370 assert!(
371 lower
372 .iter()
373 .zip(upper)
374 .all(|(&lo, &hi)| lo.is_finite() && hi.is_finite() && lo < hi),
375 "decision bounds must be finite and satisfy lower < upper"
376 );
377 for x in self.xs.iter_mut() {
378 for i in 0..self.dim {
379 x[i] = lower[i] + (upper[i] - lower[i]) * rng.uniform01();
380 }
381 }
382 }
383
384 fn encode_d(&self, d: &[f64]) -> Vec<f64> {
385 (0..self.qd_dim)
386 .map(|i| (d[i] - self.desc_lb[i]) / self.desc_scale[i])
387 .collect()
388 }
389
390 pub fn index_of_niche(&self, d: &[f64]) -> usize {
396 assert_eq!(d.len(), self.qd_dim, "descriptor length must equal qd_dim");
397 let e = self.encode_d(d);
398 if self.grid_2d {
399 return grid_index_2d(self.capacity, &e);
400 }
401 let mut best = 0;
402 let mut bd = f64::MAX;
403 for (i, c) in self.centers.iter().enumerate() {
404 let dist = dist2(&e, c);
405 if dist < bd {
406 bd = dist;
407 best = i;
408 }
409 }
410 best
411 }
412
413 pub fn set(&mut self, i: usize, y: f64, d: &[f64], x: &[f64]) {
420 assert!(i < self.capacity, "niche index out of bounds");
421 assert_eq!(d.len(), self.qd_dim, "descriptor length mismatch");
422 assert_eq!(x.len(), self.dim, "solution length mismatch");
423 self.counts[i] += 1;
424 if y.is_finite() && d.iter().all(|v| v.is_finite()) && y < self.ys[i] {
425 if self.ys[i].is_infinite() {
426 self.occupied += 1;
427 }
428 self.ys[i] = y;
429 self.xs[i].copy_from_slice(x);
430 self.ds[i].copy_from_slice(d);
431 }
432 }
433
434 pub fn update(&mut self, xs: &[Vec<f64>], fitness: &mut dyn QdFitness) -> (Vec<f64>, Vec<f64>) {
443 let evaluations: Vec<(f64, Vec<f64>)> = xs.iter().map(|x| fitness.eval(x)).collect();
444 self.update_evaluated(xs, &evaluations)
445 .expect("serial QD evaluation preserves batch length")
446 }
447
448 pub fn update_evaluated(
456 &mut self,
457 xs: &[Vec<f64>],
458 evaluations: &[(f64, Vec<f64>)],
459 ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
460 if xs.len() != evaluations.len() {
461 return Err("QD evaluation batch length must match candidate batch length");
462 }
463 let mut improvements = Vec::with_capacity(xs.len());
464 let mut real_ys = Vec::with_capacity(xs.len());
465 for (x, (y, desc)) in xs.iter().zip(evaluations) {
466 if x.len() != self.dim
467 || desc.len() != self.qd_dim
468 || !y.is_finite()
469 || desc.iter().any(|value| !value.is_finite())
470 {
471 improvements.push(f64::INFINITY);
472 real_ys.push(f64::INFINITY);
473 continue;
474 }
475 let niche = self.index_of_niche(desc);
476 let oldy = self.ys[niche];
477 let improvement = if oldy.is_infinite() { *y } else { *y - oldy };
478 self.set(niche, *y, desc, x);
479 improvements.push(improvement);
480 real_ys.push(*y);
481 }
482 Ok((improvements, real_ys))
483 }
484
485 pub fn update_batch(
493 &mut self,
494 xs: &[Vec<f64>],
495 fitness: &mut dyn QdBatchFitness,
496 ) -> Result<(Vec<f64>, Vec<f64>), &'static str> {
497 let evaluations = fitness.eval_batch(xs);
498 self.update_evaluated(xs, &evaluations)
499 }
500
501 pub fn argsort(&mut self) {
503 let mut si: Vec<usize> = (0..self.capacity).collect();
504 si.sort_by(|&a, &b| self.ys[a].total_cmp(&self.ys[b]));
505 self.si = si;
506 }
507
508 pub fn random_xs(&self, best_n: usize, chunk: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
510 let bn = best_n.max(1).min(self.capacity);
511 (0..chunk)
512 .map(|_| {
513 let sel = rng.int_below(bn as i64) as usize;
514 let niche = if bn < self.capacity {
515 self.si[sel]
516 } else {
517 sel
518 };
519 self.xs[niche].clone()
520 })
521 .collect()
522 }
523
524 pub fn random_x_one(&self, best_n: usize, rng: &mut Rng) -> (Vec<f64>, f64) {
526 let bn = best_n.max(1).min(self.capacity);
527 let selected = rng.int_below(bn as i64) as usize;
528 let niche = if bn < self.capacity {
529 self.si[selected]
530 } else {
531 selected
532 };
533 (self.xs[niche].clone(), self.ys[niche])
534 }
535
536 pub fn best_y(&self) -> f64 {
538 self.ys.iter().cloned().fold(f64::INFINITY, f64::min)
539 }
540
541 pub fn qd_score(&self) -> f64 {
545 let finite: Vec<f64> = self
546 .ys
547 .iter()
548 .copied()
549 .filter(|value| value.is_finite())
550 .collect();
551 if finite.is_empty() {
552 return 0.0;
553 }
554 if finite.iter().copied().fold(f64::INFINITY, f64::min) > 0.0 {
555 finite
556 .iter()
557 .filter(|&&value| value != 0.0)
558 .map(|&value| value.recip())
559 .sum()
560 } else {
561 finite
562 .iter()
563 .filter(|&&value| value < 0.0)
564 .map(|&value| -value)
565 .sum()
566 }
567 }
568
569 pub fn ys(&self) -> &[f64] {
571 &self.ys
572 }
573 pub fn xs(&self) -> &[Vec<f64>] {
575 &self.xs
576 }
577 pub fn descriptors(&self) -> &[Vec<f64>] {
579 &self.ds
580 }
581 pub fn counts(&self) -> &[u64] {
583 &self.counts
584 }
585
586 pub fn occupied_data(&self) -> Vec<(Vec<f64>, f64, Vec<f64>)> {
588 (0..self.capacity)
589 .filter(|&i| self.ys[i].is_finite())
590 .map(|i| (self.xs[i].clone(), self.ys[i], self.ds[i].clone()))
591 .collect()
592 }
593}
594
595pub fn variation(
602 pop: &[Vec<f64>],
603 lower: &[f64],
604 upper: &[f64],
605 rng: &mut Rng,
606 dis_c: f64,
607 dis_m: f64,
608) -> Vec<Vec<f64>> {
609 let dis_c = dis_c * (0.5 + 0.5 * rng.uniform01());
610 let dis_m = dis_m * (0.5 + 0.5 * rng.uniform01());
611 let n = (pop.len() / 2) * 2;
612 let d = lower.len();
613 let half = n / 2;
614 let mut offspring = vec![vec![0.0; d]; n];
615 for p in 0..half {
616 let p1 = &pop[p];
617 let p2 = &pop[half + p];
618 for i in 0..d {
619 let mu = rng.uniform01();
620 let mut beta = if mu <= 0.5 {
621 (2.0 * mu).powf(1.0 / (dis_c + 1.0))
622 } else {
623 (2.0 * mu).powf(-1.0 / (dis_c + 1.0))
624 };
625 if rng.int_below(2) == 1 {
626 beta = -beta;
627 }
628 if rng.uniform01() < 0.5 {
629 beta = 1.0;
630 }
631 let mean = (p1[i] + p2[i]) * 0.5;
632 let diff = (p1[i] - p2[i]) * 0.5;
633 offspring[p][i] = mean + beta * diff;
634 offspring[half + p][i] = mean - beta * diff;
635 }
636 }
637 let site_p = 1.0 / d as f64;
639 for op in offspring.iter_mut() {
640 for i in 0..d {
641 if rng.uniform01() < site_p {
642 let mu = rng.uniform01();
643 let span = upper[i] - lower[i];
644 if mu <= 0.5 {
645 let norm = (op[i] - lower[i]) / span;
646 op[i] += span
647 * ((2.0 * mu + (1.0 - 2.0 * mu) * (1.0 - norm).abs().powf(dis_m + 1.0))
648 .powf(1.0 / (dis_m + 1.0))
649 - 1.0);
650 } else {
651 let norm = (upper[i] - op[i]) / span;
652 op[i] += span
653 * (1.0
654 - (2.0 * (1.0 - mu)
655 + 2.0 * (mu - 0.5) * (1.0 - norm).abs().powf(dis_m + 1.0))
656 .powf(1.0 / (dis_m + 1.0)));
657 }
658 }
659 op[i] = op[i].clamp(lower[i], upper[i]);
660 }
661 }
662 offspring
663}
664
665pub fn iso_dd(
667 x1: &[Vec<f64>],
668 x2: &[Vec<f64>],
669 lower: &[f64],
670 upper: &[f64],
671 rng: &mut Rng,
672 iso_sigma: f64,
673 line_sigma: f64,
674) -> Vec<Vec<f64>> {
675 let d = lower.len();
676 x1.iter()
677 .zip(x2)
678 .map(|(a, b)| {
679 (0..d)
680 .map(|i| {
681 let z = a[i]
682 + rng.normreal(0.0, iso_sigma)
683 + rng.normreal(0.0, line_sigma) * (a[i] - b[i]);
684 z.clamp(lower[i], upper[i])
685 })
686 .collect()
687 })
688 .collect()
689}
690
691#[derive(Clone, Debug)]
697pub struct MapElitesParams {
698 pub generations: usize,
700 pub chunk_size: usize,
702 pub use_sbx: bool,
704 pub dis_c: f64,
706 pub dis_m: f64,
708 pub iso_sigma: f64,
710 pub line_sigma: f64,
712 pub cma_generations: usize,
714}
715
716impl Default for MapElitesParams {
717 fn default() -> Self {
718 Self {
719 generations: 100,
720 chunk_size: 20,
721 use_sbx: true,
722 dis_c: 20.0,
723 dis_m: 20.0,
724 iso_sigma: 0.02,
725 line_sigma: 0.2,
726 cma_generations: 0,
727 }
728 }
729}
730
731pub fn map_elites(
740 archive: &mut Archive,
741 fitness: &mut dyn QdFitness,
742 lower: &[f64],
743 upper: &[f64],
744 p: &MapElitesParams,
745 rng: &mut Rng,
746) {
747 let mut batch_fitness = SerialQdBatchFitness { fitness };
748 map_elites_batch(archive, &mut batch_fitness, lower, upper, p, rng)
749 .expect("serial QD evaluation preserves batch length");
750}
751
752pub fn map_elites_batch(
761 archive: &mut Archive,
762 fitness: &mut dyn QdBatchFitness,
763 lower: &[f64],
764 upper: &[f64],
765 p: &MapElitesParams,
766 rng: &mut Rng,
767) -> Result<(), &'static str> {
768 map_elites_batch_with_progress(archive, fitness, lower, upper, p, rng, &mut |_, _| {})
769}
770
771pub fn map_elites_batch_with_progress(
784 archive: &mut Archive,
785 fitness: &mut dyn QdBatchFitness,
786 lower: &[f64],
787 upper: &[f64],
788 p: &MapElitesParams,
789 rng: &mut Rng,
790 progress: &mut dyn FnMut(usize, &Archive),
791) -> Result<(), &'static str> {
792 let mut select_n = archive.capacity();
793 for generation in 0..p.generations {
794 let xs = if p.use_sbx {
795 let pop = archive.random_xs(select_n, p.chunk_size, rng);
796 variation(&pop, lower, upper, rng, p.dis_c, p.dis_m)
797 } else {
798 let x1 = archive.random_xs(select_n, p.chunk_size, rng);
799 let x2 = archive.random_xs(select_n, p.chunk_size, rng);
800 iso_dd(&x1, &x2, lower, upper, rng, p.iso_sigma, p.line_sigma)
801 };
802 archive.update_batch(&xs, fitness)?;
803 archive.argsort();
804 select_n = archive.occupied().max(1);
805 progress(generation + 1, archive);
806 }
807 for generation in 0..p.cma_generations {
808 cma_emitter_batch(archive, fitness, lower, upper, rng)?;
809 archive.argsort();
810 progress(p.generations + generation + 1, archive);
811 }
812 Ok(())
813}
814
815fn cma_emitter_batch(
818 archive: &mut Archive,
819 fitness: &mut dyn QdBatchFitness,
820 lower: &[f64],
821 upper: &[f64],
822 rng: &mut Rng,
823) -> Result<(), &'static str> {
824 let best_n = 100.min(archive.capacity());
825 let (x0, _) = archive.random_x_one(best_n, rng);
826 let sigma = {
827 let u = 0.03 + rng.uniform01() * 0.27;
828 u * u
829 };
830 let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
831 fit.set_normalize(true);
832 let params = CmaesParams {
833 popsize: 31,
834 max_evaluations: 100_000,
835 seed: rng.int_below(i64::MAX) as u64,
836 ..Default::default()
837 };
838 let mut es = Cmaes::new(fit, &x0, &[sigma], ¶ms);
839 let stall = 5;
840 let mut last_improve = 0i32;
841 let mut old_ys: Option<Vec<f64>> = None;
842 for iter in 0..100 {
843 let xs = es.ask();
844 let (improvement, _real) = archive.update_batch(&xs, fitness)?;
845 let mut sorted = improvement.clone();
846 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
847 if let Some(oy) = &old_ys
848 && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
849 {
850 last_improve = iter;
851 }
852 if last_improve + stall < iter {
853 break;
854 }
855 if es.tell(&improvement) != 0 {
856 break;
857 }
858 old_ys = Some(sorted);
859 }
860 Ok(())
861}
862
863#[derive(Clone, Debug)]
865pub struct DiversifierParams {
866 pub max_evaluations: u64,
868 pub popsize: i32,
870 pub stall_criterion: i32,
872}
873
874impl Default for DiversifierParams {
875 fn default() -> Self {
876 Self {
877 max_evaluations: 100_000,
878 popsize: 31,
879 stall_criterion: 20,
880 }
881 }
882}
883
884pub fn diversify(
894 archive: &mut Archive,
895 fitness: &mut dyn QdFitness,
896 lower: &[f64],
897 upper: &[f64],
898 p: &DiversifierParams,
899 rng: &mut Rng,
900) -> (Vec<f64>, f64) {
901 let mut batch_fitness = SerialQdBatchFitness { fitness };
902 diversify_batch(archive, &mut batch_fitness, lower, upper, p, rng)
903 .expect("serial QD evaluation preserves batch length")
904}
905
906pub fn diversify_batch(
914 archive: &mut Archive,
915 fitness: &mut dyn QdBatchFitness,
916 lower: &[f64],
917 upper: &[f64],
918 p: &DiversifierParams,
919 rng: &mut Rng,
920) -> Result<(Vec<f64>, f64), &'static str> {
921 let mut best_x = vec![0.0; archive.dim()];
922 let mut best_y = f64::INFINITY;
923 let mut evals: u64 = 0;
924 while evals < p.max_evaluations {
925 let (x0, _) = archive.random_x_one(archive.occupied().max(1), rng);
926 let sigma = {
927 let u = 0.03 + rng.uniform01() * 0.27;
928 u * u
929 };
930 let mut fit = Fitness::bounded(archive.dim(), 1, lower, upper);
931 fit.set_normalize(true);
932 let params = CmaesParams {
933 popsize: p.popsize,
934 max_evaluations: 100_000,
935 seed: rng.int_below(i64::MAX) as u64,
936 ..Default::default()
937 };
938 let mut es = Cmaes::new(fit, &x0, &[sigma], ¶ms);
939 let max_iters = 50_000 / p.popsize.max(1) as usize;
940 let stall = p.stall_criterion;
941 let mut last_improve = 0i32;
942 let mut old_ys: Option<Vec<f64>> = None;
943 for iter in 0..max_iters as i32 {
944 let xs = es.ask();
945 let (improvement, real_ys) = archive.update_batch(&xs, fitness)?;
946 evals += xs.len() as u64;
947 for (x, &ry) in xs.iter().zip(&real_ys) {
949 if ry < best_y {
950 best_y = ry;
951 best_x.copy_from_slice(x);
952 }
953 }
954 let mut sorted = improvement.clone();
955 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
956 if let Some(oy) = &old_ys
957 && sorted.iter().zip(oy).any(|(&a, &b)| a < b)
958 {
959 last_improve = iter;
960 }
961 if last_improve + stall < iter {
962 break;
963 }
964 if es.tell(&improvement) != 0 || evals >= p.max_evaluations {
965 break;
966 }
967 old_ys = Some(sorted);
968 }
969 archive.argsort();
970 }
971 Ok((best_x, best_y))
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977
978 fn qd(x: &[f64]) -> (f64, Vec<f64>) {
980 let f = x.iter().map(|v| v * v).sum();
981 (f, vec![x[0], x[1]])
982 }
983
984 #[test]
985 fn cvt_centers_are_in_unit_box() {
986 let mut rng = Rng::new(1);
987 let c = cvt_centers(20, 2, 10, &mut rng);
988 assert_eq!(c.len(), 20);
989 for center in &c {
990 for &v in center {
991 assert!((0.0..=1.0).contains(&v));
992 }
993 }
994 }
995
996 #[test]
997 fn grid_centers_are_fast_exact_and_cover_the_box() {
998 for capacity in [1, 10, 64, 1_000] {
999 let centers = grid_centers_2d(capacity);
1000 assert_eq!(centers.len(), capacity);
1001 assert!(
1002 centers
1003 .iter()
1004 .flatten()
1005 .all(|value| (0.0..=1.0).contains(value))
1006 );
1007 }
1008 let mut rng = Rng::new(1);
1009 let archive = Archive::new(2, &[0.0, 0.0], &[1.0, 1.0], 100, 0, &mut rng);
1010 assert_eq!(archive.centers.len(), 100);
1011 assert_eq!(archive.index_of_niche(&[0.0, 0.0]), 0);
1012 assert_eq!(archive.index_of_niche(&[1.0, 1.0]), 99);
1013 }
1014
1015 #[test]
1016 fn map_elites_fills_niches() {
1017 let mut rng = Rng::new(2);
1018 let lower = vec![-2.0; 4];
1019 let upper = vec![2.0; 4];
1020 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
1021 let seed_pop: Vec<Vec<f64>> = (0..64)
1023 .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
1024 .collect();
1025 archive.update(&seed_pop, &mut qd);
1026 archive.argsort();
1027 let params = MapElitesParams {
1028 generations: 200,
1029 chunk_size: 16,
1030 ..Default::default()
1031 };
1032 map_elites(&mut archive, &mut qd, &lower, &upper, ¶ms, &mut rng);
1033 assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
1034 assert!(archive.best_y() < 0.5, "best_y={}", archive.best_y());
1035 }
1036
1037 #[test]
1038 fn evaluated_batches_match_serial_updates_and_validate_lengths() {
1039 let mut rng_a = Rng::new(7);
1040 let mut rng_b = Rng::new(7);
1041 let mut serial = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_a);
1042 let mut batch = Archive::new(2, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng_b);
1043 let xs = vec![vec![-1.0, 0.5], vec![0.25, -0.75], vec![1.0, 1.0]];
1044 let expected = serial.update(&xs, &mut qd);
1045 let evaluations: Vec<_> = xs.iter().map(|x| qd(x)).collect();
1046 let actual = batch.update_evaluated(&xs, &evaluations).unwrap();
1047 assert_eq!(actual, expected);
1048 assert_eq!(batch.ys(), serial.ys());
1049 assert_eq!(batch.descriptors(), serial.descriptors());
1050 assert!(batch.update_evaluated(&xs, &evaluations[..2]).is_err());
1051 }
1052
1053 #[test]
1054 fn batch_map_elites_preserves_serial_results() {
1055 let lower = vec![-2.0; 4];
1056 let upper = vec![2.0; 4];
1057 let mut rng_a = Rng::new(19);
1058 let mut rng_b = Rng::new(19);
1059 let mut serial = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_a);
1060 let mut batch = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 32, 0, &mut rng_b);
1061 serial.seed_uniform(&lower, &upper, &mut rng_a);
1062 batch.seed_uniform(&lower, &upper, &mut rng_b);
1063 let initial = serial.xs().to_vec();
1064 let initial_evaluations: Vec<_> = initial.iter().map(|x| qd(x)).collect();
1065 serial.update(&initial, &mut qd);
1066 batch
1067 .update_evaluated(&initial, &initial_evaluations)
1068 .unwrap();
1069 serial.argsort();
1070 batch.argsort();
1071 let params = MapElitesParams {
1072 generations: 10,
1073 chunk_size: 8,
1074 ..Default::default()
1075 };
1076 map_elites(&mut serial, &mut qd, &lower, &upper, ¶ms, &mut rng_a);
1077 let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
1078 map_elites_batch(
1079 &mut batch,
1080 &mut batch_qd,
1081 &lower,
1082 &upper,
1083 ¶ms,
1084 &mut rng_b,
1085 )
1086 .unwrap();
1087 assert_eq!(batch.ys(), serial.ys());
1088 assert_eq!(batch.xs(), serial.xs());
1089 assert_eq!(batch.descriptors(), serial.descriptors());
1090 }
1091
1092 #[test]
1093 fn batch_progress_callback_is_ordered_and_observes_committed_updates() {
1094 let lower = vec![-2.0; 4];
1095 let upper = vec![2.0; 4];
1096 let mut rng = Rng::new(23);
1097 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 16, 0, &mut rng);
1098 archive.seed_uniform(&lower, &upper, &mut rng);
1099 let params = MapElitesParams {
1100 generations: 5,
1101 chunk_size: 8,
1102 ..Default::default()
1103 };
1104 let mut observed = Vec::new();
1105 let mut batch_qd = |xs: &[Vec<f64>]| xs.iter().map(|x| qd(x)).collect();
1106 map_elites_batch_with_progress(
1107 &mut archive,
1108 &mut batch_qd,
1109 &lower,
1110 &upper,
1111 ¶ms,
1112 &mut rng,
1113 &mut |generation, committed| {
1114 observed.push((generation, committed.occupied(), committed.best_y()));
1115 },
1116 )
1117 .unwrap();
1118 assert_eq!(
1119 observed.iter().map(|sample| sample.0).collect::<Vec<_>>(),
1120 vec![1, 2, 3, 4, 5]
1121 );
1122 assert!(
1123 observed
1124 .iter()
1125 .all(|(_, occupied, best)| *occupied > 0 && best.is_finite())
1126 );
1127 assert!(observed.windows(2).all(|pair| pair[1].2 <= pair[0].2));
1128 }
1129
1130 #[test]
1131 fn diversify_improves_and_fills() {
1132 let mut rng = Rng::new(3);
1133 let lower = vec![-2.0; 4];
1134 let upper = vec![2.0; 4];
1135 let mut archive = Archive::new(4, &[-2.0, -2.0], &[2.0, 2.0], 64, 10, &mut rng);
1136 let seed_pop: Vec<Vec<f64>> = (0..64)
1137 .map(|_| (0..4).map(|_| -2.0 + 4.0 * rng.uniform01()).collect())
1138 .collect();
1139 archive.update(&seed_pop, &mut qd);
1140 let params = DiversifierParams {
1141 max_evaluations: 20_000,
1142 ..Default::default()
1143 };
1144 let (bx, by) = diversify(&mut archive, &mut qd, &lower, &upper, ¶ms, &mut rng);
1145 assert_eq!(bx.len(), 4);
1146 assert!(by < 1e-3, "diversifier best_y={by}");
1147 assert!(archive.occupied() > 20, "occupied={}", archive.occupied());
1148 }
1149
1150 #[test]
1151 fn archive_validation_and_bad_evaluations() {
1152 let mut rng = Rng::new(4);
1153 assert!(Archive::try_new(0, &[0.0], &[1.0], 4, 2, &mut rng).is_err());
1154 assert!(Archive::try_new(2, &[0.0], &[0.0], 4, 2, &mut rng).is_err());
1155 assert!(Archive::try_new(2, &[0.0], &[1.0], 0, 2, &mut rng).is_err());
1156
1157 let mut archive = Archive::try_new(2, &[0.0], &[1.0], 4, 2, &mut rng).unwrap();
1158 let (improvements, values) =
1159 archive.update(&[vec![0.0, 0.0]], &mut |_: &[f64]| (f64::NAN, vec![0.5]));
1160 assert!(improvements[0].is_infinite());
1161 assert!(values[0].is_infinite());
1162 assert_eq!(archive.occupied(), 0);
1163 }
1164
1165 #[test]
1166 fn qd_score_matches_positive_and_negative_python_rules() {
1167 let mut rng = Rng::new(5);
1168 let mut positive = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1169 positive.set(0, 2.0, &[0.1], &[0.0]);
1170 positive.set(1, 4.0, &[0.5], &[0.0]);
1171 assert_eq!(positive.qd_score(), 0.75);
1172
1173 let mut mixed = Archive::new(1, &[0.0], &[1.0], 3, 2, &mut rng);
1174 mixed.set(0, -2.0, &[0.1], &[0.0]);
1175 mixed.set(1, 4.0, &[0.5], &[0.0]);
1176 assert_eq!(mixed.qd_score(), 2.0);
1177 }
1178
1179 #[test]
1180 fn random_x_one_uses_sorted_best_niches() {
1181 let mut rng = Rng::new(6);
1182 let mut archive = Archive::new(1, &[0.0], &[1.0], 4, 2, &mut rng);
1183 for (index, value) in [4.0, 1.0, 3.0, 2.0].into_iter().enumerate() {
1184 archive.set(index, value, &[index as f64 / 4.0], &[index as f64]);
1185 }
1186 archive.argsort();
1187 for _ in 0..10 {
1188 let (x, y) = archive.random_x_one(1, &mut rng);
1189 assert_eq!(x, vec![1.0]);
1190 assert_eq!(y, 1.0);
1191 }
1192 }
1193}