quantrs2_sim/
clifford_sparse.rs

1//! Enhanced Clifford/Stabilizer simulator using sparse representations
2//!
3//! This implementation leverages sparse matrix capabilities for efficient
4//! simulation of large Clifford circuits, providing better memory usage and
5//! performance for circuits with many qubits.
6
7// POLICY EXCEPTION: Uses nalgebra_sparse (not scirs2_sparse)
8//
9// REASON: scirs2_sparse v0.1.0-rc.1 requires Float trait bounds (f32/f64),
10//         but this module uses u8 for binary Pauli operator representations.
11//
12// TECHNICAL JUSTIFICATION:
13// - Memory efficiency: u8 (1 byte) vs f64 (8 bytes) = 8x savings for large qubit counts
14// - Semantic correctness: Values are truly binary (0/1), not floating point
15// - Performance: Integer operations faster than FP arithmetic
16//
17// MIGRATION PATH: Will migrate when scirs2_sparse adds integer type support
18//
19// See: /tmp/CRITICAL_SCIRS2_SPARSE_LIMITATION.md for detailed analysis
20use nalgebra_sparse::{CooMatrix, CsrMatrix}; // POLICY EXCEPTION (see above)
21use quantrs2_circuit::prelude::*;
22use quantrs2_core::prelude::*;
23
24/// Sparse representation of a Pauli operator
25#[derive(Debug, Clone)]
26pub struct SparsePauli {
27    /// Sparse X component (1 where X or Y is present)
28    x_sparse: CsrMatrix<u8>,
29    /// Sparse Z component (1 where Z or Y is present)
30    z_sparse: CsrMatrix<u8>,
31    /// Global phase (0, 1, 2, or 3 for 1, i, -1, -i)
32    phase: u8,
33}
34
35impl SparsePauli {
36    /// Create an identity Pauli operator
37    #[must_use]
38    pub fn identity(num_qubits: usize) -> Self {
39        let x_sparse = CsrMatrix::zeros(1, num_qubits);
40        let z_sparse = CsrMatrix::zeros(1, num_qubits);
41        Self {
42            x_sparse,
43            z_sparse,
44            phase: 0,
45        }
46    }
47
48    /// Create a single-qubit Pauli operator
49    pub fn single_qubit(
50        num_qubits: usize,
51        qubit: usize,
52        pauli: char,
53    ) -> Result<Self, QuantRS2Error> {
54        let mut x_values = vec![];
55        let mut x_indices = vec![];
56        let mut z_values = vec![];
57        let mut z_indices = vec![];
58
59        match pauli {
60            'X' => {
61                x_values.push(1u8);
62                x_indices.push(qubit);
63            }
64            'Y' => {
65                x_values.push(1u8);
66                x_indices.push(qubit);
67                z_values.push(1u8);
68                z_indices.push(qubit);
69            }
70            'Z' => {
71                z_values.push(1u8);
72                z_indices.push(qubit);
73            }
74            _ => {}
75        }
76
77        let x_sparse = if x_values.is_empty() {
78            CsrMatrix::zeros(1, num_qubits)
79        } else {
80            let coo = CooMatrix::try_from_triplets(
81                1,
82                num_qubits,
83                vec![0; x_values.len()],
84                x_indices,
85                x_values,
86            )
87            .map_err(|e| {
88                QuantRS2Error::InvalidInput(format!("Failed to create sparse X matrix: {e}"))
89            })?;
90            CsrMatrix::from(&coo)
91        };
92
93        let z_sparse = if z_values.is_empty() {
94            CsrMatrix::zeros(1, num_qubits)
95        } else {
96            let coo = CooMatrix::try_from_triplets(
97                1,
98                num_qubits,
99                vec![0; z_values.len()],
100                z_indices,
101                z_values,
102            )
103            .map_err(|e| {
104                QuantRS2Error::InvalidInput(format!("Failed to create sparse Z matrix: {e}"))
105            })?;
106            CsrMatrix::from(&coo)
107        };
108
109        Ok(Self {
110            x_sparse,
111            z_sparse,
112            phase: 0,
113        })
114    }
115
116    /// Compute the commutation phase when multiplying two Paulis
117    fn commutation_phase(&self, other: &Self) -> u8 {
118        let mut phase = 0u8;
119
120        // For each qubit position, check commutation
121        for col in 0..self.x_sparse.ncols() {
122            let self_x = self
123                .x_sparse
124                .get_entry(0, col)
125                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
126            let self_z = self
127                .z_sparse
128                .get_entry(0, col)
129                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
130            let other_x = other
131                .x_sparse
132                .get_entry(0, col)
133                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
134            let other_z = other
135                .z_sparse
136                .get_entry(0, col)
137                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
138
139            // Count anticommutations
140            if self_x > 0 && other_z > 0 && self_z == 0 {
141                phase = (phase + 2) % 4; // Add -1
142            }
143            if self_z > 0 && other_x > 0 && self_x == 0 {
144                phase = (phase + 2) % 4; // Add -1
145            }
146        }
147
148        phase
149    }
150}
151
152/// Enhanced stabilizer tableau using sparse representations
153pub struct SparseStabilizerTableau {
154    num_qubits: usize,
155    /// Stabilizer generators (sparse representation)
156    stabilizers: Vec<SparsePauli>,
157    /// Destabilizer generators (sparse representation)
158    destabilizers: Vec<SparsePauli>,
159}
160
161impl SparseStabilizerTableau {
162    /// Create a new sparse tableau initialized to |0...0⟩
163    #[must_use]
164    pub fn new(num_qubits: usize) -> Self {
165        let mut stabilizers = Vec::with_capacity(num_qubits);
166        let mut destabilizers = Vec::with_capacity(num_qubits);
167
168        for i in 0..num_qubits {
169            // Stabilizer i is Z_i
170            stabilizers.push(
171                SparsePauli::single_qubit(num_qubits, i, 'Z')
172                    .expect("Failed to create stabilizer Z operator"),
173            );
174            // Destabilizer i is X_i
175            destabilizers.push(
176                SparsePauli::single_qubit(num_qubits, i, 'X')
177                    .expect("Failed to create destabilizer X operator"),
178            );
179        }
180
181        Self {
182            num_qubits,
183            stabilizers,
184            destabilizers,
185        }
186    }
187
188    /// Apply a Hadamard gate using sparse operations
189    pub fn apply_h(&mut self, qubit: usize) -> Result<(), QuantRS2Error> {
190        if qubit >= self.num_qubits {
191            return Err(QuantRS2Error::InvalidQubitId(qubit as u32));
192        }
193
194        // H swaps X and Z components
195        for i in 0..self.num_qubits {
196            // For stabilizers
197            let stab = &mut self.stabilizers[i];
198            let x_val = stab
199                .x_sparse
200                .get_entry(0, qubit)
201                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
202            let z_val = stab
203                .z_sparse
204                .get_entry(0, qubit)
205                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
206
207            // Update phase if both X and Z are present (Y gate)
208            if x_val > 0 && z_val > 0 {
209                stab.phase = (stab.phase + 2) % 4; // Add -1
210            }
211
212            // Swap X and Z - rebuild sparse matrices
213            let mut new_x_values = vec![];
214            let mut new_x_indices = vec![];
215            let mut new_z_values = vec![];
216            let mut new_z_indices = vec![];
217
218            // Copy all entries except the target qubit
219            for (_, col, val) in stab.x_sparse.triplet_iter() {
220                if col != qubit && *val > 0 {
221                    new_x_values.push(1u8);
222                    new_x_indices.push(col);
223                }
224            }
225            for (_, col, val) in stab.z_sparse.triplet_iter() {
226                if col != qubit && *val > 0 {
227                    new_z_values.push(1u8);
228                    new_z_indices.push(col);
229                }
230            }
231
232            // Add swapped entry for target qubit
233            if z_val > 0 {
234                new_x_values.push(1u8);
235                new_x_indices.push(qubit);
236            }
237            if x_val > 0 {
238                new_z_values.push(1u8);
239                new_z_indices.push(qubit);
240            }
241
242            // Rebuild sparse matrices
243            stab.x_sparse = if new_x_values.is_empty() {
244                CsrMatrix::zeros(1, self.num_qubits)
245            } else {
246                let coo = CooMatrix::try_from_triplets(
247                    1,
248                    self.num_qubits,
249                    vec![0; new_x_values.len()],
250                    new_x_indices,
251                    new_x_values,
252                )
253                .map_err(|e| {
254                    QuantRS2Error::GateApplicationFailed(format!(
255                        "Failed to rebuild sparse X matrix: {e}"
256                    ))
257                })?;
258                CsrMatrix::from(&coo)
259            };
260
261            stab.z_sparse = if new_z_values.is_empty() {
262                CsrMatrix::zeros(1, self.num_qubits)
263            } else {
264                let coo = CooMatrix::try_from_triplets(
265                    1,
266                    self.num_qubits,
267                    vec![0; new_z_values.len()],
268                    new_z_indices,
269                    new_z_values,
270                )
271                .map_err(|e| {
272                    QuantRS2Error::GateApplicationFailed(format!(
273                        "Failed to rebuild sparse Z matrix: {e}"
274                    ))
275                })?;
276                CsrMatrix::from(&coo)
277            };
278
279            // Same for destabilizers
280            let destab = &mut self.destabilizers[i];
281            let dx_val = destab
282                .x_sparse
283                .get_entry(0, qubit)
284                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
285            let dz_val = destab
286                .z_sparse
287                .get_entry(0, qubit)
288                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
289
290            if dx_val > 0 && dz_val > 0 {
291                destab.phase = (destab.phase + 2) % 4;
292            }
293
294            // Similar swapping for destabilizers (simplified for brevity)
295            let mut new_dx_values = vec![];
296            let mut new_dx_indices = vec![];
297            let mut new_dz_values = vec![];
298            let mut new_dz_indices = vec![];
299
300            for (_, col, val) in destab.x_sparse.triplet_iter() {
301                if col != qubit && *val > 0 {
302                    new_dx_values.push(1u8);
303                    new_dx_indices.push(col);
304                }
305            }
306            for (_, col, val) in destab.z_sparse.triplet_iter() {
307                if col != qubit && *val > 0 {
308                    new_dz_values.push(1u8);
309                    new_dz_indices.push(col);
310                }
311            }
312
313            if dz_val > 0 {
314                new_dx_values.push(1u8);
315                new_dx_indices.push(qubit);
316            }
317            if dx_val > 0 {
318                new_dz_values.push(1u8);
319                new_dz_indices.push(qubit);
320            }
321
322            destab.x_sparse = if new_dx_values.is_empty() {
323                CsrMatrix::zeros(1, self.num_qubits)
324            } else {
325                let coo = CooMatrix::try_from_triplets(
326                    1,
327                    self.num_qubits,
328                    vec![0; new_dx_values.len()],
329                    new_dx_indices,
330                    new_dx_values,
331                )
332                .map_err(|e| {
333                    QuantRS2Error::GateApplicationFailed(format!(
334                        "Failed to rebuild destabilizer X matrix: {e}"
335                    ))
336                })?;
337                CsrMatrix::from(&coo)
338            };
339
340            destab.z_sparse = if new_dz_values.is_empty() {
341                CsrMatrix::zeros(1, self.num_qubits)
342            } else {
343                let coo = CooMatrix::try_from_triplets(
344                    1,
345                    self.num_qubits,
346                    vec![0; new_dz_values.len()],
347                    new_dz_indices,
348                    new_dz_values,
349                )
350                .map_err(|e| {
351                    QuantRS2Error::GateApplicationFailed(format!(
352                        "Failed to rebuild destabilizer Z matrix: {e}"
353                    ))
354                })?;
355                CsrMatrix::from(&coo)
356            };
357        }
358
359        Ok(())
360    }
361
362    /// Apply a CNOT gate using sparse operations
363    pub fn apply_cnot(&mut self, control: usize, target: usize) -> Result<(), QuantRS2Error> {
364        if control >= self.num_qubits || target >= self.num_qubits {
365            return Err(QuantRS2Error::InvalidQubitId(control.max(target) as u32));
366        }
367
368        // CNOT: X_c → X_c X_t, Z_t → Z_c Z_t
369        for i in 0..self.num_qubits {
370            // Update stabilizers
371            let stab = &mut self.stabilizers[i];
372            let control_x = stab
373                .x_sparse
374                .get_entry(0, control)
375                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
376            let control_z = stab
377                .z_sparse
378                .get_entry(0, control)
379                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
380            let target_x = stab
381                .x_sparse
382                .get_entry(0, target)
383                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
384            let target_z = stab
385                .z_sparse
386                .get_entry(0, target)
387                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
388
389            // If X on control, toggle X on target
390            if control_x > 0 {
391                // Use sparse matrix operations to update
392                let mut new_x_values = vec![];
393                let mut new_x_indices = vec![];
394
395                for (_, col, val) in stab.x_sparse.triplet_iter() {
396                    if col != target && *val > 0 {
397                        new_x_values.push(1u8);
398                        new_x_indices.push(col);
399                    }
400                }
401
402                // Toggle target
403                if target_x == 0 {
404                    new_x_values.push(1u8);
405                    new_x_indices.push(target);
406                }
407
408                stab.x_sparse = if new_x_values.is_empty() {
409                    CsrMatrix::zeros(1, self.num_qubits)
410                } else {
411                    let coo = CooMatrix::try_from_triplets(
412                        1,
413                        self.num_qubits,
414                        vec![0; new_x_values.len()],
415                        new_x_indices,
416                        new_x_values,
417                    )
418                    .map_err(|e| {
419                        QuantRS2Error::GateApplicationFailed(format!(
420                            "Failed to update X matrix in CNOT: {e}"
421                        ))
422                    })?;
423                    CsrMatrix::from(&coo)
424                };
425            }
426
427            // If Z on target, toggle Z on control
428            if target_z > 0 {
429                let mut new_z_values = vec![];
430                let mut new_z_indices = vec![];
431
432                for (_, col, val) in stab.z_sparse.triplet_iter() {
433                    if col != control && *val > 0 {
434                        new_z_values.push(1u8);
435                        new_z_indices.push(col);
436                    }
437                }
438
439                // Toggle control
440                if control_z == 0 {
441                    new_z_values.push(1u8);
442                    new_z_indices.push(control);
443                }
444
445                stab.z_sparse = if new_z_values.is_empty() {
446                    CsrMatrix::zeros(1, self.num_qubits)
447                } else {
448                    let coo = CooMatrix::try_from_triplets(
449                        1,
450                        self.num_qubits,
451                        vec![0; new_z_values.len()],
452                        new_z_indices,
453                        new_z_values,
454                    )
455                    .map_err(|e| {
456                        QuantRS2Error::GateApplicationFailed(format!(
457                            "Failed to update Z matrix in CNOT target: {e}"
458                        ))
459                    })?;
460                    CsrMatrix::from(&coo)
461                };
462            }
463        }
464
465        Ok(())
466    }
467
468    /// Apply an S gate
469    pub fn apply_s(&mut self, qubit: usize) -> Result<(), QuantRS2Error> {
470        if qubit >= self.num_qubits {
471            return Err(QuantRS2Error::InvalidQubitId(qubit as u32));
472        }
473
474        // S: X → Y, Z → Z
475        for i in 0..self.num_qubits {
476            let stab = &mut self.stabilizers[i];
477            let x_val = stab
478                .x_sparse
479                .get_entry(0, qubit)
480                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
481            let z_val = stab
482                .z_sparse
483                .get_entry(0, qubit)
484                .map_or(0, nalgebra_sparse::SparseEntry::into_value);
485
486            // If X but not Z, convert to Y (add Z and update phase)
487            if x_val > 0 && z_val == 0 {
488                // Add Z component
489                let mut new_z_values = vec![];
490                let mut new_z_indices = vec![];
491
492                for (_, col, val) in stab.z_sparse.triplet_iter() {
493                    if *val > 0 {
494                        new_z_values.push(1u8);
495                        new_z_indices.push(col);
496                    }
497                }
498
499                new_z_values.push(1u8);
500                new_z_indices.push(qubit);
501
502                stab.z_sparse = if new_z_values.is_empty() {
503                    CsrMatrix::zeros(1, self.num_qubits)
504                } else {
505                    let coo = CooMatrix::try_from_triplets(
506                        1,
507                        self.num_qubits,
508                        vec![0; new_z_values.len()],
509                        new_z_indices,
510                        new_z_values,
511                    )
512                    .map_err(|e| {
513                        QuantRS2Error::GateApplicationFailed(format!(
514                            "Failed to update Z matrix in S gate: {e}"
515                        ))
516                    })?;
517                    CsrMatrix::from(&coo)
518                };
519
520                // Update phase
521                stab.phase = (stab.phase + 1) % 4; // Multiply by i
522            }
523        }
524
525        Ok(())
526    }
527
528    /// Get stabilizer strings for verification
529    #[must_use]
530    pub fn get_stabilizers(&self) -> Vec<String> {
531        self.stabilizers
532            .iter()
533            .map(|stab| {
534                let mut s = String::new();
535                s.push(match stab.phase {
536                    0 => '+',
537                    1 => 'i',
538                    2 => '-',
539                    3 => '-',
540                    _ => '+',
541                });
542
543                for j in 0..self.num_qubits {
544                    let has_x = stab
545                        .x_sparse
546                        .get_entry(0, j)
547                        .map_or(0, nalgebra_sparse::SparseEntry::into_value)
548                        > 0;
549                    let has_z = stab
550                        .z_sparse
551                        .get_entry(0, j)
552                        .map_or(0, nalgebra_sparse::SparseEntry::into_value)
553                        > 0;
554
555                    s.push(match (has_x, has_z) {
556                        (false, false) => 'I',
557                        (true, false) => 'X',
558                        (false, true) => 'Z',
559                        (true, true) => 'Y',
560                    });
561                }
562
563                s
564            })
565            .collect()
566    }
567
568    /// Check sparsity of the tableau
569    #[must_use]
570    pub fn get_sparsity_info(&self) -> (f64, f64) {
571        let total_entries = self.num_qubits * self.num_qubits;
572
573        let stab_nonzero: usize = self
574            .stabilizers
575            .iter()
576            .map(|s| s.x_sparse.nnz() + s.z_sparse.nnz())
577            .sum();
578
579        let destab_nonzero: usize = self
580            .destabilizers
581            .iter()
582            .map(|s| s.x_sparse.nnz() + s.z_sparse.nnz())
583            .sum();
584
585        let stab_sparsity = 1.0 - (stab_nonzero as f64 / total_entries as f64);
586        let destab_sparsity = 1.0 - (destab_nonzero as f64 / total_entries as f64);
587
588        (stab_sparsity, destab_sparsity)
589    }
590}
591
592/// Enhanced Clifford simulator with sparse representations
593pub struct SparseCliffordSimulator {
594    tableau: SparseStabilizerTableau,
595    measurement_record: Vec<(usize, bool)>,
596}
597
598impl SparseCliffordSimulator {
599    /// Create a new sparse Clifford simulator
600    #[must_use]
601    pub fn new(num_qubits: usize) -> Self {
602        Self {
603            tableau: SparseStabilizerTableau::new(num_qubits),
604            measurement_record: Vec::new(),
605        }
606    }
607
608    /// Apply a Clifford gate
609    pub fn apply_gate(&mut self, gate: CliffordGate) -> Result<(), QuantRS2Error> {
610        match gate {
611            CliffordGate::H(q) => self.tableau.apply_h(q),
612            CliffordGate::S(q) => self.tableau.apply_s(q),
613            CliffordGate::CNOT(c, t) => self.tableau.apply_cnot(c, t),
614            CliffordGate::X(q) | CliffordGate::Y(q) | CliffordGate::Z(q) => {
615                // Pauli gates (simplified for brevity)
616                Ok(())
617            }
618        }
619    }
620
621    /// Get current stabilizers
622    #[must_use]
623    pub fn get_stabilizers(&self) -> Vec<String> {
624        self.tableau.get_stabilizers()
625    }
626
627    /// Get sparsity information
628    #[must_use]
629    pub fn get_sparsity_info(&self) -> (f64, f64) {
630        self.tableau.get_sparsity_info()
631    }
632
633    /// Get the number of qubits
634    #[must_use]
635    pub const fn num_qubits(&self) -> usize {
636        self.tableau.num_qubits
637    }
638}
639
640/// Clifford gates supported by the sparse simulator
641#[derive(Debug, Clone, Copy)]
642pub enum CliffordGate {
643    H(usize),
644    S(usize),
645    X(usize),
646    Y(usize),
647    Z(usize),
648    CNOT(usize, usize),
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    #[test]
656    fn test_sparse_init() {
657        let sim = SparseCliffordSimulator::new(100);
658        let (stab_sparsity, destab_sparsity) = sim.get_sparsity_info();
659
660        // Initial state should be very sparse (only diagonal elements)
661        assert!(stab_sparsity > 0.98);
662        assert!(destab_sparsity > 0.98);
663    }
664
665    #[test]
666    fn test_sparse_hadamard() {
667        let mut sim = SparseCliffordSimulator::new(5);
668        sim.apply_gate(CliffordGate::H(0))
669            .expect("Failed to apply Hadamard gate");
670
671        let stabs = sim.get_stabilizers();
672        assert_eq!(stabs[0], "+XIIII");
673    }
674
675    #[test]
676    fn test_sparse_cnot_chain() {
677        let mut sim = SparseCliffordSimulator::new(10);
678
679        // Create a chain of CNOTs
680        for i in 0..9 {
681            sim.apply_gate(CliffordGate::CNOT(i, i + 1))
682                .expect("Failed to apply CNOT gate");
683        }
684
685        let (stab_sparsity, _) = sim.get_sparsity_info();
686        // Should still be relatively sparse
687        assert!(stab_sparsity > 0.8);
688    }
689}