Skip to main content

quantrs2_sim/
autodiff_vqe.rs

1//! Automatic differentiation for Variational Quantum Eigensolver (VQE).
2//!
3//! This module implements automatic differentiation techniques specifically designed
4//! for variational quantum algorithms, including parameter-shift rule, finite differences,
5//! and optimization strategies for VQE.
6
7use crate::error::{Result, SimulatorError};
8use crate::pauli::{PauliOperatorSum, PauliString};
9use quantrs2_core::gate::GateOp;
10use scirs2_core::ndarray::{Array1, Array2};
11use scirs2_core::random::prelude::*;
12use scirs2_core::Complex64;
13use std::f64::consts::PI;
14
15#[cfg(feature = "optimize")]
16use crate::optirs_integration::{OptiRSConfig, OptiRSQuantumOptimizer};
17
18/// Gradient computation method
19#[derive(Debug, Clone, Copy)]
20pub enum GradientMethod {
21    /// Parameter-shift rule (exact for quantum gates)
22    ParameterShift,
23    /// Finite differences
24    FiniteDifference { step_size: f64 },
25    /// Simultaneous perturbation stochastic approximation
26    SPSA { step_size: f64 },
27}
28
29/// Automatic differentiation context for tracking gradients
30#[derive(Debug, Clone)]
31pub struct AutoDiffContext {
32    /// Parameter values
33    pub parameters: Vec<f64>,
34    /// Parameter names/indices
35    pub parameter_names: Vec<String>,
36    /// Gradient computation method
37    pub method: GradientMethod,
38    /// Current gradients
39    pub gradients: Vec<f64>,
40    /// Gradient computation count
41    pub grad_evaluations: usize,
42    /// Function evaluation count
43    pub func_evaluations: usize,
44}
45
46impl AutoDiffContext {
47    /// Create new autodiff context
48    #[must_use]
49    pub fn new(parameters: Vec<f64>, method: GradientMethod) -> Self {
50        let num_params = parameters.len();
51        Self {
52            parameters,
53            parameter_names: (0..num_params).map(|i| format!("θ{i}")).collect(),
54            method,
55            gradients: vec![0.0; num_params],
56            grad_evaluations: 0,
57            func_evaluations: 0,
58        }
59    }
60
61    /// Set parameter names
62    #[must_use]
63    pub fn with_parameter_names(mut self, names: Vec<String>) -> Self {
64        assert_eq!(names.len(), self.parameters.len());
65        self.parameter_names = names;
66        self
67    }
68
69    /// Update parameters
70    pub fn update_parameters(&mut self, new_params: Vec<f64>) {
71        assert_eq!(new_params.len(), self.parameters.len());
72        self.parameters = new_params;
73    }
74
75    /// Get parameter by name
76    #[must_use]
77    pub fn get_parameter(&self, name: &str) -> Option<f64> {
78        self.parameter_names
79            .iter()
80            .position(|n| n == name)
81            .map(|i| self.parameters[i])
82    }
83
84    /// Set parameter by name
85    pub fn set_parameter(&mut self, name: &str, value: f64) -> Result<()> {
86        if let Some(i) = self.parameter_names.iter().position(|n| n == name) {
87            self.parameters[i] = value;
88            Ok(())
89        } else {
90            Err(SimulatorError::InvalidInput(format!(
91                "Parameter '{name}' not found"
92            )))
93        }
94    }
95}
96
97/// Parametric quantum gate that supports automatic differentiation
98pub trait ParametricGate: Send + Sync {
99    /// Get gate name
100    fn name(&self) -> &str;
101
102    /// Get qubits this gate acts on
103    fn qubits(&self) -> Vec<usize>;
104
105    /// Get parameter indices this gate depends on
106    fn parameter_indices(&self) -> Vec<usize>;
107
108    /// Evaluate gate matrix given parameter values
109    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>>;
110
111    /// Compute gradient of gate matrix with respect to each parameter
112    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>>;
113
114    /// Apply parameter-shift rule for this gate
115    fn parameter_shift_gradient(
116        &self,
117        params: &[f64],
118        param_idx: usize,
119    ) -> Result<(Array2<Complex64>, Array2<Complex64>)> {
120        let shift = PI / 2.0;
121        let mut params_plus = params.to_vec();
122        let mut params_minus = params.to_vec();
123
124        if param_idx < params.len() {
125            params_plus[param_idx] += shift;
126            params_minus[param_idx] -= shift;
127        }
128
129        let matrix_plus = self.matrix(&params_plus)?;
130        let matrix_minus = self.matrix(&params_minus)?;
131
132        Ok((matrix_plus, matrix_minus))
133    }
134}
135
136/// Parametric rotation gates
137pub struct ParametricRX {
138    pub qubit: usize,
139    pub param_idx: usize,
140}
141
142impl ParametricGate for ParametricRX {
143    fn name(&self) -> &'static str {
144        "RX"
145    }
146
147    fn qubits(&self) -> Vec<usize> {
148        vec![self.qubit]
149    }
150
151    fn parameter_indices(&self) -> Vec<usize> {
152        vec![self.param_idx]
153    }
154
155    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>> {
156        let theta = params[self.param_idx];
157        let cos_half = (theta / 2.0).cos();
158        let sin_half = (theta / 2.0).sin();
159
160        Ok(scirs2_core::ndarray::array![
161            [Complex64::new(cos_half, 0.), Complex64::new(0., -sin_half)],
162            [Complex64::new(0., -sin_half), Complex64::new(cos_half, 0.)]
163        ])
164    }
165
166    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>> {
167        if param_idx != self.param_idx {
168            return Ok(Array2::zeros((2, 2)));
169        }
170
171        let theta = params[self.param_idx];
172        let cos_half = (theta / 2.0).cos();
173        let sin_half = (theta / 2.0).sin();
174
175        // d/dθ RX(θ) = -i/2 * X * RX(θ)
176        Ok(scirs2_core::ndarray::array![
177            [
178                Complex64::new(-sin_half / 2.0, 0.),
179                Complex64::new(0., -cos_half / 2.0)
180            ],
181            [
182                Complex64::new(0., -cos_half / 2.0),
183                Complex64::new(-sin_half / 2.0, 0.)
184            ]
185        ])
186    }
187}
188
189pub struct ParametricRY {
190    pub qubit: usize,
191    pub param_idx: usize,
192}
193
194impl ParametricGate for ParametricRY {
195    fn name(&self) -> &'static str {
196        "RY"
197    }
198
199    fn qubits(&self) -> Vec<usize> {
200        vec![self.qubit]
201    }
202
203    fn parameter_indices(&self) -> Vec<usize> {
204        vec![self.param_idx]
205    }
206
207    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>> {
208        let theta = params[self.param_idx];
209        let cos_half = (theta / 2.0).cos();
210        let sin_half = (theta / 2.0).sin();
211
212        Ok(scirs2_core::ndarray::array![
213            [Complex64::new(cos_half, 0.), Complex64::new(-sin_half, 0.)],
214            [Complex64::new(sin_half, 0.), Complex64::new(cos_half, 0.)]
215        ])
216    }
217
218    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>> {
219        if param_idx != self.param_idx {
220            return Ok(Array2::zeros((2, 2)));
221        }
222
223        let theta = params[self.param_idx];
224        let cos_half = (theta / 2.0).cos();
225        let sin_half = (theta / 2.0).sin();
226
227        Ok(scirs2_core::ndarray::array![
228            [
229                Complex64::new(-sin_half / 2.0, 0.),
230                Complex64::new(-cos_half / 2.0, 0.)
231            ],
232            [
233                Complex64::new(cos_half / 2.0, 0.),
234                Complex64::new(-sin_half / 2.0, 0.)
235            ]
236        ])
237    }
238}
239
240/// Non-parametric CNOT gate, used for the entangling layers of ansätze such
241/// as [`ansatze::hardware_efficient`]. It has no free parameters (an empty
242/// [`ParametricGate::parameter_indices`]), so parameter-shift/finite-
243/// difference/SPSA gradient computation simply never varies it, and its own
244/// gradient with respect to any parameter is the zero matrix.
245pub struct ParametricCNOT {
246    pub control: usize,
247    pub target: usize,
248}
249
250impl ParametricGate for ParametricCNOT {
251    fn name(&self) -> &'static str {
252        "CNOT"
253    }
254
255    fn qubits(&self) -> Vec<usize> {
256        vec![self.control, self.target]
257    }
258
259    fn parameter_indices(&self) -> Vec<usize> {
260        Vec::new()
261    }
262
263    fn matrix(&self, _params: &[f64]) -> Result<Array2<Complex64>> {
264        // Basis ordering |control, target> with `control` the high local
265        // bit, matching `apply_two_qubit_matrix`'s bit-indexing convention.
266        Ok(scirs2_core::ndarray::array![
267            [
268                Complex64::new(1., 0.),
269                Complex64::new(0., 0.),
270                Complex64::new(0., 0.),
271                Complex64::new(0., 0.)
272            ],
273            [
274                Complex64::new(0., 0.),
275                Complex64::new(1., 0.),
276                Complex64::new(0., 0.),
277                Complex64::new(0., 0.)
278            ],
279            [
280                Complex64::new(0., 0.),
281                Complex64::new(0., 0.),
282                Complex64::new(0., 0.),
283                Complex64::new(1., 0.)
284            ],
285            [
286                Complex64::new(0., 0.),
287                Complex64::new(0., 0.),
288                Complex64::new(1., 0.),
289                Complex64::new(0., 0.)
290            ]
291        ])
292    }
293
294    fn gradient(&self, _params: &[f64], _param_idx: usize) -> Result<Array2<Complex64>> {
295        // CNOT has no parameters, so its gradient w.r.t. any parameter is
296        // identically zero.
297        Ok(Array2::zeros((4, 4)))
298    }
299}
300
301pub struct ParametricRZ {
302    pub qubit: usize,
303    pub param_idx: usize,
304}
305
306impl ParametricGate for ParametricRZ {
307    fn name(&self) -> &'static str {
308        "RZ"
309    }
310
311    fn qubits(&self) -> Vec<usize> {
312        vec![self.qubit]
313    }
314
315    fn parameter_indices(&self) -> Vec<usize> {
316        vec![self.param_idx]
317    }
318
319    fn matrix(&self, params: &[f64]) -> Result<Array2<Complex64>> {
320        let theta = params[self.param_idx];
321        let exp_pos = Complex64::from_polar(1.0, theta / 2.0);
322        let exp_neg = Complex64::from_polar(1.0, -theta / 2.0);
323
324        Ok(scirs2_core::ndarray::array![
325            [exp_neg, Complex64::new(0., 0.)],
326            [Complex64::new(0., 0.), exp_pos]
327        ])
328    }
329
330    fn gradient(&self, params: &[f64], param_idx: usize) -> Result<Array2<Complex64>> {
331        if param_idx != self.param_idx {
332            return Ok(Array2::zeros((2, 2)));
333        }
334
335        let theta = params[self.param_idx];
336        let exp_pos = Complex64::from_polar(1.0, theta / 2.0);
337        let exp_neg = Complex64::from_polar(1.0, -theta / 2.0);
338
339        Ok(scirs2_core::ndarray::array![
340            [exp_neg * Complex64::new(0., -0.5), Complex64::new(0., 0.)],
341            [Complex64::new(0., 0.), exp_pos * Complex64::new(0., 0.5)]
342        ])
343    }
344}
345
346/// Parametric quantum circuit for VQE
347pub struct ParametricCircuit {
348    /// Sequence of parametric gates
349    pub gates: Vec<Box<dyn ParametricGate>>,
350    /// Number of qubits
351    pub num_qubits: usize,
352    /// Number of parameters
353    pub num_parameters: usize,
354}
355
356impl ParametricCircuit {
357    /// Create new parametric circuit
358    #[must_use]
359    pub fn new(num_qubits: usize) -> Self {
360        Self {
361            gates: Vec::new(),
362            num_qubits,
363            num_parameters: 0,
364        }
365    }
366
367    /// Add a parametric gate
368    pub fn add_gate(&mut self, gate: Box<dyn ParametricGate>) {
369        // Update parameter count
370        for &param_idx in &gate.parameter_indices() {
371            self.num_parameters = self.num_parameters.max(param_idx + 1);
372        }
373        self.gates.push(gate);
374    }
375
376    /// Add RX gate
377    pub fn rx(&mut self, qubit: usize, param_idx: usize) {
378        self.add_gate(Box::new(ParametricRX { qubit, param_idx }));
379    }
380
381    /// Add RY gate
382    pub fn ry(&mut self, qubit: usize, param_idx: usize) {
383        self.add_gate(Box::new(ParametricRY { qubit, param_idx }));
384    }
385
386    /// Add RZ gate
387    pub fn rz(&mut self, qubit: usize, param_idx: usize) {
388        self.add_gate(Box::new(ParametricRZ { qubit, param_idx }));
389    }
390
391    /// Add a (non-parametric) CNOT gate, used to build entangling layers.
392    pub fn cnot(&mut self, control: usize, target: usize) {
393        self.add_gate(Box::new(ParametricCNOT { control, target }));
394    }
395
396    /// Evaluate the circuit for the given parameters and return the final state vector.
397    ///
398    /// Starts from `|0...0>` and applies each parametric gate's matrix to the dense state
399    /// vector in sequence. Amplitudes use the little-endian convention (qubit `q` maps to
400    /// bit `q` of the basis index), matching [`compute_pauli_expectation_from_state`] so
401    /// expectation values and parameter-shift gradients are computed on the genuine state
402    /// produced by the circuit rather than a fixed placeholder.
403    pub fn evaluate(&self, params: &[f64]) -> Result<Array1<Complex64>> {
404        if params.len() != self.num_parameters {
405            return Err(SimulatorError::InvalidInput(format!(
406                "Expected {} parameters, got {}",
407                self.num_parameters,
408                params.len()
409            )));
410        }
411
412        let dim = 1usize << self.num_qubits;
413        let mut state = Array1::zeros(dim);
414        state[0] = Complex64::new(1.0, 0.0); // |0...0>
415
416        for gate in &self.gates {
417            let matrix = gate.matrix(params)?;
418            let qubits = gate.qubits();
419
420            match qubits.as_slice() {
421                [target] => {
422                    apply_single_qubit_matrix(&mut state, &matrix, *target, self.num_qubits)?;
423                }
424                [control, target] => {
425                    apply_two_qubit_matrix(
426                        &mut state,
427                        &matrix,
428                        *control,
429                        *target,
430                        self.num_qubits,
431                    )?;
432                }
433                _ => {
434                    return Err(SimulatorError::UnsupportedOperation(format!(
435                        "Parametric circuit evaluation supports one- and two-qubit gates, but '{}' acts on {} qubits",
436                        gate.name(),
437                        qubits.len()
438                    )));
439                }
440            }
441        }
442
443        Ok(state)
444    }
445
446    /// Compute gradient of expectation value using parameter-shift rule
447    pub fn gradient_expectation(
448        &self,
449        observable: &PauliOperatorSum,
450        params: &[f64],
451        method: GradientMethod,
452    ) -> Result<Vec<f64>> {
453        match method {
454            GradientMethod::ParameterShift => self.parameter_shift_gradient(observable, params),
455            GradientMethod::FiniteDifference { step_size } => {
456                self.finite_difference_gradient(observable, params, step_size)
457            }
458            GradientMethod::SPSA { step_size } => self.spsa_gradient(observable, params, step_size),
459        }
460    }
461
462    /// Parameter-shift rule gradient computation
463    fn parameter_shift_gradient(
464        &self,
465        observable: &PauliOperatorSum,
466        params: &[f64],
467    ) -> Result<Vec<f64>> {
468        let mut gradients = vec![0.0; self.num_parameters];
469
470        // Use parameter-shift rule: ∂⟨H⟩/∂θᵢ = (⟨H⟩₊ - ⟨H⟩₋) / 2
471        // where ±π/2 shifts are applied to parameter θᵢ
472        for param_idx in 0..self.num_parameters {
473            let shift = PI / 2.0;
474
475            // Forward shift
476            let mut params_plus = params.to_vec();
477            params_plus[param_idx] += shift;
478            let state_plus = self.evaluate(&params_plus)?;
479            let expectation_plus = compute_expectation_value(&state_plus, observable)?;
480
481            // Backward shift
482            let mut params_minus = params.to_vec();
483            params_minus[param_idx] -= shift;
484            let state_minus = self.evaluate(&params_minus)?;
485            let expectation_minus = compute_expectation_value(&state_minus, observable)?;
486
487            // Gradient
488            gradients[param_idx] = (expectation_plus - expectation_minus) / 2.0;
489        }
490
491        Ok(gradients)
492    }
493
494    /// Finite difference gradient computation
495    fn finite_difference_gradient(
496        &self,
497        observable: &PauliOperatorSum,
498        params: &[f64],
499        step_size: f64,
500    ) -> Result<Vec<f64>> {
501        let mut gradients = vec![0.0; self.num_parameters];
502
503        for param_idx in 0..self.num_parameters {
504            // Forward difference
505            let mut params_plus = params.to_vec();
506            params_plus[param_idx] += step_size;
507            let state_plus = self.evaluate(&params_plus)?;
508            let expectation_plus = compute_expectation_value(&state_plus, observable)?;
509
510            // Current value
511            let state = self.evaluate(params)?;
512            let expectation = compute_expectation_value(&state, observable)?;
513
514            gradients[param_idx] = (expectation_plus - expectation) / step_size;
515        }
516
517        Ok(gradients)
518    }
519
520    /// SPSA gradient estimation
521    fn spsa_gradient(
522        &self,
523        observable: &PauliOperatorSum,
524        params: &[f64],
525        step_size: f64,
526    ) -> Result<Vec<f64>> {
527        let mut rng = thread_rng();
528
529        // Generate random perturbation vector
530        let mut perturbation = vec![0.0; self.num_parameters];
531        for p in &mut perturbation {
532            *p = if rng.random::<bool>() { 1.0 } else { -1.0 };
533        }
534
535        // Two evaluations with opposite perturbations
536        let mut params_plus = params.to_vec();
537        let mut params_minus = params.to_vec();
538        for i in 0..self.num_parameters {
539            params_plus[i] += step_size * perturbation[i];
540            params_minus[i] -= step_size * perturbation[i];
541        }
542
543        let state_plus = self.evaluate(&params_plus)?;
544        let state_minus = self.evaluate(&params_minus)?;
545        let expectation_plus = compute_expectation_value(&state_plus, observable)?;
546        let expectation_minus = compute_expectation_value(&state_minus, observable)?;
547
548        // SPSA gradient estimate
549        let diff = (expectation_plus - expectation_minus) / (2.0 * step_size);
550        let gradients = perturbation.iter().map(|&p| diff / p).collect();
551
552        Ok(gradients)
553    }
554}
555
556/// VQE algorithm with automatic differentiation
557pub struct VQEWithAutodiff {
558    /// Parametric ansatz circuit
559    pub ansatz: ParametricCircuit,
560    /// Hamiltonian observable
561    pub hamiltonian: PauliOperatorSum,
562    /// Autodiff context
563    pub context: AutoDiffContext,
564    /// Optimization history
565    pub history: Vec<VQEIteration>,
566    /// Convergence criteria
567    pub convergence: ConvergenceCriteria,
568}
569
570/// Single VQE iteration data
571#[derive(Clone)]
572pub struct VQEIteration {
573    /// Iteration number
574    pub iteration: usize,
575    /// Parameters at this iteration
576    pub parameters: Vec<f64>,
577    /// Energy expectation value
578    pub energy: f64,
579    /// Gradient norm
580    pub gradient_norm: f64,
581    /// Function evaluations so far
582    pub func_evals: usize,
583    /// Gradient evaluations so far
584    pub grad_evals: usize,
585}
586
587/// Convergence criteria for VQE
588pub struct ConvergenceCriteria {
589    /// Maximum iterations
590    pub max_iterations: usize,
591    /// Energy tolerance
592    pub energy_tolerance: f64,
593    /// Gradient norm tolerance
594    pub gradient_tolerance: f64,
595    /// Maximum function evaluations
596    pub max_func_evals: usize,
597}
598
599impl Default for ConvergenceCriteria {
600    fn default() -> Self {
601        Self {
602            max_iterations: 1000,
603            energy_tolerance: 1e-6,
604            gradient_tolerance: 1e-6,
605            max_func_evals: 10_000,
606        }
607    }
608}
609
610impl VQEWithAutodiff {
611    /// Create new VQE instance
612    #[must_use]
613    pub fn new(
614        ansatz: ParametricCircuit,
615        hamiltonian: PauliOperatorSum,
616        initial_params: Vec<f64>,
617        gradient_method: GradientMethod,
618    ) -> Self {
619        let context = AutoDiffContext::new(initial_params, gradient_method);
620        Self {
621            ansatz,
622            hamiltonian,
623            context,
624            history: Vec::new(),
625            convergence: ConvergenceCriteria::default(),
626        }
627    }
628
629    /// Set convergence criteria
630    #[must_use]
631    pub const fn with_convergence(mut self, convergence: ConvergenceCriteria) -> Self {
632        self.convergence = convergence;
633        self
634    }
635
636    /// Evaluate energy for current parameters
637    pub fn evaluate_energy(&mut self) -> Result<f64> {
638        let state = self.ansatz.evaluate(&self.context.parameters)?;
639        let energy = compute_expectation_value(&state, &self.hamiltonian)?;
640        self.context.func_evaluations += 1;
641        Ok(energy)
642    }
643
644    /// Compute gradient for current parameters
645    pub fn compute_gradient(&mut self) -> Result<Vec<f64>> {
646        let gradients = self.ansatz.gradient_expectation(
647            &self.hamiltonian,
648            &self.context.parameters,
649            self.context.method,
650        )?;
651        self.context.gradients.clone_from(&gradients);
652        self.context.grad_evaluations += 1;
653        Ok(gradients)
654    }
655
656    /// Perform one VQE optimization step using gradient descent
657    pub fn step(&mut self, learning_rate: f64) -> Result<VQEIteration> {
658        let energy = self.evaluate_energy()?;
659        let gradients = self.compute_gradient()?;
660
661        // Gradient descent update
662        for (i, &grad) in gradients.iter().enumerate() {
663            self.context.parameters[i] -= learning_rate * grad;
664        }
665
666        let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
667
668        let iteration = VQEIteration {
669            iteration: self.history.len(),
670            parameters: self.context.parameters.clone(),
671            energy,
672            gradient_norm,
673            func_evals: self.context.func_evaluations,
674            grad_evals: self.context.grad_evaluations,
675        };
676
677        self.history.push(iteration.clone());
678        Ok(iteration)
679    }
680
681    /// Run VQE optimization until convergence
682    pub fn optimize(&mut self, learning_rate: f64) -> Result<VQEResult> {
683        while !self.is_converged()? {
684            let iteration = self.step(learning_rate)?;
685
686            if iteration.iteration >= self.convergence.max_iterations {
687                break;
688            }
689            if iteration.func_evals >= self.convergence.max_func_evals {
690                break;
691            }
692        }
693
694        let final_iteration = self.history.last().ok_or_else(|| {
695            SimulatorError::InvalidOperation("VQE optimization produced no iterations".to_string())
696        })?;
697        Ok(VQEResult {
698            optimal_parameters: final_iteration.parameters.clone(),
699            optimal_energy: final_iteration.energy,
700            iterations: self.history.len(),
701            converged: self.is_converged()?,
702            history: self.history.clone(),
703        })
704    }
705
706    /// Check convergence
707    fn is_converged(&self) -> Result<bool> {
708        if self.history.len() < 2 {
709            return Ok(false);
710        }
711
712        let current = &self.history[self.history.len() - 1];
713        let previous = &self.history[self.history.len() - 2];
714
715        let energy_converged =
716            (current.energy - previous.energy).abs() < self.convergence.energy_tolerance;
717        let gradient_converged = current.gradient_norm < self.convergence.gradient_tolerance;
718
719        Ok(energy_converged && gradient_converged)
720    }
721
722    /// Run VQE optimization using `OptiRS` optimizers (Adam, SGD, `RMSprop`, etc.)
723    ///
724    /// This method provides state-of-the-art optimization using `OptiRS`'s advanced
725    /// machine learning optimizers, which typically converge faster and more robustly
726    /// than basic gradient descent.
727    ///
728    /// # Arguments
729    /// * `config` - `OptiRS` optimizer configuration
730    ///
731    /// # Returns
732    /// * `VQEResult` - Optimization result with optimal parameters and energy
733    ///
734    /// # Example
735    /// ```ignore
736    /// use quantrs2_sim::autodiff_vqe::*;
737    /// use quantrs2_sim::optirs_integration::*;
738    ///
739    /// let mut vqe = VQEWithAutodiff::new(...);
740    /// let config = OptiRSConfig {
741    ///     optimizer_type: OptiRSOptimizerType::Adam,
742    ///     learning_rate: 0.01,
743    ///     ..Default::default()
744    /// };
745    /// let result = vqe.optimize_with_optirs(config)?;
746    /// ```
747    #[cfg(feature = "optimize")]
748    pub fn optimize_with_optirs(&mut self, config: OptiRSConfig) -> Result<VQEResult> {
749        use std::time::Instant;
750
751        let start_time = Instant::now();
752        let mut optimizer = OptiRSQuantumOptimizer::new(config)?;
753
754        while !self.is_converged()? && !optimizer.has_converged() {
755            // Evaluate energy and gradients
756            let energy = self.evaluate_energy()?;
757            let gradients = self.compute_gradient()?;
758
759            // OptiRS optimization step
760            let new_params =
761                optimizer.optimize_step(&self.context.parameters, &gradients, energy)?;
762
763            // Update parameters
764            self.context.parameters = new_params;
765
766            // Record iteration
767            let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
768            let iteration = VQEIteration {
769                iteration: self.history.len(),
770                parameters: self.context.parameters.clone(),
771                energy,
772                gradient_norm,
773                func_evals: self.context.func_evaluations,
774                grad_evals: self.context.grad_evaluations,
775            };
776            self.history.push(iteration);
777
778            // Check maximum iterations (use VQE's convergence criteria)
779            if self.history.len() >= self.convergence.max_iterations {
780                break;
781            }
782            if self.context.func_evaluations >= self.convergence.max_func_evals {
783                break;
784            }
785        }
786
787        let _optimization_time = start_time.elapsed();
788        let final_iteration = self.history.last().ok_or_else(|| {
789            SimulatorError::InvalidOperation(
790                "VQE optimization with OptiRS produced no iterations".to_string(),
791            )
792        })?;
793
794        Ok(VQEResult {
795            optimal_parameters: final_iteration.parameters.clone(),
796            optimal_energy: final_iteration.energy,
797            iterations: self.history.len(),
798            converged: self.is_converged()?,
799            history: self.history.clone(),
800        })
801    }
802}
803
804/// VQE optimization result
805pub struct VQEResult {
806    /// Optimal parameters found
807    pub optimal_parameters: Vec<f64>,
808    /// Optimal energy value
809    pub optimal_energy: f64,
810    /// Number of iterations performed
811    pub iterations: usize,
812    /// Whether optimization converged
813    pub converged: bool,
814    /// Full optimization history
815    pub history: Vec<VQEIteration>,
816}
817
818// Helper functions
819
820/// Apply a single-qubit gate matrix to a dense state vector in place.
821///
822/// `state` is indexed in the little-endian convention where bit `qubit` selects the
823/// physical value of that qubit. The 2x2 `gate` matrix is applied to every pair of
824/// amplitudes that differ only in qubit `qubit`.
825fn apply_single_qubit_matrix(
826    state: &mut Array1<Complex64>,
827    gate: &Array2<Complex64>,
828    qubit: usize,
829    num_qubits: usize,
830) -> Result<()> {
831    if qubit >= num_qubits {
832        return Err(SimulatorError::InvalidInput(format!(
833            "Gate targets qubit {qubit} but circuit has only {num_qubits} qubits"
834        )));
835    }
836    if gate.dim() != (2, 2) {
837        return Err(SimulatorError::InvalidInput(format!(
838            "Single-qubit gate matrix must be 2x2, got {:?}",
839            gate.dim()
840        )));
841    }
842
843    let stride = 1usize << qubit;
844    let dim = state.len();
845    let mut index = 0;
846    while index < dim {
847        if index & stride == 0 {
848            let i0 = index;
849            let i1 = index | stride;
850            let a0 = state[i0];
851            let a1 = state[i1];
852            state[i0] = gate[[0, 0]] * a0 + gate[[0, 1]] * a1;
853            state[i1] = gate[[1, 0]] * a0 + gate[[1, 1]] * a1;
854        }
855        index += 1;
856    }
857
858    Ok(())
859}
860
861/// Apply a two-qubit gate matrix to a dense state vector in place.
862///
863/// The 4x4 `gate` matrix acts on the basis `|control target>` (control is the high bit,
864/// target the low bit). Amplitudes are indexed little-endian by qubit position; for each
865/// group of four amplitudes that differ only in the control/target bits the gate is applied
866/// as a genuine matrix-vector product.
867fn apply_two_qubit_matrix(
868    state: &mut Array1<Complex64>,
869    gate: &Array2<Complex64>,
870    control: usize,
871    target: usize,
872    num_qubits: usize,
873) -> Result<()> {
874    if control >= num_qubits || target >= num_qubits {
875        return Err(SimulatorError::InvalidInput(format!(
876            "Two-qubit gate targets qubits {control} and {target} but circuit has only {num_qubits} qubits"
877        )));
878    }
879    if control == target {
880        return Err(SimulatorError::InvalidInput(
881            "Two-qubit gate requires two distinct qubits".to_string(),
882        ));
883    }
884    if gate.dim() != (4, 4) {
885        return Err(SimulatorError::InvalidInput(format!(
886            "Two-qubit gate matrix must be 4x4, got {:?}",
887            gate.dim()
888        )));
889    }
890
891    let control_mask = 1usize << control;
892    let target_mask = 1usize << target;
893    let dim = state.len();
894
895    for index in 0..dim {
896        // Process each block once, from its representative with both bits cleared.
897        if index & control_mask == 0 && index & target_mask == 0 {
898            let i00 = index;
899            let i01 = index | target_mask;
900            let i10 = index | control_mask;
901            let i11 = index | control_mask | target_mask;
902
903            let amplitudes = [state[i00], state[i01], state[i10], state[i11]];
904            let mut updated = [Complex64::new(0.0, 0.0); 4];
905            for (row, slot) in updated.iter_mut().enumerate() {
906                let mut acc = Complex64::new(0.0, 0.0);
907                for (col, &amplitude) in amplitudes.iter().enumerate() {
908                    acc += gate[[row, col]] * amplitude;
909                }
910                *slot = acc;
911            }
912
913            state[i00] = updated[0];
914            state[i01] = updated[1];
915            state[i10] = updated[2];
916            state[i11] = updated[3];
917        }
918    }
919
920    Ok(())
921}
922
923/// Compute expectation value of observable for given state
924fn compute_expectation_value(
925    state: &Array1<Complex64>,
926    observable: &PauliOperatorSum,
927) -> Result<f64> {
928    let mut expectation = 0.0;
929
930    for term in &observable.terms {
931        // Compute ⟨ψ|P|ψ⟩ for each Pauli string P
932        let pauli_expectation = compute_pauli_expectation_from_state(state, term)?;
933        expectation += term.coefficient.re * pauli_expectation.re;
934    }
935
936    Ok(expectation)
937}
938
939/// Compute expectation value of a single Pauli string
940fn compute_pauli_expectation_from_state(
941    state: &Array1<Complex64>,
942    pauli_string: &PauliString,
943) -> Result<Complex64> {
944    let num_qubits = pauli_string.num_qubits;
945    let dim = 1 << num_qubits;
946    let mut result = Complex64::new(0.0, 0.0);
947
948    for (i, &amplitude) in state.iter().enumerate() {
949        if i >= dim {
950            break;
951        }
952
953        // Apply Pauli string to basis state |i⟩
954        let mut coeff = Complex64::new(1.0, 0.0);
955        let mut target_state = i;
956
957        for (qubit, &pauli_op) in pauli_string.operators.iter().enumerate() {
958            let bit = (i >> qubit) & 1;
959            use crate::pauli::PauliOperator;
960
961            match pauli_op {
962                PauliOperator::I => {} // Identity does nothing
963                PauliOperator::X => {
964                    // X flips the bit
965                    target_state ^= 1 << qubit;
966                }
967                PauliOperator::Y => {
968                    // Y flips the bit and adds phase
969                    target_state ^= 1 << qubit;
970                    coeff *= if bit == 0 {
971                        Complex64::new(0.0, 1.0)
972                    } else {
973                        Complex64::new(0.0, -1.0)
974                    };
975                }
976                PauliOperator::Z => {
977                    // Z adds phase based on bit value
978                    if bit == 1 {
979                        coeff *= Complex64::new(-1.0, 0.0);
980                    }
981                }
982            }
983        }
984
985        if target_state < dim {
986            result += amplitude.conj() * coeff * state[target_state];
987        }
988    }
989
990    Ok(result * pauli_string.coefficient)
991}
992
993/// Convenience functions for creating common ansätze
994pub mod ansatze {
995    use super::ParametricCircuit;
996
997    /// Create a hardware-efficient ansatz.
998    ///
999    /// Each layer applies a single-qubit `RY`/`RZ` rotation to every qubit
1000    /// followed by a linear CNOT entangling ladder (`CNOT(q, q+1)` for
1001    /// `q` in `0..num_qubits-1`). Without the entangling layer this ansatz
1002    /// could only ever represent a product state; the CNOT ladder is what
1003    /// lets it represent genuinely entangled states, as any real
1004    /// hardware-efficient VQE ansatz requires.
1005    #[must_use]
1006    pub fn hardware_efficient(num_qubits: usize, num_layers: usize) -> ParametricCircuit {
1007        let mut circuit = ParametricCircuit::new(num_qubits);
1008        let mut param_idx = 0;
1009
1010        for _layer in 0..num_layers {
1011            // Single-qubit rotations
1012            for qubit in 0..num_qubits {
1013                circuit.ry(qubit, param_idx);
1014                param_idx += 1;
1015                circuit.rz(qubit, param_idx);
1016                param_idx += 1;
1017            }
1018
1019            // Linear entangling layer: CNOT(q, q+1) for every adjacent pair.
1020            // This is the standard hardware-efficient-ansatz entangling
1021            // structure (e.g. Qiskit's `EfficientSU2` with linear
1022            // entanglement) and is what makes the ansatz capable of
1023            // representing entangled states at all.
1024            for qubit in 0..num_qubits.saturating_sub(1) {
1025                circuit.cnot(qubit, qubit + 1);
1026            }
1027        }
1028
1029        circuit
1030    }
1031
1032    /// Create a QAOA ansatz for `MaxCut` problem
1033    #[must_use]
1034    pub fn qaoa_maxcut(
1035        num_qubits: usize,
1036        num_layers: usize,
1037        edges: &[(usize, usize)],
1038    ) -> ParametricCircuit {
1039        let mut circuit = ParametricCircuit::new(num_qubits);
1040        let mut param_idx = 0;
1041
1042        // Initial superposition
1043        for qubit in 0..num_qubits {
1044            circuit.ry(qubit, param_idx); // RY(π/2) for H gate equivalent
1045        }
1046
1047        for _layer in 0..num_layers {
1048            // Problem Hamiltonian evolution (ZZ terms)
1049            for &(i, j) in edges {
1050                // Would implement ZZ rotation here
1051                // For now, approximate with RZ gates
1052                circuit.rz(i, param_idx);
1053                circuit.rz(j, param_idx);
1054                param_idx += 1;
1055            }
1056
1057            // Mixer Hamiltonian evolution (X terms)
1058            for qubit in 0..num_qubits {
1059                circuit.rx(qubit, param_idx);
1060                param_idx += 1;
1061            }
1062        }
1063
1064        circuit
1065    }
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070    use super::*;
1071
1072    #[test]
1073    fn test_parametric_rx_matrix() {
1074        let rx_gate = ParametricRX {
1075            qubit: 0,
1076            param_idx: 0,
1077        };
1078        let params = vec![PI / 2.0];
1079        let matrix = rx_gate
1080            .matrix(&params)
1081            .expect("RX gate matrix computation should succeed");
1082
1083        // RX(π/2) should be approximately [[1/√2, -i/√2], [-i/√2, 1/√2]]
1084        let expected_val = 1.0 / 2.0_f64.sqrt();
1085        assert!((matrix[[0, 0]].re - expected_val).abs() < 1e-10);
1086        assert!((matrix[[0, 1]].im + expected_val).abs() < 1e-10);
1087    }
1088
1089    #[test]
1090    fn test_autodiff_context() {
1091        let params = vec![1.0, 2.0, 3.0];
1092        let mut context = AutoDiffContext::new(params.clone(), GradientMethod::ParameterShift);
1093
1094        assert_eq!(context.parameters, params);
1095        assert_eq!(context.gradients.len(), 3);
1096
1097        context.update_parameters(vec![4.0, 5.0, 6.0]);
1098        assert_eq!(context.parameters, vec![4.0, 5.0, 6.0]);
1099    }
1100
1101    #[test]
1102    fn test_parametric_circuit_creation() {
1103        let mut circuit = ParametricCircuit::new(2);
1104        circuit.rx(0, 0);
1105        circuit.ry(1, 1);
1106
1107        assert_eq!(circuit.gates.len(), 2);
1108        assert_eq!(circuit.num_parameters, 2);
1109    }
1110
1111    #[test]
1112    fn test_hardware_efficient_ansatz() {
1113        let ansatz = ansatze::hardware_efficient(3, 2);
1114        assert_eq!(ansatz.num_qubits, 3);
1115        assert!(ansatz.num_parameters > 0);
1116    }
1117
1118    /// Regression test for the P1 finding: `hardware_efficient` used to
1119    /// omit the entangling layer entirely, so the ansatz could only ever
1120    /// represent a product state. It must now include a CNOT ladder
1121    /// (`num_qubits - 1` CNOTs per layer) and actually produce an entangled
1122    /// state for generic rotation angles.
1123    #[test]
1124    fn test_hardware_efficient_ansatz_includes_entangling_layer() {
1125        let num_qubits = 2;
1126        let num_layers = 1;
1127        let ansatz = ansatze::hardware_efficient(num_qubits, num_layers);
1128
1129        // 1 layer * (2 rotations/qubit * 2 qubits + 1 CNOT) = 5 gates.
1130        assert_eq!(ansatz.gates.len(), 5);
1131        let cnot_count = ansatz.gates.iter().filter(|g| g.name() == "CNOT").count();
1132        assert_eq!(
1133            cnot_count,
1134            num_qubits - 1,
1135            "hardware_efficient must add a linear CNOT entangling ladder"
1136        );
1137
1138        // RY(pi/2) on qubit 0 (creating (|0>+|1>)/sqrt(2)), identity on
1139        // qubit 1 (|0>), then CNOT(0, 1) produces the Bell state
1140        // (|00>+|11>)/sqrt(2). Its reduced single-qubit density matrices
1141        // must be maximally mixed (purity = 0.5), which is impossible for
1142        // any product state built purely from local single-qubit
1143        // rotations -- i.e. impossible without a real entangling layer.
1144        let params = vec![PI / 2.0, 0.0, 0.0, 0.0];
1145        let state = ansatz
1146            .evaluate(&params)
1147            .expect("ansatz evaluation should succeed");
1148
1149        // Reduced density matrix of qubit 0: rho_00 = sum_{b1} |amp(b0=0,b1)|^2 etc.
1150        let mut rho00 = Complex64::new(0.0, 0.0);
1151        let mut rho01 = Complex64::new(0.0, 0.0);
1152        let mut rho11 = Complex64::new(0.0, 0.0);
1153        for other_bit in 0..2usize {
1154            let idx0 = other_bit << 1; // qubit0 = 0, qubit1 = other_bit
1155            let idx1 = (1) | (other_bit << 1); // qubit0 = 1, qubit1 = other_bit
1156            rho00 += state[idx0] * state[idx0].conj();
1157            rho11 += state[idx1] * state[idx1].conj();
1158            rho01 += state[idx0] * state[idx1].conj();
1159        }
1160        let purity = (rho00 * rho00 + rho11 * rho11 + rho01 * rho01.conj() * 2.0).re;
1161        assert!(
1162            purity < 0.999,
1163            "expected an entangled (mixed reduced state) result, got purity {purity}"
1164        );
1165    }
1166
1167    /// Test-only parametric gate that returns a fixed (parameter-independent) matrix.
1168    /// Used to drive the forward evaluation with a known gate such as Hadamard.
1169    struct FixedGate {
1170        matrix: Array2<Complex64>,
1171        wires: Vec<usize>,
1172    }
1173
1174    impl ParametricGate for FixedGate {
1175        fn name(&self) -> &str {
1176            "FIXED"
1177        }
1178
1179        fn qubits(&self) -> Vec<usize> {
1180            self.wires.clone()
1181        }
1182
1183        fn parameter_indices(&self) -> Vec<usize> {
1184            Vec::new()
1185        }
1186
1187        fn matrix(&self, _params: &[f64]) -> Result<Array2<Complex64>> {
1188            Ok(self.matrix.clone())
1189        }
1190
1191        fn gradient(&self, _params: &[f64], _param_idx: usize) -> Result<Array2<Complex64>> {
1192            Ok(Array2::zeros(self.matrix.dim()))
1193        }
1194    }
1195
1196    #[test]
1197    fn test_evaluate_hadamard_forward_state() {
1198        // A circuit consisting of a single Hadamard on qubit 0 of a 1-qubit register.
1199        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
1200        let h_matrix = scirs2_core::ndarray::array![
1201            [
1202                Complex64::new(inv_sqrt2, 0.0),
1203                Complex64::new(inv_sqrt2, 0.0)
1204            ],
1205            [
1206                Complex64::new(inv_sqrt2, 0.0),
1207                Complex64::new(-inv_sqrt2, 0.0)
1208            ]
1209        ];
1210
1211        let mut circuit = ParametricCircuit::new(1);
1212        circuit.add_gate(Box::new(FixedGate {
1213            matrix: h_matrix,
1214            wires: vec![0],
1215        }));
1216
1217        let state = circuit
1218            .evaluate(&[])
1219            .expect("forward evaluation should succeed");
1220
1221        // Expected: (|0> + |1>)/sqrt(2), NOT the |0> placeholder.
1222        assert!((state[0].re - inv_sqrt2).abs() < 1e-10, "amp(|0>) wrong");
1223        assert!((state[1].re - inv_sqrt2).abs() < 1e-10, "amp(|1>) wrong");
1224        assert!(
1225            state[1].norm() > 1e-3,
1226            "forward pass returned the |0> placeholder instead of the real state"
1227        );
1228    }
1229
1230    #[test]
1231    fn test_evaluate_rx_pi_forward_state() {
1232        // RX(pi) maps |0> -> -i|1>; this is a genuine, parameter-dependent result.
1233        let mut circuit = ParametricCircuit::new(1);
1234        circuit.rx(0, 0);
1235
1236        let state = circuit
1237            .evaluate(&[PI])
1238            .expect("forward evaluation should succeed");
1239
1240        assert!(state[0].norm() < 1e-10, "amp(|0>) should vanish for RX(pi)");
1241        assert!((state[1].im + 1.0).abs() < 1e-10, "amp(|1>) should be -i");
1242    }
1243
1244    #[test]
1245    fn test_evaluate_bell_forward_state() {
1246        // H on qubit 0 then CNOT(0, 1) prepares a Bell state through the real forward pass.
1247        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
1248        let h_matrix = scirs2_core::ndarray::array![
1249            [
1250                Complex64::new(inv_sqrt2, 0.0),
1251                Complex64::new(inv_sqrt2, 0.0)
1252            ],
1253            [
1254                Complex64::new(inv_sqrt2, 0.0),
1255                Complex64::new(-inv_sqrt2, 0.0)
1256            ]
1257        ];
1258        let cnot = scirs2_core::ndarray::array![
1259            [
1260                Complex64::new(1.0, 0.0),
1261                Complex64::new(0.0, 0.0),
1262                Complex64::new(0.0, 0.0),
1263                Complex64::new(0.0, 0.0)
1264            ],
1265            [
1266                Complex64::new(0.0, 0.0),
1267                Complex64::new(1.0, 0.0),
1268                Complex64::new(0.0, 0.0),
1269                Complex64::new(0.0, 0.0)
1270            ],
1271            [
1272                Complex64::new(0.0, 0.0),
1273                Complex64::new(0.0, 0.0),
1274                Complex64::new(0.0, 0.0),
1275                Complex64::new(1.0, 0.0)
1276            ],
1277            [
1278                Complex64::new(0.0, 0.0),
1279                Complex64::new(0.0, 0.0),
1280                Complex64::new(1.0, 0.0),
1281                Complex64::new(0.0, 0.0)
1282            ]
1283        ];
1284
1285        let mut circuit = ParametricCircuit::new(2);
1286        circuit.add_gate(Box::new(FixedGate {
1287            matrix: h_matrix,
1288            wires: vec![0],
1289        }));
1290        circuit.add_gate(Box::new(FixedGate {
1291            matrix: cnot,
1292            wires: vec![0, 1],
1293        }));
1294
1295        let state = circuit
1296            .evaluate(&[])
1297            .expect("forward evaluation should succeed");
1298
1299        // Little-endian ordering: index = bit0 + 2*bit1.
1300        // Bell state (|00> + |11>)/sqrt(2): indices 0 and 3 populated.
1301        assert!((state[0].re - inv_sqrt2).abs() < 1e-10, "amp(|00>) wrong");
1302        assert!(state[1].norm() < 1e-10, "amp(|01>) should vanish");
1303        assert!(state[2].norm() < 1e-10, "amp(|10>) should vanish");
1304        assert!((state[3].re - inv_sqrt2).abs() < 1e-10, "amp(|11>) wrong");
1305    }
1306}