1use serde::{Deserialize, Serialize};
69
70use hpr_core::random::SeededRng;
71
72use super::{Variable, check_variables};
73use crate::error::AnalysisError;
74
75pub const MAX_POPULATION: usize = 1 << 12;
80
81pub const MAX_BOUND: f64 = f64::MAX / 4.0;
84
85pub const MAX_OBJECTIVES: usize = 16;
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(try_from = "Nsga2Data")]
93pub struct Nsga2 {
94 variables: Vec<Variable>,
95 objectives: usize,
96 population: usize,
97 generations: usize,
98 crossover_probability: f64,
99 crossover_index: f64,
100 mutation_probability: f64,
101 mutation_index: f64,
102}
103
104#[derive(Deserialize)]
106#[serde(deny_unknown_fields)]
107struct Nsga2Data {
108 variables: Vec<Variable>,
109 objectives: usize,
110 population: usize,
111 generations: usize,
112 crossover_probability: f64,
113 crossover_index: f64,
114 mutation_probability: f64,
115 mutation_index: f64,
116}
117
118impl TryFrom<Nsga2Data> for Nsga2 {
119 type Error = AnalysisError;
120
121 fn try_from(data: Nsga2Data) -> Result<Self, AnalysisError> {
122 Nsga2::new(data.variables, data.objectives)?
123 .with_population(data.population)?
124 .with_generations(data.generations)?
125 .with_crossover(data.crossover_probability, data.crossover_index)?
126 .with_mutation(data.mutation_probability, data.mutation_index)
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[non_exhaustive]
138pub struct Goals {
139 #[serde(with = "infinities_as_none")]
141 pub objectives: Vec<f64>,
142 #[serde(with = "super::cmaes::infinity_as_none")]
145 pub violation: f64,
146}
147
148impl Goals {
149 pub fn feasible(objectives: Vec<f64>) -> Self {
151 Self {
152 objectives,
153 violation: 0.0,
154 }
155 }
156
157 pub fn constrained(objectives: Vec<f64>, constraints: &[f64]) -> Self {
161 Self {
162 objectives,
163 violation: super::Evaluation::constrained(0.0, constraints).violation,
164 }
165 }
166
167 pub fn failed() -> Self {
169 Self {
170 objectives: Vec::new(),
171 violation: f64::INFINITY,
172 }
173 }
174
175 pub fn is_feasible(&self) -> bool {
177 self.violation == 0.0
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183#[non_exhaustive]
184pub struct Member {
185 pub point: Vec<f64>,
187 #[serde(with = "infinities_as_none")]
189 pub objectives: Vec<f64>,
190 #[serde(with = "super::cmaes::infinity_as_none")]
193 pub violation: f64,
194 pub rank: usize,
196 #[serde(with = "super::cmaes::infinity_as_none")]
198 pub crowding: f64,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[non_exhaustive]
204pub struct Front {
205 pub members: Vec<Member>,
209 pub population: Vec<Member>,
211 pub evaluations: usize,
213 pub generations: usize,
215}
216
217impl Front {
218 pub fn is_feasible(&self) -> bool {
220 self.members.iter().all(|m| m.violation == 0.0)
221 }
222}
223
224mod infinities_as_none {
226 use serde::ser::Error as _;
227 use serde::{Deserialize, Deserializer, Serialize, Serializer};
228
229 pub(super) fn serialize<S: Serializer>(x: &[f64], serializer: S) -> Result<S::Ok, S::Error> {
230 if let Some(bad) = x.iter().find(|x| x.is_nan() || **x == f64::NEG_INFINITY) {
231 return Err(S::Error::custom(format!("{bad} is neither finite nor +∞")));
232 }
233 let values: Vec<Option<f64>> = x
234 .iter()
235 .map(|&x| (x != f64::INFINITY).then_some(x))
236 .collect();
237 values.serialize(serializer)
238 }
239
240 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
241 deserializer: D,
242 ) -> Result<Vec<f64>, D::Error> {
243 Ok(Vec::<Option<f64>>::deserialize(deserializer)?
244 .into_iter()
245 .map(|x| x.unwrap_or(f64::INFINITY))
246 .collect())
247 }
248}
249
250fn check_index(what: &'static str, index: f64) -> Result<f64, AnalysisError> {
252 if index.is_finite() && index >= 0.0 {
253 Ok(index)
254 } else {
255 Err(AnalysisError::Domain { what, value: index })
256 }
257}
258
259fn check_probability(what: &'static str, p: f64) -> Result<f64, AnalysisError> {
261 if (0.0..=1.0).contains(&p) {
262 Ok(p)
263 } else {
264 Err(AnalysisError::Domain { what, value: p })
265 }
266}
267
268impl Nsga2 {
269 pub fn new(variables: Vec<Variable>, objectives: usize) -> Result<Self, AnalysisError> {
282 check_variables(&variables)?;
283 if objectives == 0 {
284 return Err(AnalysisError::TooFew {
285 what: "objectives",
286 count: 0,
287 minimum: 1,
288 });
289 }
290 if objectives > MAX_OBJECTIVES {
291 return Err(AnalysisError::Count {
292 what: "objectives",
293 count: objectives,
294 limit: MAX_OBJECTIVES,
295 });
296 }
297 for v in &variables {
298 if !v.low().is_finite() {
299 return Err(AnalysisError::Domain {
300 what: "variable's low bound (NSGA-II needs two finite bounds)",
301 value: v.low(),
302 });
303 }
304 if !v.high().is_finite() {
305 return Err(AnalysisError::Domain {
306 what: "variable's high bound (NSGA-II needs two finite bounds)",
307 value: v.high(),
308 });
309 }
310 for bound in [v.low(), v.high()] {
312 if bound.abs() > MAX_BOUND {
313 return Err(AnalysisError::Domain {
314 what: "variable's bound (NSGA-II takes bounds within ±f64::MAX/4)",
315 value: bound,
316 });
317 }
318 }
319 if v.is_integer() {
320 return Err(AnalysisError::Unsupported(format!(
321 "integer variable {:?} in NSGA-II",
322 v.name()
323 )));
324 }
325 }
326 let mutation_probability = 1.0 / variables.len() as f64;
328 Ok(Self {
329 variables,
330 objectives,
331 population: 100,
332 generations: 250,
333 crossover_probability: 0.9,
334 crossover_index: 20.0,
335 mutation_probability,
336 mutation_index: 20.0,
337 })
338 }
339
340 pub fn with_population(mut self, size: usize) -> Result<Self, AnalysisError> {
348 if size < 4 {
349 return Err(AnalysisError::TooFew {
350 what: "population",
351 count: size,
352 minimum: 4,
353 });
354 }
355 if size > MAX_POPULATION {
356 return Err(AnalysisError::Count {
357 what: "population",
358 count: size,
359 limit: MAX_POPULATION,
360 });
361 }
362 if !size.is_multiple_of(2) {
363 return Err(AnalysisError::Domain {
364 what: "population (an even number)",
365 value: size as f64,
367 });
368 }
369 self.population = size;
370 Ok(self)
371 }
372
373 pub fn with_generations(mut self, generations: usize) -> Result<Self, AnalysisError> {
380 if generations == 0 {
381 return Err(AnalysisError::TooFew {
382 what: "generations",
383 count: 0,
384 minimum: 1,
385 });
386 }
387 self.generations = generations;
388 Ok(self)
389 }
390
391 pub fn with_crossover(mut self, probability: f64, index: f64) -> Result<Self, AnalysisError> {
399 self.crossover_probability = check_probability("crossover probability", probability)?;
400 self.crossover_index = check_index("crossover distribution index", index)?;
401 Ok(self)
402 }
403
404 pub fn with_mutation(mut self, probability: f64, index: f64) -> Result<Self, AnalysisError> {
411 self.mutation_probability = check_probability("mutation probability", probability)?;
412 self.mutation_index = check_index("mutation distribution index", index)?;
413 Ok(self)
414 }
415
416 pub fn variables(&self) -> &[Variable] {
418 &self.variables
419 }
420
421 pub fn objectives(&self) -> usize {
423 self.objectives
424 }
425
426 pub fn population(&self) -> usize {
428 self.population
429 }
430
431 pub fn generations(&self) -> usize {
433 self.generations
434 }
435
436 pub fn start(&self, seed: u64) -> Run {
438 let mut rng = SeededRng::for_stream(seed, &[0]);
439 let candidates = (0..self.population)
440 .map(|_| {
441 self.variables
442 .iter()
443 .map(|v| v.low() + rng.uniform() * (v.high() - v.low()))
444 .collect()
445 })
446 .collect();
447 Run {
448 settings: self.clone(),
449 seed,
450 generation: 0,
451 members: Vec::new(),
452 candidates,
453 }
454 }
455
456 pub fn minimize_constrained(
462 &self,
463 seed: u64,
464 mut model: impl FnMut(&[f64]) -> Goals,
465 ) -> Result<Front, AnalysisError> {
466 let mut run = self.start(seed);
467 loop {
468 let goals: Vec<Goals> = run.candidates().iter().map(|x| model(x)).collect();
469 if let Some(front) = run.tell_constrained(&goals)? {
470 return Ok(front);
471 }
472 }
473 }
474
475 pub fn minimize(
482 &self,
483 seed: u64,
484 mut model: impl FnMut(&[f64]) -> Vec<f64>,
485 ) -> Result<Front, AnalysisError> {
486 let mut run = self.start(seed);
487 loop {
488 let values: Vec<Vec<f64>> = run.candidates().iter().map(|x| model(x)).collect();
489 if let Some(front) = run.tell(&values)? {
490 return Ok(front);
491 }
492 }
493 }
494}
495
496#[derive(Debug, Clone)]
498pub struct Run {
499 settings: Nsga2,
500 seed: u64,
501 generation: usize,
503 members: Vec<Member>,
505 candidates: Vec<Vec<f64>>,
507}
508
509impl Run {
510 pub fn candidates(&self) -> &[Vec<f64>] {
513 &self.candidates
514 }
515
516 pub fn generation(&self) -> usize {
518 self.generation
519 }
520
521 pub fn members(&self) -> &[Member] {
523 &self.members
524 }
525
526 pub fn tell(&mut self, values: &[Vec<f64>]) -> Result<Option<Front>, AnalysisError> {
535 let goals: Vec<Goals> = values.iter().cloned().map(Goals::feasible).collect();
536 self.tell_constrained(&goals)
537 }
538
539 pub fn tell_constrained(&mut self, goals: &[Goals]) -> Result<Option<Front>, AnalysisError> {
552 let m = self.settings.objectives;
553 if goals.len() != self.candidates.len() || self.candidates.is_empty() {
554 return Err(AnalysisError::Length {
555 what: "goals, against the generation's candidates",
556 length: goals.len(),
557 expected: self.candidates.len(),
558 });
559 }
560 let evaluated = self.generation * self.settings.population;
561 let mut children = Vec::with_capacity(goals.len());
562 for (k, (point, g)) in self.candidates.iter().zip(goals).enumerate() {
563 if g.violation.is_nan() || g.violation < 0.0 {
564 return Err(AnalysisError::Domain {
565 what: "constraint violation (must not be negative or NaN)",
566 value: g.violation,
567 });
568 }
569 let mut failed = g.violation == f64::INFINITY;
572 if !failed {
573 if g.objectives.len() != m {
574 return Err(AnalysisError::Length {
575 what: "a design's goals",
576 length: g.objectives.len(),
577 expected: m,
578 });
579 }
580 if let Some(&value) = g
581 .objectives
582 .iter()
583 .find(|v| v.is_nan() || **v == f64::NEG_INFINITY)
584 {
585 return Err(AnalysisError::Output {
586 index: evaluated + k,
587 value,
588 });
589 }
590 failed = g.objectives.contains(&f64::INFINITY);
591 }
592 let objectives = if failed {
593 vec![f64::INFINITY; m]
594 } else {
595 g.objectives.clone()
596 };
597 children.push(Member {
598 point: point.clone(),
599 objectives,
600 violation: if failed { f64::INFINITY } else { g.violation },
601 rank: 0,
602 crowding: 0.0,
603 });
604 }
605 let mut combined = std::mem::take(&mut self.members);
606 combined.extend(children);
607 self.members = survivors(combined, self.settings.population);
608 self.generation += 1;
609 if self.generation == self.settings.generations {
610 self.candidates.clear();
611 let members = self
612 .members
613 .iter()
614 .filter(|m| m.rank == 0)
615 .cloned()
616 .collect();
617 return Ok(Some(Front {
618 members,
619 population: self.members.clone(),
620 evaluations: self.generation * self.settings.population,
621 generations: self.generation,
622 }));
623 }
624 self.candidates = self.breed();
625 Ok(None)
626 }
627
628 fn breed(&self) -> Vec<Vec<f64>> {
636 let s = &self.settings;
637 let mut rng = SeededRng::for_stream(self.seed, &[self.generation as u64]);
639 let contestants = contestants(&mut rng, self.members.len());
640 let parents: Vec<&[f64]> = contestants
641 .as_chunks::<2>()
642 .0
643 .iter()
644 .map(|&[i, j]| self.tournament(i, j, &mut rng))
645 .collect();
646 let mut children = Vec::with_capacity(s.population);
647 for &[a, b] in parents.as_chunks::<2>().0 {
648 let (mut c1, mut c2) = (a.to_vec(), b.to_vec());
649 if rng.uniform() < s.crossover_probability {
650 for (i, v) in s.variables.iter().enumerate() {
651 if rng.uniform() < 0.5 {
652 let (y1, y2) =
653 sbx(a[i], b[i], v.low(), v.high(), s.crossover_index, &mut rng);
654 (c1[i], c2[i]) = (y1, y2);
655 }
656 }
657 }
658 for child in [&mut c1, &mut c2] {
659 for (i, v) in s.variables.iter().enumerate() {
660 if rng.uniform() < s.mutation_probability {
661 child[i] = polynomial_mutation(
662 child[i],
663 v.low(),
664 v.high(),
665 s.mutation_index,
666 &mut rng,
667 );
668 }
669 }
670 }
671 children.push(c1);
672 children.push(c2);
673 }
674 children
675 }
676
677 fn tournament(&self, i: usize, j: usize, rng: &mut SeededRng) -> &[f64] {
680 let (a, b) = (&self.members[i], &self.members[j]);
681 let winner = match crowded_comparison(a, b) {
682 std::cmp::Ordering::Less => a,
683 std::cmp::Ordering::Greater => b,
684 std::cmp::Ordering::Equal => {
685 if rng.uniform() < 0.5 {
686 a
687 } else {
688 b
689 }
690 }
691 };
692 &winner.point
693 }
694}
695
696fn contestants(rng: &mut SeededRng, n: usize) -> Vec<usize> {
699 let mut contestants = Vec::with_capacity(2 * n);
700 for _ in 0..2 {
701 let mut shuffle: Vec<usize> = (0..n).collect();
702 for i in (1..n).rev() {
703 shuffle.swap(i, draw_index(rng, i + 1));
704 }
705 contestants.extend(shuffle);
706 }
707 contestants
708}
709
710fn draw_index(rng: &mut SeededRng, n: usize) -> usize {
713 let k = (rng.uniform() * n as f64).floor() as usize;
715 k.min(n - 1)
716}
717
718fn crowded_comparison(a: &Member, b: &Member) -> std::cmp::Ordering {
720 a.rank
721 .cmp(&b.rank)
722 .then_with(|| b.crowding.total_cmp(&a.crowding))
723}
724
725pub fn dominates(a: &[f64], b: &[f64]) -> bool {
728 a.iter().zip(b).all(|(x, y)| x <= y) && a.iter().zip(b).any(|(x, y)| x < y)
729}
730
731fn constrained_dominates(a: &Member, b: &Member) -> bool {
733 let (fa, fb) = (a.violation == 0.0, b.violation == 0.0);
734 match (fa, fb) {
735 (true, false) => true,
736 (false, true) => false,
737 (false, false) => a.violation < b.violation,
738 (true, true) => dominates(&a.objectives, &b.objectives),
739 }
740}
741
742fn fronts(members: &[Member]) -> Vec<Vec<usize>> {
745 let n = members.len();
746 let mut dominated: Vec<Vec<usize>> = vec![Vec::new(); n];
747 let mut count = vec![0usize; n];
748 for p in 0..n {
749 for q in p + 1..n {
750 if constrained_dominates(&members[p], &members[q]) {
751 dominated[p].push(q);
752 count[q] += 1;
753 } else if constrained_dominates(&members[q], &members[p]) {
754 dominated[q].push(p);
755 count[p] += 1;
756 }
757 }
758 }
759 let mut fronts = Vec::new();
760 let mut current: Vec<usize> = (0..n).filter(|&p| count[p] == 0).collect();
761 while !current.is_empty() {
762 let mut next = Vec::new();
763 for &p in ¤t {
764 for &q in &dominated[p] {
765 count[q] -= 1;
766 if count[q] == 0 {
767 next.push(q);
768 }
769 }
770 }
771 next.sort_unstable();
772 fronts.push(current);
773 current = next;
774 }
775 fronts
776}
777
778fn crowding(front: &[&[f64]]) -> Vec<f64> {
781 let l = front.len();
782 let mut distance = vec![0.0; l];
783 let Some(first) = front.first() else {
784 return distance;
785 };
786 #[allow(
787 clippy::needless_range_loop,
788 reason = "`m` indexes each design's goals, not `front`"
789 )]
790 for m in 0..first.len() {
791 let value = |k: usize| front[k][m];
792 let mut order: Vec<usize> = (0..l).collect();
793 order.sort_by(|&a, &b| value(a).total_cmp(&value(b)));
795 distance[order[0]] = f64::INFINITY;
796 distance[order[l - 1]] = f64::INFINITY;
797 let range = value(order[l - 1]) - value(order[0]);
798 if !(range.is_finite() && range > 0.0) {
799 continue;
800 }
801 for k in 1..l.saturating_sub(1) {
802 distance[order[k]] += (value(order[k + 1]) - value(order[k - 1])) / range;
803 }
804 }
805 distance
806}
807
808fn survivors(combined: Vec<Member>, n: usize) -> Vec<Member> {
812 let fronts = fronts(&combined);
813 let mut ranked: Vec<Option<Member>> = combined.into_iter().map(Some).collect();
814 let mut next = Vec::with_capacity(n);
815 for (rank, front) in fronts.iter().enumerate() {
816 if next.len() == n {
817 break;
818 }
819 let goals: Vec<&[f64]> = front
820 .iter()
821 .filter_map(|&i| ranked[i].as_ref().map(|m| m.objectives.as_slice()))
822 .collect();
823 let distance = crowding(&goals);
824 let mut order: Vec<usize> = (0..front.len()).collect();
825 if next.len() + front.len() > n {
826 order.sort_by(|&a, &b| distance[b].total_cmp(&distance[a]));
828 order.truncate(n - next.len());
829 order.sort_unstable();
830 }
831 for k in order {
832 if let Some(mut member) = ranked[front[k]].take() {
833 member.rank = rank;
834 member.crowding = distance[k];
835 next.push(member);
836 }
837 }
838 }
839 next
840}
841
842fn sbx(x1: f64, x2: f64, low: f64, high: f64, eta: f64, rng: &mut SeededRng) -> (f64, f64) {
855 if (x1 - x2).abs() <= 1e-14 {
856 return (x1, x2);
857 }
858 let (y1, y2) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
859 let u = rng.uniform();
860 let exponent = 1.0 / (eta + 1.0);
861 let beta_q = |beta: f64| {
863 let alpha = 2.0 - beta.powf(-(eta + 1.0));
864 if u <= 1.0 / alpha {
865 (u * alpha).powf(exponent)
866 } else {
867 (1.0 / (2.0 - u * alpha)).powf(exponent)
868 }
869 };
870 let gap = y2 - y1;
871 let c1 = 0.5 * ((y1 + y2) - beta_q(1.0 + 2.0 * (y1 - low) / gap) * gap);
872 let c2 = 0.5 * ((y1 + y2) + beta_q(1.0 + 2.0 * (high - y2) / gap) * gap);
873 let (c1, c2) = (c1.clamp(low, high), c2.clamp(low, high));
874 if rng.uniform() < 0.5 {
875 (c2, c1)
876 } else {
877 (c1, c2)
878 }
879}
880
881fn polynomial_mutation(y: f64, low: f64, high: f64, eta: f64, rng: &mut SeededRng) -> f64 {
890 let range = high - low;
891 let delta1 = (y - low) / range;
892 let delta2 = (high - y) / range;
893 let u = rng.uniform();
894 let power = 1.0 / (eta + 1.0);
895 let delta_q = if u <= 0.5 {
896 let value = 2.0 * u + (1.0 - 2.0 * u) * (1.0 - delta1).powf(eta + 1.0);
897 value.powf(power) - 1.0
898 } else {
899 let value = 2.0 * (1.0 - u) + 2.0 * (u - 0.5) * (1.0 - delta2).powf(eta + 1.0);
900 1.0 - value.powf(power)
901 };
902 (y + delta_q * range).clamp(low, high)
903}
904
905pub fn generational_distance(set: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
910 mean_nearest(set, reference)
911}
912
913pub fn inverted_generational_distance(set: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
917 mean_nearest(reference, set)
918}
919
920fn mean_nearest(from: &[Vec<f64>], to: &[Vec<f64>]) -> f64 {
922 if from.is_empty() || to.is_empty() {
923 return f64::NAN;
924 }
925 let total = from
926 .iter()
927 .map(|p| {
928 to.iter()
929 .map(|q| {
930 p.iter()
931 .zip(q)
932 .map(|(a, b)| (a - b) * (a - b))
933 .sum::<f64>()
934 .sqrt()
935 })
936 .fold(f64::INFINITY, f64::min)
937 })
938 .fold(0.0, |total, d| total + d);
939 total / from.len() as f64
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946
947 fn member(objectives: &[f64], violation: f64) -> Member {
948 Member {
949 point: Vec::new(),
950 objectives: objectives.to_vec(),
951 violation,
952 rank: 0,
953 crowding: 0.0,
954 }
955 }
956
957 fn unit(name: &str) -> Variable {
958 Variable::new(name, 0.5, 0.1)
959 .unwrap()
960 .within(0.0, 1.0)
961 .unwrap()
962 }
963
964 #[test]
965 fn domination_needs_one_strictly_better() {
966 assert!(dominates(&[1.0, 2.0], &[1.0, 3.0]));
967 assert!(!dominates(&[1.0, 2.0], &[1.0, 2.0]), "equal");
968 assert!(!dominates(&[1.0, 3.0], &[2.0, 2.0]), "a trade-off");
969 assert!(dominates(&[1.0, 2.0], &[f64::INFINITY, 2.0]));
970 assert!(constrained_dominates(
972 &member(&[9.0, 9.0], 0.0),
973 &member(&[0.0, 0.0], 0.1)
974 ));
975 assert!(constrained_dominates(
976 &member(&[9.0, 9.0], 0.1),
977 &member(&[0.0, 0.0], 0.2)
978 ));
979 assert!(!constrained_dominates(
980 &member(&[0.0, 0.0], 0.2),
981 &member(&[9.0, 9.0], 0.2)
982 ));
983 }
984
985 #[test]
988 fn fronts_peel_in_order() {
989 let members: Vec<Member> = [[3.0, 4.0], [1.0, 5.0], [5.0, 5.0], [2.0, 3.0], [4.0, 1.0]]
990 .iter()
991 .map(|f| member(f, 0.0))
992 .collect();
993 assert_eq!(fronts(&members), vec![vec![1, 3, 4], vec![0], vec![2]]);
994 let failed = member(&[f64::INFINITY, f64::INFINITY], f64::INFINITY);
996 let mut with_failed = members.clone();
997 with_failed.push(failed.clone());
998 with_failed.push(failed);
999 assert_eq!(
1000 fronts(&with_failed),
1001 vec![vec![1, 3, 4], vec![0], vec![2], vec![5, 6]]
1002 );
1003 }
1004
1005 #[test]
1007 fn crowding_by_hand() {
1008 let goals: [&[f64]; 4] = [&[0.0, 6.0], &[1.0, 4.0], &[3.0, 0.0], &[2.0, 3.0]];
1009 let d = crowding(&goals);
1010 assert_eq!(d[0], f64::INFINITY);
1011 assert_eq!(d[2], f64::INFINITY);
1012 assert!((d[1] - (2.0 / 3.0 + 3.0 / 6.0)).abs() <= 1e-15);
1014 assert!((d[3] - (2.0 / 3.0 + 4.0 / 6.0)).abs() <= 1e-15);
1016 assert_eq!(
1018 crowding(&[&[1.0, 2.0], &[2.0, 1.0]]),
1019 vec![f64::INFINITY; 2]
1020 );
1021 let flat: [&[f64]; 3] = [&[1.0, 5.0], &[2.0, 5.0], &[3.0, 5.0]];
1022 assert_eq!(crowding(&flat)[1], 1.0);
1023 assert!(crowding(&[]).is_empty());
1024 }
1025
1026 #[test]
1028 fn survivors_cut_the_last_front_by_crowding() {
1029 let combined: Vec<Member> = [
1030 [0.0, 1.0],
1031 [0.1, 0.9],
1032 [0.5, 0.5],
1033 [0.55, 0.45],
1034 [1.0, 0.0],
1035 [2.0, 2.0],
1036 ]
1037 .iter()
1038 .map(|f| member(f, 0.0))
1039 .collect();
1040 let next = survivors(combined, 3);
1041 let kept: Vec<Vec<f64>> = next.iter().map(|m| m.objectives.clone()).collect();
1042 assert_eq!(kept, vec![vec![0.0, 1.0], vec![0.1, 0.9], vec![1.0, 0.0]]);
1045 assert!(next.iter().all(|m| m.rank == 0));
1046 }
1047
1048 #[test]
1051 fn sbx_spread_follows_its_density() {
1052 let eta = 2.0;
1053 let draws = 100_000;
1054 let mut rng = SeededRng::seed_from_u64(1);
1055 let betas: Vec<f64> = (0..draws)
1056 .map(|_| {
1057 let (c1, c2) = sbx(-0.5, 0.5, -1e9, 1e9, eta, &mut rng);
1058 (c2 - c1).abs()
1059 })
1060 .collect();
1061 for b in [0.25_f64, 0.5, 0.8, 1.0, 1.5, 3.0] {
1062 let expected: f64 = if b <= 1.0 {
1063 0.5 * b.powf(eta + 1.0)
1064 } else {
1065 1.0 - 0.5 * b.powf(-(eta + 1.0))
1066 };
1067 let share = betas.iter().filter(|&&x| x <= b).count() as f64 / draws as f64;
1068 let sigma = (expected * (1.0 - expected) / draws as f64).sqrt();
1069 assert!(
1070 (share - expected).abs() <= 5.0 * sigma + 1e-6,
1071 "P(β ≤ {b}) {share} against {expected}"
1072 );
1073 }
1074 }
1075
1076 #[test]
1079 fn sbx_and_mutation_stay_within_bounds() {
1080 let mut rng = SeededRng::seed_from_u64(2);
1081 let mut at_bound = 0;
1082 for _ in 0..20_000 {
1083 let (c1, c2) = sbx(0.001, 0.3, 0.0, 1.0, 20.0, &mut rng);
1084 assert!((0.0..=1.0).contains(&c1) && (0.0..=1.0).contains(&c2));
1085 at_bound += usize::from(c1 == 0.0 || c2 == 0.0);
1086 let (c1, c2) = sbx(0.7, 0.999, 0.0, 1.0, 20.0, &mut rng);
1087 assert!((0.0..=1.0).contains(&c1) && (0.0..=1.0).contains(&c2));
1088 at_bound += usize::from(c1 == 1.0 || c2 == 1.0);
1089 let y = polynomial_mutation(0.999, 0.0, 1.0, 20.0, &mut rng);
1090 assert!((0.0..=1.0).contains(&y));
1091 at_bound += usize::from(y == 1.0);
1092 let y = polynomial_mutation(0.001, 0.0, 1.0, 20.0, &mut rng);
1093 assert!((0.0..=1.0).contains(&y));
1094 at_bound += usize::from(y == 0.0);
1095 }
1096 assert_eq!(at_bound, 0);
1097 assert_eq!(sbx(0.3, 0.3, 0.0, 1.0, 20.0, &mut rng), (0.3, 0.3));
1099 }
1100
1101 #[test]
1106 fn mutation_step_follows_its_distribution() {
1107 let draws = 100_000;
1108 for eta in [5.0_f64, 20.0] {
1109 let mut rng = SeededRng::seed_from_u64(3);
1110 let steps: Vec<f64> = (0..draws)
1111 .map(|_| polynomial_mutation(0.2, 0.0, 1.0, eta, &mut rng) - 0.2)
1112 .collect();
1113 let share = |f: &dyn Fn(f64) -> bool| {
1114 steps.iter().filter(|&&x| f(x)).count() as f64 / draws as f64
1115 };
1116 let check = |measured: f64, expected: f64, what: &str| {
1117 let sigma = (expected * (1.0 - expected) / draws as f64).sqrt();
1118 assert!(
1119 (measured - expected).abs() <= 5.0 * sigma + 1e-6,
1120 "η = {eta}, {what}: {measured} against {expected}"
1121 );
1122 };
1123 let (t1, t2) = (0.8_f64.powf(eta + 1.0), 0.2_f64.powf(eta + 1.0));
1124 for d in [0.01_f64, 0.03, 0.1, 0.19] {
1125 let below = ((1.0 - d).powf(eta + 1.0) - t1) / (2.0 * (1.0 - t1));
1126 check(share(&|x| x <= -d), below, &format!("P(δ ≤ −{d})"));
1127 let above = ((1.0 - d).powf(eta + 1.0) - t2) / (2.0 * (1.0 - t2));
1128 check(share(&|x| x >= d), above, &format!("P(δ ≥ {d})"));
1129 }
1130 check(share(&|x| x < 0.0), 0.5, "P(δ < 0)");
1131 }
1132 }
1133
1134 #[test]
1141 fn sbx_spread_is_cut_at_each_bound() {
1142 let (eta, draws) = (2.0_f64, 100_000);
1143 let (y1, y2, low, high) = (0.05_f64, 0.3_f64, 0.0_f64, 0.4_f64);
1144 let gap = y2 - y1;
1145 let mut rng = SeededRng::seed_from_u64(4);
1146 let mut lower_first = 0;
1147 let (mut lower, mut upper) = (Vec::new(), Vec::new());
1148 for _ in 0..draws {
1149 let (c1, c2) = sbx(y1, y2, low, high, eta, &mut rng);
1150 lower_first += usize::from(c1 < c2);
1151 lower.push((y1 + y2 - 2.0 * c1.min(c2)) / gap);
1152 upper.push((2.0 * c1.max(c2) - y1 - y2) / gap);
1153 }
1154 for (betas, beta_b, side) in [
1155 (&lower, 1.0 + 2.0 * (y1 - low) / gap, "lower"),
1156 (&upper, 1.0 + 2.0 * (high - y2) / gap, "upper"),
1157 ] {
1158 let alpha = 2.0 - beta_b.powf(-(eta + 1.0));
1159 for b in [
1160 0.5, 0.9, 0.94, 0.945, 0.96, 0.98, 1.0, 1.1, 1.3, 1.39, 1.6, 1.79,
1161 ] {
1162 if b > beta_b {
1163 continue;
1164 }
1165 let expected = if b <= 1.0 {
1166 b.powf(eta + 1.0) / alpha
1167 } else {
1168 (2.0 - b.powf(-(eta + 1.0))) / alpha
1169 };
1170 let share = betas.iter().filter(|&&x| x <= b).count() as f64 / draws as f64;
1171 let sigma = (expected * (1.0 - expected) / draws as f64).sqrt();
1172 assert!(
1173 (share - expected).abs() <= 5.0 * sigma + 1e-6,
1174 "{side} child: P(β ≤ {b}) {share} against {expected}"
1175 );
1176 }
1177 assert!(betas.iter().all(|&b| b <= beta_b * (1.0 + 1e-12)));
1178 }
1179 let half = lower_first as f64 / draws as f64;
1180 assert!(
1181 (half - 0.5).abs() <= 5.0 * (0.25 / draws as f64).sqrt(),
1182 "{half}"
1183 );
1184 }
1185
1186 #[test]
1188 fn every_design_plays_two_tournaments() {
1189 for n in [4, 6, 20, 100] {
1190 for seed in 0..20 {
1191 let mut rng = SeededRng::seed_from_u64(seed);
1192 let c = contestants(&mut rng, n);
1193 assert_eq!(c.len(), 2 * n);
1194 for i in 0..n {
1195 assert_eq!(c.iter().filter(|&&k| k == i).count(), 2);
1196 }
1197 assert!(c.as_chunks::<2>().0.iter().all(|[a, b]| a != b));
1198 }
1199 }
1200 let mut rng = SeededRng::seed_from_u64(9);
1204 let (mut first, mut second, mut same) = ([0_usize; 4], [0_usize; 4], 0_usize);
1205 let draws = 40_000;
1206 for _ in 0..draws {
1207 let c = contestants(&mut rng, 4);
1208 first[c[0]] += 1;
1209 second[c[4]] += 1;
1210 same += usize::from(c[0] == c[4]);
1211 }
1212 let near_quarter = |count: usize| {
1213 let share = count as f64 / draws as f64;
1214 (share - 0.25).abs() <= 5.0 * (0.1875 / draws as f64).sqrt()
1215 };
1216 assert!(
1217 first.into_iter().chain(second).all(near_quarter),
1218 "{first:?} {second:?}"
1219 );
1220 assert!(near_quarter(same), "{same}");
1221 }
1222
1223 #[test]
1226 fn infinite_goals_fail() {
1227 let n = Nsga2::new(vec![unit("x")], 2)
1228 .unwrap()
1229 .with_population(4)
1230 .unwrap()
1231 .with_generations(1)
1232 .unwrap();
1233 let mut run = n.start(1);
1234 let goals = vec![
1235 Goals::feasible(vec![f64::INFINITY, -100.0]),
1236 Goals::constrained(vec![1.0, 1.0], &[0.5]),
1237 Goals::feasible(vec![2.0, 2.0]),
1238 Goals::feasible(vec![3.0, 1.0]),
1239 ];
1240 let front = run.tell_constrained(&goals).unwrap().unwrap();
1241 let members = run.members();
1242 assert_eq!(members.len(), 4);
1243 let infinite = members
1244 .iter()
1245 .find(|m| m.violation == f64::INFINITY)
1246 .unwrap();
1247 let broken = members.iter().find(|m| m.violation == 0.5).unwrap();
1248 assert_eq!(infinite.objectives, vec![f64::INFINITY; 2]);
1249 assert!(infinite.rank > broken.rank);
1250 assert!(front.is_feasible() && front.members.len() == 2);
1251 let mut run = n.start(2);
1252 let all = vec![
1253 vec![f64::INFINITY, 0.0],
1254 vec![-100.0, f64::INFINITY],
1255 vec![f64::INFINITY, 0.0],
1256 vec![-100.0, f64::INFINITY],
1257 ];
1258 let front = run.tell(&all).unwrap().unwrap();
1259 assert!(!front.is_feasible());
1260 }
1261
1262 #[test]
1263 fn settings_are_checked() {
1264 let x = || vec![unit("x")];
1265 assert!(Nsga2::new(x(), 0).is_err());
1266 assert!(Nsga2::new(x(), MAX_OBJECTIVES + 1).is_err());
1267 assert!(Nsga2::new(vec![], 2).is_err());
1268 let open = Variable::new("x", 0.0, 1.0).unwrap();
1269 assert!(matches!(
1270 Nsga2::new(vec![open.clone()], 2),
1271 Err(AnalysisError::Domain { what, .. }) if what.contains("low bound")
1272 ));
1273 let half = open.within(0.0, f64::INFINITY).unwrap();
1274 assert!(matches!(
1275 Nsga2::new(vec![half], 2),
1276 Err(AnalysisError::Domain { what, .. }) if what.contains("high bound")
1277 ));
1278 let whole = Variable::new("k", 1.0, 1.0)
1279 .unwrap()
1280 .within(0.0, 4.0)
1281 .unwrap()
1282 .integer()
1283 .unwrap();
1284 assert!(matches!(
1285 Nsga2::new(vec![whole], 2),
1286 Err(AnalysisError::Unsupported(_))
1287 ));
1288 let n = Nsga2::new(x(), 2).unwrap();
1289 assert!(matches!(
1290 n.clone().with_population(5),
1291 Err(AnalysisError::Domain { .. })
1292 ));
1293 assert!(matches!(
1294 n.clone().with_population(2),
1295 Err(AnalysisError::TooFew { .. })
1296 ));
1297 assert!(matches!(
1298 n.clone().with_population(MAX_POPULATION + 2),
1299 Err(AnalysisError::Count { .. })
1300 ));
1301 assert!(n.clone().with_population(MAX_POPULATION).is_ok());
1302 let wide = Variable::new("x", 0.0, 1.0)
1303 .unwrap()
1304 .within(-f64::MAX, f64::MAX)
1305 .unwrap();
1306 assert!(matches!(
1307 Nsga2::new(vec![wide], 2),
1308 Err(AnalysisError::Domain { what, .. }) if what.contains("MAX/4")
1309 ));
1310 let edge = |high: f64| {
1311 Variable::new("x", 0.0, 1.0)
1312 .unwrap()
1313 .within(-MAX_BOUND, high)
1314 };
1315 assert!(Nsga2::new(vec![edge(MAX_BOUND).unwrap()], 2).is_ok());
1316 assert!(Nsga2::new(vec![edge(MAX_BOUND.next_up()).unwrap()], 2).is_err());
1317 let mut rng = SeededRng::seed_from_u64(6);
1319 for _ in 0..20_000 {
1320 let (c1, c2) = sbx(
1321 0.9 * MAX_BOUND,
1322 0.99 * MAX_BOUND,
1323 -MAX_BOUND,
1324 MAX_BOUND,
1325 20.0,
1326 &mut rng,
1327 );
1328 assert!(c1.abs() < MAX_BOUND && c2.abs() < MAX_BOUND);
1329 }
1330 assert!(n.clone().with_generations(0).is_err());
1331 assert!(n.clone().with_crossover(1.1, 20.0).is_err());
1332 assert!(n.clone().with_crossover(f64::NAN, 20.0).is_err());
1333 assert!(n.clone().with_mutation(0.5, -1.0).is_err());
1334 assert!(n.clone().with_mutation(0.5, f64::INFINITY).is_err());
1335 let n = n.with_population(4).unwrap().with_generations(3).unwrap();
1336 assert_eq!((n.population(), n.generations(), n.objectives()), (4, 3, 2));
1337 let json = serde_json::to_string(&n).unwrap();
1338 let back: Nsga2 = serde_json::from_str(&json).unwrap();
1339 assert_eq!(back, n);
1340 let bad = json.replace("\"population\":4", "\"population\":3");
1341 assert!(serde_json::from_str::<Nsga2>(&bad).is_err());
1342 }
1343
1344 #[test]
1345 fn telling_checks_the_goals() {
1346 let n = Nsga2::new(vec![unit("x")], 2)
1347 .unwrap()
1348 .with_population(4)
1349 .unwrap()
1350 .with_generations(2)
1351 .unwrap();
1352 let mut run = n.start(1);
1353 assert_eq!(run.candidates().len(), 4);
1354 assert!(
1355 run.candidates()
1356 .iter()
1357 .flatten()
1358 .all(|x| (0.0..1.0).contains(x))
1359 );
1360 let ok = vec![vec![1.0, 2.0]; 4];
1361 assert!(matches!(
1362 run.tell(&ok[..3]),
1363 Err(AnalysisError::Length { .. })
1364 ));
1365 let mut short = ok.clone();
1366 short[2] = vec![1.0];
1367 assert!(matches!(
1368 run.tell(&short),
1369 Err(AnalysisError::Length { .. })
1370 ));
1371 let mut nan = ok.clone();
1372 nan[3] = vec![1.0, f64::NAN];
1373 assert!(matches!(
1374 run.tell(&nan),
1375 Err(AnalysisError::Output { index: 3, .. })
1376 ));
1377 let mut minus = ok.clone();
1378 minus[1] = vec![f64::NEG_INFINITY, 0.0];
1379 assert!(matches!(
1380 run.tell(&minus),
1381 Err(AnalysisError::Output { index: 1, .. })
1382 ));
1383 let mut goals: Vec<Goals> = ok.iter().cloned().map(Goals::feasible).collect();
1384 goals[0].violation = -1.0;
1385 assert!(matches!(
1386 run.tell_constrained(&goals),
1387 Err(AnalysisError::Domain { .. })
1388 ));
1389 goals[0] = Goals::failed();
1392 assert_eq!(run.tell_constrained(&goals).unwrap(), None);
1393 assert_eq!(run.generation(), 1);
1394 assert_eq!(run.members().last().unwrap().violation, f64::INFINITY);
1395 assert!(matches!(
1396 run.tell(&nan),
1397 Err(AnalysisError::Output { index: 7, .. })
1398 ));
1399 let front = run.tell(&ok).unwrap().unwrap();
1400 assert_eq!((front.evaluations, front.generations), (8, 2));
1401 assert!(run.candidates().is_empty());
1402 assert!(matches!(run.tell(&ok), Err(AnalysisError::Length { .. })));
1403 let json = serde_json::to_string(&front).unwrap();
1405 assert!(json.contains("null"));
1406 assert_eq!(serde_json::from_str::<Front>(&json).unwrap(), front);
1407 }
1408
1409 #[test]
1412 fn constraints_and_failures_rank_last() {
1413 let n = Nsga2::new(vec![unit("x")], 2)
1415 .unwrap()
1416 .with_population(20)
1417 .unwrap()
1418 .with_generations(30)
1419 .unwrap();
1420 let front = n
1421 .minimize_constrained(5, |x| {
1422 if x[0] < 0.1 {
1423 Goals::failed()
1424 } else {
1425 Goals::constrained(vec![x[0], 1.0 - x[0]], &[0.6 - x[0]])
1426 }
1427 })
1428 .unwrap();
1429 assert!(front.is_feasible());
1430 assert_eq!(front.members.len(), 20);
1431 assert!(front.members.iter().all(|m| m.point[0] >= 0.6));
1432 let low = front.members.iter().map(|m| m.point[0]).fold(1.0, f64::min);
1434 let high = front.members.iter().map(|m| m.point[0]).fold(0.0, f64::max);
1435 assert!(low < 0.61 && high > 0.99, "{low} to {high}");
1436 }
1437
1438 #[test]
1439 fn distances_by_hand() {
1440 let set = vec![vec![0.0, 0.0], vec![3.0, 4.0]];
1441 let reference = vec![vec![0.0, 1.0], vec![3.0, 0.0]];
1442 assert_eq!(generational_distance(&set, &reference), 2.5);
1444 assert_eq!(inverted_generational_distance(&set, &reference), 2.0);
1445 assert!(generational_distance(&[], &reference).is_nan());
1446 }
1447}