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