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