1pub 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#[derive(Debug, Clone)]
68pub struct RunOutcome {
69 pub classical_bits: Vec<bool>,
72 pub probabilities: Option<Probabilities>,
78}
79
80#[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#[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
113pub 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 #[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
246fn 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
257fn 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#[cfg(test)]
283fn run(circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
284 run_with(BackendKind::Auto, circuit, seed)
285}
286
287pub(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 #[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
390pub fn run_on(backend: &mut dyn Backend, circuit: &Circuit) -> Result<RunOutcome> {
395 execute(backend, circuit, &SimOptions::default())
396}
397
398pub 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#[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#[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
559pub(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#[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#[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#[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 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
786pub(crate) fn run_shots_with(
788 kind: BackendKind,
789 circuit: &Circuit,
790 num_shots: usize,
791 seed: u64,
792) -> Result<ShotsResult> {
793 #[cfg(feature = "distributed")]
797 if let BackendKind::StatevectorDistributed { context } = &kind {
798 return run_shots_distributed(context.clone(), circuit, num_shots, seed);
799 }
800
801 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 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
973fn 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 #[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(); 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(); 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 let mut c = Circuit::new(10, 0);
1429
1430 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 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 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 let mut c = Circuit::new(3, 0);
1472
1473 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 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 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]); 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 let mut c = Circuit::new(6, 0);
1548
1549 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 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 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 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 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 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 #[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 #[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 #[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 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}