1use std::collections::BTreeMap;
7use std::sync::OnceLock;
8
9use num_complex::Complex64;
10
11use crate::circuit::Circuit;
12use crate::error::{PrismError, Result};
13use crate::gates::Gate;
14use crate::sim::RunMetadata;
15use crate::sim::unified_pauli::{PauliAxis, PauliTerm};
16
17#[derive(Debug, Clone, Default)]
25pub struct PauliObservable {
26 terms: Vec<(f64, Vec<PauliTerm>)>,
27 grouping: OnceLock<Grouping>,
28}
29
30impl PauliObservable {
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn from_terms(terms: impl IntoIterator<Item = (f64, Vec<PauliTerm>)>) -> Result<Self> {
40 let mut observable = Self::new();
41 for (coefficient, factors) in terms {
42 observable.add_term(coefficient, factors)?;
43 }
44 Ok(observable)
45 }
46
47 pub fn add_term(&mut self, coefficient: f64, mut factors: Vec<PauliTerm>) -> Result<()> {
53 if !coefficient.is_finite() {
54 return Err(PrismError::InvalidParameter {
55 message: format!("observable coefficient {coefficient} is not finite"),
56 });
57 }
58 factors.sort_unstable_by_key(|term| term.qubit);
59 if let Some(pair) = factors
60 .windows(2)
61 .find(|pair| pair[0].qubit == pair[1].qubit)
62 {
63 return Err(PrismError::InvalidParameter {
64 message: format!(
65 "joint Pauli observable has duplicate factor on qubit {}",
66 pair[0].qubit
67 ),
68 });
69 }
70 self.merge_term(coefficient, factors);
71 Ok(())
72 }
73
74 fn merge_term(&mut self, coefficient: f64, factors: Vec<PauliTerm>) {
75 match self
76 .terms
77 .binary_search_by(|(_, existing)| existing.as_slice().cmp(&factors))
78 {
79 Ok(i) => self.terms[i].0 += coefficient,
80 Err(i) => self.terms.insert(i, (coefficient, factors)),
81 }
82 self.grouping = OnceLock::new();
83 }
84
85 pub fn terms(&self) -> &[(f64, Vec<PauliTerm>)] {
87 &self.terms
88 }
89
90 pub fn num_terms(&self) -> usize {
91 self.terms.len()
92 }
93
94 pub fn num_groups(&self) -> usize {
97 self.grouping().groups.len()
98 }
99
100 pub(crate) fn grouping(&self) -> &Grouping {
101 self.grouping.get_or_init(|| compute_grouping(&self.terms))
102 }
103
104 pub fn split_identity(&self) -> (f64, PauliObservable) {
110 let mut offset = 0.0;
111 let mut rest = PauliObservable::new();
112 for (coefficient, string) in &self.terms {
113 if string.is_empty() {
114 offset += coefficient;
115 } else {
116 rest.merge_term(*coefficient, string.clone());
117 }
118 }
119 (offset, rest)
120 }
121
122 pub fn square(&self) -> PauliObservable {
137 let mut accumulated: BTreeMap<Vec<PauliTerm>, f64> = BTreeMap::new();
138 for (left, left_string) in &self.terms {
139 for (right, right_string) in &self.terms {
140 let (phase, product) = multiply_pauli_strings(left_string, right_string);
141 if phase % 2 == 1 {
142 continue;
143 }
144 let sign = if phase == 0 { 1.0 } else { -1.0 };
145 *accumulated.entry(product).or_insert(0.0) += sign * left * right;
146 }
147 }
148 let norm = self.terms.iter().map(|(c, _)| c.abs()).sum::<f64>();
149 let tolerance = f64::EPSILON * norm * norm * self.terms.len().max(1) as f64;
150 let mut squared = PauliObservable::new();
151 for (string, coefficient) in accumulated {
152 if coefficient.abs() > tolerance {
153 squared.merge_term(coefficient, string);
154 }
155 }
156 squared
157 }
158}
159
160fn multiply_pauli_strings(left: &[PauliTerm], right: &[PauliTerm]) -> (u32, Vec<PauliTerm>) {
162 let mut phase = 0u32;
163 let mut product = Vec::with_capacity(left.len() + right.len());
164 let (mut i, mut j) = (0, 0);
165 while i < left.len() && j < right.len() {
166 let (a, b) = (left[i], right[j]);
167 match a.qubit.cmp(&b.qubit) {
168 std::cmp::Ordering::Less => {
169 product.push(a);
170 i += 1;
171 }
172 std::cmp::Ordering::Greater => {
173 product.push(b);
174 j += 1;
175 }
176 std::cmp::Ordering::Equal => {
177 if let Some((step, axis)) = multiply_pauli_axes(a.axis, b.axis) {
178 phase = (phase + step) % 4;
179 product.push(PauliTerm::new(a.qubit, axis));
180 }
181 i += 1;
182 j += 1;
183 }
184 }
185 }
186 product.extend_from_slice(&left[i..]);
187 product.extend_from_slice(&right[j..]);
188 (phase, product)
189}
190
191fn multiply_pauli_axes(a: PauliAxis, b: PauliAxis) -> Option<(u32, PauliAxis)> {
194 use PauliAxis::{X, Y, Z};
195 match (a, b) {
196 (X, Y) => Some((1, Z)),
197 (Y, Z) => Some((1, X)),
198 (Z, X) => Some((1, Y)),
199 (Y, X) => Some((3, Z)),
200 (Z, Y) => Some((3, X)),
201 (X, Z) => Some((3, Y)),
202 _ => None,
203 }
204}
205
206impl std::ops::Add for PauliObservable {
207 type Output = PauliObservable;
208
209 fn add(mut self, rhs: PauliObservable) -> PauliObservable {
210 for (coefficient, factors) in rhs.terms {
211 self.merge_term(coefficient, factors);
212 }
213 self
214 }
215}
216
217impl std::ops::Sub for PauliObservable {
218 type Output = PauliObservable;
219
220 fn sub(self, rhs: PauliObservable) -> PauliObservable {
221 self + (-rhs)
222 }
223}
224
225impl std::ops::Neg for PauliObservable {
226 type Output = PauliObservable;
227
228 fn neg(mut self) -> PauliObservable {
229 for (coefficient, _) in &mut self.terms {
230 *coefficient = -*coefficient;
231 }
232 self
233 }
234}
235
236impl std::ops::Mul<f64> for PauliObservable {
237 type Output = PauliObservable;
238
239 fn mul(mut self, rhs: f64) -> PauliObservable {
240 for (coefficient, _) in &mut self.terms {
241 *coefficient *= rhs;
242 }
243 self
244 }
245}
246
247#[derive(Debug, Clone)]
249pub struct ObservableExpectation {
250 pub mean: f64,
252 pub variance: Option<f64>,
260 pub group_variances: Option<Vec<f64>>,
262 pub std_error: Option<f64>,
265 pub metadata: RunMetadata,
266}
267
268#[derive(Debug, Clone)]
270pub(crate) struct Grouping {
271 pub(crate) groups: Vec<QwcGroup>,
272}
273
274#[derive(Debug, Clone)]
278pub(crate) struct QwcGroup {
279 pub(crate) term_indices: Vec<usize>,
280 axis_x: Vec<u64>,
281 axis_z: Vec<u64>,
282}
283
284impl QwcGroup {
285 fn accepts(&self, tx: &[u64], tz: &[u64]) -> bool {
286 for w in 0..tx.len() {
287 let shared = (tx[w] | tz[w]) & (self.axis_x[w] | self.axis_z[w]);
288 if ((tx[w] ^ self.axis_x[w]) | (tz[w] ^ self.axis_z[w])) & shared != 0 {
289 return false;
290 }
291 }
292 true
293 }
294
295 fn absorb(&mut self, index: usize, tx: &[u64], tz: &[u64]) {
296 for w in 0..tx.len() {
297 self.axis_x[w] |= tx[w];
298 self.axis_z[w] |= tz[w];
299 }
300 self.term_indices.push(index);
301 }
302
303 pub(crate) fn is_z_only(&self) -> bool {
306 self.axis_x.iter().all(|&word| word == 0)
307 }
308
309 pub(crate) fn basis_rotation_circuit(&self, num_qubits: usize) -> Circuit {
313 let mut circuit = Circuit::new(num_qubits, 0);
314 for qubit in 0..num_qubits.min(self.axis_x.len() * 64) {
315 let bit = 1u64 << (qubit % 64);
316 if self.axis_x[qubit / 64] & bit != 0 {
317 if self.axis_z[qubit / 64] & bit != 0 {
318 circuit.add_gate(Gate::Sdg, &[qubit]);
319 }
320 circuit.add_gate(Gate::H, &[qubit]);
321 }
322 }
323 circuit
324 }
325}
326
327fn compute_grouping(terms: &[(f64, Vec<PauliTerm>)]) -> Grouping {
328 let max_qubit = terms
329 .iter()
330 .flat_map(|(_, factors)| factors.iter())
331 .map(|term| term.qubit)
332 .max();
333 let num_words = max_qubit.map_or(0, |q| q / 64 + 1);
334
335 let mut order: Vec<usize> = (0..terms.len())
337 .filter(|&i| !terms[i].1.is_empty())
338 .collect();
339 order.sort_by(|&a, &b| terms[b].1.len().cmp(&terms[a].1.len()).then(a.cmp(&b)));
340
341 let mut groups: Vec<QwcGroup> = Vec::new();
342 let mut tx = vec![0u64; num_words];
343 let mut tz = vec![0u64; num_words];
344 for &index in &order {
345 tx.fill(0);
346 tz.fill(0);
347 for term in &terms[index].1 {
348 let bit = 1u64 << (term.qubit % 64);
349 match term.axis {
350 PauliAxis::X => tx[term.qubit / 64] |= bit,
351 PauliAxis::Z => tz[term.qubit / 64] |= bit,
352 PauliAxis::Y => {
353 tx[term.qubit / 64] |= bit;
354 tz[term.qubit / 64] |= bit;
355 }
356 }
357 }
358 match groups.iter_mut().find(|group| group.accepts(&tx, &tz)) {
359 Some(group) => group.absorb(index, &tx, &tz),
360 None => groups.push(QwcGroup {
361 term_indices: vec![index],
362 axis_x: tx.clone(),
363 axis_z: tz.clone(),
364 }),
365 }
366 }
367 Grouping { groups }
368}
369
370pub(crate) fn weighted_group_moments(
377 state: &[Complex64],
378 zmasks: &[usize],
379 coefficients: &[f64],
380 norm: f64,
381) -> (f64, f64) {
382 if norm == 0.0 {
383 return (0.0, 0.0);
384 }
385
386 let accumulate = |acc: &mut (f64, f64), base: usize, block: &[Complex64]| {
387 for (offset, amp) in block.iter().enumerate() {
388 let j = base + offset;
389 let mut h = 0.0;
390 for (&zmask, &c) in zmasks.iter().zip(coefficients) {
391 h += if (j & zmask).count_ones() & 1 == 1 {
392 -c
393 } else {
394 c
395 };
396 }
397 let weighted = amp.norm_sqr() * h;
398 acc.0 += weighted;
399 acc.1 += weighted * h;
400 }
401 };
402
403 #[cfg(feature = "parallel")]
404 if state.len() >= crate::backend::MIN_PAR_REDUCE_ELEMS {
405 use rayon::prelude::*;
406 let chunk = crate::backend::MIN_PAR_ELEMS;
407 let (m1, m2) = state
408 .par_chunks(chunk)
409 .enumerate()
410 .fold(
411 || (0.0, 0.0),
412 |mut acc, (c, block)| {
413 accumulate(&mut acc, c * chunk, block);
414 acc
415 },
416 )
417 .reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
418 return (m1 / norm, m2 / norm);
419 }
420
421 let mut acc = (0.0, 0.0);
422 accumulate(&mut acc, 0, state);
423 (acc.0 / norm, acc.1 / norm)
424}
425
426#[cfg(test)]
427#[path = "observable_tests.rs"]
428mod tests;
429
430pub(crate) fn validate_observable(observable: &[PauliTerm], num_qubits: usize) -> Result<()> {
436 let mut seen = vec![false; num_qubits];
437 for term in observable {
438 if term.qubit >= num_qubits {
439 return Err(PrismError::InvalidQubit {
440 index: term.qubit,
441 register_size: num_qubits,
442 });
443 }
444 if seen[term.qubit] {
445 return Err(PrismError::InvalidParameter {
446 message: format!(
447 "joint Pauli observable has duplicate factor on qubit {}",
448 term.qubit
449 ),
450 });
451 }
452 seen[term.qubit] = true;
453 }
454 Ok(())
455}
456
457pub(crate) fn pauli_masks(
460 observable: &[PauliTerm],
461 num_qubits: usize,
462) -> Result<(usize, usize, u32)> {
463 let mut xmask = 0usize;
464 let mut zmask = 0usize;
465 let mut num_y = 0u32;
466 let mut seen = vec![false; num_qubits];
467 for term in observable {
468 if term.qubit >= num_qubits {
469 return Err(PrismError::InvalidQubit {
470 index: term.qubit,
471 register_size: num_qubits,
472 });
473 }
474 if seen[term.qubit] {
475 return Err(PrismError::InvalidParameter {
476 message: format!(
477 "joint Pauli observable has duplicate factor on qubit {}",
478 term.qubit
479 ),
480 });
481 }
482 seen[term.qubit] = true;
483 let bit = 1usize << term.qubit;
484 match term.axis {
485 PauliAxis::X => xmask |= bit,
486 PauliAxis::Z => zmask |= bit,
487 PauliAxis::Y => {
488 xmask |= bit;
489 zmask |= bit;
490 num_y += 1;
491 }
492 }
493 }
494 Ok((xmask, zmask, num_y))
495}
496
497#[cfg(feature = "parallel")]
502const SANDWICH_MIN_PAR_QUBITS: usize = 16;
503
504#[inline]
514pub(crate) fn pauli_sandwich(
515 lambda: &[Complex64],
516 phi: &[Complex64],
517 xmask: usize,
518 zmask: usize,
519 num_y: u32,
520) -> Complex64 {
521 let term = |j: usize, amp: Complex64| {
522 let partner = lambda[j ^ xmask];
523 let sign = if (j & zmask).count_ones() & 1 == 1 {
524 -1.0
525 } else {
526 1.0
527 };
528 partner.conj() * amp * sign
529 };
530
531 #[cfg(feature = "parallel")]
532 let acc: Complex64 = if phi.len() >= (1 << SANDWICH_MIN_PAR_QUBITS) {
533 use rayon::prelude::*;
534 phi.par_iter()
535 .enumerate()
536 .map(|(j, &)| term(j, amp))
537 .sum()
538 } else {
539 phi.iter().enumerate().map(|(j, &)| term(j, amp)).sum()
540 };
541 #[cfg(not(feature = "parallel"))]
542 let acc: Complex64 = phi.iter().enumerate().map(|(j, &)| term(j, amp)).sum();
543
544 acc * i_pow(num_y)
545}
546
547pub(crate) fn pauli_sandwiches_from_masks(
555 lambda: &[Complex64],
556 phi: &[Complex64],
557 masks: &[(usize, usize, u32)],
558) -> Vec<Complex64> {
559 if masks.len() < 2 {
560 return masks
561 .iter()
562 .map(|&(xmask, zmask, num_y)| pauli_sandwich(lambda, phi, xmask, zmask, num_y))
563 .collect();
564 }
565
566 let z_only: Vec<usize> = masks
567 .iter()
568 .filter(|&&(xmask, _, _)| xmask == 0)
569 .map(|&(_, zmask, _)| zmask)
570 .collect();
571 let general: Vec<(usize, usize)> = masks
572 .iter()
573 .filter(|&&(xmask, _, _)| xmask != 0)
574 .map(|&(xmask, zmask, _)| (xmask, zmask))
575 .collect();
576
577 let accumulate = |z_acc: &mut [Complex64], g_acc: &mut [Complex64], base: usize, len: usize| {
578 for j in base..base + len {
579 let amp = phi[j];
580 let aligned = lambda[j].conj() * amp;
581 for (slot, &zmask) in z_acc.iter_mut().zip(z_only.iter()) {
582 *slot += if (j & zmask).count_ones() & 1 == 1 {
583 -aligned
584 } else {
585 aligned
586 };
587 }
588 for (slot, &(xmask, zmask)) in g_acc.iter_mut().zip(general.iter()) {
589 let partner = lambda[j ^ xmask];
590 let sign = if (j & zmask).count_ones() & 1 == 1 {
591 -1.0
592 } else {
593 1.0
594 };
595 *slot += partner.conj() * amp * sign;
596 }
597 }
598 };
599
600 let zeros = || {
601 (
602 vec![Complex64::new(0.0, 0.0); z_only.len()],
603 vec![Complex64::new(0.0, 0.0); general.len()],
604 )
605 };
606 let (mut z_sum, mut g_sum) = zeros();
607
608 #[cfg(feature = "parallel")]
609 if phi.len() >= (1 << SANDWICH_MIN_PAR_QUBITS) {
610 use rayon::prelude::*;
611 let chunk = crate::backend::MIN_PAR_ELEMS;
612 let (z, g) = phi
613 .par_chunks(chunk)
614 .enumerate()
615 .fold(zeros, |mut acc, (c, block)| {
616 accumulate(&mut acc.0, &mut acc.1, c * chunk, block.len());
617 acc
618 })
619 .reduce(zeros, |mut a, b| {
620 for (slot, v) in a.0.iter_mut().zip(b.0) {
621 *slot += v;
622 }
623 for (slot, v) in a.1.iter_mut().zip(b.1) {
624 *slot += v;
625 }
626 a
627 });
628 return finish_sandwiches(masks, &z, &g);
629 }
630
631 accumulate(&mut z_sum, &mut g_sum, 0, phi.len());
632 finish_sandwiches(masks, &z_sum, &g_sum)
633}
634
635fn finish_sandwiches(
638 masks: &[(usize, usize, u32)],
639 z_sum: &[Complex64],
640 g_sum: &[Complex64],
641) -> Vec<Complex64> {
642 let (mut zi, mut gi) = (0, 0);
643 masks
644 .iter()
645 .map(|&(xmask, _, num_y)| {
646 let raw = if xmask == 0 {
647 zi += 1;
648 z_sum[zi - 1]
649 } else {
650 gi += 1;
651 g_sum[gi - 1]
652 };
653 raw * i_pow(num_y)
654 })
655 .collect()
656}
657
658#[inline]
660pub(crate) fn i_pow(num_y: u32) -> Complex64 {
661 match num_y % 4 {
662 0 => Complex64::new(1.0, 0.0),
663 1 => Complex64::new(0.0, 1.0),
664 2 => Complex64::new(-1.0, 0.0),
665 _ => Complex64::new(0.0, -1.0),
666 }
667}
668
669pub(crate) fn pauli_expectation_from_masks(
672 state: &[Complex64],
673 xmask: usize,
674 zmask: usize,
675 num_y: u32,
676 norm: f64,
677) -> f64 {
678 if norm == 0.0 {
679 return 0.0;
680 }
681 pauli_sandwich(state, state, xmask, zmask, num_y).re / norm
682}
683
684pub(crate) fn pauli_expectations_from_masks(
692 state: &[Complex64],
693 masks: &[(usize, usize, u32)],
694 norm: f64,
695) -> Vec<f64> {
696 if norm == 0.0 {
697 return vec![0.0; masks.len()];
698 }
699 if masks.len() < 2 {
700 return masks
701 .iter()
702 .map(|&(xmask, zmask, num_y)| {
703 pauli_expectation_from_masks(state, xmask, zmask, num_y, norm)
704 })
705 .collect();
706 }
707
708 let z_only: Vec<usize> = masks
709 .iter()
710 .filter(|&&(xmask, _, _)| xmask == 0)
711 .map(|&(_, zmask, _)| zmask)
712 .collect();
713 let general: Vec<(usize, usize)> = masks
714 .iter()
715 .filter(|&&(xmask, _, _)| xmask != 0)
716 .map(|&(xmask, zmask, _)| (xmask, zmask))
717 .collect();
718
719 let accumulate = |z_acc: &mut [f64], g_acc: &mut [Complex64], base: usize, len: usize| {
720 for j in base..base + len {
721 let amp = state[j];
722 let n2 = amp.norm_sqr();
723 for (slot, &zmask) in z_acc.iter_mut().zip(z_only.iter()) {
724 *slot += if (j & zmask).count_ones() & 1 == 1 {
725 -n2
726 } else {
727 n2
728 };
729 }
730 for (slot, &(xmask, zmask)) in g_acc.iter_mut().zip(general.iter()) {
731 let partner = state[j ^ xmask];
732 let sign = if (j & zmask).count_ones() & 1 == 1 {
733 -1.0
734 } else {
735 1.0
736 };
737 *slot += partner.conj() * amp * sign;
738 }
739 }
740 };
741
742 let zeros = || {
743 (
744 vec![0.0f64; z_only.len()],
745 vec![Complex64::new(0.0, 0.0); general.len()],
746 )
747 };
748 let (mut z_sum, mut g_sum) = zeros();
749
750 #[cfg(feature = "parallel")]
751 if state.len() >= crate::backend::MIN_PAR_REDUCE_ELEMS {
752 use rayon::prelude::*;
753 let chunk = crate::backend::MIN_PAR_ELEMS;
754 let (z, g) = state
755 .par_chunks(chunk)
756 .enumerate()
757 .fold(zeros, |mut acc, (c, block)| {
758 accumulate(&mut acc.0, &mut acc.1, c * chunk, block.len());
759 acc
760 })
761 .reduce(zeros, |mut a, b| {
762 for (slot, v) in a.0.iter_mut().zip(b.0) {
763 *slot += v;
764 }
765 for (slot, v) in a.1.iter_mut().zip(b.1) {
766 *slot += v;
767 }
768 a
769 });
770 return finish_expectations(masks, &z, &g, norm);
771 }
772
773 accumulate(&mut z_sum, &mut g_sum, 0, state.len());
774 finish_expectations(masks, &z_sum, &g_sum, norm)
775}
776
777pub(crate) fn finish_expectations(
783 masks: &[(usize, usize, u32)],
784 z_sum: &[f64],
785 g_sum: &[Complex64],
786 norm: f64,
787) -> Vec<f64> {
788 let (mut zi, mut gi) = (0, 0);
789 masks
790 .iter()
791 .map(|&(xmask, _, num_y)| {
792 if xmask == 0 {
793 zi += 1;
794 z_sum[zi - 1] / norm
795 } else {
796 gi += 1;
797 (g_sum[gi - 1] * i_pow(num_y)).re / norm
798 }
799 })
800 .collect()
801}