1#![allow(clippy::needless_range_loop, clippy::manual_memcpy)]
4
5use crate::fitness::Fitness;
47use crate::rng::Rng;
48
49const BIG: f64 = f64::MAX;
50
51#[derive(Clone, Debug)]
53pub struct ModeResult {
54 pub x: Vec<Vec<f64>>,
56 pub y: Vec<Vec<f64>>,
58 pub iterations: i32,
60 pub stop: i32,
62}
63
64#[derive(Clone, Debug)]
66pub struct ModeParams {
67 pub popsize: i32,
69 pub f: f64,
71 pub cr: f64,
73 pub pro_c: f64,
75 pub dis_c: f64,
77 pub pro_m: f64,
79 pub dis_m: f64,
81 pub nsga_update: bool,
83 pub pareto_update: f64,
85 pub min_mutate: f64,
87 pub max_mutate: f64,
89 pub seed: u64,
91 pub runid: i64,
93}
94
95impl Default for ModeParams {
96 fn default() -> Self {
97 Self {
98 popsize: 64,
99 f: 0.5,
100 cr: 0.9,
101 pro_c: 0.5,
102 dis_c: 15.0,
103 pro_m: 0.9,
104 dis_m: 20.0,
105 nsga_update: true,
106 pareto_update: 0.0,
107 min_mutate: 0.1,
108 max_mutate: 0.5,
109 seed: 0,
110 runid: 0,
111 }
112 }
113}
114
115fn sort_index(v: &[f64]) -> Vec<usize> {
116 let mut idx: Vec<usize> = (0..v.len()).collect();
117 idx.sort_by(|&a, &b| v[a].total_cmp(&v[b]));
118 idx
119}
120
121fn validate_mode_inputs(
122 fitfun: &Fitness,
123 nobj: usize,
124 ncon: usize,
125 ints: Option<&[bool]>,
126 p: &ModeParams,
127) -> Result<(), &'static str> {
128 if fitfun.dim() == 0 || !fitfun.has_bounds() {
129 return Err("MODE requires a non-empty bounded decision space");
130 }
131 if fitfun
132 .lower()
133 .iter()
134 .zip(fitfun.upper())
135 .any(|(&lo, &hi)| !lo.is_finite() || !hi.is_finite() || lo >= hi)
136 {
137 return Err("MODE bounds must be finite and satisfy lower < upper");
138 }
139 if nobj == 0 || fitfun.nobj() != nobj + ncon {
140 return Err("MODE requires nobj > 0 and Fitness::nobj == nobj + ncon");
141 }
142 let popsize = if p.popsize > 0 { p.popsize } else { 128 };
143 if popsize < 4 {
144 return Err("MODE population size must be at least four");
145 }
146 if ints.is_some_and(|values| values.len() != fitfun.dim()) {
147 return Err("MODE integer mask length must equal the decision dimension");
148 }
149 if !p.f.is_finite()
150 || !p.cr.is_finite()
151 || !p.pro_c.is_finite()
152 || !p.dis_c.is_finite()
153 || !p.pro_m.is_finite()
154 || !p.dis_m.is_finite()
155 || !p.pareto_update.is_finite()
156 || !p.min_mutate.is_finite()
157 || !p.max_mutate.is_finite()
158 {
159 return Err("MODE parameters must be finite");
160 }
161 if !(0.0..=1.0).contains(&p.pro_c) || !(0.0..=1.0).contains(&p.pro_m) {
162 return Err("MODE crossover and mutation probabilities must be in [0, 1]");
163 }
164 if p.dis_c <= 0.0 || p.dis_m <= 0.0 {
165 return Err("MODE distribution indices must be positive");
166 }
167 if p.min_mutate > 0.0 && p.max_mutate > 0.0 && p.min_mutate > p.max_mutate {
168 return Err("MODE min_mutate must not exceed max_mutate");
169 }
170 Ok(())
171}
172
173pub struct Mode {
178 fitfun: Fitness,
179 rng: Rng,
180 dim: usize,
181 nobj: usize,
182 ncon: usize,
183 nobj_ncon: usize,
184 popsize: usize,
185
186 f0: f64,
187 cr0: f64,
188 f: f64,
189 cr: f64,
190 pro_c: f64,
191 dis_c: f64,
192 pro_m: f64,
193 dis_m: f64,
194 nsga_update: bool,
195 pareto_update: f64,
196 min_mutate: f64,
197 max_mutate: f64,
198 is_int: Option<Vec<bool>>,
199
200 pop_x: Vec<Vec<f64>>,
202 pop_y: Vec<Vec<f64>>,
203 v_x: Vec<Vec<f64>>, vp: usize,
205
206 last_con: Option<Vec<Vec<f64>>>,
207 last_eps: Vec<f64>,
208
209 iterations: i32,
210 stop: i32,
211 pending: bool,
212}
213
214impl Mode {
215 pub fn try_new(
225 fitfun: Fitness,
226 nobj: usize,
227 ncon: usize,
228 ints: Option<Vec<bool>>,
229 p: &ModeParams,
230 ) -> Result<Self, &'static str> {
231 validate_mode_inputs(&fitfun, nobj, ncon, ints.as_deref(), p)?;
232 Ok(Self::new_unchecked(fitfun, nobj, ncon, ints, p))
233 }
234
235 pub fn new(
242 fitfun: Fitness,
243 nobj: usize,
244 ncon: usize,
245 ints: Option<Vec<bool>>,
246 p: &ModeParams,
247 ) -> Self {
248 Self::try_new(fitfun, nobj, ncon, ints, p).expect("invalid MODE configuration")
249 }
250
251 fn new_unchecked(
252 fitfun: Fitness,
253 nobj: usize,
254 ncon: usize,
255 ints: Option<Vec<bool>>,
256 p: &ModeParams,
257 ) -> Self {
258 let dim = fitfun.dim();
259 let popsize = if p.popsize > 0 {
260 p.popsize as usize
261 } else {
262 128
263 };
264 let f0 = if p.f > 0.0 { p.f } else { 0.5 };
265 let cr0 = if p.cr > 0.0 { p.cr } else { 0.9 };
266 let mut m = Mode {
267 dim,
268 nobj,
269 ncon,
270 nobj_ncon: nobj + ncon,
271 popsize,
272 f0,
273 cr0,
274 f: f0,
275 cr: cr0,
276 pro_c: p.pro_c,
277 dis_c: p.dis_c,
278 pro_m: p.pro_m,
279 dis_m: p.dis_m,
280 nsga_update: p.nsga_update,
281 pareto_update: p.pareto_update,
282 min_mutate: if p.min_mutate > 0.0 {
283 p.min_mutate
284 } else {
285 0.1
286 },
287 max_mutate: if p.max_mutate > 0.0 {
288 p.max_mutate
289 } else {
290 0.5
291 },
292 is_int: ints,
293 pop_x: vec![],
294 pop_y: vec![],
295 v_x: vec![],
296 vp: 0,
297 last_con: None,
298 last_eps: vec![0.0; ncon],
299 iterations: 0,
300 stop: 0,
301 pending: false,
302 rng: Rng::new(p.seed.wrapping_add(p.runid as u64)),
303 fitfun,
304 };
305 m.init();
306 m
307 }
308
309 fn init(&mut self) {
310 let n = 2 * self.popsize;
311 self.pop_x = (0..n)
312 .map(|i| {
313 if i < self.popsize {
314 self.fitfun.sample(&mut self.rng)
315 } else {
316 vec![0.0; self.dim]
317 }
318 })
319 .collect();
320 self.pop_y = vec![vec![BIG; self.nobj_ncon]; n];
321 self.v_x = self.pop_x[0..self.popsize].to_vec();
322 self.vp = 0;
323 self.pending = false;
324 }
325
326 pub fn dim(&self) -> usize {
328 self.dim
329 }
330 pub fn nobj(&self) -> usize {
332 self.nobj
333 }
334 pub fn ncon(&self) -> usize {
336 self.ncon
337 }
338 pub fn popsize(&self) -> usize {
340 self.popsize
341 }
342 pub fn stop(&self) -> i32 {
344 self.stop
345 }
346
347 fn variation(&mut self, pop: &[Vec<f64>]) -> Vec<Vec<f64>> {
350 let dim = self.dim;
351 let dis_c = (0.5 * self.rng.uniform01() + 0.5) * self.dis_c;
352 let dis_m = (0.5 * self.rng.uniform01() + 0.5) * self.dis_m;
353 let n2 = pop.len() / 2;
354 let n = 2 * n2;
355 let mut beta = vec![vec![0.0; dim]; n2];
357 for pb in beta.iter_mut() {
358 let cross_pair = self.rng.uniform01() < self.pro_c;
359 for i in 0..dim {
360 if !cross_pair || self.rng.uniform01() < 0.5 {
361 pb[i] = 1.0;
362 } else {
363 let r = self.rng.uniform01();
364 let mut b = if r <= 0.5 {
365 (2.0 * r).powf(1.0 / (dis_c + 1.0))
366 } else {
367 (2.0 * r).powf(-1.0 / (dis_c + 1.0))
368 };
369 if self.rng.uniform01() > 0.5 {
370 b = -b;
371 }
372 pb[i] = b;
373 }
374 }
375 }
376 let mut offspring: Vec<Vec<f64>> = Vec::with_capacity(n);
377 let mut off2: Vec<Vec<f64>> = Vec::with_capacity(n2);
378 for p in 0..n2 {
379 let p1 = &pop[p];
380 let p2 = &pop[n2 + p];
381 let mut o1 = vec![0.0; dim];
382 let mut o2 = vec![0.0; dim];
383 for i in 0..dim {
384 let base = (p1[i] + p2[i]) * 0.5;
385 let delta = beta[p][i] * (p1[i] - p2[i]) * 0.5;
386 o1[i] = base + delta;
387 o2[i] = base - delta;
388 }
389 offspring.push(o1);
390 off2.push(o2);
391 }
392 offspring.extend(off2);
393
394 if offspring.len() < pop.len() {
398 offspring.push(pop[pop.len() - 1].clone());
399 }
400
401 let limit = self.pro_m / dim as f64;
402 for op in offspring.iter_mut() {
403 for i in 0..dim {
404 if self.rng.uniform01() < limit {
405 let mu = self.rng.uniform01();
406 let norm = self.fitfun.norm_i(i, op[i]);
407 let scale = self.fitfun.scale()[i];
408 if mu <= 0.5 {
409 op[i] += scale
410 * ((2.0 * mu + (1.0 - 2.0 * mu) * (1.0 - norm).powf(dis_m + 1.0))
411 .powf(1.0 / (dis_m + 1.0))
412 - 1.0);
413 } else {
414 op[i] += scale
415 * (1.0
416 - (2.0 * (1.0 - mu)
417 + 2.0 * (mu - 0.5) * (1.0 - norm).powf(dis_m + 1.0))
418 .powf(1.0 / (dis_m + 1.0)));
419 }
420 }
421 }
422 }
423 for op in offspring.iter_mut() {
424 *op = self.fitfun.closest_feasible(op);
425 }
426 offspring
427 }
428
429 fn modify(&mut self, x: &mut [f64]) {
430 let Some(is_int) = self.is_int.clone() else {
431 return;
432 };
433 let n_ints = is_int.iter().filter(|&&b| b).count() as f64;
434 if n_ints == 0.0 {
435 return;
436 }
437 let to_mutate =
438 self.min_mutate + self.rng.uniform01() * (self.max_mutate - self.min_mutate);
439 for i in 0..self.dim {
440 if is_int[i] && self.rng.uniform01() < to_mutate / n_ints {
441 x[i] = self.fitfun.sample_i(i, &mut self.rng).trunc();
442 }
443 }
444 }
445
446 fn next_x(&mut self, p: usize) -> Vec<f64> {
447 if p == 0 {
448 self.iterations += 1;
449 }
450 if self.nsga_update {
451 let x = self.v_x[self.vp].clone();
452 self.vp = (self.vp + 1) % self.v_x.len();
453 return x;
454 }
455 if p == 0 {
456 self.cr = if self.iterations % 2 == 0 {
457 0.5 * self.cr0
458 } else {
459 self.cr0
460 };
461 self.f = if self.iterations % 2 == 0 {
462 0.5 * self.f0
463 } else {
464 self.f0
465 };
466 }
467 let ps = self.popsize;
468 let (mut r1, mut r2, mut r3);
469 loop {
470 r1 = self.rng.int_below(ps as i64) as usize;
471 r2 = self.rng.int_below(ps as i64) as usize;
472 r3 = if self.pareto_update > 0.0 {
473 (self.rng.uniform01().powf(1.0 + self.pareto_update) * ps as f64) as usize
474 } else {
475 self.rng.int_below(ps as i64) as usize
476 };
477 if r3 != p && r3 != r1 && r3 != r2 && r2 != p && r2 != r1 && r1 != p {
478 break;
479 }
480 }
481 let xp = self.pop_x[p].clone();
482 let x1 = &self.pop_x[r1];
483 let x2 = &self.pop_x[r2];
484 let x3 = &self.pop_x[r3];
485 let mut x: Vec<f64> = (0..self.dim)
486 .map(|j| x3[j] + (x1[j] - x2[j]) * self.f)
487 .collect();
488 let r = self.rng.int_below(self.dim as i64) as usize;
489 for j in 0..self.dim {
490 if j != r && self.rng.uniform01() > self.cr {
491 x[j] = xp[j];
492 }
493 }
494 self.modify(&mut x);
495 self.fitfun.closest_feasible(&x)
496 }
497
498 fn is_dominated(objs: &[Vec<f64>], i: usize, index: usize) -> bool {
503 for j in 0..objs[i].len() {
504 if objs[i][j] < objs[index][j] {
505 return false;
506 }
507 }
508 true
509 }
510
511 fn pareto_levels(objs: &[Vec<f64>]) -> Vec<f64> {
512 let n = objs.len();
513 let mut domination = vec![0.0; n];
514 let mut mask = vec![true; n];
515 let mut index = 0;
516 while index < n {
517 for i in 0..n {
518 if i != index && mask[i] && Self::is_dominated(objs, i, index) {
519 mask[i] = false;
520 }
521 }
522 for i in 0..n {
523 if mask[i] {
524 domination[i] += 1.0;
525 }
526 }
527 index += 1;
528 while index < n && !mask[index] {
529 index += 1;
530 }
531 }
532 domination
533 }
534
535 fn objranks(objs: &[Vec<f64>]) -> Vec<f64> {
536 let n = objs.len();
537 let nobj = objs[0].len();
538 let mut rank_sum = vec![0.0; n];
539 for j in 0..nobj {
540 let col: Vec<f64> = objs.iter().map(|o| o[j]).collect();
541 let order = sort_index(&col);
542 for (pos, &idx) in order.iter().enumerate() {
543 rank_sum[idx] += pos as f64;
544 }
545 }
546 rank_sum
547 }
548
549 fn ranks(cons: &[Vec<f64>], eps: &[f64]) -> Vec<f64> {
550 let n = cons.len();
551 let ncon = eps.len();
552 let mut rank = vec![vec![0.0; ncon]; n];
553 let mut alpha = vec![0.0; n];
554 for j in 0..ncon {
555 let col: Vec<f64> = cons.iter().map(|c| c[j]).collect();
556 let order = sort_index(&col);
557 for (pos, &idx) in order.iter().enumerate() {
558 if cons[idx][j] <= eps[j] {
559 rank[idx][j] = 0.0;
560 } else {
561 rank[idx][j] = pos as f64;
562 alpha[idx] += 1.0;
563 }
564 }
565 }
566 let mut csum = vec![0.0; n];
567 for i in 0..n {
568 for j in 0..ncon {
569 csum[i] += rank[i][j] * alpha[i] / ncon as f64;
570 }
571 }
572 csum
573 }
574
575 fn pareto(&mut self, ys: &[Vec<f64>]) -> Vec<f64> {
576 if self.ncon == 0 {
577 return Self::pareto_levels(ys);
578 }
579 let popn = ys.len();
580 let objs: Vec<Vec<f64>> = ys.iter().map(|y| y[0..self.nobj].to_vec()).collect();
581 let cons: Vec<Vec<f64>> = ys
582 .iter()
583 .map(|y| {
584 y[self.nobj..self.nobj_ncon]
585 .iter()
586 .map(|&c| c.max(0.0))
587 .collect()
588 })
589 .collect();
590
591 let mut eps = vec![0.0; self.ncon];
592 if self.iterations > 1
593 && let Some(last) = &self.last_con
594 {
595 let last_max = last
596 .iter()
597 .flat_map(|c| c.iter().cloned())
598 .fold(f64::MIN, f64::max);
599 if last_max < 1e90 {
600 let mut eps_mean = vec![0.0; self.ncon];
601 for j in 0..self.ncon {
602 let mean_j = last.iter().map(|c| c[j]).sum::<f64>() / last.len() as f64;
603 eps_mean[j] = 0.5 * (self.last_eps[j] + 0.5 * mean_j);
604 }
605 if eps_mean.iter().cloned().fold(f64::MIN, f64::max) > 1e-8 {
606 eps = eps_mean;
607 }
608 }
609 }
610 self.last_con = Some(cons.clone());
611 self.last_eps = eps.clone();
612
613 let feasible: Vec<bool> = cons
614 .iter()
615 .map(|c| c.iter().zip(&eps).all(|(&cv, &ev)| cv <= ev))
616 .collect();
617 let has_feasible = feasible.iter().any(|&f| f);
618 let has_infeasible = feasible.iter().any(|&f| !f);
619
620 let mut csum = Self::ranks(&cons, &eps);
621 if has_feasible {
622 let orank = Self::objranks(&objs);
623 for i in 0..popn {
624 csum[i] += orank[i];
625 }
626 }
627 let ci = sort_index(&csum);
628 let mut fiv = vec![];
629 let mut viv = vec![];
630 for &i in &ci {
631 if feasible[i] {
632 fiv.push(i);
633 } else {
634 viv.push(i);
635 }
636 }
637 let mut domination = vec![0.0; popn];
638 if has_feasible {
639 let feas_objs: Vec<Vec<f64>> = fiv.iter().map(|&i| objs[i].clone()).collect();
640 let ypar = Self::pareto_levels(&feas_objs);
641 for (k, &i) in fiv.iter().enumerate() {
642 domination[i] += ypar[k];
643 }
644 }
645 if has_infeasible {
646 for (i, &vi) in viv.iter().enumerate() {
647 domination[vi] += (viv.len() - i) as f64;
648 }
649 for &fi in &fiv {
650 domination[fi] += (viv.len() + 1) as f64;
651 }
652 }
653 domination
654 }
655
656 fn crowd_dist(sub: &[Vec<f64>], nobj: usize) -> Vec<f64> {
657 let n = sub.len();
658 if n == 0 {
659 return Vec::new();
660 }
661 if n <= 2 {
662 return vec![BIG; n];
663 }
664 let mut distance = vec![0.0; n];
665 for objective in 0..nobj {
666 let values: Vec<f64> = sub.iter().map(|y| y[objective]).collect();
667 let order = sort_index(&values);
668 let lo = values[order[0]];
669 let hi = values[order[n - 1]];
670 let span = hi - lo;
671 if !span.is_finite() || span <= 0.0 {
672 continue;
673 }
674 distance[order[0]] = BIG;
675 distance[order[n - 1]] = BIG;
676 for position in 1..n - 1 {
677 let index = order[position];
678 if distance[index] != BIG {
679 distance[index] +=
680 (values[order[position + 1]] - values[order[position - 1]]) / span;
681 }
682 }
683 }
684 if distance.iter().all(|&value| value == 0.0) {
685 return vec![0.0; n];
686 }
687 distance
688 }
689
690 fn pop_update(&mut self) {
691 let n = 2 * self.popsize;
692 let mut x0 = self.pop_x[0..n].to_vec();
693 let mut y0 = self.pop_y[0..n].to_vec();
694 if self.nobj == 1 {
695 let col: Vec<f64> = y0.iter().map(|y| y[0]).collect();
696 let mut yi = sort_index(&col);
697 yi.reverse();
698 x0 = yi.iter().map(|&i| x0[i].clone()).collect();
699 y0 = yi.iter().map(|&i| y0[i].clone()).collect();
700 }
701 let domination = self.pareto(&y0);
702 let maxdom = domination.iter().cloned().fold(f64::MIN, f64::max) as i32;
703 let mut newx: Vec<Vec<f64>> = Vec::with_capacity(self.popsize);
704 let mut newy: Vec<Vec<f64>> = Vec::with_capacity(self.popsize);
705 for dom in (0..=maxdom).rev() {
706 let level: Vec<usize> = (0..n).filter(|&i| domination[i] as i32 == dom).collect();
707 if level.is_empty() {
708 continue;
709 }
710 if newx.len() + level.len() <= self.popsize {
711 for &i in &level {
712 newx.push(x0[i].clone());
713 newy.push(y0[i].clone());
714 }
715 } else {
716 if level.len() > 1 {
717 let domy: Vec<Vec<f64>> = level.iter().map(|&i| y0[i].clone()).collect();
718 let cd = Self::crowd_dist(&domy, self.nobj);
719 let mut si = sort_index(&cd);
720 si.reverse();
721 for &k in &si {
722 if newx.len() >= self.popsize {
723 break;
724 }
725 let i = level[k];
726 newx.push(x0[i].clone());
727 newy.push(y0[i].clone());
728 }
729 } else {
730 newx.push(x0[level[0]].clone());
731 newy.push(y0[level[0]].clone());
732 }
733 break;
734 }
735 }
736 for i in 0..self.popsize {
737 self.pop_x[i] = newx[i].clone();
738 self.pop_y[i] = newy[i].clone();
739 }
740 if self.nsga_update {
741 let cur = self.pop_x[0..self.popsize].to_vec();
742 self.v_x = self.variation(&cur);
743 }
744 }
745
746 pub fn ask(&mut self) -> Vec<Vec<f64>> {
756 self.try_ask().expect("invalid MODE ask call")
757 }
758
759 pub fn try_ask(&mut self) -> Result<Vec<Vec<f64>>, &'static str> {
766 if self.pending {
767 return Err("MODE ask called before telling the pending batch");
768 }
769 for p in 0..self.popsize {
770 let x = self.next_x(p);
771 self.pop_x[self.popsize + p] = x;
772 }
773 self.pending = true;
774 Ok(self.pop_x[self.popsize..2 * self.popsize].to_vec())
775 }
776
777 fn set_x(&mut self, xs: &[Vec<f64>]) {
778 for (p, row) in xs.iter().enumerate().take(self.popsize) {
779 self.pop_x[self.popsize + p] = row.clone();
780 }
781 }
782
783 pub fn tell(&mut self, ys: &[Vec<f64>]) -> i32 {
791 self.try_tell(ys).expect("invalid MODE tell call")
792 }
793
794 pub fn try_tell(&mut self, ys: &[Vec<f64>]) -> Result<i32, &'static str> {
801 if !self.pending {
802 return Err("MODE tell called without a pending ask batch");
803 }
804 if ys.len() != self.popsize {
805 return Err("MODE tell batch length must equal popsize");
806 }
807 for (p, row) in ys.iter().enumerate() {
808 if row.len() != self.nobj_ncon {
809 return Err("MODE tell row width must equal nobj + ncon");
810 }
811 self.pop_y[self.popsize + p] = row
812 .iter()
813 .map(|&value| if value.is_finite() { value } else { BIG })
814 .collect();
815 }
816 self.pop_update();
817 self.pending = false;
818 Ok(self.stop)
819 }
820
821 pub fn tell_switch(&mut self, ys: &[Vec<f64>], nsga_update: bool, pareto_update: f64) -> i32 {
829 self.try_tell_switch(ys, nsga_update, pareto_update)
830 .expect("invalid MODE tell_switch call")
831 }
832
833 pub fn try_tell_switch(
841 &mut self,
842 ys: &[Vec<f64>],
843 nsga_update: bool,
844 pareto_update: f64,
845 ) -> Result<i32, &'static str> {
846 if !pareto_update.is_finite() {
847 return Err("MODE pareto_update must be finite");
848 }
849 self.nsga_update = nsga_update;
850 self.pareto_update = pareto_update;
851 self.try_tell(ys)
852 }
853
854 pub fn set_population(&mut self, xs: &[Vec<f64>], ys: &[Vec<f64>]) -> i32 {
862 self.try_set_population(xs, ys)
863 .expect("invalid MODE set_population call")
864 }
865
866 pub fn try_set_population(
876 &mut self,
877 xs: &[Vec<f64>],
878 ys: &[Vec<f64>],
879 ) -> Result<i32, &'static str> {
880 if xs.len() < 4 {
881 return Err("MODE population size must be at least four");
882 }
883 if xs.len() != ys.len() {
884 return Err("MODE population x/y length mismatch");
885 }
886 if xs.iter().any(|row| row.len() != self.dim) {
887 return Err("MODE population row width must equal dim");
888 }
889 if xs.len() != self.popsize {
890 self.popsize = xs.len();
891 self.init();
892 }
893 self.set_x(xs);
894 self.pending = true;
895 self.try_tell(ys)
896 }
897
898 pub fn population(&self) -> Vec<Vec<f64>> {
900 self.pop_x[0..self.popsize].to_vec()
901 }
902
903 pub fn result(&self) -> ModeResult {
905 ModeResult {
906 x: self.population(),
907 y: self.pop_y[0..self.popsize].to_vec(),
908 iterations: self.iterations,
909 stop: self.stop,
910 }
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917
918 fn eval(x: &[f64]) -> Vec<f64> {
921 let o1: f64 = x.iter().map(|v| v * v).sum();
922 let o2: f64 = x.iter().map(|v| (v - 2.0) * (v - 2.0)).sum();
923 vec![o1, o2]
924 }
925
926 fn run(nsga: bool) -> Mode {
927 let fit = Fitness::bounded(3, 2, &[-5.0; 3], &[5.0; 3]);
928 let params = ModeParams {
929 popsize: 32,
930 nsga_update: nsga,
931 seed: 1,
932 ..Default::default()
933 };
934 let mut opt = Mode::new(fit, 2, 0, None, ¶ms);
935 for _ in 0..80 {
936 let xs = opt.ask();
937 let ys: Vec<Vec<f64>> = xs.iter().map(|x| eval(x)).collect();
938 opt.tell(&ys);
939 }
940 opt
941 }
942
943 #[test]
944 fn nsga_finds_pareto_front() {
945 let opt = run(true);
946 let r = opt.result();
947 let min_o1 = r.y.iter().map(|y| y[0]).fold(f64::MAX, f64::min);
949 let min_o2 = r.y.iter().map(|y| y[1]).fold(f64::MAX, f64::min);
950 assert!(min_o1 < 0.1, "no low-o1 solution: {min_o1}");
951 assert!(min_o2 < 0.1, "no low-o2 solution: {min_o2}");
952 }
953
954 #[test]
955 fn de_update_finds_pareto_front() {
956 let opt = run(false);
957 let r = opt.result();
958 let min_o1 = r.y.iter().map(|y| y[0]).fold(f64::MAX, f64::min);
959 let min_o2 = r.y.iter().map(|y| y[1]).fold(f64::MAX, f64::min);
960 assert!(min_o1 < 0.2, "no low-o1 solution: {min_o1}");
961 assert!(min_o2 < 0.2, "no low-o2 solution: {min_o2}");
962 }
963
964 #[test]
965 fn constrained_run_progresses() {
966 let fit = Fitness::bounded(3, 2, &[-5.0; 3], &[5.0; 3]);
969 let params = ModeParams {
970 popsize: 24,
971 nsga_update: false,
972 seed: 2,
973 ..Default::default()
974 };
975 let mut opt = Mode::new(fit, 1, 1, None, ¶ms);
976 for _ in 0..100 {
977 let xs = opt.ask();
978 let ys: Vec<Vec<f64>> = xs
979 .iter()
980 .map(|x| {
981 let o: f64 = x.iter().map(|v| v * v).sum();
982 let c: f64 = 1.0 - x.iter().sum::<f64>();
983 vec![o, c]
984 })
985 .collect();
986 opt.tell(&ys);
987 }
988 let r = opt.result();
989 let best =
991 r.y.iter()
992 .filter(|y| y[1] <= 0.0)
993 .map(|y| y[0])
994 .fold(f64::MAX, f64::min);
995 assert!(best < 2.0, "constrained best too large: {best}");
996 }
997
998 #[test]
999 fn rejects_invalid_configuration() {
1000 let fit = Fitness::bounded(2, 2, &[-1.0; 2], &[1.0; 2]);
1001 let mut params = ModeParams {
1002 popsize: 3,
1003 ..Default::default()
1004 };
1005 assert!(Mode::try_new(fit.clone(), 2, 0, None, ¶ms).is_err());
1006 params.popsize = 5;
1007 assert!(Mode::try_new(fit.clone(), 0, 2, None, ¶ms).is_err());
1008 assert!(Mode::try_new(fit.clone(), 2, 0, Some(vec![true]), ¶ms).is_err());
1009 params.pro_m = 1.5;
1010 assert!(Mode::try_new(fit, 2, 0, None, ¶ms).is_err());
1011 }
1012
1013 #[test]
1014 fn odd_population_preserves_batch_size() {
1015 let fit = Fitness::bounded(2, 2, &[-1.0; 2], &[1.0; 2]);
1016 let params = ModeParams {
1017 popsize: 5,
1018 nsga_update: true,
1019 seed: 7,
1020 ..Default::default()
1021 };
1022 let mut mode = Mode::try_new(fit, 2, 0, None, ¶ms).unwrap();
1023 for _ in 0..3 {
1024 let xs = mode.ask();
1025 assert_eq!(xs.len(), 5);
1026 let ys: Vec<Vec<f64>> = xs.iter().map(|x| vec![x[0], x[1]]).collect();
1027 mode.tell(&ys);
1028 }
1029 }
1030
1031 #[test]
1032 fn crowding_uses_every_objective() {
1033 let values = vec![
1034 vec![0.0, 0.5],
1035 vec![0.25, 0.0],
1036 vec![0.5, 0.5],
1037 vec![0.75, 1.0],
1038 vec![1.0, 0.5],
1039 ];
1040 let distance = Mode::crowd_dist(&values, 2);
1041 assert_eq!(distance.iter().filter(|&&d| d == BIG).count(), 4);
1042 assert!(distance[2].is_finite() && distance[2] > 0.0);
1043 assert_eq!(Mode::crowd_dist(&vec![vec![1.0, 1.0]; 4], 2), vec![0.0; 4]);
1044 }
1045
1046 #[test]
1047 fn zero_crossover_and_mutation_preserve_parents() {
1048 let fit = Fitness::bounded(2, 2, &[-1.0; 2], &[1.0; 2]);
1049 let params = ModeParams {
1050 popsize: 5,
1051 pro_c: 0.0,
1052 pro_m: 0.0,
1053 seed: 9,
1054 ..Default::default()
1055 };
1056 let mut mode = Mode::try_new(fit, 2, 0, None, ¶ms).unwrap();
1057 let parents = vec![
1058 vec![-0.8, -0.7],
1059 vec![-0.4, -0.3],
1060 vec![0.1, 0.2],
1061 vec![0.5, 0.6],
1062 vec![0.8, 0.9],
1063 ];
1064 let offspring = mode.variation(&parents);
1065 for (child, parent) in offspring.iter().zip(&parents) {
1066 for (&actual, &expected) in child.iter().zip(parent) {
1067 assert!((actual - expected).abs() < 1e-14);
1068 }
1069 }
1070 }
1071
1072 #[test]
1073 fn tell_sanitizes_non_finite_values() {
1074 let fit = Fitness::bounded(2, 1, &[-1.0; 2], &[1.0; 2]);
1075 let params = ModeParams {
1076 popsize: 4,
1077 nsga_update: false,
1078 ..Default::default()
1079 };
1080 let mut mode = Mode::try_new(fit, 1, 0, None, ¶ms).unwrap();
1081 mode.ask();
1082 mode.tell(&[vec![f64::NAN], vec![1.0], vec![2.0], vec![3.0]]);
1083 assert!(
1084 mode.result()
1085 .y
1086 .iter()
1087 .flatten()
1088 .all(|value| !value.is_nan())
1089 );
1090 }
1091
1092 #[test]
1093 fn ask_tell_enforces_call_order_and_shapes() {
1094 let fit = Fitness::bounded(2, 1, &[-1.0; 2], &[1.0; 2]);
1095 let params = ModeParams {
1096 popsize: 4,
1097 ..Default::default()
1098 };
1099 let mut mode = Mode::try_new(fit, 1, 0, None, ¶ms).unwrap();
1100 assert!(mode.try_tell(&vec![vec![0.0]; 4]).is_err());
1101 mode.try_ask().unwrap();
1102 assert!(mode.try_ask().is_err());
1103 assert!(mode.try_tell(&vec![vec![0.0]; 3]).is_err());
1104 assert!(mode.try_tell(&vec![vec![0.0, 1.0]; 4]).is_err());
1105 assert_eq!(mode.try_tell(&vec![vec![0.0]; 4]).unwrap(), 0);
1106 }
1107}