1use crate::error::{Result, SimulatorError};
24use scirs2_core::ndarray::{Array1, Array2};
25use scirs2_core::parallel_ops::{par_chunks, par_join};
26use scirs2_core::Complex64;
27use std::collections::HashMap;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum PauliOp {
36 I,
38 X,
40 Y,
42 Z,
44}
45
46impl PauliOp {
47 pub fn from_char(c: char) -> Result<Self> {
49 match c {
50 'I' => Ok(PauliOp::I),
51 'X' => Ok(PauliOp::X),
52 'Y' => Ok(PauliOp::Y),
53 'Z' => Ok(PauliOp::Z),
54 _ => Err(SimulatorError::InvalidInput(format!(
55 "Invalid Pauli operator: {}",
56 c
57 ))),
58 }
59 }
60
61 pub fn matrix(&self) -> Array2<Complex64> {
63 match self {
64 PauliOp::I => {
65 let mut m = Array2::zeros((2, 2));
66 m[[0, 0]] = Complex64::new(1.0, 0.0);
67 m[[1, 1]] = Complex64::new(1.0, 0.0);
68 m
69 }
70 PauliOp::X => {
71 let mut m = Array2::zeros((2, 2));
72 m[[0, 1]] = Complex64::new(1.0, 0.0);
73 m[[1, 0]] = Complex64::new(1.0, 0.0);
74 m
75 }
76 PauliOp::Y => {
77 let mut m = Array2::zeros((2, 2));
78 m[[0, 1]] = Complex64::new(0.0, -1.0);
79 m[[1, 0]] = Complex64::new(0.0, 1.0);
80 m
81 }
82 PauliOp::Z => {
83 let mut m = Array2::zeros((2, 2));
84 m[[0, 0]] = Complex64::new(1.0, 0.0);
85 m[[1, 1]] = Complex64::new(-1.0, 0.0);
86 m
87 }
88 }
89 }
90
91 pub fn eigenvalues(&self) -> [f64; 2] {
93 match self {
94 PauliOp::I => [1.0, 1.0],
95 PauliOp::X | PauliOp::Y | PauliOp::Z => [1.0, -1.0],
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq)]
102pub struct PauliObservable {
103 pub operators: Vec<PauliOp>,
105 pub coefficient: Complex64,
107}
108
109impl PauliObservable {
110 pub fn new(operators: Vec<PauliOp>) -> Self {
112 Self {
113 operators,
114 coefficient: Complex64::new(1.0, 0.0),
115 }
116 }
117
118 pub fn from_string(s: &str) -> Result<Self> {
120 let operators: Result<Vec<PauliOp>> = s.chars().map(PauliOp::from_char).collect();
121 Ok(Self::new(operators?))
122 }
123
124 pub fn with_coefficient(mut self, coeff: Complex64) -> Self {
126 self.coefficient = coeff;
127 self
128 }
129
130 pub fn with_real_coefficient(mut self, coeff: f64) -> Self {
132 self.coefficient = Complex64::new(coeff, 0.0);
133 self
134 }
135
136 pub fn n_qubits(&self) -> usize {
138 self.operators.len()
139 }
140
141 pub fn is_diagonal(&self) -> bool {
143 self.operators
144 .iter()
145 .all(|op| matches!(op, PauliOp::I | PauliOp::Z))
146 }
147
148 pub fn non_identity_qubits(&self) -> Vec<(usize, PauliOp)> {
150 self.operators
151 .iter()
152 .enumerate()
153 .filter(|(_, op)| **op != PauliOp::I)
154 .map(|(i, op)| (i, *op))
155 .collect()
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct PauliHamiltonian {
162 pub terms: Vec<PauliObservable>,
164 pub n_qubits: usize,
166}
167
168impl PauliHamiltonian {
169 pub fn new(n_qubits: usize) -> Self {
171 Self {
172 terms: Vec::new(),
173 n_qubits,
174 }
175 }
176
177 pub fn add_term(&mut self, term: PauliObservable) -> Result<()> {
179 if term.n_qubits() != self.n_qubits {
180 return Err(SimulatorError::InvalidInput(format!(
181 "Term has {} qubits but Hamiltonian has {} qubits",
182 term.n_qubits(),
183 self.n_qubits
184 )));
185 }
186 self.terms.push(term);
187 Ok(())
188 }
189
190 pub fn from_terms(terms: Vec<PauliObservable>) -> Result<Self> {
192 if terms.is_empty() {
193 return Err(SimulatorError::InvalidInput(
194 "Hamiltonian must have at least one term".to_string(),
195 ));
196 }
197
198 let n_qubits = terms[0].n_qubits();
199 for term in &terms {
200 if term.n_qubits() != n_qubits {
201 return Err(SimulatorError::InvalidInput(
202 "All terms must act on the same number of qubits".to_string(),
203 ));
204 }
205 }
206
207 Ok(Self { terms, n_qubits })
208 }
209
210 pub fn n_terms(&self) -> usize {
212 self.terms.len()
213 }
214}
215
216#[derive(Debug, Clone)]
222pub struct ObservableConfig {
223 pub use_gpu: bool,
225 pub batch_size: usize,
227 pub use_diagonal_optimization: bool,
229}
230
231impl Default for ObservableConfig {
232 fn default() -> Self {
233 Self {
234 use_gpu: true,
235 batch_size: 1024,
236 use_diagonal_optimization: true,
237 }
238 }
239}
240
241pub struct ObservableCalculator {
243 config: ObservableConfig,
245 pauli_cache: HashMap<PauliOp, Array2<Complex64>>,
247}
248
249impl ObservableCalculator {
250 pub fn new() -> Self {
252 Self::with_config(ObservableConfig::default())
253 }
254
255 pub fn with_config(config: ObservableConfig) -> Self {
257 let mut pauli_cache = HashMap::new();
258 pauli_cache.insert(PauliOp::I, PauliOp::I.matrix());
259 pauli_cache.insert(PauliOp::X, PauliOp::X.matrix());
260 pauli_cache.insert(PauliOp::Y, PauliOp::Y.matrix());
261 pauli_cache.insert(PauliOp::Z, PauliOp::Z.matrix());
262
263 Self {
264 config,
265 pauli_cache,
266 }
267 }
268
269 pub fn expectation_value(
271 &self,
272 state: &Array1<Complex64>,
273 observable: &PauliObservable,
274 ) -> Result<Complex64> {
275 let n_qubits = observable.n_qubits();
276 let state_size = 1 << n_qubits;
277
278 if state.len() != state_size {
279 return Err(SimulatorError::InvalidInput(format!(
280 "State size {} does not match observable size 2^{} = {}",
281 state.len(),
282 n_qubits,
283 state_size
284 )));
285 }
286
287 if self.config.use_diagonal_optimization && observable.is_diagonal() {
289 return self.expectation_value_diagonal(state, observable);
290 }
291
292 let o_psi = self.apply_pauli_observable(state, observable)?;
294
295 let expectation = state
297 .iter()
298 .zip(o_psi.iter())
299 .map(|(a, b)| a.conj() * b)
300 .sum::<Complex64>();
301
302 Ok(expectation * observable.coefficient)
303 }
304
305 fn expectation_value_diagonal(
307 &self,
308 state: &Array1<Complex64>,
309 observable: &PauliObservable,
310 ) -> Result<Complex64> {
311 let n_qubits = observable.n_qubits();
312 let state_size = state.len();
313
314 let mut expectation = Complex64::new(0.0, 0.0);
315
316 for i in 0..state_size {
319 let mut sign = 1.0;
320
321 for (qubit_idx, op) in observable.operators.iter().enumerate() {
323 if *op == PauliOp::Z {
324 let bit = (i >> (n_qubits - 1 - qubit_idx)) & 1;
326 if bit == 1 {
327 sign *= -1.0;
328 }
329 }
330 }
331
332 expectation += state[i].norm_sqr() * sign;
333 }
334
335 Ok(expectation * observable.coefficient)
336 }
337
338 fn apply_pauli_observable(
340 &self,
341 state: &Array1<Complex64>,
342 observable: &PauliObservable,
343 ) -> Result<Array1<Complex64>> {
344 let n_qubits = observable.n_qubits();
345 let state_size = state.len();
346 let mut result = state.clone();
347
348 let non_identity = observable.non_identity_qubits();
350
351 for (qubit_idx, op) in non_identity {
352 result = self.apply_single_qubit_pauli(&result, qubit_idx, op, n_qubits)?;
353 }
354
355 Ok(result)
356 }
357
358 fn apply_single_qubit_pauli(
360 &self,
361 state: &Array1<Complex64>,
362 qubit: usize,
363 op: PauliOp,
364 n_qubits: usize,
365 ) -> Result<Array1<Complex64>> {
366 let state_size = state.len();
367 let mut new_state = Array1::zeros(state_size);
368
369 match op {
370 PauliOp::I => {
371 return Ok(state.clone());
373 }
374 PauliOp::X => {
375 for i in 0..state_size {
377 let j = i ^ (1 << (n_qubits - 1 - qubit));
378 new_state[i] = state[j];
379 }
380 }
381 PauliOp::Y => {
382 for i in 0..state_size {
384 let j = i ^ (1 << (n_qubits - 1 - qubit));
385 let bit = (i >> (n_qubits - 1 - qubit)) & 1;
386 let sign = if bit == 0 {
387 Complex64::new(0.0, -1.0)
388 } else {
389 Complex64::new(0.0, 1.0)
390 };
391 new_state[i] = sign * state[j];
392 }
393 }
394 PauliOp::Z => {
395 for i in 0..state_size {
397 let bit = (i >> (n_qubits - 1 - qubit)) & 1;
398 let sign = if bit == 0 { 1.0 } else { -1.0 };
399 new_state[i] = state[i] * sign;
400 }
401 }
402 }
403
404 Ok(new_state)
405 }
406
407 pub fn hamiltonian_expectation_value(
409 &self,
410 state: &Array1<Complex64>,
411 hamiltonian: &PauliHamiltonian,
412 ) -> Result<Complex64> {
413 if state.len() != (1 << hamiltonian.n_qubits) {
414 return Err(SimulatorError::InvalidInput(
415 "State size does not match Hamiltonian".to_string(),
416 ));
417 }
418
419 let mut total = Complex64::new(0.0, 0.0);
421 for term in &hamiltonian.terms {
422 let exp_val = self.expectation_value(state, term)?;
423 total += exp_val;
424 }
425
426 Ok(total)
427 }
428
429 pub fn variance(&self, state: &Array1<Complex64>, observable: &PauliObservable) -> Result<f64> {
431 let exp_o = self.expectation_value(state, observable)?;
433 let var = 1.0 - exp_o.norm_sqr();
434
435 Ok(var.max(0.0)) }
437
438 pub fn batch_expectation_values(
440 &self,
441 state: &Array1<Complex64>,
442 observables: &[PauliObservable],
443 ) -> Result<Vec<Complex64>> {
444 if observables.is_empty() {
445 return Ok(Vec::new());
446 }
447
448 let n_qubits = observables[0].n_qubits();
450 for obs in observables {
451 if obs.n_qubits() != n_qubits {
452 return Err(SimulatorError::InvalidInput(
453 "All observables must act on the same number of qubits".to_string(),
454 ));
455 }
456 }
457
458 if observables.len() > 4 {
460 let results: Vec<Complex64> = observables
461 .iter()
462 .map(|obs| self.expectation_value(state, obs))
463 .collect::<Result<Vec<_>>>()?;
464 Ok(results)
465 } else {
466 observables
468 .iter()
469 .map(|obs| self.expectation_value(state, obs))
470 .collect()
471 }
472 }
473}
474
475impl Default for ObservableCalculator {
476 fn default() -> Self {
477 Self::new()
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 fn normalize(mut state: Array1<Complex64>) -> Array1<Complex64> {
486 let norm = state.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
487 for c in state.iter_mut() {
488 *c /= norm;
489 }
490 state
491 }
492
493 #[test]
494 fn test_pauli_op_from_char() {
495 assert_eq!(PauliOp::from_char('I').unwrap(), PauliOp::I);
496 assert_eq!(PauliOp::from_char('X').unwrap(), PauliOp::X);
497 assert_eq!(PauliOp::from_char('Y').unwrap(), PauliOp::Y);
498 assert_eq!(PauliOp::from_char('Z').unwrap(), PauliOp::Z);
499 assert!(PauliOp::from_char('A').is_err());
500 }
501
502 #[test]
503 fn test_pauli_observable_from_string() {
504 let obs = PauliObservable::from_string("XYZ").unwrap();
505 assert_eq!(obs.n_qubits(), 3);
506 assert_eq!(obs.operators[0], PauliOp::X);
507 assert_eq!(obs.operators[1], PauliOp::Y);
508 assert_eq!(obs.operators[2], PauliOp::Z);
509 }
510
511 #[test]
512 fn test_pauli_observable_is_diagonal() {
513 let diag = PauliObservable::from_string("IZI").unwrap();
514 assert!(diag.is_diagonal());
515
516 let non_diag = PauliObservable::from_string("XZI").unwrap();
517 assert!(!non_diag.is_diagonal());
518 }
519
520 #[test]
521 fn test_expectation_value_z_basis() {
522 let calc = ObservableCalculator::new();
523
524 let state = normalize(Array1::from_vec(vec![
526 Complex64::new(1.0, 0.0),
527 Complex64::new(0.0, 0.0),
528 Complex64::new(0.0, 0.0),
529 Complex64::new(0.0, 0.0),
530 ]));
531
532 let z0 = PauliObservable::from_string("ZI").unwrap();
534 let exp_val = calc.expectation_value(&state, &z0).unwrap();
535 assert!((exp_val.re - 1.0).abs() < 1e-10); assert!(exp_val.im.abs() < 1e-10);
537
538 let z1 = PauliObservable::from_string("IZ").unwrap();
540 let exp_val = calc.expectation_value(&state, &z1).unwrap();
541 assert!((exp_val.re - 1.0).abs() < 1e-10);
542 }
543
544 #[test]
545 fn test_expectation_value_x_basis() {
546 let calc = ObservableCalculator::new();
547
548 let state = normalize(Array1::from_vec(vec![
550 Complex64::new(1.0, 0.0),
551 Complex64::new(1.0, 0.0),
552 ]));
553
554 let x = PauliObservable::from_string("X").unwrap();
556 let exp_val = calc.expectation_value(&state, &x).unwrap();
557 assert!((exp_val.re - 1.0).abs() < 1e-10); assert!(exp_val.im.abs() < 1e-10);
559 }
560
561 #[test]
562 fn test_hamiltonian_expectation() {
563 let calc = ObservableCalculator::new();
564
565 let z0 = PauliObservable::from_string("ZI").unwrap();
567 let z1 = PauliObservable::from_string("IZ").unwrap();
568 let h = PauliHamiltonian::from_terms(vec![z0, z1]).unwrap();
569
570 let state = normalize(Array1::from_vec(vec![
572 Complex64::new(1.0, 0.0),
573 Complex64::new(0.0, 0.0),
574 Complex64::new(0.0, 0.0),
575 Complex64::new(0.0, 0.0),
576 ]));
577
578 let exp_val = calc.hamiltonian_expectation_value(&state, &h).unwrap();
579 assert!((exp_val.re - 2.0).abs() < 1e-10); }
581
582 #[test]
583 fn test_variance() {
584 let calc = ObservableCalculator::new();
585
586 let state = normalize(Array1::from_vec(vec![
588 Complex64::new(1.0, 0.0),
589 Complex64::new(0.0, 0.0),
590 ]));
591
592 let z = PauliObservable::from_string("Z").unwrap();
593 let var = calc.variance(&state, &z).unwrap();
594 assert!(var.abs() < 1e-10); let state_plus = normalize(Array1::from_vec(vec![
598 Complex64::new(1.0, 0.0),
599 Complex64::new(1.0, 0.0),
600 ]));
601
602 let var = calc.variance(&state_plus, &z).unwrap();
603 assert!((var - 1.0).abs() < 1e-10); }
605
606 #[test]
607 fn test_batch_expectation_values() {
608 let calc = ObservableCalculator::new();
609
610 let state = normalize(Array1::from_vec(vec![
611 Complex64::new(1.0, 0.0),
612 Complex64::new(0.0, 0.0),
613 Complex64::new(0.0, 0.0),
614 Complex64::new(0.0, 0.0),
615 ]));
616
617 let observables = vec![
618 PauliObservable::from_string("ZI").unwrap(),
619 PauliObservable::from_string("IZ").unwrap(),
620 PauliObservable::from_string("ZZ").unwrap(),
621 ];
622
623 let results = calc.batch_expectation_values(&state, &observables).unwrap();
624 assert_eq!(results.len(), 3);
625 assert!((results[0].re - 1.0).abs() < 1e-10);
626 assert!((results[1].re - 1.0).abs() < 1e-10);
627 assert!((results[2].re - 1.0).abs() < 1e-10); }
629}