Skip to main content

prism_q/sim/
dispatch.rs

1use crate::backend::mps::MpsBackend;
2use crate::backend::product::ProductStateBackend;
3use crate::backend::sparse::SparseBackend;
4use crate::backend::stabilizer::StabilizerBackend;
5use crate::backend::statevector::StatevectorBackend;
6use crate::backend::tensornetwork::TensorNetworkBackend;
7use crate::backend::Backend;
8use crate::circuit::{Circuit, Instruction};
9use crate::error::{PrismError, Result};
10
11#[cfg(feature = "gpu")]
12use std::sync::Arc;
13
14#[cfg(feature = "gpu")]
15use crate::gpu::GpuContext;
16
17use super::{Probabilities, SimulationResult};
18
19pub(super) enum DispatchAction {
20    Backend(Box<dyn Backend>),
21    StabilizerRank,
22    StochasticPauli { num_samples: usize },
23    DeterministicPauli { epsilon: f64, max_terms: usize },
24}
25
26pub(super) fn max_statevector_qubits() -> usize {
27    static CACHED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
28    *CACHED.get_or_init(|| {
29        if let Ok(val) = std::env::var("PRISM_MAX_SV_QUBITS") {
30            if let Ok(n) = val.parse::<usize>() {
31                return n;
32            }
33        }
34        match detect_max_sv_qubits() {
35            Some(n) => n,
36            None => {
37                eprintln!(
38                    "warning: could not detect system memory; statevector qubit cap is disabled. \
39                     Large circuits may abort on allocation. Set PRISM_MAX_SV_QUBITS to suppress."
40                );
41                usize::MAX
42            }
43        }
44    })
45}
46
47#[cfg(windows)]
48fn detect_max_sv_qubits() -> Option<usize> {
49    #[repr(C)]
50    struct MemoryStatusEx {
51        dw_length: u32,
52        dw_memory_load: u32,
53        ull_total_phys: u64,
54        ull_avail_phys: u64,
55        ull_total_page_file: u64,
56        ull_avail_page_file: u64,
57        ull_total_virtual: u64,
58        ull_avail_virtual: u64,
59        ull_avail_extended_virtual: u64,
60    }
61
62    extern "system" {
63        fn GlobalMemoryStatusEx(lp_buffer: *mut MemoryStatusEx) -> i32;
64    }
65
66    // SAFETY: zeroed MemoryStatusEx is valid (all-zero bit pattern is a valid repr(C) struct)
67    let mut status: MemoryStatusEx = unsafe { std::mem::zeroed() };
68    status.dw_length = std::mem::size_of::<MemoryStatusEx>() as u32;
69    // SAFETY: status is a valid MemoryStatusEx with dw_length set; FFI call reads/writes only within the struct
70    if unsafe { GlobalMemoryStatusEx(&mut status) } == 0 {
71        return None;
72    }
73
74    let budget = status.ull_total_phys / 2;
75    let max_elements = budget / 16;
76    if max_elements == 0 {
77        return None;
78    }
79    let n = 63 - max_elements.leading_zeros() as usize;
80    Some(n.min(33))
81}
82
83#[cfg(unix)]
84fn detect_max_sv_qubits() -> Option<usize> {
85    let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
86    for line in meminfo.lines() {
87        if let Some(rest) = line.strip_prefix("MemTotal:") {
88            let kb: u64 = rest.trim().trim_end_matches(" kB").trim().parse().ok()?;
89            let budget = (kb * 1024) / 2;
90            let max_elements = budget / 16;
91            if max_elements == 0 {
92                return None;
93            }
94            let n = 63 - max_elements.leading_zeros() as usize;
95            return Some(n.min(33));
96        }
97    }
98    None
99}
100
101#[cfg(not(any(windows, unix)))]
102fn detect_max_sv_qubits() -> Option<usize> {
103    None
104}
105
106pub(super) const AUTO_MPS_BOND_DIM: usize = 256;
107
108pub(super) const MAX_AUTO_T_COUNT_EXACT: usize = 18;
109
110pub(super) const MAX_AUTO_T_COUNT_APPROX: usize = 28;
111
112pub(super) const MAX_AUTO_T_COUNT_SHOTS: usize = 40;
113
114pub(super) const MAX_STABILIZER_RANK_QUBITS: usize = 25;
115
116pub(super) const AUTO_APPROX_MAX_TERMS: usize = 8192;
117
118pub(super) const MIN_QUBITS_FOR_SPD_AUTO: usize = 12;
119
120pub(super) const AUTO_SPD_MAX_TERMS: usize = 65536;
121
122pub(super) const MIN_FACTORED_STABILIZER_QUBITS: usize = 128;
123
124pub(super) const MIN_BLOCK_FOR_FACTORED_STAB: usize = 16;
125
126// GPU crossover threshold and its env override live in `crate::gpu` so users
127// can introspect them without depending on internal dispatch plumbing. The
128// dispatch layer calls `crate::gpu::min_qubits()` directly; there is no
129// private duplicate.
130
131/// Automatically select the optimal backend based on circuit analysis.
132///
133/// Decision tree:
134/// 1. No entangling gates        → ProductState (O(n))
135/// 2. All Clifford gates         → Stabilizer (O(n²))
136/// 3. Clifford+T, t ≤ 12        → StabilizerRank (O(2^t · n²))
137/// 4. Above memory limit:
138///    a. Sparse-friendly         → Sparse (O(k) where k = non-zero amplitudes)
139///    b. Otherwise               → MPS (bounded bond dimension)
140/// 5. Otherwise                  → Statevector (exact, general-purpose)
141#[derive(Debug, Clone)]
142pub enum BackendKind {
143    Auto,
144    Statevector,
145    Stabilizer,
146    Sparse,
147    Mps {
148        max_bond_dim: usize,
149    },
150    ProductState,
151    TensorNetwork,
152    Factored,
153    StabilizerRank,
154    FilteredStabilizer,
155    FactoredStabilizer,
156    StochasticPauli {
157        num_samples: usize,
158    },
159    DeterministicPauli {
160        epsilon: f64,
161        max_terms: usize,
162    },
163    /// Statevector backed by a CUDA GPU execution context.
164    ///
165    /// Circuits (or decomposed sub-blocks) with fewer than
166    /// [`crate::gpu::min_qubits()`] qubits (tunable via
167    /// `PRISM_GPU_MIN_QUBITS`, default [`crate::gpu::MIN_QUBITS_DEFAULT`])
168    /// transparently fall back to the host statevector path, since
169    /// small states do not survive PCIe and launch-latency overhead.
170    /// Larger circuits allocate a device-resident state and route gate
171    /// application through GPU kernels.
172    ///
173    /// Compose with [`crate::sim::run_with`] to get fusion + independent-
174    /// subsystem decomposition for free; each sub-block is evaluated against
175    /// the crossover independently. See [`crate::sim::run_with_gpu`] for a
176    /// one-liner wrapper when you already have an `Arc<GpuContext>`.
177    #[cfg(feature = "gpu")]
178    StatevectorGpu {
179        context: Arc<GpuContext>,
180    },
181    /// Stabilizer backend backed by a CUDA GPU tableau.
182    ///
183    /// Circuits (or decomposed sub-blocks) with fewer than
184    /// [`crate::gpu::stabilizer_min_qubits()`] qubits (tunable via
185    /// `PRISM_STABILIZER_GPU_MIN_QUBITS`, default
186    /// [`crate::gpu::STABILIZER_MIN_QUBITS_DEFAULT`]) fall back to the CPU
187    /// stabilizer path. The GPU path routes gate application to device
188    /// kernels. Measurement and reset stay on device, while probabilities and
189    /// export-style helpers still read back to the CPU algorithms.
190    ///
191    /// Compose with [`crate::sim::run_with`] to pick up independent-subsystem
192    /// decomposition; non-Clifford circuits are rejected at dispatch time with
193    /// the same error shape as [`BackendKind::Stabilizer`].
194    #[cfg(feature = "gpu")]
195    StabilizerGpu {
196        context: Arc<GpuContext>,
197    },
198}
199
200impl BackendKind {
201    pub fn supports_noisy_per_shot(&self) -> bool {
202        !matches!(
203            self,
204            BackendKind::StabilizerRank
205                | BackendKind::StochasticPauli { .. }
206                | BackendKind::DeterministicPauli { .. }
207        )
208    }
209
210    pub fn supports_general_noise(&self) -> bool {
211        match self {
212            BackendKind::Auto
213            | BackendKind::Statevector
214            | BackendKind::Sparse
215            | BackendKind::Mps { .. }
216            | BackendKind::ProductState
217            | BackendKind::Factored => true,
218            #[cfg(feature = "gpu")]
219            BackendKind::StatevectorGpu { .. } => true,
220            _ => false,
221        }
222    }
223
224    pub(crate) fn is_stabilizer_family(&self) -> bool {
225        matches!(
226            self,
227            BackendKind::Stabilizer
228                | BackendKind::FilteredStabilizer
229                | BackendKind::FactoredStabilizer
230        ) || {
231            #[cfg(feature = "gpu")]
232            {
233                matches!(self, BackendKind::StabilizerGpu { .. })
234            }
235            #[cfg(not(feature = "gpu"))]
236            {
237                false
238            }
239        }
240    }
241
242    pub(crate) fn general_noise_backend_names() -> &'static str {
243        #[cfg(feature = "gpu")]
244        {
245            "Auto, Statevector, StatevectorGpu, Sparse, Mps, ProductState, or Factored"
246        }
247        #[cfg(not(feature = "gpu"))]
248        {
249            "Auto, Statevector, Sparse, Mps, ProductState, or Factored"
250        }
251    }
252}
253
254pub(super) fn validate_explicit_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
255    match kind {
256        BackendKind::Stabilizer
257        | BackendKind::FilteredStabilizer
258        | BackendKind::FactoredStabilizer
259            if !circuit.is_clifford_only() =>
260        {
261            return Err(PrismError::IncompatibleBackend {
262                backend: "stabilizer".into(),
263                reason: "circuit contains non-Clifford gates".into(),
264            });
265        }
266        #[cfg(feature = "gpu")]
267        BackendKind::StabilizerGpu { .. } if !circuit.is_clifford_only() => {
268            return Err(PrismError::IncompatibleBackend {
269                backend: "stabilizer".into(),
270                reason: "circuit contains non-Clifford gates".into(),
271            });
272        }
273        BackendKind::ProductState if circuit.has_entangling_gates() => {
274            return Err(PrismError::IncompatibleBackend {
275                backend: "productstate".into(),
276                reason: "circuit contains entangling gates".into(),
277            });
278        }
279        BackendKind::StabilizerRank if !circuit.has_t_gates() => {
280            return Err(PrismError::IncompatibleBackend {
281                backend: "stabilizer_rank".into(),
282                reason: "circuit has no T gates; use Stabilizer instead".into(),
283            });
284        }
285        _ => {}
286    }
287    Ok(())
288}
289
290pub(super) fn supports_fused_for_kind(kind: &BackendKind, circuit: &Circuit) -> bool {
291    match kind {
292        BackendKind::Stabilizer
293        | BackendKind::FilteredStabilizer
294        | BackendKind::FactoredStabilizer
295        | BackendKind::StabilizerRank
296        | BackendKind::StochasticPauli { .. }
297        | BackendKind::DeterministicPauli { .. } => false,
298        #[cfg(feature = "gpu")]
299        BackendKind::StabilizerGpu { .. } => false,
300        BackendKind::Auto => !(circuit.is_clifford_only() && circuit.has_entangling_gates()),
301        _ => true,
302    }
303}
304
305/// Build a `StatevectorBackend` configured for GPU execution if the circuit
306/// is large enough to clear the crossover, otherwise a plain host
307/// backend. Called from `select_dispatch` (which runs per sub-block after
308/// decomposition) so small blocks transparently stay on CPU.
309#[cfg(feature = "gpu")]
310fn statevector_gpu_with_crossover(
311    context: &Arc<GpuContext>,
312    circuit: &Circuit,
313    seed: u64,
314) -> StatevectorBackend {
315    if circuit.num_qubits >= crate::gpu::min_qubits() {
316        StatevectorBackend::new(seed).with_gpu(context.clone())
317    } else {
318        StatevectorBackend::new(seed)
319    }
320}
321
322/// Build a `StabilizerBackend` configured for GPU execution if the circuit
323/// is large enough to clear the stabilizer crossover, otherwise a plain
324/// host backend.
325#[cfg(feature = "gpu")]
326fn stabilizer_gpu_with_crossover(
327    context: &Arc<GpuContext>,
328    circuit: &Circuit,
329    seed: u64,
330) -> StabilizerBackend {
331    if circuit.num_qubits >= crate::gpu::stabilizer_min_qubits() {
332        StabilizerBackend::new(seed).with_gpu(context.clone())
333    } else {
334        StabilizerBackend::new(seed)
335    }
336}
337
338pub(super) fn select_dispatch(
339    kind: &BackendKind,
340    circuit: &Circuit,
341    seed: u64,
342    has_partial_independence: bool,
343) -> DispatchAction {
344    match kind {
345        BackendKind::Auto => {
346            if !circuit.has_entangling_gates() {
347                DispatchAction::Backend(Box::new(ProductStateBackend::new(seed)))
348            } else if circuit.is_clifford_only() {
349                DispatchAction::Backend(Box::new(StabilizerBackend::new(seed)))
350            } else if circuit.num_qubits > max_statevector_qubits() {
351                if circuit.is_sparse_friendly() {
352                    DispatchAction::Backend(Box::new(SparseBackend::new(seed)))
353                } else {
354                    DispatchAction::Backend(Box::new(MpsBackend::new(seed, AUTO_MPS_BOND_DIM)))
355                }
356            } else if has_partial_independence {
357                DispatchAction::Backend(Box::new(crate::backend::factored::FactoredBackend::new(
358                    seed,
359                )))
360            } else {
361                DispatchAction::Backend(Box::new(StatevectorBackend::new(seed)))
362            }
363        }
364        BackendKind::Statevector => {
365            DispatchAction::Backend(Box::new(StatevectorBackend::new(seed)))
366        }
367        BackendKind::Stabilizer => DispatchAction::Backend(Box::new(StabilizerBackend::new(seed))),
368        BackendKind::FilteredStabilizer => DispatchAction::Backend(Box::new(
369            crate::backend::stabilizer::FilteredStabilizerBackend::new(seed),
370        )),
371        BackendKind::Sparse => DispatchAction::Backend(Box::new(SparseBackend::new(seed))),
372        BackendKind::Mps { max_bond_dim } => {
373            DispatchAction::Backend(Box::new(MpsBackend::new(seed, *max_bond_dim)))
374        }
375        BackendKind::ProductState => {
376            DispatchAction::Backend(Box::new(ProductStateBackend::new(seed)))
377        }
378        BackendKind::TensorNetwork => {
379            DispatchAction::Backend(Box::new(TensorNetworkBackend::new(seed)))
380        }
381        BackendKind::Factored => DispatchAction::Backend(Box::new(
382            crate::backend::factored::FactoredBackend::new(seed),
383        )),
384        BackendKind::FactoredStabilizer => DispatchAction::Backend(Box::new(
385            crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed),
386        )),
387        BackendKind::StabilizerRank => DispatchAction::StabilizerRank,
388        BackendKind::StochasticPauli { num_samples } => DispatchAction::StochasticPauli {
389            num_samples: *num_samples,
390        },
391        BackendKind::DeterministicPauli { epsilon, max_terms } => {
392            DispatchAction::DeterministicPauli {
393                epsilon: *epsilon,
394                max_terms: *max_terms,
395            }
396        }
397        #[cfg(feature = "gpu")]
398        BackendKind::StatevectorGpu { context } => DispatchAction::Backend(Box::new(
399            statevector_gpu_with_crossover(context, circuit, seed),
400        )),
401        #[cfg(feature = "gpu")]
402        BackendKind::StabilizerGpu { context } => DispatchAction::Backend(Box::new(
403            stabilizer_gpu_with_crossover(context, circuit, seed),
404        )),
405    }
406}
407
408pub(super) fn select_backend(
409    kind: &BackendKind,
410    circuit: &Circuit,
411    seed: u64,
412    has_partial_independence: bool,
413) -> Box<dyn Backend> {
414    match select_dispatch(kind, circuit, seed, has_partial_independence) {
415        DispatchAction::Backend(b) => b,
416        _ => unreachable!("non-backend dispatch should be handled by caller"),
417    }
418}
419
420#[inline]
421pub(super) fn min_clifford_prefix_gates(num_qubits: usize) -> usize {
422    (num_qubits * 2).max(16)
423}
424
425pub(super) fn has_temporal_clifford_opportunity(kind: &BackendKind, circuit: &Circuit) -> bool {
426    if !matches!(kind, BackendKind::Auto) {
427        return false;
428    }
429    if circuit.num_qubits > max_statevector_qubits() {
430        return false;
431    }
432    let min_gates = min_clifford_prefix_gates(circuit.num_qubits);
433    let mut prefix_gates = 0;
434    for inst in &circuit.instructions {
435        match inst {
436            Instruction::Gate { gate, .. } => {
437                if !gate.is_clifford() {
438                    break;
439                }
440                prefix_gates += 1;
441            }
442            Instruction::Measure { .. }
443            | Instruction::Reset { .. }
444            | Instruction::Conditional { .. } => break,
445            Instruction::Barrier { .. } => {}
446        }
447    }
448    prefix_gates >= min_gates && prefix_gates < circuit.instructions.len()
449}
450
451pub(super) fn try_temporal_clifford(
452    kind: &BackendKind,
453    circuit: &Circuit,
454    seed: u64,
455) -> Option<Result<SimulationResult>> {
456    if !matches!(kind, BackendKind::Auto) {
457        return None;
458    }
459    if circuit.num_qubits > max_statevector_qubits() {
460        return None;
461    }
462    let (prefix, tail) = circuit.clifford_prefix_split()?;
463    if prefix.gate_count() < min_clifford_prefix_gates(circuit.num_qubits) {
464        return None;
465    }
466
467    let mut stab = StabilizerBackend::new(seed);
468    if let Err(e) = stab.init(prefix.num_qubits, prefix.num_classical_bits) {
469        return Some(Err(e));
470    }
471    stab.enable_lazy_destab();
472    for inst in &prefix.instructions {
473        if let Err(e) = stab.apply(inst) {
474            return Some(Err(e));
475        }
476    }
477
478    let state = match stab.export_statevector() {
479        Ok(s) => s,
480        Err(e) => return Some(Err(e)),
481    };
482
483    let mut sv = StatevectorBackend::new(seed);
484    if let Err(e) = sv.init_from_state(state, tail.num_classical_bits) {
485        return Some(Err(e));
486    }
487
488    let fused_tail = crate::circuit::fusion::fuse_circuit(&tail, sv.supports_fused_gates());
489    for inst in &fused_tail.instructions {
490        if let Err(e) = sv.apply(inst) {
491            return Some(Err(e));
492        }
493    }
494
495    let probs = sv.probabilities().ok().map(Probabilities::Dense);
496
497    Some(Ok(SimulationResult {
498        classical_bits: sv.classical_results().to_vec(),
499        probabilities: probs,
500    }))
501}
502
503#[cfg(all(test, feature = "gpu"))]
504mod gpu_crossover_tests {
505    use super::*;
506    use crate::gates::Gate;
507    use crate::sim::run_with;
508
509    fn stub_kind() -> BackendKind {
510        BackendKind::StatevectorGpu {
511            context: GpuContext::stub_for_tests(),
512        }
513    }
514
515    /// `run_with_gpu` must compose identically to constructing the variant
516    /// manually. Uses the stub context at a small circuit so crossover fires
517    /// and proves the composition is side-effect equivalent.
518    #[test]
519    fn run_with_gpu_wraps_statevector_gpu_variant() {
520        let ctx = GpuContext::stub_for_tests();
521        let mut circuit = Circuit::new(4, 0);
522        circuit.add_gate(Gate::H, &[0]);
523        circuit.add_gate(Gate::Cx, &[0, 1]);
524
525        let direct = crate::sim::run_with_gpu(&circuit, 42, ctx.clone())
526            .expect("run_with_gpu must honor crossover and route to CPU");
527        let manual = run_with(stub_kind(), &circuit, 42).expect("manual variant reference");
528
529        let dp = direct.probabilities.expect("direct probs").to_vec();
530        let mp = manual.probabilities.expect("manual probs").to_vec();
531        assert_eq!(dp, mp);
532    }
533
534    /// A 4q circuit is far below the default 14q threshold. If the dispatch
535    /// layer were to build a GPU backend anyway, `GpuState::new` on the stub
536    /// context would return `BackendUnsupported`. Success proves the
537    /// crossover in `select_dispatch` is routing small circuits to the host
538    /// path.
539    #[test]
540    fn small_circuit_routes_to_cpu() {
541        let mut circuit = Circuit::new(4, 0);
542        circuit.add_gate(Gate::H, &[0]);
543        circuit.add_gate(Gate::Cx, &[0, 1]);
544        circuit.add_gate(Gate::H, &[2]);
545        circuit.add_gate(Gate::Cx, &[2, 3]);
546
547        let result = run_with(stub_kind(), &circuit, 42)
548            .expect("stub context must not be touched for a 4q circuit");
549        let probs = result
550            .probabilities
551            .expect("probabilities missing")
552            .to_vec();
553
554        let mut expected = [0.0_f64; 16];
555        expected[0b0000] = 0.25;
556        expected[0b0011] = 0.25;
557        expected[0b1100] = 0.25;
558        expected[0b1111] = 0.25;
559        for (i, (p, e)) in probs.iter().zip(&expected).enumerate() {
560            assert!((p - e).abs() < 1e-10, "p[{i}] = {p}, expected {e}");
561        }
562    }
563
564    /// `independent_bell_pairs(8)` spans 16 qubits but decomposes into 8
565    /// independent 2q blocks. With `BackendKind::StatevectorGpu`, each
566    /// sub-block is below the 14q threshold and must route to CPU. If
567    /// decomposition failed to fire, the 16q monolithic path would attempt
568    /// `GpuState::new` through the stub and return `BackendUnsupported`.
569    /// Success here proves decomposition survives across the GPU dispatch.
570    #[test]
571    fn decomposable_16q_circuit_runs_per_block_on_cpu() {
572        let circuit = crate::circuits::independent_bell_pairs(8);
573        assert_eq!(circuit.num_qubits, 16);
574
575        let cpu = run_with(BackendKind::Statevector, &circuit, 42).expect("cpu baseline");
576        let gpu = run_with(stub_kind(), &circuit, 42).expect("stub must stay out of the way");
577
578        let cpu_p = cpu.probabilities.expect("cpu probs").to_vec();
579        let gpu_p = gpu.probabilities.expect("gpu probs").to_vec();
580        assert_eq!(cpu_p.len(), gpu_p.len());
581        for (i, (c, g)) in cpu_p.iter().zip(gpu_p.iter()).enumerate() {
582            assert!(
583                (c - g).abs() < 1e-10,
584                "prob[{i}] cpu={c}, gpu={g}, diff={}",
585                (c - g).abs()
586            );
587        }
588    }
589
590    fn stabilizer_stub_kind() -> BackendKind {
591        BackendKind::StabilizerGpu {
592            context: GpuContext::stub_for_tests(),
593        }
594    }
595
596    /// A 4q Clifford circuit is far below the stabilizer GPU threshold, so the
597    /// stub context must never be touched. Produces the same measurement bits
598    /// as a plain CPU stabilizer run.
599    #[test]
600    fn stabilizer_gpu_small_circuit_routes_to_cpu() {
601        let mut circuit = Circuit::new(4, 4);
602        circuit.add_gate(Gate::H, &[0]);
603        circuit.add_gate(Gate::Cx, &[0, 1]);
604        circuit.add_gate(Gate::Cx, &[1, 2]);
605        circuit.add_gate(Gate::Cx, &[2, 3]);
606        circuit.add_measure(0, 0);
607        circuit.add_measure(1, 1);
608        circuit.add_measure(2, 2);
609        circuit.add_measure(3, 3);
610
611        let cpu_run = run_with(BackendKind::Stabilizer, &circuit, 42).expect("cpu baseline");
612        let gpu_run = run_with(stabilizer_stub_kind(), &circuit, 42)
613            .expect("stub must stay out of the way for small circuits");
614        assert_eq!(cpu_run.classical_bits, gpu_run.classical_bits);
615    }
616
617    /// Non-Clifford circuits are rejected at dispatch time with the same error
618    /// shape as `BackendKind::Stabilizer`.
619    #[test]
620    fn stabilizer_gpu_rejects_non_clifford_at_dispatch() {
621        let mut circuit = Circuit::new(2, 0);
622        circuit.add_gate(Gate::T, &[0]);
623        let err = run_with(stabilizer_stub_kind(), &circuit, 42).unwrap_err();
624        assert!(matches!(err, PrismError::IncompatibleBackend { .. }));
625    }
626}