1use 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#[derive(Debug, Clone)]
27pub struct FermionicOperatorPool {
28 pub single_excitations: Vec<FermionicOperator>,
30 pub double_excitations: Vec<FermionicOperator>,
32 pub num_orbitals: usize,
34}
35
36impl FermionicOperatorPool {
37 pub fn new(num_orbitals: usize) -> Self {
39 let mut single_excitations = Vec::new();
40 let mut double_excitations = Vec::new();
41
42 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 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 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 pub fn size(&self) -> usize {
82 self.single_excitations.len() + self.double_excitations.len()
83 }
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub struct FermionicOperator {
89 pub creation_ops: Vec<usize>,
91 pub annihilation_ops: Vec<usize>,
93 pub label: String,
95}
96
97impl FermionicOperator {
98 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 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 pub fn to_pauli_string(&self, num_qubits: usize) -> PauliString {
118 let mut pauli_ops = vec![PauliOp::I; num_qubits];
121
122 for &idx in &self.creation_ops {
124 if idx < num_qubits {
125 pauli_ops[idx] = PauliOp::X;
126 }
127 }
128
129 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#[derive(Debug, Clone, Copy, PartialEq)]
145pub enum PauliOp {
146 I, X, Y, Z, }
151
152#[derive(Debug, Clone)]
154pub struct PauliString {
155 pub operators: Vec<PauliOp>,
157 pub coefficient: Complex64,
159}
160
161impl PauliString {
162 pub fn expectation_value(&self, state: &Array1<Complex64>) -> Complex64 {
164 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 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; }
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 pub fn commutator_with_hamiltonian(
219 &self,
220 hamiltonian: &MolecularHamiltonian,
221 state: &Array1<Complex64>,
222 ) -> Complex64 {
223 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
235fn 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#[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 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
284fn jordan_wigner_ladder(site: usize, creation: bool, num_qubits: usize) -> Vec<PauliTerm> {
290 let half = Complex64::new(0.5, 0.0);
291 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 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
314fn jordan_wigner_excitation(
320 creations: &[usize],
321 annihilations: &[usize],
322 num_qubits: usize,
323) -> Vec<PauliString> {
324 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 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#[derive(Debug, Clone)]
365pub struct MolecularHamiltonian {
366 pub one_electron_integrals: Array2<f64>,
368 pub two_electron_integrals: HashMap<(usize, usize, usize, usize), f64>,
370 pub nuclear_repulsion: f64,
372 pub num_orbitals: usize,
374}
375
376impl MolecularHamiltonian {
377 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 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 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 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 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#[derive(Debug, Clone)]
467pub struct AdaptVQEConfig {
468 pub gradient_threshold: f64,
470 pub max_iterations: usize,
472 pub energy_threshold: f64,
474 pub max_vqe_steps: usize,
476 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#[derive(Debug, Clone)]
494pub struct AdaptAnsatz {
495 pub operators: Vec<FermionicOperator>,
497 pub parameters: Vec<f64>,
499 pub energy_history: Vec<f64>,
501}
502
503impl AdaptAnsatz {
504 pub const fn new() -> Self {
506 Self {
507 operators: Vec::new(),
508 parameters: Vec::new(),
509 energy_history: Vec::new(),
510 }
511 }
512
513 pub fn add_operator(&mut self, operator: FermionicOperator, parameter: f64) {
515 self.operators.push(operator);
516 self.parameters.push(parameter);
517 }
518
519 pub fn depth(&self) -> usize {
521 self.operators.len()
522 }
523
524 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 let rotation = self.apply_pauli_rotation(&pauli_string, theta);
538 state = rotation.dot(&state);
539 }
540
541 state
542 }
543
544 fn apply_pauli_rotation(&self, pauli: &PauliString, theta: f64) -> Array2<Complex64> {
546 let n = pauli.operators.len();
547 let dim = 1 << n;
548
549 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#[derive(Debug)]
573pub struct AdaptVQE {
574 pub hamiltonian: MolecularHamiltonian,
576 pub operator_pool: FermionicOperatorPool,
578 pub config: AdaptVQEConfig,
580 pub ansatz: AdaptAnsatz,
582 pub num_qubits: usize,
584}
585
586impl AdaptVQE {
587 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 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 let gradients = self.compute_operator_gradients(¤t_state)?;
617
618 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 if max_gradient.abs() < self.config.gradient_threshold {
627 converged = true;
628 break;
629 }
630
631 let selected_operator = self.operator_pool.all_operators()[max_gradient_idx].clone();
633 self.ansatz.add_operator(selected_operator, 0.0);
634
635 let optimized_params = self.optimize_parameters(¤t_state)?;
637 self.ansatz.parameters = optimized_params;
638
639 current_state = self.ansatz.apply_to_state(initial_state, self.num_qubits);
641 let energy = self.hamiltonian.expectation_value(¤t_state);
642 self.ansatz.energy_history.push(energy);
643
644 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 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 let gradient = pauli_string.commutator_with_hamiltonian(&self.hamiltonian, state);
678 gradients.push(gradient.re);
679 }
680
681 Ok(gradients)
682 }
683
684 fn optimize_parameters(
686 &self,
687 initial_state: &Array1<Complex64>,
688 ) -> Result<Vec<f64>, QuantRS2Error> {
689 let initial_params = Array1::from_vec(self.ansatz.parameters.clone());
691
692 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 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 pub fn get_circuit_depth(&self) -> usize {
722 self.ansatz.depth()
723 }
724
725 pub fn get_pool_size(&self) -> usize {
727 self.operator_pool.size()
728 }
729}
730
731#[derive(Debug, Clone)]
733pub struct AdaptVQEResult {
734 pub final_energy: f64,
736 pub final_state: Array1<Complex64>,
738 pub ansatz: AdaptAnsatz,
740 pub num_iterations: usize,
742 pub converged: bool,
744}
745
746impl AdaptVQEResult {
747 pub fn circuit_depth(&self) -> usize {
749 self.ansatz.depth()
750 }
751
752 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 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 assert_eq!(pool.single_excitations.len(), 12);
783
784 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 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 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 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 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 let mut empty0 = Array1::<Complex64>::zeros(4);
865 empty0[0] = Complex64::new(1.0, 0.0); 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 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 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 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 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 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 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 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 assert!((out - state).iter().any(|c| c.norm() > 1e-10));
925 }
926}