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