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 current_energy = self.evaluate_energy(circuit, observable)?;
653        let mut best_parameters = circuit.parameters.clone();
654        let mut best_energy = current_energy;
655
656        for iteration in 0..self.max_iterations {
657            // Simplified gradient descent step
658            let gradients = self.compute_gradients(circuit, observable)?;
659
660            // Update parameters
661            for (i, gradient) in gradients.iter().enumerate() {
662                circuit.parameters[i] -= self.learning_rate * gradient;
663            }
664
665            // Evaluate new energy
666            current_energy = self.evaluate_energy(circuit, observable)?;
667
668            if current_energy < best_energy {
669                best_energy = current_energy;
670                best_parameters.clone_from(&circuit.parameters);
671            }
672
673            // Check convergence
674            let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
675            if gradient_norm < self.tolerance {
676                circuit.parameters = best_parameters;
677                return Ok(VQEResult {
678                    optimal_parameters: circuit.parameters.clone(),
679                    ground_state_energy: best_energy,
680                    iterations: iteration + 1,
681                    converged: true,
682                    gradient_norm,
683                });
684            }
685        }
686
687        circuit.parameters = best_parameters;
688        Ok(VQEResult {
689            optimal_parameters: circuit.parameters.clone(),
690            ground_state_energy: best_energy,
691            iterations: self.max_iterations,
692            converged: false,
693            gradient_norm: 0.0, // Would compute actual gradient norm
694        })
695    }
696
697    /// Evaluate the energy expectation value (simplified)
698    const fn evaluate_energy<const N: usize>(
699        &self,
700        _circuit: &VQECircuit<N>,
701        _observable: &VQEObservable,
702    ) -> QuantRS2Result<f64> {
703        // This is a placeholder - real implementation would:
704        // 1. Execute the circuit on a quantum simulator/device
705        // 2. Measure expectation values of Pauli strings
706        // 3. Combine measurements according to observable coefficients
707
708        // For now, return a dummy energy value
709        Ok(-1.0)
710    }
711
712    /// Compute parameter gradients (simplified)
713    fn compute_gradients<const N: usize>(
714        &self,
715        circuit: &VQECircuit<N>,
716        _observable: &VQEObservable,
717    ) -> QuantRS2Result<Vec<f64>> {
718        // This is a placeholder - real implementation would use:
719        // 1. Parameter shift rule for analytic gradients
720        // 2. Finite differences for numerical gradients
721        // 3. Or other gradient estimation methods
722
723        // For now, return dummy gradients
724        Ok(vec![0.001; circuit.parameters.len()])
725    }
726}
727
728impl Default for VQEOptimizer {
729    fn default() -> Self {
730        Self::new(VQEOptimizerType::GradientDescent)
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737
738    #[test]
739    fn test_hardware_efficient_ansatz() {
740        let circuit = VQECircuit::<4>::new(VQEAnsatz::HardwareEfficient { layers: 2 })
741            .expect("create VQE circuit");
742        assert!(!circuit.parameters.is_empty());
743        assert_eq!(circuit.parameter_names.len(), circuit.parameters.len());
744    }
745
746    #[test]
747    fn test_observable_creation() {
748        let obs = VQEObservable::heisenberg_model(4, 1.0);
749        assert!(!obs.terms.is_empty());
750    }
751
752    #[test]
753    fn test_parameter_management() {
754        let mut circuit =
755            VQECircuit::<2>::new(VQEAnsatz::Custom).expect("create custom VQE circuit");
756        circuit
757            .add_parameterized_ry(QubitId(0), "theta1")
758            .expect("add parameterized RY gate");
759        circuit
760            .set_parameter("theta1", 0.5)
761            .expect("set parameter theta1");
762        assert_eq!(circuit.get_parameter("theta1"), Some(0.5));
763    }
764
765    #[test]
766    fn test_set_parameters_updates_circuit_gates() {
767        use std::f64::consts::PI;
768
769        // Build a custom VQE circuit with one RY gate
770        let mut vqe = VQECircuit::<2>::new(VQEAnsatz::Custom).expect("custom VQE");
771        vqe.add_parameterized_ry(QubitId(0), "theta")
772            .expect("add RY");
773        vqe.add_parameterized_rz(QubitId(1), "phi").expect("add RZ");
774
775        assert_eq!(vqe.num_parameters(), 2);
776
777        // Initially parameters are zero
778        assert_eq!(vqe.get_parameter("theta"), Some(0.0));
779        assert_eq!(vqe.get_parameter("phi"), Some(0.0));
780
781        // Update both parameters
782        vqe.set_parameters(&[PI / 4.0, PI / 2.0])
783            .expect("set params");
784
785        // Parameters stored correctly
786        assert!((vqe.get_parameter("theta").unwrap() - PI / 4.0).abs() < 1e-12);
787        assert!((vqe.get_parameter("phi").unwrap() - PI / 2.0).abs() < 1e-12);
788
789        // Circuit was rebuilt: should still have the same number of gates
790        assert_eq!(vqe.circuit.gates().len(), 2);
791
792        // Verify the gates have the updated angles by inspecting their names
793        // (RY and RZ gate names)
794        let gate_names: Vec<&str> = vqe.circuit.gates().iter().map(|g| g.name()).collect();
795        assert_eq!(gate_names, vec!["RY", "RZ"]);
796    }
797
798    #[test]
799    fn test_set_parameters_hardware_efficient() {
800        use std::f64::consts::PI;
801
802        let mut vqe = VQECircuit::<2>::new(VQEAnsatz::HardwareEfficient { layers: 1 })
803            .expect("hardware-efficient VQE");
804
805        let n_params = vqe.num_parameters();
806        assert!(n_params > 0);
807
808        // Create a new parameter vector with all PI/3
809        let new_params: Vec<f64> = vec![PI / 3.0; n_params];
810        vqe.set_parameters(&new_params).expect("set all params");
811
812        // Circuit should be rebuilt with same gate structure
813        for &p in &vqe.parameters {
814            assert!((p - PI / 3.0).abs() < 1e-12);
815        }
816    }
817
818    #[test]
819    fn test_set_parameters_wrong_length_fails() {
820        let mut vqe = VQECircuit::<2>::new(VQEAnsatz::Custom).expect("custom VQE");
821        vqe.add_parameterized_ry(QubitId(0), "theta")
822            .expect("add RY");
823
824        // Providing wrong number of parameters should return an error
825        let result = vqe.set_parameters(&[0.1, 0.2]);
826        assert!(result.is_err());
827    }
828}