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;
12pub(crate) mod shots;
13pub mod stabilizer_rank;
14mod terminal_sampling;
15mod trajectory;
16pub mod unified_pauli;
17
18pub(crate) use decomposed::merge_probabilities;
19use decomposed::{
20    run_decomposed, run_decomposed_prefused, should_decompose, MIN_DECOMPOSITION_QUBITS,
21};
22pub use dispatch::BackendKind;
23use dispatch::{
24    auto_selects_cpu_statevector, has_temporal_clifford_opportunity, select_backend,
25    select_dispatch, stabilizer_rank_budget, supports_fused_for_kind, try_temporal_clifford,
26    validate_explicit_backend, DispatchAction, AUTO_APPROX_MAX_TERMS, AUTO_MPS_BOND_DIM,
27    AUTO_SPD_MAX_TERMS, MAX_AUTO_T_COUNT_APPROX, MAX_AUTO_T_COUNT_EXACT, MAX_AUTO_T_COUNT_SHOTS,
28    MAX_STABILIZER_RANK_QUBITS, MIN_BLOCK_FOR_FACTORED_STAB, MIN_FACTORED_STABILIZER_QUBITS,
29    MIN_QUBITS_FOR_SPD_AUTO,
30};
31pub use probability::{FactoredBlock, Probabilities, ProbabilitiesIter};
32pub use shots::{bitstring, ShotsResult};
33
34use std::collections::HashMap;
35
36use crate::backend::statevector::StatevectorBackend;
37use crate::backend::{max_statevector_qubits, Backend};
38use crate::circuit::{Circuit, Instruction};
39use crate::error::{PrismError, Result};
40use shots::{packed_shots_to_classical_bits, sample_shots};
41use terminal_sampling::{sample_counts_from_state, sample_shots_from_state};
42
43type TerminalStatevector = (StatevectorBackend, Vec<(usize, usize)>);
44
45#[derive(Debug, Clone, Copy)]
46pub(crate) struct SimOptions {
47    pub(crate) probabilities: bool,
48}
49
50impl Default for SimOptions {
51    fn default() -> Self {
52        Self {
53            probabilities: true,
54        }
55    }
56}
57
58impl SimOptions {
59    pub(crate) fn classical_only() -> Self {
60        Self {
61            probabilities: false,
62        }
63    }
64}
65
66/// Result of a generic simulation run.
67#[derive(Debug, Clone)]
68pub struct RunOutcome {
69    /// Classical measurement outcomes, indexed by classical bit number.
70    /// `true` = measured |1⟩.
71    pub classical_bits: Vec<bool>,
72    /// Probability of each computational basis state (length 2^n).
73    ///
74    /// `None` means the selected backend cannot expose a dense probability
75    /// distribution for this circuit. Other probability extraction failures
76    /// are returned as errors by the query that produced this result.
77    pub probabilities: Option<Probabilities>,
78}
79
80/// Frequency histogram returned by query-aware count sampling.
81#[derive(Debug, Clone)]
82pub struct CountsResult {
83    pub counts: HashMap<Vec<u64>, u64>,
84    pub num_classical_bits: usize,
85}
86
87impl CountsResult {
88    pub fn into_counts(self) -> HashMap<Vec<u64>, u64> {
89        self.counts
90    }
91}
92
93/// Per-qubit marginal probabilities returned by query-aware marginal sampling.
94#[derive(Debug, Clone)]
95pub struct MarginalsResult {
96    pub marginals: Vec<(f64, f64)>,
97}
98
99impl MarginalsResult {
100    pub fn into_vec(self) -> Vec<(f64, f64)> {
101        self.marginals
102    }
103}
104
105#[derive(Debug, Clone, Copy)]
106pub struct Unseeded;
107
108#[derive(Debug, Clone, Copy)]
109pub struct Seeded {
110    seed: u64,
111}
112
113/// Builder for query-aware simulation requests.
114pub struct Simulate<'c, SeedState> {
115    circuit: &'c Circuit,
116    kind: BackendKind,
117    seed: SeedState,
118    noise_model: Option<&'c noise::NoiseModel>,
119}
120
121impl<'c, SeedState> Simulate<'c, SeedState> {
122    #[inline]
123    pub fn backend(mut self, kind: BackendKind) -> Self {
124        self.kind = kind;
125        self
126    }
127
128    #[inline]
129    pub fn noise(mut self, model: &'c noise::NoiseModel) -> Self {
130        self.noise_model = Some(model);
131        self
132    }
133
134    #[cfg(feature = "gpu")]
135    #[inline]
136    pub fn gpu(self, context: std::sync::Arc<crate::gpu::GpuContext>) -> Self {
137        self.backend(BackendKind::StatevectorGpu { context })
138    }
139
140    /// Distribute the exact state vector across the ranks of `context`.
141    ///
142    /// With a single rank this behaves like [`Simulate::backend`] with
143    /// [`BackendKind::Statevector`].
144    #[cfg(feature = "distributed")]
145    pub fn distributed(
146        self,
147        context: std::sync::Arc<crate::distributed::DistributedContext>,
148    ) -> Self {
149        self.backend(BackendKind::StatevectorDistributed { context })
150    }
151}
152
153impl<'c> Simulate<'c, Unseeded> {
154    #[inline]
155    pub fn seed(self, seed: u64) -> Simulate<'c, Seeded> {
156        Simulate {
157            circuit: self.circuit,
158            kind: self.kind,
159            seed: Seeded { seed },
160            noise_model: self.noise_model,
161        }
162    }
163}
164
165impl<'c> Simulate<'c, Seeded> {
166    #[inline]
167    fn seed_value(&self) -> u64 {
168        self.seed.seed
169    }
170
171    #[inline]
172    pub fn run(self) -> Result<RunOutcome> {
173        let seed = self.seed_value();
174        if self.noise_model.is_some() {
175            return Err(crate::error::PrismError::BackendUnsupported {
176                backend: format!("{:?}", self.kind),
177                operation: "single-run noisy simulation through `run`".into(),
178            });
179        }
180        run_with_internal(self.kind, self.circuit, seed, SimOptions::default())
181    }
182
183    #[inline]
184    pub fn shots(self, num_shots: usize) -> Result<ShotsResult> {
185        let seed = self.seed_value();
186        if let Some(noise_model) = self.noise_model {
187            run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)
188        } else {
189            run_shots_with(self.kind, self.circuit, num_shots, seed)
190        }
191    }
192
193    #[inline]
194    pub fn sample_counts(self, num_shots: usize) -> Result<CountsResult> {
195        let seed = self.seed_value();
196        let counts = if let Some(noise_model) = self.noise_model {
197            run_shots_with_noise(self.kind, self.circuit, noise_model, num_shots, seed)?.counts()
198        } else {
199            run_counts_with(self.kind, self.circuit, num_shots, seed)?
200        };
201        Ok(CountsResult {
202            counts,
203            num_classical_bits: self.circuit.num_classical_bits,
204        })
205    }
206
207    #[inline]
208    pub fn marginals(self) -> Result<MarginalsResult> {
209        let seed = self.seed_value();
210        if self.noise_model.is_some() {
211            return Err(crate::error::PrismError::BackendUnsupported {
212                backend: format!("{:?}", self.kind),
213                operation: "marginals with inline noise model".into(),
214            });
215        }
216        run_marginals_result_with(self.kind, self.circuit, seed)
217    }
218}
219
220#[inline]
221pub fn simulate(circuit: &Circuit) -> Simulate<'_, Unseeded> {
222    Simulate {
223        circuit,
224        kind: BackendKind::Auto,
225        seed: Unseeded,
226        noise_model: None,
227    }
228}
229
230#[inline]
231fn probs_only_result(probs: Vec<f64>) -> RunOutcome {
232    RunOutcome {
233        probabilities: Some(Probabilities::Dense(probs)),
234        classical_bits: vec![],
235    }
236}
237
238fn try_backend_probabilities(backend: &dyn Backend) -> Result<Option<Probabilities>> {
239    match backend.probabilities() {
240        Ok(probs) => Ok(Some(Probabilities::Dense(probs))),
241        Err(PrismError::BackendUnsupported { .. }) => Ok(None),
242        Err(err) => Err(err),
243    }
244}
245
246/// Core execution: fuse, init, apply, extract.
247fn execute(backend: &mut dyn Backend, circuit: &Circuit, opts: &SimOptions) -> Result<RunOutcome> {
248    let expanded: std::borrow::Cow<'_, Circuit> = if backend.supports_qft_block() {
249        std::borrow::Cow::Borrowed(circuit)
250    } else {
251        crate::circuit::expand_qft_blocks(circuit)
252    };
253    let fused = crate::circuit::fusion::fuse_circuit(&expanded, backend.supports_fused_gates());
254    execute_circuit(backend, &fused, opts)
255}
256
257/// Shared init → apply → extract logic.
258fn execute_circuit(
259    backend: &mut dyn Backend,
260    circuit: &Circuit,
261    opts: &SimOptions,
262) -> Result<RunOutcome> {
263    backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
264    backend.apply_instructions(&circuit.instructions)?;
265
266    let probabilities = if opts.probabilities {
267        try_backend_probabilities(backend)?
268    } else {
269        None
270    };
271
272    Ok(RunOutcome {
273        classical_bits: backend.classical_results().to_vec(),
274        probabilities,
275    })
276}
277
278/// Execute a circuit with automatic backend selection.
279///
280/// The simplest entry point. Uses [`BackendKind::Auto`] to select the
281/// optimal backend based on circuit properties, then runs the circuit.
282#[cfg(test)]
283fn run(circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
284    run_with(BackendKind::Auto, circuit, seed)
285}
286
287/// Execute a circuit with explicit backend selection.
288///
289/// Constructs the backend internally based on [`BackendKind`], then runs
290/// the circuit. For a pre-constructed backend instance, use [`run_on`].
291pub(crate) fn run_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
292    run_with_internal(kind, circuit, seed, SimOptions::default())
293}
294
295fn run_with_internal(
296    kind: BackendKind,
297    circuit: &Circuit,
298    seed: u64,
299    opts: SimOptions,
300) -> Result<RunOutcome> {
301    if !matches!(kind, BackendKind::Auto) {
302        validate_explicit_backend(&kind, circuit)?;
303    }
304    // The distributed backend runs the whole circuit across ranks in lockstep.
305    // Subsystem decomposition, Clifford+T, and temporal-Clifford shortcuts all
306    // reshape execution per sub-block, which would desynchronize the collective
307    // calls every rank must issue in the same order. Dispatch directly.
308    #[cfg(feature = "distributed")]
309    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
310        return match select_dispatch(&kind, circuit, seed, false) {
311            DispatchAction::Backend(mut backend) => execute(&mut *backend, circuit, &opts),
312            _ => unreachable!("StatevectorDistributed always dispatches to a backend"),
313        };
314    }
315    let mut has_partial_independence = false;
316    if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
317        let components = circuit.independent_subsystems();
318        if components.len() > 1 {
319            if should_decompose(&components, circuit.num_qubits) {
320                let max_block = components.iter().map(|c| c.len()).max().unwrap_or(0);
321                if matches!(kind, BackendKind::Auto)
322                    && circuit.is_clifford_only()
323                    && circuit.num_qubits >= MIN_FACTORED_STABILIZER_QUBITS
324                    && max_block >= MIN_BLOCK_FOR_FACTORED_STAB
325                {
326                    let mut backend =
327                        crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed);
328                    let fs_opts = if circuit.num_qubits > 64 {
329                        SimOptions {
330                            probabilities: false,
331                        }
332                    } else {
333                        opts
334                    };
335                    return execute(&mut backend, circuit, &fs_opts);
336                }
337                return run_decomposed(&kind, &components, circuit, seed, &opts);
338            }
339            has_partial_independence = true;
340        }
341    }
342    if matches!(kind, BackendKind::Auto)
343        && circuit.is_clifford_plus_t()
344        && circuit.has_t_gates()
345        && !has_nonunitary_or_classical_ops(circuit)
346        && circuit.num_qubits <= MAX_STABILIZER_RANK_QUBITS
347    {
348        let t = circuit.t_count();
349        let sr_budget = stabilizer_rank_budget(circuit.num_qubits);
350        if t <= MAX_AUTO_T_COUNT_EXACT && t <= sr_budget {
351            let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
352            return Ok(probs_only_result(sr.probabilities));
353        }
354        if t <= MAX_AUTO_T_COUNT_APPROX && t <= sr_budget {
355            let sr =
356                stabilizer_rank::run_stabilizer_rank_approx(circuit, AUTO_APPROX_MAX_TERMS, seed)?;
357            return Ok(probs_only_result(sr.probabilities));
358        }
359    }
360    if let Some(result) = try_temporal_clifford(&kind, circuit, seed) {
361        return result;
362    }
363    match select_dispatch(&kind, circuit, seed, has_partial_independence) {
364        DispatchAction::Backend(mut backend) => execute(&mut *backend, circuit, &opts),
365        DispatchAction::StabilizerRank => {
366            let sr = stabilizer_rank::run_stabilizer_rank(circuit, seed)?;
367            Ok(probs_only_result(sr.probabilities))
368        }
369        DispatchAction::StochasticPauli { num_samples } => {
370            Err(crate::error::PrismError::IncompatibleBackend {
371                backend: format!(
372                    "{:?}",
373                    BackendKind::StochasticPauli { num_samples }
374                ),
375                reason: "StochasticPauli produces marginal estimates only; use `simulate(...).marginals()`".into(),
376            })
377        }
378        DispatchAction::DeterministicPauli { epsilon, max_terms } => {
379            Err(crate::error::PrismError::IncompatibleBackend {
380                backend: format!(
381                    "{:?}",
382                    BackendKind::DeterministicPauli { epsilon, max_terms }
383                ),
384                reason: "DeterministicPauli produces marginals only; use `simulate(...).marginals()`".into(),
385            })
386        }
387    }
388}
389
390/// Execute a circuit on a pre-constructed backend.
391///
392/// Use this when you need direct control over the backend instance
393/// (e.g., testing a specific backend). For automatic dispatch, use [`simulate`].
394pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<RunOutcome> {
395    execute(backend, circuit, &SimOptions::default())
396}
397
398/// Parse an OpenQASM string and execute with automatic backend selection.
399pub fn run_qasm(qasm: &str, seed: u64) -> Result<RunOutcome> {
400    let circuit = crate::circuit::openqasm::parse(qasm)?;
401    simulate(&circuit).seed(seed).run()
402}
403
404/// Execute a circuit multiple times, collecting measurement outcomes.
405#[cfg(test)]
406fn run_shots(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<ShotsResult> {
407    run_shots_with(BackendKind::Auto, circuit, num_shots, seed)
408}
409
410pub(crate) fn supports_compiled_measurement_sampling(circuit: &Circuit) -> bool {
411    circuit.is_clifford_only()
412        && !circuit.has_resets()
413        && circuit.has_terminal_measurements_only()
414        && circuit
415            .instructions
416            .iter()
417            .any(|inst| matches!(inst, Instruction::Measure { .. }))
418}
419
420fn supports_deferred_measurement_sampling(circuit: &Circuit) -> bool {
421    circuit.is_clifford_only()
422        && (circuit.has_resets() || !circuit.has_terminal_measurements_only())
423        && circuit
424            .instructions
425            .iter()
426            .any(|inst| matches!(inst, Instruction::Measure { .. }))
427        && !circuit
428            .instructions
429            .iter()
430            .any(|inst| matches!(inst, Instruction::Conditional { .. }))
431}
432
433fn is_clifford_sampler_kind(kind: &BackendKind) -> bool {
434    match kind {
435        BackendKind::Auto | BackendKind::Stabilizer | BackendKind::FactoredStabilizer => true,
436        #[cfg(feature = "gpu")]
437        BackendKind::StabilizerGpu { .. } => true,
438        _ => false,
439    }
440}
441
442fn should_use_compiled_clifford_sampling(
443    kind: &BackendKind,
444    circuit: &Circuit,
445    num_shots: usize,
446) -> bool {
447    num_shots >= 2
448        && supports_compiled_measurement_sampling(circuit)
449        && is_clifford_sampler_kind(kind)
450}
451
452fn should_use_deferred_clifford_sampling(
453    kind: &BackendKind,
454    circuit: &Circuit,
455    num_shots: usize,
456) -> bool {
457    num_shots >= 2
458        && supports_deferred_measurement_sampling(circuit)
459        && is_clifford_sampler_kind(kind)
460}
461
462fn compile_measurements_for_kind(
463    kind: &BackendKind,
464    circuit: &Circuit,
465    seed: u64,
466) -> Result<compiled::CompiledSampler> {
467    #[cfg(not(feature = "gpu"))]
468    let _ = kind;
469
470    let sampler = compiled::compile_measurements(circuit, seed)?;
471
472    #[cfg(feature = "gpu")]
473    if let BackendKind::StabilizerGpu { context } = kind {
474        return Ok(sampler.with_gpu(context.clone()));
475    }
476
477    Ok(sampler)
478}
479
480fn auto_terminal_statevector_candidate(circuit: &Circuit) -> bool {
481    let mut has_partial_independence = false;
482    if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
483        let components = circuit.independent_subsystems();
484        if components.len() > 1 {
485            if should_decompose(&components, circuit.num_qubits) {
486                return false;
487            }
488            has_partial_independence = true;
489        }
490    }
491
492    if !auto_selects_cpu_statevector(circuit, has_partial_independence) {
493        return false;
494    }
495
496    if circuit.is_clifford_plus_t()
497        && circuit.has_t_gates()
498        && circuit.num_qubits <= MAX_STABILIZER_RANK_QUBITS
499    {
500        let t = circuit.t_count();
501        let sr_budget = stabilizer_rank_budget(circuit.num_qubits);
502        if t <= MAX_AUTO_T_COUNT_APPROX && t <= sr_budget {
503            return false;
504        }
505    }
506
507    !has_temporal_clifford_opportunity(&BackendKind::Auto, circuit)
508}
509
510fn terminal_statevector_candidate(kind: &BackendKind, circuit: &Circuit) -> bool {
511    match kind {
512        BackendKind::Statevector => true,
513        BackendKind::Auto => auto_terminal_statevector_candidate(circuit),
514        _ => false,
515    }
516}
517
518fn try_terminal_statevector_backend(
519    kind: &BackendKind,
520    circuit: &Circuit,
521    seed: u64,
522) -> Result<Option<TerminalStatevector>> {
523    if !circuit.has_terminal_measurements_only() {
524        return Ok(None);
525    }
526
527    let meas_map = circuit.measurement_map();
528    if meas_map.is_empty() {
529        return Ok(None);
530    }
531
532    let stripped = circuit.without_measurements();
533    if !terminal_statevector_candidate(kind, &stripped) {
534        return Ok(None);
535    }
536
537    let mut backend = StatevectorBackend::new(seed);
538    let expanded: std::borrow::Cow<'_, Circuit> = if backend.supports_qft_block() {
539        std::borrow::Cow::Borrowed(&stripped)
540    } else {
541        crate::circuit::expand_qft_blocks(&stripped)
542    };
543    let fused = crate::circuit::fusion::fuse_circuit(&expanded, backend.supports_fused_gates());
544    backend.init(fused.num_qubits, fused.num_classical_bits)?;
545    backend.apply_instructions(&fused.instructions)?;
546
547    Ok(Some((backend, meas_map)))
548}
549
550/// Execute a circuit multiple times with automatic backend selection and return counts.
551///
552/// Use this when only a frequency histogram is needed. Optimized paths can
553/// avoid materializing per-shot bit vectors.
554#[cfg(test)]
555fn run_counts(circuit: &Circuit, num_shots: usize, seed: u64) -> Result<HashMap<Vec<u64>, u64>> {
556    run_counts_with(BackendKind::Auto, circuit, num_shots, seed)
557}
558
559/// Execute a circuit multiple times with explicit backend selection and return counts.
560///
561/// For Clifford circuits with terminal measurements and no resets, Auto,
562/// Stabilizer, FactoredStabilizer, and explicit `StabilizerGpu` route through
563/// the compiled sampler's optimized counting path. Explicit `StabilizerGpu`
564/// carries its GPU context into the compiled sampler so large shot runs avoid
565/// the raw tableau measurement round-trips. Other circuits fall back to
566/// per-shot simulation with counting.
567///
568/// Optimized terminal statevector paths sample counts directly from the output
569/// distribution. The distribution is equivalent to materializing shots first,
570/// but finite seeded counts may differ from `run_shots_with(...).counts()`.
571pub(crate) fn run_counts_with(
572    kind: BackendKind,
573    circuit: &Circuit,
574    num_shots: usize,
575    seed: u64,
576) -> Result<HashMap<Vec<u64>, u64>> {
577    if should_use_compiled_clifford_sampling(&kind, circuit, num_shots) {
578        let mut sampler = compile_measurements_for_kind(&kind, circuit, seed)?;
579        return sampler.try_sample_counts(num_shots);
580    }
581
582    if let Some((backend, meas_map)) = try_terminal_statevector_backend(&kind, circuit, seed)? {
583        return Ok(sample_counts_from_state(
584            backend.state_vector(),
585            backend.probability_scale(),
586            &meas_map,
587            circuit.num_classical_bits,
588            num_shots,
589            seed,
590        ));
591    }
592
593    let result = run_shots_with(kind, circuit, num_shots, seed)?;
594    Ok(result.counts())
595}
596
597/// Compute per-qubit marginal probabilities: P(q_i = 0) and P(q_i = 1) for each qubit.
598///
599/// Returns a `Vec<(f64, f64)>` of length `num_qubits`, where each element is `(p0, p1)`.
600///
601/// For Clifford+T circuits, uses Sparse Pauli Dynamics (SPD), Heisenberg-picture
602/// backward propagation that scales with Pauli complexity, not 2^n. This is orders
603/// of magnitude faster than statevector for structured Clifford+T circuits (25-400x
604/// at 14-22 qubits).
605///
606/// For pure Clifford circuits, exact marginals still come from backend
607/// probabilities. Sampled parity-matrix marginals remain available through
608/// `compile_measurements(...).sample_marginals(...)`.
609///
610/// For other circuits, falls back to statevector probabilities and extracts
611/// marginals.
612#[cfg(test)]
613fn run_marginals(circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
614    run_marginals_result_with(BackendKind::Auto, circuit, seed).map(MarginalsResult::into_vec)
615}
616
617/// Compute per-qubit marginal probabilities with explicit backend selection.
618#[cfg(test)]
619fn run_marginals_with(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<Vec<(f64, f64)>> {
620    run_marginals_result_with(kind, circuit, seed).map(MarginalsResult::into_vec)
621}
622
623fn expectations_to_marginals(expectations: &[f64]) -> Vec<(f64, f64)> {
624    expectations
625        .iter()
626        .map(|ez| {
627            let p0 = ((1.0 + ez) / 2.0).clamp(0.0, 1.0);
628            (p0, 1.0 - p0)
629        })
630        .collect()
631}
632
633fn has_nonunitary_or_classical_ops(circuit: &Circuit) -> bool {
634    circuit.instructions.iter().any(|inst| {
635        matches!(
636            inst,
637            Instruction::Measure { .. }
638                | Instruction::Reset { .. }
639                | Instruction::Conditional { .. }
640        )
641    })
642}
643
644fn supports_pauli_marginal_backend(circuit: &Circuit) -> bool {
645    circuit.is_clifford_plus_t() && !has_nonunitary_or_classical_ops(circuit)
646}
647
648fn validate_pauli_marginal_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
649    if !circuit.is_clifford_plus_t() {
650        return Err(PrismError::IncompatibleBackend {
651            backend: format!("{kind:?}"),
652            reason: "Pauli marginal backends require Clifford+T gates".into(),
653        });
654    }
655    if has_nonunitary_or_classical_ops(circuit) {
656        return Err(PrismError::IncompatibleBackend {
657            backend: format!("{kind:?}"),
658            reason: "Pauli marginal backends require a unitary circuit without measurements, resets, or conditionals".into(),
659        });
660    }
661    Ok(())
662}
663
664fn run_marginals_result_with(
665    kind: BackendKind,
666    circuit: &Circuit,
667    seed: u64,
668) -> Result<MarginalsResult> {
669    let n = circuit.num_qubits;
670
671    match &kind {
672        BackendKind::StochasticPauli { num_samples } => {
673            validate_pauli_marginal_backend(&kind, circuit)?;
674            let spp = unified_pauli::run_spp(circuit, *num_samples, seed)?;
675            return Ok(MarginalsResult {
676                marginals: expectations_to_marginals(&spp.expectations),
677            });
678        }
679        BackendKind::DeterministicPauli { epsilon, max_terms } => {
680            validate_pauli_marginal_backend(&kind, circuit)?;
681            let spd = unified_pauli::run_spd(circuit, *epsilon, *max_terms)?;
682            return Ok(MarginalsResult {
683                marginals: expectations_to_marginals(&spd.expectations),
684            });
685        }
686        _ => {}
687    }
688
689    if matches!(kind, BackendKind::Auto)
690        && supports_pauli_marginal_backend(circuit)
691        && circuit.has_t_gates()
692        && n >= MIN_QUBITS_FOR_SPD_AUTO
693    {
694        let spd = unified_pauli::run_spd(circuit, 0.0, AUTO_SPD_MAX_TERMS)?;
695        return Ok(MarginalsResult {
696            marginals: expectations_to_marginals(&spd.expectations),
697        });
698    }
699
700    let result = run_with(kind, circuit, seed)?;
701    if let Some(probs) = &result.probabilities {
702        Ok(MarginalsResult {
703            marginals: probs.marginals(),
704        })
705    } else {
706        Err(PrismError::BackendUnsupported {
707            backend: "simulate".into(),
708            operation: format!(
709                "marginals for {} qubits without backend probability output",
710                circuit.num_qubits
711            ),
712        })
713    }
714}
715
716/// Multi-shot execution for the distributed statevector backend.
717///
718/// Every rank runs this function in lockstep. Circuits with only terminal
719/// measurements run once and sample basis indices without gathering the dense
720/// state on any rank. Circuits with mid-circuit measurements run once per shot,
721/// prefused, with per-shot seeds matching the generic slow path.
722#[cfg(feature = "distributed")]
723fn run_shots_distributed(
724    context: std::sync::Arc<crate::distributed::DistributedContext>,
725    circuit: &Circuit,
726    num_shots: usize,
727    seed: u64,
728) -> Result<ShotsResult> {
729    use crate::backend::distributed_statevector::DistributedStatevectorBackend;
730
731    let meas_map = circuit.measurement_map();
732    if meas_map.is_empty() {
733        // No measurements means every shot is all false, but init must still
734        // run so invalid rank counts and local qubit floor violations surface as
735        // errors instead of fabricated output.
736        let mut backend = DistributedStatevectorBackend::new(context, seed);
737        backend.init(circuit.num_qubits, circuit.num_classical_bits)?;
738        return Ok(ShotsResult {
739            shots: vec![vec![false; circuit.num_classical_bits]; num_shots],
740            num_classical_bits: circuit.num_classical_bits,
741        });
742    }
743
744    if circuit.has_terminal_measurements_only() {
745        let stripped = circuit.without_measurements();
746        let mut backend = DistributedStatevectorBackend::new(context, seed);
747        execute(&mut backend, &stripped, &SimOptions::classical_only())?;
748        let indices = backend.sample_state_indices(num_shots, seed)?;
749        let shots = indices
750            .iter()
751            .map(|&idx| {
752                let mut shot = vec![false; circuit.num_classical_bits];
753                for &(qubit, cbit) in &meas_map {
754                    shot[cbit] = (idx >> qubit) & 1 == 1;
755                }
756                shot
757            })
758            .collect();
759        return Ok(ShotsResult {
760            shots,
761            num_classical_bits: circuit.num_classical_bits,
762        });
763    }
764
765    let probe = DistributedStatevectorBackend::new(context.clone(), seed);
766    let expanded: std::borrow::Cow<'_, Circuit> = if probe.supports_qft_block() {
767        std::borrow::Cow::Borrowed(circuit)
768    } else {
769        crate::circuit::expand_qft_blocks(circuit)
770    };
771    let fused = crate::circuit::fusion::fuse_circuit(&expanded, probe.supports_fused_gates());
772    let opts = SimOptions::classical_only();
773    let mut shots = Vec::with_capacity(num_shots);
774    for i in 0..num_shots {
775        let shot_seed = seed.wrapping_add(i as u64);
776        let mut backend = DistributedStatevectorBackend::new(context.clone(), shot_seed);
777        let result = execute_circuit(&mut backend, &fused, &opts)?;
778        shots.push(result.classical_bits);
779    }
780    Ok(ShotsResult {
781        shots,
782        num_classical_bits: circuit.num_classical_bits,
783    })
784}
785
786/// Execute a circuit multiple times with explicit backend selection.
787pub(crate) fn run_shots_with(
788    kind: BackendKind,
789    circuit: &Circuit,
790    num_shots: usize,
791    seed: u64,
792) -> Result<ShotsResult> {
793    // The distributed backend runs every rank in lockstep, so shot execution
794    // must not route through shortcuts that reshape the collective call
795    // sequence. Dispatch directly.
796    #[cfg(feature = "distributed")]
797    if let BackendKind::StatevectorDistributed { context } = &kind {
798        return run_shots_distributed(context.clone(), circuit, num_shots, seed);
799    }
800
801    // Compiled sampler: O(n²·m) compile + O(r·m/64) per shot with LUT.
802    // Always polynomial, avoids the O(2^k) probability computation path.
803    // Explicit `StabilizerGpu` attaches its CUDA context here so repeated shot
804    // runs use the compiled GPU sampling path instead of the raw GPU tableau
805    // measurement loop, but only for circuits the compiled sampler models
806    // exactly.
807    if should_use_compiled_clifford_sampling(&kind, circuit, num_shots) {
808        let mut sampler = compile_measurements_for_kind(&kind, circuit, seed)?;
809        let packed = sampler.try_sample_bulk_packed(num_shots)?;
810        let meas_map = circuit.measurement_map();
811        return Ok(ShotsResult {
812            shots: packed_shots_to_classical_bits(&packed, &meas_map, circuit.num_classical_bits),
813            num_classical_bits: circuit.num_classical_bits,
814        });
815    }
816
817    if should_use_deferred_clifford_sampling(&kind, circuit, num_shots) {
818        if let Ok(deferred) = compiled::defer_measure_reset_circuit(circuit) {
819            let mut sampler = compile_measurements_for_kind(&kind, &deferred, seed)?;
820            let packed = sampler.try_sample_bulk_packed(num_shots)?;
821            let meas_map = deferred.measurement_map();
822            return Ok(ShotsResult {
823                shots: packed_shots_to_classical_bits(
824                    &packed,
825                    &meas_map,
826                    circuit.num_classical_bits,
827                ),
828                num_classical_bits: circuit.num_classical_bits,
829            });
830        }
831    }
832
833    if let Some((backend, meas_map)) = try_terminal_statevector_backend(&kind, circuit, seed)? {
834        let shots = sample_shots_from_state(
835            backend.state_vector(),
836            backend.probability_scale(),
837            &meas_map,
838            circuit.num_classical_bits,
839            num_shots,
840            seed,
841        );
842        return Ok(ShotsResult {
843            shots,
844            num_classical_bits: circuit.num_classical_bits,
845        });
846    }
847
848    if matches!(kind, BackendKind::StabilizerRank) && circuit.has_t_gates() {
849        return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
850    }
851    if matches!(kind, BackendKind::Auto)
852        && circuit.has_terminal_measurements_only()
853        && circuit.is_clifford_plus_t()
854        && circuit.has_t_gates()
855        && circuit.num_qubits > MAX_STABILIZER_RANK_QUBITS
856    {
857        let t = circuit.t_count();
858        let sr_budget = stabilizer_rank_budget(circuit.num_qubits);
859        if t <= MAX_AUTO_T_COUNT_SHOTS && t <= sr_budget {
860            return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
861        }
862    }
863
864    if circuit.has_terminal_measurements_only() {
865        let stripped = circuit.without_measurements();
866        let result = run_with_internal(kind.clone(), &stripped, seed, SimOptions::default())?;
867        if let Some(probs) = result.probabilities {
868            let meas_map = circuit.measurement_map();
869            let shots = sample_shots(
870                &probs,
871                &meas_map,
872                circuit.num_classical_bits,
873                num_shots,
874                seed,
875            );
876            return Ok(ShotsResult {
877                shots,
878                num_classical_bits: circuit.num_classical_bits,
879            });
880        }
881    }
882
883    // Slow path: mid-circuit measurements require per-shot simulation.
884    // Pre-compute seed-independent analysis to avoid redundant work.
885    if !matches!(kind, BackendKind::Auto) {
886        validate_explicit_backend(&kind, circuit)?;
887    }
888
889    let mut has_partial_independence = false;
890    let decompose = if circuit.num_qubits >= MIN_DECOMPOSITION_QUBITS {
891        let comps = circuit.independent_subsystems();
892        if comps.len() > 1 {
893            if should_decompose(&comps, circuit.num_qubits) {
894                Some(comps)
895            } else {
896                has_partial_independence = true;
897                None
898            }
899        } else {
900            None
901        }
902    } else {
903        None
904    };
905
906    if matches!(kind, BackendKind::StabilizerRank) {
907        return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
908    }
909    if matches!(
910        kind,
911        BackendKind::StochasticPauli { .. } | BackendKind::DeterministicPauli { .. }
912    ) {
913        return Err(crate::error::PrismError::IncompatibleBackend {
914            backend: format!("{kind:?}"),
915            reason: "Pauli propagation backends do not support mid-circuit measurements".into(),
916        });
917    }
918    if matches!(kind, BackendKind::Auto) && circuit.is_clifford_plus_t() && circuit.has_t_gates() {
919        let t = circuit.t_count();
920        let sr_budget = stabilizer_rank_budget(circuit.num_qubits);
921        if t <= MAX_AUTO_T_COUNT_SHOTS && t <= sr_budget {
922            return stabilizer_rank::run_stabilizer_rank_shots(circuit, num_shots, seed);
923        }
924    }
925
926    if has_temporal_clifford_opportunity(&kind, circuit) {
927        return run_shots_fallback(&kind, circuit, num_shots, seed);
928    }
929
930    let supports_fused = supports_fused_for_kind(&kind, circuit);
931    let mut shots = Vec::with_capacity(num_shots);
932    let opts = SimOptions::classical_only();
933
934    if let Some(ref comps) = decompose {
935        let partitions = circuit.partition_subcircuits(comps);
936        let fused_blocks: Vec<_> = partitions
937            .iter()
938            .map(|(sub, _, _)| {
939                crate::circuit::fusion::fuse_circuit(sub, supports_fused_for_kind(&kind, sub))
940            })
941            .collect();
942
943        for i in 0..num_shots {
944            let shot_seed = seed.wrapping_add(i as u64);
945            let result = run_decomposed_prefused(
946                &kind,
947                comps,
948                &partitions,
949                &fused_blocks,
950                shot_seed,
951                &opts,
952                circuit,
953            )?;
954            shots.push(result.classical_bits);
955        }
956    } else {
957        let fused = crate::circuit::fusion::fuse_circuit(circuit, supports_fused);
958
959        for i in 0..num_shots {
960            let shot_seed = seed.wrapping_add(i as u64);
961            let mut backend = select_backend(&kind, circuit, shot_seed, has_partial_independence);
962            let result = execute_circuit(&mut *backend, &fused, &opts)?;
963            shots.push(result.classical_bits);
964        }
965    }
966
967    Ok(ShotsResult {
968        shots,
969        num_classical_bits: circuit.num_classical_bits,
970    })
971}
972
973/// Execute a noisy circuit for multiple shots with explicit backend selection.
974///
975/// For Clifford circuits with Auto/Stabilizer/FactoredStabilizer backends,
976/// uses the compiled noisy sampler (fast O(n²·m) compile + O(events·m/64) per shot).
977/// For all other cases, falls back to per-shot simulation with noise injection.
978/// The compiled noisy path is limited to terminal measurements with no resets
979/// or classical conditionals.
980fn auto_general_noise_backend(circuit: &Circuit) -> BackendKind {
981    if !circuit.has_entangling_gates() {
982        BackendKind::ProductState
983    } else if circuit.num_qubits > max_statevector_qubits() {
984        if circuit.is_sparse_friendly() {
985            BackendKind::Sparse
986        } else {
987            BackendKind::Mps {
988                max_bond_dim: AUTO_MPS_BOND_DIM,
989            }
990        }
991    } else {
992        BackendKind::Statevector
993    }
994}
995
996pub(crate) fn run_shots_with_noise(
997    kind: BackendKind,
998    circuit: &Circuit,
999    noise_model: &noise::NoiseModel,
1000    num_shots: usize,
1001    seed: u64,
1002) -> Result<ShotsResult> {
1003    // Trajectory execution runs shots on Rayon worker threads, whose
1004    // scheduling order differs per rank. Per-shot distributed backends would
1005    // issue collectives out of lockstep and deadlock or corrupt exchanges.
1006    // Reject until a lockstep noisy path exists.
1007    #[cfg(feature = "distributed")]
1008    if matches!(kind, BackendKind::StatevectorDistributed { .. }) {
1009        return Err(crate::error::PrismError::IncompatibleBackend {
1010            backend: format!("{kind:?}"),
1011            reason: "noisy shot sampling is not supported on the distributed backend; \
1012                     trajectory execution cannot keep rank collectives in lockstep"
1013                .into(),
1014        });
1015    }
1016
1017    if !kind.supports_noisy_per_shot() {
1018        return Err(crate::error::PrismError::IncompatibleBackend {
1019            backend: format!("{kind:?}"),
1020            reason: "this backend does not support noisy per-shot simulation".into(),
1021        });
1022    }
1023
1024    let is_stabilizer_kind = kind.is_stabilizer_family();
1025
1026    if is_stabilizer_kind && !noise_model.is_pauli_only() {
1027        return Err(crate::error::PrismError::IncompatibleBackend {
1028            backend: format!("{kind:?}"),
1029            reason: format!(
1030                "stabilizer backends only support Pauli/depolarizing noise; use {} for amplitude damping, phase damping, thermal relaxation, custom Kraus, or readout errors",
1031                BackendKind::general_noise_backend_names()
1032            ),
1033        });
1034    }
1035
1036    if !noise_model.is_pauli_only() && !kind.supports_general_noise() {
1037        return Err(crate::error::PrismError::IncompatibleBackend {
1038            backend: format!("{kind:?}"),
1039            reason: format!(
1040                "non-Pauli noise requires {}",
1041                BackendKind::general_noise_backend_names()
1042            ),
1043        });
1044    }
1045
1046    if is_stabilizer_kind && !circuit.is_clifford_only() {
1047        return Err(crate::error::PrismError::IncompatibleBackend {
1048            backend: format!("{kind:?}"),
1049            reason: "circuit contains non-Clifford gates".into(),
1050        });
1051    }
1052
1053    if !matches!(kind, BackendKind::Auto) {
1054        validate_explicit_backend(&kind, circuit)?;
1055    }
1056
1057    if noise_model.is_pauli_only() {
1058        let use_compiled = matches!(
1059            kind,
1060            BackendKind::Auto | BackendKind::Stabilizer | BackendKind::FactoredStabilizer
1061        ) && supports_compiled_measurement_sampling(circuit)
1062            || {
1063                #[cfg(feature = "gpu")]
1064                {
1065                    matches!(kind, BackendKind::StabilizerGpu { .. })
1066                        && supports_compiled_measurement_sampling(circuit)
1067                }
1068                #[cfg(not(feature = "gpu"))]
1069                {
1070                    false
1071                }
1072            };
1073
1074        if use_compiled {
1075            #[cfg(feature = "gpu")]
1076            if let BackendKind::StabilizerGpu { context } = &kind {
1077                return noise::run_shots_noisy_with_gpu(
1078                    circuit,
1079                    noise_model,
1080                    num_shots,
1081                    seed,
1082                    context.clone(),
1083                );
1084            }
1085            return noise::run_shots_noisy(circuit, noise_model, num_shots, seed);
1086        }
1087    }
1088
1089    let trajectory_kind = if matches!(kind, BackendKind::Auto) && !noise_model.is_pauli_only() {
1090        auto_general_noise_backend(circuit)
1091    } else {
1092        kind
1093    };
1094
1095    trajectory::run_trajectories(
1096        |s| select_backend(&trajectory_kind, circuit, s, false),
1097        circuit,
1098        noise_model,
1099        num_shots,
1100        seed,
1101    )
1102}
1103
1104fn run_shots_fallback(
1105    kind: &BackendKind,
1106    circuit: &Circuit,
1107    num_shots: usize,
1108    seed: u64,
1109) -> Result<ShotsResult> {
1110    let mut shots = Vec::with_capacity(num_shots);
1111    let opts = SimOptions::classical_only();
1112    for i in 0..num_shots {
1113        let shot_seed = seed.wrapping_add(i as u64);
1114        let result = run_with_internal(kind.clone(), circuit, shot_seed, opts)?;
1115        shots.push(result.classical_bits);
1116    }
1117    Ok(ShotsResult {
1118        shots,
1119        num_classical_bits: circuit.num_classical_bits,
1120    })
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125    use super::dispatch::min_clifford_prefix_gates;
1126    use super::*;
1127    use crate::backend::mps::MpsBackend;
1128    use crate::backend::product::ProductStateBackend;
1129    use crate::backend::sparse::SparseBackend;
1130    use crate::backend::stabilizer::StabilizerBackend;
1131    use crate::backend::statevector::StatevectorBackend;
1132    use crate::backend::tensornetwork::TensorNetworkBackend;
1133    use crate::circuit::smallvec;
1134    use crate::gates::Gate;
1135
1136    fn make_clifford_circuit() -> Circuit {
1137        let mut c = Circuit::new(3, 0);
1138        c.add_gate(Gate::H, &[0]);
1139        c.add_gate(Gate::Cx, &[0, 1]);
1140        c.add_gate(Gate::Cx, &[1, 2]);
1141        c.add_gate(Gate::S, &[0]);
1142        c
1143    }
1144
1145    fn make_product_circuit() -> Circuit {
1146        let mut c = Circuit::new(4, 0);
1147        c.add_gate(Gate::H, &[0]);
1148        c.add_gate(Gate::Rx(1.0), &[1]);
1149        c.add_gate(Gate::T, &[2]);
1150        c.add_gate(Gate::Y, &[3]);
1151        c
1152    }
1153
1154    fn make_general_circuit() -> Circuit {
1155        let mut c = Circuit::new(3, 0);
1156        c.add_gate(Gate::H, &[0]);
1157        c.add_gate(Gate::T, &[0]);
1158        c.add_gate(Gate::Cx, &[0, 1]);
1159        c
1160    }
1161
1162    #[derive(Debug, Clone, Copy)]
1163    enum ProbabilityFailure {
1164        Unsupported,
1165        Invalid,
1166    }
1167
1168    struct ProbabilityFailureBackend {
1169        failure: ProbabilityFailure,
1170        classical_bits: Vec<bool>,
1171        num_qubits: usize,
1172    }
1173
1174    impl ProbabilityFailureBackend {
1175        fn new(failure: ProbabilityFailure) -> Self {
1176            Self {
1177                failure,
1178                classical_bits: Vec::new(),
1179                num_qubits: 0,
1180            }
1181        }
1182    }
1183
1184    impl Backend for ProbabilityFailureBackend {
1185        fn name(&self) -> &'static str {
1186            "probability_failure"
1187        }
1188
1189        fn init(&mut self, num_qubits: usize, num_classical_bits: usize) -> Result<()> {
1190            self.num_qubits = num_qubits;
1191            self.classical_bits = vec![false; num_classical_bits];
1192            Ok(())
1193        }
1194
1195        fn apply(&mut self, _instruction: &Instruction) -> Result<()> {
1196            Ok(())
1197        }
1198
1199        fn classical_results(&self) -> &[bool] {
1200            &self.classical_bits
1201        }
1202
1203        fn probabilities(&self) -> Result<Vec<f64>> {
1204            match self.failure {
1205                ProbabilityFailure::Unsupported => Err(PrismError::BackendUnsupported {
1206                    backend: self.name().to_string(),
1207                    operation: "probabilities".to_string(),
1208                }),
1209                ProbabilityFailure::Invalid => Err(PrismError::InvalidParameter {
1210                    message: "probability extraction failed".to_string(),
1211                }),
1212            }
1213        }
1214
1215        fn num_qubits(&self) -> usize {
1216            self.num_qubits
1217        }
1218    }
1219
1220    fn assert_pauli_marginals_reject(circuit: &Circuit) {
1221        for backend in [
1222            BackendKind::StochasticPauli { num_samples: 100 },
1223            BackendKind::DeterministicPauli {
1224                epsilon: 0.0,
1225                max_terms: 0,
1226            },
1227        ] {
1228            assert!(matches!(
1229                simulate(circuit)
1230                    .backend(backend)
1231                    .seed(42)
1232                    .marginals()
1233                    .unwrap_err(),
1234                PrismError::IncompatibleBackend { .. }
1235            ));
1236        }
1237    }
1238
1239    #[test]
1240    fn test_circuit_is_clifford_only() {
1241        assert!(make_clifford_circuit().is_clifford_only());
1242        assert!(!make_general_circuit().is_clifford_only());
1243        assert!(!make_product_circuit().is_clifford_only());
1244    }
1245
1246    #[test]
1247    fn test_circuit_has_entangling_gates() {
1248        assert!(make_clifford_circuit().has_entangling_gates());
1249        assert!(make_general_circuit().has_entangling_gates());
1250        assert!(!make_product_circuit().has_entangling_gates());
1251    }
1252
1253    #[test]
1254    fn test_auto_selects_product() {
1255        let circuit = make_product_circuit();
1256        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
1257        assert_eq!(backend.name(), "productstate");
1258    }
1259
1260    #[test]
1261    fn test_auto_selects_stabilizer() {
1262        let circuit = make_clifford_circuit();
1263        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
1264        assert_eq!(backend.name(), "stabilizer");
1265    }
1266
1267    #[test]
1268    fn test_auto_selects_statevector() {
1269        let circuit = make_general_circuit();
1270        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
1271        assert_eq!(backend.name(), "statevector");
1272    }
1273
1274    #[test]
1275    fn test_run_with_auto_matches_explicit() {
1276        let circuit = make_general_circuit();
1277        let auto_result = run_with(BackendKind::Auto, &circuit, 42).unwrap();
1278        let sv_result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1279        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1280        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1281        for (a, b) in auto_probs.iter().zip(sv_probs.iter()) {
1282            assert!((a - b).abs() < 1e-10);
1283        }
1284    }
1285
1286    #[test]
1287    fn test_run_with_explicit_backends() {
1288        let circuit = make_clifford_circuit();
1289        assert!(run_with(BackendKind::Statevector, &circuit, 42).is_ok());
1290        assert!(run_with(BackendKind::Stabilizer, &circuit, 42).is_ok());
1291        assert!(run_with(BackendKind::Sparse, &circuit, 42).is_ok());
1292        assert!(run_with(BackendKind::Mps { max_bond_dim: 64 }, &circuit, 42).is_ok());
1293    }
1294
1295    #[test]
1296    fn test_run_auto_clifford_probs_match_statevector() {
1297        let circuit = make_clifford_circuit();
1298        let auto_result = run(&circuit, 42).unwrap();
1299        let mut sv = StatevectorBackend::new(42);
1300        let sv_result = run_on(&mut sv, &circuit).unwrap();
1301        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1302        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1303        for (a, b) in auto_probs.iter().zip(sv_probs.iter()) {
1304            assert!((a - b).abs() < 1e-10);
1305        }
1306    }
1307
1308    #[test]
1309    fn test_run_qasm() {
1310        let qasm = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];";
1311        let result = run_qasm(qasm, 42).unwrap();
1312        let probs = result.probabilities.unwrap().to_vec();
1313        assert!((probs[0] - 0.5).abs() < 1e-10);
1314        assert!((probs[3] - 0.5).abs() < 1e-10);
1315    }
1316
1317    #[test]
1318    fn test_empty_circuit_is_clifford_and_no_entangling() {
1319        let c = Circuit::new(2, 0);
1320        assert!(c.is_clifford_only());
1321        assert!(!c.has_entangling_gates());
1322    }
1323
1324    #[test]
1325    fn test_validate_stabilizer_rejects_non_clifford() {
1326        let circuit = make_general_circuit(); // has T gate
1327        let result = run_with(BackendKind::Stabilizer, &circuit, 42);
1328        assert!(result.is_err());
1329        let err = result.unwrap_err();
1330        assert!(matches!(
1331            err,
1332            crate::error::PrismError::IncompatibleBackend { .. }
1333        ));
1334    }
1335
1336    #[test]
1337    fn test_validate_product_rejects_entangling() {
1338        let circuit = make_clifford_circuit(); // has CX
1339        let result = run_with(BackendKind::ProductState, &circuit, 42);
1340        assert!(result.is_err());
1341        let err = result.unwrap_err();
1342        assert!(matches!(
1343            err,
1344            crate::error::PrismError::IncompatibleBackend { .. }
1345        ));
1346    }
1347
1348    #[test]
1349    fn test_validate_passes_for_compatible() {
1350        let clifford = make_clifford_circuit();
1351        assert!(run_with(BackendKind::Stabilizer, &clifford, 42).is_ok());
1352
1353        let product = make_product_circuit();
1354        assert!(run_with(BackendKind::ProductState, &product, 42).is_ok());
1355    }
1356
1357    #[test]
1358    fn test_auto_moderate_qubit_count_uses_statevector() {
1359        let mut circuit = Circuit::new(20, 0);
1360        circuit.add_gate(Gate::H, &[0]);
1361        circuit.add_gate(Gate::T, &[0]);
1362        circuit.add_gate(Gate::Cx, &[0, 1]);
1363        let backend = select_backend(&BackendKind::Auto, &circuit, 42, false);
1364        assert_eq!(backend.name(), "statevector");
1365    }
1366
1367    #[test]
1368    fn test_auto_selects_factored_with_partial_independence() {
1369        let mut circuit = Circuit::new(10, 0);
1370        circuit.add_gate(Gate::H, &[0]);
1371        circuit.add_gate(Gate::T, &[0]);
1372        circuit.add_gate(Gate::Cx, &[0, 1]);
1373        let backend = select_backend(&BackendKind::Auto, &circuit, 42, true);
1374        assert_eq!(backend.name(), "factored");
1375    }
1376
1377    #[test]
1378    fn test_auto_ignores_partial_independence_when_no_entangling() {
1379        let circuit = make_product_circuit();
1380        let backend = select_backend(&BackendKind::Auto, &circuit, 42, true);
1381        assert_eq!(backend.name(), "productstate");
1382    }
1383
1384    #[test]
1385    fn test_classical_only_skips_probabilities() {
1386        let qasm =
1387            "OPENQASM 3.0;\nqubit[2] q;\nbit[1] c;\nh q[0];\ncx q[0], q[1];\nc[0] = measure q[0];";
1388        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1389        let result = run_with_internal(
1390            BackendKind::Statevector,
1391            &circuit,
1392            42,
1393            SimOptions::classical_only(),
1394        )
1395        .unwrap();
1396        assert!(result.probabilities.is_none());
1397        assert_eq!(result.classical_bits.len(), 1);
1398    }
1399
1400    #[test]
1401    fn test_default_options_include_probabilities() {
1402        let circuit = make_general_circuit();
1403        let result = run_with_internal(
1404            BackendKind::Statevector,
1405            &circuit,
1406            42,
1407            SimOptions::default(),
1408        )
1409        .unwrap();
1410        assert!(result.probabilities.is_some());
1411    }
1412
1413    #[test]
1414    fn test_run_on_always_computes_probabilities() {
1415        let circuit = make_clifford_circuit();
1416        let mut backend = StatevectorBackend::new(42);
1417        let result = run_on(&mut backend, &circuit).unwrap();
1418        assert!(result.probabilities.is_some());
1419        let probs = result.probabilities.unwrap().to_vec();
1420        let sum: f64 = probs.iter().sum();
1421        assert!((sum - 1.0).abs() < 1e-10);
1422    }
1423
1424    #[test]
1425    fn test_temporal_clifford_matches_statevector() {
1426        // Circuit with a long Clifford prefix followed by non-Clifford gates.
1427        // At 10q with 20+ prefix gates, temporal decomposition should trigger.
1428        let mut c = Circuit::new(10, 0);
1429
1430        // Clifford prefix: 22 gates (above min_clifford_prefix_gates(10)=20)
1431        for i in 0..10 {
1432            c.add_gate(Gate::H, &[i]);
1433        }
1434        for i in 0..9 {
1435            c.add_gate(Gate::Cx, &[i, i + 1]);
1436        }
1437        c.add_gate(Gate::S, &[0]);
1438        c.add_gate(Gate::Sdg, &[3]);
1439        c.add_gate(Gate::SX, &[7]);
1440
1441        // Non-Clifford tail
1442        c.add_gate(Gate::T, &[0]);
1443        c.add_gate(Gate::Rx(0.7), &[1]);
1444        c.add_gate(Gate::Cx, &[2, 3]);
1445        c.add_gate(Gate::Rz(1.2), &[2]);
1446
1447        let (prefix, _tail) = c.clifford_prefix_split().unwrap();
1448        assert!(prefix.gate_count() >= min_clifford_prefix_gates(c.num_qubits));
1449
1450        let auto_result = run(&c, 42).unwrap();
1451
1452        // Pure statevector reference (no temporal decomposition)
1453        let mut sv = StatevectorBackend::new(42);
1454        let sv_result = run_on(&mut sv, &c).unwrap();
1455
1456        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1457        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1458        assert_eq!(auto_probs.len(), sv_probs.len());
1459        for (a, s) in auto_probs.iter().zip(sv_probs.iter()) {
1460            assert!(
1461                (a - s).abs() < 1e-10,
1462                "temporal decomp mismatch: auto={a}, sv={s}"
1463            );
1464        }
1465    }
1466
1467    #[test]
1468    fn test_temporal_clifford_complex_circuit_matches_sv() {
1469        // 3q circuit: prefix too short for temporal (min_clifford_prefix_gates(3)=16)
1470        // but auto must still match statevector.
1471        let mut c = Circuit::new(3, 0);
1472
1473        // Clifford prefix with i-phase generators
1474        c.add_gate(Gate::H, &[0]);
1475        c.add_gate(Gate::Y, &[1]);
1476        c.add_gate(Gate::S, &[0]);
1477        c.add_gate(Gate::Cx, &[0, 1]);
1478        c.add_gate(Gate::H, &[2]);
1479        c.add_gate(Gate::SXdg, &[2]);
1480        c.add_gate(Gate::Cz, &[1, 2]);
1481        c.add_gate(Gate::Swap, &[0, 2]);
1482        c.add_gate(Gate::S, &[1]);
1483
1484        // Non-Clifford tail
1485        c.add_gate(Gate::T, &[0]);
1486        c.add_gate(Gate::Ry(0.3), &[1]);
1487        c.add_gate(Gate::Cx, &[1, 2]);
1488
1489        let auto_result = run(&c, 42).unwrap();
1490        let mut sv = StatevectorBackend::new(42);
1491        let sv_result = run_on(&mut sv, &c).unwrap();
1492
1493        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1494        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1495        for (a, s) in auto_probs.iter().zip(sv_probs.iter()) {
1496            assert!(
1497                (a - s).abs() < 1e-10,
1498                "complex temporal mismatch: auto={a}, sv={s}"
1499            );
1500        }
1501    }
1502
1503    #[test]
1504    fn test_temporal_clifford_skipped_when_prefix_too_short() {
1505        // Short Clifford prefix, should NOT use temporal decomposition,
1506        // but result must still be correct.
1507        let mut c = Circuit::new(2, 0);
1508        c.add_gate(Gate::H, &[0]);
1509        c.add_gate(Gate::Cx, &[0, 1]);
1510        c.add_gate(Gate::T, &[0]); // 2 Clifford gates < min_clifford_prefix_gates(2)=16
1511
1512        let auto_result = run(&c, 42).unwrap();
1513        let mut sv = StatevectorBackend::new(42);
1514        let sv_result = run_on(&mut sv, &c).unwrap();
1515
1516        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1517        let sv_probs = sv_result.probabilities.unwrap().to_vec();
1518        for (a, s) in auto_probs.iter().zip(sv_probs.iter()) {
1519            assert!((a - s).abs() < 1e-10);
1520        }
1521    }
1522
1523    #[test]
1524    fn test_decomposed_random_blocks_matches_monolithic() {
1525        let circuit = crate::circuits::independent_random_blocks(10, 2, 5, 0xDEAD_BEEF);
1526        let decomposed = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1527        let mut sv = StatevectorBackend::new(42);
1528        let monolithic = run_on(&mut sv, &circuit).unwrap();
1529        let d_probs = decomposed.probabilities.unwrap().to_vec();
1530        let m_probs = monolithic.probabilities.unwrap().to_vec();
1531        assert_eq!(d_probs.len(), m_probs.len());
1532        for (d, m) in d_probs.iter().zip(m_probs.iter()) {
1533            assert!(
1534                (d - m).abs() < 1e-10,
1535                "mismatch: decomposed={d}, monolithic={m}"
1536            );
1537        }
1538    }
1539
1540    #[test]
1541    fn test_per_block_clifford_dispatch() {
1542        // 6-qubit circuit: qubits 0-2 = Clifford block, qubits 3-5 = non-Clifford block.
1543        // The two blocks are independent (no gates bridge them).
1544        // Under Auto dispatch with decomposition, block 0-2 should use Stabilizer
1545        // and block 3-5 should use Statevector. Correctness is checked by comparing
1546        // against a monolithic statevector run.
1547        let mut c = Circuit::new(6, 0);
1548
1549        // Block A (Clifford): GHZ state on qubits 0,1,2
1550        c.add_gate(Gate::H, &[0]);
1551        c.add_gate(Gate::Cx, &[0, 1]);
1552        c.add_gate(Gate::Cx, &[1, 2]);
1553        c.add_gate(Gate::S, &[0]);
1554
1555        // Block B (non-Clifford): qubits 3,4,5
1556        c.add_gate(Gate::H, &[3]);
1557        c.add_gate(Gate::T, &[3]);
1558        c.add_gate(Gate::Cx, &[3, 4]);
1559        c.add_gate(Gate::Rx(0.7), &[5]);
1560        c.add_gate(Gate::Cx, &[4, 5]);
1561
1562        let components = c.independent_subsystems();
1563        assert_eq!(components.len(), 2);
1564
1565        let (sub_a, _, _) = c.extract_subcircuit(&components[0]);
1566        assert!(sub_a.is_clifford_only());
1567        let backend_a = select_backend(&BackendKind::Auto, &sub_a, 42, false);
1568        assert_eq!(backend_a.name(), "stabilizer");
1569
1570        let (sub_b, _, _) = c.extract_subcircuit(&components[1]);
1571        assert!(!sub_b.is_clifford_only());
1572        let backend_b = select_backend(&BackendKind::Auto, &sub_b, 43, false);
1573        assert_eq!(backend_b.name(), "statevector");
1574
1575        // End-to-end: auto (decomposed) must match monolithic statevector
1576        let auto_result = run(&c, 42).unwrap();
1577        let mut sv = StatevectorBackend::new(42);
1578        let mono_result = run_on(&mut sv, &c).unwrap();
1579        let auto_probs = auto_result.probabilities.unwrap().to_vec();
1580        let mono_probs = mono_result.probabilities.unwrap().to_vec();
1581        assert_eq!(auto_probs.len(), mono_probs.len());
1582        for (a, m) in auto_probs.iter().zip(mono_probs.iter()) {
1583            assert!((a - m).abs() < 1e-10, "prob mismatch: auto={a}, mono={m}");
1584        }
1585    }
1586
1587    #[test]
1588    fn test_decomposed_bell_pairs_matches_monolithic() {
1589        let circuit = crate::circuits::independent_bell_pairs(10);
1590        let decomposed = run(&circuit, 42).unwrap();
1591        let mut sv = StatevectorBackend::new(42);
1592        let monolithic = run_on(&mut sv, &circuit).unwrap();
1593        let d_probs = decomposed.probabilities.unwrap().to_vec();
1594        let m_probs = monolithic.probabilities.unwrap().to_vec();
1595        assert_eq!(d_probs.len(), m_probs.len());
1596        for (d, m) in d_probs.iter().zip(m_probs.iter()) {
1597            assert!(
1598                (d - m).abs() < 1e-10,
1599                "mismatch: decomposed={d}, monolithic={m}"
1600            );
1601        }
1602    }
1603
1604    #[test]
1605    fn test_measurement_normalization_statevector() {
1606        let qasm = r#"
1607            OPENQASM 3.0;
1608            qubit[2] q;
1609            bit[2] c;
1610            h q[0];
1611            cx q[0], q[1];
1612            c[0] = measure q[0];
1613            c[1] = measure q[1];
1614        "#;
1615        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1616        let result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1617        let probs = result.probabilities.unwrap().to_vec();
1618        let sum: f64 = probs.iter().sum();
1619        assert!(
1620            (sum - 1.0).abs() < 1e-10,
1621            "statevector post-measurement probs sum to {sum}, expected 1.0"
1622        );
1623    }
1624
1625    #[test]
1626    fn test_measurement_normalization_mps() {
1627        let qasm = r#"
1628            OPENQASM 3.0;
1629            qubit[2] q;
1630            bit[2] c;
1631            h q[0];
1632            cx q[0], q[1];
1633            c[0] = measure q[0];
1634            c[1] = measure q[1];
1635        "#;
1636        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1637        let result = run_with(BackendKind::Mps { max_bond_dim: 64 }, &circuit, 42).unwrap();
1638        let probs = result.probabilities.unwrap().to_vec();
1639        let sum: f64 = probs.iter().sum();
1640        assert!(
1641            (sum - 1.0).abs() < 1e-10,
1642            "MPS post-measurement probs sum to {sum}, expected 1.0"
1643        );
1644    }
1645
1646    #[test]
1647    fn test_measurement_normalization_sparse() {
1648        let qasm = r#"
1649            OPENQASM 3.0;
1650            qubit[2] q;
1651            bit[2] c;
1652            h q[0];
1653            cx q[0], q[1];
1654            c[0] = measure q[0];
1655            c[1] = measure q[1];
1656        "#;
1657        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1658        let result = run_with(BackendKind::Sparse, &circuit, 42).unwrap();
1659        let probs = result.probabilities.unwrap().to_vec();
1660        let sum: f64 = probs.iter().sum();
1661        assert!(
1662            (sum - 1.0).abs() < 1e-10,
1663            "sparse post-measurement probs sum to {sum}, expected 1.0"
1664        );
1665    }
1666
1667    #[test]
1668    fn test_conditional_gate_execution() {
1669        let qasm = r#"
1670            OPENQASM 3.0;
1671            qubit[2] q;
1672            bit[1] c;
1673            x q[0];
1674            c[0] = measure q[0];
1675            if (c[0]) x q[1];
1676        "#;
1677        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1678        let result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
1679        // q[0] measured as 1, so if-gate fires, q[1] flipped to |1⟩
1680        // Final state: |11⟩ = index 3
1681        let probs = result.probabilities.unwrap().to_vec();
1682        assert!(
1683            probs[3] > 0.99,
1684            "conditional gate should flip q[1]: probs={probs:?}"
1685        );
1686        assert!(result.classical_bits[0]);
1687    }
1688
1689    fn make_bell_with_measure() -> Circuit {
1690        let qasm = r#"
1691            OPENQASM 3.0;
1692            qubit[2] q;
1693            bit[2] c;
1694            h q[0];
1695            cx q[0], q[1];
1696            c[0] = measure q[0];
1697            c[1] = measure q[1];
1698        "#;
1699        crate::circuit::openqasm::parse(qasm).unwrap()
1700    }
1701
1702    #[test]
1703    fn test_shots_deterministic() {
1704        let circuit = make_bell_with_measure();
1705        let a = run_shots(&circuit, 10, 42).unwrap();
1706        let b = run_shots(&circuit, 10, 42).unwrap();
1707        assert_eq!(a.shots, b.shots);
1708    }
1709
1710    #[test]
1711    fn test_shots_distribution_convergence() {
1712        let circuit = make_bell_with_measure();
1713        let result = run_shots(&circuit, 10000, 42).unwrap();
1714        let counts = result.counts();
1715        let n_00 = counts.get(&vec![0u64]).copied().unwrap_or(0);
1716        let n_11 = counts.get(&vec![3u64]).copied().unwrap_or(0);
1717        let n_01 = counts.get(&vec![2u64]).copied().unwrap_or(0);
1718        let n_10 = counts.get(&vec![1u64]).copied().unwrap_or(0);
1719        assert!(
1720            (4500..=5500).contains(&n_00),
1721            "|00> count {n_00} outside [4500, 5500]"
1722        );
1723        assert!(
1724            (4500..=5500).contains(&n_11),
1725            "|11> count {n_11} outside [4500, 5500]"
1726        );
1727        assert_eq!(n_01, 0, "|01> should never appear in Bell state");
1728        assert_eq!(n_10, 0, "|10> should never appear in Bell state");
1729    }
1730
1731    #[test]
1732    fn test_shots_single_valid_outcome() {
1733        let circuit = make_bell_with_measure();
1734        let shots_result = run_shots(&circuit, 1, 42).unwrap();
1735        let shot = &shots_result.shots[0];
1736        assert_eq!(shot[0], shot[1], "Bell state: both bits must agree");
1737    }
1738
1739    #[test]
1740    fn test_shots_all_zero() {
1741        let qasm = r#"
1742            OPENQASM 3.0;
1743            qubit[3] q;
1744            bit[3] c;
1745            c[0] = measure q[0];
1746            c[1] = measure q[1];
1747            c[2] = measure q[2];
1748        "#;
1749        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1750        let result = run_shots(&circuit, 100, 42).unwrap();
1751        for (i, shot) in result.shots.iter().enumerate() {
1752            assert!(
1753                shot.iter().all(|&b| !b),
1754                "shot {i} should be all-zero: {shot:?}"
1755            );
1756        }
1757    }
1758
1759    #[test]
1760    fn test_shots_mid_circuit_measurement() {
1761        let qasm = r#"
1762            OPENQASM 3.0;
1763            qubit[2] q;
1764            bit[2] c;
1765            x q[0];
1766            c[0] = measure q[0];
1767            if (c[0]) x q[1];
1768            c[1] = measure q[1];
1769        "#;
1770        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1771        let result = run_shots(&circuit, 100, 42).unwrap();
1772        for (i, shot) in result.shots.iter().enumerate() {
1773            assert!(shot[0], "shot {i}: q[0] should always be 1");
1774            assert!(shot[1], "shot {i}: q[1] should always be 1 (conditional)");
1775        }
1776    }
1777
1778    #[test]
1779    fn test_shots_counts_sum() {
1780        let circuit = make_bell_with_measure();
1781        let result = run_shots(&circuit, 500, 42).unwrap();
1782        let counts = result.counts();
1783        let total: u64 = counts.values().sum();
1784        assert_eq!(total, 500);
1785    }
1786
1787    #[test]
1788    fn test_run_counts_factored_stabilizer() {
1789        let circuit = make_bell_with_measure();
1790        let counts = run_counts_with(BackendKind::FactoredStabilizer, &circuit, 128, 42).unwrap();
1791        let total: u64 = counts.values().sum();
1792        let bell_total = counts.get(&vec![0u64]).copied().unwrap_or(0)
1793            + counts.get(&vec![3u64]).copied().unwrap_or(0);
1794
1795        assert_eq!(total, 128);
1796        assert_eq!(bell_total, 128);
1797    }
1798
1799    fn assert_unit_norm(state: &[num_complex::Complex64], label: &str) {
1800        let norm: f64 = state.iter().map(|a| a.norm_sqr()).sum();
1801        assert!(
1802            (norm - 1.0).abs() < 1e-10,
1803            "{label}: norm = {norm}, expected 1.0"
1804        );
1805    }
1806
1807    #[test]
1808    fn test_export_norm_statevector_bell() {
1809        let circuit = make_clifford_circuit();
1810        let mut backend = StatevectorBackend::new(42);
1811        run_on(&mut backend, &circuit).unwrap();
1812        assert_unit_norm(&backend.export_statevector().unwrap(), "statevector/bell");
1813    }
1814
1815    #[test]
1816    fn test_export_norm_statevector_parametric() {
1817        let circuit = crate::circuits::hardware_efficient_ansatz(6, 3, 42);
1818        let mut backend = StatevectorBackend::new(42);
1819        run_on(&mut backend, &circuit).unwrap();
1820        assert_unit_norm(&backend.export_statevector().unwrap(), "statevector/hea_6q");
1821    }
1822
1823    #[test]
1824    fn test_export_norm_stabilizer() {
1825        let circuit = make_clifford_circuit();
1826        let mut backend = StabilizerBackend::new(42);
1827        run_on(&mut backend, &circuit).unwrap();
1828        assert_unit_norm(&backend.export_statevector().unwrap(), "stabilizer");
1829    }
1830
1831    #[test]
1832    fn test_export_norm_sparse() {
1833        let circuit = make_general_circuit();
1834        let mut backend = SparseBackend::new(42);
1835        run_on(&mut backend, &circuit).unwrap();
1836        assert_unit_norm(&backend.export_statevector().unwrap(), "sparse");
1837    }
1838
1839    #[test]
1840    fn test_export_norm_mps() {
1841        let circuit = make_general_circuit();
1842        let mut backend = MpsBackend::new(64, 42);
1843        run_on(&mut backend, &circuit).unwrap();
1844        assert_unit_norm(&backend.export_statevector().unwrap(), "mps");
1845    }
1846
1847    #[test]
1848    fn test_export_norm_product_state() {
1849        let circuit = make_product_circuit();
1850        let mut backend = ProductStateBackend::new(42);
1851        run_on(&mut backend, &circuit).unwrap();
1852        assert_unit_norm(&backend.export_statevector().unwrap(), "productstate");
1853    }
1854
1855    #[test]
1856    fn test_export_norm_tensor_network() {
1857        let circuit = make_general_circuit();
1858        let mut backend = TensorNetworkBackend::new(42);
1859        run_on(&mut backend, &circuit).unwrap();
1860        assert_unit_norm(&backend.export_statevector().unwrap(), "tensornetwork");
1861    }
1862
1863    #[test]
1864    fn test_export_norm_after_measurement() {
1865        let qasm = r#"
1866            OPENQASM 3.0;
1867            qubit[3] q;
1868            bit[1] c;
1869            h q[0];
1870            cx q[0], q[1];
1871            h q[2];
1872            c[0] = measure q[0];
1873        "#;
1874        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1875        for backend_kind in [
1876            BackendKind::Statevector,
1877            BackendKind::Sparse,
1878            BackendKind::Mps { max_bond_dim: 64 },
1879        ] {
1880            let label = format!("{backend_kind:?}/post-measure");
1881            let mut backend = select_backend(&backend_kind, &circuit, 42, false);
1882            run_on(backend.as_mut(), &circuit).unwrap();
1883            let state = backend.export_statevector().unwrap();
1884            assert_unit_norm(&state, &label);
1885        }
1886    }
1887
1888    #[test]
1889    fn test_export_norm_qft() {
1890        let circuit = crate::circuits::qft_circuit(8);
1891        for (kind, label) in [
1892            (BackendKind::Statevector, "statevector/qft8"),
1893            (BackendKind::Sparse, "sparse/qft8"),
1894            (BackendKind::Mps { max_bond_dim: 128 }, "mps/qft8"),
1895            (BackendKind::TensorNetwork, "tn/qft8"),
1896        ] {
1897            let mut backend = select_backend(&kind, &circuit, 42, false);
1898            run_on(backend.as_mut(), &circuit).unwrap();
1899            let state = backend.export_statevector().unwrap();
1900            assert_unit_norm(&state, label);
1901        }
1902    }
1903
1904    #[test]
1905    fn test_export_factored_unsupported() {
1906        let circuit = make_general_circuit();
1907        let mut backend = crate::backend::factored::FactoredBackend::new(42);
1908        run_on(&mut backend, &circuit).unwrap();
1909        assert!(backend.export_statevector().is_err());
1910    }
1911
1912    #[test]
1913    fn test_shots_random_convergence() {
1914        let circuit = make_bell_with_measure();
1915        let result = run_shots(&circuit, 10000, rand::random()).unwrap();
1916        let counts = result.counts();
1917        let n_00 = counts.get(&vec![0u64]).copied().unwrap_or(0);
1918        let n_11 = counts.get(&vec![3u64]).copied().unwrap_or(0);
1919        let n_01 = counts.get(&vec![2u64]).copied().unwrap_or(0);
1920        let n_10 = counts.get(&vec![1u64]).copied().unwrap_or(0);
1921        // p=0.5, n=10000 → σ=50. Bounds at ±10σ: failure prob < 10^-23.
1922        assert!(
1923            (4500..=5500).contains(&n_00),
1924            "|00> count {n_00} outside [4500, 5500]"
1925        );
1926        assert!(
1927            (4500..=5500).contains(&n_11),
1928            "|11> count {n_11} outside [4500, 5500]"
1929        );
1930        assert_eq!(n_01, 0, "|01> should never appear in Bell state");
1931        assert_eq!(n_10, 0, "|10> should never appear in Bell state");
1932    }
1933
1934    #[test]
1935    fn test_has_terminal_measurements_only() {
1936        let mut c = Circuit::new(2, 0);
1937        c.add_gate(Gate::H, &[0]);
1938        assert!(c.has_terminal_measurements_only());
1939
1940        let qasm = r#"
1941            OPENQASM 3.0;
1942            qubit[2] q;
1943            bit[2] c;
1944            h q[0];
1945            cx q[0], q[1];
1946            c[0] = measure q[0];
1947            c[1] = measure q[1];
1948        "#;
1949        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1950        assert!(circuit.has_terminal_measurements_only());
1951
1952        let qasm = r#"
1953            OPENQASM 3.0;
1954            qubit[2] q;
1955            bit[1] c;
1956            c[0] = measure q[0];
1957            h q[1];
1958        "#;
1959        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1960        assert!(!circuit.has_terminal_measurements_only());
1961
1962        let qasm = r#"
1963            OPENQASM 3.0;
1964            qubit[2] q;
1965            bit[2] c;
1966            x q[0];
1967            c[0] = measure q[0];
1968            if (c[0]) x q[1];
1969            c[1] = measure q[1];
1970        "#;
1971        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1972        assert!(!circuit.has_terminal_measurements_only());
1973
1974        let qasm = r#"
1975            OPENQASM 3.0;
1976            qubit[1] q;
1977            bit[1] c;
1978            h q[0];
1979            c[0] = measure q[0];
1980            x q[0];
1981        "#;
1982        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1983        assert!(!circuit.has_terminal_measurements_only());
1984    }
1985
1986    #[test]
1987    fn test_measurement_map() {
1988        let qasm = r#"
1989            OPENQASM 3.0;
1990            qubit[3] q;
1991            bit[3] c;
1992            c[2] = measure q[0];
1993            c[0] = measure q[2];
1994            c[1] = measure q[1];
1995        "#;
1996        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
1997        let map = circuit.measurement_map();
1998        assert_eq!(map, vec![(0, 2), (2, 0), (1, 1)]);
1999    }
2000
2001    #[test]
2002    fn test_fast_path_deterministic_x() {
2003        let qasm = r#"
2004            OPENQASM 3.0;
2005            qubit[1] q;
2006            bit[1] c;
2007            x q[0];
2008            c[0] = measure q[0];
2009        "#;
2010        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
2011        assert!(circuit.has_terminal_measurements_only());
2012        let result = run_shots(&circuit, 100, 42).unwrap();
2013        for (i, shot) in result.shots.iter().enumerate() {
2014            assert!(shot[0], "shot {i}: X|0> should always measure 1");
2015        }
2016    }
2017
2018    #[test]
2019    fn test_fast_path_preserves_classical_bit_index() {
2020        let qasm = r#"
2021            OPENQASM 3.0;
2022            qubit[1] q;
2023            bit[3] c;
2024            x q[0];
2025            c[2] = measure q[0];
2026        "#;
2027        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
2028        let result = run_shots(&circuit, 16, 42).unwrap();
2029        assert_eq!(result.num_classical_bits, 3);
2030        for shot in &result.shots {
2031            assert_eq!(shot, &vec![false, false, true]);
2032        }
2033    }
2034
2035    #[test]
2036    fn test_terminal_statevector_sampling_matches_probability_path() {
2037        let mut c = Circuit::new(5, 5);
2038        for q in 0..5 {
2039            c.add_gate(Gate::Ry(0.17 + q as f64 * 0.11), &[q]);
2040        }
2041        c.add_gate(Gate::Cx, &[0, 1]);
2042        c.add_gate(Gate::Cx, &[1, 2]);
2043        c.add_gate(Gate::Rz(0.41), &[3]);
2044        c.add_gate(Gate::Rx(0.23), &[4]);
2045        c.add_measure(3, 1);
2046        c.add_measure(0, 4);
2047
2048        let stripped = c.without_measurements();
2049        let reference = run_with_internal(
2050            BackendKind::Statevector,
2051            &stripped,
2052            42,
2053            SimOptions::default(),
2054        )
2055        .unwrap();
2056        let probs = reference.probabilities.unwrap();
2057        let expected =
2058            shots::sample_shots(&probs, &c.measurement_map(), c.num_classical_bits, 256, 42);
2059
2060        let actual = run_shots_with(BackendKind::Statevector, &c, 256, 42).unwrap();
2061        assert_eq!(actual.shots, expected);
2062    }
2063
2064    #[test]
2065    fn test_terminal_statevector_counts_match_probability_path_all_measured() {
2066        let mut c = Circuit::new(4, 4);
2067        c.add_gate(Gate::Ry(0.31), &[0]);
2068        c.add_gate(Gate::Ry(0.47), &[1]);
2069        c.add_gate(Gate::Cx, &[0, 2]);
2070        c.add_gate(Gate::Rx(0.19), &[3]);
2071        c.add_measure(0, 0);
2072        c.add_measure(1, 1);
2073        c.add_measure(2, 2);
2074        c.add_measure(3, 3);
2075
2076        let stripped = c.without_measurements();
2077        let reference = run_with_internal(
2078            BackendKind::Statevector,
2079            &stripped,
2080            7,
2081            SimOptions::default(),
2082        )
2083        .unwrap();
2084        let probs = reference.probabilities.unwrap();
2085        let expected_shots =
2086            shots::sample_shots(&probs, &c.measurement_map(), c.num_classical_bits, 512, 7);
2087        let expected = ShotsResult {
2088            shots: expected_shots,
2089            num_classical_bits: c.num_classical_bits,
2090        }
2091        .counts();
2092
2093        let actual = run_counts_with(BackendKind::Statevector, &c, 512, 7).unwrap();
2094        assert_eq!(actual, expected);
2095    }
2096
2097    #[test]
2098    fn test_terminal_statevector_duplicate_classical_bit_uses_last_measurement() {
2099        let mut c = Circuit::new(2, 1);
2100        c.add_gate(Gate::X, &[0]);
2101        c.add_measure(0, 0);
2102        c.add_measure(1, 0);
2103
2104        let shots = run_shots_with(BackendKind::Statevector, &c, 16, 42).unwrap();
2105        for shot in &shots.shots {
2106            assert_eq!(shot, &vec![false]);
2107        }
2108
2109        let counts = run_counts_with(BackendKind::Statevector, &c, 16, 42).unwrap();
2110        assert_eq!(counts.get(&vec![0]), Some(&16));
2111    }
2112
2113    #[test]
2114    fn test_terminal_statevector_counts_wide_classical_register() {
2115        let mut c = Circuit::new(2, 72);
2116        c.add_gate(Gate::X, &[0]);
2117        c.add_measure(0, 70);
2118
2119        let counts = run_counts_with(BackendKind::Statevector, &c, 10, 11).unwrap();
2120        let mut expected = vec![0u64; 2];
2121        expected[1] = 1u64 << 6;
2122        assert_eq!(counts.get(&expected), Some(&10));
2123    }
2124
2125    #[test]
2126    fn test_terminal_statevector_subset_counts_sum_to_shots() {
2127        let mut c = Circuit::new(5, 5);
2128        for q in 0..5 {
2129            c.add_gate(Gate::Ry(0.21 + q as f64 * 0.07), &[q]);
2130        }
2131        c.add_gate(Gate::Cx, &[0, 1]);
2132        c.add_gate(Gate::Cx, &[3, 4]);
2133        c.add_measure(1, 4);
2134        c.add_measure(4, 0);
2135
2136        let counts = run_counts_with(BackendKind::Statevector, &c, 1024, 42).unwrap();
2137        assert_eq!(counts.values().sum::<u64>(), 1024);
2138        assert!(counts.keys().all(|key| key[0] & !0b1_0001 == 0));
2139    }
2140
2141    #[test]
2142    fn test_fast_path_no_measurements() {
2143        let mut c = Circuit::new(2, 2);
2144        c.add_gate(Gate::H, &[0]);
2145        let result = run_shots(&c, 50, 42).unwrap();
2146        for shot in &result.shots {
2147            assert_eq!(shot.len(), 2);
2148            assert!(!shot[0] && !shot[1], "no measurements → all-false");
2149        }
2150    }
2151
2152    #[test]
2153    fn test_shots_cached_fusion_matches_uncached() {
2154        let qasm = r#"
2155            OPENQASM 3.0;
2156            qubit[2] q;
2157            bit[2] c;
2158            h q[0];
2159            cx q[0], q[1];
2160            c[0] = measure q[0];
2161            x q[1];
2162            c[1] = measure q[1];
2163        "#;
2164        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
2165        assert!(!circuit.has_terminal_measurements_only());
2166
2167        let cached = run_shots_with(BackendKind::Statevector, &circuit, 20, 42).unwrap();
2168        for i in 0..20 {
2169            let seed_i = 42u64.wrapping_add(i as u64);
2170            let single = run_with_internal(
2171                BackendKind::Statevector,
2172                &circuit,
2173                seed_i,
2174                SimOptions::default(),
2175            )
2176            .unwrap();
2177            assert_eq!(cached.shots[i], single.classical_bits, "shot {i} mismatch");
2178        }
2179    }
2180
2181    #[test]
2182    fn test_shots_decomposed_cached() {
2183        let qasm = r#"
2184            OPENQASM 3.0;
2185            qubit[8] q;
2186            bit[8] c;
2187            h q[0];
2188            cx q[0], q[1];
2189            c[0] = measure q[0];
2190            x q[1];
2191            c[1] = measure q[1];
2192            h q[4];
2193            cx q[4], q[5];
2194            c[4] = measure q[4];
2195            x q[5];
2196            c[5] = measure q[5];
2197        "#;
2198        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
2199        assert!(!circuit.has_terminal_measurements_only());
2200        let comps = circuit.independent_subsystems();
2201        assert!(comps.len() > 1, "circuit should decompose");
2202
2203        let result = run_shots_with(BackendKind::Statevector, &circuit, 10, 42).unwrap();
2204        assert_eq!(result.shots.len(), 10);
2205        for shot in &result.shots {
2206            assert_eq!(shot.len(), 8);
2207        }
2208    }
2209
2210    #[test]
2211    fn test_shots_temporal_clifford_fallback() {
2212        let mut c = Circuit::new(4, 4);
2213        for i in 0..4 {
2214            c.add_gate(Gate::H, &[i]);
2215        }
2216        for i in 0..3 {
2217            c.add_gate(Gate::Cx, &[i, i + 1]);
2218        }
2219        c.add_gate(Gate::T, &[0]);
2220        c.add_measure(0, 0);
2221        c.add_gate(Gate::X, &[1]);
2222        c.add_measure(1, 1);
2223
2224        let result = run_shots_with(BackendKind::Auto, &c, 10, 42).unwrap();
2225        assert_eq!(result.shots.len(), 10);
2226        for shot in &result.shots {
2227            assert_eq!(shot.len(), 4);
2228        }
2229    }
2230
2231    #[test]
2232    fn test_stabilizer_rank_dispatch() {
2233        let circuit = make_general_circuit();
2234        let result = run_with(BackendKind::StabilizerRank, &circuit, 42).unwrap();
2235        let probs = result.probabilities.unwrap().to_vec();
2236        assert_eq!(probs.len(), 8);
2237        let total: f64 = probs.iter().sum();
2238        assert!((total - 1.0).abs() < 1e-10);
2239
2240        let sv_result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
2241        let sv_probs = sv_result.probabilities.unwrap().to_vec();
2242        for (i, (sr, sv)) in probs.iter().zip(sv_probs.iter()).enumerate() {
2243            assert!(
2244                (sr - sv).abs() < 1e-10,
2245                "prob[{i}]: stab_rank={sr}, statevector={sv}"
2246            );
2247        }
2248    }
2249
2250    #[test]
2251    fn test_stabilizer_rank_rejects_no_t() {
2252        let circuit = make_clifford_circuit();
2253        let result = run_with(BackendKind::StabilizerRank, &circuit, 42);
2254        assert!(result.is_err());
2255    }
2256
2257    #[test]
2258    fn test_auto_clifford_plus_t_probabilities() {
2259        let circuit = make_general_circuit();
2260        assert!(circuit.is_clifford_plus_t());
2261        assert!(circuit.has_t_gates());
2262
2263        let auto_result = run_with(BackendKind::Auto, &circuit, 42).unwrap();
2264        let sv_result = run_with(BackendKind::Statevector, &circuit, 42).unwrap();
2265
2266        let auto_probs = auto_result.probabilities.unwrap().to_vec();
2267        let sv_probs = sv_result.probabilities.unwrap().to_vec();
2268        for (i, (a, s)) in auto_probs.iter().zip(sv_probs.iter()).enumerate() {
2269            assert!(
2270                (a - s).abs() < 1e-10,
2271                "prob[{i}]: auto={a}, statevector={s}"
2272            );
2273        }
2274    }
2275
2276    #[test]
2277    fn test_auto_clifford_plus_t_shots() {
2278        let mut c = Circuit::new(2, 2);
2279        c.add_gate(Gate::H, &[0]);
2280        c.add_gate(Gate::T, &[0]);
2281        c.add_gate(Gate::Cx, &[0, 1]);
2282        c.add_measure(0, 0);
2283        c.add_measure(1, 1);
2284
2285        let result = run_shots_with(BackendKind::Auto, &c, 100, 42).unwrap();
2286        assert_eq!(result.shots.len(), 100);
2287        for shot in &result.shots {
2288            assert_eq!(shot.len(), 2);
2289        }
2290    }
2291
2292    #[test]
2293    fn test_decomposed_mixed_clifford_and_t() {
2294        // Two independent subsystems: q0-q1 (Clifford+T), q2-q3 (Clifford-only)
2295        // Under decomposition, q2-q3 should route to Stabilizer, q0-q1 to StabilizerRank
2296        let mut c = Circuit::new(4, 0);
2297        c.add_gate(Gate::H, &[0]);
2298        c.add_gate(Gate::T, &[0]);
2299        c.add_gate(Gate::Cx, &[0, 1]);
2300        c.add_gate(Gate::H, &[2]);
2301        c.add_gate(Gate::Cx, &[2, 3]);
2302
2303        let subs = c.independent_subsystems();
2304        assert_eq!(subs.len(), 2);
2305
2306        let auto_result = run_with(BackendKind::Auto, &c, 42).unwrap();
2307        let sv_result = run_with(BackendKind::Statevector, &c, 42).unwrap();
2308
2309        let auto_probs = auto_result.probabilities.unwrap().to_vec();
2310        let sv_probs = sv_result.probabilities.unwrap().to_vec();
2311        for (i, (a, s)) in auto_probs.iter().zip(sv_probs.iter()).enumerate() {
2312            assert!(
2313                (a - s).abs() < 1e-10,
2314                "prob[{i}]: auto={a}, statevector={s}"
2315            );
2316        }
2317    }
2318
2319    #[test]
2320    fn test_run_shots_with_noise_clifford_uses_compiled() {
2321        let n = 10;
2322        let mut circuit = crate::circuits::ghz_circuit(n);
2323        circuit.num_classical_bits = n;
2324        for i in 0..n {
2325            circuit.add_measure(i, i);
2326        }
2327        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2328        let result = run_shots_with_noise(BackendKind::Auto, &circuit, &noise, 100, 42).unwrap();
2329        assert_eq!(result.shots.len(), 100);
2330        assert!(result.shots[0].len() == n);
2331    }
2332
2333    #[test]
2334    fn test_run_shots_with_noise_statevector_brute() {
2335        let mut circuit = Circuit::new(3, 3);
2336        circuit.add_gate(Gate::H, &[0]);
2337        circuit.add_gate(Gate::T, &[0]);
2338        circuit.add_gate(Gate::Cx, &[0, 1]);
2339        circuit.add_measure(0, 0);
2340        circuit.add_measure(1, 1);
2341        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2342        let result =
2343            run_shots_with_noise(BackendKind::Statevector, &circuit, &noise, 50, 42).unwrap();
2344        assert_eq!(result.shots.len(), 50);
2345        assert_eq!(result.shots[0].len(), 3);
2346    }
2347
2348    #[test]
2349    fn test_run_shots_with_noise_auto_non_clifford() {
2350        let mut circuit = Circuit::new(3, 3);
2351        circuit.add_gate(Gate::H, &[0]);
2352        circuit.add_gate(Gate::T, &[0]);
2353        circuit.add_gate(Gate::Cx, &[0, 1]);
2354        circuit.add_measure(0, 0);
2355        circuit.add_measure(1, 1);
2356        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.001);
2357        let result = run_shots_with_noise(BackendKind::Auto, &circuit, &noise, 100, 42).unwrap();
2358        assert_eq!(result.shots.len(), 100);
2359    }
2360
2361    #[cfg(feature = "gpu")]
2362    #[test]
2363    fn test_run_shots_with_stabilizer_gpu_falls_back_for_reset_circuits() {
2364        let mut circuit = Circuit::new(1, 1);
2365        circuit.add_gate(Gate::X, &[0]);
2366        circuit.add_reset(0);
2367        circuit.add_measure(0, 0);
2368
2369        let cpu = run_shots_with(BackendKind::Stabilizer, &circuit, 8, 42).unwrap();
2370        let gpu = run_shots_with(
2371            BackendKind::StabilizerGpu {
2372                context: crate::gpu::GpuContext::stub_for_tests(),
2373            },
2374            &circuit,
2375            8,
2376            42,
2377        )
2378        .unwrap();
2379
2380        assert_eq!(gpu.shots, cpu.shots);
2381    }
2382
2383    #[cfg(feature = "gpu")]
2384    #[test]
2385    fn test_run_shots_with_stabilizer_gpu_falls_back_for_conditionals() {
2386        let mut circuit = Circuit::new(2, 2);
2387        circuit.add_gate(Gate::H, &[0]);
2388        circuit.add_measure(0, 0);
2389        circuit.instructions.push(Instruction::Conditional {
2390            condition: crate::circuit::ClassicalCondition::BitIsOne(0),
2391            gate: Gate::X,
2392            targets: crate::circuit::smallvec![1],
2393        });
2394        circuit.add_measure(1, 1);
2395
2396        let cpu = run_shots_with(BackendKind::Stabilizer, &circuit, 256, 42).unwrap();
2397        let gpu = run_shots_with(
2398            BackendKind::StabilizerGpu {
2399                context: crate::gpu::GpuContext::stub_for_tests(),
2400            },
2401            &circuit,
2402            256,
2403            42,
2404        )
2405        .unwrap();
2406
2407        assert_eq!(gpu.shots, cpu.shots);
2408    }
2409
2410    #[cfg(feature = "gpu")]
2411    #[test]
2412    fn test_run_shots_with_noise_stabilizer_gpu_matches_stabilizer() {
2413        let n = 8;
2414        let mut circuit = crate::circuits::ghz_circuit(n);
2415        circuit.num_classical_bits = n;
2416        for i in 0..n {
2417            circuit.add_measure(i, i);
2418        }
2419        let noise = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2420
2421        let cpu = run_shots_with_noise(BackendKind::Stabilizer, &circuit, &noise, 128, 42).unwrap();
2422        let gpu = run_shots_with_noise(
2423            BackendKind::StabilizerGpu {
2424                context: crate::gpu::GpuContext::stub_for_tests(),
2425            },
2426            &circuit,
2427            &noise,
2428            128,
2429            42,
2430        )
2431        .unwrap();
2432
2433        assert_eq!(gpu.shots, cpu.shots);
2434    }
2435
2436    #[test]
2437    fn test_run_marginals_bell_pair() {
2438        let mut c = Circuit::new(2, 0);
2439        c.add_gate(Gate::H, &[0]);
2440        c.add_gate(Gate::Cx, &[0, 1]);
2441        let m = run_marginals(&c, 42).unwrap();
2442        assert_eq!(m.len(), 2);
2443        assert!((m[0].0 - 0.5).abs() < 1e-10);
2444        assert!((m[0].1 - 0.5).abs() < 1e-10);
2445        assert!((m[1].0 - 0.5).abs() < 1e-10);
2446        assert!((m[1].1 - 0.5).abs() < 1e-10);
2447    }
2448
2449    #[test]
2450    fn test_run_marginals_x_gate() {
2451        let mut c = Circuit::new(2, 0);
2452        c.add_gate(Gate::X, &[0]);
2453        let m = run_marginals(&c, 42).unwrap();
2454        assert!((m[0].0 - 0.0).abs() < 1e-10);
2455        assert!((m[0].1 - 1.0).abs() < 1e-10);
2456        assert!((m[1].0 - 1.0).abs() < 1e-10);
2457        assert!((m[1].1 - 0.0).abs() < 1e-10);
2458    }
2459
2460    #[test]
2461    fn test_run_handles_backend_probability_failures() {
2462        let circuit = Circuit::new(1, 0);
2463
2464        let mut unsupported = ProbabilityFailureBackend::new(ProbabilityFailure::Unsupported);
2465        let result = run_on(&mut unsupported, &circuit).unwrap();
2466        assert!(result.classical_bits.is_empty());
2467        assert!(result.probabilities.is_none());
2468
2469        let mut invalid = ProbabilityFailureBackend::new(ProbabilityFailure::Invalid);
2470        let err = run_on(&mut invalid, &circuit).unwrap_err();
2471
2472        assert!(matches!(err, PrismError::InvalidParameter { .. }));
2473    }
2474
2475    #[test]
2476    fn test_run_marginals_rejects_missing_probability_output() {
2477        let circuit = Circuit::new(65, 0);
2478        let run_result = simulate(&circuit)
2479            .backend(BackendKind::FactoredStabilizer)
2480            .seed(42)
2481            .run()
2482            .unwrap();
2483        assert!(run_result.probabilities.is_none());
2484
2485        let err = simulate(&circuit)
2486            .backend(BackendKind::FactoredStabilizer)
2487            .seed(42)
2488            .marginals()
2489            .unwrap_err();
2490        assert!(matches!(err, PrismError::BackendUnsupported { .. }));
2491    }
2492
2493    #[test]
2494    fn test_run_marginals_clifford_t_spd_path() {
2495        let c = crate::circuits::clifford_t_circuit(14, 10, 0.1, 42);
2496        let m_spd = run_marginals(&c, 42).unwrap();
2497        assert_eq!(m_spd.len(), 14);
2498        for (p0, p1) in &m_spd {
2499            assert!(*p0 >= 0.0 && *p0 <= 1.0);
2500            assert!((p0 + p1 - 1.0).abs() < 1e-10);
2501        }
2502
2503        let m_sv = run_marginals_with(BackendKind::Statevector, &c, 42).unwrap();
2504        for i in 0..14 {
2505            assert!(
2506                (m_spd[i].0 - m_sv[i].0).abs() < 1e-6,
2507                "qubit {i}: SPD p0={} vs SV p0={}",
2508                m_spd[i].0,
2509                m_sv[i].0
2510            );
2511        }
2512    }
2513
2514    // ── Dispatch validation ───────────────────────────────────────────
2515
2516    #[test]
2517    fn test_simulate_builder_run_matches_run() {
2518        let mut c = Circuit::new(3, 0);
2519        c.add_gate(Gate::H, &[0]);
2520        c.add_gate(Gate::Cx, &[0, 1]);
2521        c.add_gate(Gate::Ry(0.31), &[2]);
2522
2523        let expected = run(&c, 42).unwrap();
2524        let actual = simulate(&c).seed(42).run().unwrap();
2525
2526        assert_eq!(actual.classical_bits, expected.classical_bits);
2527        assert_eq!(
2528            actual.probabilities.unwrap().to_vec(),
2529            expected.probabilities.unwrap().to_vec()
2530        );
2531    }
2532
2533    #[test]
2534    fn test_simulate_builder_sample_counts_matches_run_counts() {
2535        let mut c = Circuit::new(4, 4);
2536        c.add_gate(Gate::Ry(0.25), &[0]);
2537        c.add_gate(Gate::Cx, &[0, 1]);
2538        c.add_gate(Gate::Rx(0.17), &[2]);
2539        c.add_measure(0, 0);
2540        c.add_measure(1, 1);
2541        c.add_measure(2, 2);
2542        c.add_measure(3, 3);
2543
2544        let expected = run_counts(&c, 256, 42).unwrap();
2545        let actual = simulate(&c).seed(42).sample_counts(256).unwrap();
2546
2547        assert_eq!(actual.num_classical_bits, c.num_classical_bits);
2548        assert_eq!(actual.counts, expected);
2549    }
2550
2551    #[test]
2552    fn test_simulate_builder_marginals_matches_run_marginals() {
2553        let c = crate::circuits::clifford_t_circuit(14, 10, 0.1, 42);
2554        let expected = run_marginals(&c, 42).unwrap();
2555        let actual = simulate(&c).seed(42).marginals().unwrap();
2556
2557        assert_eq!(actual.marginals.len(), expected.len());
2558        for (a, b) in actual.marginals.iter().zip(expected.iter()) {
2559            assert!((a.0 - b.0).abs() < 1e-12);
2560            assert!((a.1 - b.1).abs() < 1e-12);
2561        }
2562    }
2563
2564    #[test]
2565    fn test_validate_factored_stabilizer_rejects_non_clifford() {
2566        let circuit = make_general_circuit();
2567        assert!(matches!(
2568            run_with(BackendKind::FactoredStabilizer, &circuit, 42).unwrap_err(),
2569            crate::error::PrismError::IncompatibleBackend { .. }
2570        ));
2571    }
2572
2573    #[test]
2574    fn test_validate_stabilizer_rank_rejects_no_t_gates() {
2575        let circuit = make_clifford_circuit();
2576        assert!(matches!(
2577            run_with(BackendKind::StabilizerRank, &circuit, 42).unwrap_err(),
2578            crate::error::PrismError::IncompatibleBackend { .. }
2579        ));
2580    }
2581
2582    #[test]
2583    fn test_validate_factored_stabilizer_accepts_clifford() {
2584        assert!(run_with(
2585            BackendKind::FactoredStabilizer,
2586            &make_clifford_circuit(),
2587            42
2588        )
2589        .is_ok());
2590    }
2591
2592    // ── Pauli backend error paths ───────────────────────────────────────
2593
2594    #[test]
2595    fn test_pauli_backends_reject_mid_circuit_measurements() {
2596        let qasm = r#"
2597            OPENQASM 3.0;
2598            qubit[2] q;
2599            bit[2] c;
2600            h q[0];
2601            c[0] = measure q[0];
2602            cx q[0], q[1];
2603            c[1] = measure q[1];
2604        "#;
2605        let circuit = crate::circuit::openqasm::parse(qasm).unwrap();
2606
2607        assert!(matches!(
2608            run_shots_with(
2609                BackendKind::StochasticPauli { num_samples: 100 },
2610                &circuit,
2611                10,
2612                42
2613            )
2614            .unwrap_err(),
2615            crate::error::PrismError::IncompatibleBackend { .. }
2616        ));
2617        assert!(matches!(
2618            run_shots_with(
2619                BackendKind::DeterministicPauli {
2620                    epsilon: 1e-3,
2621                    max_terms: 1000
2622                },
2623                &circuit,
2624                10,
2625                42,
2626            )
2627            .unwrap_err(),
2628            crate::error::PrismError::IncompatibleBackend { .. }
2629        ));
2630    }
2631
2632    // ── Noisy simulation error paths ────────────────────────────────────
2633
2634    #[test]
2635    fn test_pauli_backends_reject_generic_run() {
2636        let c = crate::circuits::clifford_t_circuit(4, 2, 0.1, 42);
2637
2638        assert!(matches!(
2639            simulate(&c)
2640                .backend(BackendKind::StochasticPauli { num_samples: 100 })
2641                .seed(42)
2642                .run()
2643                .unwrap_err(),
2644            crate::error::PrismError::IncompatibleBackend { .. }
2645        ));
2646        assert!(matches!(
2647            simulate(&c)
2648                .backend(BackendKind::DeterministicPauli {
2649                    epsilon: 0.0,
2650                    max_terms: 0
2651                })
2652                .seed(42)
2653                .run()
2654                .unwrap_err(),
2655            crate::error::PrismError::IncompatibleBackend { .. }
2656        ));
2657    }
2658
2659    #[test]
2660    fn test_pauli_backends_return_marginals_through_builder() {
2661        let c = crate::circuits::clifford_t_circuit(4, 2, 0.1, 42);
2662
2663        let spp = simulate(&c)
2664            .backend(BackendKind::StochasticPauli { num_samples: 1_000 })
2665            .seed(42)
2666            .marginals()
2667            .unwrap();
2668        let spd = simulate(&c)
2669            .backend(BackendKind::DeterministicPauli {
2670                epsilon: 0.0,
2671                max_terms: 0,
2672            })
2673            .seed(42)
2674            .marginals()
2675            .unwrap();
2676
2677        assert_eq!(spp.marginals.len(), c.num_qubits);
2678        assert_eq!(spd.marginals.len(), c.num_qubits);
2679        assert!(spp
2680            .marginals
2681            .iter()
2682            .chain(spd.marginals.iter())
2683            .all(|(p0, p1)| *p0 >= 0.0 && *p0 <= 1.0 && (p0 + p1 - 1.0).abs() < 1e-10));
2684    }
2685
2686    #[test]
2687    fn test_pauli_marginals_reject_non_clifford_t_gates() {
2688        let mut c = Circuit::new(1, 0);
2689        c.add_gate(Gate::Rx(0.25), &[0]);
2690
2691        assert_pauli_marginals_reject(&c);
2692    }
2693
2694    #[test]
2695    fn test_pauli_marginals_reject_measurements_resets_and_conditionals() {
2696        let mut measured = Circuit::new(1, 1);
2697        measured.add_gate(Gate::H, &[0]);
2698        measured.add_gate(Gate::T, &[0]);
2699        measured.add_measure(0, 0);
2700
2701        let mut reset = Circuit::new(1, 0);
2702        reset.add_gate(Gate::T, &[0]);
2703        reset.add_reset(0);
2704
2705        let mut conditional = Circuit::new(2, 1);
2706        conditional.add_measure(0, 0);
2707        conditional.instructions.push(Instruction::Conditional {
2708            condition: crate::circuit::ClassicalCondition::BitIsOne(0),
2709            gate: Gate::T,
2710            targets: smallvec![1],
2711        });
2712
2713        for circuit in [&measured, &reset, &conditional] {
2714            assert_pauli_marginals_reject(circuit);
2715        }
2716    }
2717
2718    #[test]
2719    fn test_noise_rejects_stabilizer_rank() {
2720        let circuit = make_general_circuit();
2721        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2722        assert!(matches!(
2723            run_shots_with_noise(BackendKind::StabilizerRank, &circuit, &nm, 10, 42).unwrap_err(),
2724            crate::error::PrismError::IncompatibleBackend { .. }
2725        ));
2726    }
2727
2728    #[test]
2729    fn test_noise_rejects_pauli_backends() {
2730        let circuit = make_general_circuit();
2731        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2732
2733        assert!(matches!(
2734            run_shots_with_noise(
2735                BackendKind::StochasticPauli { num_samples: 100 },
2736                &circuit,
2737                &nm,
2738                10,
2739                42,
2740            )
2741            .unwrap_err(),
2742            crate::error::PrismError::IncompatibleBackend { .. }
2743        ));
2744        assert!(matches!(
2745            run_shots_with_noise(
2746                BackendKind::DeterministicPauli {
2747                    epsilon: 1e-3,
2748                    max_terms: 1000
2749                },
2750                &circuit,
2751                &nm,
2752                10,
2753                42,
2754            )
2755            .unwrap_err(),
2756            crate::error::PrismError::IncompatibleBackend { .. }
2757        ));
2758    }
2759
2760    #[test]
2761    fn test_noise_stabilizer_rejects_non_pauli_noise() {
2762        let circuit = make_clifford_circuit();
2763        let nm = noise::NoiseModel {
2764            after_gate: {
2765                let mut ag = vec![Vec::new(); circuit.instructions.len()];
2766                ag[0].push(noise::NoiseEvent {
2767                    channel: noise::NoiseChannel::AmplitudeDamping { gamma: 0.1 },
2768                    qubits: smallvec![0],
2769                });
2770                ag
2771            },
2772            readout: vec![None; circuit.num_qubits],
2773        };
2774        let err = run_shots_with_noise(BackendKind::Stabilizer, &circuit, &nm, 10, 42).unwrap_err();
2775        match err {
2776            crate::error::PrismError::IncompatibleBackend { reason, .. } => {
2777                assert!(reason.contains("Statevector"));
2778                assert!(reason.contains("Sparse"));
2779                assert!(reason.contains("Factored"));
2780            }
2781            other => panic!("expected IncompatibleBackend, got {other:?}"),
2782        }
2783    }
2784
2785    #[test]
2786    fn test_noise_auto_general_noise_avoids_stabilizer_dispatch() {
2787        let circuit = make_clifford_circuit();
2788        let nm = noise::NoiseModel::with_amplitude_damping(&circuit, 0.1);
2789        let result = run_shots_with_noise(BackendKind::Auto, &circuit, &nm, 16, 42);
2790        assert!(result.is_ok());
2791    }
2792
2793    #[cfg(feature = "gpu")]
2794    #[test]
2795    fn test_noise_stabilizer_gpu_rejects_non_pauli_noise() {
2796        let circuit = make_clifford_circuit();
2797        let nm = noise::NoiseModel {
2798            after_gate: {
2799                let mut ag = vec![Vec::new(); circuit.instructions.len()];
2800                ag[0].push(noise::NoiseEvent {
2801                    channel: noise::NoiseChannel::AmplitudeDamping { gamma: 0.1 },
2802                    qubits: smallvec![0],
2803                });
2804                ag
2805            },
2806            readout: vec![None; circuit.num_qubits],
2807        };
2808        assert!(matches!(
2809            run_shots_with_noise(
2810                BackendKind::StabilizerGpu {
2811                    context: crate::gpu::GpuContext::stub_for_tests(),
2812                },
2813                &circuit,
2814                &nm,
2815                10,
2816                42,
2817            )
2818            .unwrap_err(),
2819            crate::error::PrismError::IncompatibleBackend { .. }
2820        ));
2821    }
2822
2823    #[test]
2824    fn test_noise_stabilizer_rejects_non_clifford() {
2825        let circuit = make_general_circuit();
2826        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2827        assert!(matches!(
2828            run_shots_with_noise(BackendKind::Stabilizer, &circuit, &nm, 10, 42).unwrap_err(),
2829            crate::error::PrismError::IncompatibleBackend { .. }
2830        ));
2831    }
2832
2833    #[cfg(feature = "gpu")]
2834    #[test]
2835    fn test_noise_stabilizer_gpu_rejects_non_clifford() {
2836        let circuit = make_general_circuit();
2837        let nm = noise::NoiseModel::uniform_depolarizing(&circuit, 0.01);
2838        assert!(matches!(
2839            run_shots_with_noise(
2840                BackendKind::StabilizerGpu {
2841                    context: crate::gpu::GpuContext::stub_for_tests(),
2842                },
2843                &circuit,
2844                &nm,
2845                10,
2846                42,
2847            )
2848            .unwrap_err(),
2849            crate::error::PrismError::IncompatibleBackend { .. }
2850        ));
2851    }
2852
2853    // ── Backend smoke tests ─────────────────────────────────────────────
2854
2855    fn assert_probs_match(kind: BackendKind, circuit: &Circuit, expected: &[f64], tol: f64) {
2856        let label = format!("{kind:?}");
2857        let result = run_with(kind, circuit, 42).unwrap();
2858        let probs = result.probabilities.unwrap().to_vec();
2859        assert_eq!(probs.len(), expected.len(), "{label}: length mismatch");
2860        for (i, (a, b)) in probs.iter().zip(expected.iter()).enumerate() {
2861            assert!(
2862                (a - b).abs() < tol,
2863                "{label}: prob[{i}] = {a}, expected {b}"
2864            );
2865        }
2866    }
2867
2868    #[test]
2869    fn test_smoke_all_backends_clifford() {
2870        let circuit = make_clifford_circuit();
2871        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2872            .unwrap()
2873            .probabilities
2874            .unwrap()
2875            .to_vec();
2876
2877        for kind in [
2878            BackendKind::Stabilizer,
2879            BackendKind::FactoredStabilizer,
2880            BackendKind::Sparse,
2881            BackendKind::Mps { max_bond_dim: 64 },
2882            BackendKind::TensorNetwork,
2883            BackendKind::Factored,
2884        ] {
2885            assert_probs_match(kind, &circuit, &sv_probs, 1e-8);
2886        }
2887    }
2888
2889    #[test]
2890    fn test_smoke_all_backends_general() {
2891        let circuit = make_general_circuit();
2892        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2893            .unwrap()
2894            .probabilities
2895            .unwrap()
2896            .to_vec();
2897
2898        for kind in [
2899            BackendKind::Sparse,
2900            BackendKind::Mps { max_bond_dim: 64 },
2901            BackendKind::TensorNetwork,
2902            BackendKind::Factored,
2903        ] {
2904            assert_probs_match(kind, &circuit, &sv_probs, 1e-8);
2905        }
2906    }
2907
2908    #[test]
2909    fn test_smoke_product_state() {
2910        let circuit = make_product_circuit();
2911        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2912            .unwrap()
2913            .probabilities
2914            .unwrap()
2915            .to_vec();
2916        assert_probs_match(BackendKind::ProductState, &circuit, &sv_probs, 1e-8);
2917    }
2918
2919    #[test]
2920    fn test_smoke_stabilizer_rank() {
2921        let mut circuit = Circuit::new(3, 0);
2922        circuit.add_gate(Gate::H, &[0]);
2923        circuit.add_gate(Gate::T, &[0]);
2924        circuit.add_gate(Gate::Cx, &[0, 1]);
2925        circuit.add_gate(Gate::H, &[2]);
2926        circuit.add_gate(Gate::T, &[2]);
2927
2928        let sv_probs = run_with(BackendKind::Statevector, &circuit, 42)
2929            .unwrap()
2930            .probabilities
2931            .unwrap()
2932            .to_vec();
2933        assert_probs_match(BackendKind::StabilizerRank, &circuit, &sv_probs, 1e-6);
2934    }
2935}