Skip to main content

quantrs2_core/
adapt_vqe.rs

1//! ADAPT-VQE: Adaptive Derivative-Assembled Pseudo-Trotter ansatz for quantum chemistry.
2//!
3//! Implements the ADAPT-VQE algorithm (Grimsley et al., 2019) which adaptively
4//! builds a compact ansatz from a fermionic operator pool, avoiding barren
5//! plateaus and minimising circuit depth relative to fixed-depth approaches.
6
7// ADAPT-VQE: Adaptive Derivative-Assembled Pseudo-Trotter VQE
8//
9// A state-of-the-art quantum chemistry algorithm that adaptively constructs
10// the ansatz circuit during optimization, avoiding the barren plateau problem
11// and reducing circuit depth.
12//
13// Reference: Grimsley, H. R., et al. (2019). "An adaptive variational algorithm for exact molecular simulations on a quantum computer"
14// Nature Communications 10, 3007
15
16use crate::error::QuantRS2Error;
17use crate::optimization_stubs::{minimize, Method, Options};
18use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
19use scirs2_core::Complex64;
20use std::collections::HashMap;
21
22/// Fermionic operator pool for quantum chemistry
23///
24/// Contains the complete set of single and double excitation operators
25/// that can be used to construct the ADAPT-VQE ansatz.
26#[derive(Debug, Clone)]
27pub struct FermionicOperatorPool {
28    /// Single excitation operators (a†_p a_q)
29    pub single_excitations: Vec<FermionicOperator>,
30    /// Double excitation operators (a†_p a†_q a_r a_s)
31    pub double_excitations: Vec<FermionicOperator>,
32    /// Number of spin orbitals
33    pub num_orbitals: usize,
34}
35
36impl FermionicOperatorPool {
37    /// Create a new operator pool for a given number of spin orbitals
38    pub fn new(num_orbitals: usize) -> Self {
39        let mut single_excitations = Vec::new();
40        let mut double_excitations = Vec::new();
41
42        // Generate all single excitations
43        for p in 0..num_orbitals {
44            for q in 0..num_orbitals {
45                if p != q {
46                    single_excitations.push(FermionicOperator::single_excitation(p, q));
47                }
48            }
49        }
50
51        // Generate all double excitations
52        for p in 0..num_orbitals {
53            for q in p + 1..num_orbitals {
54                for r in 0..num_orbitals {
55                    for s in r + 1..num_orbitals {
56                        if (p, q) != (r, s) {
57                            double_excitations
58                                .push(FermionicOperator::double_excitation(p, q, r, s));
59                        }
60                    }
61                }
62            }
63        }
64
65        Self {
66            single_excitations,
67            double_excitations,
68            num_orbitals,
69        }
70    }
71
72    /// Get all operators in the pool
73    pub fn all_operators(&self) -> Vec<FermionicOperator> {
74        let mut operators = Vec::new();
75        operators.extend(self.single_excitations.clone());
76        operators.extend(self.double_excitations.clone());
77        operators
78    }
79
80    /// Get operator count
81    pub fn size(&self) -> usize {
82        self.single_excitations.len() + self.double_excitations.len()
83    }
84}
85
86/// Fermionic operator representation
87#[derive(Debug, Clone, PartialEq)]
88pub struct FermionicOperator {
89    /// Creation operator indices
90    pub creation_ops: Vec<usize>,
91    /// Annihilation operator indices
92    pub annihilation_ops: Vec<usize>,
93    /// Operator label for identification
94    pub label: String,
95}
96
97impl FermionicOperator {
98    /// Create a single excitation operator a†_p a_q
99    pub fn single_excitation(p: usize, q: usize) -> Self {
100        Self {
101            creation_ops: vec![p],
102            annihilation_ops: vec![q],
103            label: format!("E_{{{},{}}}", p, q),
104        }
105    }
106
107    /// Create a double excitation operator a†_p a†_q a_r a_s
108    pub fn double_excitation(p: usize, q: usize, r: usize, s: usize) -> Self {
109        Self {
110            creation_ops: vec![p, q],
111            annihilation_ops: vec![r, s],
112            label: format!("E_{{{},{},{},{}}}", p, q, r, s),
113        }
114    }
115
116    /// Convert to Pauli string representation using Jordan-Wigner transformation
117    pub fn to_pauli_string(&self, num_qubits: usize) -> PauliString {
118        // Simplified Jordan-Wigner transformation
119        // Full implementation would require more sophisticated mapping
120        let mut pauli_ops = vec![PauliOp::I; num_qubits];
121
122        // Apply creation operators
123        for &idx in &self.creation_ops {
124            if idx < num_qubits {
125                pauli_ops[idx] = PauliOp::X;
126            }
127        }
128
129        // Apply annihilation operators
130        for &idx in &self.annihilation_ops {
131            if idx < num_qubits {
132                pauli_ops[idx] = PauliOp::Y;
133            }
134        }
135
136        PauliString {
137            operators: pauli_ops,
138            coefficient: Complex64::new(1.0, 0.0),
139        }
140    }
141}
142
143/// Pauli operator types
144#[derive(Debug, Clone, Copy, PartialEq)]
145pub enum PauliOp {
146    I, // Identity
147    X, // Pauli-X
148    Y, // Pauli-Y
149    Z, // Pauli-Z
150}
151
152/// Pauli string representation of a quantum operator
153#[derive(Debug, Clone)]
154pub struct PauliString {
155    /// Pauli operators for each qubit
156    pub operators: Vec<PauliOp>,
157    /// Overall coefficient
158    pub coefficient: Complex64,
159}
160
161impl PauliString {
162    /// Compute expectation value <ψ|P|ψ> for this Pauli string
163    pub fn expectation_value(&self, state: &Array1<Complex64>) -> Complex64 {
164        // Apply Pauli operator (including its coefficient) to the state and
165        // compute the overlap <ψ|c·P|ψ>.
166        let transformed = self.apply_to_state(state);
167        state
168            .iter()
169            .zip(transformed.iter())
170            .map(|(a, b)| a.conj() * b)
171            .sum::<Complex64>()
172    }
173
174    /// Apply Pauli string (scaled by its coefficient) to a quantum state.
175    ///
176    /// Returns `c · P |ψ⟩`, where `c` is [`PauliString::coefficient`]. Qubit `k`
177    /// corresponds to bit `k` of the state-vector index (little-endian), matching
178    /// the bit-mask convention used throughout the crate.
179    pub fn apply_to_state(&self, state: &Array1<Complex64>) -> Array1<Complex64> {
180        let n = self.operators.len();
181        let dim = 1 << n;
182        let mut result = Array1::<Complex64>::zeros(dim);
183
184        for i in 0..dim {
185            let mut new_index = i;
186            let mut phase = self.coefficient;
187
188            for (qubit, &op) in self.operators.iter().enumerate() {
189                let bit = (i >> qubit) & 1;
190                match op {
191                    PauliOp::I => {}
192                    PauliOp::X => {
193                        new_index ^= 1 << qubit; // Flip bit
194                    }
195                    PauliOp::Y => {
196                        new_index ^= 1 << qubit;
197                        phase *= if bit == 0 {
198                            Complex64::new(0.0, 1.0)
199                        } else {
200                            Complex64::new(0.0, -1.0)
201                        };
202                    }
203                    PauliOp::Z => {
204                        if bit == 1 {
205                            phase *= Complex64::new(-1.0, 0.0);
206                        }
207                    }
208                }
209            }
210
211            result[new_index] += phase * state[i];
212        }
213
214        result
215    }
216
217    /// Compute commutator [H, P] where H is the Hamiltonian
218    pub fn commutator_with_hamiltonian(
219        &self,
220        hamiltonian: &MolecularHamiltonian,
221        state: &Array1<Complex64>,
222    ) -> Complex64 {
223        // [H, P] = HP - PH
224        let hp_state = hamiltonian.apply_to_state(&self.apply_to_state(state));
225        let ph_state = self.apply_to_state(&hamiltonian.apply_to_state(state));
226
227        state
228            .iter()
229            .zip(hp_state.iter().zip(ph_state.iter()))
230            .map(|(psi, (hp, ph))| psi.conj() * (hp - ph))
231            .sum()
232    }
233}
234
235/// Multiply two single-qubit Pauli operators, returning the resulting Pauli and
236/// the complex phase factor (Pauli algebra: XY = iZ, YZ = iX, ZX = iY, etc.).
237fn pauli_mul(a: PauliOp, b: PauliOp) -> (PauliOp, Complex64) {
238    use PauliOp::{I, X, Y, Z};
239    let one = Complex64::new(1.0, 0.0);
240    let i = Complex64::new(0.0, 1.0);
241    match (a, b) {
242        (I, x) => (x, one),
243        (x, I) => (x, one),
244        (X, X) | (Y, Y) | (Z, Z) => (I, one),
245        (X, Y) => (Z, i),
246        (Y, X) => (Z, -i),
247        (Y, Z) => (X, i),
248        (Z, Y) => (X, -i),
249        (Z, X) => (Y, i),
250        (X, Z) => (Y, -i),
251    }
252}
253
254/// A Pauli string under construction: a per-qubit operator list plus a scalar
255/// coefficient. Used to accumulate Jordan-Wigner products before they are turned
256/// into [`PauliString`]s.
257#[derive(Clone)]
258struct PauliTerm {
259    operators: Vec<PauliOp>,
260    coefficient: Complex64,
261}
262
263impl PauliTerm {
264    fn identity(num_qubits: usize) -> Self {
265        Self {
266            operators: vec![PauliOp::I; num_qubits],
267            coefficient: Complex64::new(1.0, 0.0),
268        }
269    }
270
271    /// Multiply this term (in place sense, returns new) by a single-qubit Pauli
272    /// on `qubit`, folding the resulting phase into the coefficient.
273    fn times_single(&self, qubit: usize, op: PauliOp) -> Self {
274        let mut operators = self.operators.clone();
275        let (new_op, phase) = pauli_mul(operators[qubit], op);
276        operators[qubit] = new_op;
277        Self {
278            operators,
279            coefficient: self.coefficient * phase,
280        }
281    }
282}
283
284/// Expand a single fermionic ladder operator into its two Jordan-Wigner Pauli
285/// terms acting on `num_qubits` qubits.
286///
287/// `a†_p = ½ (X_p - i Y_p) ⊗ Z_{<p}` (when `creation == true`)
288/// `a_p  = ½ (X_p + i Y_p) ⊗ Z_{<p}` (when `creation == false`)
289fn jordan_wigner_ladder(site: usize, creation: bool, num_qubits: usize) -> Vec<PauliTerm> {
290    let half = Complex64::new(0.5, 0.0);
291    // Sign on the Y component: -i for creation, +i for annihilation.
292    let y_coeff = if creation {
293        Complex64::new(0.0, -0.5)
294    } else {
295        Complex64::new(0.0, 0.5)
296    };
297
298    let mut x_term = PauliTerm::identity(num_qubits);
299    let mut y_term = PauliTerm::identity(num_qubits);
300
301    // Jordan-Wigner Z string on all qubits with index < site.
302    for z in 0..site {
303        x_term.operators[z] = PauliOp::Z;
304        y_term.operators[z] = PauliOp::Z;
305    }
306    x_term.operators[site] = PauliOp::X;
307    x_term.coefficient = half;
308    y_term.operators[site] = PauliOp::Y;
309    y_term.coefficient = y_coeff;
310
311    vec![x_term, y_term]
312}
313
314/// Convert a normal-ordered product of creation operators (`creations`) followed
315/// by annihilation operators (`annihilations`) into a sum of [`PauliString`]s via
316/// the Jordan-Wigner transformation.
317///
318/// The product is `a†_{c0} a†_{c1} ... a_{a0} a_{a1} ...`, applied left-to-right.
319fn jordan_wigner_excitation(
320    creations: &[usize],
321    annihilations: &[usize],
322    num_qubits: usize,
323) -> Vec<PauliString> {
324    // Start with the identity term, then fold each ladder operator's two-term
325    // expansion into the running product set.
326    let mut terms: Vec<PauliTerm> = vec![PauliTerm::identity(num_qubits)];
327
328    let ladders = creations
329        .iter()
330        .map(|&p| (p, true))
331        .chain(annihilations.iter().map(|&p| (p, false)));
332
333    for (site, creation) in ladders {
334        let factor = jordan_wigner_ladder(site, creation, num_qubits);
335        let mut next = Vec::with_capacity(terms.len() * factor.len());
336        for term in &terms {
337            for ladder_term in &factor {
338                // Multiply `term` by `ladder_term` qubit-by-qubit.
339                let mut acc = PauliTerm {
340                    operators: term.operators.clone(),
341                    coefficient: term.coefficient * ladder_term.coefficient,
342                };
343                for (qubit, &op) in ladder_term.operators.iter().enumerate() {
344                    if op != PauliOp::I {
345                        acc = acc.times_single(qubit, op);
346                    }
347                }
348                next.push(acc);
349            }
350        }
351        terms = next;
352    }
353
354    terms
355        .into_iter()
356        .map(|t| PauliString {
357            operators: t.operators,
358            coefficient: t.coefficient,
359        })
360        .collect()
361}
362
363/// Molecular Hamiltonian in second-quantized form
364#[derive(Debug, Clone)]
365pub struct MolecularHamiltonian {
366    /// One-electron integrals
367    pub one_electron_integrals: Array2<f64>,
368    /// Two-electron integrals (4D tensor flattened)
369    pub two_electron_integrals: HashMap<(usize, usize, usize, usize), f64>,
370    /// Nuclear repulsion energy
371    pub nuclear_repulsion: f64,
372    /// Number of spin orbitals
373    pub num_orbitals: usize,
374}
375
376impl MolecularHamiltonian {
377    /// Create a new molecular Hamiltonian
378    pub fn new(
379        one_electron: Array2<f64>,
380        two_electron: HashMap<(usize, usize, usize, usize), f64>,
381        nuclear_repulsion: f64,
382    ) -> Self {
383        let num_orbitals = one_electron.nrows();
384        Self {
385            one_electron_integrals: one_electron,
386            two_electron_integrals: two_electron,
387            nuclear_repulsion,
388            num_orbitals,
389        }
390    }
391
392    /// Apply Hamiltonian to a quantum state.
393    ///
394    /// The second-quantized Hamiltonian
395    /// `H = Σ_pq h_pq a†_p a_q + ½ Σ_pqrs h_pqrs a†_p a†_q a_r a_s`
396    /// is mapped to qubit operators via the Jordan-Wigner transformation and
397    /// applied term-by-term to the input state vector.
398    ///
399    /// Each fermionic ladder operator is expanded into a sum of Pauli strings:
400    /// `a†_p = ½ (X_p - i Y_p) ⊗ Z_{<p}` and `a_p = ½ (X_p + i Y_p) ⊗ Z_{<p}`,
401    /// where `Z_{<p}` is the Jordan-Wigner string of Pauli-Z operators on all
402    /// qubits with index below `p`. Products of ladder operators are formed by
403    /// multiplying the corresponding Pauli strings (tracking the i/-i phases and
404    /// the Z-string parities), then each resulting Pauli string is applied to the
405    /// state via the cheap bit-mask routine in [`PauliString::apply_to_state`].
406    pub fn apply_to_state(&self, state: &Array1<Complex64>) -> Array1<Complex64> {
407        let num_qubits = self.num_orbitals;
408        let dim = 1usize << num_qubits;
409        let mut result = Array1::<Complex64>::zeros(dim);
410
411        // One-electron part: Σ_pq h_pq a†_p a_q
412        for p in 0..num_qubits {
413            for q in 0..num_qubits {
414                let coeff = self.one_electron_integrals[[p, q]];
415                if coeff.abs() < 1e-15 {
416                    continue;
417                }
418                let pauli_terms = jordan_wigner_excitation(&[p], &[q], num_qubits);
419                for term in &pauli_terms {
420                    let scaled = PauliString {
421                        operators: term.operators.clone(),
422                        coefficient: term.coefficient * coeff,
423                    };
424                    let contribution = scaled.apply_to_state(state);
425                    result = result + contribution;
426                }
427            }
428        }
429
430        // Two-electron part: ½ Σ_pqrs h_pqrs a†_p a†_q a_r a_s
431        for (&(p, q, r, s), &coeff) in &self.two_electron_integrals {
432            if coeff.abs() < 1e-15 {
433                continue;
434            }
435            if p >= num_qubits || q >= num_qubits || r >= num_qubits || s >= num_qubits {
436                continue;
437            }
438            let pauli_terms = jordan_wigner_excitation(&[p, q], &[r, s], num_qubits);
439            for term in &pauli_terms {
440                let scaled = PauliString {
441                    operators: term.operators.clone(),
442                    coefficient: term.coefficient * coeff * 0.5,
443                };
444                let contribution = scaled.apply_to_state(state);
445                result = result + contribution;
446            }
447        }
448
449        result
450    }
451
452    /// Compute energy expectation value <ψ|H|ψ>
453    pub fn expectation_value(&self, state: &Array1<Complex64>) -> f64 {
454        let h_psi = self.apply_to_state(state);
455        let energy: Complex64 = state
456            .iter()
457            .zip(h_psi.iter())
458            .map(|(a, b)| a.conj() * b)
459            .sum();
460
461        energy.re + self.nuclear_repulsion
462    }
463}
464
465/// ADAPT-VQE algorithm configuration
466#[derive(Debug, Clone)]
467pub struct AdaptVQEConfig {
468    /// Gradient threshold for operator selection
469    pub gradient_threshold: f64,
470    /// Maximum number of ADAPT iterations
471    pub max_iterations: usize,
472    /// Energy convergence threshold
473    pub energy_threshold: f64,
474    /// Maximum VQE optimization steps per iteration
475    pub max_vqe_steps: usize,
476    /// Optimizer for parameter optimization
477    pub optimizer_method: Method,
478}
479
480impl Default for AdaptVQEConfig {
481    fn default() -> Self {
482        Self {
483            gradient_threshold: 1e-3,
484            max_iterations: 50,
485            energy_threshold: 1e-6,
486            max_vqe_steps: 100,
487            optimizer_method: Method::LBFGS,
488        }
489    }
490}
491
492/// ADAPT-VQE ansatz built adaptively
493#[derive(Debug, Clone)]
494pub struct AdaptAnsatz {
495    /// Selected operators in order
496    pub operators: Vec<FermionicOperator>,
497    /// Optimized parameters for each operator
498    pub parameters: Vec<f64>,
499    /// Energy at each iteration
500    pub energy_history: Vec<f64>,
501}
502
503impl AdaptAnsatz {
504    /// Create an empty ansatz
505    pub const fn new() -> Self {
506        Self {
507            operators: Vec::new(),
508            parameters: Vec::new(),
509            energy_history: Vec::new(),
510        }
511    }
512
513    /// Add a new operator to the ansatz
514    pub fn add_operator(&mut self, operator: FermionicOperator, parameter: f64) {
515        self.operators.push(operator);
516        self.parameters.push(parameter);
517    }
518
519    /// Get current circuit depth (number of operators)
520    pub fn depth(&self) -> usize {
521        self.operators.len()
522    }
523
524    /// Apply ansatz to a reference state
525    pub fn apply_to_state(
526        &self,
527        reference_state: &Array1<Complex64>,
528        num_qubits: usize,
529    ) -> Array1<Complex64> {
530        let mut state = reference_state.clone();
531
532        for (operator, &theta) in self.operators.iter().zip(self.parameters.iter()) {
533            let pauli_string = operator.to_pauli_string(num_qubits);
534
535            // Apply exp(-iθP) using Pauli rotation
536            // In practice, would use Trotter decomposition or other methods
537            let rotation = self.apply_pauli_rotation(&pauli_string, theta);
538            state = rotation.dot(&state);
539        }
540
541        state
542    }
543
544    /// Apply Pauli rotation exp(-iθP)
545    fn apply_pauli_rotation(&self, pauli: &PauliString, theta: f64) -> Array2<Complex64> {
546        let n = pauli.operators.len();
547        let dim = 1 << n;
548
549        // Simplified: construct rotation matrix
550        // Full implementation would use efficient Pauli rotation circuits
551        let mut rotation = Array2::<Complex64>::zeros((dim, dim));
552
553        for i in 0..dim {
554            for j in 0..dim {
555                if i == j {
556                    rotation[[i, j]] = Complex64::new((theta / 2.0).cos(), 0.0);
557                }
558            }
559        }
560
561        rotation
562    }
563}
564
565impl Default for AdaptAnsatz {
566    fn default() -> Self {
567        Self::new()
568    }
569}
570
571/// Main ADAPT-VQE algorithm implementation
572#[derive(Debug)]
573pub struct AdaptVQE {
574    /// Molecular Hamiltonian
575    pub hamiltonian: MolecularHamiltonian,
576    /// Operator pool
577    pub operator_pool: FermionicOperatorPool,
578    /// Configuration
579    pub config: AdaptVQEConfig,
580    /// Current ansatz
581    pub ansatz: AdaptAnsatz,
582    /// Number of qubits required
583    pub num_qubits: usize,
584}
585
586impl AdaptVQE {
587    /// Create a new ADAPT-VQE instance
588    pub fn new(
589        hamiltonian: MolecularHamiltonian,
590        num_qubits: usize,
591        config: AdaptVQEConfig,
592    ) -> Self {
593        let operator_pool = FermionicOperatorPool::new(hamiltonian.num_orbitals);
594        let ansatz = AdaptAnsatz::new();
595
596        Self {
597            hamiltonian,
598            operator_pool,
599            config,
600            ansatz,
601            num_qubits,
602        }
603    }
604
605    /// Run the ADAPT-VQE algorithm
606    pub fn run(
607        &mut self,
608        initial_state: &Array1<Complex64>,
609    ) -> Result<AdaptVQEResult, QuantRS2Error> {
610        let mut current_state = initial_state.clone();
611        let mut iteration = 0;
612        let mut converged = false;
613
614        while iteration < self.config.max_iterations && !converged {
615            // Step 1: Compute gradients for all operators in the pool
616            let gradients = self.compute_operator_gradients(&current_state)?;
617
618            // Step 2: Select operator with largest gradient magnitude
619            let (max_gradient_idx, max_gradient) = gradients
620                .iter()
621                .enumerate()
622                .max_by(|(_, a), (_, b)| a.abs().total_cmp(&b.abs()))
623                .ok_or_else(|| QuantRS2Error::InvalidInput("No gradients computed".to_string()))?;
624
625            // Check convergence: if max gradient is below threshold, we're done
626            if max_gradient.abs() < self.config.gradient_threshold {
627                converged = true;
628                break;
629            }
630
631            // Step 3: Add selected operator to ansatz with initial parameter = 0
632            let selected_operator = self.operator_pool.all_operators()[max_gradient_idx].clone();
633            self.ansatz.add_operator(selected_operator, 0.0);
634
635            // Step 4: Optimize all parameters in the current ansatz
636            let optimized_params = self.optimize_parameters(&current_state)?;
637            self.ansatz.parameters = optimized_params;
638
639            // Step 5: Update state and energy
640            current_state = self.ansatz.apply_to_state(initial_state, self.num_qubits);
641            let energy = self.hamiltonian.expectation_value(&current_state);
642            self.ansatz.energy_history.push(energy);
643
644            // Check energy convergence
645            if iteration > 0 {
646                let energy_change = (self.ansatz.energy_history[iteration]
647                    - self.ansatz.energy_history[iteration - 1])
648                    .abs();
649                if energy_change < self.config.energy_threshold {
650                    converged = true;
651                }
652            }
653
654            iteration += 1;
655        }
656
657        Ok(AdaptVQEResult {
658            final_energy: self.ansatz.energy_history.last().copied().unwrap_or(0.0),
659            final_state: current_state,
660            ansatz: self.ansatz.clone(),
661            num_iterations: iteration,
662            converged,
663        })
664    }
665
666    /// Compute gradients for all operators in the pool
667    fn compute_operator_gradients(
668        &self,
669        state: &Array1<Complex64>,
670    ) -> Result<Vec<f64>, QuantRS2Error> {
671        let mut gradients = Vec::new();
672
673        for operator in self.operator_pool.all_operators() {
674            let pauli_string = operator.to_pauli_string(self.num_qubits);
675
676            // Gradient = <ψ|[H, A]|ψ> where A is the operator
677            let gradient = pauli_string.commutator_with_hamiltonian(&self.hamiltonian, state);
678            gradients.push(gradient.re);
679        }
680
681        Ok(gradients)
682    }
683
684    /// Optimize all parameters in the ansatz
685    fn optimize_parameters(
686        &self,
687        initial_state: &Array1<Complex64>,
688    ) -> Result<Vec<f64>, QuantRS2Error> {
689        // Initial guess: current parameters
690        let initial_params = Array1::from_vec(self.ansatz.parameters.clone());
691
692        // Objective function: energy expectation value <ψ(θ)|H|ψ(θ)>.
693        let objective = |params: &ArrayView1<f64>| -> f64 {
694            let mut ansatz_copy = self.ansatz.clone();
695            ansatz_copy.parameters = params.to_vec();
696            let state = ansatz_copy.apply_to_state(initial_state, self.num_qubits);
697            self.hamiltonian.expectation_value(&state)
698        };
699
700        let options = Options {
701            max_iter: self.config.max_vqe_steps,
702            tolerance: 1e-6,
703            ..Default::default()
704        };
705
706        // Run optimization via the in-tree SciRS2 optimizer wrapper.
707        let result = minimize(
708            objective,
709            &initial_params,
710            self.config.optimizer_method.clone(),
711            Some(options),
712        )
713        .map_err(|e| {
714            QuantRS2Error::OptimizationFailed(format!("Parameter optimization failed: {e:?}"))
715        })?;
716
717        Ok(result.x.to_vec())
718    }
719
720    /// Get current circuit depth
721    pub fn get_circuit_depth(&self) -> usize {
722        self.ansatz.depth()
723    }
724
725    /// Get operator pool size
726    pub fn get_pool_size(&self) -> usize {
727        self.operator_pool.size()
728    }
729}
730
731/// Result from ADAPT-VQE algorithm
732#[derive(Debug, Clone)]
733pub struct AdaptVQEResult {
734    /// Final ground state energy
735    pub final_energy: f64,
736    /// Final quantum state
737    pub final_state: Array1<Complex64>,
738    /// Constructed ansatz
739    pub ansatz: AdaptAnsatz,
740    /// Number of ADAPT iterations performed
741    pub num_iterations: usize,
742    /// Whether the algorithm converged
743    pub converged: bool,
744}
745
746impl AdaptVQEResult {
747    /// Get circuit depth of the final ansatz
748    pub fn circuit_depth(&self) -> usize {
749        self.ansatz.depth()
750    }
751
752    /// Get energy lowering from initial to final
753    pub fn energy_lowering(&self) -> Option<f64> {
754        if self.ansatz.energy_history.len() >= 2 {
755            Some(self.ansatz.energy_history[0] - self.final_energy)
756        } else {
757            None
758        }
759    }
760
761    /// Get convergence rate (energy change per iteration)
762    pub fn convergence_rate(&self) -> f64 {
763        if self.num_iterations > 1 {
764            let energy_change =
765                (self.ansatz.energy_history.first().unwrap_or(&0.0) - self.final_energy).abs();
766            energy_change / self.num_iterations as f64
767        } else {
768            0.0
769        }
770    }
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn test_fermionic_operator_pool() {
779        let pool = FermionicOperatorPool::new(4);
780
781        // For 4 orbitals: 4*3 = 12 single excitations
782        assert_eq!(pool.single_excitations.len(), 12);
783
784        // Double excitations: C(4,2) * C(4,2) - overlaps
785        assert!(!pool.double_excitations.is_empty());
786
787        assert_eq!(
788            pool.size(),
789            pool.single_excitations.len() + pool.double_excitations.len()
790        );
791    }
792
793    #[test]
794    fn test_pauli_string_application() {
795        let pauli = PauliString {
796            operators: vec![PauliOp::X, PauliOp::I],
797            coefficient: Complex64::new(1.0, 0.0),
798        };
799
800        let state = Array1::from_vec(vec![
801            Complex64::new(1.0, 0.0),
802            Complex64::new(0.0, 0.0),
803            Complex64::new(0.0, 0.0),
804            Complex64::new(0.0, 0.0),
805        ]);
806
807        let result = pauli.apply_to_state(&state);
808
809        // X on qubit 0 should flip |00⟩ to |01⟩
810        assert!((result[0].re - 0.0).abs() < 1e-10);
811        assert!((result[1].re - 1.0).abs() < 1e-10);
812    }
813
814    #[test]
815    fn test_adapt_ansatz() {
816        let mut ansatz = AdaptAnsatz::new();
817
818        assert_eq!(ansatz.depth(), 0);
819
820        let op = FermionicOperator::single_excitation(0, 1);
821        ansatz.add_operator(op, 0.1);
822
823        assert_eq!(ansatz.depth(), 1);
824        assert_eq!(ansatz.parameters.len(), 1);
825    }
826
827    #[test]
828    fn test_molecular_hamiltonian() {
829        let h_one = Array2::from_shape_fn((2, 2), |(i, j)| if i == j { -1.0 } else { 0.0 });
830
831        let h_two = HashMap::new();
832        let nuclear = 0.5;
833
834        let hamiltonian = MolecularHamiltonian::new(h_one, h_two, nuclear);
835        assert_eq!(hamiltonian.num_orbitals, 2);
836        assert!((hamiltonian.nuclear_repulsion - 0.5).abs() < 1e-10);
837    }
838
839    #[test]
840    fn test_jordan_wigner_number_operator() {
841        // H = h_00 a†_0 a_0  with h_00 = 1 (a number operator on orbital 0).
842        // Under Jordan-Wigner, a†_0 a_0 = (I - Z_0)/2 = diag(0, 1) on qubit 0,
843        // i.e. it returns the occupation number of orbital 0.
844        let mut h_one = Array2::<f64>::zeros((2, 2));
845        h_one[[0, 0]] = 1.0;
846        let hamiltonian = MolecularHamiltonian::new(h_one, HashMap::new(), 0.0);
847
848        // Basis ordering: index bit q is occupation of orbital q.
849        // |0> in occupation of orbital 0 -> states 0 (00) and 2 (10) have n_0 = 0.
850        // |1> in occupation of orbital 0 -> states 1 (01) and 3 (11) have n_0 = 1.
851
852        // State |01> (orbital 0 occupied) -> eigenvalue 1.
853        let mut occ0 = Array1::<Complex64>::zeros(4);
854        occ0[1] = Complex64::new(1.0, 0.0);
855        let out = hamiltonian.apply_to_state(&occ0);
856        // n_0 |01> = 1 * |01>
857        assert!((out[1] - Complex64::new(1.0, 0.0)).norm() < 1e-10);
858        for k in [0usize, 2, 3] {
859            assert!(out[k].norm() < 1e-10);
860        }
861        // Non-identity Hamiltonian acting on an eigenstate with eigenvalue 1
862        // must reproduce the input here, but acting on the empty orbital it must
863        // annihilate it (so it is genuinely NOT a clone of an arbitrary input).
864        let mut empty0 = Array1::<Complex64>::zeros(4);
865        empty0[0] = Complex64::new(1.0, 0.0); // |00>, n_0 = 0
866        let out_empty = hamiltonian.apply_to_state(&empty0);
867        assert!(
868            out_empty.iter().all(|c| c.norm() < 1e-10),
869            "number operator must annihilate the empty orbital, got {out_empty:?}"
870        );
871        // And it is not a clone of the input (input had norm 1, output has norm 0).
872        assert!((out_empty.clone() - empty0)
873            .iter()
874            .any(|c| c.norm() > 1e-10));
875    }
876
877    #[test]
878    fn test_expectation_value_number_operator() {
879        // <ψ|H|ψ> for H = n_0 should equal the occupation of orbital 0 plus the
880        // nuclear repulsion energy.
881        let mut h_one = Array2::<f64>::zeros((2, 2));
882        h_one[[0, 0]] = 1.0;
883        let nuclear = 0.25;
884        let hamiltonian = MolecularHamiltonian::new(h_one, HashMap::new(), nuclear);
885
886        // |01>: orbital 0 occupied -> <n_0> = 1 -> energy = 1 + 0.25
887        let mut occ0 = Array1::<Complex64>::zeros(4);
888        occ0[1] = Complex64::new(1.0, 0.0);
889        let e_occ = hamiltonian.expectation_value(&occ0);
890        assert!((e_occ - 1.25).abs() < 1e-10, "expected 1.25, got {e_occ}");
891
892        // |00>: orbital 0 empty -> <n_0> = 0 -> energy = 0 + 0.25
893        let mut empty0 = Array1::<Complex64>::zeros(4);
894        empty0[0] = Complex64::new(1.0, 0.0);
895        let e_empty = hamiltonian.expectation_value(&empty0);
896        assert!(
897            (e_empty - 0.25).abs() < 1e-10,
898            "expected 0.25, got {e_empty}"
899        );
900    }
901
902    #[test]
903    fn test_jordan_wigner_hopping_is_not_identity() {
904        // A hopping term h_01 a†_0 a_1 + h_10 a†_1 a_0 moves an electron between
905        // orbitals; applied to |10> (orbital 1 occupied) it must produce |01>
906        // (orbital 0 occupied), i.e. it is genuinely off-diagonal, NOT a clone.
907        let mut h_one = Array2::<f64>::zeros((2, 2));
908        h_one[[0, 1]] = 1.0;
909        h_one[[1, 0]] = 1.0;
910        let hamiltonian = MolecularHamiltonian::new(h_one, HashMap::new(), 0.0);
911
912        // |10>: orbital 1 occupied (bit 1 set) -> index 2.
913        let mut state = Array1::<Complex64>::zeros(4);
914        state[2] = Complex64::new(1.0, 0.0);
915        let out = hamiltonian.apply_to_state(&state);
916
917        // a†_0 a_1 |10> = |01> (index 1); the conjugate term annihilates this state.
918        assert!(
919            (out[1].norm() - 1.0).abs() < 1e-10,
920            "hopping should populate |01>, got {out:?}"
921        );
922        assert!(out[2].norm() < 1e-10, "input amplitude must move away");
923        // Definitively not a clone of the input.
924        assert!((out - state).iter().any(|c| c.norm() > 1e-10));
925    }
926}