Skip to main content

quantrs2_circuit/
vqe.rs

1//! Variational Quantum Eigensolver (VQE) circuit support
2//!
3//! This module provides specialized circuits and optimizers for the Variational Quantum Eigensolver
4//! algorithm, which is used to find ground state energies of quantum systems.
5
6use crate::builder::Circuit;
7use quantrs2_core::{
8    error::{QuantRS2Error, QuantRS2Result},
9    gate::single::{RotationX, RotationY, RotationZ},
10    gate::GateOp,
11    qubit::QubitId,
12};
13use scirs2_core::Complex64;
14use std::collections::HashMap;
15
16/// Which axis a parameterized rotation gate acts on.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum RotationAxis {
19    Y,
20    Z,
21    X,
22}
23
24/// Record of a parameterized gate: its position in the circuit's gate list,
25/// the target qubit, the rotation axis, and the parameter index it uses.
26#[derive(Debug, Clone)]
27pub struct ParameterizedGateRecord {
28    /// Position of this gate in `circuit.gates()` (gate list index).
29    pub gate_index: usize,
30    /// Target qubit for the rotation.
31    pub qubit: QubitId,
32    /// Rotation axis.
33    pub axis: RotationAxis,
34    /// Index into `parameters` for the angle.
35    pub param_index: usize,
36}
37
38/// A parameterized quantum circuit for VQE applications
39///
40/// VQE circuits are characterized by:
41/// - Parameterized gates whose angles can be optimized
42/// - Specific ansatz structures (e.g., UCCSD, hardware-efficient)
43/// - Observable measurement capabilities
44#[derive(Debug, Clone)]
45pub struct VQECircuit<const N: usize> {
46    /// The underlying quantum circuit
47    pub circuit: Circuit<N>,
48    /// Parameters that can be optimized
49    pub parameters: Vec<f64>,
50    /// Parameter names for identification
51    pub parameter_names: Vec<String>,
52    /// Mapping from parameter names to indices
53    parameter_map: HashMap<String, usize>,
54    /// Ordered list of parameterized gate records: used by `set_parameters` to
55    /// rebuild the circuit's rotation angles when parameters change.
56    param_gate_records: Vec<ParameterizedGateRecord>,
57}
58
59/// VQE ansatz types for different quantum chemistry problems
60#[derive(Debug, Clone, PartialEq)]
61pub enum VQEAnsatz {
62    /// Hardware-efficient ansatz with alternating rotation and entangling layers
63    HardwareEfficient { layers: usize },
64    /// Unitary Coupled-Cluster Singles and Doubles
65    UCCSD {
66        occupied_orbitals: usize,
67        virtual_orbitals: usize,
68    },
69    /// Real-space ansatz for condensed matter systems
70    RealSpace { geometry: Vec<(f64, f64, f64)> },
71    /// Custom ansatz defined by user
72    Custom,
73}
74
75/// Observable for VQE energy measurements
76#[derive(Debug, Clone)]
77pub struct VQEObservable {
78    /// Pauli string coefficients and operators
79    pub terms: Vec<(f64, Vec<(usize, PauliOperator)>)>,
80}
81
82/// Pauli operators for observable construction
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PauliOperator {
85    I, // Identity
86    X, // Pauli-X
87    Y, // Pauli-Y
88    Z, // Pauli-Z
89}
90
91/// VQE optimization result
92#[derive(Debug, Clone)]
93pub struct VQEResult {
94    /// Optimized parameters
95    pub optimal_parameters: Vec<f64>,
96    /// Ground state energy
97    pub ground_state_energy: f64,
98    /// Number of optimization iterations
99    pub iterations: usize,
100    /// Convergence status
101    pub converged: bool,
102    /// Final gradient norm
103    pub gradient_norm: f64,
104}
105
106impl<const N: usize> VQECircuit<N> {
107    /// Create a new VQE circuit with specified ansatz
108    pub fn new(ansatz: VQEAnsatz) -> QuantRS2Result<Self> {
109        let mut circuit = Circuit::new();
110        let mut parameters = Vec::new();
111        let mut parameter_names = Vec::new();
112        let mut parameter_map = HashMap::new();
113        let mut param_gate_records: Vec<ParameterizedGateRecord> = Vec::new();
114
115        match ansatz {
116            VQEAnsatz::HardwareEfficient { layers } => {
117                Self::build_hardware_efficient_ansatz(
118                    &mut circuit,
119                    &mut parameters,
120                    &mut parameter_names,
121                    &mut parameter_map,
122                    &mut param_gate_records,
123                    layers,
124                )?;
125            }
126            VQEAnsatz::UCCSD {
127                occupied_orbitals,
128                virtual_orbitals,
129            } => {
130                Self::build_uccsd_ansatz(
131                    &mut circuit,
132                    &mut parameters,
133                    &mut parameter_names,
134                    &mut parameter_map,
135                    &mut param_gate_records,
136                    occupied_orbitals,
137                    virtual_orbitals,
138                )?;
139            }
140            VQEAnsatz::RealSpace { geometry } => {
141                Self::build_real_space_ansatz(
142                    &mut circuit,
143                    &mut parameters,
144                    &mut parameter_names,
145                    &mut parameter_map,
146                    &mut param_gate_records,
147                    &geometry,
148                )?;
149            }
150            VQEAnsatz::Custom => {
151                // Custom ansatz - circuit will be built by user
152            }
153        }
154
155        Ok(Self {
156            circuit,
157            parameters,
158            parameter_names,
159            parameter_map,
160            param_gate_records,
161        })
162    }
163
164    /// Build a hardware-efficient ansatz
165    fn build_hardware_efficient_ansatz(
166        circuit: &mut Circuit<N>,
167        parameters: &mut Vec<f64>,
168        parameter_names: &mut Vec<String>,
169        parameter_map: &mut HashMap<String, usize>,
170        param_gate_records: &mut Vec<ParameterizedGateRecord>,
171        layers: usize,
172    ) -> QuantRS2Result<()> {
173        for layer in 0..layers {
174            // Single-qubit rotation layer
175            for qubit in 0..N {
176                // RY rotation
177                let param_name = format!("ry_{layer}_q{qubit}");
178                let param_idx = parameters.len();
179                parameter_names.push(param_name.clone());
180                parameter_map.insert(param_name, param_idx);
181                parameters.push(0.0);
182
183                let gate_idx = circuit.gates().len();
184                circuit.ry(QubitId(qubit as u32), 0.0)?;
185                param_gate_records.push(ParameterizedGateRecord {
186                    gate_index: gate_idx,
187                    qubit: QubitId(qubit as u32),
188                    axis: RotationAxis::Y,
189                    param_index: param_idx,
190                });
191
192                // RZ rotation
193                let param_name = format!("rz_{layer}_q{qubit}");
194                let param_idx = parameters.len();
195                parameter_names.push(param_name.clone());
196                parameter_map.insert(param_name, param_idx);
197                parameters.push(0.0);
198
199                let gate_idx = circuit.gates().len();
200                circuit.rz(QubitId(qubit as u32), 0.0)?;
201                param_gate_records.push(ParameterizedGateRecord {
202                    gate_index: gate_idx,
203                    qubit: QubitId(qubit as u32),
204                    axis: RotationAxis::Z,
205                    param_index: param_idx,
206                });
207            }
208
209            // Entangling layer (linear connectivity)
210            for qubit in 0..(N - 1) {
211                circuit.cnot(QubitId(qubit as u32), QubitId((qubit + 1) as u32))?;
212            }
213        }
214
215        Ok(())
216    }
217
218    /// Build a UCCSD ansatz (simplified version)
219    fn build_uccsd_ansatz(
220        circuit: &mut Circuit<N>,
221        parameters: &mut Vec<f64>,
222        parameter_names: &mut Vec<String>,
223        parameter_map: &mut HashMap<String, usize>,
224        param_gate_records: &mut Vec<ParameterizedGateRecord>,
225        occupied_orbitals: usize,
226        virtual_orbitals: usize,
227    ) -> QuantRS2Result<()> {
228        if occupied_orbitals + virtual_orbitals > N {
229            return Err(QuantRS2Error::InvalidInput(format!(
230                "Total orbitals ({}) exceeds number of qubits ({})",
231                occupied_orbitals + virtual_orbitals,
232                N
233            )));
234        }
235
236        // Initialize with Hartree-Fock state
237        for i in 0..occupied_orbitals {
238            circuit.x(QubitId(i as u32))?;
239        }
240
241        // Single excitations
242        for i in 0..occupied_orbitals {
243            for a in occupied_orbitals..(occupied_orbitals + virtual_orbitals) {
244                let param_name = format!("t1_{i}_{a}");
245                let param_idx = parameters.len();
246                parameter_names.push(param_name.clone());
247                parameter_map.insert(param_name, param_idx);
248                parameters.push(0.0);
249
250                circuit.cnot(QubitId(i as u32), QubitId(a as u32))?;
251                let gate_idx = circuit.gates().len();
252                circuit.ry(QubitId(a as u32), 0.0)?;
253                param_gate_records.push(ParameterizedGateRecord {
254                    gate_index: gate_idx,
255                    qubit: QubitId(a as u32),
256                    axis: RotationAxis::Y,
257                    param_index: param_idx,
258                });
259                circuit.cnot(QubitId(i as u32), QubitId(a as u32))?;
260            }
261        }
262
263        // Double excitations (simplified)
264        for i in 0..occupied_orbitals {
265            for j in (i + 1)..occupied_orbitals {
266                for a in occupied_orbitals..(occupied_orbitals + virtual_orbitals) {
267                    for b in (a + 1)..(occupied_orbitals + virtual_orbitals) {
268                        if a < N && b < N {
269                            let param_name = format!("t2_{i}_{j}_{a}_{b}");
270                            let param_idx = parameters.len();
271                            parameter_names.push(param_name.clone());
272                            parameter_map.insert(param_name, param_idx);
273                            parameters.push(0.0);
274
275                            circuit.cnot(QubitId(i as u32), QubitId(a as u32))?;
276                            circuit.cnot(QubitId(j as u32), QubitId(b as u32))?;
277                            let gate_idx = circuit.gates().len();
278                            circuit.ry(QubitId(a as u32), 0.0)?;
279                            param_gate_records.push(ParameterizedGateRecord {
280                                gate_index: gate_idx,
281                                qubit: QubitId(a as u32),
282                                axis: RotationAxis::Y,
283                                param_index: param_idx,
284                            });
285                            circuit.cnot(QubitId(j as u32), QubitId(b as u32))?;
286                            circuit.cnot(QubitId(i as u32), QubitId(a as u32))?;
287                        }
288                    }
289                }
290            }
291        }
292
293        Ok(())
294    }
295
296    /// Build a real-space ansatz
297    fn build_real_space_ansatz(
298        circuit: &mut Circuit<N>,
299        parameters: &mut Vec<f64>,
300        parameter_names: &mut Vec<String>,
301        parameter_map: &mut HashMap<String, usize>,
302        param_gate_records: &mut Vec<ParameterizedGateRecord>,
303        geometry: &[(f64, f64, f64)],
304    ) -> QuantRS2Result<()> {
305        if geometry.len() > N {
306            return Err(QuantRS2Error::InvalidInput(format!(
307                "Geometry has {} sites but circuit only has {} qubits",
308                geometry.len(),
309                N
310            )));
311        }
312
313        // Build ansatz based on geometric connectivity
314        for (i, &(x1, y1, z1)) in geometry.iter().enumerate() {
315            for (j, &(x2, y2, z2)) in geometry.iter().enumerate().skip(i + 1) {
316                let distance = (z2 - z1)
317                    .mul_add(z2 - z1, (y2 - y1).mul_add(y2 - y1, (x2 - x1).powi(2)))
318                    .sqrt();
319
320                // Only include interactions within a cutoff distance
321                if distance < 3.0 {
322                    let param_name = format!("j_{i}_{j}");
323                    let param_idx = parameters.len();
324                    parameter_names.push(param_name.clone());
325                    parameter_map.insert(param_name, param_idx);
326                    parameters.push(0.0);
327
328                    circuit.cnot(QubitId(i as u32), QubitId(j as u32))?;
329                    let gate_idx = circuit.gates().len();
330                    circuit.rz(QubitId(j as u32), 0.0)?;
331                    param_gate_records.push(ParameterizedGateRecord {
332                        gate_index: gate_idx,
333                        qubit: QubitId(j as u32),
334                        axis: RotationAxis::Z,
335                        param_index: param_idx,
336                    });
337                    circuit.cnot(QubitId(i as u32), QubitId(j as u32))?;
338                }
339            }
340        }
341
342        Ok(())
343    }
344
345    /// Update circuit parameters and rebuild all parameterized rotation gates.
346    ///
347    /// Uses `param_gate_records` to locate each parameterized gate in the gate
348    /// list.  The entire circuit is reconstructed from `gates_as_boxes()`, with
349    /// each parameterized gate replaced by a new rotation gate carrying the
350    /// updated angle.  Non-parameterized gates are kept verbatim.
351    pub fn set_parameters(&mut self, new_parameters: &[f64]) -> QuantRS2Result<()> {
352        if new_parameters.len() != self.parameters.len() {
353            return Err(QuantRS2Error::InvalidInput(format!(
354                "Expected {} parameters, got {}",
355                self.parameters.len(),
356                new_parameters.len()
357            )));
358        }
359
360        self.parameters = new_parameters.to_vec();
361
362        // Build a map from gate_index → ParameterizedGateRecord for fast lookup.
363        let record_map: HashMap<usize, &ParameterizedGateRecord> = self
364            .param_gate_records
365            .iter()
366            .map(|r| (r.gate_index, r))
367            .collect();
368
369        // Collect all existing gates as boxed trait objects.
370        let old_gates = self.circuit.gates_as_boxes();
371
372        // Rebuild a new gate list, substituting updated rotation angles where recorded.
373        let new_gates: Vec<Box<dyn GateOp>> = old_gates
374            .into_iter()
375            .enumerate()
376            .map(|(idx, gate)| -> Box<dyn GateOp> {
377                if let Some(record) = record_map.get(&idx) {
378                    let angle = self.parameters[record.param_index];
379                    match record.axis {
380                        RotationAxis::Y => Box::new(RotationY {
381                            target: record.qubit,
382                            theta: angle,
383                        }),
384                        RotationAxis::Z => Box::new(RotationZ {
385                            target: record.qubit,
386                            theta: angle,
387                        }),
388                        RotationAxis::X => Box::new(RotationX {
389                            target: record.qubit,
390                            theta: angle,
391                        }),
392                    }
393                } else {
394                    gate
395                }
396            })
397            .collect();
398
399        // Replace the circuit with the rebuilt version.
400        self.circuit = Circuit::<N>::from_gates(new_gates)?;
401
402        Ok(())
403    }
404
405    /// Get a parameter by name
406    #[must_use]
407    pub fn get_parameter(&self, name: &str) -> Option<f64> {
408        self.parameter_map
409            .get(name)
410            .map(|&index| self.parameters[index])
411    }
412
413    /// Set a parameter by name
414    pub fn set_parameter(&mut self, name: &str, value: f64) -> QuantRS2Result<()> {
415        let index = self
416            .parameter_map
417            .get(name)
418            .ok_or_else(|| QuantRS2Error::InvalidInput(format!("Parameter '{name}' not found")))?;
419
420        self.parameters[*index] = value;
421        Ok(())
422    }
423
424    /// Add a custom parameterized RY gate.
425    ///
426    /// Records the gate position so that `set_parameters` can later update its angle.
427    pub fn add_parameterized_ry(
428        &mut self,
429        qubit: QubitId,
430        parameter_name: &str,
431    ) -> QuantRS2Result<()> {
432        if self.parameter_map.contains_key(parameter_name) {
433            return Err(QuantRS2Error::InvalidInput(format!(
434                "Parameter '{parameter_name}' already exists"
435            )));
436        }
437
438        let param_idx = self.parameters.len();
439        self.parameter_names.push(parameter_name.to_string());
440        self.parameter_map
441            .insert(parameter_name.to_string(), param_idx);
442        self.parameters.push(0.0);
443
444        let gate_idx = self.circuit.gates().len();
445        self.circuit.ry(qubit, 0.0)?;
446        self.param_gate_records.push(ParameterizedGateRecord {
447            gate_index: gate_idx,
448            qubit,
449            axis: RotationAxis::Y,
450            param_index: param_idx,
451        });
452
453        Ok(())
454    }
455
456    /// Add a custom parameterized RZ gate.
457    ///
458    /// Records the gate position so that `set_parameters` can later update its angle.
459    pub fn add_parameterized_rz(
460        &mut self,
461        qubit: QubitId,
462        parameter_name: &str,
463    ) -> QuantRS2Result<()> {
464        if self.parameter_map.contains_key(parameter_name) {
465            return Err(QuantRS2Error::InvalidInput(format!(
466                "Parameter '{parameter_name}' already exists"
467            )));
468        }
469
470        let param_idx = self.parameters.len();
471        self.parameter_names.push(parameter_name.to_string());
472        self.parameter_map
473            .insert(parameter_name.to_string(), param_idx);
474        self.parameters.push(0.0);
475
476        let gate_idx = self.circuit.gates().len();
477        self.circuit.rz(qubit, 0.0)?;
478        self.param_gate_records.push(ParameterizedGateRecord {
479            gate_index: gate_idx,
480            qubit,
481            axis: RotationAxis::Z,
482            param_index: param_idx,
483        });
484
485        Ok(())
486    }
487
488    /// Get the number of parameters
489    #[must_use]
490    pub fn num_parameters(&self) -> usize {
491        self.parameters.len()
492    }
493}
494
495impl VQEObservable {
496    /// Create a new empty observable
497    #[must_use]
498    pub const fn new() -> Self {
499        Self { terms: Vec::new() }
500    }
501
502    /// Add a Pauli string term to the observable
503    pub fn add_pauli_term(&mut self, coefficient: f64, pauli_string: Vec<(usize, PauliOperator)>) {
504        self.terms.push((coefficient, pauli_string));
505    }
506
507    /// Create a Heisenberg model Hamiltonian
508    #[must_use]
509    pub fn heisenberg_model(num_qubits: usize, j_coupling: f64) -> Self {
510        let mut observable = Self::new();
511
512        for i in 0..(num_qubits - 1) {
513            // XX term
514            observable.add_pauli_term(
515                j_coupling,
516                vec![(i, PauliOperator::X), (i + 1, PauliOperator::X)],
517            );
518            // YY term
519            observable.add_pauli_term(
520                j_coupling,
521                vec![(i, PauliOperator::Y), (i + 1, PauliOperator::Y)],
522            );
523            // ZZ term
524            observable.add_pauli_term(
525                j_coupling,
526                vec![(i, PauliOperator::Z), (i + 1, PauliOperator::Z)],
527            );
528        }
529
530        observable
531    }
532
533    /// Create a transverse field Ising model Hamiltonian
534    #[must_use]
535    pub fn tfim(num_qubits: usize, j_coupling: f64, h_field: f64) -> Self {
536        let mut observable = Self::new();
537
538        // ZZ interactions
539        for i in 0..(num_qubits - 1) {
540            observable.add_pauli_term(
541                -j_coupling,
542                vec![(i, PauliOperator::Z), (i + 1, PauliOperator::Z)],
543            );
544        }
545
546        // X field terms
547        for i in 0..num_qubits {
548            observable.add_pauli_term(-h_field, vec![(i, PauliOperator::X)]);
549        }
550
551        observable
552    }
553
554    /// Create a molecular Hamiltonian (simplified version)
555    #[must_use]
556    pub fn molecular_hamiltonian(
557        one_body: &[(usize, usize, f64)],
558        two_body: &[(usize, usize, usize, usize, f64)],
559    ) -> Self {
560        let mut observable = Self::new();
561
562        // One-body terms (simplified representation)
563        for &(i, j, coeff) in one_body {
564            if i == j {
565                // Diagonal term
566                observable.add_pauli_term(coeff, vec![(i, PauliOperator::Z)]);
567            } else {
568                // Off-diagonal terms (simplified)
569                observable
570                    .add_pauli_term(coeff, vec![(i, PauliOperator::X), (j, PauliOperator::X)]);
571                observable
572                    .add_pauli_term(coeff, vec![(i, PauliOperator::Y), (j, PauliOperator::Y)]);
573            }
574        }
575
576        // Two-body terms (very simplified representation)
577        for &(i, j, k, l, coeff) in two_body {
578            // This is a simplified representation - real molecular Hamiltonians
579            // require more sophisticated fermion-to-qubit mappings
580            observable.add_pauli_term(
581                coeff,
582                vec![
583                    (i, PauliOperator::Z),
584                    (j, PauliOperator::Z),
585                    (k, PauliOperator::Z),
586                    (l, PauliOperator::Z),
587                ],
588            );
589        }
590
591        observable
592    }
593}
594
595impl Default for VQEObservable {
596    fn default() -> Self {
597        Self::new()
598    }
599}
600
601/// VQE optimizer for finding ground state energies
602pub struct VQEOptimizer {
603    /// Maximum number of iterations
604    pub max_iterations: usize,
605    /// Convergence tolerance
606    pub tolerance: f64,
607    /// Learning rate for gradient descent
608    pub learning_rate: f64,
609    /// Optimizer type
610    pub optimizer_type: VQEOptimizerType,
611}
612
613/// Types of optimizers available for VQE
614#[derive(Debug, Clone, PartialEq)]
615pub enum VQEOptimizerType {
616    /// Gradient descent
617    GradientDescent,
618    /// Adam optimizer
619    Adam { beta1: f64, beta2: f64 },
620    /// BFGS quasi-Newton method
621    BFGS,
622    /// Nelder-Mead simplex
623    NelderMead,
624    /// SPSA (Simultaneous Perturbation Stochastic Approximation)
625    SPSA { alpha: f64, gamma: f64 },
626}
627
628impl VQEOptimizer {
629    /// Create a new VQE optimizer
630    #[must_use]
631    pub const fn new(optimizer_type: VQEOptimizerType) -> Self {
632        Self {
633            max_iterations: 1000,
634            tolerance: 1e-6,
635            learning_rate: 0.01,
636            optimizer_type,
637        }
638    }
639
640    /// Optimize VQE circuit parameters
641    pub fn optimize<const N: usize>(
642        &self,
643        circuit: &mut VQECircuit<N>,
644        observable: &VQEObservable,
645    ) -> QuantRS2Result<VQEResult> {
646        // This is a simplified implementation - a full VQE optimizer would:
647        // 1. Evaluate the expectation value of the observable
648        // 2. Compute gradients (analytically or numerically)
649        // 3. Update parameters using the chosen optimization algorithm
650        // 4. Check for convergence
651
652        let mut best_energy = self.evaluate_energy(circuit, observable)?;
653        let mut best_parameters = circuit.parameters.clone();
654
655        for iteration in 0..self.max_iterations {
656            // Gradient at the current parameter point (analytic parameter-shift).
657            let gradients = self.compute_gradients(circuit, observable)?;
658            let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
659
660            // Converged: the gradient is (numerically) zero, so we are at a
661            // stationary point.  Check this *before* taking another step.
662            if gradient_norm < self.tolerance {
663                // Make sure the circuit holds the best parameters found.
664                circuit.set_parameters(&best_parameters)?;
665                return Ok(VQEResult {
666                    optimal_parameters: best_parameters.clone(),
667                    ground_state_energy: best_energy,
668                    iterations: iteration + 1,
669                    converged: true,
670                    gradient_norm,
671                });
672            }
673
674            // Gradient-descent update.  Crucially we rebuild the underlying
675            // circuit via `set_parameters` so that the next energy/gradient
676            // evaluation simulates the *updated* state (a previous version
677            // mutated `parameters` directly, leaving the circuit gates stale).
678            let mut next_parameters = circuit.parameters.clone();
679            for (param, gradient) in next_parameters.iter_mut().zip(gradients.iter()) {
680                *param -= self.learning_rate * gradient;
681            }
682            circuit.set_parameters(&next_parameters)?;
683
684            // Evaluate new energy and track the best point seen.
685            let current_energy = self.evaluate_energy(circuit, observable)?;
686            if current_energy < best_energy {
687                best_energy = current_energy;
688                best_parameters.clone_from(&circuit.parameters);
689            }
690        }
691
692        // Restore the best parameters and report the gradient norm there.
693        circuit.set_parameters(&best_parameters)?;
694        let final_gradient = self.compute_gradients(circuit, observable)?;
695        let final_gradient_norm = final_gradient.iter().map(|g| g * g).sum::<f64>().sqrt();
696        Ok(VQEResult {
697            optimal_parameters: best_parameters.clone(),
698            ground_state_energy: best_energy,
699            iterations: self.max_iterations,
700            converged: false,
701            gradient_norm: final_gradient_norm,
702        })
703    }
704
705    /// Evaluate the energy expectation value `⟨ψ(θ)|H|ψ(θ)⟩`.
706    ///
707    /// The ansatz state `|ψ(θ)⟩` is obtained by simulating the parameterized
708    /// circuit on a dense state vector starting from `|0…0⟩` (see
709    /// [`statevector::simulate`]).  The observable energy is the sum of each
710    /// Pauli-string term's coefficient times its expectation value
711    /// `⟨ψ|P|ψ⟩`, computed exactly via [`statevector::pauli_string_expectation`].
712    fn evaluate_energy<const N: usize>(
713        &self,
714        circuit: &VQECircuit<N>,
715        observable: &VQEObservable,
716    ) -> QuantRS2Result<f64> {
717        let state = statevector::simulate(&circuit.circuit)?;
718
719        let mut energy = 0.0;
720        for (coefficient, pauli_string) in &observable.terms {
721            let expectation = statevector::pauli_string_expectation(&state, N, pauli_string)?;
722            // For a physical Hamiltonian every Pauli expectation is real; we take
723            // the real part and surface any spurious imaginary component as an
724            // error rather than silently discarding it.
725            if expectation.im.abs() > 1e-9 {
726                return Err(QuantRS2Error::ComputationError(format!(
727                    "Pauli-string expectation has non-negligible imaginary part ({:.3e}); \
728                     observable is not Hermitian",
729                    expectation.im
730                )));
731            }
732            energy += coefficient * expectation.re;
733        }
734
735        Ok(energy)
736    }
737
738    /// Compute parameter gradients using the analytic parameter-shift rule.
739    ///
740    /// For a gate generated by a Pauli operator `P` (so that `U(θ) =
741    /// exp(-i θ P / 2)`, which holds for the `RX`/`RY`/`RZ` rotations used by all
742    /// the VQE ansätze), the energy gradient with respect to that parameter is
743    /// `∂E/∂θ = ½[E(θ + π/2) − E(θ − π/2)]`.  This is exact (not a finite-
744    /// difference approximation) for such gates.
745    fn compute_gradients<const N: usize>(
746        &self,
747        circuit: &VQECircuit<N>,
748        observable: &VQEObservable,
749    ) -> QuantRS2Result<Vec<f64>> {
750        let num_params = circuit.parameters.len();
751        let mut gradients = Vec::with_capacity(num_params);
752
753        // Work on a clone so the caller's circuit/parameters are untouched.
754        let mut shifted = circuit.clone();
755        let base_parameters = circuit.parameters.clone();
756        let shift = std::f64::consts::FRAC_PI_2;
757
758        for i in 0..num_params {
759            let mut plus = base_parameters.clone();
760            plus[i] += shift;
761            shifted.set_parameters(&plus)?;
762            let energy_plus = self.evaluate_energy(&shifted, observable)?;
763
764            let mut minus = base_parameters.clone();
765            minus[i] -= shift;
766            shifted.set_parameters(&minus)?;
767            let energy_minus = self.evaluate_energy(&shifted, observable)?;
768
769            gradients.push(0.5 * (energy_plus - energy_minus));
770        }
771
772        // Restore the original parameters on the working copy (defensive; the
773        // clone is dropped anyway, but keeps the helper side-effect-free).
774        shifted.set_parameters(&base_parameters)?;
775
776        Ok(gradients)
777    }
778}
779
780/// Dense state-vector simulation utilities used by the VQE energy evaluator.
781///
782/// `quantrs2-circuit` is a dependency of `quantrs2-sim`, so it cannot depend on
783/// the simulator crate (that would be a dependency cycle).  These helpers
784/// therefore provide a small, self-contained exact state-vector engine driven
785/// purely by the generic [`GateOp::matrix`] / [`GateOp::qubits`] interface, so
786/// they correctly handle *every* gate type a VQE ansatz can contain, not just a
787/// hard-coded subset.
788mod statevector {
789    use super::{Circuit, GateOp};
790    use quantrs2_core::error::{QuantRS2Error, QuantRS2Result};
791    use scirs2_core::Complex64;
792
793    use super::PauliOperator;
794
795    /// Simulate `circuit` on `2^N` amplitudes starting from `|0…0⟩`.
796    pub fn simulate<const N: usize>(circuit: &Circuit<N>) -> QuantRS2Result<Vec<Complex64>> {
797        let dim = 1usize << N;
798        let mut state = vec![Complex64::new(0.0, 0.0); dim];
799        state[0] = Complex64::new(1.0, 0.0);
800
801        for gate in circuit.gates() {
802            apply_gate(&mut state, N, gate.as_ref())?;
803        }
804
805        Ok(state)
806    }
807
808    /// Apply a single (possibly multi-qubit) gate to the state vector in place.
809    ///
810    /// The gate's `2^k × 2^k` unitary (row-major, `k = gate.num_qubits()`) is
811    /// applied to the subspace spanned by the gate's qubits.  Qubit `q` is the
812    /// bit at position `q` of the basis index (little-endian), matching the rest
813    /// of the framework's `QubitId` convention.
814    pub fn apply_gate(
815        state: &mut [Complex64],
816        num_qubits: usize,
817        gate: &dyn GateOp,
818    ) -> QuantRS2Result<()> {
819        let targets: Vec<usize> = gate.qubits().iter().map(|q| q.id() as usize).collect();
820        let k = targets.len();
821        if k == 0 {
822            // Gates with no qubits (e.g. a global barrier) act as the identity.
823            return Ok(());
824        }
825        for &t in &targets {
826            if t >= num_qubits {
827                return Err(QuantRS2Error::InvalidInput(format!(
828                    "Gate '{}' acts on qubit {} but circuit only has {} qubits",
829                    gate.name(),
830                    t,
831                    num_qubits
832                )));
833            }
834        }
835
836        let matrix = gate.matrix()?;
837        let side = 1usize << k;
838        if matrix.len() != side * side {
839            return Err(QuantRS2Error::InvalidInput(format!(
840                "Gate '{}' returned a {}-element matrix but {} qubits require {}",
841                gate.name(),
842                matrix.len(),
843                k,
844                side * side
845            )));
846        }
847
848        // Map each *local* bit position of the gate-block index to a state
849        // qubit.  The framework's gate matrices are row-major in the basis where
850        // the FIRST qubit of `qubits()` is the most-significant bit of the block
851        // index (e.g. CNOT with `qubits()=[control,target]` swaps block indices
852        // 2↔3 = |10⟩↔|11⟩, flipping the target when the control is set).  So
853        // local bit `p` (p=0 is the LSB of the block index) corresponds to
854        // `targets[k - 1 - p]`.
855        let bit_masks: Vec<usize> = (0..k).map(|p| 1usize << targets[k - 1 - p]).collect();
856        // Mask of all qubit bits touched by the gate; we iterate over every
857        // assignment of the remaining bits and apply the dense block to the 2^k
858        // amplitudes selected by the target bits.
859        let mut fixed_mask = 0usize;
860        for &m in &bit_masks {
861            fixed_mask |= m;
862        }
863        let dim = state.len();
864
865        let mut visited = vec![false; dim];
866        let mut amplitudes = vec![Complex64::new(0.0, 0.0); side];
867        let mut indices = vec![0usize; side];
868
869        for base in 0..dim {
870            if visited[base] || (base & fixed_mask) != 0 {
871                // Only start from indices whose target bits are all zero; that
872                // base seeds exactly one 2^k block.
873                continue;
874            }
875
876            // Gather the amplitudes of the block.
877            for (local, slot) in indices.iter_mut().enumerate() {
878                let mut idx = base;
879                for (bit, &mask) in bit_masks.iter().enumerate() {
880                    if (local >> bit) & 1 == 1 {
881                        idx |= mask;
882                    }
883                }
884                *slot = idx;
885                amplitudes[local] = state[idx];
886                visited[idx] = true;
887            }
888
889            // Apply the dense unitary block: out[r] = Σ_c M[r,c] · in[c].
890            for r in 0..side {
891                let mut acc = Complex64::new(0.0, 0.0);
892                let row = r * side;
893                for (c, amp) in amplitudes.iter().enumerate() {
894                    acc += matrix[row + c] * amp;
895                }
896                state[indices[r]] = acc;
897            }
898        }
899
900        Ok(())
901    }
902
903    /// Compute `⟨ψ|P|ψ⟩` for a Pauli string `P = ⊗_q P_q`.
904    ///
905    /// Qubits absent from `pauli_string` carry an implicit identity.  The Pauli
906    /// operators are applied directly to a working copy of the state (no dense
907    /// matrix is formed), then the overlap with the original state is returned.
908    pub fn pauli_string_expectation(
909        state: &[Complex64],
910        num_qubits: usize,
911        pauli_string: &[(usize, PauliOperator)],
912    ) -> QuantRS2Result<Complex64> {
913        let mut transformed = state.to_vec();
914
915        for &(qubit, op) in pauli_string {
916            if qubit >= num_qubits {
917                return Err(QuantRS2Error::InvalidInput(format!(
918                    "Pauli term targets qubit {qubit} but state has {num_qubits} qubits"
919                )));
920            }
921            apply_pauli(&mut transformed, qubit, op);
922        }
923
924        // ⟨ψ|P|ψ⟩ = Σ_i conj(ψ_i) · (Pψ)_i
925        let mut acc = Complex64::new(0.0, 0.0);
926        for (psi, pphi) in state.iter().zip(transformed.iter()) {
927            acc += psi.conj() * pphi;
928        }
929        Ok(acc)
930    }
931
932    /// Apply a single-qubit Pauli operator to `state` in place.
933    fn apply_pauli(state: &mut [Complex64], qubit: usize, op: PauliOperator) {
934        let mask = 1usize << qubit;
935        match op {
936            PauliOperator::I => {}
937            PauliOperator::X => {
938                for idx in 0..state.len() {
939                    if idx & mask == 0 {
940                        state.swap(idx, idx | mask);
941                    }
942                }
943            }
944            PauliOperator::Y => {
945                // Y|0⟩ = i|1⟩, Y|1⟩ = -i|0⟩.
946                let i = Complex64::new(0.0, 1.0);
947                for idx in 0..state.len() {
948                    if idx & mask == 0 {
949                        let partner = idx | mask;
950                        let a = state[idx];
951                        let b = state[partner];
952                        state[idx] = -i * b;
953                        state[partner] = i * a;
954                    }
955                }
956            }
957            PauliOperator::Z => {
958                for (idx, amp) in state.iter_mut().enumerate() {
959                    if idx & mask != 0 {
960                        *amp = -*amp;
961                    }
962                }
963            }
964        }
965    }
966
967    #[cfg(test)]
968    mod tests {
969        use super::super::{Circuit, QubitId};
970        use super::{apply_gate, simulate};
971        use quantrs2_core::gate::multi::CNOT;
972        use quantrs2_core::gate::single::{Hadamard, PauliX};
973        use scirs2_core::Complex64;
974
975        /// CNOT must map |10⟩ → |11⟩ (control = qubit 0, the MSB of the gate
976        /// block).  This pins down the multi-qubit endianness: a wrong mapping
977        /// would instead flip qubit 0 when qubit 1 is set.
978        #[test]
979        fn test_cnot_endianness() {
980            // 2-qubit state |10⟩: qubit 0 = 1.  Little-endian basis index = 1<<0 = 1.
981            let mut state = vec![Complex64::new(0.0, 0.0); 4];
982            state[1] = Complex64::new(1.0, 0.0); // |q1 q0⟩ index: q0=1 → idx 1 = |10⟩
983            let cnot = CNOT {
984                control: QubitId(0),
985                target: QubitId(1),
986            };
987            apply_gate(&mut state, 2, &cnot).expect("apply cnot");
988            // Expect |11⟩: q0=1, q1=1 → idx = 0b11 = 3.
989            assert!((state[3] - Complex64::new(1.0, 0.0)).norm() < 1e-12);
990            for (i, amp) in state.iter().enumerate() {
991                if i != 3 {
992                    assert!(amp.norm() < 1e-12, "unexpected amplitude at {i}: {amp}");
993                }
994            }
995        }
996
997        /// CNOT must leave |01⟩ unchanged (control qubit 0 = 0).
998        #[test]
999        fn test_cnot_control_zero_is_identity() {
1000            let mut state = vec![Complex64::new(0.0, 0.0); 4];
1001            state[2] = Complex64::new(1.0, 0.0); // q1=1, q0=0 → idx 0b10 = 2 = |01⟩
1002            let cnot = CNOT {
1003                control: QubitId(0),
1004                target: QubitId(1),
1005            };
1006            apply_gate(&mut state, 2, &cnot).expect("apply cnot");
1007            assert!((state[2] - Complex64::new(1.0, 0.0)).norm() < 1e-12);
1008        }
1009
1010        /// A Bell circuit H(0); CNOT(0,1) produces (|00⟩ + |11⟩)/√2.
1011        #[test]
1012        fn test_bell_state() {
1013            let mut circuit = Circuit::<2>::new();
1014            circuit
1015                .add_gate(Hadamard { target: QubitId(0) })
1016                .expect("h");
1017            circuit
1018                .add_gate(CNOT {
1019                    control: QubitId(0),
1020                    target: QubitId(1),
1021                })
1022                .expect("cnot");
1023
1024            let state = simulate(&circuit).expect("simulate");
1025            let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
1026            assert!(
1027                (state[0].re - inv_sqrt2).abs() < 1e-12,
1028                "|00>: {}",
1029                state[0]
1030            );
1031            assert!(state[1].norm() < 1e-12, "|10>: {}", state[1]);
1032            assert!(state[2].norm() < 1e-12, "|01>: {}", state[2]);
1033            assert!(
1034                (state[3].re - inv_sqrt2).abs() < 1e-12,
1035                "|11>: {}",
1036                state[3]
1037            );
1038        }
1039
1040        /// Applying X to qubit 1 of |00⟩ sets exactly qubit 1 (the high bit).
1041        #[test]
1042        fn test_single_qubit_targets_correct_bit() {
1043            let mut state = vec![Complex64::new(0.0, 0.0); 4];
1044            state[0] = Complex64::new(1.0, 0.0);
1045            let x = PauliX { target: QubitId(1) };
1046            apply_gate(&mut state, 2, &x).expect("apply x");
1047            // qubit 1 set → idx 0b10 = 2.
1048            assert!((state[2] - Complex64::new(1.0, 0.0)).norm() < 1e-12);
1049        }
1050    }
1051}
1052
1053impl Default for VQEOptimizer {
1054    fn default() -> Self {
1055        Self::new(VQEOptimizerType::GradientDescent)
1056    }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061    use super::*;
1062
1063    #[test]
1064    fn test_hardware_efficient_ansatz() {
1065        let circuit = VQECircuit::<4>::new(VQEAnsatz::HardwareEfficient { layers: 2 })
1066            .expect("create VQE circuit");
1067        assert!(!circuit.parameters.is_empty());
1068        assert_eq!(circuit.parameter_names.len(), circuit.parameters.len());
1069    }
1070
1071    #[test]
1072    fn test_observable_creation() {
1073        let obs = VQEObservable::heisenberg_model(4, 1.0);
1074        assert!(!obs.terms.is_empty());
1075    }
1076
1077    #[test]
1078    fn test_parameter_management() {
1079        let mut circuit =
1080            VQECircuit::<2>::new(VQEAnsatz::Custom).expect("create custom VQE circuit");
1081        circuit
1082            .add_parameterized_ry(QubitId(0), "theta1")
1083            .expect("add parameterized RY gate");
1084        circuit
1085            .set_parameter("theta1", 0.5)
1086            .expect("set parameter theta1");
1087        assert_eq!(circuit.get_parameter("theta1"), Some(0.5));
1088    }
1089
1090    #[test]
1091    fn test_set_parameters_updates_circuit_gates() {
1092        use std::f64::consts::PI;
1093
1094        // Build a custom VQE circuit with one RY gate
1095        let mut vqe = VQECircuit::<2>::new(VQEAnsatz::Custom).expect("custom VQE");
1096        vqe.add_parameterized_ry(QubitId(0), "theta")
1097            .expect("add RY");
1098        vqe.add_parameterized_rz(QubitId(1), "phi").expect("add RZ");
1099
1100        assert_eq!(vqe.num_parameters(), 2);
1101
1102        // Initially parameters are zero
1103        assert_eq!(vqe.get_parameter("theta"), Some(0.0));
1104        assert_eq!(vqe.get_parameter("phi"), Some(0.0));
1105
1106        // Update both parameters
1107        vqe.set_parameters(&[PI / 4.0, PI / 2.0])
1108            .expect("set params");
1109
1110        // Parameters stored correctly
1111        assert!((vqe.get_parameter("theta").unwrap() - PI / 4.0).abs() < 1e-12);
1112        assert!((vqe.get_parameter("phi").unwrap() - PI / 2.0).abs() < 1e-12);
1113
1114        // Circuit was rebuilt: should still have the same number of gates
1115        assert_eq!(vqe.circuit.gates().len(), 2);
1116
1117        // Verify the gates have the updated angles by inspecting their names
1118        // (RY and RZ gate names)
1119        let gate_names: Vec<&str> = vqe.circuit.gates().iter().map(|g| g.name()).collect();
1120        assert_eq!(gate_names, vec!["RY", "RZ"]);
1121    }
1122
1123    #[test]
1124    fn test_set_parameters_hardware_efficient() {
1125        use std::f64::consts::PI;
1126
1127        let mut vqe = VQECircuit::<2>::new(VQEAnsatz::HardwareEfficient { layers: 1 })
1128            .expect("hardware-efficient VQE");
1129
1130        let n_params = vqe.num_parameters();
1131        assert!(n_params > 0);
1132
1133        // Create a new parameter vector with all PI/3
1134        let new_params: Vec<f64> = vec![PI / 3.0; n_params];
1135        vqe.set_parameters(&new_params).expect("set all params");
1136
1137        // Circuit should be rebuilt with same gate structure
1138        for &p in &vqe.parameters {
1139            assert!((p - PI / 3.0).abs() < 1e-12);
1140        }
1141    }
1142
1143    #[test]
1144    fn test_set_parameters_wrong_length_fails() {
1145        let mut vqe = VQECircuit::<2>::new(VQEAnsatz::Custom).expect("custom VQE");
1146        vqe.add_parameterized_ry(QubitId(0), "theta")
1147            .expect("add RY");
1148
1149        // Providing wrong number of parameters should return an error
1150        let result = vqe.set_parameters(&[0.1, 0.2]);
1151        assert!(result.is_err());
1152    }
1153
1154    /// `⟨0|RY(θ)† Z RY(θ)|0⟩ = cos θ` is a textbook identity.  This pins the
1155    /// real expectation-value engine to an analytic value and would fail for the
1156    /// former hard-coded `-1.0`.
1157    #[test]
1158    fn test_evaluate_energy_matches_analytic_cos() {
1159        use std::f64::consts::PI;
1160
1161        let optimizer = VQEOptimizer::default();
1162        let mut z_observable = VQEObservable::new();
1163        z_observable.add_pauli_term(1.0, vec![(0, PauliOperator::Z)]);
1164
1165        for &theta in &[0.0, PI / 6.0, PI / 3.0, PI / 2.0, 2.0 * PI / 3.0, PI] {
1166            let mut vqe = VQECircuit::<1>::new(VQEAnsatz::Custom).expect("custom VQE");
1167            vqe.add_parameterized_ry(QubitId(0), "theta").expect("RY");
1168            vqe.set_parameters(&[theta]).expect("set theta");
1169
1170            let energy = optimizer
1171                .evaluate_energy(&vqe, &z_observable)
1172                .expect("evaluate energy");
1173            assert!(
1174                (energy - theta.cos()).abs() < 1e-9,
1175                "⟨Z⟩ for RY({theta}) was {energy}, expected {}",
1176                theta.cos()
1177            );
1178        }
1179    }
1180
1181    /// The energy must depend on the parameters: a constant-`-1.0` fabrication
1182    /// would make every value identical.
1183    #[test]
1184    fn test_evaluate_energy_is_not_constant() {
1185        use std::f64::consts::PI;
1186
1187        let optimizer = VQEOptimizer::default();
1188        let mut obs = VQEObservable::new();
1189        obs.add_pauli_term(1.0, vec![(0, PauliOperator::Z)]);
1190
1191        let mut vqe = VQECircuit::<1>::new(VQEAnsatz::Custom).expect("custom VQE");
1192        vqe.add_parameterized_ry(QubitId(0), "theta").expect("RY");
1193
1194        vqe.set_parameters(&[0.0]).expect("set");
1195        let e0 = optimizer.evaluate_energy(&vqe, &obs).expect("e0");
1196        vqe.set_parameters(&[PI]).expect("set");
1197        let e_pi = optimizer.evaluate_energy(&vqe, &obs).expect("e_pi");
1198
1199        assert!((e0 - 1.0).abs() < 1e-9, "⟨Z⟩ at θ=0 should be +1, got {e0}");
1200        assert!(
1201            (e_pi + 1.0).abs() < 1e-9,
1202            "⟨Z⟩ at θ=π should be -1, got {e_pi}"
1203        );
1204        assert!((e0 - e_pi).abs() > 1.0, "energy must vary with parameters");
1205    }
1206
1207    /// X expectation of `RY(θ)|0⟩` is `sin θ` — exercises a non-diagonal Pauli.
1208    #[test]
1209    fn test_evaluate_energy_pauli_x() {
1210        use std::f64::consts::PI;
1211
1212        let optimizer = VQEOptimizer::default();
1213        let mut obs = VQEObservable::new();
1214        obs.add_pauli_term(1.0, vec![(0, PauliOperator::X)]);
1215
1216        let mut vqe = VQECircuit::<1>::new(VQEAnsatz::Custom).expect("custom VQE");
1217        vqe.add_parameterized_ry(QubitId(0), "theta").expect("RY");
1218        vqe.set_parameters(&[PI / 2.0]).expect("set");
1219
1220        let energy = optimizer.evaluate_energy(&vqe, &obs).expect("energy");
1221        assert!(
1222            (energy - 1.0).abs() < 1e-9,
1223            "⟨X⟩ for RY(π/2)|0⟩ should be 1, got {energy}"
1224        );
1225    }
1226
1227    /// The analytic parameter-shift gradient must agree with a central finite
1228    /// difference of the (real) energy.
1229    #[test]
1230    fn test_parameter_shift_gradient_matches_finite_difference() {
1231        use std::f64::consts::PI;
1232
1233        let optimizer = VQEOptimizer::default();
1234        // A non-trivial 2-qubit Hamiltonian with several Pauli terms.
1235        let mut obs = VQEObservable::new();
1236        obs.add_pauli_term(0.7, vec![(0, PauliOperator::Z)]);
1237        obs.add_pauli_term(-0.4, vec![(1, PauliOperator::X)]);
1238        obs.add_pauli_term(0.55, vec![(0, PauliOperator::Z), (1, PauliOperator::Z)]);
1239
1240        let mut vqe = VQECircuit::<2>::new(VQEAnsatz::HardwareEfficient { layers: 1 })
1241            .expect("hardware-efficient VQE");
1242        let n = vqe.num_parameters();
1243
1244        // Use a generic, non-symmetric parameter point.
1245        let base: Vec<f64> = (0..n)
1246            .map(|i| 0.13 + 0.21 * (i as f64) - PI / 5.0)
1247            .collect();
1248        vqe.set_parameters(&base).expect("set base params");
1249
1250        let analytic = optimizer
1251            .compute_gradients(&vqe, &obs)
1252            .expect("analytic gradient");
1253
1254        let eps = 1e-6;
1255        for i in 0..n {
1256            let mut plus = base.clone();
1257            plus[i] += eps;
1258            vqe.set_parameters(&plus).expect("set+");
1259            let ep = optimizer.evaluate_energy(&vqe, &obs).expect("e+");
1260
1261            let mut minus = base.clone();
1262            minus[i] -= eps;
1263            vqe.set_parameters(&minus).expect("set-");
1264            let em = optimizer.evaluate_energy(&vqe, &obs).expect("e-");
1265
1266            let numeric = (ep - em) / (2.0 * eps);
1267            assert!(
1268                (analytic[i] - numeric).abs() < 1e-5,
1269                "param {i}: analytic {} vs finite-difference {}",
1270                analytic[i],
1271                numeric
1272            );
1273        }
1274    }
1275
1276    /// End-to-end: optimizing the single-qubit Hamiltonian `H = Z` with an RY
1277    /// ansatz must drive the energy toward the true ground-state energy `-1`.
1278    #[test]
1279    fn test_optimize_reaches_z_ground_state() {
1280        let optimizer = VQEOptimizer {
1281            learning_rate: 0.3,
1282            max_iterations: 500,
1283            ..VQEOptimizer::default()
1284        };
1285
1286        let mut obs = VQEObservable::new();
1287        obs.add_pauli_term(1.0, vec![(0, PauliOperator::Z)]);
1288
1289        let mut vqe = VQECircuit::<1>::new(VQEAnsatz::Custom).expect("custom VQE");
1290        vqe.add_parameterized_ry(QubitId(0), "theta").expect("RY");
1291        // Start away from both the minimum (θ=π) and the maximum (θ=0).
1292        vqe.set_parameters(&[0.6]).expect("init");
1293
1294        let result = optimizer.optimize(&mut vqe, &obs).expect("optimize");
1295        assert!(
1296            (result.ground_state_energy + 1.0).abs() < 1e-3,
1297            "optimized energy {} should approach -1",
1298            result.ground_state_energy
1299        );
1300    }
1301}