Skip to main content

prism_q/backend/
mod.rs

1//! Simulation backend trait and implementations.
2//!
3//! Backends are the core execution engines. Each backend owns its quantum state
4//! representation and applies circuit instructions to evolve the state.
5//!
6//! # Backend contract
7//!
8//! 1. Call [`Backend::init`] before any [`Backend::apply`] calls.
9//! 2. Call [`Backend::apply`] for each instruction in circuit order.
10//! 3. Measurement is destructive, it collapses the state.
11//! 4. Given the same circuit and RNG seed, results must be deterministic.
12//!
13//! # Performance requirements for implementors
14//!
15//! - Gate application must avoid heap allocation in the hot path.
16//! - Prefer direct indexing over iterator chains for state access.
17//! - Use `#[inline]` and `#[inline(always)]` on gate kernels.
18//! - Document all `unsafe` blocks with safety invariants.
19//!
20//! # Adding a new backend
21//!
22//! See `docs/architecture.md` § "Add a new backend" for the full playbook.
23
24pub mod factored;
25pub mod factored_stabilizer;
26pub mod mps;
27pub mod product;
28pub(crate) mod simd;
29pub mod sparse;
30pub mod stabilizer;
31pub mod statevector;
32pub mod tensornetwork;
33
34use num_complex::Complex64;
35
36use crate::circuit::Instruction;
37use crate::error::Result;
38
39#[cfg(feature = "parallel")]
40pub(crate) const PARALLEL_THRESHOLD_QUBITS: usize = 14;
41
42#[cfg(feature = "parallel")]
43pub(crate) const MIN_PAR_ELEMS: usize = 4096;
44
45#[cfg(feature = "parallel")]
46pub(crate) const MIN_PAR_ITERS: usize = 2048;
47
48#[cfg(feature = "parallel")]
49pub(crate) const MIN_QUBITS_FOR_PAR_GATES: usize = 128;
50
51#[cfg(feature = "parallel")]
52pub(crate) const MIN_ANTI_ROWS_FOR_PAR: usize = 4;
53
54/// Minimum probability/norm value for measurement normalization.
55///
56/// Used as `prob.clamp(NORM_CLAMP_MIN, 1.0).sqrt()` to avoid division by zero
57/// when a measurement outcome has near-zero probability due to floating point.
58pub(crate) const NORM_CLAMP_MIN: f64 = 1e-30;
59
60/// Tolerance for detecting whether a complex phase equals 1+0i.
61///
62/// Used in diagonal gate optimizations (`skip_lo`) and controlled-phase
63/// identity checks. Tighter than identity detection (1e-12) because phase
64/// errors accumulate multiplicatively.
65pub(crate) const PHASE_IS_ONE_EPS: f64 = 1e-15;
66
67pub(crate) const MAX_PROB_QUBITS: usize = 25;
68
69#[inline(always)]
70pub(crate) fn is_phase_one(phase: Complex64) -> bool {
71    (phase.re - 1.0).abs() < PHASE_IS_ONE_EPS && phase.im.abs() < PHASE_IS_ONE_EPS
72}
73
74#[inline(always)]
75pub(crate) fn measurement_inv_norm(outcome: bool, prob_one: f64) -> f64 {
76    let prob_outcome = if outcome { prob_one } else { 1.0 - prob_one };
77    1.0 / prob_outcome.clamp(NORM_CLAMP_MIN, 1.0).sqrt()
78}
79
80#[inline(always)]
81pub(crate) fn init_classical_bits(bits: &mut Vec<bool>, num: usize) {
82    if bits.len() == num {
83        bits.fill(false);
84    } else {
85        *bits = vec![false; num];
86    }
87}
88
89/// Initialize the Rayon thread pool to use all logical cores.
90///
91/// At 24q+ where state exceeds L3 cache, hyperthreads help hide memory
92/// latency. Benchmarks show 17% improvement with logical cores at 24q.
93/// The user can override via `RAYON_NUM_THREADS`.
94///
95/// Safe to call multiple times. Only the first call takes effect.
96#[cfg(feature = "parallel")]
97pub(crate) fn init_thread_pool() {
98    use std::sync::Once;
99    static INIT: Once = Once::new();
100    INIT.call_once(|| {
101        if std::env::var("RAYON_NUM_THREADS").is_err() {
102            let threads = num_cpus::get();
103            rayon::ThreadPoolBuilder::new()
104                .num_threads(threads)
105                .build_global()
106                .ok();
107        }
108    });
109}
110
111#[inline(always)]
112pub(crate) fn sorted_mcu_qubits(controls: &[usize], target: usize, buf: &mut [usize; 10]) -> usize {
113    let n = controls.len() + 1;
114    buf[..controls.len()].copy_from_slice(controls);
115    buf[controls.len()] = target;
116    buf[..n].sort_unstable();
117    n
118}
119
120/// Trait that all simulation backends must implement.
121pub trait Backend {
122    /// Human-readable backend name (for error messages, logging, and benchmarks).
123    fn name(&self) -> &'static str;
124
125    /// Initialize (or reset) state for a circuit with the given dimensions.
126    ///
127    /// After this call the backend is in the |0...0⟩ state.
128    fn init(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<()>;
129
130    /// Apply a single instruction to the current state.
131    ///
132    /// Instructions arrive in circuit order. Backends may assume:
133    /// - Qubit indices are valid (checked during circuit construction).
134    /// - Gate arity matches target count.
135    fn apply(&mut self, instruction: &Instruction) -> Result<()>;
136
137    /// Read classical measurement results.
138    ///
139    /// Returns a slice indexed by classical bit number. `true` = measured |1⟩.
140    fn classical_results(&self) -> &[bool];
141
142    /// Compute the probability of each computational basis state.
143    ///
144    /// Returns a `Vec<f64>` of length 2^num_qubits. Not all backends can
145    /// provide this efficiently, they may return `Err(BackendUnsupported)`.
146    fn probabilities(&self) -> Result<Vec<f64>>;
147
148    /// Number of qubits the backend is currently configured for.
149    fn num_qubits(&self) -> usize;
150
151    /// Apply a batch of instructions to the current state.
152    ///
153    /// The default implementation calls [`apply`](Backend::apply) in a loop.
154    /// Backends may override to batch gate operations for better cache
155    /// utilization (e.g., the stabilizer backend groups gates by target word).
156    fn apply_instructions(&mut self, instructions: &[Instruction]) -> Result<()> {
157        for instruction in instructions {
158            self.apply(instruction)?;
159        }
160        Ok(())
161    }
162
163    /// Whether this backend can handle `Gate::Fused` variants.
164    ///
165    /// Backends that operate on symbolic gate representations (e.g. stabilizer
166    /// tableau) cannot decode a fused matrix back to individual gates. The
167    /// simulation engine skips the fusion pass when this returns `false`.
168    fn supports_fused_gates(&self) -> bool {
169        true
170    }
171
172    /// Whether this backend has a native kernel for `Gate::QftBlock`.
173    ///
174    /// Native support is currently limited to whole-state CPU statevector QFT.
175    /// Other backends expand the block to textbook gates before dispatch.
176    fn supports_qft_block(&self) -> bool {
177        false
178    }
179
180    /// Export the current quantum state as a dense statevector.
181    ///
182    /// Returns a `Vec<Complex64>` of length 2^n containing the full amplitude
183    /// vector. Enables backend transitions (e.g., Stabilizer → Statevector
184    /// for temporal Clifford decomposition).
185    ///
186    /// Not all backends support this efficiently. The default implementation
187    /// returns `Err(BackendUnsupported)`.
188    fn export_statevector(&self) -> Result<Vec<Complex64>> {
189        Err(crate::error::PrismError::BackendUnsupported {
190            backend: self.name().to_string(),
191            operation: "statevector export".to_string(),
192        })
193    }
194
195    /// Compute P(qubit = |1⟩) without collapsing the state.
196    ///
197    /// Used by the trajectory engine for state-dependent noise channels
198    /// (amplitude damping, phase damping). The default derives from
199    /// [`Backend::reduced_density_matrix_1q`].
200    fn qubit_probability(&self, qubit: usize) -> Result<f64> {
201        let rho = self.reduced_density_matrix_1q(qubit)?;
202        Ok(rho[1][1].re.clamp(0.0, 1.0))
203    }
204
205    /// Compute the one-qubit reduced density matrix without collapsing the state.
206    ///
207    /// Returned as `[[p0, r*], [r, p1]]`, where `r = <1|rho|0>`.
208    /// Used by the trajectory engine for dense custom Kraus channels whose
209    /// branch probabilities depend on coherence, not just populations. The
210    /// default returns `BackendUnsupported`; backends override when their
211    /// representation can expose this quantity efficiently enough for noise
212    /// sampling.
213    fn reduced_density_matrix_1q(&self, _qubit: usize) -> Result<[[Complex64; 2]; 2]> {
214        Err(crate::error::PrismError::BackendUnsupported {
215            backend: self.name().to_string(),
216            operation: "reduced_density_matrix_1q".to_string(),
217        })
218    }
219
220    /// Reset a qubit to |0⟩, discarding any prior amplitude on that qubit.
221    ///
222    /// Destructive, non-unitary. Used by OpenQASM `reset` and as a primitive
223    /// for thermal relaxation trajectory simulation. The default returns
224    /// `BackendUnsupported`; backends override for their native representation.
225    fn reset(&mut self, _qubit: usize) -> Result<()> {
226        Err(crate::error::PrismError::BackendUnsupported {
227            backend: self.name().to_string(),
228            operation: "reset".to_string(),
229        })
230    }
231
232    /// Apply a 2×2 matrix to a single qubit without allocating.
233    ///
234    /// Used by the trajectory engine to apply Kraus operators (amplitude
235    /// damping, phase damping, thermal relaxation) without boxing into a
236    /// `Gate::Fused` on every call. The default falls back to building a
237    /// `Gate::Fused` and dispatching via `apply`; backends may override for a
238    /// zero-allocation fast path.
239    fn apply_1q_matrix(&mut self, qubit: usize, matrix: &[[Complex64; 2]; 2]) -> Result<()> {
240        use crate::circuit::smallvec;
241        self.apply(&crate::circuit::Instruction::Gate {
242            gate: crate::gates::Gate::Fused(Box::new(*matrix)),
243            targets: smallvec![qubit],
244        })
245    }
246}