1use crate::sampler::{SampleResult, Sampler};
33use quantrs2_anneal::QuboModel;
34use scirs2_core::ndarray::{Array1, Array2};
35use scirs2_core::parallel_ops;
36use scirs2_core::random::prelude::*;
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39use std::fmt;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum ObjectiveDirection {
44 Minimize,
46 Maximize,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Objective {
53 pub name: String,
55 pub direction: ObjectiveDirection,
57 pub weight: f64,
59 pub qubo_matrix: Option<Array2<f64>>,
61}
62
63impl Objective {
64 pub fn new(name: impl Into<String>, direction: ObjectiveDirection, weight: f64) -> Self {
66 Self {
67 name: name.into(),
68 direction,
69 weight,
70 qubo_matrix: None,
71 }
72 }
73
74 pub fn with_qubo(mut self, qubo_matrix: Array2<f64>) -> Self {
76 self.qubo_matrix = Some(qubo_matrix);
77 self
78 }
79
80 pub fn evaluate(&self, solution: &HashMap<String, bool>) -> f64 {
82 if let Some(ref matrix) = self.qubo_matrix {
83 let n = matrix.nrows();
85 let mut energy = 0.0;
86
87 for i in 0..n {
88 for j in 0..n {
89 let x_i = if solution.get(&format!("x{i}")).copied().unwrap_or(false) {
90 1.0
91 } else {
92 0.0
93 };
94 let x_j = if solution.get(&format!("x{j}")).copied().unwrap_or(false) {
95 1.0
96 } else {
97 0.0
98 };
99 energy += matrix[[i, j]] * x_i * x_j;
100 }
101 }
102
103 match self.direction {
104 ObjectiveDirection::Minimize => energy,
105 ObjectiveDirection::Maximize => -energy,
106 }
107 } else {
108 0.0
109 }
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct MultiObjectiveSolution {
116 pub assignments: HashMap<String, bool>,
118 pub objective_values: Vec<f64>,
120 pub rank: usize,
122 pub crowding_distance: f64,
124}
125
126impl MultiObjectiveSolution {
127 pub fn dominates(&self, other: &Self) -> bool {
129 let mut at_least_one_better = false;
130
131 for (a, b) in self.objective_values.iter().zip(&other.objective_values) {
132 if a > b {
133 return false; }
135 if a < b {
136 at_least_one_better = true;
137 }
138 }
139
140 at_least_one_better
141 }
142
143 pub fn distance_to(&self, other: &Self) -> f64 {
145 self.objective_values
146 .iter()
147 .zip(&other.objective_values)
148 .map(|(a, b)| (a - b).powi(2))
149 .sum::<f64>()
150 .sqrt()
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156pub enum ScalarizationMethod {
157 WeightedSum,
159 EpsilonConstraint,
161 Tchebycheff,
163 AugmentedTchebycheff,
165 AchievementFunction,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct MultiObjectiveConfig {
172 pub population_size: usize,
174 pub max_generations: usize,
176 pub scalarization: ScalarizationMethod,
178 pub crossover_prob: f64,
180 pub mutation_prob: f64,
182 pub num_reference_points: usize,
184 pub parallel: bool,
186}
187
188impl Default for MultiObjectiveConfig {
189 fn default() -> Self {
190 Self {
191 population_size: 100,
192 max_generations: 100,
193 scalarization: ScalarizationMethod::WeightedSum,
194 crossover_prob: 0.9,
195 mutation_prob: 0.1,
196 num_reference_points: 10,
197 parallel: true,
198 }
199 }
200}
201
202pub struct MultiObjectiveOptimizer {
204 objectives: Vec<Objective>,
206 config: MultiObjectiveConfig,
208 population: Vec<MultiObjectiveSolution>,
210 pareto_front: Vec<MultiObjectiveSolution>,
212 rng: Box<dyn RngCore>,
214}
215
216impl MultiObjectiveOptimizer {
217 pub fn new(objectives: Vec<Objective>, config: MultiObjectiveConfig) -> Self {
219 Self {
220 objectives,
221 config,
222 population: Vec::new(),
223 pareto_front: Vec::new(),
224 rng: Box::new(thread_rng()),
225 }
226 }
227
228 pub fn initialize_population(&mut self, num_variables: usize) {
230 self.population.clear();
231
232 for _ in 0..self.config.population_size {
233 let mut assignments = HashMap::new();
234
235 for i in 0..num_variables {
236 assignments.insert(format!("x{i}"), self.rng.gen::<bool>());
237 }
238
239 let objective_values = self.evaluate_objectives(&assignments);
240
241 self.population.push(MultiObjectiveSolution {
242 assignments,
243 objective_values,
244 rank: 0,
245 crowding_distance: 0.0,
246 });
247 }
248
249 self.compute_pareto_ranks();
250 self.compute_crowding_distances();
251 }
252
253 fn evaluate_objectives(&self, solution: &HashMap<String, bool>) -> Vec<f64> {
255 self.objectives
256 .iter()
257 .map(|obj| obj.evaluate(solution))
258 .collect()
259 }
260
261 fn compute_pareto_ranks(&mut self) {
263 let n = self.population.len();
264 let mut domination_count = vec![0; n];
265 let mut dominated_solutions: Vec<Vec<usize>> = vec![Vec::new(); n];
266 let mut fronts: Vec<Vec<usize>> = Vec::new();
267
268 for i in 0..n {
270 for j in 0..n {
271 if i == j {
272 continue;
273 }
274
275 if self.population[i].dominates(&self.population[j]) {
276 dominated_solutions[i].push(j);
277 } else if self.population[j].dominates(&self.population[i]) {
278 domination_count[i] += 1;
279 }
280 }
281
282 if domination_count[i] == 0 {
283 self.population[i].rank = 0;
284 if fronts.is_empty() {
285 fronts.push(Vec::new());
286 }
287 fronts[0].push(i);
288 }
289 }
290
291 let mut current_front = 0;
293 while current_front < fronts.len() && !fronts[current_front].is_empty() {
294 let mut next_front = Vec::new();
295
296 for &i in &fronts[current_front] {
297 for &j in &dominated_solutions[i] {
298 domination_count[j] -= 1;
299 if domination_count[j] == 0 {
300 self.population[j].rank = current_front + 1;
301 next_front.push(j);
302 }
303 }
304 }
305
306 if !next_front.is_empty() {
307 fronts.push(next_front);
308 }
309 current_front += 1;
310 }
311
312 if !fronts.is_empty() && !fronts[0].is_empty() {
314 self.pareto_front = fronts[0]
315 .iter()
316 .map(|&i| self.population[i].clone())
317 .collect();
318 }
319 }
320
321 fn compute_crowding_distances(&mut self) {
323 let n = self.population.len();
324 let m = self.objectives.len();
325
326 for sol in &mut self.population {
328 sol.crowding_distance = 0.0;
329 }
330
331 for obj_idx in 0..m {
333 let mut indices: Vec<usize> = (0..n).collect();
335 indices.sort_by(|&a, &b| {
336 self.population[a].objective_values[obj_idx]
337 .partial_cmp(&self.population[b].objective_values[obj_idx])
338 .unwrap_or(std::cmp::Ordering::Equal)
339 });
340
341 self.population[indices[0]].crowding_distance = f64::INFINITY;
343 self.population[indices[n - 1]].crowding_distance = f64::INFINITY;
344
345 let f_max = self.population[indices[n - 1]].objective_values[obj_idx];
347 let f_min = self.population[indices[0]].objective_values[obj_idx];
348 let range = f_max - f_min;
349
350 if range > 1e-10 {
351 for i in 1..n - 1 {
352 let idx = indices[i];
353 let f_next = self.population[indices[i + 1]].objective_values[obj_idx];
354 let f_prev = self.population[indices[i - 1]].objective_values[obj_idx];
355
356 self.population[idx].crowding_distance += (f_next - f_prev) / range;
357 }
358 }
359 }
360 }
361
362 fn tournament_selection(&mut self) -> usize {
364 let i = self.rng.gen_range(0..self.population.len());
365 let j = self.rng.gen_range(0..self.population.len());
366
367 if self.population[i].rank < self.population[j].rank {
368 i
369 } else if self.population[i].rank > self.population[j].rank {
370 j
371 } else if self.population[i].crowding_distance > self.population[j].crowding_distance {
372 i
373 } else {
374 j
375 }
376 }
377
378 fn crossover(
380 &mut self,
381 parent1: &MultiObjectiveSolution,
382 parent2: &MultiObjectiveSolution,
383 ) -> (MultiObjectiveSolution, MultiObjectiveSolution) {
384 if self.rng.gen::<f64>() > self.config.crossover_prob {
385 return (parent1.clone(), parent2.clone());
386 }
387
388 let mut child1_assignments = parent1.assignments.clone();
389 let mut child2_assignments = parent2.assignments.clone();
390
391 for key in parent1.assignments.keys() {
393 if self.rng.gen::<bool>() {
394 if let Some(&val) = parent2.assignments.get(key) {
395 child1_assignments.insert(key.clone(), val);
396 }
397 if let Some(&val) = parent1.assignments.get(key) {
398 child2_assignments.insert(key.clone(), val);
399 }
400 }
401 }
402
403 let child1 = MultiObjectiveSolution {
404 assignments: child1_assignments,
405 objective_values: Vec::new(),
406 rank: 0,
407 crowding_distance: 0.0,
408 };
409
410 let child2 = MultiObjectiveSolution {
411 assignments: child2_assignments,
412 objective_values: Vec::new(),
413 rank: 0,
414 crowding_distance: 0.0,
415 };
416
417 (child1, child2)
418 }
419
420 fn mutate(&mut self, solution: &mut MultiObjectiveSolution) {
422 for value in solution.assignments.values_mut() {
423 if self.rng.gen::<f64>() < self.config.mutation_prob {
424 *value = !*value;
425 }
426 }
427 }
428
429 pub fn optimize_nsga2(
431 &mut self,
432 num_variables: usize,
433 ) -> Result<Vec<MultiObjectiveSolution>, String> {
434 self.initialize_population(num_variables);
435
436 for generation in 0..self.config.max_generations {
437 let mut offspring = Vec::new();
438
439 while offspring.len() < self.config.population_size {
441 let parent1_idx = self.tournament_selection();
442 let parent2_idx = self.tournament_selection();
443
444 let parent1 = self.population[parent1_idx].clone();
446 let parent2 = self.population[parent2_idx].clone();
447
448 let (mut child1, mut child2) = self.crossover(&parent1, &parent2);
449
450 self.mutate(&mut child1);
451 self.mutate(&mut child2);
452
453 child1.objective_values = self.evaluate_objectives(&child1.assignments);
455 child2.objective_values = self.evaluate_objectives(&child2.assignments);
456
457 offspring.push(child1);
458 if offspring.len() < self.config.population_size {
459 offspring.push(child2);
460 }
461 }
462
463 self.population.extend(offspring);
465
466 self.compute_pareto_ranks();
468 self.compute_crowding_distances();
469
470 self.population.sort_by(|a, b| match a.rank.cmp(&b.rank) {
472 std::cmp::Ordering::Equal => b
473 .crowding_distance
474 .partial_cmp(&a.crowding_distance)
475 .unwrap_or(std::cmp::Ordering::Equal),
476 other => other,
477 });
478
479 self.population.truncate(self.config.population_size);
480
481 if generation % 10 == 0 {
482 println!(
483 "Generation {}: Pareto front size = {}",
484 generation,
485 self.pareto_front.len()
486 );
487 }
488 }
489
490 Ok(self.pareto_front.clone())
491 }
492
493 pub fn get_pareto_front(&self) -> &[MultiObjectiveSolution] {
495 &self.pareto_front
496 }
497
498 pub fn scalarize(&self, objective_values: &[f64], weights: &[f64]) -> f64 {
500 match self.config.scalarization {
501 ScalarizationMethod::WeightedSum => objective_values
502 .iter()
503 .zip(weights)
504 .map(|(val, weight)| val * weight)
505 .sum(),
506 ScalarizationMethod::Tchebycheff => {
507 let mut max_weighted = f64::NEG_INFINITY;
508 for (val, weight) in objective_values.iter().zip(weights) {
509 let weighted = weight * val;
510 if weighted > max_weighted {
511 max_weighted = weighted;
512 }
513 }
514 max_weighted
515 }
516 ScalarizationMethod::AugmentedTchebycheff => {
517 let rho = 0.00001;
518 let max_weighted = objective_values
519 .iter()
520 .zip(weights)
521 .map(|(val, weight)| weight * val)
522 .fold(f64::NEG_INFINITY, f64::max);
523 let sum: f64 = objective_values.iter().sum();
524 max_weighted + rho * sum
525 }
526 _ => objective_values
527 .iter()
528 .zip(weights)
529 .map(|(val, weight)| val * weight)
530 .sum(),
531 }
532 }
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct MultiObjectiveResult {
538 pub pareto_solutions: Vec<MultiObjectiveSolution>,
540 pub statistics: OptimizationStatistics,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct OptimizationStatistics {
547 pub num_evaluations: usize,
549 pub runtime_seconds: f64,
551 pub pareto_front_size: usize,
553 pub hypervolume: f64,
555 pub spacing: f64,
557}
558
559impl OptimizationStatistics {
560 pub fn compute_hypervolume(
562 solutions: &[MultiObjectiveSolution],
563 reference_point: &[f64],
564 ) -> f64 {
565 if solutions.is_empty() || reference_point.len() != 2 {
567 return 0.0;
568 }
569
570 let mut sorted_solutions: Vec<_> = solutions.to_vec();
571 sorted_solutions.sort_by(|a, b| {
572 a.objective_values[0]
573 .partial_cmp(&b.objective_values[0])
574 .unwrap_or(std::cmp::Ordering::Equal)
575 });
576
577 let mut hypervolume = 0.0;
578 let mut prev_y = reference_point[1];
579
580 for sol in &sorted_solutions {
581 let width = reference_point[0] - sol.objective_values[0];
582 let height = prev_y - sol.objective_values[1];
583
584 if width > 0.0 && height > 0.0 {
585 hypervolume += width * height;
586 prev_y = sol.objective_values[1];
587 }
588 }
589
590 hypervolume
591 }
592
593 pub fn compute_spacing(solutions: &[MultiObjectiveSolution]) -> f64 {
595 if solutions.len() < 2 {
596 return 0.0;
597 }
598
599 let mut distances = Vec::new();
600
601 for i in 0..solutions.len() {
602 let mut min_distance = f64::INFINITY;
603
604 for j in 0..solutions.len() {
605 if i != j {
606 let dist = solutions[i].distance_to(&solutions[j]);
607 if dist < min_distance {
608 min_distance = dist;
609 }
610 }
611 }
612
613 distances.push(min_distance);
614 }
615
616 let mean: f64 = distances.iter().sum::<f64>() / distances.len() as f64;
617 let variance: f64 =
618 distances.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / distances.len() as f64;
619
620 variance.sqrt()
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn test_objective_creation() {
630 let obj = Objective::new("test", ObjectiveDirection::Minimize, 1.0);
631 assert_eq!(obj.name, "test");
632 assert_eq!(obj.direction, ObjectiveDirection::Minimize);
633 assert_eq!(obj.weight, 1.0);
634 }
635
636 #[test]
637 fn test_dominance() {
638 let sol1 = MultiObjectiveSolution {
639 assignments: HashMap::new(),
640 objective_values: vec![1.0, 2.0],
641 rank: 0,
642 crowding_distance: 0.0,
643 };
644
645 let sol2 = MultiObjectiveSolution {
646 assignments: HashMap::new(),
647 objective_values: vec![2.0, 3.0],
648 rank: 0,
649 crowding_distance: 0.0,
650 };
651
652 assert!(sol1.dominates(&sol2));
653 assert!(!sol2.dominates(&sol1));
654 }
655
656 #[test]
657 fn test_multi_objective_optimizer_initialization() {
658 let objectives = vec![
659 Objective::new("obj1", ObjectiveDirection::Minimize, 1.0),
660 Objective::new("obj2", ObjectiveDirection::Minimize, 1.0),
661 ];
662
663 let config = MultiObjectiveConfig::default();
664 let mut optimizer = MultiObjectiveOptimizer::new(objectives, config);
665
666 optimizer.initialize_population(10);
667 assert_eq!(optimizer.population.len(), 100);
668 }
669
670 #[test]
671 fn test_scalarization() {
672 let objectives = vec![
673 Objective::new("obj1", ObjectiveDirection::Minimize, 0.5),
674 Objective::new("obj2", ObjectiveDirection::Minimize, 0.5),
675 ];
676
677 let mut config = MultiObjectiveConfig::default();
678 config.scalarization = ScalarizationMethod::WeightedSum;
679
680 let optimizer = MultiObjectiveOptimizer::new(objectives, config);
681
682 let values = vec![1.0, 2.0];
683 let weights = vec![0.5, 0.5];
684 let result = optimizer.scalarize(&values, &weights);
685
686 assert!((result - 1.5).abs() < 1e-10);
687 }
688
689 #[test]
690 fn test_hypervolume_computation() {
691 let solutions = vec![
692 MultiObjectiveSolution {
693 assignments: HashMap::new(),
694 objective_values: vec![1.0, 3.0],
695 rank: 0,
696 crowding_distance: 0.0,
697 },
698 MultiObjectiveSolution {
699 assignments: HashMap::new(),
700 objective_values: vec![2.0, 2.0],
701 rank: 0,
702 crowding_distance: 0.0,
703 },
704 ];
705
706 let reference = vec![4.0, 4.0];
707 let hv = OptimizationStatistics::compute_hypervolume(&solutions, &reference);
708
709 assert!(hv > 0.0);
710 }
711}