1use crate::error::{LogicError, LogicResult};
18use scirs2_core::ndarray::Array1;
19use std::cmp::Ordering;
20
21#[derive(Debug, Clone)]
23pub struct MultiObjectiveSolution {
24 pub variables: Array1<f32>,
26 pub objectives: Vec<f32>,
28 pub violations: Vec<f32>,
30 pub total_violation: f32,
32 pub rank: usize,
34 pub crowding_distance: f32,
36}
37
38impl MultiObjectiveSolution {
39 pub fn dominates(&self, other: &Self, minimize: &[bool]) -> bool {
45 let self_feasible = self.is_feasible();
47 let other_feasible = other.is_feasible();
48
49 if self_feasible && !other_feasible {
50 return true;
51 }
52 if !self_feasible && other_feasible {
53 return false;
54 }
55 if !self_feasible && !other_feasible {
56 return self.total_violation < other.total_violation;
58 }
59
60 let mut better_in_at_least_one = false;
62 let mut worse_in_any = false;
63
64 for (i, (&obj_a, &obj_b)) in self
65 .objectives
66 .iter()
67 .zip(other.objectives.iter())
68 .enumerate()
69 {
70 let cmp = if minimize[i] {
71 obj_a.partial_cmp(&obj_b)
72 } else {
73 obj_b.partial_cmp(&obj_a)
74 };
75
76 match cmp {
77 Some(Ordering::Less) => better_in_at_least_one = true,
78 Some(Ordering::Greater) => worse_in_any = true,
79 _ => {}
80 }
81 }
82
83 better_in_at_least_one && !worse_in_any
84 }
85
86 pub fn is_feasible(&self) -> bool {
88 self.total_violation < 1e-6
89 }
90
91 pub fn objective_distance(&self, other: &Self) -> f32 {
93 self.objectives
94 .iter()
95 .zip(other.objectives.iter())
96 .map(|(a, b)| (a - b).powi(2))
97 .sum::<f32>()
98 .sqrt()
99 }
100}
101
102pub struct MultiObjectiveOptimizer {
104 num_objectives: usize,
106 minimize: Vec<bool>,
108 population_size: usize,
110 max_generations: usize,
112 #[allow(dead_code)]
114 mutation_rate: f32,
115}
116
117impl MultiObjectiveOptimizer {
118 pub fn new(num_objectives: usize, minimize: Vec<bool>) -> Self {
120 assert_eq!(
121 num_objectives,
122 minimize.len(),
123 "Minimize vector must match number of objectives"
124 );
125
126 Self {
127 num_objectives,
128 minimize,
129 population_size: 100,
130 max_generations: 100,
131 mutation_rate: 0.1,
132 }
133 }
134
135 pub fn with_population_size(mut self, size: usize) -> Self {
137 self.population_size = size;
138 self
139 }
140
141 pub fn with_max_generations(mut self, gens: usize) -> Self {
143 self.max_generations = gens;
144 self
145 }
146
147 pub fn non_dominated_sort(&self, population: &mut [MultiObjectiveSolution]) -> Vec<Vec<usize>> {
149 let n = population.len();
150 let mut domination_count = vec![0; n];
151 let mut dominated_solutions = vec![Vec::new(); n];
152 let mut fronts = vec![Vec::new()];
153
154 for i in 0..n {
156 for j in 0..n {
157 if i == j {
158 continue;
159 }
160 if population[i].dominates(&population[j], &self.minimize) {
161 dominated_solutions[i].push(j);
162 } else if population[j].dominates(&population[i], &self.minimize) {
163 domination_count[i] += 1;
164 }
165 }
166
167 if domination_count[i] == 0 {
169 population[i].rank = 0;
170 fronts[0].push(i);
171 }
172 }
173
174 let mut current_front = 0;
176 while current_front < fronts.len() && !fronts[current_front].is_empty() {
177 let mut next_front = Vec::new();
178
179 for &i in &fronts[current_front] {
180 for &j in &dominated_solutions[i] {
181 domination_count[j] -= 1;
182 if domination_count[j] == 0 {
183 population[j].rank = current_front + 1;
184 next_front.push(j);
185 }
186 }
187 }
188
189 if !next_front.is_empty() {
190 fronts.push(next_front);
191 }
192 current_front += 1;
193 }
194
195 fronts
196 }
197
198 pub fn compute_crowding_distance(&self, population: &mut [MultiObjectiveSolution]) {
200 let n = population.len();
201
202 if n == 0 {
203 return;
204 }
205
206 for solution in population.iter_mut() {
208 solution.crowding_distance = 0.0;
209 }
210
211 for obj_idx in 0..self.num_objectives {
213 let mut indices: Vec<usize> = (0..n).collect();
215 indices.sort_by(|&a, &b| {
216 population[a].objectives[obj_idx]
217 .partial_cmp(&population[b].objectives[obj_idx])
218 .unwrap_or(Ordering::Equal)
219 });
220
221 population[indices[0]].crowding_distance = f32::INFINITY;
223 population[indices[n - 1]].crowding_distance = f32::INFINITY;
224
225 let obj_min = population[indices[0]].objectives[obj_idx];
227 let obj_max = population[indices[n - 1]].objectives[obj_idx];
228 let range = obj_max - obj_min;
229
230 if range < 1e-10 {
231 continue;
232 }
233
234 for i in 1..(n - 1) {
236 let prev_obj = population[indices[i - 1]].objectives[obj_idx];
237 let next_obj = population[indices[i + 1]].objectives[obj_idx];
238 population[indices[i]].crowding_distance += (next_obj - prev_obj) / range;
239 }
240 }
241 }
242
243 pub fn get_pareto_frontier(
245 &self,
246 population: &[MultiObjectiveSolution],
247 ) -> Vec<MultiObjectiveSolution> {
248 let mut pop_copy = population.to_vec();
249 let fronts = self.non_dominated_sort(&mut pop_copy);
250
251 if fronts.is_empty() || fronts[0].is_empty() {
252 return Vec::new();
253 }
254
255 fronts[0].iter().map(|&i| pop_copy[i].clone()).collect()
256 }
257}
258
259pub struct WeightedSumMethod {
261 weights: Vec<f32>,
263}
264
265impl WeightedSumMethod {
266 pub fn new(weights: Vec<f32>) -> LogicResult<Self> {
268 let sum: f32 = weights.iter().sum();
269 if (sum - 1.0).abs() > 1e-6 {
270 return Err(LogicError::InvalidInput(
271 "Weights must sum to 1.0".to_string(),
272 ));
273 }
274
275 for &w in &weights {
276 if w < 0.0 {
277 return Err(LogicError::InvalidInput(
278 "Weights must be non-negative".to_string(),
279 ));
280 }
281 }
282
283 Ok(Self { weights })
284 }
285
286 pub fn combine_objectives(&self, objectives: &[f32]) -> LogicResult<f32> {
288 if objectives.len() != self.weights.len() {
289 return Err(LogicError::InvalidInput(
290 "Objective count mismatch".to_string(),
291 ));
292 }
293
294 let combined = objectives
295 .iter()
296 .zip(self.weights.iter())
297 .map(|(obj, weight)| obj * weight)
298 .sum();
299
300 Ok(combined)
301 }
302}
303
304pub struct HypervolumeIndicator {
306 reference_point: Vec<f32>,
308}
309
310impl HypervolumeIndicator {
311 pub fn new(reference_point: Vec<f32>) -> Self {
313 Self { reference_point }
314 }
315
316 pub fn compute_2d(&self, front: &[MultiObjectiveSolution]) -> f32 {
320 if front.is_empty() || self.reference_point.len() != 2 {
321 return 0.0;
322 }
323
324 let mut sorted_front = front.to_vec();
326 sorted_front.sort_by(|a, b| {
327 a.objectives[0]
328 .partial_cmp(&b.objectives[0])
329 .unwrap_or(Ordering::Equal)
330 });
331
332 let mut hypervolume = 0.0;
333 let mut prev_y = self.reference_point[1];
334
335 for solution in sorted_front.iter() {
336 let x = solution.objectives[0];
337 let y = solution.objectives[1];
338
339 if x < self.reference_point[0] && y < self.reference_point[1] {
340 let width = self.reference_point[0] - x;
341 let height = prev_y - y;
342 hypervolume += width * height;
343 prev_y = y;
344 }
345 }
346
347 hypervolume
348 }
349
350 pub fn contribution(
352 &self,
353 solution: &MultiObjectiveSolution,
354 front: &[MultiObjectiveSolution],
355 ) -> f32 {
356 let hv_with = self.compute_2d(front);
358
359 let front_without: Vec<MultiObjectiveSolution> = front
360 .iter()
361 .filter(|s| !std::ptr::eq(*s, solution))
362 .cloned()
363 .collect();
364
365 let hv_without = self.compute_2d(&front_without);
366
367 hv_with - hv_without
368 }
369}
370
371pub struct EpsilonConstraintMethod {
375 primary_objective: usize,
377 epsilon_bounds: Vec<f32>,
379}
380
381impl EpsilonConstraintMethod {
382 pub fn new(primary_objective: usize, epsilon_bounds: Vec<f32>) -> Self {
384 Self {
385 primary_objective,
386 epsilon_bounds,
387 }
388 }
389
390 pub fn satisfies_constraints(&self, solution: &MultiObjectiveSolution) -> bool {
392 for (i, &bound) in self.epsilon_bounds.iter().enumerate() {
393 if i == self.primary_objective {
394 continue;
395 }
396 if solution.objectives[i] > bound {
397 return false;
398 }
399 }
400 true
401 }
402
403 pub fn primary_value(&self, solution: &MultiObjectiveSolution) -> f32 {
405 solution.objectives[self.primary_objective]
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_dominance() {
415 let sol1 = MultiObjectiveSolution {
416 variables: Array1::from_vec(vec![1.0]),
417 objectives: vec![2.0, 3.0],
418 violations: vec![],
419 total_violation: 0.0,
420 rank: 0,
421 crowding_distance: 0.0,
422 };
423
424 let sol2 = MultiObjectiveSolution {
425 variables: Array1::from_vec(vec![2.0]),
426 objectives: vec![3.0, 4.0],
427 violations: vec![],
428 total_violation: 0.0,
429 rank: 0,
430 crowding_distance: 0.0,
431 };
432
433 let minimize = vec![true, true];
434 assert!(sol1.dominates(&sol2, &minimize));
435 assert!(!sol2.dominates(&sol1, &minimize));
436 }
437
438 #[test]
439 fn test_non_dominated_sorting() {
440 let optimizer = MultiObjectiveOptimizer::new(2, vec![true, true]);
441
442 let mut population = vec![
443 MultiObjectiveSolution {
444 variables: Array1::from_vec(vec![1.0]),
445 objectives: vec![1.0, 4.0],
446 violations: vec![],
447 total_violation: 0.0,
448 rank: 0,
449 crowding_distance: 0.0,
450 },
451 MultiObjectiveSolution {
452 variables: Array1::from_vec(vec![2.0]),
453 objectives: vec![2.0, 3.0],
454 violations: vec![],
455 total_violation: 0.0,
456 rank: 0,
457 crowding_distance: 0.0,
458 },
459 MultiObjectiveSolution {
460 variables: Array1::from_vec(vec![3.0]),
461 objectives: vec![3.0, 2.0],
462 violations: vec![],
463 total_violation: 0.0,
464 rank: 0,
465 crowding_distance: 0.0,
466 },
467 MultiObjectiveSolution {
468 variables: Array1::from_vec(vec![4.0]),
469 objectives: vec![4.0, 1.0],
470 violations: vec![],
471 total_violation: 0.0,
472 rank: 0,
473 crowding_distance: 0.0,
474 },
475 MultiObjectiveSolution {
476 variables: Array1::from_vec(vec![5.0]),
477 objectives: vec![2.5, 2.5],
478 violations: vec![],
479 total_violation: 0.0,
480 rank: 0,
481 crowding_distance: 0.0,
482 },
483 ];
484
485 let fronts = optimizer.non_dominated_sort(&mut population);
486
487 assert!(!fronts.is_empty());
489 assert_eq!(fronts[0].len(), 5);
490 }
491
492 #[test]
493 fn test_crowding_distance() {
494 let optimizer = MultiObjectiveOptimizer::new(2, vec![true, true]);
495
496 let mut population = vec![
497 MultiObjectiveSolution {
498 variables: Array1::from_vec(vec![1.0]),
499 objectives: vec![1.0, 5.0],
500 violations: vec![],
501 total_violation: 0.0,
502 rank: 0,
503 crowding_distance: 0.0,
504 },
505 MultiObjectiveSolution {
506 variables: Array1::from_vec(vec![2.0]),
507 objectives: vec![3.0, 3.0],
508 violations: vec![],
509 total_violation: 0.0,
510 rank: 0,
511 crowding_distance: 0.0,
512 },
513 MultiObjectiveSolution {
514 variables: Array1::from_vec(vec![3.0]),
515 objectives: vec![5.0, 1.0],
516 violations: vec![],
517 total_violation: 0.0,
518 rank: 0,
519 crowding_distance: 0.0,
520 },
521 ];
522
523 optimizer.compute_crowding_distance(&mut population);
524
525 assert_eq!(population[0].crowding_distance, f32::INFINITY);
527 assert_eq!(population[2].crowding_distance, f32::INFINITY);
528 assert!(population[1].crowding_distance.is_finite());
530 assert!(population[1].crowding_distance > 0.0);
531 }
532
533 #[test]
534 fn test_weighted_sum() {
535 let method = WeightedSumMethod::new(vec![0.6, 0.4]).unwrap();
536 let objectives = vec![10.0, 20.0];
537 let combined = method.combine_objectives(&objectives).unwrap();
538
539 assert!((combined - 14.0).abs() < 1e-5); }
541
542 #[test]
543 fn test_weighted_sum_validation() {
544 let result = WeightedSumMethod::new(vec![0.5, 0.4]);
546 assert!(result.is_err());
547
548 let result = WeightedSumMethod::new(vec![1.5, -0.5]);
550 assert!(result.is_err());
551 }
552
553 #[test]
554 fn test_hypervolume_2d() {
555 let indicator = HypervolumeIndicator::new(vec![10.0, 10.0]);
556
557 let front = vec![
558 MultiObjectiveSolution {
559 variables: Array1::from_vec(vec![1.0]),
560 objectives: vec![2.0, 8.0],
561 violations: vec![],
562 total_violation: 0.0,
563 rank: 0,
564 crowding_distance: 0.0,
565 },
566 MultiObjectiveSolution {
567 variables: Array1::from_vec(vec![2.0]),
568 objectives: vec![5.0, 5.0],
569 violations: vec![],
570 total_violation: 0.0,
571 rank: 0,
572 crowding_distance: 0.0,
573 },
574 MultiObjectiveSolution {
575 variables: Array1::from_vec(vec![3.0]),
576 objectives: vec![8.0, 2.0],
577 violations: vec![],
578 total_violation: 0.0,
579 rank: 0,
580 crowding_distance: 0.0,
581 },
582 ];
583
584 let hv = indicator.compute_2d(&front);
585 assert!(hv > 0.0);
586 }
587
588 #[test]
589 fn test_epsilon_constraint() {
590 let method = EpsilonConstraintMethod::new(0, vec![f32::MAX, 5.0]);
591
592 let sol1 = MultiObjectiveSolution {
593 variables: Array1::from_vec(vec![1.0]),
594 objectives: vec![2.0, 3.0],
595 violations: vec![],
596 total_violation: 0.0,
597 rank: 0,
598 crowding_distance: 0.0,
599 };
600
601 let sol2 = MultiObjectiveSolution {
602 variables: Array1::from_vec(vec![2.0]),
603 objectives: vec![1.0, 6.0],
604 violations: vec![],
605 total_violation: 0.0,
606 rank: 0,
607 crowding_distance: 0.0,
608 };
609
610 assert!(method.satisfies_constraints(&sol1));
611 assert!(!method.satisfies_constraints(&sol2));
612 assert_eq!(method.primary_value(&sol1), 2.0);
613 }
614
615 #[test]
616 fn test_pareto_frontier() {
617 let optimizer = MultiObjectiveOptimizer::new(2, vec![true, true]);
618
619 let population = vec![
620 MultiObjectiveSolution {
621 variables: Array1::from_vec(vec![1.0]),
622 objectives: vec![1.0, 5.0],
623 violations: vec![],
624 total_violation: 0.0,
625 rank: 0,
626 crowding_distance: 0.0,
627 },
628 MultiObjectiveSolution {
629 variables: Array1::from_vec(vec![2.0]),
630 objectives: vec![3.0, 3.0],
631 violations: vec![],
632 total_violation: 0.0,
633 rank: 0,
634 crowding_distance: 0.0,
635 },
636 MultiObjectiveSolution {
637 variables: Array1::from_vec(vec![3.0]),
638 objectives: vec![5.0, 1.0],
639 violations: vec![],
640 total_violation: 0.0,
641 rank: 0,
642 crowding_distance: 0.0,
643 },
644 MultiObjectiveSolution {
645 variables: Array1::from_vec(vec![4.0]),
646 objectives: vec![2.0, 6.0], violations: vec![],
648 total_violation: 0.0,
649 rank: 0,
650 crowding_distance: 0.0,
651 },
652 ];
653
654 let frontier = optimizer.get_pareto_frontier(&population);
655 assert_eq!(frontier.len(), 3); }
657}