Skip to main content

quantrs2_anneal/
variational_quantum_annealing.rs

1//! Variational Quantum Annealing for advanced optimization
2//!
3//! This module implements variational quantum annealing (VQA) algorithms that combine
4//! classical optimization with quantum annealing to solve complex optimization problems.
5//! VQA uses parameterized quantum circuits and classical optimization to find optimal
6//! solutions through iterative refinement.
7
8use scirs2_core::random::prelude::*;
9use scirs2_core::random::ChaCha8Rng;
10use scirs2_core::random::{Rng, SeedableRng};
11use std::collections::HashMap;
12use std::time::{Duration, Instant};
13use thiserror::Error;
14
15use crate::ising::{IsingError, IsingModel};
16use crate::simulator::{AnnealingParams, AnnealingSolution, QuantumAnnealingSimulator};
17
18/// Errors that can occur in variational quantum annealing
19#[derive(Error, Debug)]
20pub enum VqaError {
21    /// Ising model error
22    #[error("Ising error: {0}")]
23    IsingError(#[from] IsingError),
24
25    /// Invalid variational parameters
26    #[error("Invalid parameters: {0}")]
27    InvalidParameters(String),
28
29    /// Optimization failed
30    #[error("Optimization failed: {0}")]
31    OptimizationFailed(String),
32
33    /// Circuit construction error
34    #[error("Circuit error: {0}")]
35    CircuitError(String),
36
37    /// Convergence error
38    #[error("Convergence error: {0}")]
39    ConvergenceError(String),
40}
41
42/// Result type for VQA operations
43pub type VqaResult<T> = Result<T, VqaError>;
44
45/// Types of variational ansatz circuits
46#[derive(Debug, Clone, PartialEq)]
47pub enum AnsatzType {
48    /// Hardware-efficient ansatz with parameterized rotations
49    HardwareEfficient {
50        depth: usize,
51        entangling_gates: EntanglingGateType,
52    },
53
54    /// QAOA-inspired ansatz with problem and mixer terms
55    QaoaInspired {
56        layers: usize,
57        mixer_type: MixerType,
58    },
59
60    /// Adiabatic-inspired ansatz with time evolution
61    AdiabaticInspired {
62        time_steps: usize,
63        evolution_time: f64,
64    },
65
66    /// Custom ansatz with user-defined structure
67    Custom { structure: Vec<QuantumGate> },
68}
69
70/// Types of entangling gates for hardware-efficient ansatz
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum EntanglingGateType {
73    /// CNOT gates in nearest-neighbor topology
74    CNot,
75    /// Controlled-Z gates
76    CZ,
77    /// Ising-style ZZ interactions
78    ZZ,
79    /// XY gates for spin exchange
80    XY,
81}
82
83/// Types of mixer operations
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum MixerType {
86    /// X-rotation mixer (transverse field)
87    XRotation,
88    /// XY mixer for hard constraints
89    XY,
90    /// Multi-angle mixer
91    MultiAngle,
92}
93
94/// Quantum gate representation for variational circuits
95#[derive(Debug, Clone, PartialEq)]
96pub enum QuantumGate {
97    /// Single-qubit rotation around X-axis
98    RX { qubit: usize, angle: ParameterRef },
99    /// Single-qubit rotation around Y-axis
100    RY { qubit: usize, angle: ParameterRef },
101    /// Single-qubit rotation around Z-axis
102    RZ { qubit: usize, angle: ParameterRef },
103    /// Two-qubit CNOT gate
104    CNOT { control: usize, target: usize },
105    /// Two-qubit controlled-Z gate
106    CZ { control: usize, target: usize },
107    /// Parameterized ZZ interaction
108    ZZ {
109        qubit1: usize,
110        qubit2: usize,
111        angle: ParameterRef,
112    },
113}
114
115/// Reference to a variational parameter
116#[derive(Debug, Clone, PartialEq)]
117pub struct ParameterRef {
118    /// Parameter index
119    pub index: usize,
120    /// Optional scaling factor
121    pub scale: f64,
122}
123
124impl ParameterRef {
125    /// Create a new parameter reference
126    #[must_use]
127    pub const fn new(index: usize) -> Self {
128        Self { index, scale: 1.0 }
129    }
130
131    /// Create a scaled parameter reference
132    #[must_use]
133    pub const fn scaled(index: usize, scale: f64) -> Self {
134        Self { index, scale }
135    }
136}
137
138/// Variational quantum annealing configuration
139#[derive(Debug, Clone)]
140pub struct VqaConfig {
141    /// Ansatz type and structure
142    pub ansatz: AnsatzType,
143
144    /// Classical optimizer for parameters
145    pub optimizer: ClassicalOptimizer,
146
147    /// Maximum iterations for variational optimization
148    pub max_iterations: usize,
149
150    /// Convergence tolerance
151    pub convergence_tolerance: f64,
152
153    /// Number of quantum annealing shots per evaluation
154    pub num_shots: usize,
155
156    /// Base annealing parameters
157    pub annealing_params: AnnealingParams,
158
159    /// Parameter initialization range
160    pub parameter_init_range: (f64, f64),
161
162    /// Use gradient-based optimization
163    pub use_gradients: bool,
164
165    /// Finite difference step for gradient estimation
166    pub gradient_step: f64,
167
168    /// Random seed
169    pub seed: Option<u64>,
170
171    /// Maximum runtime
172    pub max_runtime: Option<Duration>,
173
174    /// Logging frequency
175    pub log_frequency: usize,
176}
177
178impl Default for VqaConfig {
179    fn default() -> Self {
180        Self {
181            ansatz: AnsatzType::HardwareEfficient {
182                depth: 3,
183                entangling_gates: EntanglingGateType::CNot,
184            },
185            optimizer: ClassicalOptimizer::Adam {
186                learning_rate: 0.01,
187                beta1: 0.9,
188                beta2: 0.999,
189                epsilon: 1e-8,
190            },
191            max_iterations: 100,
192            convergence_tolerance: 1e-6,
193            num_shots: 100,
194            annealing_params: AnnealingParams::default(),
195            parameter_init_range: (-0.5, 0.5),
196            use_gradients: true,
197            gradient_step: 0.01,
198            seed: None,
199            max_runtime: Some(Duration::from_secs(3600)),
200            log_frequency: 10,
201        }
202    }
203}
204
205/// Classical optimizers for variational parameters
206#[derive(Debug, Clone)]
207pub enum ClassicalOptimizer {
208    /// Gradient descent
209    GradientDescent { learning_rate: f64 },
210
211    /// Adam optimizer
212    Adam {
213        learning_rate: f64,
214        beta1: f64,
215        beta2: f64,
216        epsilon: f64,
217    },
218
219    /// `RMSprop` optimizer
220    RMSprop {
221        learning_rate: f64,
222        decay_rate: f64,
223        epsilon: f64,
224    },
225
226    /// Nelder-Mead simplex
227    NelderMead {
228        initial_simplex_size: f64,
229        alpha: f64,
230        gamma: f64,
231        rho: f64,
232        sigma: f64,
233    },
234
235    /// BFGS quasi-Newton method
236    BFGS {
237        line_search_tolerance: f64,
238        max_line_search_iterations: usize,
239    },
240}
241
242/// Variational quantum annealing results
243#[derive(Debug, Clone)]
244pub struct VqaResults {
245    /// Best solution found
246    pub best_solution: Vec<i8>,
247
248    /// Best energy achieved
249    pub best_energy: f64,
250
251    /// Optimal variational parameters
252    pub optimal_parameters: Vec<f64>,
253
254    /// Energy history over iterations
255    pub energy_history: Vec<f64>,
256
257    /// Parameter history over iterations
258    pub parameter_history: Vec<Vec<f64>>,
259
260    /// Gradient norms over iterations
261    pub gradient_norms: Vec<f64>,
262
263    /// Number of iterations completed
264    pub iterations_completed: usize,
265
266    /// Convergence achieved
267    pub converged: bool,
268
269    /// Total optimization time
270    pub total_time: Duration,
271
272    /// Statistics about the optimization
273    pub statistics: VqaStatistics,
274}
275
276/// Statistics for VQA optimization
277#[derive(Debug, Clone)]
278pub struct VqaStatistics {
279    /// Total function evaluations
280    pub function_evaluations: usize,
281
282    /// Total gradient evaluations
283    pub gradient_evaluations: usize,
284
285    /// Total quantum annealing time
286    pub total_annealing_time: Duration,
287
288    /// Average energy per iteration
289    pub average_energy: f64,
290
291    /// Energy variance over iterations
292    pub energy_variance: f64,
293
294    /// Parameter update statistics
295    pub parameter_stats: ParameterStatistics,
296
297    /// Classical optimizer performance
298    pub optimizer_stats: OptimizerStatistics,
299}
300
301/// Statistics about parameter updates
302#[derive(Debug, Clone)]
303pub struct ParameterStatistics {
304    /// Average parameter magnitude
305    pub average_magnitude: f64,
306
307    /// Parameter variance
308    pub parameter_variance: f64,
309
310    /// Number of parameter updates
311    pub num_updates: usize,
312
313    /// Largest parameter change per iteration
314    pub max_parameter_change: Vec<f64>,
315}
316
317/// Statistics about classical optimizer performance
318#[derive(Debug, Clone)]
319pub struct OptimizerStatistics {
320    /// Step acceptance rate
321    pub step_acceptance_rate: f64,
322
323    /// Average step size
324    pub average_step_size: f64,
325
326    /// Number of line search iterations (for applicable optimizers)
327    pub line_search_iterations: usize,
328
329    /// Optimizer-specific metrics
330    pub optimizer_metrics: HashMap<String, f64>,
331}
332
333/// Variational quantum annealing optimizer
334pub struct VariationalQuantumAnnealer {
335    /// Configuration
336    config: VqaConfig,
337
338    /// Current variational parameters
339    parameters: Vec<f64>,
340
341    /// Classical optimizer state
342    optimizer_state: OptimizerState,
343
344    /// Random number generator
345    rng: ChaCha8Rng,
346
347    /// Optimization history
348    history: OptimizationHistory,
349}
350
351/// Internal state for classical optimizers
352#[derive(Debug)]
353enum OptimizerState {
354    GradientDescent {
355        momentum: Option<Vec<f64>>,
356    },
357
358    Adam {
359        m: Vec<f64>, // First moment estimate
360        v: Vec<f64>, // Second moment estimate
361        t: usize,    // Time step
362    },
363
364    RMSprop {
365        s: Vec<f64>, // Moving average of squared gradients
366    },
367
368    NelderMead {
369        simplex: Vec<Vec<f64>>,
370        function_values: Vec<f64>,
371    },
372
373    BFGS {
374        hessian_inverse: Vec<Vec<f64>>,
375        previous_gradient: Option<Vec<f64>>,
376        previous_parameters: Option<Vec<f64>>,
377    },
378}
379
380/// Optimization history tracking
381#[derive(Debug)]
382struct OptimizationHistory {
383    energies: Vec<f64>,
384    parameters: Vec<Vec<f64>>,
385    gradients: Vec<Vec<f64>>,
386    function_evals: usize,
387    gradient_evals: usize,
388    start_time: Instant,
389}
390
391impl VariationalQuantumAnnealer {
392    /// Create a new variational quantum annealer
393    pub fn new(config: VqaConfig) -> VqaResult<Self> {
394        let num_parameters = Self::count_parameters(&config.ansatz)?;
395
396        let rng = match config.seed {
397            Some(seed) => ChaCha8Rng::seed_from_u64(seed),
398            None => ChaCha8Rng::seed_from_u64(thread_rng().random()),
399        };
400
401        let mut vqa = Self {
402            config: config.clone(),
403            parameters: vec![0.0; num_parameters],
404            optimizer_state: Self::initialize_optimizer_state(&config.optimizer, num_parameters)?,
405            rng,
406            history: OptimizationHistory {
407                energies: Vec::new(),
408                parameters: Vec::new(),
409                gradients: Vec::new(),
410                function_evals: 0,
411                gradient_evals: 0,
412                start_time: Instant::now(),
413            },
414        };
415
416        vqa.initialize_parameters()?;
417        Ok(vqa)
418    }
419
420    /// Count the number of parameters in an ansatz.
421    ///
422    /// The hardware-efficient ansatz parameter count depends on the number of
423    /// qubits, which is not known until a problem is supplied. At construction
424    /// time we therefore size it with a single-qubit estimate; [`optimize`]
425    /// recomputes the exact count for the actual problem (via
426    /// [`Self::exact_parameter_count`]) and resizes the parameter vector before
427    /// optimization begins, so no gates are ever silently dropped.
428    fn count_parameters(ansatz: &AnsatzType) -> VqaResult<usize> {
429        Self::exact_parameter_count(ansatz, 1)
430    }
431
432    /// Exact number of variational parameters for `ansatz` on `num_qubits`.
433    ///
434    /// This mirrors the gate-emission logic of the circuit builders so the
435    /// parameter vector is always exactly the size the circuit consumes:
436    /// - hardware-efficient: `depth · (2·n + e)` where `e` is the per-layer
437    ///   count of parameterised entangling gates (`n-1` for `ZZ`/`XY`, `0` for
438    ///   `CNot`/`CZ`),
439    /// - QAOA-inspired: `2·layers` (one γ and one β per layer),
440    /// - adiabatic-inspired: one angle per time step,
441    /// - custom: one past the largest referenced parameter index.
442    fn exact_parameter_count(ansatz: &AnsatzType, num_qubits: usize) -> VqaResult<usize> {
443        match ansatz {
444            AnsatzType::HardwareEfficient {
445                depth,
446                entangling_gates,
447            } => {
448                let rotations_per_layer = 2 * num_qubits;
449                let entangling_params_per_layer = match entangling_gates {
450                    EntanglingGateType::ZZ | EntanglingGateType::XY => num_qubits.saturating_sub(1),
451                    EntanglingGateType::CNot | EntanglingGateType::CZ => 0,
452                };
453                Ok(depth * (rotations_per_layer + entangling_params_per_layer))
454            }
455
456            AnsatzType::QaoaInspired { layers, .. } => {
457                // For QAOA: 2 parameters per layer (gamma and beta)
458                Ok(layers * 2)
459            }
460
461            AnsatzType::AdiabaticInspired { time_steps, .. } => {
462                // For adiabatic: one parameter per time step
463                Ok(*time_steps)
464            }
465
466            AnsatzType::Custom { structure } => {
467                // Count unique parameter references
468                let mut max_param_index = 0;
469                for gate in structure {
470                    if let Some(param_ref) = Self::extract_parameter_ref(gate) {
471                        max_param_index = max_param_index.max(param_ref.index);
472                    }
473                }
474                Ok(max_param_index + 1)
475            }
476        }
477    }
478
479    /// Extract parameter reference from a gate
480    const fn extract_parameter_ref(gate: &QuantumGate) -> Option<&ParameterRef> {
481        match gate {
482            QuantumGate::RX { angle, .. }
483            | QuantumGate::RY { angle, .. }
484            | QuantumGate::RZ { angle, .. }
485            | QuantumGate::ZZ { angle, .. } => Some(angle),
486            _ => None,
487        }
488    }
489
490    /// Initialize optimizer state
491    fn initialize_optimizer_state(
492        optimizer: &ClassicalOptimizer,
493        num_params: usize,
494    ) -> VqaResult<OptimizerState> {
495        match optimizer {
496            ClassicalOptimizer::GradientDescent { .. } => {
497                Ok(OptimizerState::GradientDescent { momentum: None })
498            }
499
500            ClassicalOptimizer::Adam { .. } => Ok(OptimizerState::Adam {
501                m: vec![0.0; num_params],
502                v: vec![0.0; num_params],
503                t: 0,
504            }),
505
506            ClassicalOptimizer::RMSprop { .. } => Ok(OptimizerState::RMSprop {
507                s: vec![0.0; num_params],
508            }),
509
510            ClassicalOptimizer::NelderMead {
511                initial_simplex_size,
512                ..
513            } => {
514                // Initialize simplex
515                let mut simplex = vec![vec![0.0; num_params]; num_params + 1];
516                for i in 0..num_params {
517                    simplex[i + 1][i] = *initial_simplex_size;
518                }
519
520                Ok(OptimizerState::NelderMead {
521                    simplex,
522                    function_values: vec![f64::INFINITY; num_params + 1],
523                })
524            }
525
526            ClassicalOptimizer::BFGS { .. } => {
527                // Initialize identity matrix for Hessian inverse
528                let mut hessian_inverse = vec![vec![0.0; num_params]; num_params];
529                for i in 0..num_params {
530                    hessian_inverse[i][i] = 1.0;
531                }
532
533                Ok(OptimizerState::BFGS {
534                    hessian_inverse,
535                    previous_gradient: None,
536                    previous_parameters: None,
537                })
538            }
539        }
540    }
541
542    /// Initialize variational parameters
543    fn initialize_parameters(&mut self) -> VqaResult<()> {
544        let (min, max) = self.config.parameter_init_range;
545
546        for param in &mut self.parameters {
547            *param = self.rng.random_range(min..max);
548        }
549
550        Ok(())
551    }
552
553    /// Optimize the variational quantum annealing problem
554    pub fn optimize(&mut self, problem: &IsingModel) -> VqaResult<VqaResults> {
555        println!("Starting variational quantum annealing optimization...");
556
557        // The exact parameter count of (e.g.) the hardware-efficient ansatz is
558        // problem-dependent. Recompute it for this problem and resize/reinit the
559        // parameter vector and optimizer state if it differs from the
560        // construction-time estimate, so the circuit is parameterised exactly.
561        let exact_params = Self::exact_parameter_count(&self.config.ansatz, problem.num_qubits)?;
562        if exact_params != self.parameters.len() {
563            self.parameters = vec![0.0; exact_params];
564            self.initialize_parameters()?;
565            self.optimizer_state =
566                Self::initialize_optimizer_state(&self.config.optimizer, exact_params)?;
567        }
568
569        self.history.start_time = Instant::now();
570        let mut best_energy = f64::INFINITY;
571        let mut best_solution = vec![0; problem.num_qubits];
572        let mut best_parameters = self.parameters.clone();
573
574        for iteration in 0..self.config.max_iterations {
575            let iteration_start = Instant::now();
576
577            // Check runtime limit
578            if let Some(max_runtime) = self.config.max_runtime {
579                if self.history.start_time.elapsed() > max_runtime {
580                    println!("Maximum runtime exceeded");
581                    break;
582                }
583            }
584
585            // Evaluate current parameters
586            let current_params = self.parameters.clone();
587            let (energy, solution) = self.evaluate_objective(problem, &current_params)?;
588
589            // Update best solution
590            if energy < best_energy {
591                best_energy = energy;
592                best_solution = solution;
593                best_parameters = self.parameters.clone();
594            }
595
596            // Record history
597            self.history.energies.push(energy);
598            self.history.parameters.push(self.parameters.clone());
599
600            // Compute gradients if needed
601            let gradients = if self.config.use_gradients {
602                let grads = self.compute_gradients(problem)?;
603                self.history.gradients.push(grads.clone());
604                Some(grads)
605            } else {
606                None
607            };
608
609            // Update parameters using classical optimizer
610            self.update_parameters(gradients.as_ref().map(std::vec::Vec::as_slice))?;
611
612            // Logging
613            if iteration % self.config.log_frequency == 0 {
614                let grad_norm = gradients
615                    .as_ref()
616                    .map_or(0.0, |g| g.iter().map(|&x| x.powi(2)).sum::<f64>().sqrt());
617
618                println!(
619                    "Iteration {}: Energy = {:.6}, Gradient norm = {:.6}, Time = {:.2?}",
620                    iteration,
621                    energy,
622                    grad_norm,
623                    iteration_start.elapsed()
624                );
625            }
626
627            // Check convergence
628            if self.check_convergence()? {
629                println!("Converged at iteration {iteration}");
630                break;
631            }
632        }
633
634        let total_time = self.history.start_time.elapsed();
635
636        // Calculate statistics
637        let statistics = self.calculate_statistics();
638
639        Ok(VqaResults {
640            best_solution,
641            best_energy,
642            optimal_parameters: best_parameters,
643            energy_history: self.history.energies.clone(),
644            parameter_history: self.history.parameters.clone(),
645            gradient_norms: self
646                .history
647                .gradients
648                .iter()
649                .map(|g| g.iter().map(|&x| x.powi(2)).sum::<f64>().sqrt())
650                .collect(),
651            iterations_completed: self.history.energies.len(),
652            converged: self.check_convergence()?,
653            total_time,
654            statistics,
655        })
656    }
657
658    /// Evaluate the objective function for given parameters
659    fn evaluate_objective(
660        &mut self,
661        problem: &IsingModel,
662        parameters: &[f64],
663    ) -> VqaResult<(f64, Vec<i8>)> {
664        self.history.function_evals += 1;
665
666        // Construct the parameterized quantum circuit
667        let circuit = self.build_quantum_circuit(problem, parameters)?;
668
669        // Execute quantum annealing with the parameterized problem
670        let modified_problem = self.apply_circuit_to_problem(problem, &circuit)?;
671
672        // Perform quantum annealing
673        let mut simulator = QuantumAnnealingSimulator::new(self.config.annealing_params.clone())
674            .map_err(|e| VqaError::OptimizationFailed(e.to_string()))?;
675
676        let mut best_energy = f64::INFINITY;
677        let mut best_solution = vec![0; problem.num_qubits];
678
679        // Multiple shots for statistical averaging
680        for _ in 0..self.config.num_shots {
681            let result = simulator
682                .solve(&modified_problem)
683                .map_err(|e| VqaError::OptimizationFailed(e.to_string()))?;
684
685            if result.best_energy < best_energy {
686                best_energy = result.best_energy;
687                best_solution = result.best_spins;
688            }
689        }
690
691        Ok((best_energy, best_solution))
692    }
693
694    /// Build the quantum circuit for current parameters
695    fn build_quantum_circuit(
696        &self,
697        problem: &IsingModel,
698        parameters: &[f64],
699    ) -> VqaResult<QuantumCircuit> {
700        let num_qubits = problem.num_qubits;
701        let mut circuit = QuantumCircuit::new(num_qubits);
702
703        match &self.config.ansatz {
704            AnsatzType::HardwareEfficient {
705                depth,
706                entangling_gates,
707            } => {
708                self.build_hardware_efficient_circuit(
709                    &mut circuit,
710                    *depth,
711                    entangling_gates,
712                    parameters,
713                )?;
714            }
715
716            AnsatzType::QaoaInspired { layers, mixer_type } => {
717                self.build_qaoa_inspired_circuit(
718                    &mut circuit,
719                    problem,
720                    *layers,
721                    mixer_type,
722                    parameters,
723                )?;
724            }
725
726            AnsatzType::AdiabaticInspired {
727                time_steps,
728                evolution_time,
729            } => {
730                self.build_adiabatic_inspired_circuit(
731                    &mut circuit,
732                    problem,
733                    *time_steps,
734                    *evolution_time,
735                    parameters,
736                )?;
737            }
738
739            AnsatzType::Custom { structure } => {
740                self.build_custom_circuit(&mut circuit, structure, parameters)?;
741            }
742        }
743
744        Ok(circuit)
745    }
746
747    /// Build hardware-efficient ansatz circuit
748    fn build_hardware_efficient_circuit(
749        &self,
750        circuit: &mut QuantumCircuit,
751        depth: usize,
752        entangling_gates: &EntanglingGateType,
753        parameters: &[f64],
754    ) -> VqaResult<()> {
755        let num_qubits = circuit.num_qubits;
756        let mut param_idx = 0;
757
758        for layer in 0..depth {
759            // Single-qubit rotations. The resolved angle is stored in the
760            // `scale` field (consistent with the QAOA/adiabatic builders) so the
761            // mean-field transform can read the actual rotation angle.
762            for qubit in 0..num_qubits {
763                if param_idx < parameters.len() {
764                    circuit.add_gate(QuantumGate::RY {
765                        qubit,
766                        angle: ParameterRef::scaled(param_idx, parameters[param_idx]),
767                    });
768                    param_idx += 1;
769                }
770
771                if param_idx < parameters.len() {
772                    circuit.add_gate(QuantumGate::RZ {
773                        qubit,
774                        angle: ParameterRef::scaled(param_idx, parameters[param_idx]),
775                    });
776                    param_idx += 1;
777                }
778            }
779
780            // Entangling gates
781            match entangling_gates {
782                EntanglingGateType::CNot => {
783                    for qubit in 0..num_qubits - 1 {
784                        circuit.add_gate(QuantumGate::CNOT {
785                            control: qubit,
786                            target: qubit + 1,
787                        });
788                    }
789                }
790
791                EntanglingGateType::CZ => {
792                    for qubit in 0..num_qubits - 1 {
793                        circuit.add_gate(QuantumGate::CZ {
794                            control: qubit,
795                            target: qubit + 1,
796                        });
797                    }
798                }
799
800                EntanglingGateType::ZZ => {
801                    for qubit in 0..num_qubits - 1 {
802                        if param_idx < parameters.len() {
803                            circuit.add_gate(QuantumGate::ZZ {
804                                qubit1: qubit,
805                                qubit2: qubit + 1,
806                                angle: ParameterRef::scaled(param_idx, parameters[param_idx]),
807                            });
808                            param_idx += 1;
809                        }
810                    }
811                }
812
813                EntanglingGateType::XY => {
814                    // Implement XY gates as combination of other gates
815                    for qubit in 0..num_qubits - 1 {
816                        if param_idx < parameters.len() {
817                            // Simplified XY implementation
818                            circuit.add_gate(QuantumGate::CNOT {
819                                control: qubit,
820                                target: qubit + 1,
821                            });
822                            param_idx += 1;
823                        }
824                    }
825                }
826            }
827        }
828
829        Ok(())
830    }
831
832    /// Build QAOA-inspired circuit
833    fn build_qaoa_inspired_circuit(
834        &self,
835        circuit: &mut QuantumCircuit,
836        problem: &IsingModel,
837        layers: usize,
838        mixer_type: &MixerType,
839        parameters: &[f64],
840    ) -> VqaResult<()> {
841        let num_qubits = circuit.num_qubits;
842
843        for layer in 0..layers {
844            let gamma_idx = layer * 2;
845            let beta_idx = layer * 2 + 1;
846
847            if gamma_idx >= parameters.len() || beta_idx >= parameters.len() {
848                break;
849            }
850
851            let gamma = parameters[gamma_idx];
852            let beta = parameters[beta_idx];
853
854            // Problem Hamiltonian layer (ZZ interactions)
855            for i in 0..num_qubits {
856                for j in (i + 1)..num_qubits {
857                    if let Ok(coupling) = problem.get_coupling(i, j) {
858                        if coupling != 0.0 {
859                            circuit.add_gate(QuantumGate::ZZ {
860                                qubit1: i,
861                                qubit2: j,
862                                angle: ParameterRef::scaled(gamma_idx, gamma * coupling),
863                            });
864                        }
865                    }
866                }
867
868                // Bias terms
869                if let Ok(bias) = problem.get_bias(i) {
870                    if bias != 0.0 {
871                        circuit.add_gate(QuantumGate::RZ {
872                            qubit: i,
873                            angle: ParameterRef::scaled(gamma_idx, gamma * bias),
874                        });
875                    }
876                }
877            }
878
879            // Mixer layer
880            match mixer_type {
881                MixerType::XRotation => {
882                    for qubit in 0..num_qubits {
883                        circuit.add_gate(QuantumGate::RX {
884                            qubit,
885                            angle: ParameterRef::scaled(beta_idx, beta),
886                        });
887                    }
888                }
889
890                MixerType::XY => {
891                    // XY mixer for hard constraints
892                    for qubit in 0..num_qubits - 1 {
893                        circuit.add_gate(QuantumGate::CNOT {
894                            control: qubit,
895                            target: qubit + 1,
896                        });
897                    }
898                }
899
900                MixerType::MultiAngle => {
901                    // Multi-angle mixer
902                    for qubit in 0..num_qubits {
903                        circuit.add_gate(QuantumGate::RX {
904                            qubit,
905                            angle: ParameterRef::scaled(beta_idx, beta),
906                        });
907                        circuit.add_gate(QuantumGate::RY {
908                            qubit,
909                            angle: ParameterRef::scaled(beta_idx, beta * 0.5),
910                        });
911                    }
912                }
913            }
914        }
915
916        Ok(())
917    }
918
919    /// Build adiabatic-inspired circuit
920    fn build_adiabatic_inspired_circuit(
921        &self,
922        circuit: &mut QuantumCircuit,
923        problem: &IsingModel,
924        time_steps: usize,
925        evolution_time: f64,
926        parameters: &[f64],
927    ) -> VqaResult<()> {
928        let num_qubits = circuit.num_qubits;
929        let dt = evolution_time / time_steps as f64;
930
931        for step in 0..time_steps {
932            if step >= parameters.len() {
933                break;
934            }
935
936            let s = parameters[step]; // Annealing parameter s(t)
937
938            // Transverse field (initial Hamiltonian)
939            for qubit in 0..num_qubits {
940                circuit.add_gate(QuantumGate::RX {
941                    qubit,
942                    angle: ParameterRef::scaled(step, -2.0 * (1.0 - s) * dt),
943                });
944            }
945
946            // Problem Hamiltonian
947            for i in 0..num_qubits {
948                for j in (i + 1)..num_qubits {
949                    if let Ok(coupling) = problem.get_coupling(i, j) {
950                        if coupling != 0.0 {
951                            circuit.add_gate(QuantumGate::ZZ {
952                                qubit1: i,
953                                qubit2: j,
954                                angle: ParameterRef::scaled(step, -s * coupling * dt),
955                            });
956                        }
957                    }
958                }
959            }
960        }
961
962        Ok(())
963    }
964
965    /// Build custom circuit from structure
966    fn build_custom_circuit(
967        &self,
968        circuit: &mut QuantumCircuit,
969        structure: &[QuantumGate],
970        parameters: &[f64],
971    ) -> VqaResult<()> {
972        for gate in structure {
973            // Create gate with current parameter values
974            let parameterized_gate = match gate {
975                QuantumGate::RX { qubit, angle } => {
976                    let param_value = if angle.index < parameters.len() {
977                        parameters[angle.index] * angle.scale
978                    } else {
979                        0.0
980                    };
981                    QuantumGate::RX {
982                        qubit: *qubit,
983                        angle: ParameterRef::scaled(angle.index, param_value),
984                    }
985                }
986
987                QuantumGate::RY { qubit, angle } => {
988                    let param_value = if angle.index < parameters.len() {
989                        parameters[angle.index] * angle.scale
990                    } else {
991                        0.0
992                    };
993                    QuantumGate::RY {
994                        qubit: *qubit,
995                        angle: ParameterRef::scaled(angle.index, param_value),
996                    }
997                }
998
999                QuantumGate::RZ { qubit, angle } => {
1000                    let param_value = if angle.index < parameters.len() {
1001                        parameters[angle.index] * angle.scale
1002                    } else {
1003                        0.0
1004                    };
1005                    QuantumGate::RZ {
1006                        qubit: *qubit,
1007                        angle: ParameterRef::scaled(angle.index, param_value),
1008                    }
1009                }
1010
1011                QuantumGate::ZZ {
1012                    qubit1,
1013                    qubit2,
1014                    angle,
1015                } => {
1016                    let param_value = if angle.index < parameters.len() {
1017                        parameters[angle.index] * angle.scale
1018                    } else {
1019                        0.0
1020                    };
1021                    QuantumGate::ZZ {
1022                        qubit1: *qubit1,
1023                        qubit2: *qubit2,
1024                        angle: ParameterRef::scaled(angle.index, param_value),
1025                    }
1026                }
1027
1028                // Non-parameterized gates
1029                _ => gate.clone(),
1030            };
1031
1032            circuit.add_gate(parameterized_gate);
1033        }
1034
1035        Ok(())
1036    }
1037
1038    /// Apply the parameterized variational circuit to the problem Hamiltonian.
1039    ///
1040    /// This produces the **mean-field effective Ising model** induced by the
1041    /// ansatz: each qubit's reduced single-qubit state is evolved on the Bloch
1042    /// sphere under the circuit's single-qubit rotations, giving a per-qubit
1043    /// longitudinal magnetization `m_i = ⟨Z_i⟩ ∈ [-1, 1]`. The problem
1044    /// Hamiltonian is then reweighted by these expectations,
1045    ///
1046    /// ```text
1047    /// h_i  →  h_i · m_i
1048    /// J_ij →  J_ij · m_i · m_j
1049    /// ```
1050    ///
1051    /// which is the standard mean-field (product-state) energy
1052    /// `⟨ψ(θ)| H | ψ(θ)⟩` for an Ising Hamiltonian. Entangling gates (CNOT/CZ)
1053    /// generate correlations that a product state cannot represent; we account
1054    /// for their depolarizing effect on the single-qubit marginals by damping
1055    /// the magnetization of the involved qubits, the standard mean-field
1056    /// correction. The transform is the identity when every rotation angle is
1057    /// zero (then `m_i = 1`), and the parameters genuinely reshape the energy
1058    /// landscape, so the variational gradient is non-trivial.
1059    fn apply_circuit_to_problem(
1060        &self,
1061        problem: &IsingModel,
1062        circuit: &QuantumCircuit,
1063    ) -> VqaResult<IsingModel> {
1064        let magnetizations = Self::compute_magnetizations(problem.num_qubits, circuit);
1065
1066        let mut modified = IsingModel::new(problem.num_qubits);
1067
1068        // Reweight biases by single-qubit magnetization.
1069        for (qubit, bias) in problem.biases() {
1070            let new_bias = bias * magnetizations[qubit];
1071            if new_bias != 0.0 {
1072                modified.set_bias(qubit, new_bias)?;
1073            }
1074        }
1075
1076        // Reweight couplings by the product of the two magnetizations.
1077        for coupling in problem.couplings() {
1078            let new_strength =
1079                coupling.strength * magnetizations[coupling.i] * magnetizations[coupling.j];
1080            if new_strength != 0.0 {
1081                modified.set_coupling(coupling.i, coupling.j, new_strength)?;
1082            }
1083        }
1084
1085        Ok(modified)
1086    }
1087
1088    /// Evolve each qubit's Bloch vector under the circuit's single-qubit
1089    /// rotations and return the longitudinal magnetization `⟨Z_i⟩` per qubit.
1090    ///
1091    /// Each qubit starts polarized along `+Z` (Bloch vector `(0, 0, 1)`).
1092    /// Rotations act as the usual SO(3) Bloch-sphere rotations; CNOT/CZ
1093    /// entangling gates depolarize the single-qubit marginal of the involved
1094    /// qubits (mean-field correction), shrinking `|⟨Z⟩|` towards 0.
1095    fn compute_magnetizations(num_qubits: usize, circuit: &QuantumCircuit) -> Vec<f64> {
1096        // Bloch vectors, one per qubit, initialized along +Z.
1097        let mut bloch: Vec<[f64; 3]> = vec![[0.0, 0.0, 1.0]; num_qubits];
1098
1099        // Damping factor applied to a qubit's transverse/longitudinal components
1100        // when it participates in an entangling gate (loss of single-qubit purity
1101        // to two-qubit correlations). 1/√2 reflects a maximally-entangling step.
1102        const ENTANGLE_DAMP: f64 = std::f64::consts::FRAC_1_SQRT_2;
1103
1104        for gate in &circuit.gates {
1105            match gate {
1106                QuantumGate::RX { qubit, angle } => {
1107                    if *qubit < num_qubits {
1108                        bloch[*qubit] = rotate_x(bloch[*qubit], angle.scale);
1109                    }
1110                }
1111                QuantumGate::RY { qubit, angle } => {
1112                    if *qubit < num_qubits {
1113                        bloch[*qubit] = rotate_y(bloch[*qubit], angle.scale);
1114                    }
1115                }
1116                QuantumGate::RZ { qubit, angle } => {
1117                    if *qubit < num_qubits {
1118                        bloch[*qubit] = rotate_z(bloch[*qubit], angle.scale);
1119                    }
1120                }
1121                QuantumGate::ZZ {
1122                    qubit1,
1123                    qubit2,
1124                    angle,
1125                } => {
1126                    // A ZZ(θ) interaction correlates the two qubits; in mean field
1127                    // it damps each qubit's transverse magnetization by cos(θ) and
1128                    // shrinks the longitudinal marginals proportionally.
1129                    let damp = angle.scale.cos().abs();
1130                    for &q in &[*qubit1, *qubit2] {
1131                        if q < num_qubits {
1132                            bloch[q][0] *= damp;
1133                            bloch[q][1] *= damp;
1134                            bloch[q][2] *= damp;
1135                        }
1136                    }
1137                }
1138                QuantumGate::CNOT { control, target } | QuantumGate::CZ { control, target } => {
1139                    for &q in &[*control, *target] {
1140                        if q < num_qubits {
1141                            bloch[q][0] *= ENTANGLE_DAMP;
1142                            bloch[q][1] *= ENTANGLE_DAMP;
1143                            bloch[q][2] *= ENTANGLE_DAMP;
1144                        }
1145                    }
1146                }
1147            }
1148        }
1149
1150        // The longitudinal magnetization is the Z-component of the Bloch vector.
1151        bloch.iter().map(|v| v[2]).collect()
1152    }
1153
1154    /// Compute gradients using finite differences
1155    fn compute_gradients(&mut self, problem: &IsingModel) -> VqaResult<Vec<f64>> {
1156        self.history.gradient_evals += 1;
1157
1158        let mut gradients = vec![0.0; self.parameters.len()];
1159        let step = self.config.gradient_step;
1160
1161        for i in 0..self.parameters.len() {
1162            // Create modified parameter vectors
1163            let mut params_plus = self.parameters.clone();
1164            let mut params_minus = self.parameters.clone();
1165
1166            params_plus[i] += step;
1167            params_minus[i] -= step;
1168
1169            let (energy_plus, _) = self.evaluate_objective(problem, &params_plus)?;
1170            let (energy_minus, _) = self.evaluate_objective(problem, &params_minus)?;
1171
1172            // Compute gradient
1173            gradients[i] = (energy_plus - energy_minus) / (2.0 * step);
1174        }
1175
1176        Ok(gradients)
1177    }
1178
1179    /// Update parameters using classical optimizer
1180    fn update_parameters(&mut self, gradients: Option<&[f64]>) -> VqaResult<()> {
1181        match (&mut self.optimizer_state, &self.config.optimizer) {
1182            (
1183                OptimizerState::Adam { m, v, t },
1184                ClassicalOptimizer::Adam {
1185                    learning_rate,
1186                    beta1,
1187                    beta2,
1188                    epsilon,
1189                },
1190            ) => {
1191                if let Some(grads) = gradients {
1192                    *t += 1;
1193
1194                    for i in 0..self.parameters.len() {
1195                        // Update biased first moment estimate
1196                        m[i] = (1.0 - beta1).mul_add(grads[i], beta1 * m[i]);
1197
1198                        // Update biased second moment estimate
1199                        v[i] = (1.0 - beta2).mul_add(grads[i].powi(2), beta2 * v[i]);
1200
1201                        // Compute bias-corrected estimates
1202                        let m_hat = m[i] / (1.0 - beta1.powi(*t as i32));
1203                        let v_hat = v[i] / (1.0 - beta2.powi(*t as i32));
1204
1205                        // Update parameter
1206                        self.parameters[i] -= learning_rate * m_hat / (v_hat.sqrt() + epsilon);
1207                    }
1208                }
1209            }
1210
1211            (
1212                OptimizerState::GradientDescent { .. },
1213                ClassicalOptimizer::GradientDescent { learning_rate },
1214            ) => {
1215                if let Some(grads) = gradients {
1216                    for i in 0..self.parameters.len() {
1217                        self.parameters[i] -= learning_rate * grads[i];
1218                    }
1219                }
1220            }
1221
1222            _ => {
1223                // Implement other optimizers as needed
1224                return Err(VqaError::OptimizationFailed(
1225                    "Optimizer not implemented".to_string(),
1226                ));
1227            }
1228        }
1229
1230        Ok(())
1231    }
1232
1233    /// Check convergence criteria
1234    fn check_convergence(&self) -> VqaResult<bool> {
1235        if self.history.energies.len() < 2 {
1236            return Ok(false);
1237        }
1238
1239        let recent_energies =
1240            &self.history.energies[self.history.energies.len().saturating_sub(5)..];
1241        let energy_range = recent_energies
1242            .iter()
1243            .copied()
1244            .fold(f64::NEG_INFINITY, f64::max)
1245            - recent_energies
1246                .iter()
1247                .copied()
1248                .fold(f64::INFINITY, f64::min);
1249
1250        Ok(energy_range < self.config.convergence_tolerance)
1251    }
1252
1253    /// Calculate optimization statistics
1254    fn calculate_statistics(&self) -> VqaStatistics {
1255        let average_energy = if self.history.energies.is_empty() {
1256            0.0
1257        } else {
1258            self.history.energies.iter().sum::<f64>() / self.history.energies.len() as f64
1259        };
1260
1261        let energy_variance = if self.history.energies.len() > 1 {
1262            let mean = average_energy;
1263            self.history
1264                .energies
1265                .iter()
1266                .map(|&e| (e - mean).powi(2))
1267                .sum::<f64>()
1268                / (self.history.energies.len() - 1) as f64
1269        } else {
1270            0.0
1271        };
1272
1273        // Calculate parameter statistics
1274        let parameter_stats = self.calculate_parameter_statistics();
1275
1276        // Wall-clock time spent in optimization (dominated by the annealing
1277        // shots). Computed from the run's start instant rather than reported as
1278        // a fixed zero.
1279        let total_annealing_time = self.history.start_time.elapsed();
1280
1281        // Step acceptance rate: fraction of optimization steps that strictly
1282        // lowered the objective, measured from the recorded energy trajectory.
1283        let step_acceptance_rate = if self.history.energies.len() > 1 {
1284            let improving = self
1285                .history
1286                .energies
1287                .windows(2)
1288                .filter(|w| w[1] < w[0])
1289                .count();
1290            improving as f64 / (self.history.energies.len() - 1) as f64
1291        } else {
1292            0.0
1293        };
1294
1295        // Average step size: mean Euclidean distance between consecutive
1296        // parameter vectors actually visited during optimization.
1297        let average_step_size = if self.history.parameters.len() > 1 {
1298            let total: f64 = self
1299                .history
1300                .parameters
1301                .windows(2)
1302                .map(|w| {
1303                    w[0].iter()
1304                        .zip(w[1].iter())
1305                        .map(|(a, b)| (a - b).powi(2))
1306                        .sum::<f64>()
1307                        .sqrt()
1308                })
1309                .sum();
1310            total / (self.history.parameters.len() - 1) as f64
1311        } else {
1312            0.0
1313        };
1314
1315        VqaStatistics {
1316            function_evaluations: self.history.function_evals,
1317            gradient_evaluations: self.history.gradient_evals,
1318            total_annealing_time,
1319            average_energy,
1320            energy_variance,
1321            parameter_stats,
1322            optimizer_stats: OptimizerStatistics {
1323                step_acceptance_rate,
1324                average_step_size,
1325                line_search_iterations: 0,
1326                optimizer_metrics: HashMap::new(),
1327            },
1328        }
1329    }
1330
1331    /// Calculate parameter statistics
1332    fn calculate_parameter_statistics(&self) -> ParameterStatistics {
1333        let average_magnitude = if self.parameters.is_empty() {
1334            0.0
1335        } else {
1336            self.parameters.iter().map(|&p| p.abs()).sum::<f64>() / self.parameters.len() as f64
1337        };
1338
1339        let parameter_variance = if self.parameters.len() > 1 {
1340            let mean = self.parameters.iter().sum::<f64>() / self.parameters.len() as f64;
1341            self.parameters
1342                .iter()
1343                .map(|&p| (p - mean).powi(2))
1344                .sum::<f64>()
1345                / (self.parameters.len() - 1) as f64
1346        } else {
1347            0.0
1348        };
1349
1350        // Per-parameter maximum absolute change observed across consecutive
1351        // recorded parameter vectors during optimization.
1352        let max_parameter_change = if self.history.parameters.len() > 1 {
1353            let num_params = self.parameters.len();
1354            let mut max_change = vec![0.0_f64; num_params];
1355            for window in self.history.parameters.windows(2) {
1356                for (idx, slot) in max_change.iter_mut().enumerate() {
1357                    if let (Some(&prev), Some(&curr)) = (window[0].get(idx), window[1].get(idx)) {
1358                        *slot = slot.max((curr - prev).abs());
1359                    }
1360                }
1361            }
1362            max_change
1363        } else {
1364            Vec::new()
1365        };
1366
1367        ParameterStatistics {
1368            average_magnitude,
1369            parameter_variance,
1370            num_updates: self.history.parameters.len(),
1371            max_parameter_change,
1372        }
1373    }
1374}
1375
1376/// Quantum circuit representation
1377#[derive(Debug, Clone)]
1378pub struct QuantumCircuit {
1379    /// Number of qubits
1380    pub num_qubits: usize,
1381
1382    /// Sequence of quantum gates
1383    pub gates: Vec<QuantumGate>,
1384}
1385
1386impl QuantumCircuit {
1387    /// Create a new quantum circuit
1388    #[must_use]
1389    pub const fn new(num_qubits: usize) -> Self {
1390        Self {
1391            num_qubits,
1392            gates: Vec::new(),
1393        }
1394    }
1395
1396    /// Add a gate to the circuit
1397    pub fn add_gate(&mut self, gate: QuantumGate) {
1398        self.gates.push(gate);
1399    }
1400
1401    /// Get the depth of the circuit
1402    #[must_use]
1403    pub fn depth(&self) -> usize {
1404        // Simplified depth calculation
1405        self.gates.len()
1406    }
1407}
1408
1409/// Rotate a Bloch vector by `theta` about the X axis (right-handed SO(3)).
1410fn rotate_x(v: [f64; 3], theta: f64) -> [f64; 3] {
1411    let (s, c) = theta.sin_cos();
1412    [
1413        v[0],
1414        c.mul_add(v[1], -(s * v[2])),
1415        s.mul_add(v[1], c * v[2]),
1416    ]
1417}
1418
1419/// Rotate a Bloch vector by `theta` about the Y axis (right-handed SO(3)).
1420fn rotate_y(v: [f64; 3], theta: f64) -> [f64; 3] {
1421    let (s, c) = theta.sin_cos();
1422    [
1423        s.mul_add(v[2], c * v[0]),
1424        v[1],
1425        c.mul_add(v[2], -(s * v[0])),
1426    ]
1427}
1428
1429/// Rotate a Bloch vector by `theta` about the Z axis (right-handed SO(3)).
1430fn rotate_z(v: [f64; 3], theta: f64) -> [f64; 3] {
1431    let (s, c) = theta.sin_cos();
1432    [
1433        c.mul_add(v[0], -(s * v[1])),
1434        s.mul_add(v[0], c * v[1]),
1435        v[2],
1436    ]
1437}
1438
1439/// Helper functions for creating common VQA configurations
1440
1441/// Create a QAOA-style VQA configuration
1442#[must_use]
1443pub fn create_qaoa_vqa_config(layers: usize, max_iterations: usize) -> VqaConfig {
1444    VqaConfig {
1445        ansatz: AnsatzType::QaoaInspired {
1446            layers,
1447            mixer_type: MixerType::XRotation,
1448        },
1449        max_iterations,
1450        ..Default::default()
1451    }
1452}
1453
1454/// Create a hardware-efficient VQA configuration
1455#[must_use]
1456pub fn create_hardware_efficient_vqa_config(depth: usize, max_iterations: usize) -> VqaConfig {
1457    VqaConfig {
1458        ansatz: AnsatzType::HardwareEfficient {
1459            depth,
1460            entangling_gates: EntanglingGateType::CNot,
1461        },
1462        max_iterations,
1463        ..Default::default()
1464    }
1465}
1466
1467/// Create an adiabatic-inspired VQA configuration
1468#[must_use]
1469pub fn create_adiabatic_vqa_config(
1470    time_steps: usize,
1471    evolution_time: f64,
1472    max_iterations: usize,
1473) -> VqaConfig {
1474    VqaConfig {
1475        ansatz: AnsatzType::AdiabaticInspired {
1476            time_steps,
1477            evolution_time,
1478        },
1479        max_iterations,
1480        ..Default::default()
1481    }
1482}
1483
1484#[cfg(test)]
1485mod tests {
1486    use super::*;
1487
1488    #[test]
1489    fn test_vqa_config_creation() {
1490        let config = create_qaoa_vqa_config(3, 50);
1491
1492        match config.ansatz {
1493            AnsatzType::QaoaInspired { layers, .. } => {
1494                assert_eq!(layers, 3);
1495            }
1496            _ => panic!("Expected QAOA ansatz"),
1497        }
1498
1499        assert_eq!(config.max_iterations, 50);
1500    }
1501
1502    #[test]
1503    fn test_parameter_ref() {
1504        let param_ref = ParameterRef::new(5);
1505        assert_eq!(param_ref.index, 5);
1506        assert_eq!(param_ref.scale, 1.0);
1507
1508        let scaled_ref = ParameterRef::scaled(3, 2.5);
1509        assert_eq!(scaled_ref.index, 3);
1510        assert_eq!(scaled_ref.scale, 2.5);
1511    }
1512
1513    #[test]
1514    fn test_quantum_circuit() {
1515        let mut circuit = QuantumCircuit::new(3);
1516        assert_eq!(circuit.num_qubits, 3);
1517        assert_eq!(circuit.gates.len(), 0);
1518
1519        circuit.add_gate(QuantumGate::RX {
1520            qubit: 0,
1521            angle: ParameterRef::new(0),
1522        });
1523
1524        assert_eq!(circuit.gates.len(), 1);
1525        assert_eq!(circuit.depth(), 1);
1526    }
1527
1528    #[test]
1529    fn test_parameter_counting() {
1530        let ansatz = AnsatzType::QaoaInspired {
1531            layers: 5,
1532            mixer_type: MixerType::XRotation,
1533        };
1534
1535        let count = VariationalQuantumAnnealer::count_parameters(&ansatz)
1536            .expect("parameter counting should succeed");
1537        assert_eq!(count, 10); // 2 parameters per layer
1538    }
1539}