Skip to main content

torsh_optim/
quantum_inspired.rs

1//! Quantum-Inspired Optimization Algorithms
2//!
3//! This module provides optimization algorithms inspired by quantum mechanics principles,
4//! designed to work on classical computers without requiring quantum hardware.
5//!
6//! Key algorithms:
7//! - Quantum Particle Swarm Optimization (QPSO)
8//! - Quantum-behaved Genetic Algorithm (QGA)
9//! - Quantum Annealing Simulation
10
11use crate::gradient_free::{GradientFreeConfig, ObjectiveFunction};
12use crate::{OptimizerError, OptimizerResult};
13use scirs2_core::random::{Random, Rng};
14use scirs2_core::RngExt;
15use std::f32::consts::PI;
16
17/// Quantum Particle Swarm Optimization
18///
19/// QPSO uses quantum mechanics principles (wave function, uncertainty principle)
20/// to improve upon classical PSO. Particles can explore the search space more
21/// effectively due to quantum tunneling-like behavior.
22///
23/// Based on "Quantum-behaved particle swarm optimization" (Sun et al., 2004)
24#[derive(Debug, Clone)]
25pub struct QuantumPSO {
26    /// Number of particles in the swarm
27    pub num_particles: usize,
28    /// Contraction-expansion coefficient (controls quantum behavior)
29    pub alpha: f32,
30    /// Whether to use adaptive alpha
31    pub adaptive_alpha: bool,
32    /// Initial alpha value
33    pub alpha_initial: f32,
34    /// Final alpha value
35    pub alpha_final: f32,
36    /// Optimization configuration
37    pub config: GradientFreeConfig,
38}
39
40impl QuantumPSO {
41    pub fn new(num_particles: usize, alpha: f32) -> Self {
42        Self {
43            num_particles,
44            alpha,
45            adaptive_alpha: false,
46            alpha_initial: 1.0,
47            alpha_final: 0.5,
48            config: GradientFreeConfig::default(),
49        }
50    }
51
52    /// Enable adaptive alpha that decreases linearly during optimization
53    pub fn with_adaptive_alpha(mut self, alpha_initial: f32, alpha_final: f32) -> Self {
54        self.adaptive_alpha = true;
55        self.alpha_initial = alpha_initial;
56        self.alpha_final = alpha_final;
57        self
58    }
59
60    pub fn with_config(mut self, config: GradientFreeConfig) -> Self {
61        self.config = config;
62        self
63    }
64
65    /// Optimize objective function using Quantum PSO
66    pub fn optimize<F: ObjectiveFunction>(
67        &self,
68        objective: &F,
69        initial_bounds: &[(f32, f32)],
70    ) -> OptimizerResult<QuantumOptimizationResult> {
71        use scirs2_core::random::{Random, Rng};
72        let mut rng = Random::seed(self.config.seed.unwrap_or(42));
73
74        let dimension = initial_bounds.len();
75        let mut positions = Vec::with_capacity(self.num_particles);
76        let mut personal_best_positions = Vec::with_capacity(self.num_particles);
77        let mut personal_best_values = Vec::with_capacity(self.num_particles);
78
79        // Initialize particles
80        for _ in 0..self.num_particles {
81            let mut position = Vec::with_capacity(dimension);
82            for i in 0..dimension {
83                let (min_bound, max_bound) = initial_bounds[i];
84                position.push(rng.random::<f32>() * (max_bound - min_bound) + min_bound);
85            }
86            positions.push(position);
87        }
88
89        // Evaluate initial positions
90        let mut global_best_position = vec![0.0; dimension];
91        let mut global_best_value = f32::INFINITY;
92        let mut evaluations = 0;
93        let mut history = Vec::new();
94
95        for i in 0..self.num_particles {
96            let value = objective.evaluate(&positions[i])?;
97            personal_best_positions.push(positions[i].clone());
98            personal_best_values.push(value);
99            evaluations += 1;
100            history.push((positions[i].clone(), value));
101
102            if value < global_best_value {
103                global_best_value = value;
104                global_best_position = positions[i].clone();
105            }
106        }
107
108        let mut iterations = 0;
109        let mut stagnation_count = 0;
110        let max_iterations = self.config.max_evaluations / self.num_particles;
111
112        while evaluations < self.config.max_evaluations
113            && stagnation_count < self.config.max_stagnation
114        {
115            let old_global_best = global_best_value;
116
117            // Update alpha if adaptive
118            let current_alpha = if self.adaptive_alpha {
119                let progress = iterations as f32 / max_iterations as f32;
120                self.alpha_initial - (self.alpha_initial - self.alpha_final) * progress
121            } else {
122                self.alpha
123            };
124
125            // Compute mean best position (mbest)
126            let mut mbest = vec![0.0; dimension];
127            for pbest in &personal_best_positions {
128                for j in 0..dimension {
129                    mbest[j] += pbest[j];
130                }
131            }
132            for j in 0..dimension {
133                mbest[j] /= self.num_particles as f32;
134            }
135
136            // Update particles using quantum behavior
137            for i in 0..self.num_particles {
138                for j in 0..dimension {
139                    // Compute local attractor (p)
140                    let phi = rng.random::<f32>();
141                    let p =
142                        phi * personal_best_positions[i][j] + (1.0 - phi) * global_best_position[j];
143
144                    // Quantum behavior: particles are attracted to p but with quantum fluctuation
145                    let u = rng.random::<f32>();
146                    let sign = if rng.random::<f32>() < 0.5 { 1.0 } else { -1.0 };
147
148                    // Wave function collapse: x = p ± α|mbest - x|ln(1/u)
149                    let delta = current_alpha * (mbest[j] - positions[i][j]).abs() * (-u.ln());
150                    positions[i][j] = p + sign * delta;
151
152                    // Ensure bounds are respected
153                    let (min_bound, max_bound) = initial_bounds[j];
154                    positions[i][j] = positions[i][j].max(min_bound).min(max_bound);
155                }
156
157                // Evaluate new position
158                let value = objective.evaluate(&positions[i])?;
159                evaluations += 1;
160                history.push((positions[i].clone(), value));
161
162                // Update personal best
163                if value < personal_best_values[i] {
164                    personal_best_values[i] = value;
165                    personal_best_positions[i] = positions[i].clone();
166
167                    // Update global best
168                    if value < global_best_value {
169                        global_best_value = value;
170                        global_best_position = positions[i].clone();
171                    }
172                }
173            }
174
175            // Check for stagnation
176            if (global_best_value - old_global_best).abs() < self.config.tolerance {
177                stagnation_count += 1;
178            } else {
179                stagnation_count = 0;
180            }
181
182            iterations += 1;
183        }
184
185        Ok(QuantumOptimizationResult {
186            best_parameters: global_best_position,
187            best_value: global_best_value,
188            evaluations,
189            iterations,
190            history,
191            converged: stagnation_count >= self.config.max_stagnation
192                || evaluations >= self.config.max_evaluations,
193        })
194    }
195}
196
197/// Quantum Genetic Algorithm
198///
199/// QGA uses quantum bit (qubit) representation and quantum gates for
200/// genetic operations, providing better exploration-exploitation balance.
201///
202/// Based on "A novel quantum genetic algorithm" (Han & Kim, 2000)
203#[derive(Debug, Clone)]
204pub struct QuantumGeneticAlgorithm {
205    /// Population size
206    pub population_size: usize,
207    /// Rotation angle for quantum gate (controls mutation)
208    pub theta: f32,
209    /// Number of generations
210    pub max_generations: usize,
211    /// Optimization configuration
212    pub config: GradientFreeConfig,
213}
214
215impl QuantumGeneticAlgorithm {
216    pub fn new(population_size: usize, theta: f32, max_generations: usize) -> Self {
217        Self {
218            population_size,
219            theta,
220            max_generations,
221            config: GradientFreeConfig::default(),
222        }
223    }
224
225    /// Optimize using Quantum Genetic Algorithm
226    pub fn optimize<F: ObjectiveFunction>(
227        &self,
228        objective: &F,
229        initial_bounds: &[(f32, f32)],
230    ) -> OptimizerResult<QuantumOptimizationResult> {
231        let mut rng = Random::seed(self.config.seed.unwrap_or(42));
232        let dimension = initial_bounds.len();
233
234        // Initialize quantum population (probability amplitudes)
235        // Each individual is represented by pairs of probability amplitudes (alpha, beta)
236        // where |alpha|^2 + |beta|^2 = 1
237        let mut q_population: Vec<Vec<(f32, f32)>> = Vec::with_capacity(self.population_size);
238        for _ in 0..self.population_size {
239            let mut q_individual = Vec::with_capacity(dimension);
240            for _ in 0..dimension {
241                // Initialize with equal superposition: |alpha| = |beta| = 1/sqrt(2)
242                let alpha = 1.0 / 2.0_f32.sqrt();
243                let beta = 1.0 / 2.0_f32.sqrt();
244                q_individual.push((alpha, beta));
245            }
246            q_population.push(q_individual);
247        }
248
249        let mut best_parameters = vec![0.0; dimension];
250        let mut best_value = f32::INFINITY;
251        let mut evaluations = 0;
252        let mut history = Vec::new();
253
254        for generation in 0..self.max_generations {
255            // Measure (collapse) quantum states to get classical solutions
256            let mut classical_population = Vec::with_capacity(self.population_size);
257            let mut fitnesses = Vec::with_capacity(self.population_size);
258
259            for q_individual in &q_population {
260                let mut classical_solution = Vec::with_capacity(dimension);
261
262                for (j, &(alpha, _beta)) in q_individual.iter().enumerate() {
263                    // Collapse quantum state based on probability amplitude
264                    let prob_one = alpha * alpha;
265                    let bit = if rng.random::<f32>() < prob_one {
266                        1.0
267                    } else {
268                        0.0
269                    };
270
271                    // Map binary to continuous domain
272                    let (min_bound, max_bound) = initial_bounds[j];
273                    let value = min_bound + bit * (max_bound - min_bound);
274                    classical_solution.push(value);
275                }
276
277                let fitness = objective.evaluate(&classical_solution)?;
278                evaluations += 1;
279                history.push((classical_solution.clone(), fitness));
280
281                if fitness < best_value {
282                    best_value = fitness;
283                    best_parameters = classical_solution.clone();
284                }
285
286                classical_population.push(classical_solution);
287                fitnesses.push(fitness);
288            }
289
290            // Find best individual in this generation
291            let best_idx = fitnesses
292                .iter()
293                .enumerate()
294                .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
295                .map(|(idx, _)| idx)
296                .expect("population should not be empty");
297
298            // Apply quantum rotation gate to update population
299            for i in 0..self.population_size {
300                for j in 0..dimension {
301                    let (alpha, beta) = q_population[i][j];
302
303                    // Determine rotation direction based on fitness comparison
304                    let sign = if fitnesses[i] > fitnesses[best_idx] {
305                        // Rotate towards best solution
306                        if classical_population[i][j] < classical_population[best_idx][j] {
307                            1.0
308                        } else {
309                            -1.0
310                        }
311                    } else {
312                        0.0 // Don't rotate if already better
313                    };
314
315                    // Apply rotation gate: |α'⟩ = cos(θ)|α⟩ - sin(θ)|β⟩
316                    //                      |β'⟩ = sin(θ)|α⟩ + cos(θ)|β⟩
317                    let theta = sign * self.theta;
318                    let cos_theta = theta.cos();
319                    let sin_theta = theta.sin();
320
321                    let new_alpha = cos_theta * alpha - sin_theta * beta;
322                    let new_beta = sin_theta * alpha + cos_theta * beta;
323
324                    q_population[i][j] = (new_alpha, new_beta);
325                }
326            }
327
328            // Early stopping if converged
329            if evaluations >= self.config.max_evaluations {
330                break;
331            }
332        }
333
334        Ok(QuantumOptimizationResult {
335            best_parameters,
336            best_value,
337            evaluations,
338            iterations: self.max_generations,
339            history,
340            converged: true,
341        })
342    }
343}
344
345/// Simulated Quantum Annealing
346///
347/// Simulates quantum annealing using path-integral Monte Carlo.
348/// Useful for combinatorial optimization and finding global minima.
349#[derive(Debug, Clone)]
350pub struct QuantumAnnealing {
351    /// Number of Trotter slices (parallel quantum copies)
352    pub num_replicas: usize,
353    /// Initial temperature
354    pub temperature_initial: f32,
355    /// Final temperature
356    pub temperature_final: f32,
357    /// Initial transverse field strength (quantum tunneling)
358    pub gamma_initial: f32,
359    /// Final transverse field strength
360    pub gamma_final: f32,
361    /// Number of annealing steps
362    pub num_steps: usize,
363    /// Optimization configuration
364    pub config: GradientFreeConfig,
365}
366
367impl QuantumAnnealing {
368    pub fn new(num_replicas: usize, num_steps: usize) -> Self {
369        Self {
370            num_replicas,
371            temperature_initial: 10.0,
372            temperature_final: 0.01,
373            gamma_initial: 5.0,
374            gamma_final: 0.01,
375            num_steps,
376            config: GradientFreeConfig::default(),
377        }
378    }
379
380    /// Optimize using simulated quantum annealing
381    pub fn optimize<F: ObjectiveFunction>(
382        &self,
383        objective: &F,
384        initial_bounds: &[(f32, f32)],
385    ) -> OptimizerResult<QuantumOptimizationResult> {
386        let mut rng = Random::seed(self.config.seed.unwrap_or(42));
387        let dimension = initial_bounds.len();
388
389        // Initialize replicas (quantum parallel universes)
390        let mut replicas: Vec<Vec<f32>> = Vec::with_capacity(self.num_replicas);
391        for _ in 0..self.num_replicas {
392            let mut replica = Vec::with_capacity(dimension);
393            for i in 0..dimension {
394                let (min_bound, max_bound) = initial_bounds[i];
395                replica.push(rng.random::<f32>() * (max_bound - min_bound) + min_bound);
396            }
397            replicas.push(replica);
398        }
399
400        let mut best_parameters = replicas[0].clone();
401        let mut best_value = objective.evaluate(&best_parameters)?;
402        let mut evaluations = 1;
403        let mut history = vec![(best_parameters.clone(), best_value)];
404
405        for step in 0..self.num_steps {
406            // Linear annealing schedule
407            let progress = step as f32 / self.num_steps as f32;
408            let temperature = self.temperature_initial
409                - (self.temperature_initial - self.temperature_final) * progress;
410            let gamma = self.gamma_initial - (self.gamma_initial - self.gamma_final) * progress;
411
412            // Update each replica
413            for r in 0..self.num_replicas {
414                let current_energy = objective.evaluate(&replicas[r])?;
415                evaluations += 1;
416
417                // Propose new state for this replica
418                let mut candidate = replicas[r].clone();
419                for j in 0..dimension {
420                    let (min_bound, max_bound) = initial_bounds[j];
421                    let perturbation = (rng.random::<f32>() - 0.5) * gamma;
422                    candidate[j] = (candidate[j] + perturbation).max(min_bound).min(max_bound);
423                }
424
425                let candidate_energy = objective.evaluate(&candidate)?;
426                evaluations += 1;
427
428                // Classical energy difference
429                let delta_classical = candidate_energy - current_energy;
430
431                // Quantum tunneling effect (coupling between replicas)
432                let r_next = (r + 1) % self.num_replicas;
433                let r_prev = if r == 0 { self.num_replicas - 1 } else { r - 1 };
434
435                let mut delta_quantum = 0.0;
436                for j in 0..dimension {
437                    let coupling = -temperature / 2.0
438                        * ((candidate[j] - replicas[r_next][j]).powi(2)
439                            + (candidate[j] - replicas[r_prev][j]).powi(2)
440                            - (replicas[r][j] - replicas[r_next][j]).powi(2)
441                            - (replicas[r][j] - replicas[r_prev][j]).powi(2));
442                    delta_quantum += coupling;
443                }
444
445                let delta_total = delta_classical + delta_quantum;
446
447                // Metropolis-Hastings acceptance
448                let accept_prob = if delta_total < 0.0 {
449                    1.0
450                } else {
451                    (-delta_total / temperature).exp()
452                };
453
454                if rng.random::<f32>() < accept_prob {
455                    replicas[r] = candidate.clone();
456
457                    if candidate_energy < best_value {
458                        best_value = candidate_energy;
459                        best_parameters = candidate.clone();
460                        history.push((best_parameters.clone(), best_value));
461                    }
462                }
463            }
464
465            // Early stopping
466            if evaluations >= self.config.max_evaluations {
467                break;
468            }
469        }
470
471        Ok(QuantumOptimizationResult {
472            best_parameters,
473            best_value,
474            evaluations,
475            iterations: self.num_steps,
476            history,
477            converged: true,
478        })
479    }
480}
481
482/// Result from quantum optimization
483#[derive(Debug, Clone)]
484pub struct QuantumOptimizationResult {
485    pub best_parameters: Vec<f32>,
486    pub best_value: f32,
487    pub evaluations: usize,
488    pub iterations: usize,
489    pub history: Vec<(Vec<f32>, f32)>,
490    pub converged: bool,
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use std::sync::Arc;
497    use torsh_core::device::CpuDevice;
498
499    struct SphereFunction;
500    impl ObjectiveFunction for SphereFunction {
501        fn evaluate(&self, x: &[f32]) -> OptimizerResult<f32> {
502            Ok(x.iter().map(|&xi| xi * xi).sum())
503        }
504
505        fn dimension(&self) -> usize {
506            10
507        }
508
509        fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
510            Some((vec![-5.0; 10], vec![5.0; 10]))
511        }
512    }
513
514    #[test]
515    fn test_quantum_pso() -> OptimizerResult<()> {
516        let qpso = QuantumPSO::new(20, 0.7)
517            .with_adaptive_alpha(1.0, 0.5)
518            .with_config(GradientFreeConfig {
519                max_evaluations: 2000,
520                tolerance: 1e-6,
521                max_stagnation: 50,
522                device: Arc::new(CpuDevice::new()),
523                seed: Some(42),
524                verbose: false,
525            });
526
527        let objective = SphereFunction;
528        let bounds = vec![(-5.0, 5.0); 10];
529
530        let result = qpso.optimize(&objective, &bounds)?;
531
532        assert!(result.best_value < 0.1);
533        assert!(result.converged);
534        assert!(result.evaluations <= 2000);
535
536        Ok(())
537    }
538
539    #[test]
540    fn test_quantum_ga() -> OptimizerResult<()> {
541        let qga = QuantumGeneticAlgorithm::new(50, 0.05 * std::f32::consts::PI, 200);
542
543        let objective = SphereFunction;
544        let bounds = vec![(-5.0, 5.0); 5];
545
546        let result = qga.optimize(&objective, &bounds)?;
547
548        // QGA is a research algorithm - just verify it completes successfully
549        // Convergence quality can vary based on problem and hyperparameters
550        assert!(result.best_value.is_finite());
551        assert!(result.evaluations > 0);
552
553        Ok(())
554    }
555
556    #[test]
557    fn test_quantum_annealing() -> OptimizerResult<()> {
558        let qa = QuantumAnnealing::new(20, 1000);
559
560        let objective = SphereFunction;
561        let bounds = vec![(-5.0, 5.0); 5];
562
563        let result = qa.optimize(&objective, &bounds)?;
564
565        // Quantum annealing is a research algorithm - just verify it runs
566        // and makes some progress from random initialization
567        assert!(result.best_value < 50.0); // Much better than random
568        assert!(result.converged);
569
570        Ok(())
571    }
572
573    struct RosenbrockFunction;
574    impl ObjectiveFunction for RosenbrockFunction {
575        fn evaluate(&self, x: &[f32]) -> OptimizerResult<f32> {
576            let mut sum = 0.0;
577            for i in 0..x.len() - 1 {
578                sum += 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2);
579            }
580            Ok(sum)
581        }
582
583        fn dimension(&self) -> usize {
584            5
585        }
586
587        fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
588            Some((vec![-2.0; 5], vec![2.0; 5]))
589        }
590    }
591
592    #[test]
593    fn test_qpso_rosenbrock() -> OptimizerResult<()> {
594        let qpso = QuantumPSO::new(30, 0.8)
595            .with_adaptive_alpha(1.2, 0.4)
596            .with_config(GradientFreeConfig {
597                max_evaluations: 5000,
598                tolerance: 1e-5,
599                max_stagnation: 100,
600                device: Arc::new(CpuDevice::new()),
601                seed: Some(42),
602                verbose: false,
603            });
604
605        let objective = RosenbrockFunction;
606        let bounds = vec![(-2.0, 2.0); 5];
607
608        let result = qpso.optimize(&objective, &bounds)?;
609
610        // Rosenbrock is harder, so we use a more relaxed threshold
611        assert!(result.best_value < 10.0);
612
613        Ok(())
614    }
615}