1#![allow(non_snake_case)] use scirs2_core::ndarray::{s, Array1, Array2, ArrayView2};
19use scirs2_core::random::thread_rng;
20use scirs2_core::random::RandNormal;
21use sklears_core::{
22 error::{Result as SklResult, SklearsError},
23 traits::{Estimator, Fit, Predict, Untrained},
24 types::Float,
25};
26
27#[derive(Debug, Clone)]
29pub struct MultiObjectiveOptimizer<S = Untrained> {
30 state: S,
31 config: MultiObjectiveConfig,
32}
33
34#[derive(Debug, Clone)]
36pub struct MultiObjectiveConfig {
37 pub population_size: usize,
39 pub generations: usize,
41 pub mutation_rate: Float,
43 pub crossover_rate: Float,
45 pub selection_pressure: Float,
47 pub objectives: Vec<String>,
49 pub random_state: Option<u64>,
51}
52
53impl Default for MultiObjectiveConfig {
54 fn default() -> Self {
55 Self {
56 population_size: 100,
57 generations: 100,
58 mutation_rate: 0.1,
59 crossover_rate: 0.8,
60 selection_pressure: 2.0,
61 objectives: vec!["accuracy".to_string(), "complexity".to_string()],
62 random_state: None,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct ParetoSolution {
70 pub parameters: Array1<Float>,
72 pub objectives: Array1<Float>,
74 pub rank: usize,
76 pub crowding_distance: Float,
78}
79
80#[derive(Debug, Clone)]
82pub struct MultiObjectiveOptimizerTrained {
83 pub pareto_solutions: Vec<ParetoSolution>,
85 pub best_solution: ParetoSolution,
87 pub convergence_history: Vec<Float>,
89 pub config: MultiObjectiveConfig,
91 pub n_outputs: usize,
93}
94
95impl MultiObjectiveOptimizer<Untrained> {
96 pub fn new() -> Self {
98 Self {
99 state: Untrained,
100 config: MultiObjectiveConfig::default(),
101 }
102 }
103
104 pub fn config(mut self, config: MultiObjectiveConfig) -> Self {
106 self.config = config;
107 self
108 }
109
110 pub fn population_size(mut self, population_size: usize) -> Self {
112 self.config.population_size = population_size;
113 self
114 }
115
116 pub fn generations(mut self, generations: usize) -> Self {
118 self.config.generations = generations;
119 self
120 }
121
122 pub fn mutation_rate(mut self, mutation_rate: Float) -> Self {
124 self.config.mutation_rate = mutation_rate;
125 self
126 }
127
128 pub fn crossover_rate(mut self, crossover_rate: Float) -> Self {
130 self.config.crossover_rate = crossover_rate;
131 self
132 }
133
134 pub fn selection_pressure(mut self, selection_pressure: Float) -> Self {
136 self.config.selection_pressure = selection_pressure;
137 self
138 }
139
140 pub fn objectives(mut self, objectives: Vec<String>) -> Self {
142 self.config.objectives = objectives;
143 self
144 }
145
146 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
148 self.config.random_state = random_state;
149 self
150 }
151}
152
153impl Default for MultiObjectiveOptimizer<Untrained> {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159impl Estimator for MultiObjectiveOptimizer<Untrained> {
160 type Config = MultiObjectiveConfig;
161 type Error = SklearsError;
162 type Float = Float;
163
164 fn config(&self) -> &Self::Config {
165 &self.config
166 }
167}
168
169impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, Float>> for MultiObjectiveOptimizer<Untrained> {
170 type Fitted = MultiObjectiveOptimizer<MultiObjectiveOptimizerTrained>;
171
172 fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView2<'_, Float>) -> SklResult<Self::Fitted> {
173 let (n_samples, n_features) = X.dim();
174 let (y_samples, n_outputs) = y.dim();
175
176 if n_samples != y_samples {
177 return Err(SklearsError::InvalidInput(
178 "X and y must have the same number of samples".to_string(),
179 ));
180 }
181
182 let mut rng = thread_rng();
183
184 let mut population = self.initialize_population(n_features, n_outputs, &mut rng)?;
186 let mut convergence_history = Vec::new();
187
188 for _generation in 0..self.config.generations {
189 self.evaluate_population(&mut population, X, y)?;
191
192 self.non_dominated_sort(&mut population)?;
194
195 self.calculate_crowding_distance(&mut population)?;
197
198 population = self.evolve_population(population, &mut rng)?;
200
201 let hypervolume = self.calculate_hypervolume(&population)?;
203 convergence_history.push(hypervolume);
204 }
205
206 self.evaluate_population(&mut population, X, y)?;
208 self.non_dominated_sort(&mut population)?;
209
210 let pareto_solutions: Vec<ParetoSolution> =
212 population.into_iter().filter(|sol| sol.rank == 0).collect();
213
214 let best_solution = self.find_best_compromise(&pareto_solutions)?;
216
217 Ok(MultiObjectiveOptimizer {
218 state: MultiObjectiveOptimizerTrained {
219 pareto_solutions,
220 best_solution,
221 convergence_history,
222 config: self.config.clone(),
223 n_outputs,
224 },
225 config: self.config,
226 })
227 }
228}
229
230impl MultiObjectiveOptimizer<Untrained> {
231 fn initialize_population(
233 &self,
234 n_features: usize,
235 n_outputs: usize,
236 rng: &mut scirs2_core::random::CoreRandom,
237 ) -> SklResult<Vec<ParetoSolution>> {
238 let mut population = Vec::new();
239
240 for _ in 0..self.config.population_size {
241 let param_size = n_features * n_outputs + n_outputs;
243 let normal_dist = RandNormal::new(0.0, 1.0).expect("operation should succeed");
244 let mut parameters = Array1::<Float>::zeros(param_size);
245 for i in 0..param_size {
246 parameters[i] = rng.sample(normal_dist);
247 }
248
249 let solution = ParetoSolution {
250 parameters,
251 objectives: Array1::<Float>::zeros(self.config.objectives.len()),
252 rank: 0,
253 crowding_distance: 0.0,
254 };
255
256 population.push(solution);
257 }
258
259 Ok(population)
260 }
261
262 fn evaluate_population(
264 &self,
265 population: &mut [ParetoSolution],
266 X: &ArrayView2<'_, Float>,
267 y: &ArrayView2<'_, Float>,
268 ) -> SklResult<()> {
269 let (_n_samples, n_features) = X.dim();
270 let n_outputs = y.ncols();
271
272 for solution in population.iter_mut() {
273 let weights_size = n_features * n_outputs;
275 let weights = solution
276 .parameters
277 .slice(s![..weights_size])
278 .to_owned()
279 .into_shape_with_order((
280 (n_features, n_outputs),
281 scirs2_core::ndarray::Order::RowMajor,
282 ))
283 .expect("operation should succeed");
284 let bias = solution.parameters.slice(s![weights_size..]).to_owned();
285
286 let predictions = X.dot(&weights) + &bias;
288
289 let mut objectives = Array1::<Float>::zeros(self.config.objectives.len());
291
292 for (i, objective) in self.config.objectives.iter().enumerate() {
293 let objective_value = match objective.as_str() {
294 "accuracy" => self.calculate_accuracy(&predictions, y)?,
295 "complexity" => self.calculate_complexity(&weights, &bias)?,
296 "mse" => self.calculate_mse(&predictions, y)?,
297 "mae" => self.calculate_mae(&predictions, y)?,
298 _ => {
299 return Err(SklearsError::InvalidInput(format!(
300 "Unknown objective: {}",
301 objective
302 )))
303 }
304 };
305 objectives[i] = objective_value;
306 }
307
308 solution.objectives = objectives;
309 }
310
311 Ok(())
312 }
313
314 fn calculate_accuracy(
316 &self,
317 predictions: &Array2<Float>,
318 y: &ArrayView2<'_, Float>,
319 ) -> SklResult<Float> {
320 let mse = predictions
321 .iter()
322 .zip(y.iter())
323 .map(|(pred, true_val)| (pred - true_val).powi(2))
324 .sum::<Float>()
325 / (predictions.len() as Float);
326 Ok(-mse) }
328
329 fn calculate_complexity(
331 &self,
332 weights: &Array2<Float>,
333 bias: &Array1<Float>,
334 ) -> SklResult<Float> {
335 let weight_complexity = weights.mapv(|x| x.abs()).sum();
336 let bias_complexity = bias.mapv(|x| x.abs()).sum();
337 Ok(weight_complexity + bias_complexity)
338 }
339
340 fn calculate_mse(
342 &self,
343 predictions: &Array2<Float>,
344 y: &ArrayView2<'_, Float>,
345 ) -> SklResult<Float> {
346 let mse = predictions
347 .iter()
348 .zip(y.iter())
349 .map(|(pred, true_val)| (pred - true_val).powi(2))
350 .sum::<Float>()
351 / (predictions.len() as Float);
352 Ok(mse)
353 }
354
355 fn calculate_mae(
357 &self,
358 predictions: &Array2<Float>,
359 y: &ArrayView2<'_, Float>,
360 ) -> SklResult<Float> {
361 let mae = predictions
362 .iter()
363 .zip(y.iter())
364 .map(|(pred, true_val)| (pred - true_val).abs())
365 .sum::<Float>()
366 / (predictions.len() as Float);
367 Ok(mae)
368 }
369
370 fn non_dominated_sort(&self, population: &mut [ParetoSolution]) -> SklResult<()> {
372 let n = population.len();
373 let mut domination_count = vec![0; n];
374 let mut dominated_solutions = vec![Vec::new(); n];
375
376 for i in 0..n {
378 for j in 0..n {
379 if i != j {
380 if self.dominates(&population[i], &population[j]) {
381 dominated_solutions[i].push(j);
382 } else if self.dominates(&population[j], &population[i]) {
383 domination_count[i] += 1;
384 }
385 }
386 }
387 }
388
389 let mut current_rank = 0;
391 let mut current_front: Vec<usize> = (0..n).filter(|&i| domination_count[i] == 0).collect();
392
393 while !current_front.is_empty() {
394 let mut next_front = Vec::new();
395
396 for &i in ¤t_front {
397 population[i].rank = current_rank;
398
399 for &j in &dominated_solutions[i] {
400 domination_count[j] -= 1;
401 if domination_count[j] == 0 {
402 next_front.push(j);
403 }
404 }
405 }
406
407 current_front = next_front;
408 current_rank += 1;
409 }
410
411 Ok(())
412 }
413
414 fn dominates(&self, a: &ParetoSolution, b: &ParetoSolution) -> bool {
416 let mut at_least_one_better = false;
417
418 for i in 0..a.objectives.len() {
419 if a.objectives[i] < b.objectives[i] {
420 return false; } else if a.objectives[i] > b.objectives[i] {
422 at_least_one_better = true;
423 }
424 }
425
426 at_least_one_better
427 }
428
429 fn calculate_crowding_distance(&self, population: &mut [ParetoSolution]) -> SklResult<()> {
431 let n = population.len();
432 let n_objectives = self.config.objectives.len();
433
434 for solution in population.iter_mut() {
436 solution.crowding_distance = 0.0;
437 }
438
439 for obj_idx in 0..n_objectives {
441 let mut indices: Vec<usize> = (0..n).collect();
443 indices.sort_by(|&i, &j| {
444 population[i].objectives[obj_idx]
445 .partial_cmp(&population[j].objectives[obj_idx])
446 .expect("operation should succeed")
447 });
448
449 population[indices[0]].crowding_distance = Float::INFINITY;
451 population[indices[n - 1]].crowding_distance = Float::INFINITY;
452
453 let obj_range = population[indices[n - 1]].objectives[obj_idx]
455 - population[indices[0]].objectives[obj_idx];
456
457 if obj_range > 0.0 {
458 for i in 1..n - 1 {
459 let distance = (population[indices[i + 1]].objectives[obj_idx]
460 - population[indices[i - 1]].objectives[obj_idx])
461 / obj_range;
462 population[indices[i]].crowding_distance += distance;
463 }
464 }
465 }
466
467 Ok(())
468 }
469
470 fn evolve_population(
472 &self,
473 population: Vec<ParetoSolution>,
474 rng: &mut scirs2_core::random::CoreRandom,
475 ) -> SklResult<Vec<ParetoSolution>> {
476 let mut new_population = Vec::new();
477
478 while new_population.len() < self.config.population_size {
479 let parent1 = self.tournament_selection(&population, rng)?;
481 let parent2 = self.tournament_selection(&population, rng)?;
482
483 let (mut child1, mut child2) = self.crossover(&parent1, &parent2, rng)?;
485
486 self.mutate(&mut child1, rng)?;
488 self.mutate(&mut child2, rng)?;
489
490 new_population.push(child1);
491 if new_population.len() < self.config.population_size {
492 new_population.push(child2);
493 }
494 }
495
496 Ok(new_population)
497 }
498
499 fn tournament_selection(
501 &self,
502 population: &[ParetoSolution],
503 rng: &mut scirs2_core::random::CoreRandom,
504 ) -> SklResult<ParetoSolution> {
505 let tournament_size = 3;
506 let mut best_solution = None;
507
508 for _ in 0..tournament_size {
509 let idx = rng.gen_range(0..population.len());
510 let candidate = &population[idx];
511
512 if let Some(ref current_best) = best_solution {
513 if self.is_better_solution(candidate, current_best) {
514 best_solution = Some(candidate.clone());
515 }
516 } else {
517 best_solution = Some(candidate.clone());
518 }
519 }
520
521 best_solution
522 .ok_or_else(|| SklearsError::InvalidInput("Tournament selection failed".to_string()))
523 }
524
525 fn is_better_solution(&self, a: &ParetoSolution, b: &ParetoSolution) -> bool {
527 if a.rank < b.rank {
528 true
529 } else if a.rank == b.rank {
530 a.crowding_distance > b.crowding_distance
531 } else {
532 false
533 }
534 }
535
536 fn crossover(
538 &self,
539 parent1: &ParetoSolution,
540 parent2: &ParetoSolution,
541 rng: &mut scirs2_core::random::CoreRandom,
542 ) -> SklResult<(ParetoSolution, ParetoSolution)> {
543 let mut child1 = parent1.clone();
544 let mut child2 = parent2.clone();
545
546 if rng.random::<Float>() < self.config.crossover_rate {
547 for i in 0..parent1.parameters.len() {
549 if rng.random::<Float>() < 0.5 {
550 child1.parameters[i] = parent2.parameters[i];
551 child2.parameters[i] = parent1.parameters[i];
552 }
553 }
554 }
555
556 Ok((child1, child2))
557 }
558
559 fn mutate(
561 &self,
562 solution: &mut ParetoSolution,
563 rng: &mut scirs2_core::random::CoreRandom,
564 ) -> SklResult<()> {
565 for param in solution.parameters.iter_mut() {
566 if rng.random::<Float>() < self.config.mutation_rate {
567 let mutation = rng.gen_range(-0.1..0.1);
568 *param += mutation;
569 }
570 }
571 Ok(())
572 }
573
574 fn calculate_hypervolume(&self, population: &[ParetoSolution]) -> SklResult<Float> {
576 let pareto_front: Vec<&ParetoSolution> =
578 population.iter().filter(|sol| sol.rank == 0).collect();
579
580 if pareto_front.is_empty() {
581 return Ok(0.0);
582 }
583
584 let hypervolume = pareto_front
586 .iter()
587 .map(|sol| sol.objectives.sum())
588 .sum::<Float>()
589 / pareto_front.len() as Float;
590
591 Ok(hypervolume)
592 }
593
594 fn find_best_compromise(
596 &self,
597 pareto_solutions: &[ParetoSolution],
598 ) -> SklResult<ParetoSolution> {
599 if pareto_solutions.is_empty() {
600 return Err(SklearsError::InvalidInput(
601 "No Pareto solutions available".to_string(),
602 ));
603 }
604
605 let mut best_solution = pareto_solutions[0].clone();
607 let mut best_distance = Float::INFINITY;
608
609 for solution in pareto_solutions {
610 let distance = solution.objectives.mapv(|x| x * x).sum().sqrt();
611 if distance < best_distance {
612 best_distance = distance;
613 best_solution = solution.clone();
614 }
615 }
616
617 Ok(best_solution)
618 }
619}
620
621impl Predict<ArrayView2<'_, Float>, Array2<Float>>
622 for MultiObjectiveOptimizer<MultiObjectiveOptimizerTrained>
623{
624 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
625 let (_n_samples, n_features) = X.dim();
626 let best_solution = &self.state.best_solution;
627
628 let n_outputs = self.state.n_outputs;
630 let weights_size = n_features * n_outputs;
631 let weights = best_solution
632 .parameters
633 .slice(s![..weights_size])
634 .to_owned()
635 .into_shape_with_order((
636 (n_features, n_outputs),
637 scirs2_core::ndarray::Order::RowMajor,
638 ))
639 .expect("operation should succeed");
640 let bias = best_solution
641 .parameters
642 .slice(s![weights_size..weights_size + n_outputs])
643 .to_owned();
644
645 let predictions = X.dot(&weights) + &bias;
646 Ok(predictions)
647 }
648}
649
650impl Estimator for MultiObjectiveOptimizer<MultiObjectiveOptimizerTrained> {
651 type Config = MultiObjectiveConfig;
652 type Error = SklearsError;
653 type Float = Float;
654
655 fn config(&self) -> &Self::Config {
656 &self.state.config
657 }
658}
659
660impl MultiObjectiveOptimizer<MultiObjectiveOptimizerTrained> {
661 pub fn pareto_solutions(&self) -> &[ParetoSolution] {
663 &self.state.pareto_solutions
664 }
665
666 pub fn best_solution(&self) -> &ParetoSolution {
668 &self.state.best_solution
669 }
670
671 pub fn convergence_history(&self) -> &[Float] {
673 &self.state.convergence_history
674 }
675}