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