1use crate::error::OptimError;
8use crate::types::{OptimResult, OptimStatus, ParetoPoint, ParetoResult};
9use numra_core::Scalar;
10use rand::rngs::SmallRng;
11use rand::{Rng, SeedableRng};
12
13type ObjFnSlice<'a, S> = &'a [&'a dyn Fn(&[S]) -> S];
15
16#[derive(Clone, Debug)]
18pub struct NsgaIIOptions<S: Scalar> {
19 pub pop_size: usize,
21 pub max_generations: usize,
23 pub crossover_eta: S,
25 pub mutation_eta: S,
27 pub crossover_prob: S,
29 pub mutation_prob: Option<S>,
31 pub seed: u64,
33 pub verbose: bool,
35}
36
37impl<S: Scalar> Default for NsgaIIOptions<S> {
38 fn default() -> Self {
39 Self {
40 pop_size: 100,
41 max_generations: 200,
42 crossover_eta: S::from_f64(20.0),
43 mutation_eta: S::from_f64(20.0),
44 crossover_prob: S::from_f64(0.9),
45 mutation_prob: None,
46 seed: 42,
47 verbose: false,
48 }
49 }
50}
51
52fn non_dominated_sort<S: Scalar>(objectives: &[Vec<S>]) -> Vec<Vec<usize>> {
57 let n = objectives.len();
58 if n == 0 {
59 return vec![];
60 }
61
62 let mut s_p: Vec<Vec<usize>> = vec![vec![]; n];
66 let mut n_p: Vec<usize> = vec![0; n];
67
68 for i in 0..n {
69 for j in (i + 1)..n {
70 let dom_ij = dominates(&objectives[i], &objectives[j]);
71 let dom_ji = dominates(&objectives[j], &objectives[i]);
72 if dom_ij {
73 s_p[i].push(j);
74 n_p[j] += 1;
75 } else if dom_ji {
76 s_p[j].push(i);
77 n_p[i] += 1;
78 }
79 }
80 }
81
82 let mut fronts: Vec<Vec<usize>> = vec![];
83
84 let mut current_front: Vec<usize> = (0..n).filter(|&i| n_p[i] == 0).collect();
86
87 while !current_front.is_empty() {
88 let mut next_front = vec![];
89 for &p in ¤t_front {
90 for &q in &s_p[p] {
91 n_p[q] -= 1;
92 if n_p[q] == 0 {
93 next_front.push(q);
94 }
95 }
96 }
97 fronts.push(current_front);
98 current_front = next_front;
99 }
100
101 fronts
102}
103
104fn dominates<S: Scalar>(a: &[S], b: &[S]) -> bool {
106 let mut any_strictly_less = false;
107 for (ai, bi) in a.iter().zip(b.iter()) {
108 if *ai > *bi {
109 return false;
110 }
111 if *ai < *bi {
112 any_strictly_less = true;
113 }
114 }
115 any_strictly_less
116}
117
118fn crowding_distance<S: Scalar>(front: &[usize], objectives: &[Vec<S>]) -> Vec<S> {
123 let n = front.len();
124 if n <= 2 {
125 return vec![S::INFINITY; n];
126 }
127
128 let n_obj = objectives[front[0]].len();
129 let mut distances = vec![S::ZERO; n];
130
131 #[allow(clippy::needless_range_loop)]
132 for m in 0..n_obj {
133 let mut sorted_indices: Vec<usize> = (0..n).collect();
135 sorted_indices.sort_by(|&a, &b| {
136 let fa = objectives[front[a]][m];
137 let fb = objectives[front[b]][m];
138 fa.partial_cmp(&fb).unwrap_or(std::cmp::Ordering::Equal)
139 });
140
141 let obj_min = objectives[front[sorted_indices[0]]][m];
142 let obj_max = objectives[front[sorted_indices[n - 1]]][m];
143 let range = obj_max - obj_min;
144
145 distances[sorted_indices[0]] = S::INFINITY;
147 distances[sorted_indices[n - 1]] = S::INFINITY;
148
149 if range < S::from_f64(1e-30) {
150 continue;
152 }
153
154 for i in 1..(n - 1) {
155 let prev_obj = objectives[front[sorted_indices[i - 1]]][m];
156 let next_obj = objectives[front[sorted_indices[i + 1]]][m];
157 distances[sorted_indices[i]] += (next_obj - prev_obj) / range;
158 }
159 }
160
161 distances
162}
163
164fn tournament_select<S: Scalar>(
167 rng: &mut SmallRng,
168 ranks: &[usize],
169 crowding: &[S],
170 pop_size: usize,
171) -> usize {
172 let a = rng.gen_range(0..pop_size);
173 let b = rng.gen_range(0..pop_size);
174 if ranks[a] < ranks[b] {
175 a
176 } else if ranks[b] < ranks[a] {
177 b
178 } else if crowding[a] > crowding[b] {
179 a
180 } else {
181 b
182 }
183}
184
185fn sbx_crossover<S: Scalar>(
188 p1: &[S],
189 p2: &[S],
190 bounds: &[(S, S)],
191 eta: S,
192 prob: S,
193 rng: &mut SmallRng,
194) -> (Vec<S>, Vec<S>) {
195 let n = p1.len();
196 let mut c1 = p1.to_vec();
197 let mut c2 = p2.to_vec();
198
199 if S::from_f64(rng.gen::<f64>()) > prob {
200 return (c1, c2);
201 }
202
203 let half = S::HALF;
204 let one = S::ONE;
205
206 for j in 0..n {
207 if (p1[j] - p2[j]).abs() < S::from_f64(1e-14) {
208 continue;
209 }
210
211 let u = S::from_f64(rng.gen::<f64>());
212 let exp = one / (eta + one);
213 let beta = if u <= half {
214 (S::TWO * u).powf(exp)
215 } else {
216 (one / (S::TWO * (one - u))).powf(exp)
217 };
218
219 c1[j] = half * ((one + beta) * p1[j] + (one - beta) * p2[j]);
220 c2[j] = half * ((one - beta) * p1[j] + (one + beta) * p2[j]);
221
222 c1[j] = c1[j].clamp(bounds[j].0, bounds[j].1);
224 c2[j] = c2[j].clamp(bounds[j].0, bounds[j].1);
225 }
226
227 (c1, c2)
228}
229
230fn polynomial_mutation<S: Scalar>(
233 x: &mut [S],
234 bounds: &[(S, S)],
235 eta: S,
236 prob: S,
237 rng: &mut SmallRng,
238) {
239 let half = S::HALF;
240 let one = S::ONE;
241 let two = S::TWO;
242
243 for j in 0..x.len() {
244 if S::from_f64(rng.gen::<f64>()) >= prob {
245 continue;
246 }
247
248 let u = S::from_f64(rng.gen::<f64>());
249 let exp = one / (eta + one);
250 let delta = if u < half {
251 (two * u).powf(exp) - one
252 } else {
253 one - (two * (one - u)).powf(exp)
254 };
255
256 let range = bounds[j].1 - bounds[j].0;
257 x[j] += delta * range;
258 x[j] = x[j].clamp(bounds[j].0, bounds[j].1);
259 }
260}
261
262fn evaluate<S: Scalar>(objectives: ObjFnSlice<'_, S>, x: &[S]) -> Vec<S> {
264 objectives.iter().map(|f| f(x)).collect()
265}
266
267pub fn nsga2_optimize<S: Scalar>(
284 objectives: ObjFnSlice<'_, S>,
285 bounds: &[(S, S)],
286 opts: &NsgaIIOptions<S>,
287) -> Result<OptimResult<S>, OptimError> {
288 let start = std::time::Instant::now();
289 let n = bounds.len();
290 let pop_size = opts.pop_size;
291 let mutation_prob = opts
292 .mutation_prob
293 .unwrap_or_else(|| S::ONE / S::from_usize(n));
294 let mut rng = SmallRng::seed_from_u64(opts.seed);
295
296 let mut population: Vec<Vec<S>> = (0..pop_size)
298 .map(|_| {
299 (0..n)
300 .map(|j| {
301 let (lo, hi) = bounds[j];
302 lo + S::from_f64(rng.gen::<f64>()) * (hi - lo)
303 })
304 .collect()
305 })
306 .collect();
307
308 let mut obj_values: Vec<Vec<S>> = population.iter().map(|x| evaluate(objectives, x)).collect();
310
311 for gen in 0..opts.max_generations {
313 let fronts = non_dominated_sort(&obj_values);
315 let mut ranks = vec![0_usize; pop_size];
316 let mut crowding = vec![S::ZERO; pop_size];
317 for (rank, front) in fronts.iter().enumerate() {
318 let cd = crowding_distance(front, &obj_values);
319 for (i, &idx) in front.iter().enumerate() {
320 ranks[idx] = rank;
321 crowding[idx] = cd[i];
322 }
323 }
324
325 if opts.verbose && gen % 10 == 0 {
326 let n_front0 = fronts.first().map_or(0, |f| f.len());
327 eprintln!(
328 "NSGA-II gen {}: front 0 size = {}, pop = {}",
329 gen, n_front0, pop_size
330 );
331 }
332
333 let mut offspring: Vec<Vec<S>> = Vec::with_capacity(pop_size);
335 while offspring.len() < pop_size {
336 let p1_idx = tournament_select(&mut rng, &ranks, &crowding, pop_size);
337 let p2_idx = tournament_select(&mut rng, &ranks, &crowding, pop_size);
338
339 let (mut c1, mut c2) = sbx_crossover(
340 &population[p1_idx],
341 &population[p2_idx],
342 bounds,
343 opts.crossover_eta,
344 opts.crossover_prob,
345 &mut rng,
346 );
347
348 polynomial_mutation(&mut c1, bounds, opts.mutation_eta, mutation_prob, &mut rng);
349 polynomial_mutation(&mut c2, bounds, opts.mutation_eta, mutation_prob, &mut rng);
350
351 offspring.push(c1);
352 if offspring.len() < pop_size {
353 offspring.push(c2);
354 }
355 }
356
357 let offspring_obj: Vec<Vec<S>> =
359 offspring.iter().map(|x| evaluate(objectives, x)).collect();
360
361 let mut combined_pop: Vec<Vec<S>> = population;
363 combined_pop.extend(offspring);
364 let mut combined_obj: Vec<Vec<S>> = obj_values;
365 combined_obj.extend(offspring_obj);
366
367 let combined_fronts = non_dominated_sort(&combined_obj);
369
370 let mut new_population: Vec<Vec<S>> = Vec::with_capacity(pop_size);
372 let mut new_obj: Vec<Vec<S>> = Vec::with_capacity(pop_size);
373
374 for front in &combined_fronts {
375 if new_population.len() + front.len() <= pop_size {
376 for &idx in front {
378 new_population.push(combined_pop[idx].clone());
379 new_obj.push(combined_obj[idx].clone());
380 }
381 } else {
382 let remaining = pop_size - new_population.len();
384 let cd = crowding_distance(front, &combined_obj);
385 let mut sorted: Vec<usize> = (0..front.len()).collect();
386 sorted.sort_by(|&a, &b| {
387 cd[b]
388 .partial_cmp(&cd[a])
389 .unwrap_or(std::cmp::Ordering::Equal)
390 });
391 for &i in sorted.iter().take(remaining) {
392 let idx = front[i];
393 new_population.push(combined_pop[idx].clone());
394 new_obj.push(combined_obj[idx].clone());
395 }
396 break;
397 }
398 }
399
400 population = new_population;
401 obj_values = new_obj;
402 }
403
404 let final_fronts = non_dominated_sort(&obj_values);
406 let front0 = &final_fronts[0];
407
408 let pareto_points: Vec<ParetoPoint<S>> = front0
409 .iter()
410 .map(|&idx| ParetoPoint {
411 x: population[idx].clone(),
412 objectives: obj_values[idx].clone(),
413 })
414 .collect();
415
416 let best_idx = front0
418 .iter()
419 .min_by(|&&a, &&b| {
420 obj_values[a][0]
421 .partial_cmp(&obj_values[b][0])
422 .unwrap_or(std::cmp::Ordering::Equal)
423 })
424 .copied()
425 .unwrap_or(0);
426
427 let result = OptimResult {
428 x: population[best_idx].clone(),
429 f: obj_values[best_idx][0],
430 grad: vec![],
431 iterations: opts.max_generations,
432 n_feval: 0,
433 n_geval: 0,
434 converged: true,
435 message: format!(
436 "NSGA-II completed {} generations, Pareto front size = {}",
437 opts.max_generations,
438 pareto_points.len()
439 ),
440 status: OptimStatus::MaxIterations,
441 history: vec![],
442 lambda_eq: vec![],
443 lambda_ineq: vec![],
444 active_bounds: vec![],
445 constraint_violation: S::ZERO,
446 wall_time_secs: 0.0,
447 pareto: Some(ParetoResult {
448 points: pareto_points,
449 }),
450 sensitivity: None,
451 }
452 .with_wall_time(start);
453
454 Ok(result)
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 type ObjRef<'a> = Vec<&'a dyn Fn(&[f64]) -> f64>;
462
463 #[test]
464 fn test_nsga2_zdt1() {
465 let n = 3;
467 let bounds = vec![(0.0, 1.0); n];
468 let f1 = |x: &[f64]| x[0];
469 let f2 = |x: &[f64]| {
470 let g = 1.0 + 9.0 * x[1..].iter().copied().sum::<f64>() / (x.len() - 1) as f64;
471 g * (1.0 - (x[0] / g).sqrt())
472 };
473 let objectives: ObjRef<'_> = vec![&f1, &f2];
474 let opts = NsgaIIOptions {
475 pop_size: 50,
476 max_generations: 100,
477 ..Default::default()
478 };
479 let result = nsga2_optimize(&objectives, &bounds, &opts).unwrap();
480 let pareto = result.pareto.as_ref().unwrap();
481 assert!(
482 pareto.points.len() >= 5,
483 "too few points: {}",
484 pareto.points.len()
485 );
486 for p in &pareto.points {
487 assert_eq!(p.objectives.len(), 2);
488 }
489 }
490
491 #[test]
492 fn test_nsga2_simple_biobj() {
493 let bounds = vec![(0.0, 4.0)];
495 let f1 = |x: &[f64]| x[0] * x[0];
496 let f2 = |x: &[f64]| (x[0] - 2.0).powi(2);
497 let objectives: ObjRef<'_> = vec![&f1, &f2];
498 let opts = NsgaIIOptions {
499 pop_size: 50,
500 max_generations: 100,
501 ..Default::default()
502 };
503 let result = nsga2_optimize(&objectives, &bounds, &opts).unwrap();
504 let pareto = result.pareto.as_ref().unwrap();
505 assert!(pareto.points.len() >= 3);
506 for p in &pareto.points {
507 assert!(p.x[0] >= -0.5 && p.x[0] <= 2.5, "x={}", p.x[0]);
508 }
509 }
510
511 #[test]
512 fn test_nsga2_three_objectives() {
513 let bounds = vec![(0.0, 2.0), (0.0, 2.0)];
514 let f1 = |x: &[f64]| x[0] * x[0];
515 let f2 = |x: &[f64]| x[1] * x[1];
516 let f3 = |x: &[f64]| (x[0] - 1.0).powi(2) + (x[1] - 1.0).powi(2);
517 let objectives: ObjRef<'_> = vec![&f1, &f2, &f3];
518 let opts = NsgaIIOptions {
519 pop_size: 50,
520 max_generations: 100,
521 ..Default::default()
522 };
523 let result = nsga2_optimize(&objectives, &bounds, &opts).unwrap();
524 let pareto = result.pareto.as_ref().unwrap();
525 assert!(!pareto.points.is_empty());
526 for p in &pareto.points {
527 assert_eq!(p.objectives.len(), 3);
528 }
529 }
530
531 #[test]
532 fn test_nsga2_deterministic() {
533 let bounds = vec![(0.0, 1.0); 2];
534 let f1 = |x: &[f64]| x[0];
535 let f2 = |x: &[f64]| 1.0 - x[0] + x[1];
536 let objectives: ObjRef<'_> = vec![&f1, &f2];
537 let opts = NsgaIIOptions {
538 pop_size: 20,
539 max_generations: 50,
540 ..Default::default()
541 };
542 let r1 = nsga2_optimize(&objectives, &bounds, &opts).unwrap();
543 let r2 = nsga2_optimize(&objectives, &bounds, &opts).unwrap();
544 assert_eq!(
545 r1.pareto.unwrap().points.len(),
546 r2.pareto.unwrap().points.len()
547 );
548 }
549
550 #[test]
551 fn test_nsga2_returns_nondominated() {
552 let bounds = vec![(0.0, 5.0); 2];
553 let f1 = |x: &[f64]| x[0] * x[0] + x[1];
554 let f2 = |x: &[f64]| x[1] * x[1] + x[0];
555 let objectives: ObjRef<'_> = vec![&f1, &f2];
556 let opts = NsgaIIOptions {
557 pop_size: 30,
558 max_generations: 80,
559 ..Default::default()
560 };
561 let result = nsga2_optimize(&objectives, &bounds, &opts).unwrap();
562 let pareto = result.pareto.as_ref().unwrap();
563 for (i, pi) in pareto.points.iter().enumerate() {
564 for (j, pj) in pareto.points.iter().enumerate() {
565 if i == j {
566 continue;
567 }
568 let all_leq = pi
569 .objectives
570 .iter()
571 .zip(&pj.objectives)
572 .all(|(a, b)| a <= b);
573 let any_lt = pi.objectives.iter().zip(&pj.objectives).any(|(a, b)| a < b);
574 assert!(
575 !(all_leq && any_lt),
576 "point {} dominates point {}: {:?} vs {:?}",
577 i,
578 j,
579 pi.objectives,
580 pj.objectives
581 );
582 }
583 }
584 }
585}