quantrs2_tytan/
multi_objective_optimization.rs

1//! Multi-Objective QUBO Optimization
2//!
3//! This module implements advanced multi-objective optimization algorithms for QUBO problems,
4//! allowing users to optimize multiple conflicting objectives simultaneously and explore
5//! Pareto frontiers.
6//!
7//! # Features
8//!
9//! - Pareto frontier computation
10//! - Multiple scalarization methods (weighted sum, epsilon-constraint, Tchebycheff)
11//! - Evolutionary multi-objective algorithms (NSGA-II, MOEA/D)
12//! - Interactive objective exploration
13//! - Trade-off visualization and analysis
14//!
15//! # Examples
16//!
17//! ```rust
18//! use quantrs2_tytan::multi_objective_optimization::*;
19//! use std::collections::HashMap;
20//!
21//! // Define multiple objectives
22//! let objectives = vec![
23//!     Objective::new("cost", ObjectiveDirection::Minimize, 1.0),
24//!     Objective::new("quality", ObjectiveDirection::Maximize, 1.0),
25//! ];
26//!
27//! // Create multi-objective optimizer
28//! let config = MultiObjectiveConfig::default();
29//! let mut optimizer = MultiObjectiveOptimizer::new(objectives, config);
30//! ```
31
32use 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/// Objective direction
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum ObjectiveDirection {
44    /// Minimize the objective
45    Minimize,
46    /// Maximize the objective
47    Maximize,
48}
49
50/// Objective definition
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Objective {
53    /// Objective name
54    pub name: String,
55    /// Optimization direction
56    pub direction: ObjectiveDirection,
57    /// Weight for scalarization
58    pub weight: f64,
59    /// QUBO matrix for this objective
60    pub qubo_matrix: Option<Array2<f64>>,
61}
62
63impl Objective {
64    /// Create a new objective
65    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    /// Set QUBO matrix for this objective
75    pub fn with_qubo(mut self, qubo_matrix: Array2<f64>) -> Self {
76        self.qubo_matrix = Some(qubo_matrix);
77        self
78    }
79
80    /// Evaluate this objective for a given solution
81    pub fn evaluate(&self, solution: &HashMap<String, bool>) -> f64 {
82        if let Some(ref matrix) = self.qubo_matrix {
83            // Compute QUBO energy
84            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/// A solution in objective space
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct MultiObjectiveSolution {
116    /// Variable assignments
117    pub assignments: HashMap<String, bool>,
118    /// Objective values
119    pub objective_values: Vec<f64>,
120    /// Dominance rank (lower is better)
121    pub rank: usize,
122    /// Crowding distance (higher is better for diversity)
123    pub crowding_distance: f64,
124}
125
126impl MultiObjectiveSolution {
127    /// Check if this solution dominates another
128    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; // Worse in at least one objective (assuming minimization)
134            }
135            if a < b {
136                at_least_one_better = true;
137            }
138        }
139
140        at_least_one_better
141    }
142
143    /// Compute Euclidean distance in objective space
144    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/// Scalarization method for multi-objective optimization
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156pub enum ScalarizationMethod {
157    /// Weighted sum of objectives
158    WeightedSum,
159    /// Epsilon-constraint method
160    EpsilonConstraint,
161    /// Tchebycheff method
162    Tchebycheff,
163    /// Augmented Tchebycheff
164    AugmentedTchebycheff,
165    /// Achievement scalarizing function
166    AchievementFunction,
167}
168
169/// Multi-objective optimization configuration
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct MultiObjectiveConfig {
172    /// Population size for evolutionary algorithms
173    pub population_size: usize,
174    /// Number of generations
175    pub max_generations: usize,
176    /// Scalarization method
177    pub scalarization: ScalarizationMethod,
178    /// Crossover probability
179    pub crossover_prob: f64,
180    /// Mutation probability
181    pub mutation_prob: f64,
182    /// Number of reference points for decomposition
183    pub num_reference_points: usize,
184    /// Enable parallel evaluation
185    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
202/// Multi-objective optimizer
203pub struct MultiObjectiveOptimizer {
204    /// Objectives to optimize
205    objectives: Vec<Objective>,
206    /// Configuration
207    config: MultiObjectiveConfig,
208    /// Current population
209    population: Vec<MultiObjectiveSolution>,
210    /// Pareto front
211    pareto_front: Vec<MultiObjectiveSolution>,
212    /// Random number generator
213    rng: Box<dyn RngCore>,
214}
215
216impl MultiObjectiveOptimizer {
217    /// Create a new multi-objective optimizer
218    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    /// Initialize population randomly
229    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    /// Evaluate all objectives for a solution
254    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    /// Compute Pareto ranks using fast non-dominated sorting
262    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        // Count dominations
269        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        // Build subsequent fronts
292        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        // Update Pareto front
313        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    /// Compute crowding distances for diversity preservation
322    fn compute_crowding_distances(&mut self) {
323        let n = self.population.len();
324        let m = self.objectives.len();
325
326        // Initialize distances to zero
327        for sol in &mut self.population {
328            sol.crowding_distance = 0.0;
329        }
330
331        // For each objective
332        for obj_idx in 0..m {
333            // Sort by objective value
334            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            // Boundary solutions get infinite distance
342            self.population[indices[0]].crowding_distance = f64::INFINITY;
343            self.population[indices[n - 1]].crowding_distance = f64::INFINITY;
344
345            // Compute range
346            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    /// Perform binary tournament selection
363    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    /// Perform crossover
379    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        // Uniform crossover
392        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    /// Perform mutation
421    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    /// Run NSGA-II algorithm
430    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            // Generate offspring
440            while offspring.len() < self.config.population_size {
441                let parent1_idx = self.tournament_selection();
442                let parent2_idx = self.tournament_selection();
443
444                // Clone parents to avoid borrowing issues
445                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                // Evaluate offspring
454                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            // Combine parent and offspring populations
464            self.population.extend(offspring);
465
466            // Non-dominated sorting
467            self.compute_pareto_ranks();
468            self.compute_crowding_distances();
469
470            // Environmental selection
471            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    /// Get the Pareto front
494    pub fn get_pareto_front(&self) -> &[MultiObjectiveSolution] {
495        &self.pareto_front
496    }
497
498    /// Scalarize multiple objectives into a single objective
499    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/// Result of multi-objective optimization
536#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct MultiObjectiveResult {
538    /// Pareto-optimal solutions
539    pub pareto_solutions: Vec<MultiObjectiveSolution>,
540    /// Statistics
541    pub statistics: OptimizationStatistics,
542}
543
544/// Statistics from multi-objective optimization
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct OptimizationStatistics {
547    /// Total number of evaluations
548    pub num_evaluations: usize,
549    /// Total runtime in seconds
550    pub runtime_seconds: f64,
551    /// Size of final Pareto front
552    pub pareto_front_size: usize,
553    /// Hypervolume indicator
554    pub hypervolume: f64,
555    /// Spacing metric
556    pub spacing: f64,
557}
558
559impl OptimizationStatistics {
560    /// Compute hypervolume for the Pareto front
561    pub fn compute_hypervolume(
562        solutions: &[MultiObjectiveSolution],
563        reference_point: &[f64],
564    ) -> f64 {
565        // Simplified 2D hypervolume computation
566        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    /// Compute spacing metric for distribution quality
594    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}