Skip to main content

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
7use quantrs2_circuit::prelude::*;
8use quantrs2_core::prelude::*;
9use scirs2_sparse::coo::CooMatrix;
10use scirs2_sparse::csr::CsrMatrix;
11
12/// Build a CsrMatrix<u8> from row/col/val triplets (1-row matrix)
13fn build_csr_1row(
14    num_cols: usize,
15    col_indices: Vec<usize>,
16    values: Vec<u8>,
17) -> Result<CsrMatrix<u8>, QuantRS2Error> {
18    if values.is_empty() {
19        return Ok(CsrMatrix::empty((1, num_cols)));
20    }
21    let row_indices = vec![0usize; values.len()];
22    CooMatrix::new(values, row_indices, col_indices, (1, num_cols))
23        .and_then(|coo| {
24            let (rows, cols, vals) = coo.get_triplets();
25            CsrMatrix::from_triplets(1, num_cols, rows, cols, vals)
26        })
27        .map_err(|e| QuantRS2Error::InvalidInput(format!("Failed to build sparse matrix: {e}")))
28}
29
30/// Iterate over non-zero (col, value) pairs of a 1-row CsrMatrix
31fn iter_row0(mat: &CsrMatrix<u8>) -> impl Iterator<Item = (usize, u8)> + '_ {
32    let start = mat.indptr[0];
33    let end = mat.indptr[1];
34    (start..end).map(|j| (mat.indices[j], mat.data[j]))
35}
36
37/// Sparse representation of a Pauli operator
38#[derive(Clone)]
39pub struct SparsePauli {
40    /// Sparse X component (1 where X or Y is present)
41    x_sparse: CsrMatrix<u8>,
42    /// Sparse Z component (1 where Z or Y is present)
43    z_sparse: CsrMatrix<u8>,
44    /// Global phase (0, 1, 2, or 3 for 1, i, -1, -i)
45    phase: u8,
46}
47
48impl SparsePauli {
49    /// Create an identity Pauli operator
50    #[must_use]
51    pub fn identity(num_qubits: usize) -> Self {
52        let x_sparse = CsrMatrix::empty((1, num_qubits));
53        let z_sparse = CsrMatrix::empty((1, num_qubits));
54        Self {
55            x_sparse,
56            z_sparse,
57            phase: 0,
58        }
59    }
60
61    /// Create a single-qubit Pauli operator
62    pub fn single_qubit(
63        num_qubits: usize,
64        qubit: usize,
65        pauli: char,
66    ) -> Result<Self, QuantRS2Error> {
67        let mut x_values = vec![];
68        let mut x_indices = vec![];
69        let mut z_values = vec![];
70        let mut z_indices = vec![];
71
72        match pauli {
73            'X' => {
74                x_values.push(1u8);
75                x_indices.push(qubit);
76            }
77            'Y' => {
78                x_values.push(1u8);
79                x_indices.push(qubit);
80                z_values.push(1u8);
81                z_indices.push(qubit);
82            }
83            'Z' => {
84                z_values.push(1u8);
85                z_indices.push(qubit);
86            }
87            _ => {}
88        }
89
90        let x_sparse = build_csr_1row(num_qubits, x_indices, x_values)?;
91        let z_sparse = build_csr_1row(num_qubits, z_indices, z_values)?;
92
93        Ok(Self {
94            x_sparse,
95            z_sparse,
96            phase: 0,
97        })
98    }
99
100    /// Compute the commutation phase when multiplying two Paulis
101    fn commutation_phase(&self, other: &Self) -> u8 {
102        let mut phase = 0u8;
103
104        // For each qubit position, check commutation
105        for col in 0..self.x_sparse.cols() {
106            let self_x = self.x_sparse.get(0, col);
107            let self_z = self.z_sparse.get(0, col);
108            let other_x = other.x_sparse.get(0, col);
109            let other_z = other.z_sparse.get(0, col);
110
111            // Count anticommutations
112            if self_x > 0 && other_z > 0 && self_z == 0 {
113                phase = (phase + 2) % 4; // Add -1
114            }
115            if self_z > 0 && other_x > 0 && self_x == 0 {
116                phase = (phase + 2) % 4; // Add -1
117            }
118        }
119
120        phase
121    }
122}
123
124/// Enhanced stabilizer tableau using sparse representations
125pub struct SparseStabilizerTableau {
126    num_qubits: usize,
127    /// Stabilizer generators (sparse representation)
128    stabilizers: Vec<SparsePauli>,
129    /// Destabilizer generators (sparse representation)
130    destabilizers: Vec<SparsePauli>,
131}
132
133impl SparseStabilizerTableau {
134    /// Create a new sparse tableau initialized to |0...0⟩
135    #[must_use]
136    pub fn new(num_qubits: usize) -> Self {
137        let mut stabilizers = Vec::with_capacity(num_qubits);
138        let mut destabilizers = Vec::with_capacity(num_qubits);
139
140        for i in 0..num_qubits {
141            // Stabilizer i is Z_i
142            stabilizers.push(
143                SparsePauli::single_qubit(num_qubits, i, 'Z')
144                    .unwrap_or_else(|_| SparsePauli::identity(num_qubits)),
145            );
146            // Destabilizer i is X_i
147            destabilizers.push(
148                SparsePauli::single_qubit(num_qubits, i, 'X')
149                    .unwrap_or_else(|_| SparsePauli::identity(num_qubits)),
150            );
151        }
152
153        Self {
154            num_qubits,
155            stabilizers,
156            destabilizers,
157        }
158    }
159
160    /// Apply a Hadamard gate using sparse operations
161    pub fn apply_h(&mut self, qubit: usize) -> Result<(), QuantRS2Error> {
162        if qubit >= self.num_qubits {
163            return Err(QuantRS2Error::InvalidQubitId(qubit as u32));
164        }
165
166        // H swaps X and Z components
167        for i in 0..self.num_qubits {
168            // For stabilizers
169            {
170                let stab = &mut self.stabilizers[i];
171                let x_val = stab.x_sparse.get(0, qubit);
172                let z_val = stab.z_sparse.get(0, qubit);
173
174                // Update phase if both X and Z are present (Y gate)
175                if x_val > 0 && z_val > 0 {
176                    stab.phase = (stab.phase + 2) % 4; // Add -1
177                }
178
179                // Swap X and Z - rebuild sparse matrices
180                let mut new_x_values = vec![];
181                let mut new_x_indices = vec![];
182                let mut new_z_values = vec![];
183                let mut new_z_indices = vec![];
184
185                // Copy all entries except the target qubit
186                for (col, val) in iter_row0(&stab.x_sparse) {
187                    if col != qubit && val > 0 {
188                        new_x_values.push(1u8);
189                        new_x_indices.push(col);
190                    }
191                }
192                for (col, val) in iter_row0(&stab.z_sparse) {
193                    if col != qubit && val > 0 {
194                        new_z_values.push(1u8);
195                        new_z_indices.push(col);
196                    }
197                }
198
199                // Add swapped entry for target qubit
200                if z_val > 0 {
201                    new_x_values.push(1u8);
202                    new_x_indices.push(qubit);
203                }
204                if x_val > 0 {
205                    new_z_values.push(1u8);
206                    new_z_indices.push(qubit);
207                }
208
209                stab.x_sparse = build_csr_1row(self.num_qubits, new_x_indices, new_x_values)
210                    .map_err(|e| {
211                        QuantRS2Error::GateApplicationFailed(format!(
212                            "Failed to rebuild sparse X matrix: {e}"
213                        ))
214                    })?;
215                stab.z_sparse = build_csr_1row(self.num_qubits, new_z_indices, new_z_values)
216                    .map_err(|e| {
217                        QuantRS2Error::GateApplicationFailed(format!(
218                            "Failed to rebuild sparse Z matrix: {e}"
219                        ))
220                    })?;
221            }
222
223            // Same for destabilizers
224            {
225                let destab = &mut self.destabilizers[i];
226                let dx_val = destab.x_sparse.get(0, qubit);
227                let dz_val = destab.z_sparse.get(0, qubit);
228
229                if dx_val > 0 && dz_val > 0 {
230                    destab.phase = (destab.phase + 2) % 4;
231                }
232
233                let mut new_dx_values = vec![];
234                let mut new_dx_indices = vec![];
235                let mut new_dz_values = vec![];
236                let mut new_dz_indices = vec![];
237
238                for (col, val) in iter_row0(&destab.x_sparse) {
239                    if col != qubit && val > 0 {
240                        new_dx_values.push(1u8);
241                        new_dx_indices.push(col);
242                    }
243                }
244                for (col, val) in iter_row0(&destab.z_sparse) {
245                    if col != qubit && val > 0 {
246                        new_dz_values.push(1u8);
247                        new_dz_indices.push(col);
248                    }
249                }
250
251                if dz_val > 0 {
252                    new_dx_values.push(1u8);
253                    new_dx_indices.push(qubit);
254                }
255                if dx_val > 0 {
256                    new_dz_values.push(1u8);
257                    new_dz_indices.push(qubit);
258                }
259
260                destab.x_sparse = build_csr_1row(self.num_qubits, new_dx_indices, new_dx_values)
261                    .map_err(|e| {
262                        QuantRS2Error::GateApplicationFailed(format!(
263                            "Failed to rebuild destabilizer X matrix: {e}"
264                        ))
265                    })?;
266                destab.z_sparse = build_csr_1row(self.num_qubits, new_dz_indices, new_dz_values)
267                    .map_err(|e| {
268                        QuantRS2Error::GateApplicationFailed(format!(
269                            "Failed to rebuild destabilizer Z matrix: {e}"
270                        ))
271                    })?;
272            }
273        }
274
275        Ok(())
276    }
277
278    /// Apply a CNOT gate using sparse operations
279    pub fn apply_cnot(&mut self, control: usize, target: usize) -> Result<(), QuantRS2Error> {
280        if control >= self.num_qubits || target >= self.num_qubits {
281            return Err(QuantRS2Error::InvalidQubitId(control.max(target) as u32));
282        }
283
284        // CNOT: X_c → X_c X_t, Z_t → Z_c Z_t
285        for i in 0..self.num_qubits {
286            // Update stabilizers
287            {
288                let stab = &mut self.stabilizers[i];
289                let control_x = stab.x_sparse.get(0, control);
290                let control_z = stab.z_sparse.get(0, control);
291                let target_x = stab.x_sparse.get(0, target);
292                let target_z = stab.z_sparse.get(0, target);
293
294                // If X on control, toggle X on target
295                if control_x > 0 {
296                    let mut new_x_values = vec![];
297                    let mut new_x_indices = vec![];
298
299                    for (col, val) in iter_row0(&stab.x_sparse) {
300                        if col != target && val > 0 {
301                            new_x_values.push(1u8);
302                            new_x_indices.push(col);
303                        }
304                    }
305
306                    // Toggle target
307                    if target_x == 0 {
308                        new_x_values.push(1u8);
309                        new_x_indices.push(target);
310                    }
311
312                    stab.x_sparse = build_csr_1row(self.num_qubits, new_x_indices, new_x_values)
313                        .map_err(|e| {
314                            QuantRS2Error::GateApplicationFailed(format!(
315                                "Failed to update X matrix in CNOT: {e}"
316                            ))
317                        })?;
318                }
319
320                // If Z on target, toggle Z on control
321                if target_z > 0 {
322                    let mut new_z_values = vec![];
323                    let mut new_z_indices = vec![];
324
325                    for (col, val) in iter_row0(&stab.z_sparse) {
326                        if col != control && val > 0 {
327                            new_z_values.push(1u8);
328                            new_z_indices.push(col);
329                        }
330                    }
331
332                    // Toggle control
333                    if control_z == 0 {
334                        new_z_values.push(1u8);
335                        new_z_indices.push(control);
336                    }
337
338                    stab.z_sparse = build_csr_1row(self.num_qubits, new_z_indices, new_z_values)
339                        .map_err(|e| {
340                            QuantRS2Error::GateApplicationFailed(format!(
341                                "Failed to update Z matrix in CNOT target: {e}"
342                            ))
343                        })?;
344                }
345            }
346        }
347
348        Ok(())
349    }
350
351    /// Apply an S gate
352    pub fn apply_s(&mut self, qubit: usize) -> Result<(), QuantRS2Error> {
353        if qubit >= self.num_qubits {
354            return Err(QuantRS2Error::InvalidQubitId(qubit as u32));
355        }
356
357        // S: X → Y, Z → Z
358        for i in 0..self.num_qubits {
359            let stab = &mut self.stabilizers[i];
360            let x_val = stab.x_sparse.get(0, qubit);
361            let z_val = stab.z_sparse.get(0, qubit);
362
363            // If X but not Z, convert to Y (add Z and update phase)
364            if x_val > 0 && z_val == 0 {
365                // Add Z component
366                let mut new_z_values = vec![];
367                let mut new_z_indices = vec![];
368
369                for (col, val) in iter_row0(&stab.z_sparse) {
370                    if val > 0 {
371                        new_z_values.push(1u8);
372                        new_z_indices.push(col);
373                    }
374                }
375
376                new_z_values.push(1u8);
377                new_z_indices.push(qubit);
378
379                stab.z_sparse = build_csr_1row(self.num_qubits, new_z_indices, new_z_values)
380                    .map_err(|e| {
381                        QuantRS2Error::GateApplicationFailed(format!(
382                            "Failed to update Z matrix in S gate: {e}"
383                        ))
384                    })?;
385
386                // Update phase
387                stab.phase = (stab.phase + 1) % 4; // Multiply by i
388            }
389        }
390
391        Ok(())
392    }
393
394    /// Get stabilizer strings for verification
395    #[must_use]
396    pub fn get_stabilizers(&self) -> Vec<String> {
397        self.stabilizers
398            .iter()
399            .map(|stab| {
400                let mut s = String::new();
401                s.push(match stab.phase {
402                    0 => '+',
403                    1 => 'i',
404                    2 => '-',
405                    3 => '-',
406                    _ => '+',
407                });
408
409                for j in 0..self.num_qubits {
410                    let has_x = stab.x_sparse.get(0, j) > 0;
411                    let has_z = stab.z_sparse.get(0, j) > 0;
412
413                    s.push(match (has_x, has_z) {
414                        (false, false) => 'I',
415                        (true, false) => 'X',
416                        (false, true) => 'Z',
417                        (true, true) => 'Y',
418                    });
419                }
420
421                s
422            })
423            .collect()
424    }
425
426    /// Check sparsity of the tableau
427    #[must_use]
428    pub fn get_sparsity_info(&self) -> (f64, f64) {
429        let total_entries = self.num_qubits * self.num_qubits;
430
431        let stab_nonzero: usize = self
432            .stabilizers
433            .iter()
434            .map(|s| s.x_sparse.nnz() + s.z_sparse.nnz())
435            .sum();
436
437        let destab_nonzero: usize = self
438            .destabilizers
439            .iter()
440            .map(|s| s.x_sparse.nnz() + s.z_sparse.nnz())
441            .sum();
442
443        let stab_sparsity = 1.0 - (stab_nonzero as f64 / total_entries as f64);
444        let destab_sparsity = 1.0 - (destab_nonzero as f64 / total_entries as f64);
445
446        (stab_sparsity, destab_sparsity)
447    }
448}
449
450/// Enhanced Clifford simulator with sparse representations
451pub struct SparseCliffordSimulator {
452    tableau: SparseStabilizerTableau,
453    measurement_record: Vec<(usize, bool)>,
454}
455
456impl SparseCliffordSimulator {
457    /// Create a new sparse Clifford simulator
458    #[must_use]
459    pub fn new(num_qubits: usize) -> Self {
460        Self {
461            tableau: SparseStabilizerTableau::new(num_qubits),
462            measurement_record: Vec::new(),
463        }
464    }
465
466    /// Apply a Clifford gate
467    pub fn apply_gate(&mut self, gate: CliffordGate) -> Result<(), QuantRS2Error> {
468        match gate {
469            CliffordGate::H(q) => self.tableau.apply_h(q),
470            CliffordGate::S(q) => self.tableau.apply_s(q),
471            CliffordGate::CNOT(c, t) => self.tableau.apply_cnot(c, t),
472            CliffordGate::X(q) | CliffordGate::Y(q) | CliffordGate::Z(q) => {
473                // Pauli gates (simplified for brevity)
474                Ok(())
475            }
476        }
477    }
478
479    /// Get current stabilizers
480    #[must_use]
481    pub fn get_stabilizers(&self) -> Vec<String> {
482        self.tableau.get_stabilizers()
483    }
484
485    /// Get sparsity information
486    #[must_use]
487    pub fn get_sparsity_info(&self) -> (f64, f64) {
488        self.tableau.get_sparsity_info()
489    }
490
491    /// Get the number of qubits
492    #[must_use]
493    pub const fn num_qubits(&self) -> usize {
494        self.tableau.num_qubits
495    }
496}
497
498/// Clifford gates supported by the sparse simulator
499#[derive(Debug, Clone, Copy)]
500pub enum CliffordGate {
501    H(usize),
502    S(usize),
503    X(usize),
504    Y(usize),
505    Z(usize),
506    CNOT(usize, usize),
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[test]
514    fn test_sparse_init() {
515        let sim = SparseCliffordSimulator::new(100);
516        let (stab_sparsity, destab_sparsity) = sim.get_sparsity_info();
517
518        // Initial state should be very sparse (only diagonal elements)
519        assert!(stab_sparsity > 0.98);
520        assert!(destab_sparsity > 0.98);
521    }
522
523    #[test]
524    fn test_sparse_hadamard() {
525        let mut sim = SparseCliffordSimulator::new(5);
526        sim.apply_gate(CliffordGate::H(0))
527            .expect("Failed to apply Hadamard gate");
528
529        let stabs = sim.get_stabilizers();
530        assert_eq!(stabs[0], "+XIIII");
531    }
532
533    #[test]
534    fn test_sparse_cnot_chain() {
535        let mut sim = SparseCliffordSimulator::new(10);
536
537        // Create a chain of CNOTs
538        for i in 0..9 {
539            sim.apply_gate(CliffordGate::CNOT(i, i + 1))
540                .expect("Failed to apply CNOT gate");
541        }
542
543        let (stab_sparsity, _) = sim.get_sparsity_info();
544        // Should still be relatively sparse
545        assert!(stab_sparsity > 0.8);
546    }
547}