quantrs2_sim/
pauli.rs

1//! Pauli string evolution and operations.
2//!
3//! This module provides efficient operations for Pauli strings, including:
4//! - Pauli string construction and manipulation
5//! - Time evolution of Pauli observables
6//! - Commutation relations and algebra
7//! - Measurement expectation values
8
9use crate::prelude::SimulatorError;
10use ndarray::Array2;
11use num_complex::Complex64;
12use scirs2_core::parallel_ops::*;
13use std::collections::HashMap;
14use std::fmt;
15
16use crate::error::Result;
17use crate::trotter::{DynamicCircuit, Hamiltonian, TrotterDecomposer, TrotterMethod};
18use quantrs2_core::gate::{multi::*, single::*, GateOp};
19use quantrs2_core::qubit::QubitId;
20
21/// Single Pauli operator type
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum PauliOperator {
24    /// Identity operator
25    I,
26    /// Pauli-X operator
27    X,
28    /// Pauli-Y operator
29    Y,
30    /// Pauli-Z operator
31    Z,
32}
33
34impl fmt::Display for PauliOperator {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            PauliOperator::I => write!(f, "I"),
38            PauliOperator::X => write!(f, "X"),
39            PauliOperator::Y => write!(f, "Y"),
40            PauliOperator::Z => write!(f, "Z"),
41        }
42    }
43}
44
45impl PauliOperator {
46    /// Parse from string
47    pub fn from_str(s: &str) -> Result<Self> {
48        match s.to_uppercase().as_str() {
49            "I" => Ok(PauliOperator::I),
50            "X" => Ok(PauliOperator::X),
51            "Y" => Ok(PauliOperator::Y),
52            "Z" => Ok(PauliOperator::Z),
53            _ => Err(SimulatorError::InvalidInput(format!(
54                "Invalid Pauli operator: {}",
55                s
56            ))),
57        }
58    }
59
60    /// Get matrix representation
61    pub fn matrix(&self) -> Array2<Complex64> {
62        match self {
63            PauliOperator::I => Array2::from_shape_vec(
64                (2, 2),
65                vec![
66                    Complex64::new(1.0, 0.0),
67                    Complex64::new(0.0, 0.0),
68                    Complex64::new(0.0, 0.0),
69                    Complex64::new(1.0, 0.0),
70                ],
71            )
72            .unwrap(),
73            PauliOperator::X => Array2::from_shape_vec(
74                (2, 2),
75                vec![
76                    Complex64::new(0.0, 0.0),
77                    Complex64::new(1.0, 0.0),
78                    Complex64::new(1.0, 0.0),
79                    Complex64::new(0.0, 0.0),
80                ],
81            )
82            .unwrap(),
83            PauliOperator::Y => Array2::from_shape_vec(
84                (2, 2),
85                vec![
86                    Complex64::new(0.0, 0.0),
87                    Complex64::new(0.0, -1.0),
88                    Complex64::new(0.0, 1.0),
89                    Complex64::new(0.0, 0.0),
90                ],
91            )
92            .unwrap(),
93            PauliOperator::Z => Array2::from_shape_vec(
94                (2, 2),
95                vec![
96                    Complex64::new(1.0, 0.0),
97                    Complex64::new(0.0, 0.0),
98                    Complex64::new(0.0, 0.0),
99                    Complex64::new(-1.0, 0.0),
100                ],
101            )
102            .unwrap(),
103        }
104    }
105
106    /// Check if commutes with another Pauli
107    pub fn commutes_with(&self, other: &PauliOperator) -> bool {
108        match (self, other) {
109            (PauliOperator::I, _) | (_, PauliOperator::I) => true,
110            (a, b) if a == b => true,
111            _ => false,
112        }
113    }
114
115    /// Multiplication of Pauli operators (returns (result, phase))
116    pub fn multiply(&self, other: &PauliOperator) -> (PauliOperator, Complex64) {
117        match (self, other) {
118            (PauliOperator::I, p) | (p, PauliOperator::I) => (*p, Complex64::new(1.0, 0.0)),
119            (PauliOperator::X, PauliOperator::X)
120            | (PauliOperator::Y, PauliOperator::Y)
121            | (PauliOperator::Z, PauliOperator::Z) => (PauliOperator::I, Complex64::new(1.0, 0.0)),
122            (PauliOperator::X, PauliOperator::Y) => (PauliOperator::Z, Complex64::new(0.0, 1.0)),
123            (PauliOperator::Y, PauliOperator::X) => (PauliOperator::Z, Complex64::new(0.0, -1.0)),
124            (PauliOperator::Y, PauliOperator::Z) => (PauliOperator::X, Complex64::new(0.0, 1.0)),
125            (PauliOperator::Z, PauliOperator::Y) => (PauliOperator::X, Complex64::new(0.0, -1.0)),
126            (PauliOperator::Z, PauliOperator::X) => (PauliOperator::Y, Complex64::new(0.0, 1.0)),
127            (PauliOperator::X, PauliOperator::Z) => (PauliOperator::Y, Complex64::new(0.0, -1.0)),
128        }
129    }
130}
131
132/// A Pauli string is a tensor product of Pauli operators
133#[derive(Debug, Clone)]
134pub struct PauliString {
135    /// Pauli operators at each qubit position
136    pub operators: Vec<PauliOperator>,
137    /// Overall coefficient
138    pub coefficient: Complex64,
139    /// Number of qubits
140    pub num_qubits: usize,
141}
142
143impl PauliString {
144    /// Create a new Pauli string
145    pub fn new(num_qubits: usize) -> Self {
146        Self {
147            operators: vec![PauliOperator::I; num_qubits],
148            coefficient: Complex64::new(1.0, 0.0),
149            num_qubits,
150        }
151    }
152
153    /// Create from string representation like "XYZI"
154    pub fn from_string(pauli_str: &str, coefficient: Complex64) -> Result<Self> {
155        let operators: Result<Vec<_>> = pauli_str
156            .chars()
157            .map(|c| PauliOperator::from_str(&c.to_string()))
158            .collect();
159
160        Ok(Self {
161            operators: operators?,
162            coefficient,
163            num_qubits: pauli_str.len(),
164        })
165    }
166
167    /// Create from qubit indices and Pauli operators
168    pub fn from_ops(
169        num_qubits: usize,
170        ops: &[(usize, PauliOperator)],
171        coefficient: Complex64,
172    ) -> Result<Self> {
173        let mut pauli_string = Self::new(num_qubits);
174        pauli_string.coefficient = coefficient;
175
176        for &(qubit, op) in ops {
177            if qubit >= num_qubits {
178                return Err(SimulatorError::IndexOutOfBounds(qubit));
179            }
180            pauli_string.operators[qubit] = op;
181        }
182
183        Ok(pauli_string)
184    }
185
186    /// Set operator at specific qubit
187    pub fn set_operator(&mut self, qubit: usize, op: PauliOperator) -> Result<()> {
188        if qubit >= self.num_qubits {
189            return Err(SimulatorError::IndexOutOfBounds(qubit));
190        }
191        self.operators[qubit] = op;
192        Ok(())
193    }
194
195    /// Get operator at specific qubit
196    pub fn get_operator(&self, qubit: usize) -> Result<PauliOperator> {
197        if qubit >= self.num_qubits {
198            return Err(SimulatorError::IndexOutOfBounds(qubit));
199        }
200        Ok(self.operators[qubit])
201    }
202
203    /// Get non-identity operators
204    pub fn non_identity_ops(&self) -> Vec<(usize, PauliOperator)> {
205        self.operators
206            .iter()
207            .enumerate()
208            .filter(|(_, &op)| op != PauliOperator::I)
209            .map(|(i, &op)| (i, op))
210            .collect()
211    }
212
213    /// Check if this Pauli string commutes with another
214    pub fn commutes_with(&self, other: &PauliString) -> bool {
215        if self.num_qubits != other.num_qubits {
216            return false;
217        }
218
219        let mut anti_commute_count = 0;
220        for i in 0..self.num_qubits {
221            if !self.operators[i].commutes_with(&other.operators[i]) {
222                anti_commute_count += 1;
223            }
224        }
225
226        // Pauli strings commute if they anti-commute at an even number of positions
227        anti_commute_count % 2 == 0
228    }
229
230    /// Multiply two Pauli strings
231    pub fn multiply(&self, other: &PauliString) -> Result<PauliString> {
232        if self.num_qubits != other.num_qubits {
233            return Err(SimulatorError::DimensionMismatch(format!(
234                "Pauli strings have different lengths: {} vs {}",
235                self.num_qubits, other.num_qubits
236            )));
237        }
238
239        let mut result = PauliString::new(self.num_qubits);
240        let mut total_phase = self.coefficient * other.coefficient;
241
242        for i in 0..self.num_qubits {
243            let (op, phase) = self.operators[i].multiply(&other.operators[i]);
244            result.operators[i] = op;
245            total_phase *= phase;
246        }
247
248        result.coefficient = total_phase;
249        Ok(result)
250    }
251
252    /// Get weight (number of non-identity operators)
253    pub fn weight(&self) -> usize {
254        self.operators
255            .iter()
256            .filter(|&&op| op != PauliOperator::I)
257            .count()
258    }
259
260    /// Convert to Pauli string representation
261    pub fn pauli_string(&self) -> String {
262        self.operators.iter().map(|op| op.to_string()).collect()
263    }
264
265    /// Create time evolution circuit for this Pauli string
266    pub fn evolution_circuit(&self, time: f64) -> Result<DynamicCircuit> {
267        let mut circuit = DynamicCircuit::new(self.num_qubits);
268
269        if self.weight() == 0 {
270            // Identity operator - no gates needed
271            return Ok(circuit);
272        }
273
274        let non_identity = self.non_identity_ops();
275        let angle = -2.0 * self.coefficient.re * time;
276
277        if non_identity.len() == 1 {
278            // Single-qubit Pauli evolution
279            let (qubit, op) = non_identity[0];
280            match op {
281                PauliOperator::X => circuit.add_gate(Box::new(RotationX {
282                    target: QubitId::new(qubit as u32),
283                    theta: angle,
284                }))?,
285                PauliOperator::Y => circuit.add_gate(Box::new(RotationY {
286                    target: QubitId::new(qubit as u32),
287                    theta: angle,
288                }))?,
289                PauliOperator::Z => circuit.add_gate(Box::new(RotationZ {
290                    target: QubitId::new(qubit as u32),
291                    theta: angle,
292                }))?,
293                PauliOperator::I => {} // Should not happen
294            }
295        } else {
296            // Multi-qubit Pauli string evolution
297
298            // Apply basis rotations to convert all non-identity operators to Z
299            for &(qubit, op) in &non_identity {
300                match op {
301                    PauliOperator::X => circuit.add_gate(Box::new(Hadamard {
302                        target: QubitId::new(qubit as u32),
303                    }))?,
304                    PauliOperator::Y => {
305                        circuit.add_gate(Box::new(Hadamard {
306                            target: QubitId::new(qubit as u32),
307                        }))?;
308                        circuit.add_gate(Box::new(Phase {
309                            target: QubitId::new(qubit as u32),
310                        }))?;
311                    }
312                    PauliOperator::Z => {} // No basis change needed
313                    PauliOperator::I => {} // Should not happen
314                }
315            }
316
317            // Apply CNOT ladder to disentangle all Z operators to the last qubit
318            for i in 0..non_identity.len() - 1 {
319                circuit.add_gate(Box::new(CNOT {
320                    control: QubitId::new(non_identity[i].0 as u32),
321                    target: QubitId::new(non_identity[i + 1].0 as u32),
322                }))?;
323            }
324
325            // Apply Z rotation on the last qubit
326            circuit.add_gate(Box::new(RotationZ {
327                target: QubitId::new(non_identity[non_identity.len() - 1].0 as u32),
328                theta: angle,
329            }))?;
330
331            // Reverse CNOT ladder
332            for i in (0..non_identity.len() - 1).rev() {
333                circuit.add_gate(Box::new(CNOT {
334                    control: QubitId::new(non_identity[i].0 as u32),
335                    target: QubitId::new(non_identity[i + 1].0 as u32),
336                }))?;
337            }
338
339            // Reverse basis rotations
340            for &(qubit, op) in non_identity.iter().rev() {
341                match op {
342                    PauliOperator::X => circuit.add_gate(Box::new(Hadamard {
343                        target: QubitId::new(qubit as u32),
344                    }))?,
345                    PauliOperator::Y => {
346                        circuit.add_gate(Box::new(PhaseDagger {
347                            target: QubitId::new(qubit as u32),
348                        }))?;
349                        circuit.add_gate(Box::new(Hadamard {
350                            target: QubitId::new(qubit as u32),
351                        }))?;
352                    }
353                    PauliOperator::Z => {} // No basis change needed
354                    PauliOperator::I => {} // Should not happen
355                }
356            }
357        }
358
359        Ok(circuit)
360    }
361}
362
363impl fmt::Display for PauliString {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        write!(f, "({}) {}", self.coefficient, self.pauli_string())
366    }
367}
368
369/// Sum of Pauli strings (Pauli operator)
370#[derive(Debug, Clone)]
371pub struct PauliOperatorSum {
372    /// Individual Pauli string terms
373    pub terms: Vec<PauliString>,
374    /// Number of qubits
375    pub num_qubits: usize,
376}
377
378impl PauliOperatorSum {
379    /// Create new empty sum
380    pub fn new(num_qubits: usize) -> Self {
381        Self {
382            terms: Vec::new(),
383            num_qubits,
384        }
385    }
386
387    /// Add a Pauli string term
388    pub fn add_term(&mut self, pauli_string: PauliString) -> Result<()> {
389        if pauli_string.num_qubits != self.num_qubits {
390            return Err(SimulatorError::DimensionMismatch(format!(
391                "Pauli string has {} qubits, expected {}",
392                pauli_string.num_qubits, self.num_qubits
393            )));
394        }
395        self.terms.push(pauli_string);
396        Ok(())
397    }
398
399    /// Combine like terms
400    pub fn simplify(&mut self) {
401        let mut simplified_terms: HashMap<String, Complex64> = HashMap::new();
402
403        for term in &self.terms {
404            let key = format!("{}", term);
405            *simplified_terms
406                .entry(key)
407                .or_insert(Complex64::new(0.0, 0.0)) += term.coefficient;
408        }
409
410        self.terms.clear();
411        for (pauli_str, coeff) in simplified_terms {
412            if coeff.norm() > 1e-15 {
413                if let Ok(term) = PauliString::from_string(&pauli_str, coeff) {
414                    self.terms.push(term);
415                }
416            }
417        }
418    }
419
420    /// Convert to Hamiltonian for Trotter evolution
421    pub fn to_hamiltonian(&self) -> Hamiltonian {
422        let mut ham = Hamiltonian::new(self.num_qubits);
423
424        for term in &self.terms {
425            let non_identity = term.non_identity_ops();
426
427            match non_identity.len() {
428                0 => {} // Identity term - ignore for Hamiltonian
429                1 => {
430                    let (qubit, op) = non_identity[0];
431                    let pauli_str = match op {
432                        PauliOperator::X => "X",
433                        PauliOperator::Y => "Y",
434                        PauliOperator::Z => "Z",
435                        PauliOperator::I => continue,
436                    };
437                    let _ = ham.add_single_pauli(qubit, pauli_str, term.coefficient.re);
438                }
439                2 => {
440                    let (q1, op1) = non_identity[0];
441                    let (q2, op2) = non_identity[1];
442                    let pauli1 = match op1 {
443                        PauliOperator::X => "X",
444                        PauliOperator::Y => "Y",
445                        PauliOperator::Z => "Z",
446                        PauliOperator::I => continue,
447                    };
448                    let pauli2 = match op2 {
449                        PauliOperator::X => "X",
450                        PauliOperator::Y => "Y",
451                        PauliOperator::Z => "Z",
452                        PauliOperator::I => continue,
453                    };
454                    let _ = ham.add_two_pauli(q1, q2, pauli1, pauli2, term.coefficient.re);
455                }
456                _ => {
457                    // Multi-qubit case
458                    let qubits: Vec<usize> = non_identity.iter().map(|&(q, _)| q).collect();
459                    let paulis: Vec<String> = non_identity
460                        .iter()
461                        .map(|&(_, op)| match op {
462                            PauliOperator::X => "X".to_string(),
463                            PauliOperator::Y => "Y".to_string(),
464                            PauliOperator::Z => "Z".to_string(),
465                            PauliOperator::I => "I".to_string(),
466                        })
467                        .collect();
468                    let _ = ham.add_pauli_string(qubits, paulis, term.coefficient.re);
469                }
470            }
471        }
472
473        ham
474    }
475
476    /// Time evolution using Trotter decomposition
477    pub fn time_evolution_circuit(
478        &self,
479        time: f64,
480        trotter_steps: usize,
481        method: TrotterMethod,
482    ) -> Result<DynamicCircuit> {
483        let hamiltonian = self.to_hamiltonian();
484        let decomposer = TrotterDecomposer::new(method, trotter_steps);
485        decomposer.decompose(&hamiltonian, time)
486    }
487
488    /// Direct time evolution without Trotter approximation (for single terms)
489    pub fn exact_evolution_circuit(&self, time: f64) -> Result<DynamicCircuit> {
490        let mut circuit = DynamicCircuit::new(self.num_qubits);
491
492        for term in &self.terms {
493            let term_circuit = term.evolution_circuit(time)?;
494            for gate in term_circuit.gates() {
495                circuit.add_gate(gate.clone())?;
496            }
497        }
498
499        Ok(circuit)
500    }
501}
502
503impl fmt::Display for PauliOperatorSum {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        if self.terms.is_empty() {
506            write!(f, "0")
507        } else {
508            let term_strs: Vec<String> = self.terms.iter().map(|t| format!("{}", t)).collect();
509            write!(f, "{}", term_strs.join(" + "))
510        }
511    }
512}
513
514/// Utilities for common Pauli string operations
515pub struct PauliUtils;
516
517impl PauliUtils {
518    /// Create a single-qubit Pauli string
519    pub fn single_qubit(
520        num_qubits: usize,
521        qubit: usize,
522        op: PauliOperator,
523        coeff: Complex64,
524    ) -> Result<PauliString> {
525        PauliString::from_ops(num_qubits, &[(qubit, op)], coeff)
526    }
527
528    /// Create an all-X Pauli string
529    pub fn all_x(num_qubits: usize, coeff: Complex64) -> PauliString {
530        let mut pauli = PauliString::new(num_qubits);
531        pauli.coefficient = coeff;
532        for i in 0..num_qubits {
533            pauli.operators[i] = PauliOperator::X;
534        }
535        pauli
536    }
537
538    /// Create an all-Z Pauli string
539    pub fn all_z(num_qubits: usize, coeff: Complex64) -> PauliString {
540        let mut pauli = PauliString::new(num_qubits);
541        pauli.coefficient = coeff;
542        for i in 0..num_qubits {
543            pauli.operators[i] = PauliOperator::Z;
544        }
545        pauli
546    }
547
548    /// Create random Pauli string
549    pub fn random(num_qubits: usize, weight: usize, coeff: Complex64) -> Result<PauliString> {
550        if weight > num_qubits {
551            return Err(SimulatorError::InvalidInput(
552                "Weight cannot exceed number of qubits".to_string(),
553            ));
554        }
555
556        let mut pauli = PauliString::new(num_qubits);
557        pauli.coefficient = coeff;
558
559        // Randomly select positions for non-identity operators
560        let mut positions: Vec<usize> = (0..num_qubits).collect();
561        fastrand::shuffle(&mut positions);
562
563        let ops = [PauliOperator::X, PauliOperator::Y, PauliOperator::Z];
564
565        for i in 0..weight {
566            pauli.operators[positions[i]] = ops[fastrand::usize(0..3)];
567        }
568
569        Ok(pauli)
570    }
571
572    /// Check if a set of Pauli strings are mutually commuting
573    pub fn are_mutually_commuting(pauli_strings: &[PauliString]) -> bool {
574        for i in 0..pauli_strings.len() {
575            for j in i + 1..pauli_strings.len() {
576                if !pauli_strings[i].commutes_with(&pauli_strings[j]) {
577                    return false;
578                }
579            }
580        }
581        true
582    }
583
584    /// Find maximal set of mutually commuting Pauli strings
585    pub fn maximal_commuting_set(pauli_strings: &[PauliString]) -> Vec<usize> {
586        let mut commuting_set = Vec::new();
587
588        for (i, pauli) in pauli_strings.iter().enumerate() {
589            let mut can_add = true;
590            for &j in &commuting_set {
591                if !pauli.commutes_with(&pauli_strings[j]) {
592                    can_add = false;
593                    break;
594                }
595            }
596
597            if can_add {
598                commuting_set.push(i);
599            }
600        }
601
602        commuting_set
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    #[test]
611    fn test_pauli_operator_multiply() {
612        let (result, phase) = PauliOperator::X.multiply(&PauliOperator::Y);
613        assert_eq!(result, PauliOperator::Z);
614        assert_eq!(phase, Complex64::new(0.0, 1.0));
615
616        let (result, phase) = PauliOperator::Y.multiply(&PauliOperator::X);
617        assert_eq!(result, PauliOperator::Z);
618        assert_eq!(phase, Complex64::new(0.0, -1.0));
619    }
620
621    #[test]
622    fn test_pauli_string_creation() {
623        let pauli = PauliString::from_string("XYZ", Complex64::new(1.0, 0.0)).unwrap();
624        assert_eq!(pauli.num_qubits, 3);
625        assert_eq!(pauli.operators[0], PauliOperator::X);
626        assert_eq!(pauli.operators[1], PauliOperator::Y);
627        assert_eq!(pauli.operators[2], PauliOperator::Z);
628    }
629
630    #[test]
631    fn test_pauli_string_multiply() {
632        let p1 = PauliString::from_string("XY", Complex64::new(1.0, 0.0)).unwrap();
633        let p2 = PauliString::from_string("YZ", Complex64::new(1.0, 0.0)).unwrap();
634
635        let result = p1.multiply(&p2).unwrap();
636        assert_eq!(result.operators[0], PauliOperator::Z);
637        assert_eq!(result.operators[1], PauliOperator::X);
638        assert_eq!(result.coefficient, Complex64::new(-1.0, 0.0));
639    }
640
641    #[test]
642    fn test_pauli_string_commutation() {
643        let p1 = PauliString::from_string("XY", Complex64::new(1.0, 0.0)).unwrap();
644        let p2 = PauliString::from_string("ZI", Complex64::new(1.0, 0.0)).unwrap();
645        let p3 = PauliString::from_string("XI", Complex64::new(1.0, 0.0)).unwrap();
646
647        assert!(!p1.commutes_with(&p2)); // XY and ZI anti-commute at first qubit
648        assert!(p1.commutes_with(&p3)); // XY and XI commute
649    }
650
651    #[test]
652    fn test_pauli_string_weight() {
653        let p1 = PauliString::from_string("XIYZ", Complex64::new(1.0, 0.0)).unwrap();
654        assert_eq!(p1.weight(), 3);
655
656        let p2 = PauliString::from_string("IIII", Complex64::new(1.0, 0.0)).unwrap();
657        assert_eq!(p2.weight(), 0);
658    }
659
660    #[test]
661    fn test_pauli_operator_sum() {
662        let mut sum = PauliOperatorSum::new(2);
663
664        let p1 = PauliString::from_string("XX", Complex64::new(1.0, 0.0)).unwrap();
665        let p2 = PauliString::from_string("YY", Complex64::new(0.5, 0.0)).unwrap();
666
667        sum.add_term(p1).unwrap();
668        sum.add_term(p2).unwrap();
669
670        assert_eq!(sum.terms.len(), 2);
671    }
672
673    #[test]
674    fn test_evolution_circuit_single_qubit() {
675        let pauli = PauliString::from_string("X", Complex64::new(1.0, 0.0)).unwrap();
676        let circuit = pauli.evolution_circuit(1.0).unwrap();
677        assert!(circuit.gate_count() > 0);
678    }
679
680    #[test]
681    fn test_evolution_circuit_multi_qubit() {
682        let pauli = PauliString::from_string("XYZ", Complex64::new(1.0, 0.0)).unwrap();
683        let circuit = pauli.evolution_circuit(1.0).unwrap();
684        assert!(circuit.gate_count() > 0);
685    }
686
687    #[test]
688    fn test_utils_single_qubit() {
689        let pauli =
690            PauliUtils::single_qubit(3, 1, PauliOperator::X, Complex64::new(1.0, 0.0)).unwrap();
691        assert_eq!(pauli.operators[0], PauliOperator::I);
692        assert_eq!(pauli.operators[1], PauliOperator::X);
693        assert_eq!(pauli.operators[2], PauliOperator::I);
694    }
695
696    #[test]
697    fn test_mutually_commuting() {
698        let p1 = PauliString::from_string("XX", Complex64::new(1.0, 0.0)).unwrap();
699        let p2 = PauliString::from_string("ZZ", Complex64::new(1.0, 0.0)).unwrap();
700        let p3 = PauliString::from_string("YY", Complex64::new(1.0, 0.0)).unwrap();
701
702        let paulis = vec![p1, p2, p3];
703        assert!(PauliUtils::are_mutually_commuting(&paulis));
704    }
705}