1use scirs2_core::random::prelude::*;
8use scirs2_core::random::ChaCha8Rng;
9use scirs2_core::random::{Rng, SeedableRng};
10use std::collections::HashMap;
11use std::time::{Duration, Instant};
12use thiserror::Error;
13
14use crate::ising::{IsingError, IsingModel};
15use crate::simulator::{AnnealingParams, AnnealingSolution, QuantumAnnealingSimulator};
16
17#[derive(Error, Debug)]
19pub enum MultiObjectiveError {
20 #[error("Ising error: {0}")]
22 IsingError(#[from] IsingError),
23
24 #[error("Invalid objective: {0}")]
26 InvalidObjective(String),
27
28 #[error("Optimization failed: {0}")]
30 OptimizationFailed(String),
31
32 #[error("Pareto analysis error: {0}")]
34 ParetoAnalysisError(String),
35
36 #[error("Scalarization error: {0}")]
38 ScalarizationError(String),
39}
40
41pub type MultiObjectiveResult<T> = Result<T, MultiObjectiveError>;
43
44pub type MultiObjectiveFunction = Box<dyn Fn(&[i8]) -> Vec<f64> + Send + Sync>;
46
47#[derive(Debug, Clone)]
49pub struct MultiObjectiveSolution {
50 pub variables: Vec<i8>,
52
53 pub objectives: Vec<f64>,
55
56 pub pareto_rank: usize,
58
59 pub crowding_distance: f64,
61
62 pub metadata: HashMap<String, String>,
64}
65
66impl MultiObjectiveSolution {
67 #[must_use]
69 pub fn new(variables: Vec<i8>, objectives: Vec<f64>) -> Self {
70 Self {
71 variables,
72 objectives,
73 pareto_rank: 0,
74 crowding_distance: 0.0,
75 metadata: HashMap::new(),
76 }
77 }
78
79 #[must_use]
81 pub fn dominates(&self, other: &Self) -> bool {
82 if self.objectives.len() != other.objectives.len() {
83 return false;
84 }
85
86 let mut at_least_one_better = false;
87 for (self_obj, other_obj) in self.objectives.iter().zip(other.objectives.iter()) {
88 if self_obj > other_obj {
89 return false; }
91 if self_obj < other_obj {
92 at_least_one_better = true;
93 }
94 }
95
96 at_least_one_better
97 }
98
99 #[must_use]
101 pub fn is_non_dominated_by(&self, other: &Self) -> bool {
102 !other.dominates(self)
103 }
104}
105
106#[derive(Debug, Clone)]
108pub enum ScalarizationMethod {
109 WeightedSum { weights: Vec<f64> },
111
112 WeightedChebyshev {
114 weights: Vec<f64>,
115 reference_point: Vec<f64>,
116 },
117
118 AugmentedChebyshev {
120 weights: Vec<f64>,
121 reference_point: Vec<f64>,
122 rho: f64,
123 },
124
125 Achievement {
127 weights: Vec<f64>,
128 reference_point: Vec<f64>,
129 },
130
131 EpsilonConstraint {
133 primary_objective: usize,
134 constraints: Vec<f64>,
135 },
136}
137
138impl ScalarizationMethod {
139 pub fn scalarize(&self, objectives: &[f64]) -> MultiObjectiveResult<f64> {
141 match self {
142 Self::WeightedSum { weights } => {
143 if weights.len() != objectives.len() {
144 return Err(MultiObjectiveError::ScalarizationError(
145 "Weights and objectives dimension mismatch".to_string(),
146 ));
147 }
148
149 let weighted_sum = weights
150 .iter()
151 .zip(objectives.iter())
152 .map(|(w, obj)| w * obj)
153 .sum();
154
155 Ok(weighted_sum)
156 }
157
158 Self::WeightedChebyshev {
159 weights,
160 reference_point,
161 } => {
162 if weights.len() != objectives.len() || reference_point.len() != objectives.len() {
163 return Err(MultiObjectiveError::ScalarizationError(
164 "Dimension mismatch in Chebyshev scalarization".to_string(),
165 ));
166 }
167
168 let max_weighted_deviation = weights
169 .iter()
170 .zip(objectives.iter())
171 .zip(reference_point.iter())
172 .map(|((w, obj), ref_val)| w * (obj - ref_val).abs())
173 .fold(0.0, f64::max);
174
175 Ok(max_weighted_deviation)
176 }
177
178 Self::AugmentedChebyshev {
179 weights,
180 reference_point,
181 rho,
182 } => {
183 let chebyshev_value =
184 self.scalarize_chebyshev(objectives, weights, reference_point)?;
185
186 let augmentation_term = rho
187 * weights
188 .iter()
189 .zip(objectives.iter())
190 .zip(reference_point.iter())
191 .map(|((w, obj), ref_val)| w * (obj - ref_val))
192 .sum::<f64>();
193
194 Ok(chebyshev_value + augmentation_term)
195 }
196
197 Self::Achievement {
198 weights,
199 reference_point,
200 } => {
201 let achievement_value = weights
202 .iter()
203 .zip(objectives.iter())
204 .zip(reference_point.iter())
205 .map(|((w, obj), ref_val)| (obj - ref_val) / w.max(1e-8))
206 .fold(f64::NEG_INFINITY, f64::max);
207
208 Ok(achievement_value)
209 }
210
211 Self::EpsilonConstraint {
212 primary_objective,
213 constraints,
214 } => {
215 if *primary_objective >= objectives.len() {
216 return Err(MultiObjectiveError::ScalarizationError(
217 "Primary objective index out of bounds".to_string(),
218 ));
219 }
220
221 let mut penalty = 0.0;
223 for (i, &constraint_bound) in constraints.iter().enumerate() {
224 if i != *primary_objective && i < objectives.len() {
225 if objectives[i] > constraint_bound {
226 penalty += 1000.0 * (objectives[i] - constraint_bound);
227 }
228 }
229 }
230
231 Ok(objectives[*primary_objective] + penalty)
232 }
233 }
234 }
235
236 fn scalarize_chebyshev(
238 &self,
239 objectives: &[f64],
240 weights: &[f64],
241 reference_point: &[f64],
242 ) -> MultiObjectiveResult<f64> {
243 if weights.len() != objectives.len() || reference_point.len() != objectives.len() {
244 return Err(MultiObjectiveError::ScalarizationError(
245 "Dimension mismatch in Chebyshev scalarization".to_string(),
246 ));
247 }
248
249 let max_weighted_deviation = weights
250 .iter()
251 .zip(objectives.iter())
252 .zip(reference_point.iter())
253 .map(|((w, obj), ref_val)| w * (obj - ref_val).abs())
254 .fold(0.0, f64::max);
255
256 Ok(max_weighted_deviation)
257 }
258}
259
260#[derive(Debug, Clone)]
262pub struct MultiObjectiveConfig {
263 pub annealing_params: AnnealingParams,
265
266 pub scalarization: ScalarizationMethod,
268
269 pub num_pareto_runs: usize,
271
272 pub population_size: usize,
274
275 pub enable_pareto_analysis: bool,
277
278 pub max_pareto_solutions: usize,
280
281 pub seed: Option<u64>,
283
284 pub timeout: Option<Duration>,
286}
287
288impl Default for MultiObjectiveConfig {
289 fn default() -> Self {
290 Self {
291 annealing_params: AnnealingParams::default(),
292 scalarization: ScalarizationMethod::WeightedSum {
293 weights: vec![1.0, 1.0],
294 },
295 num_pareto_runs: 20,
296 population_size: 100,
297 enable_pareto_analysis: true,
298 max_pareto_solutions: 50,
299 seed: None,
300 timeout: Some(Duration::from_secs(600)),
301 }
302 }
303}
304
305#[derive(Debug, Clone)]
307pub struct MultiObjectiveResults {
308 pub all_solutions: Vec<MultiObjectiveSolution>,
310
311 pub pareto_front: Vec<MultiObjectiveSolution>,
313
314 pub stats: MultiObjectiveStats,
316
317 pub objective_bounds: Vec<(f64, f64)>, }
320
321#[derive(Debug, Clone)]
323pub struct MultiObjectiveStats {
324 pub total_runtime: Duration,
326
327 pub unique_solutions: usize,
329
330 pub pareto_front_size: usize,
332
333 pub average_crowding_distance: f64,
335
336 pub hypervolume: Option<f64>,
338
339 pub convergence_metrics: HashMap<String, f64>,
341}
342
343pub struct MultiObjectiveOptimizer {
345 config: MultiObjectiveConfig,
347
348 rng: ChaCha8Rng,
350}
351
352impl MultiObjectiveOptimizer {
353 #[must_use]
355 pub fn new(config: MultiObjectiveConfig) -> Self {
356 let rng = match config.seed {
357 Some(seed) => ChaCha8Rng::seed_from_u64(seed),
358 None => ChaCha8Rng::seed_from_u64(thread_rng().gen()),
359 };
360
361 Self { config, rng }
362 }
363
364 pub fn solve(
366 &mut self,
367 model: &IsingModel,
368 objective_function: MultiObjectiveFunction,
369 num_objectives: usize,
370 ) -> MultiObjectiveResult<MultiObjectiveResults> {
371 let start_time = Instant::now();
372 let mut all_solutions = Vec::new();
373
374 let scalarizations = self.generate_scalarizations(num_objectives)?;
376
377 for (run_idx, scalarization) in scalarizations.into_iter().enumerate() {
379 if let Some(timeout) = self.config.timeout {
381 if start_time.elapsed() > timeout {
382 break;
383 }
384 }
385
386 println!(
387 "Running scalarization {}/{}",
388 run_idx + 1,
389 self.config.num_pareto_runs
390 );
391
392 let solution =
394 self.solve_scalarized_problem(model, &objective_function, &scalarization)?;
395
396 let objectives = objective_function(&solution.best_spins);
398 let mo_solution = MultiObjectiveSolution::new(solution.best_spins, objectives);
399
400 all_solutions.push(mo_solution);
401 }
402
403 all_solutions = self.remove_duplicates(all_solutions);
405
406 if self.config.enable_pareto_analysis {
408 self.perform_pareto_analysis(&mut all_solutions)?;
409 }
410
411 let pareto_front: Vec<MultiObjectiveSolution> = all_solutions
413 .iter()
414 .filter(|sol| sol.pareto_rank == 0)
415 .cloned()
416 .collect();
417
418 let objective_bounds = self.calculate_objective_bounds(&all_solutions, num_objectives);
420
421 let stats = self.calculate_statistics(&all_solutions, &pareto_front, start_time.elapsed());
423
424 Ok(MultiObjectiveResults {
425 all_solutions,
426 pareto_front,
427 stats,
428 objective_bounds,
429 })
430 }
431
432 fn generate_scalarizations(
434 &mut self,
435 num_objectives: usize,
436 ) -> MultiObjectiveResult<Vec<ScalarizationMethod>> {
437 let mut scalarizations = Vec::new();
438 let num_runs = self.config.num_pareto_runs;
439
440 match self.config.scalarization.clone() {
441 ScalarizationMethod::WeightedSum { .. } => {
442 for i in 0..num_runs {
444 let weights = self.generate_weight_vector(num_objectives, i, num_runs);
445 scalarizations.push(ScalarizationMethod::WeightedSum { weights });
446 }
447 }
448
449 ScalarizationMethod::WeightedChebyshev {
450 reference_point, ..
451 } => {
452 for i in 0..num_runs {
454 let weights = self.generate_weight_vector(num_objectives, i, num_runs);
455 scalarizations.push(ScalarizationMethod::WeightedChebyshev {
456 weights,
457 reference_point: reference_point.clone(),
458 });
459 }
460 }
461
462 ScalarizationMethod::EpsilonConstraint {
463 primary_objective, ..
464 } => {
465 for i in 0..num_runs {
467 let constraints = self.generate_constraint_bounds(num_objectives, i);
468 scalarizations.push(ScalarizationMethod::EpsilonConstraint {
469 primary_objective,
470 constraints,
471 });
472 }
473 }
474
475 method => {
476 for _ in 0..num_runs {
478 scalarizations.push(method.clone());
479 }
480 }
481 }
482
483 Ok(scalarizations)
484 }
485
486 fn generate_weight_vector(
488 &mut self,
489 num_objectives: usize,
490 run_index: usize,
491 num_runs: usize,
492 ) -> Vec<f64> {
493 if num_objectives == 2 {
494 let alpha = if num_runs > 1 {
496 run_index as f64 / (num_runs - 1) as f64
497 } else {
498 0.5
499 };
500 vec![alpha, 1.0 - alpha]
501 } else {
502 let mut weights: Vec<f64> = (0..num_objectives)
504 .map(|_| self.rng.gen_range(0.1..1.0))
505 .collect();
506
507 let sum: f64 = weights.iter().sum();
509 for weight in &mut weights {
510 *weight /= sum;
511 }
512
513 weights
514 }
515 }
516
517 fn generate_constraint_bounds(&mut self, num_objectives: usize, _run_index: usize) -> Vec<f64> {
519 (0..num_objectives)
521 .map(|_| self.rng.gen_range(-10.0..10.0))
522 .collect()
523 }
524
525 fn solve_scalarized_problem(
527 &self,
528 model: &IsingModel,
529 objective_function: &MultiObjectiveFunction,
530 scalarization: &ScalarizationMethod,
531 ) -> MultiObjectiveResult<AnnealingSolution> {
532 let mut scalarized_model = model.clone();
534
535 let mut simulator = QuantumAnnealingSimulator::new(self.config.annealing_params.clone())
539 .map_err(|e| MultiObjectiveError::OptimizationFailed(e.to_string()))?;
540
541 let solution = simulator
542 .solve(&scalarized_model)
543 .map_err(|e| MultiObjectiveError::OptimizationFailed(e.to_string()))?;
544
545 Ok(solution)
546 }
547
548 fn remove_duplicates(
550 &self,
551 mut solutions: Vec<MultiObjectiveSolution>,
552 ) -> Vec<MultiObjectiveSolution> {
553 solutions.sort_by(|a, b| {
554 a.variables
555 .partial_cmp(&b.variables)
556 .unwrap_or(std::cmp::Ordering::Equal)
557 });
558
559 solutions.dedup_by(|a, b| a.variables == b.variables);
560 solutions
561 }
562
563 fn perform_pareto_analysis(
565 &self,
566 solutions: &mut [MultiObjectiveSolution],
567 ) -> MultiObjectiveResult<()> {
568 self.fast_non_dominated_sort(solutions)?;
570
571 self.calculate_crowding_distances(solutions)?;
573
574 Ok(())
575 }
576
577 fn fast_non_dominated_sort(
579 &self,
580 solutions: &mut [MultiObjectiveSolution],
581 ) -> MultiObjectiveResult<()> {
582 let n = solutions.len();
583 let mut domination_count = vec![0; n];
584 let mut dominated_solutions = vec![Vec::new(); n];
585 let mut fronts = Vec::new();
586 let mut current_front = Vec::new();
587
588 for i in 0..n {
590 for j in 0..n {
591 if i != j {
592 if solutions[i].dominates(&solutions[j]) {
593 dominated_solutions[i].push(j);
594 } else if solutions[j].dominates(&solutions[i]) {
595 domination_count[i] += 1;
596 }
597 }
598 }
599
600 if domination_count[i] == 0 {
601 solutions[i].pareto_rank = 0;
602 current_front.push(i);
603 }
604 }
605
606 fronts.push(current_front.clone());
607
608 let mut front_index = 0;
610 while !fronts[front_index].is_empty() {
611 let mut next_front = Vec::new();
612
613 for &p in &fronts[front_index] {
614 for &q in &dominated_solutions[p] {
615 domination_count[q] -= 1;
616 if domination_count[q] == 0 {
617 solutions[q].pareto_rank = front_index + 1;
618 next_front.push(q);
619 }
620 }
621 }
622
623 front_index += 1;
624 fronts.push(next_front);
625 }
626
627 Ok(())
628 }
629
630 fn calculate_crowding_distances(
632 &self,
633 solutions: &mut [MultiObjectiveSolution],
634 ) -> MultiObjectiveResult<()> {
635 if solutions.is_empty() {
636 return Ok(());
637 }
638
639 let num_objectives = solutions[0].objectives.len();
640
641 for solution in solutions.iter_mut() {
643 solution.crowding_distance = 0.0;
644 }
645
646 for obj_idx in 0..num_objectives {
648 let mut indices: Vec<usize> = (0..solutions.len()).collect();
650 indices.sort_by(|&a, &b| {
651 solutions[a].objectives[obj_idx]
652 .partial_cmp(&solutions[b].objectives[obj_idx])
653 .unwrap_or(std::cmp::Ordering::Equal)
654 });
655
656 if !indices.is_empty() {
658 solutions[indices[0]].crowding_distance = f64::INFINITY;
659 if indices.len() > 1 {
660 solutions[indices[indices.len() - 1]].crowding_distance = f64::INFINITY;
661 }
662 }
663
664 if indices.len() > 2 {
666 let min_obj = solutions[indices[0]].objectives[obj_idx];
667 let max_obj = solutions[indices[indices.len() - 1]].objectives[obj_idx];
668 let range = max_obj - min_obj;
669
670 if range > 0.0 {
671 for i in 1..indices.len() - 1 {
673 let distance = (solutions[indices[i + 1]].objectives[obj_idx]
674 - solutions[indices[i - 1]].objectives[obj_idx])
675 / range;
676 solutions[indices[i]].crowding_distance += distance;
677 }
678 }
679 }
680 }
681
682 Ok(())
683 }
684
685 fn calculate_objective_bounds(
687 &self,
688 solutions: &[MultiObjectiveSolution],
689 num_objectives: usize,
690 ) -> Vec<(f64, f64)> {
691 let mut bounds = vec![(f64::INFINITY, f64::NEG_INFINITY); num_objectives];
692
693 for solution in solutions {
694 for (obj_idx, &obj_value) in solution.objectives.iter().enumerate() {
695 if obj_idx < bounds.len() {
696 bounds[obj_idx].0 = bounds[obj_idx].0.min(obj_value);
697 bounds[obj_idx].1 = bounds[obj_idx].1.max(obj_value);
698 }
699 }
700 }
701
702 bounds
703 }
704
705 fn calculate_statistics(
707 &self,
708 all_solutions: &[MultiObjectiveSolution],
709 pareto_front: &[MultiObjectiveSolution],
710 runtime: Duration,
711 ) -> MultiObjectiveStats {
712 let unique_solutions = all_solutions.len();
713 let pareto_front_size = pareto_front.len();
714
715 let average_crowding_distance = if pareto_front.is_empty() {
716 0.0
717 } else {
718 pareto_front
719 .iter()
720 .map(|sol| sol.crowding_distance)
721 .filter(|&d| d.is_finite())
722 .sum::<f64>()
723 / pareto_front.len() as f64
724 };
725
726 let mut convergence_metrics = HashMap::new();
727 convergence_metrics.insert(
728 "pareto_ratio".to_string(),
729 pareto_front_size as f64 / unique_solutions.max(1) as f64,
730 );
731
732 MultiObjectiveStats {
733 total_runtime: runtime,
734 unique_solutions,
735 pareto_front_size,
736 average_crowding_distance,
737 hypervolume: None, convergence_metrics,
739 }
740 }
741}
742
743pub struct QualityMetrics;
745
746impl QualityMetrics {
747 pub fn hypervolume(
749 solutions: &[MultiObjectiveSolution],
750 reference_point: &[f64],
751 ) -> MultiObjectiveResult<f64> {
752 if solutions.is_empty() {
753 return Ok(0.0);
754 }
755
756 let num_objectives = solutions[0].objectives.len();
757 if reference_point.len() != num_objectives {
758 return Err(MultiObjectiveError::ParetoAnalysisError(
759 "Reference point dimension mismatch".to_string(),
760 ));
761 }
762
763 if num_objectives == 2 {
765 let mut sorted_solutions = solutions.to_vec();
766 sorted_solutions.sort_by(|a, b| {
767 a.objectives[0]
768 .partial_cmp(&b.objectives[0])
769 .unwrap_or(std::cmp::Ordering::Equal)
770 });
771
772 let mut volume = 0.0;
773 let mut prev_y = 0.0;
774
775 for solution in &sorted_solutions {
776 if solution.objectives[0] < reference_point[0]
777 && solution.objectives[1] < reference_point[1]
778 {
779 let width = reference_point[0] - solution.objectives[0];
780 let height = solution.objectives[1] - prev_y;
781 if height > 0.0 {
782 volume += width * height;
783 }
784 prev_y = solution.objectives[1];
785 }
786 }
787
788 Ok(volume)
789 } else {
790 Ok(0.0)
792 }
793 }
794
795 #[must_use]
797 pub fn spacing(solutions: &[MultiObjectiveSolution]) -> f64 {
798 if solutions.len() < 2 {
799 return 0.0;
800 }
801
802 let mut distances = Vec::new();
803
804 for (i, sol1) in solutions.iter().enumerate() {
805 let mut min_distance = f64::INFINITY;
806
807 for (j, sol2) in solutions.iter().enumerate() {
808 if i != j {
809 let distance: f64 = sol1
810 .objectives
811 .iter()
812 .zip(sol2.objectives.iter())
813 .map(|(a, b)| (a - b).abs())
814 .sum();
815
816 min_distance = min_distance.min(distance);
817 }
818 }
819
820 distances.push(min_distance);
821 }
822
823 let mean_distance = distances.iter().sum::<f64>() / distances.len() as f64;
824 let variance = distances
825 .iter()
826 .map(|d| (d - mean_distance).powi(2))
827 .sum::<f64>()
828 / distances.len() as f64;
829
830 variance.sqrt()
831 }
832}
833
834#[cfg(test)]
835mod tests {
836 use super::*;
837
838 #[test]
839 fn test_solution_dominance() {
840 let sol1 = MultiObjectiveSolution::new(vec![1, -1], vec![1.0, 2.0]);
841 let sol2 = MultiObjectiveSolution::new(vec![-1, 1], vec![2.0, 1.0]);
842 let sol3 = MultiObjectiveSolution::new(vec![1, 1], vec![0.5, 1.5]);
843
844 assert!(sol3.dominates(&sol1));
846 assert!(!sol3.dominates(&sol2));
848
849 assert!(!sol1.dominates(&sol2));
851 assert!(!sol2.dominates(&sol1));
852 }
853
854 #[test]
855 fn test_weighted_sum_scalarization() {
856 let weights = vec![0.3, 0.7];
857 let scalarization = ScalarizationMethod::WeightedSum { weights };
858
859 let objectives = vec![2.0, 4.0];
860 let result = scalarization
861 .scalarize(&objectives)
862 .expect("Scalarization failed");
863
864 assert_eq!(result, 0.3 * 2.0 + 0.7 * 4.0);
865 }
866
867 #[test]
868 fn test_chebyshev_scalarization() {
869 let weights = vec![1.0, 1.0];
870 let reference_point = vec![0.0, 0.0];
871 let scalarization = ScalarizationMethod::WeightedChebyshev {
872 weights,
873 reference_point,
874 };
875
876 let objectives = vec![3.0, 1.0];
877 let result = scalarization
878 .scalarize(&objectives)
879 .expect("Chebyshev scalarization failed");
880
881 assert_eq!(result, 3.0); }
883
884 #[test]
885 fn test_quality_metrics() {
886 let solutions = vec![
887 MultiObjectiveSolution::new(vec![1, -1], vec![1.0, 3.0]),
888 MultiObjectiveSolution::new(vec![-1, 1], vec![2.0, 1.0]),
889 MultiObjectiveSolution::new(vec![1, 1], vec![0.5, 2.0]),
890 ];
891
892 let spacing = QualityMetrics::spacing(&solutions);
893 assert!(spacing >= 0.0);
894
895 let reference_point = vec![4.0, 4.0];
896 let hypervolume = QualityMetrics::hypervolume(&solutions, &reference_point)
897 .expect("Hypervolume calculation failed");
898 assert!(hypervolume >= 0.0);
899 }
900}