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