1use crate::error::{OptimizeError, OptimizeResult};
25use scirs2_core::random::rngs::StdRng;
26use scirs2_core::random::{Rng, RngExt, SeedableRng};
27
28pub fn dominates(a: &[f64], b: &[f64]) -> bool {
46 if a.len() != b.len() || a.is_empty() {
47 return false;
48 }
49 let mut strictly_better = false;
50 for (&ai, &bi) in a.iter().zip(b.iter()) {
51 if ai > bi {
52 return false;
53 }
54 if ai < bi {
55 strictly_better = true;
56 }
57 }
58 strictly_better
59}
60
61pub fn non_dominated_sort(solutions: &[Vec<f64>]) -> Vec<Vec<usize>> {
75 let n = solutions.len();
76 let mut dom_count = vec![0usize; n];
77 let mut dom_set: Vec<Vec<usize>> = vec![Vec::new(); n];
78
79 for i in 0..n {
80 for j in (i + 1)..n {
81 if dominates(&solutions[i], &solutions[j]) {
82 dom_set[i].push(j);
83 dom_count[j] += 1;
84 } else if dominates(&solutions[j], &solutions[i]) {
85 dom_set[j].push(i);
86 dom_count[i] += 1;
87 }
88 }
89 }
90
91 let mut fronts: Vec<Vec<usize>> = Vec::new();
92 let mut current_front: Vec<usize> = (0..n).filter(|&i| dom_count[i] == 0).collect();
93
94 while !current_front.is_empty() {
95 let mut next_front = Vec::new();
96 for &p in ¤t_front {
97 for &q in &dom_set[p] {
98 dom_count[q] -= 1;
99 if dom_count[q] == 0 {
100 next_front.push(q);
101 }
102 }
103 }
104 fronts.push(current_front);
105 current_front = next_front;
106 }
107
108 fronts
109}
110
111pub fn hypervolume(pareto_front: &[Vec<f64>], reference_point: &[f64]) -> f64 {
129 if pareto_front.is_empty() {
130 return 0.0;
131 }
132 match reference_point.len() {
133 0 => 0.0,
134 1 => hv_1d(pareto_front, reference_point),
135 2 => hv_2d(pareto_front, reference_point),
136 _ => hv_mc(pareto_front, reference_point, 100_000),
137 }
138}
139
140fn hv_1d(front: &[Vec<f64>], ref_pt: &[f64]) -> f64 {
141 let min_f = front
142 .iter()
143 .filter_map(|f| f.first().copied())
144 .fold(f64::INFINITY, f64::min);
145 (ref_pt[0] - min_f).max(0.0)
146}
147
148fn hv_2d(front: &[Vec<f64>], ref_pt: &[f64]) -> f64 {
149 let mut pts: Vec<(f64, f64)> = front
150 .iter()
151 .filter(|f| f.len() >= 2 && f[0] < ref_pt[0] && f[1] < ref_pt[1])
152 .map(|f| (f[0], f[1]))
153 .collect();
154
155 if pts.is_empty() {
156 return 0.0;
157 }
158
159 pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
164
165 let n = pts.len();
166 let mut hv = 0.0f64;
167 let mut min_y = ref_pt[1];
168
169 for i in 0..n {
170 let (x_i, y_i) = pts[i];
171 let next_x = if i + 1 < n { pts[i + 1].0 } else { ref_pt[0] };
172 if y_i < min_y {
173 min_y = y_i;
174 }
175 hv += (next_x - x_i) * (ref_pt[1] - min_y);
176 }
177 hv
178}
179
180fn hv_mc(front: &[Vec<f64>], ref_pt: &[f64], n_samples: usize) -> f64 {
181 let n_obj = ref_pt.len();
182 let ideal: Vec<f64> = (0..n_obj)
183 .map(|j| {
184 front
185 .iter()
186 .filter_map(|f| f.get(j).copied())
187 .fold(f64::INFINITY, f64::min)
188 })
189 .collect();
190
191 let box_vol: f64 = (0..n_obj)
192 .map(|j| (ref_pt[j] - ideal[j]).max(0.0))
193 .product();
194 if box_vol <= 0.0 {
195 return 0.0;
196 }
197
198 let mut rng = StdRng::seed_from_u64(42);
199 let count = (0..n_samples)
200 .filter(|_| {
201 let sample: Vec<f64> = (0..n_obj)
202 .map(|j| ideal[j] + rng.random::<f64>() * (ref_pt[j] - ideal[j]))
203 .collect();
204 front
205 .iter()
206 .any(|f| f.len() >= n_obj && (0..n_obj).all(|j| f[j] <= sample[j]))
207 })
208 .count();
209
210 box_vol * count as f64 / n_samples as f64
211}
212
213pub fn crowding_distance(front: &[Vec<f64>]) -> Vec<f64> {
231 let n = front.len();
232 if n == 0 {
233 return Vec::new();
234 }
235 if n <= 2 {
236 return vec![f64::INFINITY; n];
237 }
238
239 let n_obj = front[0].len();
240 let mut dist = vec![0.0f64; n];
241
242 for obj in 0..n_obj {
243 let mut order: Vec<usize> = (0..n).collect();
244 order.sort_by(|&a, &b| {
245 front[a][obj]
246 .partial_cmp(&front[b][obj])
247 .unwrap_or(std::cmp::Ordering::Equal)
248 });
249
250 let f_min = front[order[0]][obj];
251 let f_max = front[order[n - 1]][obj];
252 let range = f_max - f_min;
253
254 dist[order[0]] = f64::INFINITY;
255 dist[order[n - 1]] = f64::INFINITY;
256
257 if range < 1e-14 {
258 continue;
259 }
260
261 for k in 1..(n - 1) {
262 dist[order[k]] += (front[order[k + 1]][obj] - front[order[k - 1]][obj]) / range;
263 }
264 }
265
266 dist
267}
268
269#[derive(Debug, Clone)]
289pub struct NsgaIII {
290 pub n_objectives: usize,
292 pub population_size: usize,
294 pub n_generations: usize,
296 pub crossover_prob: f64,
298 pub mutation_prob: f64,
300 pub seed: u64,
302 pub n_divisions: usize,
304}
305
306impl NsgaIII {
307 pub fn new(n_objectives: usize) -> Self {
309 Self {
310 n_objectives,
311 population_size: 100,
312 n_generations: 100,
313 crossover_prob: 0.9,
314 mutation_prob: 0.01,
315 seed: 42,
316 n_divisions: 4,
317 }
318 }
319
320 pub fn optimize<F>(&self, objectives: F, bounds: &[(f64, f64)]) -> OptimizeResult<NsgaResult>
322 where
323 F: Fn(&[f64]) -> Vec<f64>,
324 {
325 if bounds.is_empty() {
326 return Err(OptimizeError::InvalidInput(
327 "bounds must not be empty".to_string(),
328 ));
329 }
330
331 let (pareto_dec, pareto_obj, n_eval) = run_nsga3_internal(
332 &objectives,
333 bounds,
334 self.n_objectives,
335 self.population_size,
336 self.n_generations,
337 self.crossover_prob,
338 self.mutation_prob,
339 self.seed,
340 self.n_divisions,
341 );
342
343 Ok(NsgaResult {
344 pareto_front: pareto_dec,
345 objective_values: pareto_obj,
346 n_evaluations: n_eval,
347 })
348 }
349}
350
351#[derive(Debug, Clone)]
353pub struct NsgaResult {
354 pub pareto_front: Vec<Vec<f64>>,
356 pub objective_values: Vec<Vec<f64>>,
358 pub n_evaluations: usize,
360}
361
362pub fn weighted_sum_optimize<F>(
371 objectives: F,
372 weights: &[f64],
373 bounds: &[(f64, f64)],
374 n_starts: usize,
375 seed: u64,
376) -> (Vec<f64>, Vec<f64>)
377where
378 F: Fn(&[f64]) -> Vec<f64>,
379{
380 let n_vars = bounds.len();
381 let n_starts = n_starts.max(1);
382
383 let scalar = |x: &[f64]| -> f64 {
384 objectives(x)
385 .iter()
386 .zip(weights.iter())
387 .map(|(&fi, &wi)| wi * fi)
388 .sum::<f64>()
389 };
390
391 let mut rng = StdRng::seed_from_u64(seed);
392 let mut best_x = vec![0.0f64; n_vars];
393 let mut best_val = f64::INFINITY;
394
395 for _ in 0..n_starts {
396 let mut x: Vec<f64> = bounds
397 .iter()
398 .map(|&(lb, ub)| lb + rng.random::<f64>() * (ub - lb))
399 .collect();
400
401 let avg_range =
402 bounds.iter().map(|&(lb, ub)| (ub - lb).abs()).sum::<f64>() / n_vars.max(1) as f64;
403 let mut step = 0.1 * avg_range;
404 let mut fx = scalar(&x);
405
406 loop {
407 let mut improved = false;
408 for j in 0..n_vars {
409 let (lb, ub) = bounds[j];
410 for &dir in &[1.0f64, -1.0] {
411 let xj_new = (x[j] + dir * step).clamp(lb, ub);
412 let mut x_try = x.clone();
413 x_try[j] = xj_new;
414 let f_try = scalar(&x_try);
415 if f_try < fx - 1e-12 {
416 x[j] = xj_new;
417 fx = f_try;
418 improved = true;
419 }
420 }
421 }
422 if !improved {
423 step *= 0.5;
424 if step < 1e-8 {
425 break;
426 }
427 }
428 }
429
430 if fx < best_val {
431 best_val = fx;
432 best_x = x;
433 }
434 }
435
436 let best_f = objectives(&best_x);
437 (best_x, best_f)
438}
439
440pub fn epsilon_constraint_optimize<F>(
451 objectives: F,
452 primary_idx: usize,
453 epsilon_bounds: &[(f64, f64)],
454 bounds: &[(f64, f64)],
455 n_starts: usize,
456 seed: u64,
457) -> (Vec<f64>, Vec<f64>)
458where
459 F: Fn(&[f64]) -> Vec<f64>,
460{
461 let n_vars = bounds.len();
462 let n_starts = n_starts.max(1);
463 let penalty_rho = 1e4;
464
465 let penalized = |x: &[f64]| -> f64 {
466 let f = objectives(x);
467 let primary = f.get(primary_idx).copied().unwrap_or(f64::INFINITY);
468 let mut viol = 0.0f64;
469 let mut eps_idx = 0usize;
470 for (i, &fi) in f.iter().enumerate() {
471 if i == primary_idx {
472 continue;
473 }
474 if let Some(&(lb, ub)) = epsilon_bounds.get(eps_idx) {
475 viol += (lb - fi).max(0.0) + (fi - ub).max(0.0);
476 }
477 eps_idx += 1;
478 }
479 primary + penalty_rho * viol
480 };
481
482 let mut rng = StdRng::seed_from_u64(seed);
483 let mut best_x = vec![0.0f64; n_vars];
484 let mut best_val = f64::INFINITY;
485
486 for _ in 0..n_starts {
487 let mut x: Vec<f64> = bounds
488 .iter()
489 .map(|&(lb, ub)| lb + rng.random::<f64>() * (ub - lb))
490 .collect();
491
492 let avg_range =
493 bounds.iter().map(|&(lb, ub)| (ub - lb).abs()).sum::<f64>() / n_vars.max(1) as f64;
494 let mut step = 0.1 * avg_range;
495 let mut fx = penalized(&x);
496
497 loop {
498 let mut improved = false;
499 for j in 0..n_vars {
500 let (lb, ub) = bounds[j];
501 for &dir in &[1.0f64, -1.0] {
502 let xj_new = (x[j] + dir * step).clamp(lb, ub);
503 let mut x_try = x.clone();
504 x_try[j] = xj_new;
505 let f_try = penalized(&x_try);
506 if f_try < fx - 1e-12 {
507 x[j] = xj_new;
508 fx = f_try;
509 improved = true;
510 }
511 }
512 }
513 if !improved {
514 step *= 0.5;
515 if step < 1e-8 {
516 break;
517 }
518 }
519 }
520
521 if fx < best_val {
522 best_val = fx;
523 best_x = x;
524 }
525 }
526
527 let best_f = objectives(&best_x);
528 (best_x, best_f)
529}
530
531#[allow(clippy::too_many_arguments)]
536fn run_nsga3_internal<F>(
537 objectives: &F,
538 bounds: &[(f64, f64)],
539 n_obj: usize,
540 pop_size: usize,
541 n_gen: usize,
542 cx_prob: f64,
543 mut_prob: f64,
544 seed: u64,
545 n_div: usize,
546) -> (Vec<Vec<f64>>, Vec<Vec<f64>>, usize)
547where
548 F: Fn(&[f64]) -> Vec<f64>,
549{
550 let n_vars = bounds.len();
551 let mut rng = StdRng::seed_from_u64(seed);
552 let mut n_eval = 0usize;
553
554 let ref_pts = gen_reference_points(n_obj, n_div);
555
556 let mut pop: Vec<Vec<f64>> = (0..pop_size)
557 .map(|_| {
558 bounds
559 .iter()
560 .map(|&(lb, ub)| lb + rng.random::<f64>() * (ub - lb))
561 .collect()
562 })
563 .collect();
564
565 let mut obj_vals: Vec<Vec<f64>> = pop
566 .iter()
567 .map(|x| {
568 n_eval += 1;
569 objectives(x)
570 })
571 .collect();
572
573 for _gen in 0..n_gen {
574 let mut offspring: Vec<Vec<f64>> = Vec::with_capacity(pop_size);
575
576 while offspring.len() < pop_size {
577 let p1 = rng.random_range(0..pop_size);
578 let p2 = rng.random_range(0..pop_size);
579
580 let (c1, c2) = if rng.random::<f64>() < cx_prob {
581 sbx_crossover(&pop[p1], &pop[p2], bounds, &mut rng, 20.0)
582 } else {
583 (pop[p1].clone(), pop[p2].clone())
584 };
585
586 let c1 = poly_mutation(c1, bounds, &mut rng, mut_prob, 20.0);
587 let c2 = poly_mutation(c2, bounds, &mut rng, mut_prob, 20.0);
588
589 offspring.push(c1);
590 if offspring.len() < pop_size {
591 offspring.push(c2);
592 }
593 }
594
595 let offspring_obj: Vec<Vec<f64>> = offspring
596 .iter()
597 .map(|x| {
598 n_eval += 1;
599 objectives(x)
600 })
601 .collect();
602
603 let combined: Vec<Vec<f64>> = pop.iter().chain(offspring.iter()).cloned().collect();
604 let combined_obj: Vec<Vec<f64>> = obj_vals
605 .iter()
606 .chain(offspring_obj.iter())
607 .cloned()
608 .collect();
609
610 let fronts = non_dominated_sort(&combined_obj);
611
612 let mut new_pop: Vec<Vec<f64>> = Vec::with_capacity(pop_size);
613 let mut new_obj: Vec<Vec<f64>> = Vec::with_capacity(pop_size);
614 let mut critical_indices: Vec<usize> = Vec::new();
615
616 for front in &fronts {
617 if new_pop.len() + front.len() <= pop_size {
618 for &idx in front {
619 new_pop.push(combined[idx].clone());
620 new_obj.push(combined_obj[idx].clone());
621 }
622 } else {
623 critical_indices = front.clone();
624 break;
625 }
626 }
627
628 let remaining = pop_size.saturating_sub(new_pop.len());
629 if remaining > 0 && !critical_indices.is_empty() {
630 let selected = niche_select(
631 &critical_indices,
632 &combined_obj,
633 &new_obj,
634 &ref_pts,
635 remaining,
636 n_obj,
637 &mut rng,
638 );
639 for idx in selected {
640 new_pop.push(combined[idx].clone());
641 new_obj.push(combined_obj[idx].clone());
642 }
643 }
644
645 pop = new_pop;
646 obj_vals = new_obj;
647 }
648
649 let fronts = non_dominated_sort(&obj_vals);
650 let pareto_idx = fronts.into_iter().next().unwrap_or_default();
651 let dec: Vec<Vec<f64>> = pareto_idx.iter().map(|&i| pop[i].clone()).collect();
652 let obj: Vec<Vec<f64>> = pareto_idx.iter().map(|&i| obj_vals[i].clone()).collect();
653
654 (dec, obj, n_eval)
655}
656
657fn gen_reference_points(n_obj: usize, n_div: usize) -> Vec<Vec<f64>> {
658 let mut points = Vec::new();
659 let mut cur = vec![0usize; n_obj];
660 gen_ref_rec(n_obj, n_div, 0, n_div, &mut cur, &mut points);
661 points
662}
663
664fn gen_ref_rec(
665 n_obj: usize,
666 n_div: usize,
667 idx: usize,
668 rem: usize,
669 cur: &mut Vec<usize>,
670 points: &mut Vec<Vec<f64>>,
671) {
672 if idx == n_obj - 1 {
673 cur[idx] = rem;
674 points.push(
675 cur.iter()
676 .map(|&v| v as f64 / n_div.max(1) as f64)
677 .collect(),
678 );
679 return;
680 }
681 for v in 0..=rem {
682 cur[idx] = v;
683 gen_ref_rec(n_obj, n_div, idx + 1, rem - v, cur, points);
684 }
685}
686
687#[allow(clippy::too_many_arguments)]
688fn niche_select(
689 critical: &[usize],
690 all_obj: &[Vec<f64>],
691 selected_obj: &[Vec<f64>],
692 ref_pts: &[Vec<f64>],
693 n_select: usize,
694 n_obj: usize,
695 rng: &mut StdRng,
696) -> Vec<usize> {
697 let n_ref = ref_pts.len();
698 if n_ref == 0 {
699 let mut idx = critical.to_vec();
700 idx.truncate(n_select);
701 return idx;
702 }
703
704 let all_selected_obj: Vec<&Vec<f64>> = selected_obj
705 .iter()
706 .chain(critical.iter().map(|&i| &all_obj[i]))
707 .collect();
708
709 let ideal: Vec<f64> = (0..n_obj)
710 .map(|j| {
711 all_selected_obj
712 .iter()
713 .filter_map(|f| f.get(j).copied())
714 .fold(f64::INFINITY, f64::min)
715 })
716 .collect();
717 let nadir: Vec<f64> = (0..n_obj)
718 .map(|j| {
719 all_selected_obj
720 .iter()
721 .filter_map(|f| f.get(j).copied())
722 .fold(f64::NEG_INFINITY, f64::max)
723 })
724 .collect();
725
726 let norm = |f: &[f64]| -> Vec<f64> {
727 (0..n_obj)
728 .map(|j| {
729 let range = (nadir[j] - ideal[j]).max(1e-10);
730 (f.get(j).copied().unwrap_or(0.0) - ideal[j]) / range
731 })
732 .collect()
733 };
734
735 let line_dist = |f_n: &[f64], rp: &[f64]| -> f64 {
736 let rp_norm: f64 = rp.iter().map(|&r| r * r).sum::<f64>().sqrt().max(1e-12);
737 let dot: f64 = f_n.iter().zip(rp).map(|(&fi, &ri)| fi * ri).sum();
738 let proj = dot / rp_norm;
739 let proj_pt: Vec<f64> = rp.iter().map(|&ri| proj * ri / rp_norm).collect();
740 f_n.iter()
741 .zip(&proj_pt)
742 .map(|(&fi, &pi)| (fi - pi).powi(2))
743 .sum::<f64>()
744 .sqrt()
745 };
746
747 let mut niche_count = vec![0usize; n_ref];
748 for f in selected_obj {
749 let fn_ = norm(f);
750 let best = (0..n_ref)
751 .min_by(|&r1, &r2| {
752 line_dist(&fn_, &ref_pts[r1])
753 .partial_cmp(&line_dist(&fn_, &ref_pts[r2]))
754 .unwrap_or(std::cmp::Ordering::Equal)
755 })
756 .unwrap_or(0);
757 niche_count[best] += 1;
758 }
759
760 let info: Vec<(usize, usize, f64)> = critical
761 .iter()
762 .map(|&idx| {
763 let fn_ = norm(&all_obj[idx]);
764 let (r, d) = (0..n_ref)
765 .map(|r| (r, line_dist(&fn_, &ref_pts[r])))
766 .min_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap_or(std::cmp::Ordering::Equal))
767 .unwrap_or((0, f64::INFINITY));
768 (idx, r, d)
769 })
770 .collect();
771
772 let mut remaining = info;
773 let mut selected = Vec::with_capacity(n_select);
774
775 while selected.len() < n_select && !remaining.is_empty() {
776 let min_nc = remaining
777 .iter()
778 .map(|&(_, r, _)| niche_count[r])
779 .min()
780 .unwrap_or(0);
781 let cands: Vec<usize> = remaining
782 .iter()
783 .enumerate()
784 .filter(|(_, &(_, r, _))| niche_count[r] == min_nc)
785 .map(|(i, _)| i)
786 .collect();
787
788 let chosen = if min_nc == 0 {
789 cands[rng.random_range(0..cands.len())]
790 } else {
791 *cands
792 .iter()
793 .min_by(|&&a, &&b| {
794 remaining[a]
795 .2
796 .partial_cmp(&remaining[b].2)
797 .unwrap_or(std::cmp::Ordering::Equal)
798 })
799 .unwrap_or(&cands[0])
800 };
801
802 let (sol, ref_idx, _) = remaining.remove(chosen);
803 selected.push(sol);
804 niche_count[ref_idx] += 1;
805 }
806
807 selected
808}
809
810fn sbx_crossover(
811 p1: &[f64],
812 p2: &[f64],
813 bounds: &[(f64, f64)],
814 rng: &mut StdRng,
815 eta: f64,
816) -> (Vec<f64>, Vec<f64>) {
817 let mut c1 = p1.to_vec();
818 let mut c2 = p2.to_vec();
819 for i in 0..p1.len() {
820 let (lb, ub) = bounds[i];
821 if rng.random::<f64>() > 0.5 {
822 continue;
823 }
824 let u: f64 = rng.random();
825 let beta: f64 = if u <= 0.5 {
826 (2.0_f64 * u).powf(1.0_f64 / (eta + 1.0))
827 } else {
828 (1.0_f64 / (2.0_f64 * (1.0 - u))).powf(1.0_f64 / (eta + 1.0))
829 };
830 c1[i] = (0.5 * ((1.0 + beta) * p1[i] + (1.0 - beta) * p2[i])).clamp(lb, ub);
831 c2[i] = (0.5 * ((1.0 - beta) * p1[i] + (1.0 + beta) * p2[i])).clamp(lb, ub);
832 }
833 (c1, c2)
834}
835
836fn poly_mutation(
837 mut x: Vec<f64>,
838 bounds: &[(f64, f64)],
839 rng: &mut StdRng,
840 prob: f64,
841 eta: f64,
842) -> Vec<f64> {
843 for i in 0..x.len() {
844 if rng.random::<f64>() >= prob {
845 continue;
846 }
847 let (lb, ub) = bounds[i];
848 let range = (ub - lb).max(1e-12);
849 let d1 = (x[i] - lb) / range;
850 let d2 = (ub - x[i]) / range;
851 let u: f64 = rng.random();
852 let dq = if u <= 0.5 {
853 let v = 2.0 * u + (1.0 - 2.0 * u) * (1.0 - d1).powf(eta + 1.0);
854 v.powf(1.0 / (eta + 1.0)) - 1.0
855 } else {
856 let v = 2.0 * (1.0 - u) + 2.0 * (u - 0.5) * (1.0 - d2).powf(eta + 1.0);
857 1.0 - v.powf(1.0 / (eta + 1.0))
858 };
859 x[i] = (x[i] + dq * range).clamp(lb, ub);
860 }
861 x
862}
863
864#[cfg(test)]
869mod tests {
870 use super::*;
871 use approx::assert_abs_diff_eq;
872
873 #[test]
874 fn test_dominates_basic() {
875 assert!(dominates(&[1.0, 2.0], &[2.0, 3.0]));
876 assert!(!dominates(&[1.0, 2.0], &[1.0, 2.0]));
877 assert!(!dominates(&[2.0, 1.0], &[1.0, 2.0]));
878 }
879
880 #[test]
881 fn test_non_dominated_sort_basic() {
882 let sols = vec![
883 vec![1.0, 3.0],
884 vec![2.0, 2.0],
885 vec![3.0, 1.0],
886 vec![4.0, 4.0],
887 ];
888 let fronts = non_dominated_sort(&sols);
889 assert_eq!(fronts[0].len(), 3);
890 assert!(fronts[1].contains(&3));
891 }
892
893 #[test]
894 fn test_hypervolume_2d() {
895 let front = vec![vec![1.0, 2.0], vec![2.0, 1.0]];
896 let ref_pt = vec![3.0, 3.0];
897 let hv = hypervolume(&front, &ref_pt);
898 assert_abs_diff_eq!(hv, 3.0, epsilon = 1e-10);
899 }
900
901 #[test]
902 fn test_hypervolume_empty() {
903 assert_eq!(hypervolume(&[], &[3.0, 3.0]), 0.0);
904 }
905
906 #[test]
907 fn test_crowding_distance_boundary() {
908 let front = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
909 let d = crowding_distance(&front);
910 assert_eq!(d[0], f64::INFINITY);
911 assert_eq!(d[2], f64::INFINITY);
912 assert!(d[1].is_finite());
913 }
914
915 #[test]
916 fn test_nsga3_basic() {
917 let mut nsga = NsgaIII::new(2);
918 nsga.population_size = 10;
919 nsga.n_generations = 3;
920 nsga.n_divisions = 2;
921
922 let bounds: Vec<(f64, f64)> = vec![(0.0, 1.0); 2];
923 let result = nsga
924 .optimize(|x| vec![x[0], 1.0 - x[0].sqrt()], &bounds)
925 .expect("failed to create result");
926 assert!(!result.pareto_front.is_empty());
927 assert!(result.n_evaluations > 0);
928 }
929
930 #[test]
931 fn test_nsga3_empty_bounds_err() {
932 let nsga = NsgaIII::new(2);
933 assert!(nsga.optimize(|x| vec![x[0]], &[]).is_err());
934 }
935
936 #[test]
937 fn test_weighted_sum_single() {
938 let (x, f) = weighted_sum_optimize(|x| vec![x[0].powi(2)], &[1.0], &[(0.0, 2.0)], 5, 0);
939 assert!(x[0] < 0.2, "x near 0, got {}", x[0]);
940 assert!(f[0] < 0.05, "f near 0, got {}", f[0]);
941 }
942
943 #[test]
944 fn test_epsilon_constraint_basic() {
945 let (x, f) = epsilon_constraint_optimize(
947 |x| vec![x[0], x[1]],
948 0,
949 &[(0.3, 0.7)],
950 &[(0.0, 1.0), (0.0, 1.0)],
951 5,
952 42,
953 );
954 assert!(x[0] < 0.2, "x[0] near 0, got {}", x[0]);
955 assert!(f[1] >= 0.2 && f[1] <= 0.8, "f[1] in bounds, got {}", f[1]);
956 }
957
958 #[test]
959 fn test_hypervolume_3d_positive() {
960 let front = vec![
961 vec![0.1, 0.5, 0.9],
962 vec![0.5, 0.1, 0.9],
963 vec![0.9, 0.9, 0.1],
964 ];
965 let ref_pt = vec![1.0, 1.0, 1.0];
966 let hv = hypervolume(&front, &ref_pt);
967 assert!(hv > 0.0, "3D HV should be positive, got {}", hv);
968 }
969}