1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum RotationAxis {
19 Y,
20 Z,
21 X,
22}
23
24#[derive(Debug, Clone)]
27pub struct ParameterizedGateRecord {
28 pub gate_index: usize,
30 pub qubit: QubitId,
32 pub axis: RotationAxis,
34 pub param_index: usize,
36}
37
38#[derive(Debug, Clone)]
45pub struct VQECircuit<const N: usize> {
46 pub circuit: Circuit<N>,
48 pub parameters: Vec<f64>,
50 pub parameter_names: Vec<String>,
52 parameter_map: HashMap<String, usize>,
54 param_gate_records: Vec<ParameterizedGateRecord>,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum VQEAnsatz {
62 HardwareEfficient { layers: usize },
64 UCCSD {
66 occupied_orbitals: usize,
67 virtual_orbitals: usize,
68 },
69 RealSpace { geometry: Vec<(f64, f64, f64)> },
71 Custom,
73}
74
75#[derive(Debug, Clone)]
77pub struct VQEObservable {
78 pub terms: Vec<(f64, Vec<(usize, PauliOperator)>)>,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PauliOperator {
85 I, X, Y, Z, }
90
91#[derive(Debug, Clone)]
93pub struct VQEResult {
94 pub optimal_parameters: Vec<f64>,
96 pub ground_state_energy: f64,
98 pub iterations: usize,
100 pub converged: bool,
102 pub gradient_norm: f64,
104}
105
106impl<const N: usize> VQECircuit<N> {
107 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 }
153 }
154
155 Ok(Self {
156 circuit,
157 parameters,
158 parameter_names,
159 parameter_map,
160 param_gate_records,
161 })
162 }
163
164 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 for qubit in 0..N {
176 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 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 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 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 for i in 0..occupied_orbitals {
238 circuit.x(QubitId(i as u32))?;
239 }
240
241 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 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 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 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 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 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 let record_map: HashMap<usize, &ParameterizedGateRecord> = self
364 .param_gate_records
365 .iter()
366 .map(|r| (r.gate_index, r))
367 .collect();
368
369 let old_gates = self.circuit.gates_as_boxes();
371
372 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 self.circuit = Circuit::<N>::from_gates(new_gates)?;
401
402 Ok(())
403 }
404
405 #[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 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 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 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 #[must_use]
490 pub fn num_parameters(&self) -> usize {
491 self.parameters.len()
492 }
493}
494
495impl VQEObservable {
496 #[must_use]
498 pub const fn new() -> Self {
499 Self { terms: Vec::new() }
500 }
501
502 pub fn add_pauli_term(&mut self, coefficient: f64, pauli_string: Vec<(usize, PauliOperator)>) {
504 self.terms.push((coefficient, pauli_string));
505 }
506
507 #[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 observable.add_pauli_term(
515 j_coupling,
516 vec![(i, PauliOperator::X), (i + 1, PauliOperator::X)],
517 );
518 observable.add_pauli_term(
520 j_coupling,
521 vec![(i, PauliOperator::Y), (i + 1, PauliOperator::Y)],
522 );
523 observable.add_pauli_term(
525 j_coupling,
526 vec![(i, PauliOperator::Z), (i + 1, PauliOperator::Z)],
527 );
528 }
529
530 observable
531 }
532
533 #[must_use]
535 pub fn tfim(num_qubits: usize, j_coupling: f64, h_field: f64) -> Self {
536 let mut observable = Self::new();
537
538 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 for i in 0..num_qubits {
548 observable.add_pauli_term(-h_field, vec![(i, PauliOperator::X)]);
549 }
550
551 observable
552 }
553
554 #[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 for &(i, j, coeff) in one_body {
564 if i == j {
565 observable.add_pauli_term(coeff, vec![(i, PauliOperator::Z)]);
567 } else {
568 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 for &(i, j, k, l, coeff) in two_body {
578 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
601pub struct VQEOptimizer {
603 pub max_iterations: usize,
605 pub tolerance: f64,
607 pub learning_rate: f64,
609 pub optimizer_type: VQEOptimizerType,
611}
612
613#[derive(Debug, Clone, PartialEq)]
615pub enum VQEOptimizerType {
616 GradientDescent,
618 Adam { beta1: f64, beta2: f64 },
620 BFGS,
622 NelderMead,
624 SPSA { alpha: f64, gamma: f64 },
626}
627
628impl VQEOptimizer {
629 #[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 pub fn optimize<const N: usize>(
642 &self,
643 circuit: &mut VQECircuit<N>,
644 observable: &VQEObservable,
645 ) -> QuantRS2Result<VQEResult> {
646 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 let gradients = self.compute_gradients(circuit, observable)?;
658 let gradient_norm = gradients.iter().map(|g| g * g).sum::<f64>().sqrt();
659
660 if gradient_norm < self.tolerance {
663 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 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 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 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 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 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 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 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 shifted.set_parameters(&base_parameters)?;
775
776 Ok(gradients)
777 }
778}
779
780mod 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 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 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 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 let bit_masks: Vec<usize> = (0..k).map(|p| 1usize << targets[k - 1 - p]).collect();
856 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 continue;
874 }
875
876 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 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 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 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 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 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 #[test]
979 fn test_cnot_endianness() {
980 let mut state = vec![Complex64::new(0.0, 0.0); 4];
982 state[1] = Complex64::new(1.0, 0.0); let cnot = CNOT {
984 control: QubitId(0),
985 target: QubitId(1),
986 };
987 apply_gate(&mut state, 2, &cnot).expect("apply cnot");
988 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 #[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); 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 #[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 #[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 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 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 assert_eq!(vqe.get_parameter("theta"), Some(0.0));
1104 assert_eq!(vqe.get_parameter("phi"), Some(0.0));
1105
1106 vqe.set_parameters(&[PI / 4.0, PI / 2.0])
1108 .expect("set params");
1109
1110 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 assert_eq!(vqe.circuit.gates().len(), 2);
1116
1117 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 let new_params: Vec<f64> = vec![PI / 3.0; n_params];
1135 vqe.set_parameters(&new_params).expect("set all params");
1136
1137 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 let result = vqe.set_parameters(&[0.1, 0.2]);
1151 assert!(result.is_err());
1152 }
1153
1154 #[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 #[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 #[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 #[test]
1230 fn test_parameter_shift_gradient_matches_finite_difference() {
1231 use std::f64::consts::PI;
1232
1233 let optimizer = VQEOptimizer::default();
1234 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 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 #[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 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}