scirs2_optimize/quantum_classical/
vqe.rs1use crate::error::OptimizeError;
7use crate::quantum_classical::statevector::{cabs2, Statevector};
8use crate::quantum_classical::QcResult;
9
10#[non_exhaustive]
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum PauliOp {
16 I,
18 X,
20 Y,
22 Z,
24}
25
26#[derive(Debug, Clone)]
34pub struct PauliHamiltonian {
35 pub terms: Vec<(f64, Vec<(usize, PauliOp)>)>,
37 pub n_qubits: usize,
39}
40
41impl PauliHamiltonian {
42 pub fn new(n_qubits: usize) -> Self {
44 Self {
45 terms: Vec::new(),
46 n_qubits,
47 }
48 }
49
50 pub fn add_term(&mut self, coefficient: f64, ops: Vec<(usize, PauliOp)>) {
52 self.terms.push((coefficient, ops));
53 }
54
55 pub fn expectation(&self, sv: &Statevector) -> QcResult<f64> {
60 let mut total = 0.0;
61 for (coeff, ops) in &self.terms {
62 let mut temp = sv.clone();
64 for &(qubit, ref pauli) in ops {
65 match pauli {
66 PauliOp::I => {} PauliOp::X => temp.apply_x(qubit)?,
68 PauliOp::Y => temp.apply_y(qubit)?,
69 PauliOp::Z => temp.apply_z(qubit)?,
70 _ => {} }
72 }
73 let dot: f64 = sv
75 .amplitudes
76 .iter()
77 .zip(temp.amplitudes.iter())
78 .map(|(&(ar, ai), &(br, bi))| ar * br + ai * bi)
79 .sum();
80 total += coeff * dot;
81 }
82 Ok(total)
83 }
84
85 pub fn transverse_ising_1d(n_qubits: usize, j: f64, h: f64) -> Self {
87 let mut ham = Self::new(n_qubits);
88 for i in 0..(n_qubits - 1) {
90 ham.add_term(-j, vec![(i, PauliOp::Z), (i + 1, PauliOp::Z)]);
91 }
92 for i in 0..n_qubits {
94 ham.add_term(-h, vec![(i, PauliOp::X)]);
95 }
96 ham
97 }
98}
99
100impl Statevector {
103 pub fn apply_x(&mut self, qubit: usize) -> QcResult<()> {
105 let bit = 1usize << qubit;
107 let dim = self.amplitudes.len();
108 if qubit >= self.n_qubits {
109 return Err(OptimizeError::ValueError(format!(
110 "Qubit {qubit} out of range"
111 )));
112 }
113 for i in 0..dim {
114 if i & bit == 0 {
115 self.amplitudes.swap(i, i | bit);
116 }
117 }
118 Ok(())
119 }
120
121 pub fn apply_y(&mut self, qubit: usize) -> QcResult<()> {
126 if qubit >= self.n_qubits {
127 return Err(OptimizeError::ValueError(format!(
128 "Qubit {qubit} out of range"
129 )));
130 }
131 let bit = 1usize << qubit;
132 let dim = self.amplitudes.len();
133 for i in 0..dim {
134 if i & bit == 0 {
135 let j = i | bit;
136 let (ar, ai) = self.amplitudes[i]; let (br, bi) = self.amplitudes[j]; self.amplitudes[i] = (bi, -br);
141 self.amplitudes[j] = (-ai, ar);
142 }
143 }
144 Ok(())
145 }
146
147 pub fn apply_z(&mut self, qubit: usize) -> QcResult<()> {
151 if qubit >= self.n_qubits {
152 return Err(OptimizeError::ValueError(format!(
153 "Qubit {qubit} out of range"
154 )));
155 }
156 let bit = 1usize << qubit;
157 for (i, amp) in self.amplitudes.iter_mut().enumerate() {
158 if i & bit != 0 {
159 amp.0 = -amp.0;
160 amp.1 = -amp.1;
161 }
162 }
163 Ok(())
164 }
165}
166
167#[derive(Debug, Clone)]
180pub struct HardwareEfficientAnsatz {
181 pub n_qubits: usize,
183 pub n_layers: usize,
185}
186
187impl HardwareEfficientAnsatz {
188 pub fn new(n_qubits: usize, n_layers: usize) -> Self {
190 Self { n_qubits, n_layers }
191 }
192
193 pub fn n_params(&self) -> usize {
195 (2 * self.n_layers + 1) * self.n_qubits
196 }
197
198 pub fn run(&self, params: &[f64]) -> QcResult<Statevector> {
200 let expected = self.n_params();
201 if params.len() != expected {
202 return Err(OptimizeError::ValueError(format!(
203 "Expected {expected} parameters, got {}",
204 params.len()
205 )));
206 }
207
208 let mut state = Statevector::zero_state(self.n_qubits)?;
209 let n = self.n_qubits;
210 let mut offset = 0;
211
212 for _layer in 0..self.n_layers {
213 for q in 0..n {
215 state.apply_ry(q, params[offset + q])?;
216 }
217 offset += n;
218
219 for q in 0..n {
221 state.apply_rz(q, params[offset + q])?;
222 }
223 offset += n;
224
225 for q in 0..(n - 1) {
227 state.apply_cnot(q, q + 1)?;
228 }
229 }
230
231 for q in 0..n {
233 state.apply_ry(q, params[offset + q])?;
234 }
235
236 Ok(state)
237 }
238}
239
240#[derive(Debug, Clone)]
244pub struct VqeOptimizer {
245 pub hamiltonian: PauliHamiltonian,
247 pub ansatz: HardwareEfficientAnsatz,
249 pub init_params: Vec<f64>,
251 pub max_iter: usize,
253 pub tol: f64,
255 pub learning_rate: f64,
257}
258
259impl VqeOptimizer {
260 pub fn new(
262 hamiltonian: PauliHamiltonian,
263 ansatz: HardwareEfficientAnsatz,
264 init_params: Vec<f64>,
265 ) -> Self {
266 Self {
267 hamiltonian,
268 ansatz,
269 init_params,
270 max_iter: 500,
271 tol: 1e-7,
272 learning_rate: 0.1,
273 }
274 }
275
276 pub fn energy(&self, params: &[f64]) -> QcResult<f64> {
278 let state = self.ansatz.run(params)?;
279 self.hamiltonian.expectation(&state)
280 }
281
282 pub fn gradient(&self, params: &[f64]) -> QcResult<Vec<f64>> {
286 let n = params.len();
287 let shift = std::f64::consts::FRAC_PI_2;
288 let mut grad = vec![0.0; n];
289
290 for k in 0..n {
291 let mut p_plus = params.to_vec();
292 let mut p_minus = params.to_vec();
293 p_plus[k] += shift;
294 p_minus[k] -= shift;
295 let e_plus = self.energy(&p_plus)?;
296 let e_minus = self.energy(&p_minus)?;
297 grad[k] = 0.5 * (e_plus - e_minus);
298 }
299 Ok(grad)
300 }
301
302 pub fn run(&mut self) -> QcResult<VqeResult> {
304 let n_params = self.ansatz.n_params();
305 if self.init_params.len() != n_params {
306 return Err(OptimizeError::ValueError(format!(
307 "Expected {n_params} initial parameters, got {}",
308 self.init_params.len()
309 )));
310 }
311
312 let mut params = self.init_params.clone();
313 let mut n_evals = 0usize;
314
315 let beta1: f64 = 0.9;
317 let beta2: f64 = 0.999;
318 let eps_adam: f64 = 1e-8;
319 let mut m = vec![0.0; n_params]; let mut v = vec![0.0; n_params]; let mut t = 0u32;
322
323 let mut prev_energy = self.energy(¶ms)?;
324 n_evals += 1;
325
326 for _iter in 0..self.max_iter {
327 let grad = self.gradient(¶ms)?;
328 n_evals += 2 * n_params; t += 1;
330
331 let t_f = t as f64;
332 let lr_t =
333 self.learning_rate * (1.0 - beta2.powf(t_f)).sqrt() / (1.0 - beta1.powf(t_f));
334
335 for k in 0..n_params {
336 m[k] = beta1 * m[k] + (1.0 - beta1) * grad[k];
337 v[k] = beta2 * v[k] + (1.0 - beta2) * grad[k] * grad[k];
338 params[k] -= lr_t * m[k] / (v[k].sqrt() + eps_adam);
339 }
340
341 let energy = self.energy(¶ms)?;
342 n_evals += 1;
343
344 if (prev_energy - energy).abs() < self.tol {
345 return Ok(VqeResult {
346 ground_state_energy: energy,
347 optimal_params: params,
348 n_evaluations: n_evals,
349 converged: true,
350 });
351 }
352 prev_energy = energy;
353 }
354
355 Ok(VqeResult {
356 ground_state_energy: prev_energy,
357 optimal_params: params,
358 n_evaluations: n_evals,
359 converged: false,
360 })
361 }
362}
363
364#[derive(Debug, Clone)]
368pub struct VqeResult {
369 pub ground_state_energy: f64,
371 pub optimal_params: Vec<f64>,
373 pub n_evaluations: usize,
375 pub converged: bool,
377}
378
379#[cfg(test)]
382mod tests {
383 use super::*;
384
385 const EPS: f64 = 1e-10;
386
387 #[test]
388 fn test_pauli_z_expectation_zero_state() {
389 let sv = Statevector::zero_state(1).unwrap();
391 let mut ham = PauliHamiltonian::new(1);
392 ham.add_term(1.0, vec![(0, PauliOp::Z)]);
393 let e = ham.expectation(&sv).unwrap();
394 assert!((e - 1.0).abs() < EPS, "Expected +1, got {e}");
395 }
396
397 #[test]
398 fn test_pauli_z_expectation_one_state() {
399 let mut sv = Statevector::zero_state(1).unwrap();
401 sv.amplitudes[0] = (0.0, 0.0);
402 sv.amplitudes[1] = (1.0, 0.0);
403 let mut ham = PauliHamiltonian::new(1);
404 ham.add_term(1.0, vec![(0, PauliOp::Z)]);
405 let e = ham.expectation(&sv).unwrap();
406 assert!((e + 1.0).abs() < EPS, "Expected -1, got {e}");
407 }
408
409 #[test]
410 fn test_vqe_ising_2qubit_energy() {
411 let j = 1.0_f64;
417 let h = 0.5_f64;
418 let ham = PauliHamiltonian::transverse_ising_1d(2, j, h);
419
420 let ansatz = HardwareEfficientAnsatz::new(2, 2);
421 let n_p = ansatz.n_params();
422 let init_params: Vec<f64> = (0..n_p).map(|i| 0.1 * (i as f64 + 1.0)).collect();
424
425 let mut vqe = VqeOptimizer::new(ham, ansatz, init_params);
426 vqe.max_iter = 300;
427 vqe.learning_rate = 0.05;
428
429 let result = vqe.run().unwrap();
430 assert!(
436 result.ground_state_energy <= -1.2 * j,
437 "VQE energy {:.4} should be ≤ -1.2 (below classical minimum of -J=-1.0)",
438 result.ground_state_energy
439 );
440 }
441
442 #[test]
443 fn test_vqe_gradient_vs_finite_differences() {
444 let ham = PauliHamiltonian::transverse_ising_1d(2, 1.0, 1.0);
445 let ansatz = HardwareEfficientAnsatz::new(2, 1);
446 let n_p = ansatz.n_params();
447 let params: Vec<f64> = (0..n_p).map(|i| 0.3 * (i as f64 + 1.0)).collect();
448 let init = params.clone();
449
450 let vqe = VqeOptimizer::new(ham, ansatz, init);
451 let grad = vqe.gradient(¶ms).unwrap();
452
453 let eps = 1e-5;
454 for k in 0..n_p {
455 let mut pp = params.clone();
456 let mut pm = params.clone();
457 pp[k] += eps;
458 pm[k] -= eps;
459 let fd = (vqe.energy(&pp).unwrap() - vqe.energy(&pm).unwrap()) / (2.0 * eps);
460 assert!(
461 (grad[k] - fd).abs() < 0.01,
462 "Parameter-shift gradient {:.4} vs FD {:.4} at param {k}",
463 grad[k],
464 fd
465 );
466 }
467 }
468
469 #[test]
470 fn test_hea_norm_preserved() {
471 let ansatz = HardwareEfficientAnsatz::new(3, 2);
472 let n_p = ansatz.n_params();
473 let params: Vec<f64> = (0..n_p).map(|i| 0.5 + 0.1 * i as f64).collect();
474 let state = ansatz.run(¶ms).unwrap();
475 let norm = state.norm_squared();
476 assert!((norm - 1.0).abs() < 1e-12, "HEA norm must be 1, got {norm}");
477 }
478
479 #[test]
480 fn test_pauli_x_expectation() {
481 let mut sv = Statevector::zero_state(1).unwrap();
483 sv.apply_hadamard(0).unwrap();
484 let mut ham = PauliHamiltonian::new(1);
485 ham.add_term(1.0, vec![(0, PauliOp::X)]);
486 let e = ham.expectation(&sv).unwrap();
487 assert!((e - 1.0).abs() < 1e-10, "⟨X⟩ of |+⟩ should be 1, got {e}");
488 }
489
490 #[test]
491 fn test_hea_n_params() {
492 let ansatz = HardwareEfficientAnsatz::new(4, 3);
494 assert_eq!(ansatz.n_params(), 7 * 4);
495 }
496}