Skip to main content

quantrs2_sim/quantum_inspired_classical/
framework.rs

1//! Quantum-inspired framework implementation
2
3use crate::error::{Result, SimulatorError};
4use crate::scirs2_integration::SciRS2Backend;
5use scirs2_core::ndarray::{Array1, Array2};
6use scirs2_core::random::prelude::*;
7use scirs2_core::Complex64;
8use std::collections::HashMap;
9use std::f64::consts::PI;
10use std::sync::{Arc, Mutex};
11
12use super::types::*;
13
14impl QuantumInspiredFramework {
15    /// Create a new quantum-inspired framework
16    pub fn new(config: QuantumInspiredConfig) -> Result<Self> {
17        let state = QuantumInspiredState {
18            variables: Array1::zeros(config.num_variables),
19            objective_value: f64::INFINITY,
20            iteration: 0,
21            best_solution: Array1::zeros(config.num_variables),
22            best_objective: f64::INFINITY,
23            convergence_history: Vec::new(),
24            runtime_stats: RuntimeStats::default(),
25        };
26        let rng = thread_rng();
27        Ok(Self {
28            config,
29            state,
30            backend: None,
31            stats: QuantumInspiredStats::default(),
32            rng: Arc::new(Mutex::new(rng)),
33        })
34    }
35    /// Set `SciRS2` backend for numerical operations
36    pub fn set_backend(&mut self, backend: SciRS2Backend) {
37        self.backend = Some(backend);
38    }
39    /// Run optimization algorithm
40    pub fn optimize(&mut self) -> Result<OptimizationResult> {
41        let start_time = std::time::Instant::now();
42        match self.config.optimization_config.algorithm_type {
43            OptimizationAlgorithm::QuantumGeneticAlgorithm => self.quantum_genetic_algorithm(),
44            OptimizationAlgorithm::QuantumParticleSwarm => {
45                self.quantum_particle_swarm_optimization()
46            }
47            OptimizationAlgorithm::QuantumSimulatedAnnealing => self.quantum_simulated_annealing(),
48            OptimizationAlgorithm::QuantumDifferentialEvolution => {
49                self.quantum_differential_evolution()
50            }
51            OptimizationAlgorithm::ClassicalQAOA => self.classical_qaoa_simulation(),
52            OptimizationAlgorithm::ClassicalVQE => self.classical_vqe_simulation(),
53            OptimizationAlgorithm::QuantumAntColony => self.quantum_ant_colony_optimization(),
54            OptimizationAlgorithm::QuantumHarmonySearch => self.quantum_harmony_search(),
55        }
56    }
57    /// Quantum-inspired genetic algorithm
58    pub(super) fn quantum_genetic_algorithm(&mut self) -> Result<OptimizationResult> {
59        let pop_size = self.config.algorithm_config.population_size;
60        let num_vars = self.config.num_variables;
61        let max_iterations = self.config.algorithm_config.max_iterations;
62        let mut population = self.initialize_quantum_population(pop_size, num_vars)?;
63        let mut fitness_values = vec![0.0; pop_size];
64        for (i, individual) in population.iter().enumerate() {
65            fitness_values[i] = self.evaluate_objective(individual)?;
66            self.state.runtime_stats.function_evaluations += 1;
67        }
68        for generation in 0..max_iterations {
69            self.state.iteration = generation;
70            let parents = self.quantum_selection(&population, &fitness_values)?;
71            let mut offspring = self.quantum_crossover(&parents)?;
72            self.quantum_mutation(&mut offspring)?;
73            let mut offspring_fitness = vec![0.0; offspring.len()];
74            for (i, individual) in offspring.iter().enumerate() {
75                offspring_fitness[i] = self.evaluate_objective(individual)?;
76                self.state.runtime_stats.function_evaluations += 1;
77            }
78            self.quantum_replacement(
79                &mut population,
80                &mut fitness_values,
81                offspring,
82                offspring_fitness,
83            )?;
84            let best_idx = fitness_values
85                .iter()
86                .enumerate()
87                .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
88                .map(|(idx, _)| idx)
89                .unwrap_or(0);
90            if fitness_values[best_idx] < self.state.best_objective {
91                self.state.best_objective = fitness_values[best_idx];
92                self.state.best_solution = population[best_idx].clone();
93            }
94            self.state
95                .convergence_history
96                .push(self.state.best_objective);
97            if self.check_convergence()? {
98                break;
99            }
100        }
101        Ok(OptimizationResult {
102            solution: self.state.best_solution.clone(),
103            objective_value: self.state.best_objective,
104            iterations: self.state.iteration,
105            converged: self.check_convergence()?,
106            runtime_stats: self.state.runtime_stats.clone(),
107            metadata: HashMap::new(),
108        })
109    }
110    /// Initialize quantum-inspired population with superposition
111    pub(super) fn initialize_quantum_population(
112        &self,
113        pop_size: usize,
114        num_vars: usize,
115    ) -> Result<Vec<Array1<f64>>> {
116        let mut population = Vec::with_capacity(pop_size);
117        let bounds = &self.config.optimization_config.bounds;
118        let quantum_params = &self.config.algorithm_config.quantum_parameters;
119        for _ in 0..pop_size {
120            let mut individual = Array1::zeros(num_vars);
121            for j in 0..num_vars {
122                let (min_bound, max_bound) = if j < bounds.len() {
123                    bounds[j]
124                } else {
125                    (-1.0, 1.0)
126                };
127                let mut rng = self.rng.lock().expect("RNG lock poisoned");
128                let base_value = rng
129                    .random::<f64>()
130                    .mul_add(max_bound - min_bound, min_bound);
131                let superposition_noise = (rng.random::<f64>() - 0.5)
132                    * quantum_params.superposition_strength
133                    * (max_bound - min_bound);
134                individual[j] = (base_value + superposition_noise).clamp(min_bound, max_bound);
135            }
136            population.push(individual);
137        }
138        Ok(population)
139    }
140    /// Quantum-inspired selection using interference
141    pub(super) fn quantum_selection(
142        &self,
143        population: &[Array1<f64>],
144        fitness: &[f64],
145    ) -> Result<Vec<Array1<f64>>> {
146        let pop_size = population.len();
147        let elite_size = (self.config.algorithm_config.elite_ratio * pop_size as f64) as usize;
148        let quantum_params = &self.config.algorithm_config.quantum_parameters;
149        let mut indexed_fitness: Vec<(usize, f64)> =
150            fitness.iter().enumerate().map(|(i, &f)| (i, f)).collect();
151        indexed_fitness.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
152        let mut parents = Vec::new();
153        for i in 0..elite_size {
154            parents.push(population[indexed_fitness[i].0].clone());
155        }
156        let mut rng = self.rng.lock().expect("RNG lock poisoned");
157        while parents.len() < pop_size {
158            let tournament_size = 3;
159            let mut tournament_indices = Vec::new();
160            for _ in 0..tournament_size {
161                tournament_indices.push(rng.random_range(0..pop_size));
162            }
163            let mut selection_probabilities = vec![0.0; tournament_size];
164            for (i, &idx) in tournament_indices.iter().enumerate() {
165                let normalized_fitness = 1.0 / (1.0 + fitness[idx]);
166                let interference_factor = (quantum_params.interference_strength
167                    * (i as f64 * PI / tournament_size as f64))
168                    .cos()
169                    .abs();
170                selection_probabilities[i] = normalized_fitness * (1.0 + interference_factor);
171            }
172            let sum: f64 = selection_probabilities.iter().sum();
173            for prob in &mut selection_probabilities {
174                *prob /= sum;
175            }
176            let mut cumulative = 0.0;
177            let random_val = rng.random::<f64>();
178            for (i, &prob) in selection_probabilities.iter().enumerate() {
179                cumulative += prob;
180                if random_val <= cumulative {
181                    parents.push(population[tournament_indices[i]].clone());
182                    break;
183                }
184            }
185        }
186        Ok(parents)
187    }
188    /// Quantum-inspired crossover with entanglement
189    pub(super) fn quantum_crossover(&self, parents: &[Array1<f64>]) -> Result<Vec<Array1<f64>>> {
190        let mut offspring = Vec::new();
191        let crossover_rate = self.config.algorithm_config.crossover_rate;
192        let quantum_params = &self.config.algorithm_config.quantum_parameters;
193        let mut rng = self.rng.lock().expect("RNG lock poisoned");
194        for i in (0..parents.len()).step_by(2) {
195            if i + 1 < parents.len() && rng.random::<f64>() < crossover_rate {
196                let parent1 = &parents[i];
197                let parent2 = &parents[i + 1];
198                let mut child1 = parent1.clone();
199                let mut child2 = parent2.clone();
200                for j in 0..parent1.len() {
201                    let entanglement_strength = quantum_params.entanglement_strength;
202                    let alpha = rng.random::<f64>();
203                    let entangled_val1 = alpha.mul_add(parent1[j], (1.0 - alpha) * parent2[j]);
204                    let entangled_val2 = (1.0 - alpha).mul_add(parent1[j], alpha * parent2[j]);
205                    let correlation = entanglement_strength
206                        * (parent1[j] - parent2[j]).abs()
207                        * (rng.random::<f64>() - 0.5);
208                    child1[j] = entangled_val1 + correlation;
209                    child2[j] = entangled_val2 - correlation;
210                }
211                offspring.push(child1);
212                offspring.push(child2);
213            } else {
214                offspring.push(parents[i].clone());
215                if i + 1 < parents.len() {
216                    offspring.push(parents[i + 1].clone());
217                }
218            }
219        }
220        Ok(offspring)
221    }
222    /// Quantum-inspired mutation with tunneling
223    pub(super) fn quantum_mutation(&mut self, population: &mut [Array1<f64>]) -> Result<()> {
224        let mutation_rate = self.config.algorithm_config.mutation_rate;
225        let quantum_params = &self.config.algorithm_config.quantum_parameters;
226        let bounds = &self.config.optimization_config.bounds;
227        let mut rng = self.rng.lock().expect("RNG lock poisoned");
228        for individual in population.iter_mut() {
229            for j in 0..individual.len() {
230                if rng.random::<f64>() < mutation_rate {
231                    let (min_bound, max_bound) = if j < bounds.len() {
232                        bounds[j]
233                    } else {
234                        (-1.0, 1.0)
235                    };
236                    let current_val = individual[j];
237                    let range = max_bound - min_bound;
238                    let gaussian_mutation =
239                        rng.random::<f64>() * 0.1 * range * (rng.random::<f64>() - 0.5);
240                    let tunneling_prob = quantum_params.tunneling_probability;
241                    let tunneling_mutation = if rng.random::<f64>() < tunneling_prob {
242                        (rng.random::<f64>() - 0.5) * range
243                    } else {
244                        0.0
245                    };
246                    individual[j] = (current_val + gaussian_mutation + tunneling_mutation)
247                        .clamp(min_bound, max_bound);
248                }
249            }
250        }
251        self.state.runtime_stats.quantum_operations += population.len();
252        Ok(())
253    }
254    /// Quantum-inspired replacement using quantum measurement
255    pub(super) fn quantum_replacement(
256        &self,
257        population: &mut Vec<Array1<f64>>,
258        fitness: &mut Vec<f64>,
259        offspring: Vec<Array1<f64>>,
260        offspring_fitness: Vec<f64>,
261    ) -> Result<()> {
262        let quantum_params = &self.config.algorithm_config.quantum_parameters;
263        let measurement_prob = quantum_params.measurement_probability;
264        let mut rng = self.rng.lock().expect("RNG lock poisoned");
265        let mut combined_population = population.clone();
266        combined_population.extend(offspring);
267        let mut combined_fitness = fitness.clone();
268        combined_fitness.extend(offspring_fitness);
269        let pop_size = population.len();
270        let mut new_population = Vec::with_capacity(pop_size);
271        let mut new_fitness = Vec::with_capacity(pop_size);
272        let mut indexed_combined: Vec<(usize, f64)> = combined_fitness
273            .iter()
274            .enumerate()
275            .map(|(i, &f)| (i, f))
276            .collect();
277        indexed_combined.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
278        for i in 0..pop_size {
279            if i < indexed_combined.len() {
280                let idx = indexed_combined[i].0;
281                let acceptance_prob = if rng.random::<f64>() < measurement_prob {
282                    1.0
283                } else {
284                    1.0 / (1.0 + (i as f64 / pop_size as f64))
285                };
286                if rng.random::<f64>() < acceptance_prob {
287                    new_population.push(combined_population[idx].clone());
288                    new_fitness.push(combined_fitness[idx]);
289                }
290            }
291        }
292        while new_population.len() < pop_size {
293            for i in 0..indexed_combined.len() {
294                if new_population.len() >= pop_size {
295                    break;
296                }
297                let idx = indexed_combined[i].0;
298                if !new_population.iter().any(|x| {
299                    x.iter()
300                        .zip(combined_population[idx].iter())
301                        .all(|(a, b)| (a - b).abs() < 1e-10)
302                }) {
303                    new_population.push(combined_population[idx].clone());
304                    new_fitness.push(combined_fitness[idx]);
305                }
306            }
307        }
308        new_population.truncate(pop_size);
309        new_fitness.truncate(pop_size);
310        *population = new_population;
311        *fitness = new_fitness;
312        Ok(())
313    }
314    /// Quantum particle swarm optimization
315    pub(super) fn quantum_particle_swarm_optimization(&mut self) -> Result<OptimizationResult> {
316        let pop_size = self.config.algorithm_config.population_size;
317        let num_vars = self.config.num_variables;
318        let max_iterations = self.config.algorithm_config.max_iterations;
319        let quantum_params = self.config.algorithm_config.quantum_parameters.clone();
320        let bounds = self.config.optimization_config.bounds.clone();
321        let mut particles = self.initialize_quantum_population(pop_size, num_vars)?;
322        let mut velocities: Vec<Array1<f64>> = vec![Array1::zeros(num_vars); pop_size];
323        let mut personal_best = particles.clone();
324        let mut personal_best_fitness = vec![f64::INFINITY; pop_size];
325        let mut global_best = Array1::zeros(num_vars);
326        let mut global_best_fitness = f64::INFINITY;
327        for (i, particle) in particles.iter().enumerate() {
328            let fitness = self.evaluate_objective(particle)?;
329            personal_best_fitness[i] = fitness;
330            if fitness < global_best_fitness {
331                global_best_fitness = fitness;
332                global_best = particle.clone();
333            }
334            self.state.runtime_stats.function_evaluations += 1;
335        }
336        let w = 0.7;
337        let c1 = 2.0;
338        let c2 = 2.0;
339        for iteration in 0..max_iterations {
340            self.state.iteration = iteration;
341            for i in 0..pop_size {
342                let mut rng = self.rng.lock().expect("RNG lock poisoned");
343                for j in 0..num_vars {
344                    let r1 = rng.random::<f64>();
345                    let r2 = rng.random::<f64>();
346                    let cognitive_term = c1 * r1 * (personal_best[i][j] - particles[i][j]);
347                    let social_term = c2 * r2 * (global_best[j] - particles[i][j]);
348                    let quantum_fluctuation =
349                        quantum_params.superposition_strength * (rng.random::<f64>() - 0.5);
350                    let quantum_tunneling =
351                        if rng.random::<f64>() < quantum_params.tunneling_probability {
352                            (rng.random::<f64>() - 0.5) * 2.0
353                        } else {
354                            0.0
355                        };
356                    velocities[i][j] = w * velocities[i][j]
357                        + cognitive_term
358                        + social_term
359                        + quantum_fluctuation
360                        + quantum_tunneling;
361                }
362                for j in 0..num_vars {
363                    particles[i][j] += velocities[i][j];
364                    let (min_bound, max_bound) = if j < bounds.len() {
365                        bounds[j]
366                    } else {
367                        (-10.0, 10.0)
368                    };
369                    particles[i][j] = particles[i][j].clamp(min_bound, max_bound);
370                }
371                drop(rng);
372                let fitness = self.evaluate_objective(&particles[i])?;
373                self.state.runtime_stats.function_evaluations += 1;
374                if fitness < personal_best_fitness[i] {
375                    personal_best_fitness[i] = fitness;
376                    personal_best[i] = particles[i].clone();
377                }
378                if fitness < global_best_fitness {
379                    global_best_fitness = fitness;
380                    global_best = particles[i].clone();
381                }
382            }
383            self.state.best_objective = global_best_fitness;
384            self.state.best_solution = global_best.clone();
385            self.state.convergence_history.push(global_best_fitness);
386            if self.check_convergence()? {
387                break;
388            }
389        }
390        Ok(OptimizationResult {
391            solution: global_best,
392            objective_value: global_best_fitness,
393            iterations: self.state.iteration,
394            converged: self.check_convergence()?,
395            runtime_stats: self.state.runtime_stats.clone(),
396            metadata: HashMap::new(),
397        })
398    }
399    /// Quantum-inspired simulated annealing
400    pub(super) fn quantum_simulated_annealing(&mut self) -> Result<OptimizationResult> {
401        let max_iterations = self.config.algorithm_config.max_iterations;
402        let temperature_schedule = self.config.algorithm_config.temperature_schedule;
403        let quantum_parameters = self.config.algorithm_config.quantum_parameters.clone();
404        let bounds = self.config.optimization_config.bounds.clone();
405        let num_vars = self.config.num_variables;
406        let mut current_solution = Array1::zeros(num_vars);
407        let mut rng = self.rng.lock().expect("RNG lock poisoned");
408        for i in 0..num_vars {
409            let (min_bound, max_bound) = if i < bounds.len() {
410                bounds[i]
411            } else {
412                (-10.0, 10.0)
413            };
414            current_solution[i] = rng
415                .random::<f64>()
416                .mul_add(max_bound - min_bound, min_bound);
417        }
418        drop(rng);
419        let mut current_energy = self.evaluate_objective(&current_solution)?;
420        let mut best_solution = current_solution.clone();
421        let mut best_energy = current_energy;
422        self.state.runtime_stats.function_evaluations += 1;
423        let initial_temp: f64 = 100.0;
424        let final_temp: f64 = 0.01;
425        for iteration in 0..max_iterations {
426            self.state.iteration = iteration;
427            let temp = match temperature_schedule {
428                TemperatureSchedule::Exponential => {
429                    initial_temp
430                        * (final_temp / initial_temp).powf(iteration as f64 / max_iterations as f64)
431                }
432                TemperatureSchedule::Linear => (initial_temp - final_temp)
433                    .mul_add(-(iteration as f64 / max_iterations as f64), initial_temp),
434                TemperatureSchedule::Logarithmic => initial_temp / (1.0 + (iteration as f64).ln()),
435                TemperatureSchedule::QuantumAdiabatic => {
436                    let s = iteration as f64 / max_iterations as f64;
437                    initial_temp.mul_add(1.0 - s, final_temp * s * (1.0 - (1.0 - s).powi(3)))
438                }
439                TemperatureSchedule::Custom => initial_temp * 0.95_f64.powi(iteration as i32),
440            };
441            let mut neighbor = current_solution.clone();
442            let quantum_params = &quantum_parameters;
443            let mut rng = self.rng.lock().expect("RNG lock poisoned");
444            for i in 0..num_vars {
445                if rng.random::<f64>() < 0.5 {
446                    let (min_bound, max_bound) = if i < bounds.len() {
447                        bounds[i]
448                    } else {
449                        (-10.0, 10.0)
450                    };
451                    let step_size = temp / initial_temp;
452                    let gaussian_step =
453                        rng.random::<f64>() * step_size * (max_bound - min_bound) * 0.1;
454                    let tunneling_move =
455                        if rng.random::<f64>() < quantum_params.tunneling_probability {
456                            (rng.random::<f64>() - 0.5) * (max_bound - min_bound) * 0.5
457                        } else {
458                            0.0
459                        };
460                    neighbor[i] = (current_solution[i] + gaussian_step + tunneling_move)
461                        .clamp(min_bound, max_bound);
462                }
463            }
464            drop(rng);
465            let neighbor_energy = self.evaluate_objective(&neighbor)?;
466            self.state.runtime_stats.function_evaluations += 1;
467            let delta_energy = neighbor_energy - current_energy;
468            let acceptance_prob = if delta_energy < 0.0 {
469                1.0
470            } else {
471                let boltzmann_factor = (-delta_energy / temp).exp();
472                let quantum_correction = quantum_params.interference_strength
473                    * (2.0 * PI * iteration as f64 / max_iterations as f64).cos()
474                    * 0.1;
475                (boltzmann_factor + quantum_correction).clamp(0.0, 1.0)
476            };
477            let mut rng = self.rng.lock().expect("RNG lock poisoned");
478            if rng.random::<f64>() < acceptance_prob {
479                current_solution = neighbor;
480                current_energy = neighbor_energy;
481                if current_energy < best_energy {
482                    best_solution = current_solution.clone();
483                    best_energy = current_energy;
484                }
485            }
486            drop(rng);
487            self.state.best_objective = best_energy;
488            self.state.best_solution = best_solution.clone();
489            self.state.convergence_history.push(best_energy);
490            if temp < final_temp || self.check_convergence()? {
491                break;
492            }
493        }
494        Ok(OptimizationResult {
495            solution: best_solution,
496            objective_value: best_energy,
497            iterations: self.state.iteration,
498            converged: self.check_convergence()?,
499            runtime_stats: self.state.runtime_stats.clone(),
500            metadata: HashMap::new(),
501        })
502    }
503    /// Quantum differential evolution
504    pub(super) fn quantum_differential_evolution(&mut self) -> Result<OptimizationResult> {
505        let pop_size = self.config.algorithm_config.population_size.max(4);
506        let num_vars = self.config.num_variables;
507        let max_iterations = self.config.algorithm_config.max_iterations;
508        // F in DE literature: scale factor for the difference vector
509        let mutation_factor = 0.5_f64;
510        let crossover_rate = self.config.algorithm_config.crossover_rate;
511        let bounds = self.config.optimization_config.bounds.clone();
512        let quantum_params = self.config.algorithm_config.quantum_parameters.clone();
513
514        // Initialize population using quantum-inspired initialization
515        let mut population = self.initialize_quantum_population(pop_size, num_vars)?;
516        let mut fitness: Vec<f64> = Vec::with_capacity(pop_size);
517        let mut best_solution = population[0].clone();
518        let mut best_fitness = f64::INFINITY;
519
520        // Evaluate initial population fitness
521        for individual in &population {
522            let f = self.evaluate_objective(individual)?;
523            fitness.push(f);
524            self.state.runtime_stats.function_evaluations += 1;
525            if f < best_fitness {
526                best_fitness = f;
527                best_solution = individual.clone();
528            }
529        }
530
531        for iteration in 0..max_iterations {
532            self.state.iteration = iteration;
533
534            for i in 0..pop_size {
535                // Pick 3 distinct random indices r1, r2, r3, all != i
536                let mut rng = self.rng.lock().expect("RNG lock poisoned");
537                let mut indices: Vec<usize> = Vec::with_capacity(3);
538                let mut attempts = 0usize;
539                while indices.len() < 3 && attempts < 1000 {
540                    let idx = (rng.random::<f64>() * pop_size as f64) as usize % pop_size;
541                    if idx != i && !indices.contains(&idx) {
542                        indices.push(idx);
543                    }
544                    attempts += 1;
545                }
546                // Fallback if pop_size < 4: wrap with offset to ensure distinct-ish values
547                while indices.len() < 3 {
548                    let candidate = (i + indices.len() + 1) % pop_size;
549                    if !indices.contains(&candidate) {
550                        indices.push(candidate);
551                    } else {
552                        indices.push((candidate + 1) % pop_size);
553                    }
554                }
555                let (r1, r2, r3) = (indices[0], indices[1], indices[2]);
556
557                // Mutant vector: v = x_r1 + F * (x_r2 - x_r3)
558                let mut mutant = population[r1].clone();
559                for j in 0..num_vars {
560                    mutant[j] =
561                        mutation_factor.mul_add(population[r2][j] - population[r3][j], mutant[j]);
562                }
563
564                // Binomial crossover: trial inherits from mutant with rate CR,
565                // at least one dimension guaranteed (jrand)
566                let mut trial = population[i].clone();
567                let guaranteed_j = (rng.random::<f64>() * num_vars as f64) as usize % num_vars;
568                for j in 0..num_vars {
569                    if j == guaranteed_j || rng.random::<f64>() < crossover_rate {
570                        trial[j] = mutant[j];
571                    }
572                    // Quantum tunneling: probabilistic random reset to bypass local basins
573                    if rng.random::<f64>() < quantum_params.tunneling_probability {
574                        let (min_b, max_b) = if j < bounds.len() {
575                            bounds[j]
576                        } else {
577                            (-10.0, 10.0)
578                        };
579                        trial[j] = rng.random::<f64>().mul_add(max_b - min_b, min_b);
580                    }
581                    // Clamp to feasible bounds
582                    let (min_b, max_b) = if j < bounds.len() {
583                        bounds[j]
584                    } else {
585                        (-10.0, 10.0)
586                    };
587                    trial[j] = trial[j].clamp(min_b, max_b);
588                }
589                drop(rng);
590
591                // Greedy selection: replace parent only if trial is better
592                let trial_fitness = self.evaluate_objective(&trial)?;
593                self.state.runtime_stats.function_evaluations += 1;
594                if trial_fitness < fitness[i] {
595                    fitness[i] = trial_fitness;
596                    population[i] = trial;
597                    if trial_fitness < best_fitness {
598                        best_fitness = trial_fitness;
599                        best_solution = population[i].clone();
600                    }
601                }
602            }
603
604            self.state.best_objective = best_fitness;
605            self.state.best_solution = best_solution.clone();
606            self.state.convergence_history.push(best_fitness);
607
608            if self.check_convergence()? {
609                break;
610            }
611        }
612
613        Ok(OptimizationResult {
614            solution: best_solution,
615            objective_value: best_fitness,
616            iterations: self.state.iteration,
617            converged: self.check_convergence()?,
618            runtime_stats: self.state.runtime_stats.clone(),
619            metadata: HashMap::new(),
620        })
621    }
622    /// Classical QAOA simulation
623    ///
624    /// Implements a QAOA-inspired coordinate landscape search using alternating
625    /// problem (cost) and mixing (driver) phases with sinusoidal annealing schedules.
626    pub(super) fn classical_qaoa_simulation(&mut self) -> Result<OptimizationResult> {
627        let num_vars = self.config.num_variables;
628        let max_iterations = self.config.algorithm_config.max_iterations;
629        let bounds = self.config.optimization_config.bounds.clone();
630        let quantum_params = self.config.algorithm_config.quantum_parameters.clone();
631
632        // Initialize solution with continuous relaxation in [lo, hi]
633        let mut current = {
634            let mut rng = self.rng.lock().expect("RNG lock poisoned");
635            let mut v = Array1::zeros(num_vars);
636            for j in 0..num_vars {
637                let (lo, hi) = if j < bounds.len() {
638                    bounds[j]
639                } else {
640                    (-10.0, 10.0)
641                };
642                v[j] = rng.random::<f64>().mul_add(hi - lo, lo);
643            }
644            v
645        };
646        let mut current_energy = self.evaluate_objective(&current)?;
647        let mut best = current.clone();
648        let mut best_energy = current_energy;
649        self.state.runtime_stats.function_evaluations += 1;
650
651        for iteration in 0..max_iterations {
652            self.state.iteration = iteration;
653            let layers = iteration as f64 / max_iterations.max(1) as f64;
654            // gamma: problem phase angle increases with layer depth (QAOA schedule)
655            let gamma = PI * layers;
656            // beta: mixing angle decreases (adiabatic annealing schedule)
657            let beta = std::f64::consts::FRAC_PI_2 * (1.0 - layers);
658
659            let mut next = current.clone();
660            let mut rng = self.rng.lock().expect("RNG lock poisoned");
661            for j in 0..num_vars {
662                let (lo, hi) = if j < bounds.len() {
663                    bounds[j]
664                } else {
665                    (-10.0, 10.0)
666                };
667                let range = hi - lo;
668                // Problem phase: gradient-guided shift using cost landscape curvature
669                let shift = range * 0.05;
670                let cost_shift = (gamma.sin() * shift).mul_add(rng.random::<f64>() - 0.5, 0.0);
671                // Mixing phase: random exploration with decreasing strength
672                let mix_shift = if rng.random::<f64>() < beta.sin().powi(2) {
673                    (rng.random::<f64>() - 0.5) * range * quantum_params.superposition_strength
674                } else {
675                    0.0
676                };
677                // Quantum tunneling jump
678                let tunnel_shift = if rng.random::<f64>() < quantum_params.tunneling_probability {
679                    (rng.random::<f64>() - 0.5) * range
680                } else {
681                    0.0
682                };
683                next[j] = (current[j] + cost_shift + mix_shift + tunnel_shift).clamp(lo, hi);
684            }
685            drop(rng);
686
687            let next_energy = self.evaluate_objective(&next)?;
688            self.state.runtime_stats.function_evaluations += 1;
689
690            // Accept if improved (greedy acceptance analogous to classical QAOA measurement)
691            if next_energy < current_energy {
692                current = next;
693                current_energy = next_energy;
694                if current_energy < best_energy {
695                    best_energy = current_energy;
696                    best = current.clone();
697                }
698            }
699
700            self.state.best_objective = best_energy;
701            self.state.best_solution = best.clone();
702            self.state.convergence_history.push(best_energy);
703            if self.check_convergence()? {
704                break;
705            }
706        }
707
708        Ok(OptimizationResult {
709            solution: best,
710            objective_value: best_energy,
711            iterations: self.state.iteration,
712            converged: self.check_convergence()?,
713            runtime_stats: self.state.runtime_stats.clone(),
714            metadata: HashMap::new(),
715        })
716    }
717
718    /// Classical VQE simulation
719    ///
720    /// Implements coordinate descent with parameter-shift-inspired steps to mimic
721    /// variational quantum eigensolver parameter optimization classically.
722    pub(super) fn classical_vqe_simulation(&mut self) -> Result<OptimizationResult> {
723        let num_vars = self.config.num_variables;
724        let max_iterations = self.config.algorithm_config.max_iterations;
725        let bounds = self.config.optimization_config.bounds.clone();
726        let quantum_params = self.config.algorithm_config.quantum_parameters.clone();
727
728        // Initialize parameter vector randomly
729        let mut theta = {
730            let mut rng = self.rng.lock().expect("RNG lock poisoned");
731            let mut v = Array1::zeros(num_vars);
732            for j in 0..num_vars {
733                let (lo, hi) = if j < bounds.len() {
734                    bounds[j]
735                } else {
736                    (-10.0, 10.0)
737                };
738                v[j] = rng.random::<f64>().mul_add(hi - lo, lo);
739            }
740            v
741        };
742        let mut current_energy = self.evaluate_objective(&theta)?;
743        let mut best = theta.clone();
744        let mut best_energy = current_energy;
745        self.state.runtime_stats.function_evaluations += 1;
746
747        for iteration in 0..max_iterations {
748            self.state.iteration = iteration;
749            // Interference phase oscillates like a quantum circuit expectation value
750            let phase = 2.0 * PI * iteration as f64 / max_iterations.max(1) as f64;
751            let interference = quantum_params.interference_strength * phase.cos();
752
753            // Coordinate descent with parameter-shift rule (analogous to quantum gradient)
754            for j in 0..num_vars {
755                let (lo, hi) = if j < bounds.len() {
756                    bounds[j]
757                } else {
758                    (-10.0, 10.0)
759                };
760                let delta = 0.1 * (hi - lo) + interference.abs() * 0.01 * (hi - lo);
761
762                let mut plus = theta.clone();
763                plus[j] = (plus[j] + delta).clamp(lo, hi);
764                let mut minus = theta.clone();
765                minus[j] = (minus[j] - delta).clamp(lo, hi);
766
767                let e_plus = self.evaluate_objective(&plus)?;
768                let e_minus = self.evaluate_objective(&minus)?;
769                self.state.runtime_stats.function_evaluations += 2;
770
771                if e_plus < current_energy && e_plus <= e_minus {
772                    theta = plus;
773                    current_energy = e_plus;
774                } else if e_minus < current_energy {
775                    theta = minus;
776                    current_energy = e_minus;
777                }
778            }
779
780            // Quantum tunneling escape from local minima
781            {
782                let mut rng = self.rng.lock().expect("RNG lock poisoned");
783                if rng.random::<f64>() < quantum_params.tunneling_probability {
784                    let j = (rng.random::<f64>() * num_vars as f64) as usize % num_vars;
785                    let (lo, hi) = if j < bounds.len() {
786                        bounds[j]
787                    } else {
788                        (-10.0, 10.0)
789                    };
790                    theta[j] = rng.random::<f64>().mul_add(hi - lo, lo);
791                    current_energy = self.evaluate_objective(&theta)?;
792                    self.state.runtime_stats.function_evaluations += 1;
793                }
794            }
795
796            if current_energy < best_energy {
797                best_energy = current_energy;
798                best = theta.clone();
799            }
800
801            self.state.best_objective = best_energy;
802            self.state.best_solution = best.clone();
803            self.state.convergence_history.push(best_energy);
804            if self.check_convergence()? {
805                break;
806            }
807        }
808
809        Ok(OptimizationResult {
810            solution: best,
811            objective_value: best_energy,
812            iterations: self.state.iteration,
813            converged: self.check_convergence()?,
814            runtime_stats: self.state.runtime_stats.clone(),
815            metadata: HashMap::new(),
816        })
817    }
818
819    /// Quantum ant colony optimization
820    ///
821    /// ACO with quantum amplitude-modulated pheromones: pheromone attractiveness is
822    /// boosted by quantum interference effects and tunneling enables non-greedy exploration.
823    pub(super) fn quantum_ant_colony_optimization(&mut self) -> Result<OptimizationResult> {
824        const N_LEVELS: usize = 10;
825        let n_ants = self.config.algorithm_config.population_size.max(5);
826        let num_vars = self.config.num_variables;
827        let max_iterations = self.config.algorithm_config.max_iterations;
828        let bounds = self.config.optimization_config.bounds.clone();
829        let quantum_params = self.config.algorithm_config.quantum_parameters.clone();
830        let evaporation_rate = 1.0 - self.config.algorithm_config.mutation_rate.clamp(0.01, 0.99);
831
832        // Pheromone matrix: pheromones[j][k] = strength for variable j at discretization level k
833        let mut pheromones = vec![vec![1.0f64; N_LEVELS]; num_vars];
834
835        let mut best_solution = {
836            let mut rng = self.rng.lock().expect("RNG lock poisoned");
837            let mut v = Array1::zeros(num_vars);
838            for j in 0..num_vars {
839                let (lo, hi) = if j < bounds.len() {
840                    bounds[j]
841                } else {
842                    (-10.0, 10.0)
843                };
844                v[j] = rng.random::<f64>().mul_add(hi - lo, lo);
845            }
846            v
847        };
848        let mut best_energy = self.evaluate_objective(&best_solution)?;
849        self.state.runtime_stats.function_evaluations += 1;
850
851        for iteration in 0..max_iterations {
852            self.state.iteration = iteration;
853            // Quantum interference modulation of pheromone attractiveness
854            let q_phase = 2.0 * PI * iteration as f64 / max_iterations.max(1) as f64;
855            let q_boost = 1.0 + quantum_params.interference_strength * q_phase.cos();
856
857            let mut ant_solutions: Vec<Array1<f64>> = Vec::with_capacity(n_ants);
858            let mut ant_energies: Vec<f64> = Vec::with_capacity(n_ants);
859
860            // Each ant constructs a solution via pheromone-guided roulette selection
861            for _ in 0..n_ants {
862                let mut solution = Array1::zeros(num_vars);
863                {
864                    let mut rng = self.rng.lock().expect("RNG lock poisoned");
865                    for j in 0..num_vars {
866                        let (lo, hi) = if j < bounds.len() {
867                            bounds[j]
868                        } else {
869                            (-10.0, 10.0)
870                        };
871                        // Quantum tunneling: occasionally pick uniformly regardless of pheromone
872                        if rng.random::<f64>() < quantum_params.tunneling_probability {
873                            solution[j] = rng.random::<f64>().mul_add(hi - lo, lo);
874                            continue;
875                        }
876                        // Roulette wheel over N_LEVELS using quantum-boosted pheromones
877                        let total: f64 = pheromones[j]
878                            .iter()
879                            .map(|&p| (p * q_boost).max(1e-10))
880                            .sum();
881                        let mut pick = rng.random::<f64>() * total;
882                        let mut chosen_level = N_LEVELS - 1;
883                        for (k, &p) in pheromones[j].iter().enumerate() {
884                            pick -= (p * q_boost).max(1e-10);
885                            if pick <= 0.0 {
886                                chosen_level = k;
887                                break;
888                            }
889                        }
890                        solution[j] =
891                            lo + (chosen_level as f64 + 0.5) / N_LEVELS as f64 * (hi - lo);
892                    }
893                }
894
895                let energy = self.evaluate_objective(&solution)?;
896                self.state.runtime_stats.function_evaluations += 1;
897                if energy < best_energy {
898                    best_energy = energy;
899                    best_solution = solution.clone();
900                }
901                ant_solutions.push(solution);
902                ant_energies.push(energy);
903            }
904
905            // Pheromone evaporation
906            for j in 0..num_vars {
907                for k in 0..N_LEVELS {
908                    pheromones[j][k] *= evaporation_rate;
909                }
910            }
911
912            // Pheromone deposit: lower-energy ants contribute more
913            let min_e = ant_energies.iter().cloned().fold(f64::INFINITY, f64::min);
914            let max_e = ant_energies
915                .iter()
916                .cloned()
917                .fold(f64::NEG_INFINITY, f64::max);
918            let range = (max_e - min_e).max(1e-10);
919            for (ant_idx, solution) in ant_solutions.iter().enumerate() {
920                let deposit = 1.0 - (ant_energies[ant_idx] - min_e) / range;
921                for j in 0..num_vars {
922                    let (lo, hi) = if j < bounds.len() {
923                        bounds[j]
924                    } else {
925                        (-10.0, 10.0)
926                    };
927                    let level = ((solution[j] - lo) / (hi - lo) * N_LEVELS as f64) as usize;
928                    let level = level.min(N_LEVELS - 1);
929                    pheromones[j][level] += deposit;
930                }
931            }
932
933            self.state.best_objective = best_energy;
934            self.state.best_solution = best_solution.clone();
935            self.state.convergence_history.push(best_energy);
936            if self.check_convergence()? {
937                break;
938            }
939        }
940
941        Ok(OptimizationResult {
942            solution: best_solution,
943            objective_value: best_energy,
944            iterations: self.state.iteration,
945            converged: self.check_convergence()?,
946            runtime_stats: self.state.runtime_stats.clone(),
947            metadata: HashMap::new(),
948        })
949    }
950    /// Quantum harmony search
951    pub(super) fn quantum_harmony_search(&mut self) -> Result<OptimizationResult> {
952        let hm_size = self.config.algorithm_config.population_size.max(5);
953        let num_vars = self.config.num_variables;
954        let max_iterations = self.config.algorithm_config.max_iterations;
955        // HMCR: probability of selecting from harmony memory (typical: 0.7–0.95)
956        let hmcr = 0.9_f64;
957        // PAR: probability of pitch adjustment when selected from memory (typical: 0.1–0.5)
958        let par = 0.3_f64;
959        let bounds = self.config.optimization_config.bounds.clone();
960        let quantum_params = self.config.algorithm_config.quantum_parameters.clone();
961
962        // Initialize harmony memory using quantum-inspired population initializer
963        let mut harmony_memory = self.initialize_quantum_population(hm_size, num_vars)?;
964        let mut hm_fitness: Vec<f64> = Vec::with_capacity(hm_size);
965        let mut best_solution = harmony_memory[0].clone();
966        let mut best_fitness = f64::INFINITY;
967
968        // Evaluate initial harmonies
969        for harmony in &harmony_memory {
970            let f = self.evaluate_objective(harmony)?;
971            hm_fitness.push(f);
972            self.state.runtime_stats.function_evaluations += 1;
973            if f < best_fitness {
974                best_fitness = f;
975                best_solution = harmony.clone();
976            }
977        }
978
979        for iteration in 0..max_iterations {
980            self.state.iteration = iteration;
981
982            // Improvise a new harmony vector
983            let mut new_harmony = Array1::zeros(num_vars);
984            let mut rng = self.rng.lock().expect("RNG lock poisoned");
985
986            for j in 0..num_vars {
987                let (min_b, max_b) = if j < bounds.len() {
988                    bounds[j]
989                } else {
990                    (-10.0, 10.0)
991                };
992
993                if rng.random::<f64>() < hmcr {
994                    // Memory consideration: borrow a value from a randomly chosen harmony
995                    let hm_idx = (rng.random::<f64>() * hm_size as f64) as usize % hm_size;
996                    new_harmony[j] = harmony_memory[hm_idx][j];
997
998                    // Pitch adjustment: perturb the recalled value within a bandwidth
999                    if rng.random::<f64>() < par {
1000                        // Bandwidth proportional to 5% of the variable range
1001                        let bw = 0.05 * (max_b - min_b);
1002                        let perturbation = (rng.random::<f64>() - 0.5) * 2.0 * bw;
1003                        new_harmony[j] = (new_harmony[j] + perturbation).clamp(min_b, max_b);
1004                    }
1005                } else {
1006                    // Random selection: uniform draw within variable bounds
1007                    new_harmony[j] = rng.random::<f64>().mul_add(max_b - min_b, min_b);
1008                }
1009
1010                // Quantum tunneling: escape local optima by random reset
1011                if rng.random::<f64>() < quantum_params.tunneling_probability {
1012                    new_harmony[j] = rng.random::<f64>().mul_add(max_b - min_b, min_b);
1013                }
1014            }
1015            drop(rng);
1016
1017            // Evaluate the improvised harmony
1018            let new_fitness = self.evaluate_objective(&new_harmony)?;
1019            self.state.runtime_stats.function_evaluations += 1;
1020
1021            // Update harmony memory: replace the worst harmony if the new one is better
1022            if let Some((worst_idx, &worst_fitness)) = hm_fitness
1023                .iter()
1024                .enumerate()
1025                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1026            {
1027                if new_fitness < worst_fitness {
1028                    hm_fitness[worst_idx] = new_fitness;
1029                    harmony_memory[worst_idx] = new_harmony;
1030                    if new_fitness < best_fitness {
1031                        best_fitness = new_fitness;
1032                        best_solution = harmony_memory[worst_idx].clone();
1033                    }
1034                }
1035            }
1036
1037            self.state.best_objective = best_fitness;
1038            self.state.best_solution = best_solution.clone();
1039            self.state.convergence_history.push(best_fitness);
1040
1041            if self.check_convergence()? {
1042                break;
1043            }
1044        }
1045
1046        Ok(OptimizationResult {
1047            solution: best_solution,
1048            objective_value: best_fitness,
1049            iterations: self.state.iteration,
1050            converged: self.check_convergence()?,
1051            runtime_stats: self.state.runtime_stats.clone(),
1052            metadata: HashMap::new(),
1053        })
1054    }
1055    /// Evaluate objective function
1056    pub(super) fn evaluate_objective(&self, solution: &Array1<f64>) -> Result<f64> {
1057        let result = match self.config.optimization_config.objective_function {
1058            ObjectiveFunction::Quadratic => solution.iter().map(|&x| x * x).sum(),
1059            ObjectiveFunction::Rastrigin => {
1060                let n = solution.len() as f64;
1061                let a = 10.0;
1062                a * n
1063                    + solution
1064                        .iter()
1065                        .map(|&x| x.mul_add(x, -(a * (2.0 * PI * x).cos())))
1066                        .sum::<f64>()
1067            }
1068            ObjectiveFunction::Rosenbrock => {
1069                if solution.len() < 2 {
1070                    return Ok(0.0);
1071                }
1072                let mut result = 0.0;
1073                for i in 0..solution.len() - 1 {
1074                    let x = solution[i];
1075                    let y = solution[i + 1];
1076                    result += (1.0 - x).mul_add(1.0 - x, 100.0 * x.mul_add(-x, y).powi(2));
1077                }
1078                result
1079            }
1080            ObjectiveFunction::Ackley => {
1081                let n = solution.len() as f64;
1082                let a: f64 = 20.0;
1083                let b: f64 = 0.2;
1084                let c: f64 = 2.0 * PI;
1085                let sum1 = solution.iter().map(|&x| x * x).sum::<f64>() / n;
1086                let sum2 = solution.iter().map(|&x| (c * x).cos()).sum::<f64>() / n;
1087                (-a).mul_add((-b * sum1.sqrt()).exp(), -sum2.exp()) + a + std::f64::consts::E
1088            }
1089            ObjectiveFunction::Sphere => solution.iter().map(|&x| x * x).sum(),
1090            ObjectiveFunction::Griewank => {
1091                let sum_sq = solution.iter().map(|&x| x * x).sum::<f64>() / 4000.0;
1092                let prod_cos = solution
1093                    .iter()
1094                    .enumerate()
1095                    .map(|(i, &x)| (x / ((i + 1) as f64).sqrt()).cos())
1096                    .product::<f64>();
1097                1.0 + sum_sq - prod_cos
1098            }
1099            ObjectiveFunction::Custom => solution.iter().map(|&x| x * x).sum(),
1100        };
1101        Ok(result)
1102    }
1103    /// Check convergence
1104    pub(super) fn check_convergence(&self) -> Result<bool> {
1105        if self.state.convergence_history.len() < 2 {
1106            return Ok(false);
1107        }
1108        let tolerance = self.config.algorithm_config.tolerance;
1109        let recent_improvements = &self.state.convergence_history
1110            [self.state.convergence_history.len().saturating_sub(10)..];
1111        if recent_improvements.len() < 2 {
1112            return Ok(false);
1113        }
1114        let last_value = recent_improvements
1115            .last()
1116            .expect("recent_improvements has at least 2 elements");
1117        let second_last_value = recent_improvements[recent_improvements.len() - 2];
1118        let change = (last_value - second_last_value).abs();
1119        Ok(change < tolerance)
1120    }
1121    /// Train machine learning model using quantum-inspired stochastic gradient descent.
1122    ///
1123    /// Trains a linear model `y = W * x + b` on the provided `(input, target)` pairs.
1124    /// Quantum tunneling perturbations are injected with probability
1125    /// `quantum_parameters.tunneling_probability` to help escape local minima.
1126    pub fn train_ml_model(
1127        &mut self,
1128        training_data: &[(Array1<f64>, Array1<f64>)],
1129    ) -> Result<MLTrainingResult> {
1130        if training_data.is_empty() {
1131            return Err(SimulatorError::InvalidInput(
1132                "Training data is empty".to_string(),
1133            ));
1134        }
1135
1136        let input_dim = training_data[0].0.len();
1137        let output_dim = training_data[0].1.len();
1138        // Weight layout: for output neuron o, weights at indices
1139        // [o*(input_dim+1) .. o*(input_dim+1)+input_dim] and bias at
1140        // o*(input_dim+1)+input_dim.
1141        let n_params = (input_dim + 1) * output_dim;
1142
1143        let lr = self
1144            .config
1145            .algorithm_config
1146            .quantum_parameters
1147            .superposition_strength
1148            .clamp(0.001, 0.1);
1149        let epochs = self.config.algorithm_config.max_iterations;
1150        let tunneling_prob = self
1151            .config
1152            .algorithm_config
1153            .quantum_parameters
1154            .tunneling_probability;
1155
1156        // Initialise parameters with small random values.
1157        let mut params: Array1<f64> = {
1158            let mut rng = self.rng.lock().expect("RNG lock poisoned");
1159            let mut p = Array1::zeros(n_params);
1160            for v in p.iter_mut() {
1161                *v = (rng.random::<f64>() - 0.5) * 0.1;
1162            }
1163            p
1164        };
1165
1166        let mut loss_history: Vec<f64> = Vec::with_capacity(epochs);
1167
1168        for epoch in 0..epochs {
1169            let mut total_loss = 0.0_f64;
1170            let mut grad: Array1<f64> = Array1::zeros(n_params);
1171
1172            for (x, y_true) in training_data {
1173                // Forward pass: y_pred[o] = sum_i(w[o*(d+1)+i] * x[i]) + bias[o].
1174                let mut y_pred: Array1<f64> = Array1::zeros(output_dim);
1175                for o in 0..output_dim {
1176                    let w_start = o * (input_dim + 1);
1177                    for i in 0..input_dim {
1178                        y_pred[o] += params[w_start + i] * x[i];
1179                    }
1180                    y_pred[o] += params[o * (input_dim + 1) + input_dim];
1181                }
1182
1183                // MSE loss per sample.
1184                let residual: Array1<f64> =
1185                    Array1::from_iter(y_pred.iter().zip(y_true.iter()).map(|(p, t)| *p - *t));
1186                total_loss += residual.iter().map(|r| r * r).sum::<f64>() / output_dim as f64;
1187
1188                // Accumulate gradients.
1189                for o in 0..output_dim {
1190                    let w_start = o * (input_dim + 1);
1191                    let err: f64 = residual[o] * 2.0 / output_dim as f64;
1192                    for i in 0..input_dim {
1193                        grad[w_start + i] += err * x[i];
1194                    }
1195                    grad[o * (input_dim + 1) + input_dim] += err;
1196                }
1197            }
1198
1199            let n: f64 = training_data.len() as f64;
1200            total_loss /= n;
1201
1202            // SGD parameter update.
1203            for i in 0..n_params {
1204                params[i] -= lr * grad[i] / n;
1205            }
1206
1207            // Quantum tunneling: stochastic perturbation to escape local minima.
1208            {
1209                let mut rng = self.rng.lock().expect("RNG lock poisoned");
1210                if rng.random::<f64>() < tunneling_prob {
1211                    let j = (rng.random::<f64>() * n_params as f64) as usize % n_params;
1212                    params[j] += (rng.random::<f64>() - 0.5) * 0.01;
1213                }
1214            }
1215
1216            loss_history.push(total_loss);
1217
1218            // Early-exit if loss is not improving.
1219            if epoch >= 2 && (loss_history[epoch - 1] - total_loss).abs() < 1e-8 {
1220                break;
1221            }
1222        }
1223
1224        // Validation accuracy: fraction of samples whose mean absolute error is
1225        // within 10 % of the target magnitude (floor 0.1 near zero).
1226        let validation_accuracy = {
1227            let mut correct = 0_usize;
1228            for (x, y_true) in training_data {
1229                let mut y_pred: Array1<f64> = Array1::zeros(output_dim);
1230                for o in 0..output_dim {
1231                    let w_start = o * (input_dim + 1);
1232                    for i in 0..input_dim {
1233                        y_pred[o] += params[w_start + i] * x[i];
1234                    }
1235                    y_pred[o] += params[o * (input_dim + 1) + input_dim];
1236                }
1237                let err: f64 = y_pred
1238                    .iter()
1239                    .zip(y_true.iter())
1240                    .map(|(p, t)| (p - t).abs())
1241                    .sum::<f64>()
1242                    / output_dim as f64;
1243                let mag: f64 = y_true.iter().map(|t| t.abs()).sum::<f64>() / output_dim as f64;
1244                if err < 0.1 * mag.max(1.0) {
1245                    correct += 1;
1246                }
1247            }
1248            correct as f64 / training_data.len() as f64
1249        };
1250
1251        Ok(MLTrainingResult {
1252            parameters: params,
1253            loss_history,
1254            validation_accuracy,
1255            training_time: 0.0,
1256            complexity_metrics: HashMap::new(),
1257        })
1258    }
1259
1260    /// Perform quantum-inspired Metropolis-Hastings MCMC sampling.
1261    ///
1262    /// Uses the framework's configured objective function as the (negative log) energy.
1263    /// Quantum tunneling jumps are mixed in with probability
1264    /// `quantum_parameters.tunneling_probability` to improve chain mixing.
1265    pub fn sample(&mut self) -> Result<SamplingResult> {
1266        let n_samples = self.config.algorithm_config.max_iterations.max(100);
1267        let num_vars = self.config.num_variables;
1268        let bounds = self.config.optimization_config.bounds.clone();
1269        let tunneling_prob = self
1270            .config
1271            .algorithm_config
1272            .quantum_parameters
1273            .tunneling_probability;
1274
1275        let mut samples: Vec<Array1<f64>> = Vec::with_capacity(n_samples);
1276        let mut accepted = 0_usize;
1277
1278        // Initialise current state uniformly within bounds.
1279        let mut current: Array1<f64> = {
1280            let mut rng = self.rng.lock().expect("RNG lock poisoned");
1281            let mut v = Array1::zeros(num_vars);
1282            for j in 0..num_vars {
1283                let (lo, hi) = if j < bounds.len() {
1284                    bounds[j]
1285                } else {
1286                    (-10.0, 10.0)
1287                };
1288                v[j] = rng.random::<f64>().mul_add(hi - lo, lo);
1289            }
1290            v
1291        };
1292        let mut current_energy = self.evaluate_objective(&current)?;
1293
1294        // Fixed temperature — callers may extend with annealing schedules later.
1295        let temperature = 1.0_f64;
1296
1297        for _ in 0..n_samples {
1298            // Propose new state: local Gaussian step or full quantum tunnel jump.
1299            let proposed: Array1<f64> = {
1300                let mut rng = self.rng.lock().expect("RNG lock poisoned");
1301                let mut prop = current.clone();
1302                for j in 0..num_vars {
1303                    let (lo, hi) = if j < bounds.len() {
1304                        bounds[j]
1305                    } else {
1306                        (-10.0, 10.0)
1307                    };
1308                    let step = (hi - lo) * 0.1;
1309                    if rng.random::<f64>() < tunneling_prob {
1310                        // Quantum tunneling: resample coordinate uniformly.
1311                        prop[j] = rng.random::<f64>().mul_add(hi - lo, lo);
1312                    } else {
1313                        prop[j] = (current[j] + (rng.random::<f64>() - 0.5) * step).clamp(lo, hi);
1314                    }
1315                }
1316                prop
1317            };
1318
1319            let proposed_energy = self.evaluate_objective(&proposed)?;
1320            let delta = proposed_energy - current_energy;
1321
1322            let accept = {
1323                let mut rng = self.rng.lock().expect("RNG lock poisoned");
1324                delta < 0.0 || rng.random::<f64>() < (-delta / temperature).exp()
1325            };
1326
1327            if accept {
1328                current = proposed;
1329                current_energy = proposed_energy;
1330                accepted += 1;
1331            }
1332            samples.push(current.clone());
1333        }
1334
1335        let acceptance_rate = accepted as f64 / n_samples as f64;
1336
1337        // Pack samples into a 2-D array (n_samples × num_vars).
1338        let mut samples_array = Array2::zeros((n_samples, num_vars));
1339        for (i, s) in samples.iter().enumerate() {
1340            samples_array.row_mut(i).assign(s);
1341        }
1342
1343        // Per-variable statistics.
1344        let mean: Array1<f64> = samples_array
1345            .mean_axis(scirs2_core::ndarray::Axis(0))
1346            .unwrap_or_else(|| Array1::zeros(num_vars));
1347        let variance: Array1<f64> = samples_array.var_axis(scirs2_core::ndarray::Axis(0), 0.0);
1348
1349        // Skewness: E[(X-mu)^3] / sigma^3.
1350        let skewness: Array1<f64> = Array1::from_iter((0..num_vars).map(|j| {
1351            let mu = mean[j];
1352            let sigma = variance[j].sqrt();
1353            if sigma < 1e-12 {
1354                return 0.0;
1355            }
1356            let m3: f64 = samples_array
1357                .column(j)
1358                .iter()
1359                .map(|&v| (v - mu).powi(3))
1360                .sum::<f64>()
1361                / n_samples as f64;
1362            m3 / sigma.powi(3)
1363        }));
1364
1365        // Excess kurtosis: E[(X-mu)^4] / sigma^4 - 3.
1366        let kurtosis: Array1<f64> = Array1::from_iter((0..num_vars).map(|j| {
1367            let mu = mean[j];
1368            let sigma = variance[j].sqrt();
1369            if sigma < 1e-12 {
1370                return 0.0;
1371            }
1372            let m4: f64 = samples_array
1373                .column(j)
1374                .iter()
1375                .map(|&v| (v - mu).powi(4))
1376                .sum::<f64>()
1377                / n_samples as f64;
1378            m4 / sigma.powi(4) - 3.0
1379        }));
1380
1381        // Pearson correlation matrix (num_vars × num_vars).
1382        let mut correlation_matrix = Array2::zeros((num_vars, num_vars));
1383        for i in 0..num_vars {
1384            for j in 0..num_vars {
1385                if i == j {
1386                    correlation_matrix[[i, j]] = 1.0;
1387                    continue;
1388                }
1389                let mu_i = mean[i];
1390                let mu_j = mean[j];
1391                let si = variance[i].sqrt();
1392                let sj = variance[j].sqrt();
1393                if si < 1e-12 || sj < 1e-12 {
1394                    continue;
1395                }
1396                let cov: f64 = samples_array
1397                    .column(i)
1398                    .iter()
1399                    .zip(samples_array.column(j).iter())
1400                    .map(|(&a, &b)| (a - mu_i) * (b - mu_j))
1401                    .sum::<f64>()
1402                    / n_samples as f64;
1403                correlation_matrix[[i, j]] = cov / (si * sj);
1404            }
1405        }
1406
1407        let effective_sample_size = (n_samples as f64 * acceptance_rate).max(1.0) as usize;
1408        // Autocorrelation time lower-bounded at 1.
1409        let autocorr_times: Array1<f64> =
1410            Array1::from_elem(num_vars, (1.0_f64 / acceptance_rate.max(1e-6)).max(1.0));
1411
1412        Ok(SamplingResult {
1413            samples: samples_array,
1414            statistics: SampleStatistics {
1415                mean,
1416                variance,
1417                skewness,
1418                kurtosis,
1419                correlation_matrix,
1420            },
1421            acceptance_rate,
1422            effective_sample_size,
1423            autocorr_times,
1424        })
1425    }
1426
1427    /// Solve a complex linear system `A · x = b` using a quantum-inspired iterative
1428    /// Gauss-Seidel method.
1429    ///
1430    /// The solver terminates early when the successive-iterate L2 difference drops
1431    /// below `algorithm_config.tolerance`.  Callers should check `residual_norm` to
1432    /// verify convergence for ill-conditioned or non-diagonally-dominant matrices.
1433    pub fn solve_linear_algebra(
1434        &mut self,
1435        matrix: &Array2<Complex64>,
1436        rhs: &Array1<Complex64>,
1437    ) -> Result<LinalgResult> {
1438        let n = matrix.nrows();
1439        if n != matrix.ncols() {
1440            return Err(SimulatorError::InvalidInput(format!(
1441                "Matrix must be square: got {}×{}",
1442                n,
1443                matrix.ncols()
1444            )));
1445        }
1446        if n != rhs.len() {
1447            return Err(SimulatorError::InvalidInput(format!(
1448                "Matrix dimension {} incompatible with rhs length {}",
1449                n,
1450                rhs.len()
1451            )));
1452        }
1453        if n == 0 {
1454            return Ok(LinalgResult {
1455                solution: Array1::zeros(0),
1456                eigenvalues: None,
1457                eigenvectors: None,
1458                singular_values: None,
1459                residual_norm: 0.0,
1460                iterations: 0,
1461            });
1462        }
1463
1464        let max_iter = self.config.algorithm_config.max_iterations.max(1000);
1465        let tol = self.config.algorithm_config.tolerance.max(1e-10);
1466
1467        let mut x: Array1<Complex64> = Array1::from_elem(n, Complex64::new(0.0, 0.0));
1468
1469        let mut converged_iter = max_iter;
1470
1471        for iter in 0..max_iter {
1472            let x_old = x.clone();
1473
1474            // Gauss-Seidel sweep.
1475            for i in 0..n {
1476                let diag = matrix[[i, i]];
1477                if diag.norm() < 1e-14 {
1478                    // Skip singular diagonal entries.
1479                    continue;
1480                }
1481                let mut sigma = Complex64::new(0.0, 0.0);
1482                for j in 0..n {
1483                    if j != i {
1484                        sigma += matrix[[i, j]] * x[j];
1485                    }
1486                }
1487                x[i] = (rhs[i] - sigma) / diag;
1488            }
1489
1490            // Convergence: L2 norm of iterate change.
1491            let delta: f64 = x
1492                .iter()
1493                .zip(x_old.iter())
1494                .map(|(a, b)| (a - b).norm())
1495                .sum::<f64>();
1496
1497            if delta < tol {
1498                converged_iter = iter + 1;
1499                break;
1500            }
1501        }
1502
1503        // Final residual ‖A·x − b‖₁.
1504        let ax: Array1<Complex64> = Array1::from_iter((0..n).map(|i| {
1505            (0..n)
1506                .map(|j| matrix[[i, j]] * x[j])
1507                .fold(Complex64::new(0.0, 0.0), |acc, v| acc + v)
1508        }));
1509        let residual_norm: f64 = ax
1510            .iter()
1511            .zip(rhs.iter())
1512            .map(|(a, b)| (a - b).norm())
1513            .sum::<f64>();
1514
1515        Ok(LinalgResult {
1516            solution: x,
1517            eigenvalues: None,
1518            eigenvectors: None,
1519            singular_values: None,
1520            residual_norm,
1521            iterations: converged_iter,
1522        })
1523    }
1524
1525    /// Solve a graph optimisation problem on a weighted undirected graph represented
1526    /// by `adjacency_matrix`.
1527    ///
1528    /// Applies quantum-inspired greedy graph colouring and computes the induced
1529    /// max-cut value (sum of edge weights crossing the even/odd colour bipartition).
1530    /// Graph metrics (modularity, clustering coefficient, average path length,
1531    /// diameter) are also computed via BFS.
1532    pub fn solve_graph_problem(&mut self, adjacency_matrix: &Array2<f64>) -> Result<GraphResult> {
1533        let n = adjacency_matrix.nrows();
1534        if n == 0 {
1535            return Ok(GraphResult {
1536                solution: vec![],
1537                objective_value: 0.0,
1538                graph_metrics: GraphMetrics {
1539                    modularity: 0.0,
1540                    clustering_coefficient: 0.0,
1541                    average_path_length: 0.0,
1542                    diameter: 0,
1543                },
1544                walk_stats: None,
1545            });
1546        }
1547
1548        // Quantum-inspired greedy graph colouring.
1549        // With probability `tunneling_probability` the assigned colour is incremented
1550        // by 1 (a small quantum perturbation that can expose better colourings).
1551        let tunneling_prob = self
1552            .config
1553            .algorithm_config
1554            .quantum_parameters
1555            .tunneling_probability;
1556
1557        let mut coloring = vec![0_usize; n];
1558        let mut max_color = 0_usize;
1559
1560        for v in 0..n {
1561            let mut neighbor_colors: std::collections::HashSet<usize> =
1562                std::collections::HashSet::new();
1563            for u in 0..n {
1564                if adjacency_matrix[[v, u]] > 0.0 {
1565                    neighbor_colors.insert(coloring[u]);
1566                }
1567            }
1568
1569            let mut color = 0;
1570            while neighbor_colors.contains(&color) {
1571                color += 1;
1572            }
1573
1574            // Quantum tunneling perturbation.
1575            {
1576                let mut rng = self.rng.lock().expect("RNG lock poisoned");
1577                if rng.random::<f64>() < tunneling_prob && color <= max_color {
1578                    color = (color + 1).min(max_color + 1);
1579                }
1580            }
1581
1582            coloring[v] = color;
1583            if color > max_color {
1584                max_color = color;
1585            }
1586        }
1587
1588        // Max-cut: sum of weights crossing the even/odd colour bipartition.
1589        let mut cut_value = 0.0_f64;
1590        for u in 0..n {
1591            for v in (u + 1)..n {
1592                if coloring[u] % 2 != coloring[v] % 2 {
1593                    cut_value += adjacency_matrix[[u, v]];
1594                }
1595            }
1596        }
1597
1598        // Degree sequence.
1599        let degrees: Vec<f64> = (0..n)
1600            .map(|v| (0..n).map(|u| adjacency_matrix[[v, u]].abs()).sum::<f64>())
1601            .collect();
1602
1603        // Local clustering coefficient averaged over all vertices.
1604        let clustering_coefficient = {
1605            let mut total_cc = 0.0_f64;
1606            for v in 0..n {
1607                let neighbors: Vec<usize> = (0..n)
1608                    .filter(|&u| u != v && adjacency_matrix[[v, u]] > 0.0)
1609                    .collect();
1610                let k = neighbors.len();
1611                if k < 2 {
1612                    continue;
1613                }
1614                let mut triangles = 0_usize;
1615                for i in 0..k {
1616                    for j in (i + 1)..k {
1617                        if adjacency_matrix[[neighbors[i], neighbors[j]]] > 0.0 {
1618                            triangles += 1;
1619                        }
1620                    }
1621                }
1622                total_cc += triangles as f64 / (k * (k - 1) / 2) as f64;
1623            }
1624            total_cc / n as f64
1625        };
1626
1627        // Average path length and diameter via BFS on the unweighted adjacency.
1628        let (average_path_length, diameter) = {
1629            let mut total_dist = 0_usize;
1630            let mut max_dist = 0_usize;
1631            let mut reachable_pairs = 0_usize;
1632
1633            for src in 0..n {
1634                let mut dist = vec![usize::MAX; n];
1635                dist[src] = 0;
1636                let mut queue = std::collections::VecDeque::new();
1637                queue.push_back(src);
1638                while let Some(u) = queue.pop_front() {
1639                    for v in 0..n {
1640                        if adjacency_matrix[[u, v]] > 0.0 && dist[v] == usize::MAX {
1641                            dist[v] = dist[u] + 1;
1642                            queue.push_back(v);
1643                        }
1644                    }
1645                }
1646                for dst in 0..n {
1647                    if dst != src && dist[dst] != usize::MAX {
1648                        total_dist += dist[dst];
1649                        reachable_pairs += 1;
1650                        if dist[dst] > max_dist {
1651                            max_dist = dist[dst];
1652                        }
1653                    }
1654                }
1655            }
1656
1657            let avg = if reachable_pairs > 0 {
1658                total_dist as f64 / reachable_pairs as f64
1659            } else {
1660                0.0
1661            };
1662            (avg, max_dist)
1663        };
1664
1665        // Newman-Girvan modularity for the colour partition.
1666        let total_weight: f64 = degrees.iter().sum::<f64>() / 2.0;
1667        let modularity = if total_weight > 1e-12 {
1668            let two_m = 2.0 * total_weight;
1669            let mut q = 0.0_f64;
1670            for u in 0..n {
1671                for v in 0..n {
1672                    if coloring[u] == coloring[v] {
1673                        let a_uv = adjacency_matrix[[u, v]];
1674                        let expected = degrees[u] * degrees[v] / two_m;
1675                        q += a_uv - expected;
1676                    }
1677                }
1678            }
1679            q / two_m
1680        } else {
1681            0.0
1682        };
1683
1684        Ok(GraphResult {
1685            solution: coloring,
1686            objective_value: cut_value,
1687            graph_metrics: GraphMetrics {
1688                modularity,
1689                clustering_coefficient,
1690                average_path_length,
1691                diameter,
1692            },
1693            walk_stats: None,
1694        })
1695    }
1696    /// Get current statistics
1697    #[must_use]
1698    pub const fn get_stats(&self) -> &QuantumInspiredStats {
1699        &self.stats
1700    }
1701    /// Get current state
1702    #[must_use]
1703    pub const fn get_state(&self) -> &QuantumInspiredState {
1704        &self.state
1705    }
1706    /// Get mutable state access
1707    pub const fn get_state_mut(&mut self) -> &mut QuantumInspiredState {
1708        &mut self.state
1709    }
1710    /// Evaluate objective function (public version)
1711    pub fn evaluate_objective_public(&mut self, solution: &Array1<f64>) -> Result<f64> {
1712        self.evaluate_objective(solution)
1713    }
1714    /// Check convergence (public version)
1715    pub fn check_convergence_public(&self) -> Result<bool> {
1716        self.check_convergence()
1717    }
1718    /// Reset framework state
1719    pub fn reset(&mut self) {
1720        self.state = QuantumInspiredState {
1721            variables: Array1::zeros(self.config.num_variables),
1722            objective_value: f64::INFINITY,
1723            iteration: 0,
1724            best_solution: Array1::zeros(self.config.num_variables),
1725            best_objective: f64::INFINITY,
1726            convergence_history: Vec::new(),
1727            runtime_stats: RuntimeStats::default(),
1728        };
1729        self.stats = QuantumInspiredStats::default();
1730    }
1731}