Skip to main content

oxicuda_quantum/statevec/
sparse.rs

1//! Sparse state-vector representation for low-occupancy circuits.
2//!
3//! Many circuits of interest keep the state supported on only a *small* subset of
4//! the `2^n` computational-basis kets at any time: a permutation circuit (X /
5//! CNOT / Toffoli) keeps it a single basis state; an oracle / arithmetic circuit
6//! touches only a polynomial number of branches; the early layers of a sparse
7//! ansatz stay near `|0…0⟩`. Storing the full dense `2^n` amplitude array is then
8//! wasteful.
9//!
10//! [`SparseStateVector`] stores only the **nonzero** amplitudes in a hash map
11//! keyed by the basis index. Single-qubit and CNOT/Toffoli gate application
12//! operate directly on the populated entries, allocating new keys only when an
13//! amplitude actually becomes nonzero, so the time and memory cost scales with
14//! the *occupancy* (number of nonzero amplitudes) rather than with `2^n`.
15//! Near-zero amplitudes are pruned after each operation to keep the
16//! representation tight.
17
18use std::collections::HashMap;
19
20use num_complex::Complex;
21
22use crate::error::{QuantumError, QuantumResult};
23use crate::statevec::state::StateVector;
24
25type Complex32 = Complex<f32>;
26
27/// Amplitudes with magnitude below this are dropped from the sparse map.
28const PRUNE_EPS: f32 = 1e-12;
29
30/// A sparse pure state: only nonzero basis amplitudes are stored.
31#[derive(Debug, Clone)]
32pub struct SparseStateVector {
33    /// Map from computational-basis index → amplitude (only nonzero kept).
34    amps: HashMap<usize, Complex32>,
35    /// Number of qubits.
36    pub n_qubits: usize,
37}
38
39impl SparseStateVector {
40    /// Construct the sparse `|0…0⟩` state (a single populated key).
41    ///
42    /// # Errors
43    /// Returns [`QuantumError::InvalidQubitCount`] for `n_qubits == 0` or
44    /// `n_qubits > 30`.
45    pub fn new_zero_state(n_qubits: usize) -> QuantumResult<Self> {
46        if n_qubits == 0 || n_qubits > 30 {
47            return Err(QuantumError::InvalidQubitCount { n: n_qubits });
48        }
49        let mut amps = HashMap::new();
50        amps.insert(0usize, Complex32::new(1.0, 0.0));
51        Ok(Self { amps, n_qubits })
52    }
53
54    /// Build a sparse representation from a dense state vector (drops zeros).
55    #[must_use]
56    pub fn from_dense(sv: &StateVector) -> Self {
57        let mut amps = HashMap::new();
58        for (i, a) in sv.amps.iter().enumerate() {
59            if a.norm_sqr() > PRUNE_EPS * PRUNE_EPS {
60                amps.insert(i, *a);
61            }
62        }
63        Self {
64            amps,
65            n_qubits: sv.n_qubits,
66        }
67    }
68
69    /// Expand back to a dense [`StateVector`].
70    ///
71    /// # Errors
72    /// Propagates the dense constructor's qubit-count validation.
73    pub fn to_dense(&self) -> QuantumResult<StateVector> {
74        let dim = 1usize << self.n_qubits;
75        let mut dense = vec![Complex32::new(0.0, 0.0); dim];
76        for (&i, &a) in &self.amps {
77            if i < dim {
78                dense[i] = a;
79            }
80        }
81        Ok(StateVector {
82            amps: dense,
83            n_qubits: self.n_qubits,
84        })
85    }
86
87    /// Number of stored (nonzero) amplitudes.
88    #[must_use]
89    pub fn occupancy(&self) -> usize {
90        self.amps.len()
91    }
92
93    /// Squared norm Σ|a|².
94    #[must_use]
95    pub fn norm_sq(&self) -> f32 {
96        self.amps.values().map(|a| a.norm_sqr()).sum()
97    }
98
99    /// Amplitude at a basis index (zero if not stored).
100    #[must_use]
101    pub fn amp(&self, idx: usize) -> Complex32 {
102        self.amps
103            .get(&idx)
104            .copied()
105            .unwrap_or_else(|| Complex32::new(0.0, 0.0))
106    }
107
108    /// Remove entries that have decayed below the prune threshold.
109    fn prune(&mut self) {
110        self.amps
111            .retain(|_, a| a.norm_sqr() > PRUNE_EPS * PRUNE_EPS);
112    }
113
114    /// Apply a single-qubit gate to `qubit`.
115    ///
116    /// Iterates the current support, pairs each key with its `qubit`-flipped
117    /// partner, and writes the mixed amplitudes into a fresh map (so concurrent
118    /// reads of the old amplitudes are consistent).
119    ///
120    /// # Errors
121    /// Returns [`QuantumError::QubitIndexOutOfRange`] if `qubit >= n_qubits`.
122    pub fn apply_1q(&mut self, qubit: usize, gate: &[[Complex32; 2]; 2]) -> QuantumResult<()> {
123        if qubit >= self.n_qubits {
124            return Err(QuantumError::QubitIndexOutOfRange {
125                index: qubit,
126                n_qubits: self.n_qubits,
127            });
128        }
129        let mask = 1usize << qubit;
130
131        // Collect the set of "low" keys (qubit bit = 0) that need processing; for
132        // every populated key, its partner is key ^ mask.
133        let mut low_keys: Vec<usize> = Vec::new();
134        let mut seen: std::collections::HashSet<usize> = std::collections::HashSet::new();
135        for &k in self.amps.keys() {
136            let low = k & !mask;
137            if seen.insert(low) {
138                low_keys.push(low);
139            }
140        }
141
142        let mut new_amps = self.amps.clone();
143        for low in low_keys {
144            let high = low | mask;
145            let x0 = self.amp(low);
146            let x1 = self.amp(high);
147            let y0 = gate[0][0] * x0 + gate[0][1] * x1;
148            let y1 = gate[1][0] * x0 + gate[1][1] * x1;
149            write_or_remove(&mut new_amps, low, y0);
150            write_or_remove(&mut new_amps, high, y1);
151        }
152        self.amps = new_amps;
153        self.prune();
154        Ok(())
155    }
156
157    /// Apply a CNOT with the given control and target.
158    ///
159    /// A CNOT only permutes basis indices (flips `tgt` when `ctrl=1`), so this is
160    /// a pure key remap that exactly preserves occupancy.
161    ///
162    /// # Errors
163    /// Returns [`QuantumError::QubitIndexOutOfRange`] for an out-of-range index or
164    /// [`QuantumError::InvalidParameter`] if `ctrl == tgt`.
165    pub fn apply_cnot(&mut self, ctrl: usize, tgt: usize) -> QuantumResult<()> {
166        if ctrl >= self.n_qubits {
167            return Err(QuantumError::QubitIndexOutOfRange {
168                index: ctrl,
169                n_qubits: self.n_qubits,
170            });
171        }
172        if tgt >= self.n_qubits {
173            return Err(QuantumError::QubitIndexOutOfRange {
174                index: tgt,
175                n_qubits: self.n_qubits,
176            });
177        }
178        if ctrl == tgt {
179            return Err(QuantumError::InvalidParameter {
180                name: "ctrl and tgt must differ".into(),
181            });
182        }
183        let cmask = 1usize << ctrl;
184        let tmask = 1usize << tgt;
185        let mut new_amps = HashMap::with_capacity(self.amps.len());
186        for (&k, &a) in &self.amps {
187            let nk = if k & cmask != 0 { k ^ tmask } else { k };
188            new_amps.insert(nk, a);
189        }
190        self.amps = new_amps;
191        Ok(())
192    }
193
194    /// Apply a Toffoli (CCX): flips `tgt` when both controls are 1. Pure key remap.
195    ///
196    /// # Errors
197    /// Returns an error for out-of-range or non-distinct qubit indices.
198    pub fn apply_ccx(&mut self, c0: usize, c1: usize, tgt: usize) -> QuantumResult<()> {
199        for &q in &[c0, c1, tgt] {
200            if q >= self.n_qubits {
201                return Err(QuantumError::QubitIndexOutOfRange {
202                    index: q,
203                    n_qubits: self.n_qubits,
204                });
205            }
206        }
207        if c0 == c1 || c0 == tgt || c1 == tgt {
208            return Err(QuantumError::InvalidParameter {
209                name: "controls and target must be distinct".into(),
210            });
211        }
212        let m0 = 1usize << c0;
213        let m1 = 1usize << c1;
214        let tmask = 1usize << tgt;
215        let mut new_amps = HashMap::with_capacity(self.amps.len());
216        for (&k, &a) in &self.amps {
217            let nk = if (k & m0 != 0) && (k & m1 != 0) {
218                k ^ tmask
219            } else {
220                k
221            };
222            new_amps.insert(nk, a);
223        }
224        self.amps = new_amps;
225        Ok(())
226    }
227
228    /// Normalize to unit norm (no-op if ~zero).
229    pub fn normalize(&mut self) {
230        let norm = self.norm_sq().sqrt();
231        if norm > 1e-20 {
232            let inv = 1.0 / norm;
233            for a in self.amps.values_mut() {
234                *a *= inv;
235            }
236        }
237    }
238}
239
240/// Insert `value` at `key`, or remove the key if `value` is ~zero.
241fn write_or_remove(map: &mut HashMap<usize, Complex32>, key: usize, value: Complex32) {
242    if value.norm_sqr() > PRUNE_EPS * PRUNE_EPS {
243        map.insert(key, value);
244    } else {
245        map.remove(&key);
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::gates::controlled::apply_cnot as dense_cnot;
253    use crate::gates::hadamard::gate_h;
254    use crate::gates::pauli::gate_x;
255    use crate::statevec::apply_1q::apply_1q_inplace;
256
257    #[test]
258    fn zero_state_occupancy_is_one() {
259        let s = SparseStateVector::new_zero_state(5).expect("valid");
260        assert_eq!(s.occupancy(), 1);
261        assert!((s.norm_sq() - 1.0).abs() < 1e-6);
262        assert!((s.amp(0).re - 1.0).abs() < 1e-6);
263    }
264
265    #[test]
266    fn permutation_circuit_stays_sparse() {
267        // X on a few qubits + CNOT ladder keeps occupancy = 1 (a single basis ket).
268        let mut s = SparseStateVector::new_zero_state(8).expect("valid");
269        s.apply_1q(0, &gate_x()).expect("x0");
270        s.apply_1q(3, &gate_x()).expect("x3");
271        s.apply_cnot(0, 1).expect("cnot");
272        s.apply_ccx(0, 3, 5).expect("ccx");
273        assert_eq!(s.occupancy(), 1, "permutation must keep occupancy 1");
274        assert!((s.norm_sq() - 1.0).abs() < 1e-6);
275    }
276
277    #[test]
278    fn hadamard_doubles_occupancy() {
279        let mut s = SparseStateVector::new_zero_state(3).expect("valid");
280        s.apply_1q(0, &gate_h()).expect("h");
281        assert_eq!(s.occupancy(), 2);
282        let inv_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
283        assert!((s.amp(0).re - inv_sqrt2).abs() < 1e-5);
284        assert!((s.amp(1).re - inv_sqrt2).abs() < 1e-5);
285    }
286
287    #[test]
288    fn bell_state_matches_dense() {
289        // Build Bell state sparsely and densely; compare amplitudes.
290        let mut s = SparseStateVector::new_zero_state(2).expect("valid");
291        s.apply_1q(0, &gate_h()).expect("h");
292        s.apply_cnot(0, 1).expect("cnot");
293
294        let mut dv = StateVector::new_zero_state(2).expect("valid");
295        apply_1q_inplace(&mut dv, 0, &gate_h()).expect("h");
296        dense_cnot(&mut dv, 0, 1).expect("cnot");
297
298        let recon = s.to_dense().expect("dense");
299        for i in 0..4 {
300            assert!(
301                (recon.amps[i] - dv.amps[i]).norm() < 1e-5,
302                "idx {i}: sparse {:?} dense {:?}",
303                recon.amps[i],
304                dv.amps[i]
305            );
306        }
307    }
308
309    #[test]
310    fn roundtrip_dense_sparse_dense() {
311        let mut dv = StateVector::new_zero_state(3).expect("valid");
312        apply_1q_inplace(&mut dv, 0, &gate_h()).expect("h0");
313        apply_1q_inplace(&mut dv, 2, &gate_h()).expect("h2");
314        let sparse = SparseStateVector::from_dense(&dv);
315        let back = sparse.to_dense().expect("dense");
316        for (a, b) in back.amps.iter().zip(dv.amps.iter()) {
317            assert!((a - b).norm() < 1e-6);
318        }
319    }
320
321    #[test]
322    fn out_of_range_rejected() {
323        let mut s = SparseStateVector::new_zero_state(2).expect("valid");
324        assert!(s.apply_1q(5, &gate_x()).is_err());
325        assert!(s.apply_cnot(0, 0).is_err());
326    }
327}