Skip to main content

quantrs2_core/gpu/
cpu_backend.rs

1//! CPU backend implementation for GPU abstraction
2//!
3//! This provides a CPU-based fallback implementation of the GPU backend
4//! interface, useful for testing and systems without GPU support.
5
6use super::{GpuBackend, GpuBuffer, GpuKernel};
7use crate::{
8    error::{QuantRS2Error, QuantRS2Result},
9    qubit::QubitId,
10};
11use scirs2_core::ndarray::Array2;
12use scirs2_core::Complex64;
13use std::sync::{Arc, Mutex};
14
15/// CPU-based buffer implementation
16pub struct CpuBuffer {
17    data: Arc<Mutex<Vec<Complex64>>>,
18}
19
20impl CpuBuffer {
21    /// Create a new CPU buffer
22    pub fn new(size: usize) -> Self {
23        Self {
24            data: Arc::new(Mutex::new(vec![Complex64::new(0.0, 0.0); size])),
25        }
26    }
27
28    /// Get a reference to the data
29    pub fn data(&self) -> std::sync::MutexGuard<'_, Vec<Complex64>> {
30        self.data.lock().unwrap_or_else(|e| e.into_inner())
31    }
32}
33
34impl GpuBuffer for CpuBuffer {
35    fn size(&self) -> usize {
36        self.data.lock().unwrap_or_else(|e| e.into_inner()).len() * std::mem::size_of::<Complex64>()
37    }
38
39    fn upload(&mut self, data: &[Complex64]) -> QuantRS2Result<()> {
40        let mut buffer = self.data.lock().unwrap_or_else(|e| e.into_inner());
41        if buffer.len() != data.len() {
42            return Err(QuantRS2Error::InvalidInput(format!(
43                "Buffer size mismatch: {} != {}",
44                buffer.len(),
45                data.len()
46            )));
47        }
48        buffer.copy_from_slice(data);
49        Ok(())
50    }
51
52    fn download(&self, data: &mut [Complex64]) -> QuantRS2Result<()> {
53        let buffer = self.data.lock().unwrap_or_else(|e| e.into_inner());
54        if buffer.len() != data.len() {
55            return Err(QuantRS2Error::InvalidInput(format!(
56                "Buffer size mismatch: {} != {}",
57                buffer.len(),
58                data.len()
59            )));
60        }
61        data.copy_from_slice(&buffer);
62        Ok(())
63    }
64
65    fn sync(&self) -> QuantRS2Result<()> {
66        // No-op for CPU backend
67        Ok(())
68    }
69
70    fn as_any(&self) -> &dyn std::any::Any {
71        self
72    }
73
74    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
75        self
76    }
77}
78
79/// CPU-based kernel implementation
80pub struct CpuKernel;
81
82impl CpuKernel {
83    /// Apply a gate matrix to specific qubit indices
84    fn apply_gate_to_indices(state: &mut [Complex64], gate: &[Complex64], indices: &[usize]) {
85        let gate_size = indices.len();
86        let mut temp = vec![Complex64::new(0.0, 0.0); gate_size];
87
88        // Read values
89        for (i, &idx) in indices.iter().enumerate() {
90            temp[i] = state[idx];
91        }
92
93        // Apply gate
94        for (i, &idx) in indices.iter().enumerate() {
95            let mut sum = Complex64::new(0.0, 0.0);
96            for j in 0..gate_size {
97                sum += gate[i * gate_size + j] * temp[j];
98            }
99            state[idx] = sum;
100        }
101    }
102}
103
104impl GpuKernel for CpuKernel {
105    fn apply_single_qubit_gate(
106        &self,
107        state: &mut dyn GpuBuffer,
108        gate_matrix: &[Complex64; 4],
109        qubit: QubitId,
110        n_qubits: usize,
111    ) -> QuantRS2Result<()> {
112        let cpu_buffer = state
113            .as_any_mut()
114            .downcast_mut::<CpuBuffer>()
115            .ok_or_else(|| QuantRS2Error::InvalidInput("Expected CpuBuffer".to_string()))?;
116
117        let mut data = cpu_buffer.data();
118        let qubit_idx = qubit.0 as usize;
119        let stride = 1 << qubit_idx;
120        let pairs = 1 << (n_qubits - 1);
121
122        // Apply gate using bit manipulation
123        for i in 0..pairs {
124            let i0 = ((i >> qubit_idx) << (qubit_idx + 1)) | (i & ((1 << qubit_idx) - 1));
125            let i1 = i0 | stride;
126
127            let a = data[i0];
128            let b = data[i1];
129
130            data[i0] = gate_matrix[0] * a + gate_matrix[1] * b;
131            data[i1] = gate_matrix[2] * a + gate_matrix[3] * b;
132        }
133
134        Ok(())
135    }
136
137    fn apply_two_qubit_gate(
138        &self,
139        state: &mut dyn GpuBuffer,
140        gate_matrix: &[Complex64; 16],
141        control: QubitId,
142        target: QubitId,
143        n_qubits: usize,
144    ) -> QuantRS2Result<()> {
145        let cpu_buffer = state
146            .as_any_mut()
147            .downcast_mut::<CpuBuffer>()
148            .ok_or_else(|| QuantRS2Error::InvalidInput("Expected CpuBuffer".to_string()))?;
149
150        let mut data = cpu_buffer.data();
151        let control_idx = control.0 as usize;
152        let target_idx = target.0 as usize;
153
154        if control_idx >= n_qubits || target_idx >= n_qubits {
155            return Err(QuantRS2Error::InvalidInput(format!(
156                "Qubit index out of range: control={control_idx}, target={target_idx}, \
157                 n_qubits={n_qubits}"
158            )));
159        }
160        if control_idx == target_idx {
161            return Err(QuantRS2Error::InvalidInput(format!(
162                "Control and target must differ, both are qubit {control_idx}"
163            )));
164        }
165
166        // Basis convention: qubit `q` occupies bit `q` of the state-vector index,
167        // while the 4x4 gate matrix is ordered |control target> with the *control*
168        // as the high bit of the local index (row/col 2 and 3 have control = 1).
169        // The two orderings are independent -- mapping the local index by numeric
170        // qubit order instead of by the control/target roles silently exchanges the
171        // two operands whenever `control < target`.
172        let control_stride = 1usize << control_idx;
173        let target_stride = 1usize << target_idx;
174
175        let (high_idx, low_idx) = if control_idx > target_idx {
176            (control_idx, target_idx)
177        } else {
178            (target_idx, control_idx)
179        };
180
181        // Expand a dense counter over the `n_qubits - 2` spectator qubits into a
182        // state-vector index with bit `low_idx` and bit `high_idx` cleared.
183        let low_mask = (1usize << low_idx) - 1;
184        let mid_mask = ((1usize << (high_idx - 1)) - 1) ^ low_mask;
185        let groups = 1usize << (n_qubits - 2);
186
187        for group in 0..groups {
188            let base = (group & low_mask)
189                | ((group & mid_mask) << 1)
190                | ((group >> (high_idx - 1)) << (high_idx + 1));
191
192            let indices = [
193                base,
194                base | target_stride,
195                base | control_stride,
196                base | control_stride | target_stride,
197            ];
198
199            Self::apply_gate_to_indices(&mut data, gate_matrix, &indices);
200        }
201
202        Ok(())
203    }
204
205    fn apply_multi_qubit_gate(
206        &self,
207        state: &mut dyn GpuBuffer,
208        gate_matrix: &Array2<Complex64>,
209        qubits: &[QubitId],
210        n_qubits: usize,
211    ) -> QuantRS2Result<()> {
212        let cpu_buffer = state
213            .as_any_mut()
214            .downcast_mut::<CpuBuffer>()
215            .ok_or_else(|| QuantRS2Error::InvalidInput("Expected CpuBuffer".to_string()))?;
216
217        let mut data = cpu_buffer.data();
218        let gate_qubits = qubits.len();
219        let gate_dim = 1 << gate_qubits;
220
221        if gate_matrix.dim() != (gate_dim, gate_dim) {
222            return Err(QuantRS2Error::InvalidInput(format!(
223                "Gate matrix dimension mismatch: {:?} != ({}, {})",
224                gate_matrix.dim(),
225                gate_dim,
226                gate_dim
227            )));
228        }
229
230        // Operand order carries meaning: `qubits[0]` is the most significant bit of
231        // the gate's local basis index, `qubits[gate_qubits - 1]` the least. Sorting
232        // the list before mapping local index bits onto state-vector bits would
233        // permute the gate's operands, so the caller-supplied order is kept here and
234        // a sorted copy is used only to work out which bits are spectators.
235        let operand_indices: Vec<usize> = qubits.iter().map(|q| q.0 as usize).collect();
236
237        if let Some(&bad) = operand_indices.iter().find(|&&q| q >= n_qubits) {
238            return Err(QuantRS2Error::InvalidInput(format!(
239                "Qubit index {bad} out of range for a {n_qubits}-qubit state"
240            )));
241        }
242
243        let mut sorted_indices = operand_indices.clone();
244        sorted_indices.sort_unstable();
245        if sorted_indices.windows(2).any(|w| w[0] == w[1]) {
246            return Err(QuantRS2Error::InvalidInput(format!(
247                "Gate operands must be distinct qubits, got {operand_indices:?}"
248            )));
249        }
250
251        // Convert gate matrix to flat array for easier indexing
252        let gate_flat: Vec<Complex64> = gate_matrix.iter().copied().collect();
253
254        // Calculate indices for all affected basis states
255        let affected_states = 1usize << gate_qubits;
256        let unaffected_qubits = n_qubits - gate_qubits;
257        let iterations = 1usize << unaffected_qubits;
258
259        let mut indices = vec![0usize; affected_states];
260
261        // Apply gate to each group of affected states
262        for i in 0..iterations {
263            // Scatter the spectator counter across the bit positions the gate
264            // does not touch, leaving every operand bit clear.
265            let mut base = 0usize;
266            let mut remaining = i;
267            let mut qubit_pos = 0;
268
269            for bit in 0..n_qubits {
270                if qubit_pos < gate_qubits && bit == sorted_indices[qubit_pos] {
271                    qubit_pos += 1;
272                } else {
273                    if remaining & 1 == 1 {
274                        base |= 1 << bit;
275                    }
276                    remaining >>= 1;
277                }
278            }
279
280            // Generate all indices for this gate application, MSB-first over the
281            // operand list so local basis state |q0 q1 ... qk> lines up with the
282            // row/column ordering of `gate_matrix`.
283            for (j, slot) in indices.iter_mut().enumerate() {
284                let mut idx = base;
285                for (position, &qubit_idx) in operand_indices.iter().enumerate() {
286                    if (j >> (gate_qubits - 1 - position)) & 1 == 1 {
287                        idx |= 1 << qubit_idx;
288                    }
289                }
290                *slot = idx;
291            }
292
293            Self::apply_gate_to_indices(&mut data, &gate_flat, &indices);
294        }
295
296        Ok(())
297    }
298
299    fn measure_qubit(
300        &self,
301        state: &dyn GpuBuffer,
302        qubit: QubitId,
303        n_qubits: usize,
304    ) -> QuantRS2Result<(bool, f64)> {
305        let cpu_buffer = state
306            .as_any()
307            .downcast_ref::<CpuBuffer>()
308            .ok_or_else(|| QuantRS2Error::InvalidInput("Expected CpuBuffer".to_string()))?;
309
310        let data = cpu_buffer.data();
311        let qubit_idx = qubit.0 as usize;
312        // let _stride = 1 << qubit_idx;
313
314        // Calculate probability of measuring |1⟩
315        let mut prob_one = 0.0;
316        for i in 0..(1 << n_qubits) {
317            if (i >> qubit_idx) & 1 == 1 {
318                prob_one += data[i].norm_sqr();
319            }
320        }
321
322        // Simulate measurement
323        use scirs2_core::random::prelude::*;
324        let outcome = thread_rng().random::<f64>() < prob_one;
325
326        Ok((outcome, if outcome { prob_one } else { 1.0 - prob_one }))
327    }
328
329    fn expectation_value(
330        &self,
331        state: &dyn GpuBuffer,
332        observable: &Array2<Complex64>,
333        qubits: &[QubitId],
334        n_qubits: usize,
335    ) -> QuantRS2Result<f64> {
336        let cpu_buffer = state
337            .as_any()
338            .downcast_ref::<CpuBuffer>()
339            .ok_or_else(|| QuantRS2Error::InvalidInput("Expected CpuBuffer".to_string()))?;
340
341        let data = cpu_buffer.data();
342
343        // For now, implement expectation value for single-qubit observables
344        if qubits.len() != 1 || observable.dim() != (2, 2) {
345            return Err(QuantRS2Error::UnsupportedOperation(
346                "Only single-qubit observables supported currently".to_string(),
347            ));
348        }
349
350        let qubit_idx = qubits[0].0 as usize;
351        let stride = 1 << qubit_idx;
352        let pairs = 1 << (n_qubits - 1);
353
354        let mut expectation = Complex64::new(0.0, 0.0);
355
356        for i in 0..pairs {
357            let i0 = ((i >> qubit_idx) << (qubit_idx + 1)) | (i & ((1 << qubit_idx) - 1));
358            let i1 = i0 | stride;
359
360            let a = data[i0];
361            let b = data[i1];
362
363            expectation += a.conj() * (observable[(0, 0)] * a + observable[(0, 1)] * b);
364            expectation += b.conj() * (observable[(1, 0)] * a + observable[(1, 1)] * b);
365        }
366
367        if expectation.im.abs() > 1e-10 {
368            return Err(QuantRS2Error::InvalidInput(
369                "Observable expectation value is not real".to_string(),
370            ));
371        }
372
373        Ok(expectation.re)
374    }
375}
376
377/// CPU backend implementation
378pub struct CpuBackend {
379    kernel: CpuKernel,
380}
381
382impl CpuBackend {
383    /// Create a new CPU backend
384    pub const fn new() -> Self {
385        Self { kernel: CpuKernel }
386    }
387}
388
389impl Default for CpuBackend {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395impl GpuBackend for CpuBackend {
396    fn is_available() -> bool {
397        true // CPU is always available
398    }
399
400    fn name(&self) -> &'static str {
401        "CPU"
402    }
403
404    fn device_info(&self) -> String {
405        // Use scirs2_core::parallel_ops (SciRS2 POLICY compliant)
406        use scirs2_core::parallel_ops::current_num_threads;
407        format!("CPU backend with {} threads", current_num_threads())
408    }
409
410    fn allocate_state_vector(&self, n_qubits: usize) -> QuantRS2Result<Box<dyn GpuBuffer>> {
411        let size = 1 << n_qubits;
412        Ok(Box::new(CpuBuffer::new(size)))
413    }
414
415    fn allocate_density_matrix(&self, n_qubits: usize) -> QuantRS2Result<Box<dyn GpuBuffer>> {
416        let size = 1 << (2 * n_qubits);
417        Ok(Box::new(CpuBuffer::new(size)))
418    }
419
420    fn kernel(&self) -> &dyn GpuKernel {
421        &self.kernel
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn test_cpu_buffer() {
431        let mut buffer = CpuBuffer::new(4);
432        let data = vec![
433            Complex64::new(1.0, 0.0),
434            Complex64::new(0.0, 1.0),
435            Complex64::new(-1.0, 0.0),
436            Complex64::new(0.0, -1.0),
437        ];
438
439        buffer
440            .upload(&data)
441            .expect("Failed to upload data to buffer");
442
443        let mut downloaded = vec![Complex64::new(0.0, 0.0); 4];
444        buffer
445            .download(&mut downloaded)
446            .expect("Failed to download data from buffer");
447
448        assert_eq!(data, downloaded);
449    }
450
451    #[test]
452    fn test_cpu_backend() {
453        let backend = CpuBackend::new();
454        assert!(CpuBackend::is_available());
455        assert_eq!(backend.name(), "CPU");
456
457        // Test state vector allocation
458        let buffer = backend
459            .allocate_state_vector(3)
460            .expect("Failed to allocate state vector");
461        assert_eq!(buffer.size(), 8 * std::mem::size_of::<Complex64>());
462    }
463
464    /// Flat, row-major CNOT in the |control target> basis, i.e. the same layout
465    /// `gate::multi::CNOT::matrix()` produces.
466    fn cnot_matrix() -> [Complex64; 16] {
467        let one = Complex64::new(1.0, 0.0);
468        let zero = Complex64::new(0.0, 0.0);
469        [
470            one, zero, zero, zero, //
471            zero, one, zero, zero, //
472            zero, zero, zero, one, //
473            zero, zero, one, zero,
474        ]
475    }
476
477    fn state_after_cnot(
478        n_qubits: usize,
479        control: u32,
480        target: u32,
481        initial: &[Complex64],
482    ) -> Vec<Complex64> {
483        let backend = CpuBackend::new();
484        let mut buffer = backend
485            .allocate_state_vector(n_qubits)
486            .expect("Failed to allocate state vector");
487        buffer.upload(initial).expect("Failed to upload state");
488
489        backend
490            .kernel()
491            .apply_two_qubit_gate(
492                buffer.as_mut(),
493                &cnot_matrix(),
494                QubitId(control),
495                QubitId(target),
496                n_qubits,
497            )
498            .expect("Failed to apply CNOT");
499
500        let mut out = vec![Complex64::new(0.0, 0.0); initial.len()];
501        buffer.download(&mut out).expect("Failed to download state");
502        out
503    }
504
505    /// A two-qubit gate's operands are identified by role, not by numeric qubit
506    /// index. Deriving the local basis ordering from `min`/`max` silently swaps
507    /// control and target whenever `control < target`, which turns the Bell
508    /// circuit H(q0)·CNOT(q0→q1) into an unentangled state.
509    #[test]
510    fn test_cnot_respects_control_target_order() {
511        let zero = Complex64::new(0.0, 0.0);
512        let one = Complex64::new(1.0, 0.0);
513
514        // |01> in bit order (q0 = 1, q1 = 0) is index 1.
515        let mut basis_q0_set = vec![zero; 4];
516        basis_q0_set[1] = one;
517
518        // CNOT(control = q0, target = q1) must flip q1 -> index 3.
519        let flipped = state_after_cnot(2, 0, 1, &basis_q0_set);
520        assert_eq!(flipped[3], one, "CNOT(q0->q1) must flip the target qubit");
521        assert_eq!(flipped[1], zero);
522
523        // The reversed CNOT sees control q1 = 0, so it must leave the state alone.
524        let unchanged = state_after_cnot(2, 1, 0, &basis_q0_set);
525        assert_eq!(
526            unchanged[1], one,
527            "CNOT(q1->q0) must be the identity when q1 = 0"
528        );
529    }
530
531    #[test]
532    fn test_cnot_builds_bell_state() {
533        let backend = CpuBackend::new();
534        let mut buffer = backend
535            .allocate_state_vector(2)
536            .expect("Failed to allocate state vector");
537
538        let inv_sqrt2 = std::f64::consts::FRAC_1_SQRT_2;
539        let h = [
540            Complex64::new(inv_sqrt2, 0.0),
541            Complex64::new(inv_sqrt2, 0.0),
542            Complex64::new(inv_sqrt2, 0.0),
543            Complex64::new(-inv_sqrt2, 0.0),
544        ];
545
546        let mut initial = vec![Complex64::new(0.0, 0.0); 4];
547        initial[0] = Complex64::new(1.0, 0.0);
548        buffer.upload(&initial).expect("Failed to upload state");
549
550        backend
551            .kernel()
552            .apply_single_qubit_gate(buffer.as_mut(), &h, QubitId(0), 2)
553            .expect("Failed to apply H");
554        backend
555            .kernel()
556            .apply_two_qubit_gate(buffer.as_mut(), &cnot_matrix(), QubitId(0), QubitId(1), 2)
557            .expect("Failed to apply CNOT");
558
559        let mut out = vec![Complex64::new(0.0, 0.0); 4];
560        buffer.download(&mut out).expect("Failed to download state");
561
562        let probs: Vec<f64> = out.iter().map(|c| c.norm_sqr()).collect();
563        assert!((probs[0] - 0.5).abs() < 1e-12, "probs = {probs:?}");
564        assert!(probs[1].abs() < 1e-12, "probs = {probs:?}");
565        assert!(probs[2].abs() < 1e-12, "probs = {probs:?}");
566        assert!((probs[3] - 0.5).abs() < 1e-12, "probs = {probs:?}");
567    }
568
569    /// Spectator qubits must be preserved exactly, including when the operands
570    /// straddle them and are not adjacent.
571    #[test]
572    fn test_cnot_on_non_adjacent_qubits_preserves_spectators() {
573        let zero = Complex64::new(0.0, 0.0);
574        let one = Complex64::new(1.0, 0.0);
575
576        // 5 qubits, control = q1, target = q4, spectators q0/q2/q3 all set.
577        let n_qubits = 5;
578        let spectators = (1 << 0) | (1 << 2) | (1 << 3);
579        let start_index = spectators | (1 << 1); // control q1 = 1, target q4 = 0
580
581        let mut initial = vec![zero; 1 << n_qubits];
582        initial[start_index] = one;
583
584        let out = state_after_cnot(n_qubits, 1, 4, &initial);
585
586        let expected_index = start_index | (1 << 4);
587        assert_eq!(out[expected_index], one, "target q4 should have flipped");
588        assert_eq!(out[start_index], zero);
589        assert_eq!(
590            out.iter().filter(|c| c.norm_sqr() > 1e-24).count(),
591            1,
592            "exactly one basis state should carry amplitude"
593        );
594    }
595
596    /// The multi-qubit path shares the same convention: `qubits[0]` is the most
597    /// significant bit of the gate's local basis index.
598    #[test]
599    fn test_multi_qubit_gate_respects_operand_order() {
600        use scirs2_core::ndarray::Array2;
601
602        let zero = Complex64::new(0.0, 0.0);
603        let one = Complex64::new(1.0, 0.0);
604
605        // Toffoli: flips the last operand when the first two are both |1>.
606        let mut toffoli = Array2::from_elem((8, 8), zero);
607        for i in 0..6 {
608            toffoli[(i, i)] = one;
609        }
610        toffoli[(6, 7)] = one;
611        toffoli[(7, 6)] = one;
612
613        let backend = CpuBackend::new();
614
615        // Controls q2 and q0 set, target q1 clear: index = 1 + 4 = 5.
616        let mut initial = vec![zero; 8];
617        initial[5] = one;
618
619        let mut buffer = backend
620            .allocate_state_vector(3)
621            .expect("Failed to allocate state vector");
622        buffer.upload(&initial).expect("Failed to upload state");
623
624        backend
625            .kernel()
626            .apply_multi_qubit_gate(
627                buffer.as_mut(),
628                &toffoli,
629                &[QubitId(2), QubitId(0), QubitId(1)],
630                3,
631            )
632            .expect("Failed to apply Toffoli");
633
634        let mut out = vec![zero; 8];
635        buffer.download(&mut out).expect("Failed to download state");
636
637        assert_eq!(out[7], one, "target q1 should have flipped, got {out:?}");
638        assert_eq!(out[5], zero);
639    }
640
641    #[test]
642    fn test_two_qubit_gate_rejects_invalid_operands() {
643        let backend = CpuBackend::new();
644        let mut buffer = backend
645            .allocate_state_vector(2)
646            .expect("Failed to allocate state vector");
647
648        assert!(backend
649            .kernel()
650            .apply_two_qubit_gate(buffer.as_mut(), &cnot_matrix(), QubitId(0), QubitId(0), 2)
651            .is_err());
652        assert!(backend
653            .kernel()
654            .apply_two_qubit_gate(buffer.as_mut(), &cnot_matrix(), QubitId(0), QubitId(5), 2)
655            .is_err());
656    }
657}