1use std::collections::BTreeMap;
7use std::sync::OnceLock;
8
9use num_complex::Complex64;
10
11use crate::error::{PrismError, Result};
12use crate::sim::RunMetadata;
13use crate::sim::unified_pauli::{PauliAxis, PauliTerm};
14
15#[derive(Debug, Clone, Default)]
23pub struct PauliObservable {
24 terms: Vec<(f64, Vec<PauliTerm>)>,
25 grouping: OnceLock<Grouping>,
26}
27
28impl PauliObservable {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn from_terms(terms: impl IntoIterator<Item = (f64, Vec<PauliTerm>)>) -> Result<Self> {
38 let mut observable = Self::new();
39 for (coefficient, factors) in terms {
40 observable.add_term(coefficient, factors)?;
41 }
42 Ok(observable)
43 }
44
45 pub fn add_term(&mut self, coefficient: f64, mut factors: Vec<PauliTerm>) -> Result<()> {
51 if !coefficient.is_finite() {
52 return Err(PrismError::InvalidParameter {
53 message: format!("observable coefficient {coefficient} is not finite"),
54 });
55 }
56 factors.sort_unstable_by_key(|term| term.qubit);
57 if let Some(pair) = factors
58 .windows(2)
59 .find(|pair| pair[0].qubit == pair[1].qubit)
60 {
61 return Err(PrismError::InvalidParameter {
62 message: format!(
63 "joint Pauli observable has duplicate factor on qubit {}",
64 pair[0].qubit
65 ),
66 });
67 }
68 self.merge_term(coefficient, factors);
69 Ok(())
70 }
71
72 fn merge_term(&mut self, coefficient: f64, factors: Vec<PauliTerm>) {
73 match self
74 .terms
75 .binary_search_by(|(_, existing)| existing.as_slice().cmp(&factors))
76 {
77 Ok(i) => self.terms[i].0 += coefficient,
78 Err(i) => self.terms.insert(i, (coefficient, factors)),
79 }
80 self.grouping = OnceLock::new();
81 }
82
83 pub fn terms(&self) -> &[(f64, Vec<PauliTerm>)] {
85 &self.terms
86 }
87
88 pub fn num_terms(&self) -> usize {
89 self.terms.len()
90 }
91
92 pub fn num_groups(&self) -> usize {
95 self.grouping().groups.len()
96 }
97
98 pub(crate) fn grouping(&self) -> &Grouping {
99 self.grouping.get_or_init(|| compute_grouping(&self.terms))
100 }
101
102 pub fn split_identity(&self) -> (f64, PauliObservable) {
108 let mut offset = 0.0;
109 let mut rest = PauliObservable::new();
110 for (coefficient, string) in &self.terms {
111 if string.is_empty() {
112 offset += coefficient;
113 } else {
114 rest.merge_term(*coefficient, string.clone());
115 }
116 }
117 (offset, rest)
118 }
119
120 pub fn square(&self) -> PauliObservable {
135 let mut accumulated: BTreeMap<Vec<PauliTerm>, f64> = BTreeMap::new();
136 for (left, left_string) in &self.terms {
137 for (right, right_string) in &self.terms {
138 let (phase, product) = multiply_pauli_strings(left_string, right_string);
139 if phase % 2 == 1 {
140 continue;
141 }
142 let sign = if phase == 0 { 1.0 } else { -1.0 };
143 *accumulated.entry(product).or_insert(0.0) += sign * left * right;
144 }
145 }
146 let norm = self.terms.iter().map(|(c, _)| c.abs()).sum::<f64>();
147 let tolerance = f64::EPSILON * norm * norm * self.terms.len().max(1) as f64;
148 let mut squared = PauliObservable::new();
149 for (string, coefficient) in accumulated {
150 if coefficient.abs() > tolerance {
151 squared.merge_term(coefficient, string);
152 }
153 }
154 squared
155 }
156}
157
158fn multiply_pauli_strings(left: &[PauliTerm], right: &[PauliTerm]) -> (u32, Vec<PauliTerm>) {
160 let mut phase = 0u32;
161 let mut product = Vec::with_capacity(left.len() + right.len());
162 let (mut i, mut j) = (0, 0);
163 while i < left.len() && j < right.len() {
164 let (a, b) = (left[i], right[j]);
165 match a.qubit.cmp(&b.qubit) {
166 std::cmp::Ordering::Less => {
167 product.push(a);
168 i += 1;
169 }
170 std::cmp::Ordering::Greater => {
171 product.push(b);
172 j += 1;
173 }
174 std::cmp::Ordering::Equal => {
175 if let Some((step, axis)) = multiply_pauli_axes(a.axis, b.axis) {
176 phase = (phase + step) % 4;
177 product.push(PauliTerm::new(a.qubit, axis));
178 }
179 i += 1;
180 j += 1;
181 }
182 }
183 }
184 product.extend_from_slice(&left[i..]);
185 product.extend_from_slice(&right[j..]);
186 (phase, product)
187}
188
189fn multiply_pauli_axes(a: PauliAxis, b: PauliAxis) -> Option<(u32, PauliAxis)> {
192 use PauliAxis::{X, Y, Z};
193 match (a, b) {
194 (X, Y) => Some((1, Z)),
195 (Y, Z) => Some((1, X)),
196 (Z, X) => Some((1, Y)),
197 (Y, X) => Some((3, Z)),
198 (Z, Y) => Some((3, X)),
199 (X, Z) => Some((3, Y)),
200 _ => None,
201 }
202}
203
204impl std::ops::Add for PauliObservable {
205 type Output = PauliObservable;
206
207 fn add(mut self, rhs: PauliObservable) -> PauliObservable {
208 for (coefficient, factors) in rhs.terms {
209 self.merge_term(coefficient, factors);
210 }
211 self
212 }
213}
214
215impl std::ops::Sub for PauliObservable {
216 type Output = PauliObservable;
217
218 fn sub(self, rhs: PauliObservable) -> PauliObservable {
219 self + (-rhs)
220 }
221}
222
223impl std::ops::Neg for PauliObservable {
224 type Output = PauliObservable;
225
226 fn neg(mut self) -> PauliObservable {
227 for (coefficient, _) in &mut self.terms {
228 *coefficient = -*coefficient;
229 }
230 self
231 }
232}
233
234impl std::ops::Mul<f64> for PauliObservable {
235 type Output = PauliObservable;
236
237 fn mul(mut self, rhs: f64) -> PauliObservable {
238 for (coefficient, _) in &mut self.terms {
239 *coefficient *= rhs;
240 }
241 self
242 }
243}
244
245#[derive(Debug, Clone)]
247pub struct ObservableExpectation {
248 pub mean: f64,
250 pub variance: Option<f64>,
258 pub group_variances: Option<Vec<f64>>,
260 pub std_error: Option<f64>,
263 pub metadata: RunMetadata,
264}
265
266#[derive(Debug, Clone)]
268pub(crate) struct Grouping {
269 pub(crate) groups: Vec<QwcGroup>,
270}
271
272#[derive(Debug, Clone)]
276pub(crate) struct QwcGroup {
277 pub(crate) term_indices: Vec<usize>,
278 axis_x: Vec<u64>,
279 axis_z: Vec<u64>,
280}
281
282impl QwcGroup {
283 fn accepts(&self, tx: &[u64], tz: &[u64]) -> bool {
284 for w in 0..tx.len() {
285 let shared = (tx[w] | tz[w]) & (self.axis_x[w] | self.axis_z[w]);
286 if ((tx[w] ^ self.axis_x[w]) | (tz[w] ^ self.axis_z[w])) & shared != 0 {
287 return false;
288 }
289 }
290 true
291 }
292
293 fn absorb(&mut self, index: usize, tx: &[u64], tz: &[u64]) {
294 for w in 0..tx.len() {
295 self.axis_x[w] |= tx[w];
296 self.axis_z[w] |= tz[w];
297 }
298 self.term_indices.push(index);
299 }
300
301 pub(crate) fn is_z_only(&self) -> bool {
304 self.axis_x.iter().all(|&word| word == 0)
305 }
306
307 pub(crate) fn rotation_masks(&self) -> (usize, usize) {
311 let (x, z) = (self.axis_x[0], self.axis_z[0]);
312 ((x & !z) as usize, (x & z) as usize)
313 }
314}
315
316fn compute_grouping(terms: &[(f64, Vec<PauliTerm>)]) -> Grouping {
317 let max_qubit = terms
318 .iter()
319 .flat_map(|(_, factors)| factors.iter())
320 .map(|term| term.qubit)
321 .max();
322 let num_words = max_qubit.map_or(0, |q| q / 64 + 1);
323
324 let mut order: Vec<usize> = (0..terms.len())
326 .filter(|&i| !terms[i].1.is_empty())
327 .collect();
328 order.sort_by(|&a, &b| terms[b].1.len().cmp(&terms[a].1.len()).then(a.cmp(&b)));
329
330 let mut groups: Vec<QwcGroup> = Vec::new();
331 let mut tx = vec![0u64; num_words];
332 let mut tz = vec![0u64; num_words];
333 for &index in &order {
334 tx.fill(0);
335 tz.fill(0);
336 for term in &terms[index].1 {
337 let bit = 1u64 << (term.qubit % 64);
338 match term.axis {
339 PauliAxis::X => tx[term.qubit / 64] |= bit,
340 PauliAxis::Z => tz[term.qubit / 64] |= bit,
341 PauliAxis::Y => {
342 tx[term.qubit / 64] |= bit;
343 tz[term.qubit / 64] |= bit;
344 }
345 }
346 }
347 match groups.iter_mut().find(|group| group.accepts(&tx, &tz)) {
348 Some(group) => group.absorb(index, &tx, &tz),
349 None => groups.push(QwcGroup {
350 term_indices: vec![index],
351 axis_x: tx.clone(),
352 axis_z: tz.clone(),
353 }),
354 }
355 }
356 Grouping { groups }
357}
358
359pub(crate) fn weighted_group_moments(
370 state: &[Complex64],
371 zmasks: &[usize],
372 coefficients: &[f64],
373 norm: f64,
374) -> (f64, f64) {
375 if norm == 0.0 {
376 return (0.0, 0.0);
377 }
378
379 let accumulate = |acc: &mut (f64, f64), base: usize, chunk: &[Complex64]| {
380 for (b, block) in chunk.chunks(MOMENT_BLOCK).enumerate() {
381 let block_base = base + b * MOMENT_BLOCK;
382 let mut h = [0.0f64; MOMENT_BLOCK];
383 for (&zmask, &c) in zmasks.iter().zip(coefficients) {
384 let flip = u64::from((block_base & zmask).count_ones() & 1) << 63;
385 h[zmask & (MOMENT_BLOCK - 1)] += f64::from_bits(c.to_bits() ^ flip);
386 }
387 let mut half = 1;
388 while half < MOMENT_BLOCK {
389 for pair in h.chunks_exact_mut(2 * half) {
390 let (lo, hi) = pair.split_at_mut(half);
391 for (a, b) in lo.iter_mut().zip(hi) {
392 (*a, *b) = (*a + *b, *a - *b);
393 }
394 }
395 half *= 2;
396 }
397 for (amp, &hj) in block.iter().zip(&h) {
398 let weighted = amp.norm_sqr() * hj;
399 acc.0 += weighted;
400 acc.1 += weighted * hj;
401 }
402 }
403 };
404
405 #[cfg(feature = "parallel")]
406 if state.len() >= crate::backend::MIN_PAR_REDUCE_ELEMS {
407 use rayon::prelude::*;
408 let chunk = crate::backend::MIN_PAR_ELEMS.max(MOMENT_BLOCK);
409 let (m1, m2) = state
410 .par_chunks(chunk)
411 .enumerate()
412 .fold(
413 || (0.0, 0.0),
414 |mut acc, (c, block)| {
415 accumulate(&mut acc, c * chunk, block);
416 acc
417 },
418 )
419 .reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1));
420 return (m1 / norm, m2 / norm);
421 }
422
423 let mut acc = (0.0, 0.0);
424 accumulate(&mut acc, 0, state);
425 (acc.0 / norm, acc.1 / norm)
426}
427
428const MOMENT_BLOCK: usize = 64;
432
433pub(crate) fn rotate_to_z_basis(
444 state: &[Complex64],
445 out: &mut Vec<Complex64>,
446 x_bits: usize,
447 y_bits: usize,
448) {
449 out.resize(state.len(), Complex64::ZERO);
450 let block = ROTATION_BLOCK.min(state.len());
451 let levels = |range: std::ops::Range<usize>| {
452 range
453 .filter(|&q| (x_bits | y_bits) >> q & 1 == 1)
454 .map(|q| (q, y_bits >> q & 1 == 1))
455 .collect::<Vec<_>>()
456 };
457 let low = levels(0..block.trailing_zeros() as usize);
458 let high = levels(block.trailing_zeros() as usize..state.len().trailing_zeros() as usize);
459
460 let fill = |(dst, src): (&mut [Complex64], &[Complex64])| {
461 dst.copy_from_slice(src);
462 rotate_levels(dst, &low);
463 };
464
465 #[cfg(feature = "parallel")]
466 if state.len() >= 1 << crate::backend::PARALLEL_THRESHOLD_QUBITS {
467 use rayon::prelude::*;
468 out.par_chunks_mut(block)
469 .zip(state.par_chunks(block))
470 .for_each(fill);
471 for pair in high.chunks(2) {
472 rotate_pair_par(out, pair);
473 }
474 return;
475 }
476
477 out.chunks_mut(block)
478 .zip(state.chunks(block))
479 .for_each(fill);
480 rotate_levels(out, &high);
481}
482
483const ROTATION_BLOCK: usize = 1 << 10;
485
486#[inline(always)]
489fn twiddle<const Y: bool>(b: Complex64) -> Complex64 {
490 if Y { Complex64::new(b.im, -b.re) } else { b }
491}
492
493#[inline(always)]
494fn butterfly2<const Y: bool>(lo: &mut [Complex64], hi: &mut [Complex64]) {
495 for (a, b) in lo.iter_mut().zip(hi) {
496 let t = twiddle::<Y>(*b);
497 (*a, *b) = (*a + t, *a - t);
498 }
499}
500
501#[inline(always)]
504fn quad<const Y1: bool, const Y2: bool>(
505 a: &mut Complex64,
506 b: &mut Complex64,
507 c: &mut Complex64,
508 d: &mut Complex64,
509) {
510 let (tb, td) = (twiddle::<Y1>(*b), twiddle::<Y1>(*d));
511 let (a1, b1, c1, d1) = (*a + tb, *a - tb, *c + td, *c - td);
512 let (tc, td) = (twiddle::<Y2>(c1), twiddle::<Y2>(d1));
513 (*a, *b, *c, *d) = (a1 + tc, b1 + td, a1 - tc, b1 - td);
514}
515
516#[inline(always)]
517fn butterfly4<const Y1: bool, const Y2: bool>(
518 a: &mut [Complex64],
519 b: &mut [Complex64],
520 c: &mut [Complex64],
521 d: &mut [Complex64],
522) {
523 let n = a.len();
524 let (b, c, d) = (&mut b[..n], &mut c[..n], &mut d[..n]);
525 for i in 0..n {
526 quad::<Y1, Y2>(&mut a[i], &mut b[i], &mut c[i], &mut d[i]);
527 }
528}
529
530fn rotate_levels(amps: &mut [Complex64], levels: &[(usize, bool)]) {
533 let mut rest = levels;
534 while let Some((&(q1, y1), tail)) = rest.split_first() {
535 match tail.first() {
536 Some(&(q2, y2)) if q1 >= MIN_RADIX4_QUBIT || (q1, q2) == (0, 1) => {
537 match (y1, y2) {
538 (false, false) => rotate4::<false, false>(amps, q1, q2),
539 (false, true) => rotate4::<false, true>(amps, q1, q2),
540 (true, false) => rotate4::<true, false>(amps, q1, q2),
541 (true, true) => rotate4::<true, true>(amps, q1, q2),
542 }
543 rest = &tail[1..];
544 }
545 _ => {
546 if y1 {
547 rotate2::<true>(amps, q1);
548 } else {
549 rotate2::<false>(amps, q1);
550 }
551 rest = tail;
552 }
553 }
554 }
555}
556
557const MIN_RADIX4_QUBIT: usize = 3;
561
562fn rotate2<const Y: bool>(amps: &mut [Complex64], q: usize) {
563 for pair in amps.chunks_exact_mut(2 << q) {
564 let (lo, hi) = pair.split_at_mut(1 << q);
565 butterfly2::<Y>(lo, hi);
566 }
567}
568
569fn rotate4<const Y1: bool, const Y2: bool>(amps: &mut [Complex64], q1: usize, q2: usize) {
570 if (q1, q2) == (0, 1) {
571 for [a, b, c, d] in amps.as_chunks_mut::<4>().0 {
572 quad::<Y1, Y2>(a, b, c, d);
573 }
574 return;
575 }
576 for quad in amps.chunks_exact_mut(2 << q2) {
577 let (lo, hi) = quad.split_at_mut(1 << q2);
578 for (l, h) in lo
579 .chunks_exact_mut(2 << q1)
580 .zip(hi.chunks_exact_mut(2 << q1))
581 {
582 let (a, b) = l.split_at_mut(1 << q1);
583 let (c, d) = h.split_at_mut(1 << q1);
584 butterfly4::<Y1, Y2>(a, b, c, d);
585 }
586 }
587}
588
589#[cfg(feature = "parallel")]
593fn rotate_pair_par(amps: &mut [Complex64], levels: &[(usize, bool)]) {
594 use rayon::prelude::*;
595 let run = crate::backend::MIN_PAR_ELEMS;
596 match *levels {
597 [(q, y)] => amps.par_chunks_mut(2 << q).for_each(|pair| {
598 let (lo, hi) = pair.split_at_mut(1 << q);
599 lo.par_chunks_mut(run)
600 .zip(hi.par_chunks_mut(run))
601 .for_each(|(lo, hi)| {
602 if y {
603 butterfly2::<true>(lo, hi)
604 } else {
605 butterfly2::<false>(lo, hi)
606 }
607 });
608 }),
609 [(q1, y1), (q2, y2)] => amps.par_chunks_mut(2 << q2).for_each(|quad| {
610 let (lo, hi) = quad.split_at_mut(1 << q2);
611 lo.par_chunks_mut(2 << q1)
612 .zip(hi.par_chunks_mut(2 << q1))
613 .for_each(|(l, h)| {
614 let (a, b) = l.split_at_mut(1 << q1);
615 let (c, d) = h.split_at_mut(1 << q1);
616 a.par_chunks_mut(run)
617 .zip(b.par_chunks_mut(run))
618 .zip(c.par_chunks_mut(run))
619 .zip(d.par_chunks_mut(run))
620 .for_each(|(((a, b), c), d)| match (y1, y2) {
621 (false, false) => butterfly4::<false, false>(a, b, c, d),
622 (false, true) => butterfly4::<false, true>(a, b, c, d),
623 (true, false) => butterfly4::<true, false>(a, b, c, d),
624 (true, true) => butterfly4::<true, true>(a, b, c, d),
625 });
626 });
627 }),
628 _ => unreachable!(),
629 }
630}
631
632#[cfg(test)]
633#[path = "observable_tests.rs"]
634mod tests;
635
636pub(crate) fn validate_observable(observable: &[PauliTerm], num_qubits: usize) -> Result<()> {
642 let mut seen = vec![false; num_qubits];
643 for term in observable {
644 if term.qubit >= num_qubits {
645 return Err(PrismError::InvalidQubit {
646 index: term.qubit,
647 register_size: num_qubits,
648 });
649 }
650 if seen[term.qubit] {
651 return Err(PrismError::InvalidParameter {
652 message: format!(
653 "joint Pauli observable has duplicate factor on qubit {}",
654 term.qubit
655 ),
656 });
657 }
658 seen[term.qubit] = true;
659 }
660 Ok(())
661}
662
663pub(crate) fn pauli_masks(
666 observable: &[PauliTerm],
667 num_qubits: usize,
668) -> Result<(usize, usize, u32)> {
669 let mut xmask = 0usize;
670 let mut zmask = 0usize;
671 let mut num_y = 0u32;
672 let mut seen = vec![false; num_qubits];
673 for term in observable {
674 if term.qubit >= num_qubits {
675 return Err(PrismError::InvalidQubit {
676 index: term.qubit,
677 register_size: num_qubits,
678 });
679 }
680 if seen[term.qubit] {
681 return Err(PrismError::InvalidParameter {
682 message: format!(
683 "joint Pauli observable has duplicate factor on qubit {}",
684 term.qubit
685 ),
686 });
687 }
688 seen[term.qubit] = true;
689 let bit = 1usize << term.qubit;
690 match term.axis {
691 PauliAxis::X => xmask |= bit,
692 PauliAxis::Z => zmask |= bit,
693 PauliAxis::Y => {
694 xmask |= bit;
695 zmask |= bit;
696 num_y += 1;
697 }
698 }
699 }
700 Ok((xmask, zmask, num_y))
701}
702
703#[cfg(feature = "parallel")]
708const SANDWICH_MIN_PAR_QUBITS: usize = 16;
709
710#[inline]
720pub(crate) fn pauli_sandwich(
721 lambda: &[Complex64],
722 phi: &[Complex64],
723 xmask: usize,
724 zmask: usize,
725 num_y: u32,
726) -> Complex64 {
727 let term = |j: usize, amp: Complex64| {
728 let partner = lambda[j ^ xmask];
729 let sign = if (j & zmask).count_ones() & 1 == 1 {
730 -1.0
731 } else {
732 1.0
733 };
734 partner.conj() * amp * sign
735 };
736
737 #[cfg(feature = "parallel")]
738 let acc: Complex64 = if phi.len() >= (1 << SANDWICH_MIN_PAR_QUBITS) {
739 use rayon::prelude::*;
740 phi.par_iter()
741 .enumerate()
742 .map(|(j, &)| term(j, amp))
743 .sum()
744 } else {
745 phi.iter().enumerate().map(|(j, &)| term(j, amp)).sum()
746 };
747 #[cfg(not(feature = "parallel"))]
748 let acc: Complex64 = phi.iter().enumerate().map(|(j, &)| term(j, amp)).sum();
749
750 acc * i_pow(num_y)
751}
752
753pub(crate) fn pauli_sandwiches_from_masks(
761 lambda: &[Complex64],
762 phi: &[Complex64],
763 masks: &[(usize, usize, u32)],
764) -> Vec<Complex64> {
765 if masks.len() < 2 {
766 return masks
767 .iter()
768 .map(|&(xmask, zmask, num_y)| pauli_sandwich(lambda, phi, xmask, zmask, num_y))
769 .collect();
770 }
771
772 let z_only: Vec<usize> = masks
773 .iter()
774 .filter(|&&(xmask, _, _)| xmask == 0)
775 .map(|&(_, zmask, _)| zmask)
776 .collect();
777 let general: Vec<(usize, usize)> = masks
778 .iter()
779 .filter(|&&(xmask, _, _)| xmask != 0)
780 .map(|&(xmask, zmask, _)| (xmask, zmask))
781 .collect();
782
783 let accumulate = |z_acc: &mut [Complex64], g_acc: &mut [Complex64], base: usize, len: usize| {
784 for j in base..base + len {
785 let amp = phi[j];
786 let aligned = lambda[j].conj() * amp;
787 for (slot, &zmask) in z_acc.iter_mut().zip(z_only.iter()) {
788 *slot += if (j & zmask).count_ones() & 1 == 1 {
789 -aligned
790 } else {
791 aligned
792 };
793 }
794 for (slot, &(xmask, zmask)) in g_acc.iter_mut().zip(general.iter()) {
795 let partner = lambda[j ^ xmask];
796 let sign = if (j & zmask).count_ones() & 1 == 1 {
797 -1.0
798 } else {
799 1.0
800 };
801 *slot += partner.conj() * amp * sign;
802 }
803 }
804 };
805
806 let zeros = || {
807 (
808 vec![Complex64::new(0.0, 0.0); z_only.len()],
809 vec![Complex64::new(0.0, 0.0); general.len()],
810 )
811 };
812 let (mut z_sum, mut g_sum) = zeros();
813
814 #[cfg(feature = "parallel")]
815 if phi.len() >= (1 << SANDWICH_MIN_PAR_QUBITS) {
816 use rayon::prelude::*;
817 let chunk = crate::backend::MIN_PAR_ELEMS;
818 let (z, g) = phi
819 .par_chunks(chunk)
820 .enumerate()
821 .fold(zeros, |mut acc, (c, block)| {
822 accumulate(&mut acc.0, &mut acc.1, c * chunk, block.len());
823 acc
824 })
825 .reduce(zeros, |mut a, b| {
826 for (slot, v) in a.0.iter_mut().zip(b.0) {
827 *slot += v;
828 }
829 for (slot, v) in a.1.iter_mut().zip(b.1) {
830 *slot += v;
831 }
832 a
833 });
834 return finish_sandwiches(masks, &z, &g);
835 }
836
837 accumulate(&mut z_sum, &mut g_sum, 0, phi.len());
838 finish_sandwiches(masks, &z_sum, &g_sum)
839}
840
841fn finish_sandwiches(
844 masks: &[(usize, usize, u32)],
845 z_sum: &[Complex64],
846 g_sum: &[Complex64],
847) -> Vec<Complex64> {
848 let (mut zi, mut gi) = (0, 0);
849 masks
850 .iter()
851 .map(|&(xmask, _, num_y)| {
852 let raw = if xmask == 0 {
853 zi += 1;
854 z_sum[zi - 1]
855 } else {
856 gi += 1;
857 g_sum[gi - 1]
858 };
859 raw * i_pow(num_y)
860 })
861 .collect()
862}
863
864#[inline]
866pub(crate) fn i_pow(num_y: u32) -> Complex64 {
867 match num_y % 4 {
868 0 => Complex64::new(1.0, 0.0),
869 1 => Complex64::new(0.0, 1.0),
870 2 => Complex64::new(-1.0, 0.0),
871 _ => Complex64::new(0.0, -1.0),
872 }
873}
874
875pub(crate) fn pauli_expectation_from_masks(
878 state: &[Complex64],
879 xmask: usize,
880 zmask: usize,
881 num_y: u32,
882 norm: f64,
883) -> f64 {
884 if norm == 0.0 {
885 return 0.0;
886 }
887 pauli_sandwich(state, state, xmask, zmask, num_y).re / norm
888}
889
890pub(crate) fn pauli_expectations_from_masks(
899 state: &[Complex64],
900 masks: &[(usize, usize, u32)],
901) -> (Vec<f64>, f64) {
902 if masks.is_empty() {
903 return (Vec::new(), crate::backend::state_norm_sqr(state));
904 }
905 let (z_sum, g_sum, norm) = expectation_sums(state, masks);
906 if norm == 0.0 {
907 return (vec![0.0; masks.len()], norm);
908 }
909 (finish_expectations(masks, &z_sum, &g_sum, norm), norm)
910}
911
912fn expectation_sums(
915 state: &[Complex64],
916 masks: &[(usize, usize, u32)],
917) -> (Vec<f64>, Vec<Complex64>, f64) {
918 let z_only: Vec<usize> = masks
919 .iter()
920 .filter(|&&(xmask, _, _)| xmask == 0)
921 .map(|&(_, zmask, _)| zmask)
922 .collect();
923 let general: Vec<(usize, usize)> = masks
924 .iter()
925 .filter(|&&(xmask, _, _)| xmask != 0)
926 .map(|&(xmask, zmask, _)| (xmask, zmask))
927 .collect();
928
929 let accumulate =
930 |z_acc: &mut [f64], g_acc: &mut [Complex64], norm: &mut f64, base: usize, len: usize| {
931 for j in base..base + len {
932 let amp = state[j];
933 let n2 = amp.norm_sqr();
934 *norm += n2;
935 for (slot, &zmask) in z_acc.iter_mut().zip(z_only.iter()) {
936 *slot += if (j & zmask).count_ones() & 1 == 1 {
937 -n2
938 } else {
939 n2
940 };
941 }
942 for (slot, &(xmask, zmask)) in g_acc.iter_mut().zip(general.iter()) {
943 let partner = state[j ^ xmask];
944 let sign = if (j & zmask).count_ones() & 1 == 1 {
945 -1.0
946 } else {
947 1.0
948 };
949 *slot += partner.conj() * amp * sign;
950 }
951 }
952 };
953
954 let zeros = || {
955 (
956 vec![0.0f64; z_only.len()],
957 vec![Complex64::new(0.0, 0.0); general.len()],
958 0.0f64,
959 )
960 };
961
962 #[cfg(feature = "parallel")]
963 if state.len() >= crate::backend::MIN_PAR_REDUCE_ELEMS {
964 use rayon::prelude::*;
965 let chunk = crate::backend::MIN_PAR_ELEMS;
966 return state
967 .par_chunks(chunk)
968 .enumerate()
969 .fold(zeros, |mut acc, (c, block)| {
970 accumulate(&mut acc.0, &mut acc.1, &mut acc.2, c * chunk, block.len());
971 acc
972 })
973 .reduce(zeros, |mut a, b| {
974 for (slot, v) in a.0.iter_mut().zip(b.0) {
975 *slot += v;
976 }
977 for (slot, v) in a.1.iter_mut().zip(b.1) {
978 *slot += v;
979 }
980 a.2 += b.2;
981 a
982 });
983 }
984
985 let mut sums = zeros();
986 accumulate(&mut sums.0, &mut sums.1, &mut sums.2, 0, state.len());
987 sums
988}
989
990pub(crate) fn finish_expectations(
996 masks: &[(usize, usize, u32)],
997 z_sum: &[f64],
998 g_sum: &[Complex64],
999 norm: f64,
1000) -> Vec<f64> {
1001 let (mut zi, mut gi) = (0, 0);
1002 masks
1003 .iter()
1004 .map(|&(xmask, _, num_y)| {
1005 if xmask == 0 {
1006 zi += 1;
1007 z_sum[zi - 1] / norm
1008 } else {
1009 gi += 1;
1010 (g_sum[gi - 1] * i_pow(num_y)).re / norm
1011 }
1012 })
1013 .collect()
1014}