1#![allow(non_snake_case)] use scirs2_core::ndarray::{array, s, Array1, Array2, ArrayView2};
19use scirs2_core::random::RandNormal;
20use scirs2_core::random::{Rng, RngExt};
21use sklears_core::{
22 error::{Result as SklResult, SklearsError},
23 traits::{Estimator, Fit, Predict, Untrained},
24 types::Float,
25};
26
27use super::multi_objective_optimization::ParetoSolution;
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum NSGA2Algorithm {
32 Standard,
34 SBX,
36 DE,
38}
39
40#[derive(Debug, Clone)]
42pub struct NSGA2Config {
43 pub population_size: usize,
45 pub generations: usize,
47 pub crossover_prob: Float,
49 pub mutation_prob: Float,
51 pub eta_c: Float,
53 pub eta_m: Float,
55 pub algorithm: NSGA2Algorithm,
57 pub random_state: Option<u64>,
59}
60
61impl Default for NSGA2Config {
62 fn default() -> Self {
63 Self {
64 population_size: 100,
65 generations: 250,
66 crossover_prob: 0.9,
67 mutation_prob: 0.1,
68 eta_c: 20.0,
69 eta_m: 20.0,
70 algorithm: NSGA2Algorithm::Standard,
71 random_state: None,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct NSGA2Optimizer<S = Untrained> {
79 state: S,
80 config: NSGA2Config,
81}
82
83#[derive(Debug, Clone)]
85pub struct NSGA2OptimizerTrained {
86 pub pareto_solutions: Vec<ParetoSolution>,
88 pub best_solution: ParetoSolution,
90 pub convergence_history: Vec<Float>,
92 pub final_population: Vec<ParetoSolution>,
94 pub config: NSGA2Config,
96 pub n_objectives: usize,
98}
99
100impl Default for NSGA2Optimizer<Untrained> {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl NSGA2Optimizer<Untrained> {
107 pub fn new() -> Self {
109 Self {
110 state: Untrained,
111 config: NSGA2Config::default(),
112 }
113 }
114
115 pub fn config(mut self, config: NSGA2Config) -> Self {
117 self.config = config;
118 self
119 }
120
121 pub fn population_size(mut self, population_size: usize) -> Self {
123 self.config.population_size = population_size;
124 self
125 }
126
127 pub fn generations(mut self, generations: usize) -> Self {
129 self.config.generations = generations;
130 self
131 }
132
133 pub fn crossover_prob(mut self, crossover_prob: Float) -> Self {
135 self.config.crossover_prob = crossover_prob;
136 self
137 }
138
139 pub fn mutation_prob(mut self, mutation_prob: Float) -> Self {
141 self.config.mutation_prob = mutation_prob;
142 self
143 }
144
145 pub fn algorithm(mut self, algorithm: NSGA2Algorithm) -> Self {
147 self.config.algorithm = algorithm;
148 self
149 }
150}
151
152impl Estimator for NSGA2Optimizer<Untrained> {
153 type Config = NSGA2Config;
154 type Error = SklearsError;
155 type Float = Float;
156
157 fn config(&self) -> &Self::Config {
158 &self.config
159 }
160}
161
162impl Estimator for NSGA2Optimizer<NSGA2OptimizerTrained> {
163 type Config = NSGA2Config;
164 type Error = SklearsError;
165 type Float = Float;
166
167 fn config(&self) -> &Self::Config {
168 &self.state.config
169 }
170}
171
172impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for NSGA2Optimizer<Untrained> {
173 type Fitted = NSGA2Optimizer<NSGA2OptimizerTrained>;
174
175 fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView2<'_, Float>) -> SklResult<Self::Fitted> {
176 if X.nrows() != y.nrows() {
177 return Err(SklearsError::InvalidInput(
178 "X and y must have the same number of samples".to_string(),
179 ));
180 }
181
182 let _n_samples = X.nrows();
183 let n_features = X.ncols();
184 let n_outputs = y.ncols();
185 let n_objectives = 2; let mut rng = match self.config.random_state {
188 Some(seed) => scirs2_core::random::seeded_rng(seed),
189 None => scirs2_core::random::seeded_rng(42), };
191
192 let mut population = self.initialize_population(n_features, n_outputs, &mut rng)?;
194
195 self.evaluate_population(&mut population, X, y)?;
197
198 let mut convergence_history = Vec::new();
199
200 for generation in 0..self.config.generations {
202 self.nsga2_non_dominated_sort(&mut population)?;
204
205 self.nsga2_crowding_distance(&mut population)?;
207
208 let hypervolume = self.calculate_hypervolume(&population)?;
210 convergence_history.push(hypervolume);
211
212 let mut offspring = self.nsga2_generate_offspring(&population, &mut rng)?;
214
215 self.evaluate_population(&mut offspring, X, y)?;
217
218 population.extend(offspring);
220
221 population = self.nsga2_environmental_selection(population)?;
223
224 if generation % 50 == 0 {
225 println!(
226 "Generation {}: Hypervolume = {:.6}",
227 generation, hypervolume
228 );
229 }
230 }
231
232 self.nsga2_non_dominated_sort(&mut population)?;
234 let pareto_solutions = self.extract_pareto_front(&population)?;
235 let best_solution = self.find_best_compromise(&pareto_solutions)?;
236
237 Ok(NSGA2Optimizer {
238 state: NSGA2OptimizerTrained {
239 pareto_solutions: pareto_solutions.clone(),
240 best_solution,
241 convergence_history,
242 final_population: population,
243 config: self.config.clone(),
244 n_objectives,
245 },
246 config: self.config,
247 })
248 }
249}
250
251impl NSGA2Optimizer<Untrained> {
252 fn initialize_population<R: Rng>(
254 &self,
255 n_features: usize,
256 n_outputs: usize,
257 rng: &mut R,
258 ) -> SklResult<Vec<ParetoSolution>> {
259 let mut population = Vec::with_capacity(self.config.population_size);
260 let param_size = n_features * n_outputs + n_outputs; for _ in 0..self.config.population_size {
263 let parameters = Array1::from_shape_fn(param_size, |_| rng.random_range(-1.0..1.0));
264 let solution = ParetoSolution {
265 parameters,
266 objectives: Array1::zeros(2), rank: 0,
268 crowding_distance: 0.0,
269 };
270 population.push(solution);
271 }
272
273 Ok(population)
274 }
275
276 fn nsga2_non_dominated_sort(&self, population: &mut [ParetoSolution]) -> SklResult<()> {
278 let n = population.len();
279 let mut domination_counts = vec![0; n];
280 let mut dominated_solutions = vec![Vec::new(); n];
281 let mut fronts: Vec<Vec<usize>> = Vec::new();
282
283 for i in 0..n {
285 for j in 0..n {
286 if i != j {
287 if self.nsga2_dominates(&population[i], &population[j]) {
288 dominated_solutions[i].push(j);
289 } else if self.nsga2_dominates(&population[j], &population[i]) {
290 domination_counts[i] += 1;
291 }
292 }
293 }
294 }
295
296 let mut current_front: Vec<usize> = (0..n).filter(|&i| domination_counts[i] == 0).collect();
298 let mut rank = 0;
299
300 while !current_front.is_empty() {
301 for &i in ¤t_front {
303 population[i].rank = rank;
304 }
305
306 fronts.push(current_front.clone());
307
308 let mut next_front = Vec::new();
310 for &i in ¤t_front {
311 for &j in &dominated_solutions[i] {
312 domination_counts[j] -= 1;
313 if domination_counts[j] == 0 {
314 next_front.push(j);
315 }
316 }
317 }
318
319 current_front = next_front;
320 rank += 1;
321 }
322
323 Ok(())
324 }
325
326 pub fn nsga2_dominates(&self, a: &ParetoSolution, b: &ParetoSolution) -> bool {
328 let mut at_least_one_better = false;
329
330 for i in 0..a.objectives.len() {
331 if a.objectives[i] > b.objectives[i] {
332 return false; }
334 if a.objectives[i] < b.objectives[i] {
335 at_least_one_better = true;
336 }
337 }
338
339 at_least_one_better
340 }
341
342 fn nsga2_crowding_distance(&self, population: &mut [ParetoSolution]) -> SklResult<()> {
344 let n = population.len();
345 if n == 0 {
346 return Ok(());
347 }
348
349 for solution in population.iter_mut() {
351 solution.crowding_distance = 0.0;
352 }
353
354 let n_objectives = population[0].objectives.len();
355
356 for obj_idx in 0..n_objectives {
357 let mut indices: Vec<usize> = (0..n).collect();
359 indices.sort_by(|&a, &b| {
360 population[a].objectives[obj_idx]
361 .partial_cmp(&population[b].objectives[obj_idx])
362 .unwrap_or(std::cmp::Ordering::Equal)
363 });
364
365 population[indices[0]].crowding_distance = Float::INFINITY;
367 population[indices[n - 1]].crowding_distance = Float::INFINITY;
368
369 let obj_range = population[indices[n - 1]].objectives[obj_idx]
371 - population[indices[0]].objectives[obj_idx];
372
373 if obj_range > 0.0 {
374 for i in 1..(n - 1) {
375 let distance = (population[indices[i + 1]].objectives[obj_idx]
376 - population[indices[i - 1]].objectives[obj_idx])
377 / obj_range;
378 population[indices[i]].crowding_distance += distance;
379 }
380 }
381 }
382
383 Ok(())
384 }
385
386 fn nsga2_generate_offspring<R: Rng>(
388 &self,
389 population: &[ParetoSolution],
390 rng: &mut R,
391 ) -> SklResult<Vec<ParetoSolution>> {
392 let mut offspring = Vec::new();
393
394 for _ in 0..self.config.population_size {
395 let parent1 = self.nsga2_tournament_selection(population, rng)?;
397 let parent2 = self.nsga2_tournament_selection(population, rng)?;
398
399 let mut child = match self.config.algorithm {
401 NSGA2Algorithm::SBX => self.simulated_binary_crossover(&parent1, &parent2, rng)?,
402 _ => self.uniform_crossover(&parent1, &parent2, rng)?,
403 };
404
405 match self.config.algorithm {
407 NSGA2Algorithm::SBX => self.polynomial_mutation(&mut child, rng)?,
408 _ => self.gaussian_mutation(&mut child, rng)?,
409 }
410
411 offspring.push(child);
412 }
413
414 Ok(offspring)
415 }
416
417 fn nsga2_tournament_selection<R: Rng>(
419 &self,
420 population: &[ParetoSolution],
421 rng: &mut R,
422 ) -> SklResult<ParetoSolution> {
423 let idx1 = rng.random_range(0..population.len());
424 let idx2 = rng.random_range(0..population.len());
425
426 let solution1 = &population[idx1];
427 let solution2 = &population[idx2];
428
429 if solution1.rank < solution2.rank {
431 Ok(solution1.clone())
432 } else if solution1.rank > solution2.rank {
433 Ok(solution2.clone())
434 } else {
435 if solution1.crowding_distance > solution2.crowding_distance {
437 Ok(solution1.clone())
438 } else {
439 Ok(solution2.clone())
440 }
441 }
442 }
443
444 fn simulated_binary_crossover<R: Rng>(
446 &self,
447 parent1: &ParetoSolution,
448 parent2: &ParetoSolution,
449 rng: &mut R,
450 ) -> SklResult<ParetoSolution> {
451 let mut child_params = parent1.parameters.clone();
452
453 if rng.random::<Float>() <= self.config.crossover_prob {
454 for i in 0..child_params.len() {
455 let p1 = parent1.parameters[i];
456 let p2 = parent2.parameters[i];
457
458 if rng.random::<Float>() <= 0.5 {
459 let u = rng.random::<Float>();
460 let beta = if u <= 0.5 {
461 (2.0 * u).powf(1.0 / (self.config.eta_c + 1.0))
462 } else {
463 (1.0 / (2.0 * (1.0 - u))).powf(1.0 / (self.config.eta_c + 1.0))
464 };
465
466 let child_val = 0.5 * ((1.0 + beta) * p1 + (1.0 - beta) * p2);
467 child_params[i] = child_val.clamp(-2.0, 2.0);
468 }
469 }
470 }
471
472 Ok(ParetoSolution {
473 parameters: child_params,
474 objectives: Array1::zeros(parent1.objectives.len()),
475 rank: 0,
476 crowding_distance: 0.0,
477 })
478 }
479
480 fn polynomial_mutation<R: Rng>(
482 &self,
483 solution: &mut ParetoSolution,
484 rng: &mut R,
485 ) -> SklResult<()> {
486 for i in 0..solution.parameters.len() {
487 if rng.random::<Float>() <= self.config.mutation_prob {
488 let u = rng.random::<Float>();
489 let delta = if u < 0.5 {
490 (2.0 * u).powf(1.0 / (self.config.eta_m + 1.0)) - 1.0
491 } else {
492 1.0 - (2.0 * (1.0 - u)).powf(1.0 / (self.config.eta_m + 1.0))
493 };
494
495 solution.parameters[i] += delta * 0.1;
496 solution.parameters[i] = solution.parameters[i].clamp(-2.0, 2.0);
497 }
498 }
499 Ok(())
500 }
501
502 fn nsga2_environmental_selection(
504 &self,
505 mut population: Vec<ParetoSolution>,
506 ) -> SklResult<Vec<ParetoSolution>> {
507 self.nsga2_non_dominated_sort(&mut population)?;
509 self.nsga2_crowding_distance(&mut population)?;
510
511 population.sort_by(|a, b| {
513 match a.rank.cmp(&b.rank) {
514 std::cmp::Ordering::Equal => {
515 b.crowding_distance
517 .partial_cmp(&a.crowding_distance)
518 .unwrap_or(std::cmp::Ordering::Equal)
519 }
520 other => other,
521 }
522 });
523
524 population.truncate(self.config.population_size);
526 Ok(population)
527 }
528
529 fn extract_pareto_front(
531 &self,
532 population: &[ParetoSolution],
533 ) -> SklResult<Vec<ParetoSolution>> {
534 Ok(population
535 .iter()
536 .filter(|sol| sol.rank == 0)
537 .cloned()
538 .collect())
539 }
540
541 fn evaluate_population(
543 &self,
544 population: &mut [ParetoSolution],
545 X: &ArrayView2<Float>,
546 y: &ArrayView2<Float>,
547 ) -> SklResult<()> {
548 let n_features = X.ncols();
549 let n_outputs = y.ncols();
550
551 for solution in population.iter_mut() {
552 let weights = solution
554 .parameters
555 .slice(s![..n_features * n_outputs])
556 .to_owned()
557 .into_shape_with_order((
558 (n_features, n_outputs),
559 scirs2_core::ndarray::Order::RowMajor,
560 ))
561 .map_err(|e| SklearsError::InvalidInput(format!("Shape error: {}", e)))?;
562
563 let bias = solution
564 .parameters
565 .slice(s![n_features * n_outputs..])
566 .to_owned();
567
568 let mut predictions = X.dot(&weights);
570 for mut row in predictions.rows_mut() {
571 row += &bias;
572 }
573
574 let mse = self.calculate_mse(&predictions.view(), y)?;
576 let complexity = self.calculate_complexity(&weights)?;
577
578 solution.objectives = array![mse, complexity];
579 }
580
581 Ok(())
582 }
583
584 fn calculate_mse(
586 &self,
587 predictions: &ArrayView2<Float>,
588 y: &ArrayView2<Float>,
589 ) -> SklResult<Float> {
590 let diff = predictions - y;
591 let squared_diff = &diff * &diff;
592 Ok(squared_diff.sum() / (predictions.nrows() * predictions.ncols()) as Float)
593 }
594
595 fn calculate_complexity(&self, weights: &Array2<Float>) -> SklResult<Float> {
597 Ok(weights.mapv(|x| x.abs()).sum())
598 }
599
600 fn uniform_crossover<R: Rng>(
602 &self,
603 parent1: &ParetoSolution,
604 parent2: &ParetoSolution,
605 rng: &mut R,
606 ) -> SklResult<ParetoSolution> {
607 let mut child_params = parent1.parameters.clone();
608
609 if rng.random::<Float>() <= self.config.crossover_prob {
610 for i in 0..child_params.len() {
611 if rng.random::<Float>() <= 0.5 {
612 child_params[i] = parent2.parameters[i];
613 }
614 }
615 }
616
617 Ok(ParetoSolution {
618 parameters: child_params,
619 objectives: Array1::zeros(parent1.objectives.len()),
620 rank: 0,
621 crowding_distance: 0.0,
622 })
623 }
624
625 fn gaussian_mutation<R: Rng>(
627 &self,
628 solution: &mut ParetoSolution,
629 rng: &mut R,
630 ) -> SklResult<()> {
631 for i in 0..solution.parameters.len() {
632 if rng.random::<Float>() <= self.config.mutation_prob {
633 let normal = RandNormal::new(0.0, 0.1).map_err(|e| {
634 SklearsError::InvalidInput(format!(
635 "Failed to create normal distribution: {}",
636 e
637 ))
638 })?;
639 let mutation = rng.sample(normal);
640 solution.parameters[i] += mutation;
641 solution.parameters[i] = solution.parameters[i].clamp(-2.0, 2.0);
642 }
643 }
644 Ok(())
645 }
646
647 fn calculate_hypervolume(&self, population: &[ParetoSolution]) -> SklResult<Float> {
649 let pareto_front: Vec<&ParetoSolution> =
651 population.iter().filter(|sol| sol.rank == 0).collect();
652
653 if pareto_front.is_empty() {
654 return Ok(0.0);
655 }
656
657 let reference_point = array![1.0, 1.0];
659 let mut hypervolume = 0.0;
660
661 for solution in &pareto_front {
662 let mut volume = 1.0;
663 for i in 0..solution.objectives.len() {
664 let contribution = (reference_point[i] - solution.objectives[i]).max(0.0);
665 volume *= contribution;
666 }
667 hypervolume += volume;
668 }
669
670 Ok(hypervolume / pareto_front.len() as Float)
671 }
672
673 fn find_best_compromise(
675 &self,
676 pareto_solutions: &[ParetoSolution],
677 ) -> SklResult<ParetoSolution> {
678 if pareto_solutions.is_empty() {
679 return Err(SklearsError::InvalidInput(
680 "No Pareto solutions available".to_string(),
681 ));
682 }
683
684 let mut best_solution = pareto_solutions[0].clone();
685 let mut best_distance = Float::INFINITY;
686
687 for solution in pareto_solutions {
689 let distance = solution.objectives.mapv(|x| x * x).sum().sqrt();
690 if distance < best_distance {
691 best_distance = distance;
692 best_solution = solution.clone();
693 }
694 }
695
696 Ok(best_solution)
697 }
698}
699
700impl Predict<ArrayView2<'_, Float>, Array2<Float>> for NSGA2Optimizer<NSGA2OptimizerTrained> {
701 #[allow(non_snake_case)] fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
703 let _n_samples = X.nrows();
704 let n_features = X.ncols();
705 let n_outputs = self.state.best_solution.parameters.len() / (n_features + 1);
706
707 let weights = self
709 .state
710 .best_solution
711 .parameters
712 .slice(s![..n_features * n_outputs])
713 .to_owned()
714 .into_shape_with_order((
715 (n_features, n_outputs),
716 scirs2_core::ndarray::Order::RowMajor,
717 ))
718 .map_err(|e| SklearsError::InvalidInput(format!("Shape error: {}", e)))?;
719
720 let bias = self
721 .state
722 .best_solution
723 .parameters
724 .slice(s![n_features * n_outputs..])
725 .to_owned();
726
727 let mut predictions = X.dot(&weights);
729 for mut row in predictions.rows_mut() {
730 row += &bias;
731 }
732
733 Ok(predictions)
734 }
735}
736
737impl NSGA2Optimizer<NSGA2OptimizerTrained> {
738 pub fn pareto_solutions(&self) -> &[ParetoSolution] {
740 &self.state.pareto_solutions
741 }
742
743 pub fn best_solution(&self) -> &ParetoSolution {
745 &self.state.best_solution
746 }
747
748 pub fn convergence_history(&self) -> &[Float] {
750 &self.state.convergence_history
751 }
752
753 pub fn final_population(&self) -> &[ParetoSolution] {
755 &self.state.final_population
756 }
757}