1use std::ops::{BitAnd, BitOr, BitXor, BitXorAssign};
2
3use binar::{BitVec, Bitwise, BitwiseMut, BitwisePair, BitwisePairMut};
4use strum::{Display, EnumString};
5use thiserror::Error;
6
7#[derive(Debug, Clone, PartialEq, Eq, Error)]
9pub enum PauliError {
10 #[error("cannot convert Pauli {0} to Basis")]
12 NotBasis(Pauli),
13 #[error("invalid u8 value for Pauli: {0}")]
15 InvalidByte(u8),
16 #[error("invalid char for Pauli: {0}")]
18 InvalidChar(char),
19}
20
21#[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,
39 Z,
41}
42
43impl Basis {
44 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#[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,
83 Y,
85 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#[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 I = 0,
119 X = 1,
121 Z = 2,
123 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 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 pub fn iter_xz(self) -> impl Iterator<Item = Self> {
167 [Self::X, Self::Z].into_iter().filter(move |&p| p & self)
168 }
169
170 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#[derive(Clone, PartialEq, Eq)]
249pub struct PauliString {
250 xs: BitVec,
251 zs: BitVec,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct PhasedPauliString {
257 pub paulis: PauliString,
259 phase: u8,
260}
261
262impl PhasedPauliString {
263 pub fn new(paulis: PauliString, phase: u8) -> Self {
265 Self {
266 paulis,
267 phase: phase & 3,
268 }
269 }
270
271 pub fn positive(paulis: PauliString) -> Self {
273 Self::new(paulis, 0)
274 }
275
276 pub const fn phase(&self) -> u8 {
278 self.phase
279 }
280
281 pub fn shift_phase(&mut self, exponent: u8) {
283 self.phase = self.phase.wrapping_add(exponent) & 3;
284 }
285
286 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 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 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 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 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 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 pub fn commutes_with(&self, other: &Self) -> bool {
396 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 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 pub fn x_words(&self) -> &[u64] {
429 self.xs.as_words()
430 }
431
432 pub fn z_words(&self) -> &[u64] {
434 self.zs.as_words()
435 }
436
437 pub fn len(&self) -> usize {
439 self.xs.len()
440 }
441
442 pub fn is_empty(&self) -> bool {
444 self.len() == 0
445 }
446
447 pub fn weight(&self) -> usize {
449 self.xs.or_weight(&self.zs)
452 }
453
454 pub fn is_identity(&self) -> bool {
456 self.xs.is_zero() && self.zs.is_zero()
457 }
458
459 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 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 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 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 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 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 #[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 let disjoint = PauliString::single(6, 5, Pauli::Z);
677 assert!(narrow.commutes_with(&disjoint));
678 assert!(disjoint.commutes_with(&narrow));
679
680 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 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}