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