quantrs2_tytan/
grover_amplitude_amplification.rs

1//! Grover-Inspired Amplitude Amplification for QUBO Optimization
2//!
3//! This module implements a quantum-inspired classical algorithm that uses
4//! Grover's amplitude amplification concepts to boost the probability of
5//! finding optimal solutions to QUBO problems.
6//!
7//! # Theory
8//!
9//! Grover's algorithm amplifies the amplitude of target states through:
10//! 1. Oracle marking (inverting phase of target states)
11//! 2. Diffusion operator (inversion about average)
12//!
13//! For classical QUBO optimization, we adapt this by:
14//! - Using energy-based marking (lower energy = higher weight)
15//! - Employing iterative refinement with population diversity
16//! - Applying amplitude-inspired probability distributions
17
18use crate::sampler::{SampleResult, Sampler, SamplerError, SamplerResult};
19use scirs2_core::ndarray::{Array1, Array2, ArrayD};
20use scirs2_core::random::prelude::*;
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23
24/// Configuration for Grover-inspired amplitude amplification
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct GroverAmplificationConfig {
27    /// Number of Grover-like iterations
28    pub grover_iterations: usize,
29    /// Population size for each iteration
30    pub population_size: usize,
31    /// Number of elite solutions to preserve
32    pub elite_count: usize,
33    /// Temperature for probabilistic selection
34    pub temperature: f64,
35    /// Amplification factor (analogous to quantum amplitude boost)
36    pub amplification_factor: f64,
37    /// Enable adaptive amplification
38    pub adaptive_amplification: bool,
39    /// Diversity preservation weight
40    pub diversity_weight: f64,
41}
42
43impl Default for GroverAmplificationConfig {
44    fn default() -> Self {
45        Self {
46            grover_iterations: 20,
47            population_size: 100,
48            elite_count: 10,
49            temperature: 1.0,
50            amplification_factor: 2.0,
51            adaptive_amplification: true,
52            diversity_weight: 0.1,
53        }
54    }
55}
56
57/// State representation in the amplitude amplification process
58#[derive(Debug, Clone)]
59struct AmplitudeState {
60    /// Binary state vector
61    state: Vec<bool>,
62    /// Energy of the state
63    energy: f64,
64    /// Amplitude (probability weight)
65    amplitude: f64,
66    /// Hamming distance to best known solution
67    distance_to_best: usize,
68}
69
70/// Grover-inspired QUBO solver
71#[derive(Debug, Clone)]
72pub struct GroverAmplifiedSolver {
73    config: GroverAmplificationConfig,
74    /// Best solution found so far
75    best_solution: Option<Vec<bool>>,
76    /// Best energy found
77    best_energy: f64,
78    /// History of energy improvements
79    energy_history: Vec<f64>,
80}
81
82impl GroverAmplifiedSolver {
83    /// Create a new Grover-amplified solver
84    pub const fn new(config: GroverAmplificationConfig) -> Self {
85        Self {
86            config,
87            best_solution: None,
88            best_energy: f64::INFINITY,
89            energy_history: Vec::new(),
90        }
91    }
92
93    /// Initialize population with random states
94    fn initialize_population(&self, n_vars: usize, qubo: &Array2<f64>) -> Vec<AmplitudeState> {
95        let mut rng = thread_rng();
96        let mut population = Vec::new();
97
98        for _ in 0..self.config.population_size {
99            let state: Vec<bool> = (0..n_vars).map(|_| rng.gen_bool(0.5)).collect();
100            let energy = self.compute_energy(&state, qubo);
101
102            population.push(AmplitudeState {
103                state,
104                energy,
105                amplitude: 1.0 / self.config.population_size as f64,
106                distance_to_best: 0,
107            });
108        }
109
110        population
111    }
112
113    /// Compute QUBO energy for a state
114    fn compute_energy(&self, state: &[bool], qubo: &Array2<f64>) -> f64 {
115        let n = state.len();
116        let mut energy = 0.0;
117
118        for i in 0..n {
119            if state[i] {
120                energy += qubo[[i, i]];
121            }
122            for j in (i + 1)..n {
123                if state[i] && state[j] {
124                    energy += qubo[[i, j]];
125                }
126            }
127        }
128
129        energy
130    }
131
132    /// Oracle operation: mark good solutions (low energy)
133    fn oracle_marking(&self, population: &mut [AmplitudeState]) {
134        // Find energy range
135        let min_energy = population
136            .iter()
137            .map(|s| s.energy)
138            .fold(f64::INFINITY, f64::min);
139        let max_energy = population
140            .iter()
141            .map(|s| s.energy)
142            .fold(f64::NEG_INFINITY, f64::max);
143
144        let energy_range = max_energy - min_energy;
145
146        // Mark states based on energy (lower energy = higher marking)
147        for state in population.iter_mut() {
148            let normalized_energy = if energy_range > 1e-10 {
149                (state.energy - min_energy) / energy_range
150            } else {
151                0.5
152            };
153
154            // Oracle marks good states (inverts phase in quantum case,
155            // here we boost amplitude)
156            let marking_factor = (-normalized_energy * self.config.amplification_factor).exp();
157            state.amplitude *= marking_factor;
158        }
159    }
160
161    /// Diffusion operation: inversion about average
162    fn diffusion_operator(&self, population: &mut [AmplitudeState]) {
163        // Compute average amplitude
164        let avg_amplitude: f64 =
165            population.iter().map(|s| s.amplitude).sum::<f64>() / population.len() as f64;
166
167        // Invert about average (2*avg - amplitude)
168        for state in population.iter_mut() {
169            state.amplitude = 2.0f64.mul_add(avg_amplitude, -state.amplitude);
170            state.amplitude = state.amplitude.max(0.0); // Keep non-negative
171        }
172
173        // Renormalize amplitudes
174        let total: f64 = population.iter().map(|s| s.amplitude).sum();
175        if total > 1e-10 {
176            for state in population.iter_mut() {
177                state.amplitude /= total;
178            }
179        }
180    }
181
182    /// Sample new states based on amplitudes (quantum measurement analogue)
183    fn amplitude_sampling(
184        &mut self,
185        population: &[AmplitudeState],
186        n_vars: usize,
187        qubo: &Array2<f64>,
188    ) -> Vec<AmplitudeState> {
189        let mut rng = thread_rng();
190        let mut new_population = Vec::new();
191
192        // Keep elite solutions
193        let mut sorted_pop = population.to_vec();
194        sorted_pop.sort_by(|a, b| {
195            a.energy
196                .partial_cmp(&b.energy)
197                .unwrap_or(std::cmp::Ordering::Equal)
198        });
199
200        for state in sorted_pop.iter().take(self.config.elite_count) {
201            new_population.push(state.clone());
202        }
203
204        // Sample remaining based on amplitudes
205        let cumulative: Vec<f64> = population
206            .iter()
207            .scan(0.0, |acc, s| {
208                *acc += s.amplitude;
209                Some(*acc)
210            })
211            .collect();
212
213        while new_population.len() < self.config.population_size {
214            // Sample parent based on amplitude
215            let r: f64 = rng.gen();
216            let parent_idx = cumulative
217                .iter()
218                .position(|&c| r <= c)
219                .unwrap_or(population.len() - 1);
220
221            // Create offspring with mutations
222            let mut offspring = population[parent_idx].state.clone();
223
224            // Mutate with probability based on amplitude
225            let mutation_prob = 0.1 * (1.0 - population[parent_idx].amplitude);
226            for bit in &mut offspring {
227                if rng.gen_bool(mutation_prob) {
228                    *bit = !*bit;
229                }
230            }
231
232            let energy = self.compute_energy(&offspring, qubo);
233            let distance = if let Some(ref best) = self.best_solution {
234                offspring
235                    .iter()
236                    .zip(best.iter())
237                    .filter(|(a, b)| a != b)
238                    .count()
239            } else {
240                0
241            };
242
243            new_population.push(AmplitudeState {
244                state: offspring,
245                energy,
246                amplitude: 1.0 / self.config.population_size as f64,
247                distance_to_best: distance,
248            });
249        }
250
251        new_population
252    }
253
254    /// Adaptive amplification: adjust amplification factor based on progress
255    fn adaptive_adjustment(&mut self, iteration: usize) {
256        if !self.config.adaptive_amplification {
257            return;
258        }
259
260        // Check progress
261        if self.energy_history.len() > 5 {
262            let last_energy = self.energy_history.last().copied().unwrap_or(f64::INFINITY);
263            let recent_improvement =
264                self.energy_history[self.energy_history.len() - 5] - last_energy;
265
266            // If stuck, increase amplification
267            if recent_improvement.abs() < 0.01 {
268                self.config.amplification_factor *= 1.1;
269            } else {
270                // If improving, moderate amplification
271                self.config.amplification_factor *= 0.95;
272            }
273
274            // Keep in reasonable range
275            self.config.amplification_factor = self.config.amplification_factor.clamp(1.5, 5.0);
276        }
277    }
278
279    /// Run Grover-inspired optimization
280    pub fn optimize(&mut self, qubo: &Array2<f64>) -> Result<Vec<HashMap<String, bool>>, String> {
281        let n_vars = qubo.nrows();
282
283        // Initialize population
284        let mut population = self.initialize_population(n_vars, qubo);
285
286        println!("Starting Grover-inspired amplitude amplification...");
287
288        for iteration in 0..self.config.grover_iterations {
289            // Oracle marking
290            self.oracle_marking(&mut population);
291
292            // Diffusion operator
293            self.diffusion_operator(&mut population);
294
295            // Sample new population
296            population = self.amplitude_sampling(&population, n_vars, qubo);
297
298            // Update best solution
299            for state in &population {
300                if state.energy < self.best_energy {
301                    self.best_energy = state.energy;
302                    self.best_solution = Some(state.state.clone());
303                }
304            }
305
306            self.energy_history.push(self.best_energy);
307
308            // Adaptive adjustment
309            self.adaptive_adjustment(iteration);
310
311            if (iteration + 1) % 5 == 0 {
312                println!(
313                    "Iteration {}/{}: Best energy = {:.4}, Amplification factor = {:.2}",
314                    iteration + 1,
315                    self.config.grover_iterations,
316                    self.best_energy,
317                    self.config.amplification_factor
318                );
319            }
320        }
321
322        // Convert final population to result format
323        let mut results = Vec::new();
324        let mut seen_energies = std::collections::HashSet::new();
325
326        for state in &population {
327            // Only return unique energy levels
328            let energy_key = (state.energy * 1000.0) as i64;
329            if seen_energies.insert(energy_key) {
330                let mut solution = HashMap::new();
331                for (i, &bit) in state.state.iter().enumerate() {
332                    solution.insert(format!("x{i}"), bit);
333                }
334                results.push(solution);
335            }
336        }
337
338        // Sort by energy (best first)
339        results.sort_by_key(|sol| {
340            let state: Vec<bool> = (0..n_vars)
341                .map(|i| *sol.get(&format!("x{i}")).unwrap_or(&false))
342                .collect();
343            (self.compute_energy(&state, qubo) * 1000.0) as i64
344        });
345
346        Ok(results)
347    }
348
349    /// Get optimization statistics
350    pub fn get_statistics(&self) -> OptimizationStatistics {
351        OptimizationStatistics {
352            best_energy: self.best_energy,
353            iterations: self.energy_history.len(),
354            final_amplification_factor: self.config.amplification_factor,
355            convergence_rate: if self.energy_history.len() > 1 {
356                (self.energy_history[0] - self.best_energy) / self.energy_history.len() as f64
357            } else {
358                0.0
359            },
360        }
361    }
362}
363
364/// Optimization statistics
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct OptimizationStatistics {
367    pub best_energy: f64,
368    pub iterations: usize,
369    pub final_amplification_factor: f64,
370    pub convergence_rate: f64,
371}
372
373/// Grover-amplified sampler
374#[derive(Debug, Clone)]
375pub struct GroverAmplifiedSampler {
376    config: GroverAmplificationConfig,
377}
378
379impl GroverAmplifiedSampler {
380    /// Create a new Grover-amplified sampler
381    pub const fn new(config: GroverAmplificationConfig) -> Self {
382        Self { config }
383    }
384}
385
386impl Sampler for GroverAmplifiedSampler {
387    fn run_qubo(
388        &self,
389        problem: &(Array2<f64>, HashMap<String, usize>),
390        shots: usize,
391    ) -> SamplerResult<Vec<SampleResult>> {
392        let (matrix, _var_map) = problem;
393
394        // Create solver
395        let mut solver = GroverAmplifiedSolver::new(self.config.clone());
396
397        // Run optimization
398        let solutions = solver
399            .optimize(matrix)
400            .map_err(SamplerError::InvalidParameter)?;
401
402        // Convert to SampleResult format
403        let results: Vec<SampleResult> = solutions
404            .into_iter()
405            .take(shots)
406            .map(|solution| {
407                let state: Vec<bool> = (0..matrix.nrows())
408                    .map(|i| *solution.get(&format!("x{i}")).unwrap_or(&false))
409                    .collect();
410                let energy = solver.compute_energy(&state, matrix);
411
412                SampleResult {
413                    assignments: solution,
414                    energy,
415                    occurrences: 1,
416                }
417            })
418            .collect();
419
420        Ok(results)
421    }
422
423    fn run_hobo(
424        &self,
425        _problem: &(ArrayD<f64>, HashMap<String, usize>),
426        _shots: usize,
427    ) -> SamplerResult<Vec<SampleResult>> {
428        Err(SamplerError::UnsupportedOperation(
429            "HOBO sampling not yet implemented for Grover-amplified sampler".to_string(),
430        ))
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_grover_solver_creation() {
440        let config = GroverAmplificationConfig::default();
441        let solver = GroverAmplifiedSolver::new(config);
442        assert_eq!(solver.best_energy, f64::INFINITY);
443    }
444
445    #[test]
446    fn test_energy_computation() {
447        let config = GroverAmplificationConfig::default();
448        let solver = GroverAmplifiedSolver::new(config);
449
450        let mut qubo = Array2::zeros((3, 3));
451        qubo[[0, 0]] = -1.0;
452        qubo[[1, 1]] = -1.0;
453        qubo[[2, 2]] = -1.0;
454
455        let state = vec![true, true, false];
456        let energy = solver.compute_energy(&state, &qubo);
457        assert_eq!(energy, -2.0);
458    }
459
460    #[test]
461    fn test_small_problem_optimization() {
462        let config = GroverAmplificationConfig {
463            grover_iterations: 10,
464            population_size: 20,
465            ..Default::default()
466        };
467        let mut solver = GroverAmplifiedSolver::new(config);
468
469        // Simple QUBO: minimize x0 + x1
470        let mut qubo = Array2::zeros((2, 2));
471        qubo[[0, 0]] = 1.0;
472        qubo[[1, 1]] = 1.0;
473
474        let _results = solver
475            .optimize(&qubo)
476            .expect("optimization should succeed for simple problem");
477
478        // Best solution should be [false, false] with energy 0
479        assert!(solver.best_energy <= 0.1);
480    }
481
482    #[test]
483    fn test_oracle_marking() {
484        let config = GroverAmplificationConfig::default();
485        let solver = GroverAmplifiedSolver::new(config);
486
487        let mut population = vec![
488            AmplitudeState {
489                state: vec![true, false],
490                energy: -2.0,
491                amplitude: 0.5,
492                distance_to_best: 0,
493            },
494            AmplitudeState {
495                state: vec![false, false],
496                energy: 0.0,
497                amplitude: 0.5,
498                distance_to_best: 1,
499            },
500        ];
501
502        solver.oracle_marking(&mut population);
503
504        // Lower energy should have higher amplitude
505        assert!(population[0].amplitude > population[1].amplitude);
506    }
507
508    #[test]
509    fn test_diffusion_operator() {
510        let config = GroverAmplificationConfig::default();
511        let solver = GroverAmplifiedSolver::new(config);
512
513        let mut population = vec![
514            AmplitudeState {
515                state: vec![true, false],
516                energy: -2.0,
517                amplitude: 0.8,
518                distance_to_best: 0,
519            },
520            AmplitudeState {
521                state: vec![false, false],
522                energy: 0.0,
523                amplitude: 0.2,
524                distance_to_best: 1,
525            },
526        ];
527
528        solver.diffusion_operator(&mut population);
529
530        // Amplitudes should still sum to ~1
531        let total: f64 = population.iter().map(|s| s.amplitude).sum();
532        assert!((total - 1.0).abs() < 0.01);
533    }
534}