Skip to main content

bloq_utils/
pauli.rs

1use std::ops::{BitAnd, BitOr, BitXor, BitXorAssign};
2
3use binar::{BitVec, Bitwise, BitwiseMut, BitwisePair, BitwisePairMut};
4use strum::{Display, EnumString};
5use thiserror::Error;
6
7/// Error returned by the fallible conversions into [`Pauli`] and [`Basis`].
8#[derive(Debug, Clone, PartialEq, Eq, Error)]
9pub enum PauliError {
10    /// The [`Pauli`] is `I` or `Y`, which have no [`Basis`] counterpart.
11    #[error("cannot convert Pauli {0} to Basis")]
12    NotBasis(Pauli),
13    /// The byte is outside the `0..=3` range that encodes a [`Pauli`].
14    #[error("invalid u8 value for Pauli: {0}")]
15    InvalidByte(u8),
16    /// The character is not one of `I`, `_`, `X`, `Y`, `Z`.
17    #[error("invalid char for Pauli: {0}")]
18    InvalidChar(char),
19}
20
21/// Pauli `X` or `Z` basis (excludes `Y` and identity).
22#[derive(
23    Debug,
24    Clone,
25    Copy,
26    PartialEq,
27    Eq,
28    Hash,
29    PartialOrd,
30    Ord,
31    Display,
32    EnumString,
33    serde::Serialize,
34    serde::Deserialize,
35)]
36pub enum Basis {
37    /// X basis.
38    X,
39    /// Z basis.
40    Z,
41}
42
43impl Basis {
44    /// Returns the complementary basis (`X` <-> `Z`).
45    pub const fn flip(self) -> Self {
46        match self {
47            Self::X => Self::Z,
48            Self::Z => Self::X,
49        }
50    }
51}
52
53impl TryFrom<Pauli> for Basis {
54    type Error = PauliError;
55
56    fn try_from(value: Pauli) -> Result<Self, Self::Error> {
57        match value {
58            Pauli::X => Ok(Basis::X),
59            Pauli::Z => Ok(Basis::Z),
60            _ => Err(PauliError::NotBasis(value)),
61        }
62    }
63}
64
65/// Pauli basis: `X`, `Y`, or `Z` (excludes identity).
66#[derive(
67    Debug,
68    Clone,
69    Copy,
70    PartialEq,
71    Eq,
72    PartialOrd,
73    Ord,
74    Hash,
75    EnumString,
76    Display,
77    serde::Serialize,
78    serde::Deserialize,
79)]
80pub enum PauliBasis {
81    /// X basis.
82    X,
83    /// Y basis.
84    Y,
85    /// Z basis.
86    Z,
87}
88
89impl From<Basis> for PauliBasis {
90    fn from(basis: Basis) -> Self {
91        match basis {
92            Basis::X => PauliBasis::X,
93            Basis::Z => PauliBasis::Z,
94        }
95    }
96}
97
98/// A single-qubit Pauli operator.
99///
100/// The discriminants are the XZ symplectic encoding (`X` sets bit 0, `Z` sets
101/// bit 1), so the bit algebra in [`BitOr`]/[`BitXor`]/[`BitAnd`] and
102/// [`PauliString`] depends on these exact values. Keep them fixed.
103#[repr(u8)]
104#[derive(
105    Debug,
106    Clone,
107    Copy,
108    PartialEq,
109    Eq,
110    Hash,
111    EnumString,
112    Display,
113    serde::Serialize,
114    serde::Deserialize,
115)]
116pub enum Pauli {
117    /// Identity.
118    I = 0,
119    /// Pauli X.
120    X = 1,
121    /// Pauli Z.
122    Z = 2,
123    /// Pauli Y.
124    Y = 3,
125}
126
127impl From<Basis> for Pauli {
128    fn from(basis: Basis) -> Self {
129        match basis {
130            Basis::X => Pauli::X,
131            Basis::Z => Pauli::Z,
132        }
133    }
134}
135
136impl From<PauliBasis> for Pauli {
137    fn from(pb: PauliBasis) -> Self {
138        match pb {
139            PauliBasis::X => Pauli::X,
140            PauliBasis::Y => Pauli::Y,
141            PauliBasis::Z => Pauli::Z,
142        }
143    }
144}
145
146impl Pauli {
147    const fn from_xz(x: bool, z: bool) -> Self {
148        match (x, z) {
149            (false, false) => Self::I,
150            (true, false) => Self::X,
151            (false, true) => Self::Z,
152            (true, true) => Self::Y,
153        }
154    }
155
156    /// Swaps `X` and `Z`; leaves `I` and `Y` unchanged.
157    pub const fn flip(self) -> Self {
158        match self {
159            Self::X => Self::Z,
160            Self::Z => Self::X,
161            _ => self,
162        }
163    }
164
165    /// Iterates the `X` and `Z` components present in this Pauli.
166    pub fn iter_xz(self) -> impl Iterator<Item = Self> {
167        [Self::X, Self::Z].into_iter().filter(move |&p| p & self)
168    }
169
170    /// Two Paulis anticommute iff both are non-identity and differ.
171    pub const fn anticommutes(self, other: Self) -> bool {
172        !matches!(self, Self::I) && !matches!(other, Self::I) && self as u8 != other as u8
173    }
174}
175
176impl BitOr for Pauli {
177    type Output = Pauli;
178
179    fn bitor(self, rhs: Self) -> Self::Output {
180        let int_or = self as u8 | rhs as u8;
181        Pauli::try_from(int_or).expect("OR of two Paulis is always a valid Pauli")
182    }
183}
184
185impl BitXor for Pauli {
186    type Output = Pauli;
187
188    fn bitxor(self, rhs: Self) -> Self::Output {
189        let int_xor = self as u8 ^ rhs as u8;
190        Pauli::try_from(int_xor).expect("XOR of two Paulis is always a valid Pauli")
191    }
192}
193
194impl BitAnd for Pauli {
195    type Output = bool;
196
197    fn bitand(self, rhs: Self) -> Self::Output {
198        let int_and = self as u8 & rhs as u8;
199        int_and != 0
200    }
201}
202
203impl TryFrom<u8> for Pauli {
204    type Error = PauliError;
205
206    fn try_from(value: u8) -> Result<Self, Self::Error> {
207        match value {
208            0 => Ok(Pauli::I),
209            1 => Ok(Pauli::X),
210            2 => Ok(Pauli::Z),
211            3 => Ok(Pauli::Y),
212            _ => Err(PauliError::InvalidByte(value)),
213        }
214    }
215}
216
217impl From<Pauli> for u8 {
218    fn from(value: Pauli) -> Self {
219        value as u8
220    }
221}
222
223impl TryFrom<char> for Pauli {
224    type Error = PauliError;
225
226    fn try_from(value: char) -> Result<Self, Self::Error> {
227        match value {
228            'I' | '_' => Ok(Pauli::I),
229            'X' => Ok(Pauli::X),
230            'Y' => Ok(Pauli::Y),
231            'Z' => Ok(Pauli::Z),
232            _ => Err(PauliError::InvalidChar(value)),
233        }
234    }
235}
236
237/// A dense unsigned Pauli operator over `len` qubits, stored as XZ bit vectors.
238///
239/// Phaseless, with each site read as the *Hermitian* single-qubit Pauli it
240/// names — a site carrying both bits is `Y = iXZ`, not the bare product `XZ`.
241/// A product of Hermitian factors on distinct sites is Hermitian, so every
242/// `PauliString` is a valid measurement observable or rotation axis by
243/// construction; there is no phase to make it otherwise.
244/// Operations between different widths treat missing trailing sites as
245/// identities; XOR keeps the wider width.
246/// Parse strings of `I`, `_`, `X`, `Y`, and `Z` with [`str::parse`];
247/// [`Display`](std::fmt::Display) renders identity as `_`.
248#[derive(Clone, PartialEq, Eq)]
249pub struct PauliString {
250    xs: BitVec,
251    zs: BitVec,
252}
253
254/// A packed Hermitian [`PauliString`] with an exact phase `i^k`.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct PhasedPauliString {
257    /// Hermitian body. Mutating it does not change the phase.
258    pub paulis: PauliString,
259    phase: u8,
260}
261
262impl PhasedPauliString {
263    /// Attaches `i^phase` to `paulis`, reducing the exponent modulo four.
264    pub fn new(paulis: PauliString, phase: u8) -> Self {
265        Self {
266            paulis,
267            phase: phase & 3,
268        }
269    }
270
271    /// Creates `+paulis`.
272    pub fn positive(paulis: PauliString) -> Self {
273        Self::new(paulis, 0)
274    }
275
276    /// Returns the phase exponent in `0..=3`.
277    pub const fn phase(&self) -> u8 {
278        self.phase
279    }
280
281    /// Adds a phase exponent modulo four.
282    pub fn shift_phase(&mut self, exponent: u8) {
283        self.phase = self.phase.wrapping_add(exponent) & 3;
284    }
285
286    /// Multiplies by `other`, retaining the exact phase.
287    pub fn multiply_assign(&mut self, other: &Self) {
288        self.shift_phase(other.phase + self.paulis.product_phase(&other.paulis));
289        self.paulis ^= &other.paulis;
290    }
291
292    /// Conjugates one column by Hadamard, including `HYH = -Y`.
293    pub fn conjugate_h(&mut self, column: usize) {
294        let pauli = self.paulis.get(column);
295        self.paulis.set(column, pauli.flip());
296        if pauli == Pauli::Y {
297            self.shift_phase(2);
298        }
299    }
300
301    /// Returns the sign of a Hermitian operator (`true` means negative).
302    pub const fn hermitian_sign(&self) -> Option<bool> {
303        match self.phase {
304            0 => Some(false),
305            2 => Some(true),
306            _ => None,
307        }
308    }
309}
310
311impl PauliString {
312    /// Creates the identity Pauli string over `num_qubits` qubits.
313    pub fn new(num_qubits: usize) -> Self {
314        Self {
315            xs: BitVec::zeros(num_qubits),
316            zs: BitVec::zeros(num_qubits),
317        }
318    }
319
320    /// Creates the string with `pauli` at `index` and identity everywhere else.
321    ///
322    /// # Panics
323    ///
324    /// Panics if `index >= num_qubits`.
325    ///
326    /// # Examples
327    ///
328    /// ```
329    /// use bloq_utils::{Pauli, PauliString};
330    ///
331    /// let observable = PauliString::single(4, 2, Pauli::Y);
332    /// assert_eq!(observable.to_string(), "__Y_");
333    /// ```
334    pub fn single(num_qubits: usize, index: usize, pauli: Pauli) -> Self {
335        let mut paulis = PauliString::new(num_qubits);
336        paulis.set(index, pauli);
337        paulis
338    }
339
340    /// Collects `(index, pauli)` terms into a string over `num_qubits` qubits.
341    ///
342    /// Terms are *assignments*, not factors: a repeated index keeps the last
343    /// Pauli given for it rather than multiplying the two.
344    ///
345    /// # Panics
346    ///
347    /// Panics if any index is `>= num_qubits`.
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use bloq_utils::{Pauli, PauliString};
353    ///
354    /// let stabilizer = PauliString::from_terms(3, [(0, Pauli::Z), (2, Pauli::Z)]);
355    /// assert_eq!(stabilizer.to_string(), "Z_Z");
356    /// ```
357    pub fn from_terms(num_qubits: usize, terms: impl IntoIterator<Item = (usize, Pauli)>) -> Self {
358        let mut paulis = PauliString::new(num_qubits);
359        for (index, pauli) in terms {
360            paulis.set(index, pauli);
361        }
362        paulis
363    }
364
365    /// Returns `true` if `self` and `other` commute as operators.
366    ///
367    /// Two Pauli strings anticommute exactly when they anticommute on an odd
368    /// number of sites, which the symplectic form `⟨x, other.z⟩ + ⟨z, other.x⟩`
369    /// counts mod 2.
370    ///
371    /// The two strings need not name the same number of qubits. Where one runs
372    /// out the other is being compared against implicit identities, which
373    /// commute with everything — so the fold below stops at the shorter operand
374    /// rather than requiring equal widths. Going through the words directly is
375    /// also what avoids `BitVec::dot`, whose length assertion holds in release
376    /// builds and would turn a width mismatch into a panic.
377    ///
378    /// # Examples
379    ///
380    /// ```
381    /// use bloq_utils::PauliString;
382    ///
383    /// let xx = PauliString::try_from("XX")?;
384    /// let zz = PauliString::try_from("ZZ")?;
385    /// let zi = PauliString::try_from("Z_")?;
386    /// assert!(xx.commutes_with(&zz));   // two anticommuting sites
387    /// assert!(!xx.commutes_with(&zi));  // one
388    ///
389    /// // Widths may differ; the shorter string ends in implicit identities.
390    /// let x_wide = PauliString::try_from("X___")?;
391    /// assert!(!x_wide.commutes_with(&PauliString::try_from("Z")?));
392    /// assert!(x_wide.commutes_with(&PauliString::try_from("_Z")?));
393    /// # Ok::<(), bloq_utils::PauliError>(())
394    /// ```
395    pub fn commutes_with(&self, other: &Self) -> bool {
396        /// Parity of `⟨a, b⟩` over the words the two operands share.
397        fn dot_parity(a: &[u64], b: &[u64]) -> bool {
398            a.iter().zip(b).fold(false, |parity, (&left, &right)| {
399                parity ^ ((left & right).count_ones() % 2 != 0)
400            })
401        }
402        dot_parity(self.x_words(), other.z_words()) == dot_parity(self.z_words(), other.x_words())
403    }
404
405    /// Returns the power of `i` contributed by multiplying these strings.
406    /// Missing sites are identities.
407    pub fn product_phase(&self, other: &Self) -> u8 {
408        self.x_words()
409            .iter()
410            .zip(self.z_words())
411            .zip(other.x_words().iter().zip(other.z_words()))
412            .fold(0, |phase, ((&ax, &az), (&bx, &bz))| {
413                let (ax_only, ay, az_only) = (ax & !az, ax & az, az & !ax);
414                let (bx_only, by, bz_only) = (bx & !bz, bx & bz, bz & !bx);
415                let positive =
416                    ((ax_only & by) | (ay & bz_only) | (az_only & bx_only)).count_ones() as u8;
417                let negative =
418                    ((ay & bx_only) | (az_only & by) | (ax_only & bz_only)).count_ones() as u8;
419                phase.wrapping_add(positive).wrapping_sub(negative) & 3
420            })
421    }
422
423    /// The `X` components as `u64` words, least-significant qubit first.
424    ///
425    /// Word-level access for bit algebra over whole strings — the simulator's
426    /// frame decomposition walks these directly rather than one site at a time.
427    /// Bits at or beyond [`len`](Self::len) are always clear.
428    pub fn x_words(&self) -> &[u64] {
429        self.xs.as_words()
430    }
431
432    /// The `Z` components as `u64` words. See [`x_words`](Self::x_words).
433    pub fn z_words(&self) -> &[u64] {
434        self.zs.as_words()
435    }
436
437    /// Returns the number of qubits.
438    pub fn len(&self) -> usize {
439        self.xs.len()
440    }
441
442    /// Returns `true` if there are no qubits.
443    pub fn is_empty(&self) -> bool {
444        self.len() == 0
445    }
446
447    /// Returns the number of non-identity Paulis.
448    pub fn weight(&self) -> usize {
449        // A column is non-identity iff either axis bit is set, so the weight is
450        // the popcount of `xs | zs`. Bits past `len` are never set.
451        self.xs.or_weight(&self.zs)
452    }
453
454    /// Returns whether every site is identity, stopping at the first nonzero word.
455    pub fn is_identity(&self) -> bool {
456        self.xs.is_zero() && self.zs.is_zero()
457    }
458
459    /// Iterates the non-identity sites in ascending index order.
460    ///
461    /// # Panics
462    ///
463    /// Panics if the packed X/Z representation contains an impossible support bit.
464    pub fn iter_support(&self) -> impl Iterator<Item = (usize, Pauli)> + '_ {
465        self.x_words()
466            .iter()
467            .copied()
468            .zip(self.z_words().iter().copied())
469            .enumerate()
470            .flat_map(|(word_index, (xs, zs))| {
471                let mut support = xs | zs;
472                std::iter::from_fn(move || {
473                    if support == 0 {
474                        return None;
475                    }
476                    let bit_index = support.trailing_zeros() as usize;
477                    support &= support - 1;
478                    let bit = 1 << bit_index;
479                    let pauli = match (xs & bit != 0, zs & bit != 0) {
480                        (true, false) => Pauli::X,
481                        (false, true) => Pauli::Z,
482                        (true, true) => Pauli::Y,
483                        (false, false) => unreachable!("support bit has an X or Z component"),
484                    };
485                    Some((word_index * u64::BITS as usize + bit_index, pauli))
486                })
487            })
488    }
489
490    /// Returns the Pauli at `index`.
491    ///
492    /// # Panics
493    ///
494    /// Panics if `index` is out of bounds.
495    pub fn get(&self, index: usize) -> Pauli {
496        assert!(
497            index < self.len(),
498            "index {index} out of bounds for PauliString of length {}",
499            self.len()
500        );
501        Pauli::from_xz(self.xs.index(index), self.zs.index(index))
502    }
503
504    /// Sets the Pauli at `index`.
505    ///
506    /// # Panics
507    ///
508    /// Panics if `index` is out of bounds.
509    pub fn set(&mut self, index: usize, pauli: Pauli) {
510        assert!(
511            index < self.len(),
512            "index {index} out of bounds for PauliString of length {}",
513            self.len()
514        );
515        self.xs.assign_index(index, pauli as u8 & 1 != 0);
516        self.zs.assign_index(index, pauli as u8 & 2 != 0);
517    }
518}
519
520impl std::fmt::Debug for PauliString {
521    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522        write!(f, "PauliString({self})")
523    }
524}
525
526impl std::fmt::Display for PauliString {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        for index in 0..self.len() {
529            f.write_str(match self.get(index) {
530                Pauli::I => "_",
531                Pauli::X => "X",
532                Pauli::Y => "Y",
533                Pauli::Z => "Z",
534            })?;
535        }
536        Ok(())
537    }
538}
539
540impl std::str::FromStr for PauliString {
541    type Err = PauliError;
542
543    fn from_str(value: &str) -> Result<Self, Self::Err> {
544        Self::try_from(value)
545    }
546}
547
548impl TryFrom<&str> for PauliString {
549    type Error = PauliError;
550
551    fn try_from(value: &str) -> Result<Self, Self::Error> {
552        let num_qubits = value.len();
553        let mut ps = PauliString::new(num_qubits);
554        for (i, c) in value.chars().enumerate() {
555            let p = Pauli::try_from(c)?;
556            ps.set(i, p);
557        }
558        Ok(ps)
559    }
560}
561
562impl BitXor for &PauliString {
563    type Output = PauliString;
564
565    fn bitxor(self, rhs: Self) -> Self::Output {
566        let mut result = self.clone();
567        result ^= rhs;
568        result
569    }
570}
571
572impl BitXorAssign<&PauliString> for PauliString {
573    fn bitxor_assign(&mut self, other: &Self) {
574        // Missing trailing sites are identities, so pad whichever operand is
575        // shorter before using BitVec's equal-width operation.
576        if self.len() > other.len() {
577            let (mut xs, mut zs) = (other.xs.clone(), other.zs.clone());
578            xs.resize(self.len());
579            zs.resize(self.len());
580            self.xs.bitxor_assign(&xs);
581            self.zs.bitxor_assign(&zs);
582            return;
583        }
584        if self.len() < other.len() {
585            self.xs.resize(other.len());
586            self.zs.resize(other.len());
587        }
588        self.xs.bitxor_assign(&other.xs);
589        self.zs.bitxor_assign(&other.zs);
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    #[test]
598    fn parsing_and_indexing_preserve_the_declared_width() {
599        let paulis: PauliString = "IXYZ_".parse().unwrap();
600        assert_eq!(paulis.to_string(), "_XYZ_");
601        assert_eq!(paulis.to_string().parse::<PauliString>().unwrap(), paulis);
602        assert_eq!(
603            "Xλ".parse::<PauliString>(),
604            Err(PauliError::InvalidChar('λ'))
605        );
606
607        // Index 1 is inside the backing word, but outside this string.
608        let mut single = PauliString::new(1);
609        std::panic::catch_unwind(|| single.get(1)).expect_err("out-of-width reads must panic");
610        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
611            single.set(1, Pauli::X);
612        }))
613        .expect_err("out-of-width writes must panic");
614        assert_eq!(single.iter_support().count(), 0);
615        assert_eq!(single.weight(), 0);
616    }
617
618    #[test]
619    fn support_statistics_cross_word_boundaries() {
620        // 70 columns spans two u64 blocks; X, Y, and Z must each count once.
621        let mut ps = PauliString::new(70);
622        for (index, pauli) in [
623            (0, Pauli::X),
624            (63, Pauli::Y),
625            (64, Pauli::Z),
626            (69, Pauli::X),
627        ] {
628            ps.set(index, pauli);
629        }
630        assert_eq!(ps.weight(), 4);
631        ps.set(63, Pauli::I);
632        assert_eq!(ps.weight(), 3);
633        assert_eq!(PauliString::new(70).weight(), 0);
634        for len in [0, 70] {
635            assert!(PauliString::new(len).is_identity());
636        }
637        for pauli in [Pauli::X, Pauli::Y, Pauli::Z] {
638            assert!(!PauliString::single(70, 64, pauli).is_identity());
639        }
640    }
641
642    #[test]
643    fn iter_support_skips_identities_across_word_boundaries() {
644        let ps = PauliString::from_terms(
645            130,
646            [
647                (0, Pauli::X),
648                (63, Pauli::Y),
649                (64, Pauli::Z),
650                (129, Pauli::X),
651            ],
652        );
653        assert_eq!(
654            ps.iter_support().collect::<Vec<_>>(),
655            [
656                (0, Pauli::X),
657                (63, Pauli::Y),
658                (64, Pauli::Z),
659                (129, Pauli::X)
660            ]
661        );
662    }
663
664    /// Operands of different widths used to reach `BitVec::dot`, whose length
665    /// assertion fires in release builds — so a commutation test between a
666    /// 4-qubit and a 6-qubit string aborted instead of answering. The shorter
667    /// string's missing sites are identities and commute with anything.
668    #[test]
669    fn commutation_spans_operands_of_different_widths() {
670        let narrow = PauliString::single(4, 0, Pauli::X);
671        let wide = PauliString::single(6, 0, Pauli::Z);
672        assert!(!narrow.commutes_with(&wide), "one anticommuting site");
673        assert!(!wide.commutes_with(&narrow), "and symmetrically");
674
675        // Only the sites the two share can anticommute.
676        let disjoint = PauliString::single(6, 5, Pauli::Z);
677        assert!(narrow.commutes_with(&disjoint));
678        assert!(disjoint.commutes_with(&narrow));
679
680        // Across a word boundary, where the wider operand has words the
681        // narrower one does not.
682        let short = PauliString::single(64, 63, Pauli::X);
683        let long = PauliString::from_terms(200, [(63, Pauli::Z), (150, Pauli::X)]);
684        assert!(!short.commutes_with(&long));
685        assert!(!long.commutes_with(&short));
686
687        // Two anticommuting sites commute overall, whatever the widths.
688        let pair = PauliString::from_terms(4, [(0, Pauli::X), (1, Pauli::X)]);
689        let pair_wide = PauliString::from_terms(9, [(0, Pauli::Z), (1, Pauli::Z)]);
690        assert!(pair.commutes_with(&pair_wide));
691    }
692
693    #[test]
694    fn xor_extends_the_shorter_operand_with_identities() {
695        let short = PauliString::try_from("X").unwrap();
696        let wide = PauliString::try_from("_Z").unwrap();
697
698        assert_eq!((&short ^ &wide).to_string(), "XZ");
699        assert_eq!(&short ^ &wide, &wide ^ &short);
700    }
701
702    #[test]
703    fn packed_product_phase_matches_sitewise_pauli_products() {
704        fn sitewise(left: &PauliString, right: &PauliString) -> u8 {
705            (0..left.len().min(right.len())).fold(0, |phase, index| {
706                (phase
707                    + match (left.get(index), right.get(index)) {
708                        (Pauli::X, Pauli::Y) | (Pauli::Y, Pauli::Z) | (Pauli::Z, Pauli::X) => 1,
709                        (Pauli::Y, Pauli::X) | (Pauli::Z, Pauli::Y) | (Pauli::X, Pauli::Z) => 3,
710                        _ => 0,
711                    })
712                    % 4
713            })
714        }
715
716        let left = PauliString::from_terms(
717            130,
718            (0..130).map(|index| (index, [Pauli::I, Pauli::X, Pauli::Y, Pauli::Z][index % 4])),
719        );
720        let right = PauliString::from_terms(
721            70,
722            (0..70).map(|index| (index, [Pauli::Z, Pauli::Y, Pauli::X, Pauli::I][index % 4])),
723        );
724        assert_eq!(left.product_phase(&right), sitewise(&left, &right));
725        assert_eq!(right.product_phase(&left), sitewise(&right, &left));
726    }
727
728    #[test]
729    fn phased_pauli_retains_exact_products_and_clifford_signs() {
730        let positive = |value| PhasedPauliString::positive(PauliString::try_from(value).unwrap());
731
732        let mut product = positive("X");
733        product.multiply_assign(&positive("Z"));
734        assert_eq!(product.paulis.to_string(), "Y");
735        assert_eq!(product.phase(), 3);
736
737        let mut y = positive("Y");
738        y.conjugate_h(0);
739        assert_eq!(y.paulis.to_string(), "Y");
740        assert_eq!(y.hermitian_sign(), Some(true));
741    }
742}