Skip to main content

prism_q/sim/
mod.rs

1//! Simulation orchestration.
2//!
3//! Connects the circuit IR to a backend. This module is deliberately thin,
4//! the complexity lives in the backends and the parser.
5
6pub mod compiled;
7mod decomposed;
8mod dispatch;
9pub mod homological;
10pub mod noise;
11mod probability;
12mod shots;
13pub mod stabilizer_rank;
14mod trajectory;
15pub mod unified_pauli;
16
17pub(crate) use decomposed::merge_probabilities;
18use decomposed::{
19    run_decomposed, run_decomposed_prefused, should_decompose, MIN_DECOMPOSITION_QUBITS,
20};
21pub use dispatch::BackendKind;
22use dispatch::{
23    has_temporal_clifford_opportunity, select_backend, select_dispatch, stabilizer_rank_budget,
24    supports_fused_for_kind, try_temporal_clifford, validate_explicit_backend, DispatchAction,
25    AUTO_APPROX_MAX_TERMS, AUTO_MPS_BOND_DIM, AUTO_SPD_MAX_TERMS, MAX_AUTO_T_COUNT_APPROX,
26    MAX_AUTO_T_COUNT_EXACT, MAX_AUTO_T_COUNT_SHOTS, MAX_STABILIZER_RANK_QUBITS,
27    MIN_BLOCK_FOR_FACTORED_STAB, MIN_FACTORED_STABILIZER_QUBITS, MIN_QUBITS_FOR_SPD_AUTO,
28};
29pub use probability::{FactoredBlock, Probabilities, ProbabilitiesIter};
30pub use shots::{bitstring, ShotsResult};
31
32use std::collections::HashMap;
33
34use crate::backend::{max_statevector_qubits, Backend};
35use crate::circuit::{Circuit, Instruction};
36use crate::error::Result;
37use shots::{packed_shots_to_classical_bits, sample_shots};
38
39#[derive(Debug, Clone, Copy)]
40pub(crate) struct SimOptions {
41    pub(crate) probabilities: bool,
42}
43
44impl Default for SimOptions {
45    fn default() -> Self {
46        Self {
47            probabilities: true,
48        }
49    }
50}
51
52impl SimOptions {
53    pub(crate) fn classical_only() -> Self {
54        Self {
55            probabilities: false,
56        }
57    }
58}
59
60/// Result of a simulation run.
61#[derive(Debug, Clone)]
62pub struct SimulationResult {
63    /// Classical measurement outcomes, indexed by classical bit number.
64    /// `true` = measured |1⟩.
65    pub classical_bits: Vec<bool>,
66    /// Probability of each computational basis state (length 2^n).
67    /// `None` if the backend does not support probability extraction.
68    pub probabilities: Option<Probabilities>,
69}
70
71#[inline]
72fn probs_only_result(probs: Vec<f64>) -> SimulationResult {
73    SimulationResult {
74        probabilities: Some(Probabilities::Dense(probs)),
75        classical_bits: vec![],
76    }
77}
78
79/// Core execution: fuse, init, apply, extract.
80fn execute(
81    backend: &mut dyn Backend,
82    circuit: &Circuit,
83    opts: &SimOptions,
84) -> Result<SimulationResult> {
85    let expanded: std::borrow::Cow<'_, Circuit> = if backend.supports_qft_block() {
86        std::borrow::Cow::Borrowed(circuit)
87    } else {
88        crate::circuit::expand_qft_blocks(circuit)
89    };
90    let fused = crate::circuit::fusion::fuse_circuit(&expanded, backend.supports_fused_gates());
91    execute_circuit(backend, &fused, opts)
92}
93
94/// Shared init → apply → extract logic.
95fn execute_circuit(
96    backend: &mut dyn Backend,
97    circuit: &Circuit,
98    opts: &SimOptions,
99) -> Result<SimulationResult> {
100    backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
101    backend.apply_instructions(&circuit.instructions)?;
102
103    let probs = if opts.probabilities {
104        backend.probabilities().ok().map(Probabilities::Dense)
105    } else {
106        None
107    };
108
109    Ok(SimulationResult {
110        classical_bits: backend.classical_results().to_vec(),
111        probabilities: probs,
112    })
113}
114
115/// Execute a circuit with automatic backend selection.
116///
117/// The simplest entry point. Uses [`BackendKind::Auto`] to select the
118/// optimal backend based on circuit properties, then runs the circuit.
119pub fn run(circuit: &Circuit, seed: u64) -> Result<SimulationResult> {
120    run_with(BackendKind::Auto, circuit, seed)
121}
122
123/// Execute a circuit with explicit backend selection.
124///
125/// Constructs the backend internally based on [`BackendKind`], then runs
126/// the circuit. For a pre-constructed backend instance, use [`run_on`].
127pub fn run_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<SimulationResult> {
128    run_with_internal(kind, circuit, seed, SimOptions::default())
129}
130
131/// Execute a circuit on the CUDA GPU statevector dispatch.
132///
133/// Wraps [`run_with`] with `BackendKind::StatevectorGpu { context }`. The
134/// dispatch still applies fusion, independent-subsystem decomposition, and
135/// the qubit-count crossover ([`crate::gpu::min_qubits`]); sub-circuits
136/// below threshold automatically run on the host statevector.
137///
138/// ```no_run
139/// # #[cfg(feature = "gpu")] {
140/// use prism_q::gpu::GpuContext;
141/// use prism_q::{run_with_gpu, Circuit};
142///
143/// let ctx = GpuContext::new(0).expect("no usable GPU");
144/// let circuit = Circuit::new(16, 0);
145/// let _result = run_with_gpu(&circuit, 42, ctx).unwrap();
146/// # }
147/// ```
148#[cfg(feature = "gpu")]
149pub fn run_with_gpu(
150    circuit: &Circuit,
151    seed: u64,
152    context: std::sync::Arc<crate::gpu::GpuContext>,
153) -> Result<SimulationResult> {
154    run_with(BackendKind::StatevectorGpu { context }, circuit, seed)
155}
156
157/// Execute a Clifford-only circuit on the CUDA GPU stabilizer dispatch.
158///
159/// Wraps [`run_with`] with `BackendKind::StabilizerGpu { context }`. The
160/// dispatch applies independent-subsystem decomposition and the qubit-count
161/// crossover ([`crate::gpu::stabilizer_min_qubits`]); sub-circuits below
162/// threshold automatically run on the CPU stabilizer. Non-Clifford circuits
163/// are rejected at dispatch time with the same error shape as
164/// [`BackendKind::Stabilizer`].
165///
166/// ```no_run
167/// # #[cfg(feature = "gpu")] {
168/// use prism_q::gpu::GpuContext;
169/// use prism_q::{run_with_stabilizer_gpu, Circuit};
170///
171/// let ctx = GpuContext::new(0).expect("no usable GPU");
172/// let circuit = Circuit::new(1024, 0);
173/// let _result = run_with_stabilizer_gpu(&circuit, 42, ctx).unwrap();
174/// # }
175/// ```
176#[cfg(feature = "gpu")]
177pub fn run_with_stabilizer_gpu(
178    circuit: &Circuit,
179    seed: u64,
180    context: std::sync::Arc<crate::gpu::GpuContext>,
181) -> Result<SimulationResult> {
182    run_with(BackendKind::StabilizerGpu { context }, circuit, seed)
183}
184
185fn run_with_internal(
186    kind: BackendKind,
187    circuit: &Circuit,
188    seed: u64,
189    opts: SimOptions,
190) -> Result<SimulationResult> {
191    if !matches!(kind, BackendKind::Auto) {
192        validate_explicit_backend(&kind, circuit)?;
193    }
194    let mut has_partial_independence = false;
195    if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
196        let components = circuit.independent_subsystems();
197        if components.len() > 1 {
198            if should_decompose(&components, circuit.num_qubits) {
199                let max_block = components.iter().map(|c| c.len()).max().unwrap_or(0);
200                if matches!(kind, BackendKind::Auto)
201                    && circuit.is_clifford_only()
202                    && circuit.num_qubits >= MIN_FACTORED_STABILIZER_QUBITS
203                    && max_block >= MIN_BLOCK_FOR_FACTORED_STAB
204                {
205                    let mut backend =
206                        crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed);
207                    let fs_opts = if circuit.num_qubits > 64 {
208                        SimOptions {
209                            probabilities: false,
210                        }
211                    } else {
212                        opts
213                    };
214                    return execute(&mut backend, circuit, &fs_opts);
215                }
216                return run_decomposed(&kind, &components, circuit, seed, &opts);
217            }
218            has_partial_independence = true;
219        }
220    }
221    if matches!(kind, BackendKind::Auto)
222        && circuit.is_clifford_plus_t()
223        && circuit.has_t_gates()
224        && circuit.num_qubits <= MAX_STABILIZER_RANK_QUBITS
225    {
226        let t = circuit.t_count();
227        let sr_budget = stabilizer_rank_budget(circuit.num_qubits);
228        if t <= MAX_AUTO_T_COUNT_EXACT && t <= sr_budget {
229            let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
230            return Ok(probs_only_result(sr.probabilities));
231        }
232        if t <= MAX_AUTO_T_COUNT_APPROX && t <= sr_budget {
233            let sr =
234                stabilizer_rank::run_stabilizer_rank_approx(circuit, AUTO_APPROX_MAX_TERMS, seed)?;
235            return Ok(probs_only_result(sr.probabilities));
236        }
237    }
238    if let Some(result) = try_temporal_clifford(&kind, circuit, seed) {
239        return result;
240    }
241    match select_dispatch(&kind, circuit, seed, has_partial_independence) {
242        DispatchAction::Backend(mut backend) => execute(&mut *backend, circuit, &opts),
243        DispatchAction::StabilizerRank => {
244            let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
245            Ok(probs_only_result(sr.probabilities))
246        }
247        DispatchAction::StochasticPauli { num_samples } => {
248            let spp = unified_pauli::run_spp(circuit, num_samples, seed)?;
249            Ok(probs_only_result(unified_pauli::spp_to_probabilities(&spp)))
250        }
251        DispatchAction::DeterministicPauli { epsilon, max_terms } => {
252            let spd = unified_pauli::run_spd(circuit, epsilon, max_terms)?;
253            Ok(probs_only_result(unified_pauli::spd_to_probabilities(&spd)))
254        }
255    }
256}
257
258/// Execute a circuit on a pre-constructed backend.
259///
260/// Use this when you need direct control over the backend instance
261/// (e.g., testing a specific backend). For automatic dispatch, use [`run`].
262pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<SimulationResult> {
263    execute(backend, circuit, &SimOptions::default())
264}
265
266/// Parse an OpenQASM string and execute with automatic backend selection.
267pub fn run_qasm(qasm: &str, seed: u64) -> Result<SimulationResult> {
268    let circuit = crate::circuit::openqasm::parse(qasm)?;
269    run(&circuit, seed)
270}
271
272/// Execute a circuit multiple times, collecting measurement outcomes.
273pub fn run_shots(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
274    run_shots_with(BackendKind::Auto, circuit, num_shots, seed)
275}
276
277pub(crate) fn supports_compiled_measurement_sampling(circuit: &Circuit) -> bool {
278    circuit.is_clifford_only()
279        && !circuit.has_resets()
280        && circuit.has_terminal_measurements_only()
281        && circuit
282            .instructions
283            .iter()
284            .any(|inst| matches!(inst, Instruction::Measure { .. }))
285}
286
287fn supports_deferred_measurement_sampling(circuit: &Circuit) -> bool {
288    circuit.is_clifford_only()
289        && (circuit.has_resets() || !circuit.has_terminal_measurements_only())
290        && circuit
291            .instructions
292            .iter()
293            .any(|inst| matches!(inst, Instruction::Measure { .. }))
294        && !circuit
295            .instructions
296            .iter()
297            .any(|inst| matches!(inst, Instruction::Conditional { .. }))
298}
299
300fn is_clifford_sampler_kind(kind: &BackendKind) -> bool {
301    match kind {
302        BackendKind::Auto | BackendKind::Stabilizer | BackendKind::FactoredStabilizer => true,
303        #[cfg(feature = "gpu")]
304        BackendKind::StabilizerGpu { .. } => true,
305        _ => false,
306    }
307}
308
309fn should_use_compiled_clifford_sampling(
310    kind: &BackendKind,
311    circuit: &Circuit,
312    num_shots: usize,
313) -> bool {
314    num_shots >= 2
315        && supports_compiled_measurement_sampling(circuit)
316        && is_clifford_sampler_kind(kind)
317}
318
319fn should_use_deferred_clifford_sampling(
320    kind: &BackendKind,
321    circuit: &Circuit,
322    num_shots: usize,
323) -> bool {
324    num_shots >= 2
325        && supports_deferred_measurement_sampling(circuit)
326        && is_clifford_sampler_kind(kind)
327}
328
329fn compile_measurements_for_kind(
330    kind: &BackendKind,
331    circuit: &Circuit,
332    seed: u64,
333) -> Result<compiled::CompiledSampler> {
334    #[cfg(not(feature = "gpu"))]
335    let _ = kind;
336
337    let sampler = compiled::compile_measurements(circuit, seed)?;
338
339    #[cfg(feature = "gpu")]
340    if let BackendKind::StabilizerGpu { context } = kind {
341        return Ok(sampler.with_gpu(context.clone()));
342    }
343
344    Ok(sampler)
345}
346
347/// Execute a circuit multiple times and return outcome counts directly.
348///
349/// For Clifford circuits with terminal measurements and no resets, Auto,
350/// Stabilizer, FactoredStabilizer, and explicit `StabilizerGpu` route through
351/// the compiled sampler's optimized counting path. Explicit `StabilizerGpu`
352/// carries its GPU context into the compiled sampler so large shot runs avoid
353/// the raw tableau measurement round-trips. Other circuits fall back to
354/// per-shot simulation with counting.
355pub fn run_counts(
356    kind: BackendKind,
357    circuit: &Circuit,
358    num_shots: usize,
359    seed: u64,
360) -> Result<HashMap<Vec<u64>, u64>> {
361    if should_use_compiled_clifford_sampling(&kind, circuit, num_shots) {
362        let mut sampler = compile_measurements_for_kind(&kind, circuit, seed)?;
363        return sampler.try_sample_counts(num_shots);
364    }
365
366    let result = run_shots_with(kind, circuit, num_shots, seed)?;
367    Ok(result.counts())
368}
369
370/// Compute per-qubit marginal probabilities: P(q_i = 0) and P(q_i = 1) for each qubit.
371///
372/// Returns a `Vec<(f64, f64)>` of length `num_qubits`, where each element is `(p0, p1)`.
373///
374/// For Clifford+T circuits, uses Sparse Pauli Dynamics (SPD), Heisenberg-picture
375/// backward propagation that scales with Pauli complexity, not 2^n. This is orders
376/// of magnitude faster than statevector for structured Clifford+T circuits (25-400x
377/// at 14-22 qubits).
378///
379/// For pure Clifford circuits, exact marginals still come from backend
380/// probabilities. Sampled parity-matrix marginals remain available through
381/// `compile_measurements(...).sample_marginals(...)`.
382///
383/// For other circuits, falls back to statevector probabilities and extracts
384/// marginals.
385pub fn run_marginals(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
386    let n = circuit.num_qubits;
387
388    if (matches!(
389        kind,
390        BackendKind::Auto | BackendKind::DeterministicPauli { .. }
391    )) && circuit.is_clifford_plus_t()
392        && circuit.has_t_gates()
393        && n >= MIN_QUBITS_FOR_SPD_AUTO
394    {
395        let spd = unified_pauli::run_spd(circuit, 0.0, AUTO_SPD_MAX_TERMS)?;
396        return Ok(spd
397            .expectations
398            .iter()
399            .map(|ez| {
400                let p0 = ((1.0 + ez) / 2.0).clamp(0.0, 1.0);
401                (p0, 1.0 - p0)
402            })
403            .collect());
404    }
405
406    let result = run_with(kind, circuit, seed)?;
407    if let Some(probs) = &result.probabilities {
408        let mut marginals = vec![(0.0f64, 0.0f64); n];
409        let dense = probs.to_vec();
410        for (idx, &p) in dense.iter().enumerate() {
411            for (q, m) in marginals.iter_mut().enumerate() {
412                if (idx >> q) & 1 == 0 {
413                    m.0 += p;
414                } else {
415                    m.1 += p;
416                }
417            }
418        }
419        Ok(marginals)
420    } else {
421        Ok(vec![(0.5, 0.5); n])
422    }
423}
424
425/// Execute a circuit multiple times with explicit backend selection.
426pub fn run_shots_with(
427    kind: BackendKind,
428    circuit: &Circuit,
429    num_shots: usize,
430    seed: u64,
431) -> Result<ShotsResult> {
432    // Compiled sampler: O(n²·m) compile + O(r·m/64) per shot with LUT.
433    // Always polynomial, avoids the O(2^k) probability computation path.
434    // Explicit `StabilizerGpu` attaches its CUDA context here so repeated shot
435    // runs use the compiled GPU sampling path instead of the raw GPU tableau
436    // measurement loop, but only for circuits the compiled sampler models
437    // exactly.
438    if should_use_compiled_clifford_sampling(&kind, circuit, num_shots) {
439        let mut sampler = compile_measurements_for_kind(&kind, circuit, seed)?;
440        let packed = sampler.try_sample_bulk_packed(num_shots)?;
441        let meas_map = circuit.measurement_map();
442        return Ok(ShotsResult {
443            shots: packed_shots_to_classical_bits(&packed, &meas_map, circuit.num_classical_bits),
444            num_classical_bits: circuit.num_classical_bits,
445        });
446    }
447
448    if should_use_deferred_clifford_sampling(&kind, circuit, num_shots) {
449        if let Ok(deferred) = compiled::defer_measure_reset_circuit(circuit) {
450            let mut sampler = compile_measurements_for_kind(&kind, &deferred, seed)?;
451            let packed = sampler.try_sample_bulk_packed(num_shots)?;
452            let meas_map = deferred.measurement_map();
453            return Ok(ShotsResult {
454                shots: packed_shots_to_classical_bits(
455                    &packed,
456                    &meas_map,
457                    circuit.num_classical_bits,
458                ),
459                num_classical_bits: circuit.num_classical_bits,
460            });
461        }
462    }
463
464    if circuit.has_terminal_measurements_only() {
465        let stripped = circuit.without_measurements();
466        let result = run_with_internal(kind.clone(), &stripped, seed, SimOptions::default())?;
467        if let Some(probs) = result.probabilities {
468            let meas_map = circuit.measurement_map();
469            let shots = sample_shots(
470                &probs,
471                &meas_map,
472                circuit.num_classical_bits,
473                num_shots,
474                seed,
475            );
476            return Ok(ShotsResult {
477                shots,
478                num_classical_bits: circuit.num_classical_bits,
479            });
480        }
481    }
482
483    // Slow path: mid-circuit measurements require per-shot simulation.
484    // Pre-compute seed-independent analysis to avoid redundant work.
485    if !matches!(kind, BackendKind::Auto) {
486        validate_explicit_backend(&kind, circuit)?;
487    }
488
489    let mut has_partial_independence = false;
490    let decompose = if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
491        let comps = circuit.independent_subsystems();
492        if comps.len() > 1 {
493            if should_decompose(&comps, circuit.num_qubits) {
494                Some(comps)
495            } else {
496                has_partial_independence = true;
497                None
498            }
499        } else {
500            None
501        }
502    } else {
503        None
504    };
505
506    if matches!(kind, BackendKind::StabilizerRank) {
507        return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
508    }
509    if matches!(
510        kind,
511        BackendKind::StochasticPauli { .. } | BackendKind::DeterministicPauli { .. }
512    ) {
513        return Err(crate::error::PrismError::IncompatibleBackend {
514            backend: format!("{kind:?}"),
515            reason: "Pauli propagation backends do not support mid-circuit measurements".into(),
516        });
517    }
518    if matches!(kind, BackendKind::Auto) && circuit.is_clifford_plus_t() && circuit.has_t_gates() {
519        let t = circuit.t_count();
520        let sr_budget = stabilizer_rank_budget(circuit.num_qubits);
521        if t <= MAX_AUTO_T_COUNT_SHOTS && t <= sr_budget {
522            return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
523        }
524    }
525
526    if has_temporal_clifford_opportunity(&kind, circuit) {
527        return run_shots_fallback(&kind, circuit, num_shots, seed);
528    }
529
530    let supports_fused = supports_fused_for_kind(&kind, circuit);
531    let mut shots = Vec::with_capacity(num_shots);
532    let opts = SimOptions::classical_only();
533
534    if let Some(ref comps) = decompose {
535        let partitions = circuit.partition_subcircuits(comps);
536        let fused_blocks: Vec<_> = partitions
537            .iter()
538            .map(|(sub, _, _)| {
539                crate::circuit::fusion::fuse_circuit(sub, supports_fused_for_kind(&kind, sub))
540            })
541            .collect();
542
543        for i in 0..num_shots {
544            let shot_seed = seed.wrapping_add(i as u64);
545            let result = run_decomposed_prefused(
546                &kind,
547                comps,
548                &partitions,
549                &fused_blocks,
550                shot_seed,
551                &opts,
552                circuit,
553            )?;
554            shots.push(result.classical_bits);
555        }
556    } else {
557        let fused = crate::circuit::fusion::fuse_circuit(circuit, supports_fused);
558
559        for i in 0..num_shots {
560            let shot_seed = seed.wrapping_add(i as u64);
561            let mut backend = select_backend(&kind, circuit, shot_seed, has_partial_independence);
562            let result = execute_circuit(&mut *backend, &fused, &opts)?;
563            shots.push(result.classical_bits);
564        }
565    }
566
567    Ok(ShotsResult {
568        shots,
569        num_classical_bits: circuit.num_classical_bits,
570    })
571}
572
573/// Execute a noisy circuit for multiple shots with explicit backend selection.
574///
575/// For Clifford circuits with Auto/Stabilizer/FactoredStabilizer backends,
576/// uses the compiled noisy sampler (fast O(n²·m) compile + O(events·m/64) per shot).
577/// For all other cases, falls back to per-shot simulation with noise injection.
578/// The compiled noisy path is limited to terminal measurements with no resets
579/// or classical conditionals.
580fn auto_general_noise_backend(circuit: &Circuit) -> BackendKind {
581    if !circuit.has_entangling_gates() {
582        BackendKind::ProductState
583    } else if circuit.num_qubits > max_statevector_qubits() {
584        if circuit.is_sparse_friendly() {
585            BackendKind::Sparse
586        } else {
587            BackendKind::Mps {
588                max_bond_dim: AUTO_MPS_BOND_DIM,
589            }
590        }
591    } else {
592        BackendKind::Statevector
593    }
594}
595
596pub fn run_shots_with_noise(
597    kind: BackendKind,
598    circuit: &Circuit,
599    noise_model: &noise::NoiseModel,
600    num_shots: usize,
601    seed: u64,
602) -> Result<ShotsResult> {
603    if !kind.supports_noisy_per_shot() {
604        return Err(crate::error::PrismError::IncompatibleBackend {
605            backend: format!("{kind:?}"),
606            reason: "this backend does not support noisy per-shot simulation".into(),
607        });
608    }
609
610    let is_stabilizer_kind = kind.is_stabilizer_family();
611
612    if is_stabilizer_kind && !noise_model.is_pauli_only() {
613        return Err(crate::error::PrismError::IncompatibleBackend {
614            backend: format!("{kind:?}"),
615            reason: format!(
616                "stabilizer backends only support Pauli/depolarizing noise; use {} for amplitude damping, phase damping, thermal relaxation, custom Kraus, or readout errors",
617                BackendKind::general_noise_backend_names()
618            ),
619        });
620    }
621
622    if !noise_model.is_pauli_only() && !kind.supports_general_noise() {
623        return Err(crate::error::PrismError::IncompatibleBackend {
624            backend: format!("{kind:?}"),
625            reason: format!(
626                "non-Pauli noise requires {}",
627                BackendKind::general_noise_backend_names()
628            ),
629        });
630    }
631
632    if is_stabilizer_kind && !circuit.is_clifford_only() {
633        return Err(crate::error::PrismError::IncompatibleBackend {
634            backend: format!("{kind:?}"),
635            reason: "circuit contains non-Clifford gates".into(),
636        });
637    }
638
639    if !matches!(kind, BackendKind::Auto) {
640        validate_explicit_backend(&kind, circuit)?;
641    }
642
643    if noise_model.is_pauli_only() {
644        let use_compiled = matches!(
645            kind,
646            BackendKind::Auto | BackendKind::Stabilizer | BackendKind::FactoredStabilizer
647        ) && supports_compiled_measurement_sampling(circuit)
648            || {
649                #[cfg(feature = "gpu")]
650                {
651                    matches!(kind, BackendKind::StabilizerGpu { .. })
652                        && supports_compiled_measurement_sampling(circuit)
653                }
654                #[cfg(not(feature = "gpu"))]
655                {
656                    false
657                }
658            };
659
660        if use_compiled {
661            #[cfg(feature = "gpu")]
662            if let BackendKind::StabilizerGpu { context } = &kind {
663                return noise::run_shots_noisy_with_gpu(
664                    circuit,
665                    noise_model,
666                    num_shots,
667                    seed,
668                    context.clone(),
669                );
670            }
671            return noise::run_shots_noisy(circuit, noise_model, num_shots, seed);
672        }
673    }
674
675    let trajectory_kind = if matches!(kind, BackendKind::Auto) && !noise_model.is_pauli_only() {
676        auto_general_noise_backend(circuit)
677    } else {
678        kind
679    };
680
681    trajectory::run_trajectories(
682        |s| select_backend(&trajectory_kind, circuit, s, false),
683        circuit,
684        noise_model,
685        num_shots,
686        seed,
687    )
688}
689
690fn run_shots_fallback(
691    kind: &BackendKind,
692    circuit: &Circuit,
693    num_shots: usize,
694    seed: u64,
695) -> Result<ShotsResult> {
696    let mut shots = Vec::with_capacity(num_shots);
697    let opts = SimOptions::classical_only();
698    for i in 0..num_shots {
699        let shot_seed = seed.wrapping_add(i as u64);
700        let result = run_with_internal(kind.clone(), circuit, shot_seed, opts)?;
701        shots.push(result.classical_bits);
702    }
703    Ok(ShotsResult {
704        shots,
705        num_classical_bits: circuit.num_classical_bits,
706    })
707}
708
709#[cfg(test)]
710mod tests {
711    use super::dispatch::min_clifford_prefix_gates;
712    use super::*;
713    use crate::backend::mps::MpsBackend;
714    use crate::backend::product::ProductStateBackend;
715    use crate::backend::sparse::SparseBackend;
716    use crate::backend::stabilizer::StabilizerBackend;
717    use crate::backend::statevector::StatevectorBackend;
718    use crate::backend::tensornetwork::TensorNetworkBackend;
719    use crate::circuit::smallvec;
720    use crate::gates::Gate;
721
722    fn make_clifford_circuit() -> Circuit {
723        let mut c = Circuit::new(3, 0);
724        c.add_gate(Gate::H, &[0]);
725        c.add_gate(Gate::Cx, &[0, 1]);
726        c.add_gate(Gate::Cx, &[1, 2]);
727        c.add_gate(Gate::S, &[0]);
728        c
729    }
730
731    fn make_product_circuit() -> Circuit {
732        let mut c = Circuit::new(4, 0);
733        c.add_gate(Gate::H, &[0]);
734        c.add_gate(Gate::Rx(1.0), &[1]);
735        c.add_gate(Gate::T, &[2]);
736        c.add_gate(Gate::Y, &[3]);
737        c
738    }
739
740    fn make_general_circuit() -> Circuit {
741        let mut c = Circuit::new(3, 0);
742        c.add_gate(Gate::H, &[0]);
743        c.add_gate(Gate::T, &[0]);
744        c.add_gate(Gate::Cx, &[0, 1]);
745        c
746    }
747
748    #[test]
749    fn test_circuit_is_clifford_only() {
750        assert!(make_clifford_circuit().is_clifford_only());
751        assert!(!make_general_circuit().is_clifford_only());
752        assert!(!make_product_circuit().is_clifford_only());
753    }
754
755    #[test]
756    fn test_circuit_has_entangling_gates() {
757        assert!(make_clifford_circuit().has_entangling_gates());
758        assert!(make_general_circuit().has_entangling_gates());
759        assert!(!make_product_circuit().has_entangling_gates());
760    }
761
762    #[test]
763    fn test_auto_selects_product() {
764        let circuit = make_product_circuit();
765        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
766        assert_eq!(backend.name(), "productstate");
767    }
768
769    #[test]
770    fn test_auto_selects_stabilizer() {
771        let circuit = make_clifford_circuit();
772        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
773        assert_eq!(backend.name(), "stabilizer");
774    }
775
776    #[test]
777    fn test_auto_selects_statevector() {
778        let circuit = make_general_circuit();
779        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
780        assert_eq!(backend.name(), "statevector");
781    }
782
783    #[test]
784    fn test_run_with_auto_matches_explicit() {
785        let circuit = make_general_circuit();
786        let auto_result = run_with(BackendKind::Auto, &circuit, 42).unwrap();
787        let sv_result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
788        let auto_probs = auto_result.probabilities.unwrap().to_vec();
789        let sv_probs = sv_result.probabilities.unwrap().to_vec();
790        for (a, b) in auto_probs.iter().zip(sv_probs.iter()) {
791            assert!((a - b).abs() < 1e-10);
792        }
793    }
794
795    #[test]
796    fn test_run_with_explicit_backends() {
797        let circuit = make_clifford_circuit();
798        assert!(run_with(BackendKind::Statevector, &circuit, 42).is_ok());
799        assert!(run_with(BackendKind::Stabilizer, &circuit, 42).is_ok());
800        assert!(run_with(BackendKind::Sparse, &circuit, 42).is_ok());
801        assert!(run_with(BackendKind::Mps { max_bond_dim: 64 }, &circuit, 42).is_ok());
802    }
803
804    #[test]
805    fn test_run_auto_clifford_probs_match_statevector() {
806        let circuit = make_clifford_circuit();
807        let auto_result = run(&circuit, 42).unwrap();
808        let mut sv = StatevectorBackend::new(42);
809        let sv_result = run_on(&mut sv, &circuit).unwrap();
810        let auto_probs = auto_result.probabilities.unwrap().to_vec();
811        let sv_probs = sv_result.probabilities.unwrap().to_vec();
812        for (a, b) in auto_probs.iter().zip(sv_probs.iter()) {
813            assert!((a - b).abs() < 1e-10);
814        }
815    }
816
817    #[test]
818    fn test_run_qasm() {
819        let qasm = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];";
820        let result = run_qasm(qasm, 42).unwrap();
821        let probs = result.probabilities.unwrap().to_vec();
822        assert!((probs[0] - 0.5).abs() < 1e-10);
823        assert!((probs[3] - 0.5).abs() < 1e-10);
824    }
825
826    #[test]
827    fn test_empty_circuit_is_clifford_and_no_entangling() {
828        let c = Circuit::new(2, 0);
829        assert!(c.is_clifford_only());
830        assert!(!c.has_entangling_gates());
831    }
832
833    #[test]
834    fn test_validate_stabilizer_rejects_non_clifford() {
835        let circuit = make_general_circuit(); // has T gate
836        let result = run_with(BackendKind::Stabilizer, &circuit, 42);
837        assert!(result.is_err());
838        let err = result.unwrap_err();
839        assert!(matches!(
840            err,
841            crate::error::PrismError::IncompatibleBackend { .. }
842        ));
843    }
844
845    #[test]
846    fn test_validate_product_rejects_entangling() {
847        let circuit = make_clifford_circuit(); // has CX
848        let result = run_with(BackendKind::ProductState, &circuit, 42);
849        assert!(result.is_err());
850        let err = result.unwrap_err();
851        assert!(matches!(
852            err,
853            crate::error::PrismError::IncompatibleBackend { .. }
854        ));
855    }
856
857    #[test]
858    fn test_validate_passes_for_compatible() {
859        let clifford = make_clifford_circuit();
860        assert!(run_with(BackendKind::Stabilizer, &clifford, 42).is_ok());
861
862        let product = make_product_circuit();
863        assert!(run_with(BackendKind::ProductState, &product, 42).is_ok());
864    }
865
866    #[test]
867    fn test_auto_moderate_qubit_count_uses_statevector() {
868        let mut circuit = Circuit::new(20, 0);
869        circuit.add_gate(Gate::H, &[0]);
870        circuit.add_gate(Gate::T, &[0]);
871        circuit.add_gate(Gate::Cx, &[0, 1]);
872        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
873        assert_eq!(backend.name(), "statevector");
874    }
875
876    #[test]
877    fn test_auto_selects_factored_with_partial_independence() {
878        let mut circuit = Circuit::new(10, 0);
879        circuit.add_gate(Gate::H, &[0]);
880        circuit.add_gate(Gate::T, &[0]);
881        circuit.add_gate(Gate::Cx, &[0, 1]);
882        let backend = select_backend(&BackendKind::Auto, &circuit, 42, true);
883        assert_eq!(backend.name(), "factored");
884    }
885
886    #[test]
887    fn test_auto_ignores_partial_independence_when_no_entangling() {
888        let circuit = make_product_circuit();
889        let backend = select_backend(&BackendKind::Auto, &circuit, 42, true);
890        assert_eq!(backend.name(), "productstate");
891    }
892
893    #[test]
894    fn test_classical_only_skips_probabilities() {
895        let qasm =
896            "OPENQASM 3.0;\nqubit[2] q;\nbit[1] c;\nh q[0];\ncx q[0], q[1];\nc[0] = measure q[0];";
897        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
898        let result = run_with_internal(
899            BackendKind::Statevector,
900            &circuit,
901            42,
902            SimOptions::classical_only(),
903        )
904        .unwrap();
905        assert!(result.probabilities.is_none());
906        assert_eq!(result.classical_bits.len(), 1);
907    }
908
909    #[test]
910    fn test_default_options_include_probabilities() {
911        let circuit = make_general_circuit();
912        let result = run_with_internal(
913            BackendKind::Statevector,
914            &circuit,
915            42,
916            SimOptions::default(),
917        )
918        .unwrap();
919        assert!(result.probabilities.is_some());
920    }
921
922    #[test]
923    fn test_run_on_always_computes_probabilities() {
924        let circuit = make_clifford_circuit();
925        let mut backend = StatevectorBackend::new(42);
926        let result = run_on(&mut backend, &circuit).unwrap();
927        assert!(result.probabilities.is_some());
928        let probs = result.probabilities.unwrap().to_vec();
929        let sum: f64 = probs.iter().sum();
930        assert!((sum - 1.0).abs() < 1e-10);
931    }
932
933    #[test]
934    fn test_temporal_clifford_matches_statevector() {
935        // Circuit with a long Clifford prefix followed by non-Clifford gates.
936        // At 10q with 20+ prefix gates, temporal decomposition should trigger.
937        let mut c = Circuit::new(10, 0);
938
939        // Clifford prefix: 22 gates (above min_clifford_prefix_gates(10)=20)
940        for i in 0..10 {
941            c.add_gate(Gate::H, &[i]);
942        }
943        for i in 0..9 {
944            c.add_gate(Gate::Cx, &[i, i + 1]);
945        }
946        c.add_gate(Gate::S, &[0]);
947        c.add_gate(Gate::Sdg, &[3]);
948        c.add_gate(Gate::SX, &[7]);
949
950        // Non-Clifford tail
951        c.add_gate(Gate::T, &[0]);
952        c.add_gate(Gate::Rx(0.7), &[1]);
953        c.add_gate(Gate::Cx, &[2, 3]);
954        c.add_gate(Gate::Rz(1.2), &[2]);
955
956        let (prefix, _tail) = c.clifford_prefix_split().unwrap();
957        assert!(prefix.gate_count() >= min_clifford_prefix_gates(c.num_qubits));
958
959        let auto_result = run(&c, 42).unwrap();
960
961        // Pure statevector reference (no temporal decomposition)
962        let mut sv = StatevectorBackend::new(42);
963        let sv_result = run_on(&mut sv, &c).unwrap();
964
965        let auto_probs = auto_result.probabilities.unwrap().to_vec();
966        let sv_probs = sv_result.probabilities.unwrap().to_vec();
967        assert_eq!(auto_probs.len(), sv_probs.len());
968        for (a, s) in auto_probs.iter().zip(sv_probs.iter()) {
969            assert!(
970                (a - s).abs() < 1e-10,
971                "temporal decomp mismatch: auto={a}, sv={s}"
972            );
973        }
974    }
975
976    #[test]
977    fn test_temporal_clifford_complex_circuit_matches_sv() {
978        // 3q circuit: prefix too short for temporal (min_clifford_prefix_gates(3)=16)
979        // but auto must still match statevector.
980        let mut c = Circuit::new(3, 0);
981
982        // Clifford prefix with i-phase generators
983        c.add_gate(Gate::H, &[0]);
984        c.add_gate(Gate::Y, &[1]);
985        c.add_gate(Gate::S, &[0]);
986        c.add_gate(Gate::Cx, &[0, 1]);
987        c.add_gate(Gate::H, &[2]);
988        c.add_gate(Gate::SXdg, &[2]);
989        c.add_gate(Gate::Cz, &[1, 2]);
990        c.add_gate(Gate::Swap, &[0, 2]);
991        c.add_gate(Gate::S, &[1]);
992
993        // Non-Clifford tail
994        c.add_gate(Gate::T, &[0]);
995        c.add_gate(Gate::Ry(0.3), &[1]);
996        c.add_gate(Gate::Cx, &[1, 2]);
997
998        let auto_result = run(&c, 42).unwrap();
999        let mut sv = StatevectorBackend::new(42);
1000        let sv_result = run_on(&mut sv, &c).unwrap();
1001
1002        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1003        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1004        for (a, s) in auto_probs.iter().zip(sv_probs.iter()) {
1005            assert!(
1006                (a - s).abs() < 1e-10,
1007                "complex temporal mismatch: auto={a}, sv={s}"
1008            );
1009        }
1010    }
1011
1012    #[test]
1013    fn test_temporal_clifford_skipped_when_prefix_too_short() {
1014        // Short Clifford prefix, should NOT use temporal decomposition,
1015        // but result must still be correct.
1016        let mut c = Circuit::new(2, 0);
1017        c.add_gate(Gate::H, &[0]);
1018        c.add_gate(Gate::Cx, &[0, 1]);
1019        c.add_gate(Gate::T, &[0]); // 2 Clifford gates < min_clifford_prefix_gates(2)=16
1020
1021        let auto_result = run(&c, 42).unwrap();
1022        let mut sv = StatevectorBackend::new(42);
1023        let sv_result = run_on(&mut sv, &c).unwrap();
1024
1025        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1026        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1027        for (a, s) in auto_probs.iter().zip(sv_probs.iter()) {
1028            assert!((a - s).abs() < 1e-10);
1029        }
1030    }
1031
1032    #[test]
1033    fn test_decomposed_random_blocks_matches_monolithic() {
1034        let circuit = crate::circuits::independent_random_blocks(10, 2, 5, 0xDEAD_BEEF);
1035        let decomposed = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1036        let mut sv = StatevectorBackend::new(42);
1037        let monolithic = run_on(&mut sv, &circuit).unwrap();
1038        let d_probs = decomposed.probabilities.unwrap().to_vec();
1039        let m_probs = monolithic.probabilities.unwrap().to_vec();
1040        assert_eq!(d_probs.len(), m_probs.len());
1041        for (d, m) in d_probs.iter().zip(m_probs.iter()) {
1042            assert!(
1043                (d - m).abs() < 1e-10,
1044                "mismatch: decomposed={d}, monolithic={m}"
1045            );
1046        }
1047    }
1048
1049    #[test]
1050    fn test_per_block_clifford_dispatch() {
1051        // 6-qubit circuit: qubits 0-2 = Clifford block, qubits 3-5 = non-Clifford block.
1052        // The two blocks are independent (no gates bridge them).
1053        // Under Auto dispatch with decomposition, block 0-2 should use Stabilizer
1054        // and block 3-5 should use Statevector. Correctness is checked by comparing
1055        // against a monolithic statevector run.
1056        let mut c = Circuit::new(6, 0);
1057
1058        // Block A (Clifford): GHZ state on qubits 0,1,2
1059        c.add_gate(Gate::H, &[0]);
1060        c.add_gate(Gate::Cx, &[0, 1]);
1061        c.add_gate(Gate::Cx, &[1, 2]);
1062        c.add_gate(Gate::S, &[0]);
1063
1064        // Block B (non-Clifford): qubits 3,4,5
1065        c.add_gate(Gate::H, &[3]);
1066        c.add_gate(Gate::T, &[3]);
1067        c.add_gate(Gate::Cx, &[3, 4]);
1068        c.add_gate(Gate::Rx(0.7), &[5]);
1069        c.add_gate(Gate::Cx, &[4, 5]);
1070
1071        let components = c.independent_subsystems();
1072        assert_eq!(components.len(), 2);
1073
1074        let (sub_a, _, _) = c.extract_subcircuit(&components[0]);
1075        assert!(sub_a.is_clifford_only());
1076        let backend_a = select_backend(&BackendKind::Auto, &sub_a, 42, false);
1077        assert_eq!(backend_a.name(), "stabilizer");
1078
1079        let (sub_b, _, _) = c.extract_subcircuit(&components[1]);
1080        assert!(!sub_b.is_clifford_only());
1081        let backend_b = select_backend(&BackendKind::Auto, &sub_b, 43, false);
1082        assert_eq!(backend_b.name(), "statevector");
1083
1084        // End-to-end: auto (decomposed) must match monolithic statevector
1085        let auto_result = run(&c, 42).unwrap();
1086        let mut sv = StatevectorBackend::new(42);
1087        let mono_result = run_on(&mut sv, &c).unwrap();
1088        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1089        let mono_probs = mono_result.probabilities.unwrap().to_vec();
1090        assert_eq!(auto_probs.len(), mono_probs.len());
1091        for (a, m) in auto_probs.iter().zip(mono_probs.iter()) {
1092            assert!((a - m).abs() < 1e-10, "prob mismatch: auto={a}, mono={m}");
1093        }
1094    }
1095
1096    #[test]
1097    fn test_decomposed_bell_pairs_matches_monolithic() {
1098        let circuit = crate::circuits::independent_bell_pairs(10);
1099        let decomposed = run(&circuit, 42).unwrap();
1100        let mut sv = StatevectorBackend::new(42);
1101        let monolithic = run_on(&mut sv, &circuit).unwrap();
1102        let d_probs = decomposed.probabilities.unwrap().to_vec();
1103        let m_probs = monolithic.probabilities.unwrap().to_vec();
1104        assert_eq!(d_probs.len(), m_probs.len());
1105        for (d, m) in d_probs.iter().zip(m_probs.iter()) {
1106            assert!(
1107                (d - m).abs() < 1e-10,
1108                "mismatch: decomposed={d}, monolithic={m}"
1109            );
1110        }
1111    }
1112
1113    #[test]
1114    fn test_measurement_normalization_statevector() {
1115        let qasm = r#"
1116            OPENQASM 3.0;
1117            qubit[2] q;
1118            bit[2] c;
1119            h q[0];
1120            cx q[0], q[1];
1121            c[0] = measure q[0];
1122            c[1] = measure q[1];
1123        "#;
1124        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1125        let result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1126        let probs = result.probabilities.unwrap().to_vec();
1127        let sum: f64 = probs.iter().sum();
1128        assert!(
1129            (sum - 1.0).abs() < 1e-10,
1130            "statevector post-measurement probs sum to {sum}, expected 1.0"
1131        );
1132    }
1133
1134    #[test]
1135    fn test_measurement_normalization_mps() {
1136        let qasm = r#"
1137            OPENQASM 3.0;
1138            qubit[2] q;
1139            bit[2] c;
1140            h q[0];
1141            cx q[0], q[1];
1142            c[0] = measure q[0];
1143            c[1] = measure q[1];
1144        "#;
1145        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1146        let result = run_with(BackendKind::Mps { max_bond_dim: 64 }, &circuit, 42).unwrap();
1147        let probs = result.probabilities.unwrap().to_vec();
1148        let sum: f64 = probs.iter().sum();
1149        assert!(
1150            (sum - 1.0).abs() < 1e-10,
1151            "MPS post-measurement probs sum to {sum}, expected 1.0"
1152        );
1153    }
1154
1155    #[test]
1156    fn test_measurement_normalization_sparse() {
1157        let qasm = r#"
1158            OPENQASM 3.0;
1159            qubit[2] q;
1160            bit[2] c;
1161            h q[0];
1162            cx q[0], q[1];
1163            c[0] = measure q[0];
1164            c[1] = measure q[1];
1165        "#;
1166        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1167        let result = run_with(BackendKind::Sparse, &circuit, 42).unwrap();
1168        let probs = result.probabilities.unwrap().to_vec();
1169        let sum: f64 = probs.iter().sum();
1170        assert!(
1171            (sum - 1.0).abs() < 1e-10,
1172            "sparse post-measurement probs sum to {sum}, expected 1.0"
1173        );
1174    }
1175
1176    #[test]
1177    fn test_conditional_gate_execution() {
1178        let qasm = r#"
1179            OPENQASM 3.0;
1180            qubit[2] q;
1181            bit[1] c;
1182            x q[0];
1183            c[0] = measure q[0];
1184            if (c[0]) x q[1];
1185        "#;
1186        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1187        let result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1188        // q[0] measured as 1, so if-gate fires, q[1] flipped to |1⟩
1189        // Final state: |11⟩ = index 3
1190        let probs = result.probabilities.unwrap().to_vec();
1191        assert!(
1192            probs[3] > 0.99,
1193            "conditional gate should flip q[1]: probs={probs:?}"
1194        );
1195        assert!(result.classical_bits[0]);
1196    }
1197
1198    fn make_bell_with_measure() -> Circuit {
1199        let qasm = r#"
1200            OPENQASM 3.0;
1201            qubit[2] q;
1202            bit[2] c;
1203            h q[0];
1204            cx q[0], q[1];
1205            c[0] = measure q[0];
1206            c[1] = measure q[1];
1207        "#;
1208        crate::circuit::openqasm::parse(qasm).unwrap()
1209    }
1210
1211    #[test]
1212    fn test_shots_deterministic() {
1213        let circuit = make_bell_with_measure();
1214        let a = run_shots(&circuit, 10, 42).unwrap();
1215        let b = run_shots(&circuit, 10, 42).unwrap();
1216        assert_eq!(a.shots, b.shots);
1217    }
1218
1219    #[test]
1220    fn test_shots_distribution_convergence() {
1221        let circuit = make_bell_with_measure();
1222        let result = run_shots(&circuit, 10000, 42).unwrap();
1223        let counts = result.counts();
1224        let n_00 = counts.get(&vec![0u64]).copied().unwrap_or(0);
1225        let n_11 = counts.get(&vec![3u64]).copied().unwrap_or(0);
1226        let n_01 = counts.get(&vec![2u64]).copied().unwrap_or(0);
1227        let n_10 = counts.get(&vec![1u64]).copied().unwrap_or(0);
1228        assert!(
1229            (4500..=5500).contains(&n_00),
1230            "|00> count {n_00} outside [4500, 5500]"
1231        );
1232        assert!(
1233            (4500..=5500).contains(&n_11),
1234            "|11> count {n_11} outside [4500, 5500]"
1235        );
1236        assert_eq!(n_01, 0, "|01> should never appear in Bell state");
1237        assert_eq!(n_10, 0, "|10> should never appear in Bell state");
1238    }
1239
1240    #[test]
1241    fn test_shots_single_valid_outcome() {
1242        let circuit = make_bell_with_measure();
1243        let shots_result = run_shots(&circuit, 1, 42).unwrap();
1244        let shot = &shots_result.shots[0];
1245        assert_eq!(shot[0], shot[1], "Bell state: both bits must agree");
1246    }
1247
1248    #[test]
1249    fn test_shots_all_zero() {
1250        let qasm = r#"
1251            OPENQASM 3.0;
1252            qubit[3] q;
1253            bit[3] c;
1254            c[0] = measure q[0];
1255            c[1] = measure q[1];
1256            c[2] = measure q[2];
1257        "#;
1258        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1259        let result = run_shots(&circuit, 100, 42).unwrap();
1260        for (i, shot) in result.shots.iter().enumerate() {
1261            assert!(
1262                shot.iter().all(|&b| !b),
1263                "shot {i} should be all-zero: {shot:?}"
1264            );
1265        }
1266    }
1267
1268    #[test]
1269    fn test_shots_mid_circuit_measurement() {
1270        let qasm = r#"
1271            OPENQASM 3.0;
1272            qubit[2] q;
1273            bit[2] c;
1274            x q[0];
1275            c[0] = measure q[0];
1276            if (c[0]) x q[1];
1277            c[1] = measure q[1];
1278        "#;
1279        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1280        let result = run_shots(&circuit, 100, 42).unwrap();
1281        for (i, shot) in result.shots.iter().enumerate() {
1282            assert!(shot[0], "shot {i}: q[0] should always be 1");
1283            assert!(shot[1], "shot {i}: q[1] should always be 1 (conditional)");
1284        }
1285    }
1286
1287    #[test]
1288    fn test_shots_counts_sum() {
1289        let circuit = make_bell_with_measure();
1290        let result = run_shots(&circuit, 500, 42).unwrap();
1291        let counts = result.counts();
1292        let total: u64 = counts.values().sum();
1293        assert_eq!(total, 500);
1294    }
1295
1296    #[test]
1297    fn test_run_counts_factored_stabilizer() {
1298        let circuit = make_bell_with_measure();
1299        let counts = run_counts(BackendKind::FactoredStabilizer, &circuit, 128, 42).unwrap();
1300        let total: u64 = counts.values().sum();
1301        let bell_total = counts.get(&vec![0u64]).copied().unwrap_or(0)
1302            + counts.get(&vec![3u64]).copied().unwrap_or(0);
1303
1304        assert_eq!(total, 128);
1305        assert_eq!(bell_total, 128);
1306    }
1307
1308    fn assert_unit_norm(state: &[num_complex::Complex64], label: &str) {
1309        let norm: f64 = state.iter().map(|a| a.norm_sqr()).sum();
1310        assert!(
1311            (norm - 1.0).abs() < 1e-10,
1312            "{label}: norm = {norm}, expected 1.0"
1313        );
1314    }
1315
1316    #[test]
1317    fn test_export_norm_statevector_bell() {
1318        let circuit = make_clifford_circuit();
1319        let mut backend = StatevectorBackend::new(42);
1320        run_on(&mut backend, &circuit).unwrap();
1321        assert_unit_norm(&backend.export_statevector().unwrap(), "statevector/bell");
1322    }
1323
1324    #[test]
1325    fn test_export_norm_statevector_parametric() {
1326        let circuit = crate::circuits::hardware_efficient_ansatz(6, 3, 42);
1327        let mut backend = StatevectorBackend::new(42);
1328        run_on(&mut backend, &circuit).unwrap();
1329        assert_unit_norm(&backend.export_statevector().unwrap(), "statevector/hea_6q");
1330    }
1331
1332    #[test]
1333    fn test_export_norm_stabilizer() {
1334        let circuit = make_clifford_circuit();
1335        let mut backend = StabilizerBackend::new(42);
1336        run_on(&mut backend, &circuit).unwrap();
1337        assert_unit_norm(&backend.export_statevector().unwrap(), "stabilizer");
1338    }
1339
1340    #[test]
1341    fn test_export_norm_sparse() {
1342        let circuit = make_general_circuit();
1343        let mut backend = SparseBackend::new(42);
1344        run_on(&mut backend, &circuit).unwrap();
1345        assert_unit_norm(&backend.export_statevector().unwrap(), "sparse");
1346    }
1347
1348    #[test]
1349    fn test_export_norm_mps() {
1350        let circuit = make_general_circuit();
1351        let mut backend = MpsBackend::new(64, 42);
1352        run_on(&mut backend, &circuit).unwrap();
1353        assert_unit_norm(&backend.export_statevector().unwrap(), "mps");
1354    }
1355
1356    #[test]
1357    fn test_export_norm_product_state() {
1358        let circuit = make_product_circuit();
1359        let mut backend = ProductStateBackend::new(42);
1360        run_on(&mut backend, &circuit).unwrap();
1361        assert_unit_norm(&backend.export_statevector().unwrap(), "productstate");
1362    }
1363
1364    #[test]
1365    fn test_export_norm_tensor_network() {
1366        let circuit = make_general_circuit();
1367        let mut backend = TensorNetworkBackend::new(42);
1368        run_on(&mut backend, &circuit).unwrap();
1369        assert_unit_norm(&backend.export_statevector().unwrap(), "tensornetwork");
1370    }
1371
1372    #[test]
1373    fn test_export_norm_after_measurement() {
1374        let qasm = r#"
1375            OPENQASM 3.0;
1376            qubit[3] q;
1377            bit[1] c;
1378            h q[0];
1379            cx q[0], q[1];
1380            h q[2];
1381            c[0] = measure q[0];
1382        "#;
1383        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1384        for backend_kind in [
1385            BackendKind::Statevector,
1386            BackendKind::Sparse,
1387            BackendKind::Mps { max_bond_dim: 64 },
1388        ] {
1389            let label = format!("{backend_kind:?}/post-measure");
1390            let mut backend = select_backend(&backend_kind, &circuit, 42, false);
1391            run_on(backend.as_mut(), &circuit).unwrap();
1392            let state = backend.export_statevector().unwrap();
1393            assert_unit_norm(&state, &label);
1394        }
1395    }
1396
1397    #[test]
1398    fn test_export_norm_qft() {
1399        let circuit = crate::circuits::qft_circuit(8);
1400        for (kind, label) in [
1401            (BackendKind::Statevector, "statevector/qft8"),
1402            (BackendKind::Sparse, "sparse/qft8"),
1403            (BackendKind::Mps { max_bond_dim: 128 }, "mps/qft8"),
1404            (BackendKind::TensorNetwork, "tn/qft8"),
1405        ] {
1406            let mut backend = select_backend(&kind, &circuit, 42, false);
1407            run_on(backend.as_mut(), &circuit).unwrap();
1408            let state = backend.export_statevector().unwrap();
1409            assert_unit_norm(&state, label);
1410        }
1411    }
1412
1413    #[test]
1414    fn test_export_factored_unsupported() {
1415        let circuit = make_general_circuit();
1416        let mut backend = crate::backend::factored::FactoredBackend::new(42);
1417        run_on(&mut backend, &circuit).unwrap();
1418        assert!(backend.export_statevector().is_err());
1419    }
1420
1421    #[test]
1422    fn test_shots_random_convergence() {
1423        let circuit = make_bell_with_measure();
1424        let result = run_shots(&circuit, 10000, rand::random()).unwrap();
1425        let counts = result.counts();
1426        let n_00 = counts.get(&vec![0u64]).copied().unwrap_or(0);
1427        let n_11 = counts.get(&vec![3u64]).copied().unwrap_or(0);
1428        let n_01 = counts.get(&vec![2u64]).copied().unwrap_or(0);
1429        let n_10 = counts.get(&vec![1u64]).copied().unwrap_or(0);
1430        // p=0.5, n=10000 → σ=50. Bounds at ±10σ: failure prob < 10^-23.
1431        assert!(
1432            (4500..=5500).contains(&n_00),
1433            "|00> count {n_00} outside [4500, 5500]"
1434        );
1435        assert!(
1436            (4500..=5500).contains(&n_11),
1437            "|11> count {n_11} outside [4500, 5500]"
1438        );
1439        assert_eq!(n_01, 0, "|01> should never appear in Bell state");
1440        assert_eq!(n_10, 0, "|10> should never appear in Bell state");
1441    }
1442
1443    #[test]
1444    fn test_has_terminal_measurements_only() {
1445        let mut c = Circuit::new(2, 0);
1446        c.add_gate(Gate::H, &[0]);
1447        assert!(c.has_terminal_measurements_only());
1448
1449        let qasm = r#"
1450            OPENQASM 3.0;
1451            qubit[2] q;
1452            bit[2] c;
1453            h q[0];
1454            cx q[0], q[1];
1455            c[0] = measure q[0];
1456            c[1] = measure q[1];
1457        "#;
1458        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1459        assert!(circuit.has_terminal_measurements_only());
1460
1461        let qasm = r#"
1462            OPENQASM 3.0;
1463            qubit[2] q;
1464            bit[1] c;
1465            c[0] = measure q[0];
1466            h q[1];
1467        "#;
1468        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1469        assert!(!circuit.has_terminal_measurements_only());
1470
1471        let qasm = r#"
1472            OPENQASM 3.0;
1473            qubit[2] q;
1474            bit[2] c;
1475            x q[0];
1476            c[0] = measure q[0];
1477            if (c[0]) x q[1];
1478            c[1] = measure q[1];
1479        "#;
1480        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1481        assert!(!circuit.has_terminal_measurements_only());
1482
1483        let qasm = r#"
1484            OPENQASM 3.0;
1485            qubit[1] q;
1486            bit[1] c;
1487            h q[0];
1488            c[0] = measure q[0];
1489            x q[0];
1490        "#;
1491        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1492        assert!(!circuit.has_terminal_measurements_only());
1493    }
1494
1495    #[test]
1496    fn test_measurement_map() {
1497        let qasm = r#"
1498            OPENQASM 3.0;
1499            qubit[3] q;
1500            bit[3] c;
1501            c[2] = measure q[0];
1502            c[0] = measure q[2];
1503            c[1] = measure q[1];
1504        "#;
1505        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1506        let map = circuit.measurement_map();
1507        assert_eq!(map, vec![(0, 2), (2, 0), (1, 1)]);
1508    }
1509
1510    #[test]
1511    fn test_fast_path_deterministic_x() {
1512        let qasm = r#"
1513            OPENQASM 3.0;
1514            qubit[1] q;
1515            bit[1] c;
1516            x q[0];
1517            c[0] = measure q[0];
1518        "#;
1519        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1520        assert!(circuit.has_terminal_measurements_only());
1521        let result = run_shots(&circuit, 100, 42).unwrap();
1522        for (i, shot) in result.shots.iter().enumerate() {
1523            assert!(shot[0], "shot {i}: X|0> should always measure 1");
1524        }
1525    }
1526
1527    #[test]
1528    fn test_fast_path_preserves_classical_bit_index() {
1529        let qasm = r#"
1530            OPENQASM 3.0;
1531            qubit[1] q;
1532            bit[3] c;
1533            x q[0];
1534            c[2] = measure q[0];
1535        "#;
1536        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1537        let result = run_shots(&circuit, 16, 42).unwrap();
1538        assert_eq!(result.num_classical_bits, 3);
1539        for shot in &result.shots {
1540            assert_eq!(shot, &vec![false, false, true]);
1541        }
1542    }
1543
1544    #[test]
1545    fn test_fast_path_no_measurements() {
1546        let mut c = Circuit::new(2, 2);
1547        c.add_gate(Gate::H, &[0]);
1548        let result = run_shots(&c, 50, 42).unwrap();
1549        for shot in &result.shots {
1550            assert_eq!(shot.len(), 2);
1551            assert!(!shot[0] && !shot[1], "no measurements → all-false");
1552        }
1553    }
1554
1555    #[test]
1556    fn test_shots_cached_fusion_matches_uncached() {
1557        let qasm = r#"
1558            OPENQASM 3.0;
1559            qubit[2] q;
1560            bit[2] c;
1561            h q[0];
1562            cx q[0], q[1];
1563            c[0] = measure q[0];
1564            x q[1];
1565            c[1] = measure q[1];
1566        "#;
1567        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1568        assert!(!circuit.has_terminal_measurements_only());
1569
1570        let cached = run_shots_with(BackendKind::Statevector, &circuit, 20, 42).unwrap();
1571        for i in 0..20 {
1572            let seed_i = 42u64.wrapping_add(i as u64);
1573            let single = run_with_internal(
1574                BackendKind::Statevector,
1575                &circuit,
1576                seed_i,
1577                SimOptions::default(),
1578            )
1579            .unwrap();
1580            assert_eq!(cached.shots[i], single.classical_bits, "shot {i} mismatch");
1581        }
1582    }
1583
1584    #[test]
1585    fn test_shots_decomposed_cached() {
1586        let qasm = r#"
1587            OPENQASM 3.0;
1588            qubit[8] q;
1589            bit[8] c;
1590            h q[0];
1591            cx q[0], q[1];
1592            c[0] = measure q[0];
1593            x q[1];
1594            c[1] = measure q[1];
1595            h q[4];
1596            cx q[4], q[5];
1597            c[4] = measure q[4];
1598            x q[5];
1599            c[5] = measure q[5];
1600        "#;
1601        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1602        assert!(!circuit.has_terminal_measurements_only());
1603        let comps = circuit.independent_subsystems();
1604        assert!(comps.len() > 1, "circuit should decompose");
1605
1606        let result = run_shots_with(BackendKind::Statevector, &circuit, 10, 42).unwrap();
1607        assert_eq!(result.shots.len(), 10);
1608        for shot in &result.shots {
1609            assert_eq!(shot.len(), 8);
1610        }
1611    }
1612
1613    #[test]
1614    fn test_shots_temporal_clifford_fallback() {
1615        let mut c = Circuit::new(4, 4);
1616        for i in 0..4 {
1617            c.add_gate(Gate::H, &[i]);
1618        }
1619        for i in 0..3 {
1620            c.add_gate(Gate::Cx, &[i, i + 1]);
1621        }
1622        c.add_gate(Gate::T, &[0]);
1623        c.add_measure(0, 0);
1624        c.add_gate(Gate::X, &[1]);
1625        c.add_measure(1, 1);
1626
1627        let result = run_shots_with(BackendKind::Auto, &c, 10, 42).unwrap();
1628        assert_eq!(result.shots.len(), 10);
1629        for shot in &result.shots {
1630            assert_eq!(shot.len(), 4);
1631        }
1632    }
1633
1634    #[test]
1635    fn test_stabilizer_rank_dispatch() {
1636        let circuit = make_general_circuit();
1637        let result = run_with(BackendKind::StabilizerRank, &circuit, 42).unwrap();
1638        let probs = result.probabilities.unwrap().to_vec();
1639        assert_eq!(probs.len(), 8);
1640        let total: f64 = probs.iter().sum();
1641        assert!((total - 1.0).abs() < 1e-10);
1642
1643        let sv_result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1644        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1645        for (i, (sr, sv)) in probs.iter().zip(sv_probs.iter()).enumerate() {
1646            assert!(
1647                (sr - sv).abs() < 1e-10,
1648                "prob[{i}]: stab_rank={sr}, statevector={sv}"
1649            );
1650        }
1651    }
1652
1653    #[test]
1654    fn test_stabilizer_rank_rejects_no_t() {
1655        let circuit = make_clifford_circuit();
1656        let result = run_with(BackendKind::StabilizerRank, &circuit, 42);
1657        assert!(result.is_err());
1658    }
1659
1660    #[test]
1661    fn test_auto_clifford_plus_t_probabilities() {
1662        let circuit = make_general_circuit();
1663        assert!(circuit.is_clifford_plus_t());
1664        assert!(circuit.has_t_gates());
1665
1666        let auto_result = run_with(BackendKind::Auto, &circuit, 42).unwrap();
1667        let sv_result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1668
1669        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1670        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1671        for (i, (a, s)) in auto_probs.iter().zip(sv_probs.iter()).enumerate() {
1672            assert!(
1673                (a - s).abs() < 1e-10,
1674                "prob[{i}]: auto={a}, statevector={s}"
1675            );
1676        }
1677    }
1678
1679    #[test]
1680    fn test_auto_clifford_plus_t_shots() {
1681        let mut c = Circuit::new(2, 2);
1682        c.add_gate(Gate::H, &[0]);
1683        c.add_gate(Gate::T, &[0]);
1684        c.add_gate(Gate::Cx, &[0, 1]);
1685        c.add_measure(0, 0);
1686        c.add_measure(1, 1);
1687
1688        let result = run_shots_with(BackendKind::Auto, &c, 100, 42).unwrap();
1689        assert_eq!(result.shots.len(), 100);
1690        for shot in &result.shots {
1691            assert_eq!(shot.len(), 2);
1692        }
1693    }
1694
1695    #[test]
1696    fn test_decomposed_mixed_clifford_and_t() {
1697        // Two independent subsystems: q0-q1 (Clifford+T), q2-q3 (Clifford-only)
1698        // Under decomposition, q2-q3 should route to Stabilizer, q0-q1 to StabilizerRank
1699        let mut c = Circuit::new(4, 0);
1700        c.add_gate(Gate::H, &[0]);
1701        c.add_gate(Gate::T, &[0]);
1702        c.add_gate(Gate::Cx, &[0, 1]);
1703        c.add_gate(Gate::H, &[2]);
1704        c.add_gate(Gate::Cx, &[2, 3]);
1705
1706        let subs = c.independent_subsystems();
1707        assert_eq!(subs.len(), 2);
1708
1709        let auto_result = run_with(BackendKind::Auto, &c, 42).unwrap();
1710        let sv_result = run_with(BackendKind::Statevector, &c, 42).unwrap();
1711
1712        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1713        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1714        for (i, (a, s)) in auto_probs.iter().zip(sv_probs.iter()).enumerate() {
1715            assert!(
1716                (a - s).abs() < 1e-10,
1717                "prob[{i}]: auto={a}, statevector={s}"
1718            );
1719        }
1720    }
1721
1722    #[test]
1723    fn test_run_shots_with_noise_clifford_uses_compiled() {
1724        let n = 10;
1725        let mut circuit = crate::circuits::ghz_circuit(n);
1726        circuit.num_classical_bits = n;
1727        for i in 0..n {
1728            circuit.add_measure(i, i);
1729        }
1730        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
1731        let result = run_shots_with_noise(BackendKind::Auto, &circuit, &noise, 100, 42).unwrap();
1732        assert_eq!(result.shots.len(), 100);
1733        assert!(result.shots[0].len() == n);
1734    }
1735
1736    #[test]
1737    fn test_run_shots_with_noise_statevector_brute() {
1738        let mut circuit = Circuit::new(3, 3);
1739        circuit.add_gate(Gate::H, &[0]);
1740        circuit.add_gate(Gate::T, &[0]);
1741        circuit.add_gate(Gate::Cx, &[0, 1]);
1742        circuit.add_measure(0, 0);
1743        circuit.add_measure(1, 1);
1744        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
1745        let result =
1746            run_shots_with_noise(BackendKind::Statevector, &circuit, &noise, 50, 42).unwrap();
1747        assert_eq!(result.shots.len(), 50);
1748        assert_eq!(result.shots[0].len(), 3);
1749    }
1750
1751    #[test]
1752    fn test_run_shots_with_noise_auto_non_clifford() {
1753        let mut circuit = Circuit::new(3, 3);
1754        circuit.add_gate(Gate::H, &[0]);
1755        circuit.add_gate(Gate::T, &[0]);
1756        circuit.add_gate(Gate::Cx, &[0, 1]);
1757        circuit.add_measure(0, 0);
1758        circuit.add_measure(1, 1);
1759        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.001);
1760        let result = run_shots_with_noise(BackendKind::Auto, &circuit, &noise, 100, 42).unwrap();
1761        assert_eq!(result.shots.len(), 100);
1762    }
1763
1764    #[cfg(feature = "gpu")]
1765    #[test]
1766    fn test_run_shots_with_stabilizer_gpu_falls_back_for_reset_circuits() {
1767        let mut circuit = Circuit::new(1, 1);
1768        circuit.add_gate(Gate::X, &[0]);
1769        circuit.add_reset(0);
1770        circuit.add_measure(0, 0);
1771
1772        let cpu = run_shots_with(BackendKind::Stabilizer, &circuit, 8, 42).unwrap();
1773        let gpu = run_shots_with(
1774            BackendKind::StabilizerGpu {
1775                context: crate::gpu::GpuContext::stub_for_tests(),
1776            },
1777            &circuit,
1778            8,
1779            42,
1780        )
1781        .unwrap();
1782
1783        assert_eq!(gpu.shots, cpu.shots);
1784    }
1785
1786    #[cfg(feature = "gpu")]
1787    #[test]
1788    fn test_run_shots_with_stabilizer_gpu_falls_back_for_conditionals() {
1789        let mut circuit = Circuit::new(2, 2);
1790        circuit.add_gate(Gate::H, &[0]);
1791        circuit.add_measure(0, 0);
1792        circuit.instructions.push(Instruction::Conditional {
1793            condition: crate::circuit::ClassicalCondition::BitIsOne(0),
1794            gate: Gate::X,
1795            targets: crate::circuit::smallvec![1],
1796        });
1797        circuit.add_measure(1, 1);
1798
1799        let cpu = run_shots_with(BackendKind::Stabilizer, &circuit, 256, 42).unwrap();
1800        let gpu = run_shots_with(
1801            BackendKind::StabilizerGpu {
1802                context: crate::gpu::GpuContext::stub_for_tests(),
1803            },
1804            &circuit,
1805            256,
1806            42,
1807        )
1808        .unwrap();
1809
1810        assert_eq!(gpu.shots, cpu.shots);
1811    }
1812
1813    #[cfg(feature = "gpu")]
1814    #[test]
1815    fn test_run_shots_with_noise_stabilizer_gpu_matches_stabilizer() {
1816        let n = 8;
1817        let mut circuit = crate::circuits::ghz_circuit(n);
1818        circuit.num_classical_bits = n;
1819        for i in 0..n {
1820            circuit.add_measure(i, i);
1821        }
1822        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
1823
1824        let cpu = run_shots_with_noise(BackendKind::Stabilizer, &circuit, &noise, 128, 42).unwrap();
1825        let gpu = run_shots_with_noise(
1826            BackendKind::StabilizerGpu {
1827                context: crate::gpu::GpuContext::stub_for_tests(),
1828            },
1829            &circuit,
1830            &noise,
1831            128,
1832            42,
1833        )
1834        .unwrap();
1835
1836        assert_eq!(gpu.shots, cpu.shots);
1837    }
1838
1839    #[test]
1840    fn test_run_marginals_bell_pair() {
1841        let mut c = Circuit::new(2, 0);
1842        c.add_gate(Gate::H, &[0]);
1843        c.add_gate(Gate::Cx, &[0, 1]);
1844        let m = run_marginals(BackendKind::Auto, &c, 42).unwrap();
1845        assert_eq!(m.len(), 2);
1846        assert!((m[0].0 - 0.5).abs() < 1e-10);
1847        assert!((m[0].1 - 0.5).abs() < 1e-10);
1848        assert!((m[1].0 - 0.5).abs() < 1e-10);
1849        assert!((m[1].1 - 0.5).abs() < 1e-10);
1850    }
1851
1852    #[test]
1853    fn test_run_marginals_x_gate() {
1854        let mut c = Circuit::new(2, 0);
1855        c.add_gate(Gate::X, &[0]);
1856        let m = run_marginals(BackendKind::Auto, &c, 42).unwrap();
1857        assert!((m[0].0 - 0.0).abs() < 1e-10);
1858        assert!((m[0].1 - 1.0).abs() < 1e-10);
1859        assert!((m[1].0 - 1.0).abs() < 1e-10);
1860        assert!((m[1].1 - 0.0).abs() < 1e-10);
1861    }
1862
1863    #[test]
1864    fn test_run_marginals_clifford_t_spd_path() {
1865        let c = crate::circuits::clifford_t_circuit(14, 10, 0.1, 42);
1866        let m_spd = run_marginals(BackendKind::Auto, &c, 42).unwrap();
1867        assert_eq!(m_spd.len(), 14);
1868        for (p0, p1) in &m_spd {
1869            assert!(*p0 >= 0.0 && *p0 <= 1.0);
1870            assert!((p0 + p1 - 1.0).abs() < 1e-10);
1871        }
1872
1873        let m_sv = run_marginals(BackendKind::Statevector, &c, 42).unwrap();
1874        for i in 0..14 {
1875            assert!(
1876                (m_spd[i].0 - m_sv[i].0).abs() < 1e-6,
1877                "qubit {i}: SPD p0={} vs SV p0={}",
1878                m_spd[i].0,
1879                m_sv[i].0
1880            );
1881        }
1882    }
1883
1884    // ── Dispatch validation ───────────────────────────────────────────
1885
1886    #[test]
1887    fn test_validate_factored_stabilizer_rejects_non_clifford() {
1888        let circuit = make_general_circuit();
1889        assert!(matches!(
1890            run_with(BackendKind::FactoredStabilizer, &circuit, 42).unwrap_err(),
1891            crate::error::PrismError::IncompatibleBackend { .. }
1892        ));
1893    }
1894
1895    #[test]
1896    fn test_validate_stabilizer_rank_rejects_no_t_gates() {
1897        let circuit = make_clifford_circuit();
1898        assert!(matches!(
1899            run_with(BackendKind::StabilizerRank, &circuit, 42).unwrap_err(),
1900            crate::error::PrismError::IncompatibleBackend { .. }
1901        ));
1902    }
1903
1904    #[test]
1905    fn test_validate_factored_stabilizer_accepts_clifford() {
1906        assert!(run_with(
1907            BackendKind::FactoredStabilizer,
1908            &make_clifford_circuit(),
1909            42
1910        )
1911        .is_ok());
1912    }
1913
1914    // ── Pauli backend error paths ───────────────────────────────────────
1915
1916    #[test]
1917    fn test_pauli_backends_reject_mid_circuit_measurements() {
1918        let qasm = r#"
1919            OPENQASM 3.0;
1920            qubit[2] q;
1921            bit[2] c;
1922            h q[0];
1923            c[0] = measure q[0];
1924            cx q[0], q[1];
1925            c[1] = measure q[1];
1926        "#;
1927        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1928
1929        assert!(matches!(
1930            run_shots_with(
1931                BackendKind::StochasticPauli { num_samples: 100 },
1932                &circuit,
1933                10,
1934                42
1935            )
1936            .unwrap_err(),
1937            crate::error::PrismError::IncompatibleBackend { .. }
1938        ));
1939        assert!(matches!(
1940            run_shots_with(
1941                BackendKind::DeterministicPauli {
1942                    epsilon: 1e-3,
1943                    max_terms: 1000
1944                },
1945                &circuit,
1946                10,
1947                42,
1948            )
1949            .unwrap_err(),
1950            crate::error::PrismError::IncompatibleBackend { .. }
1951        ));
1952    }
1953
1954    // ── Noisy simulation error paths ────────────────────────────────────
1955
1956    #[test]
1957    fn test_noise_rejects_stabilizer_rank() {
1958        let circuit = make_general_circuit();
1959        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
1960        assert!(matches!(
1961            run_shots_with_noise(BackendKind::StabilizerRank, &circuit, &nm, 10, 42).unwrap_err(),
1962            crate::error::PrismError::IncompatibleBackend { .. }
1963        ));
1964    }
1965
1966    #[test]
1967    fn test_noise_rejects_pauli_backends() {
1968        let circuit = make_general_circuit();
1969        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
1970
1971        assert!(matches!(
1972            run_shots_with_noise(
1973                BackendKind::StochasticPauli { num_samples: 100 },
1974                &circuit,
1975                &nm,
1976                10,
1977                42,
1978            )
1979            .unwrap_err(),
1980            crate::error::PrismError::IncompatibleBackend { .. }
1981        ));
1982        assert!(matches!(
1983            run_shots_with_noise(
1984                BackendKind::DeterministicPauli {
1985                    epsilon: 1e-3,
1986                    max_terms: 1000
1987                },
1988                &circuit,
1989                &nm,
1990                10,
1991                42,
1992            )
1993            .unwrap_err(),
1994            crate::error::PrismError::IncompatibleBackend { .. }
1995        ));
1996    }
1997
1998    #[test]
1999    fn test_noise_stabilizer_rejects_non_pauli_noise() {
2000        let circuit = make_clifford_circuit();
2001        let nm = noise::NoiseModel {
2002            after_gate: {
2003                let mut ag = vec![Vec::new(); circuit.instructions.len()];
2004                ag[0].push(noise::NoiseEvent {
2005                    channel: noise::NoiseChannel::AmplitudeDamping { gamma: 0.1 },
2006                    qubits: smallvec![0],
2007                });
2008                ag
2009            },
2010            readout: vec![None; circuit.num_qubits],
2011        };
2012        let err = run_shots_with_noise(BackendKind::Stabilizer, &circuit, &nm, 10, 42).unwrap_err();
2013        match err {
2014            crate::error::PrismError::IncompatibleBackend { reason, .. } => {
2015                assert!(reason.contains("Statevector"));
2016                assert!(reason.contains("Sparse"));
2017                assert!(reason.contains("Factored"));
2018            }
2019            other => panic!("expected IncompatibleBackend, got {other:?}"),
2020        }
2021    }
2022
2023    #[test]
2024    fn test_noise_auto_general_noise_avoids_stabilizer_dispatch() {
2025        let circuit = make_clifford_circuit();
2026        let nm = noise::NoiseModel::with_amplitude_damping(&circuit, 0.1);
2027        let result = run_shots_with_noise(BackendKind::Auto, &circuit, &nm, 16, 42);
2028        assert!(result.is_ok());
2029    }
2030
2031    #[cfg(feature = "gpu")]
2032    #[test]
2033    fn test_noise_stabilizer_gpu_rejects_non_pauli_noise() {
2034        let circuit = make_clifford_circuit();
2035        let nm = noise::NoiseModel {
2036            after_gate: {
2037                let mut ag = vec![Vec::new(); circuit.instructions.len()];
2038                ag[0].push(noise::NoiseEvent {
2039                    channel: noise::NoiseChannel::AmplitudeDamping { gamma: 0.1 },
2040                    qubits: smallvec![0],
2041                });
2042                ag
2043            },
2044            readout: vec![None; circuit.num_qubits],
2045        };
2046        assert!(matches!(
2047            run_shots_with_noise(
2048                BackendKind::StabilizerGpu {
2049                    context: crate::gpu::GpuContext::stub_for_tests(),
2050                },
2051                &circuit,
2052                &nm,
2053                10,
2054                42,
2055            )
2056            .unwrap_err(),
2057            crate::error::PrismError::IncompatibleBackend { .. }
2058        ));
2059    }
2060
2061    #[test]
2062    fn test_noise_stabilizer_rejects_non_clifford() {
2063        let circuit = make_general_circuit();
2064        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2065        assert!(matches!(
2066            run_shots_with_noise(BackendKind::Stabilizer, &circuit, &nm, 10, 42).unwrap_err(),
2067            crate::error::PrismError::IncompatibleBackend { .. }
2068        ));
2069    }
2070
2071    #[cfg(feature = "gpu")]
2072    #[test]
2073    fn test_noise_stabilizer_gpu_rejects_non_clifford() {
2074        let circuit = make_general_circuit();
2075        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2076        assert!(matches!(
2077            run_shots_with_noise(
2078                BackendKind::StabilizerGpu {
2079                    context: crate::gpu::GpuContext::stub_for_tests(),
2080                },
2081                &circuit,
2082                &nm,
2083                10,
2084                42,
2085            )
2086            .unwrap_err(),
2087            crate::error::PrismError::IncompatibleBackend { .. }
2088        ));
2089    }
2090
2091    // ── Backend smoke tests ─────────────────────────────────────────────
2092
2093    fn assert_probs_match(kind: BackendKind, circuit: &Circuit, expected: &[f64], tol: f64) {
2094        let label = format!("{kind:?}");
2095        let result = run_with(kind, circuit, 42).unwrap();
2096        let probs = result.probabilities.unwrap().to_vec();
2097        assert_eq!(probs.len(), expected.len(), "{label}: length mismatch");
2098        for (i, (a, b)) in probs.iter().zip(expected.iter()).enumerate() {
2099            assert!(
2100                (a - b).abs() < tol,
2101                "{label}: prob[{i}] = {a}, expected {b}"
2102            );
2103        }
2104    }
2105
2106    #[test]
2107    fn test_smoke_all_backends_clifford() {
2108        let circuit = make_clifford_circuit();
2109        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2110            .unwrap()
2111            .probabilities
2112            .unwrap()
2113            .to_vec();
2114
2115        for kind in [
2116            BackendKind::Stabilizer,
2117            BackendKind::FactoredStabilizer,
2118            BackendKind::Sparse,
2119            BackendKind::Mps { max_bond_dim: 64 },
2120            BackendKind::TensorNetwork,
2121            BackendKind::Factored,
2122        ] {
2123            assert_probs_match(kind, &circuit, &sv_probs, 1e-8);
2124        }
2125    }
2126
2127    #[test]
2128    fn test_smoke_all_backends_general() {
2129        let circuit = make_general_circuit();
2130        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2131            .unwrap()
2132            .probabilities
2133            .unwrap()
2134            .to_vec();
2135
2136        for kind in [
2137            BackendKind::Sparse,
2138            BackendKind::Mps { max_bond_dim: 64 },
2139            BackendKind::TensorNetwork,
2140            BackendKind::Factored,
2141        ] {
2142            assert_probs_match(kind, &circuit, &sv_probs, 1e-8);
2143        }
2144    }
2145
2146    #[test]
2147    fn test_smoke_product_state() {
2148        let circuit = make_product_circuit();
2149        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2150            .unwrap()
2151            .probabilities
2152            .unwrap()
2153            .to_vec();
2154        assert_probs_match(BackendKind::ProductState, &circuit, &sv_probs, 1e-8);
2155    }
2156
2157    #[test]
2158    fn test_smoke_stabilizer_rank() {
2159        let mut circuit = Circuit::new(3, 0);
2160        circuit.add_gate(Gate::H, &[0]);
2161        circuit.add_gate(Gate::T, &[0]);
2162        circuit.add_gate(Gate::Cx, &[0, 1]);
2163        circuit.add_gate(Gate::H, &[2]);
2164        circuit.add_gate(Gate::T, &[2]);
2165
2166        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2167            .unwrap()
2168            .probabilities
2169            .unwrap()
2170            .to_vec();
2171        assert_probs_match(BackendKind::StabilizerRank, &circuit, &sv_probs, 1e-6);
2172    }
2173}