1use crate::backend::density_matrix::DensityMatrixBackend;
7use crate::backend::mps::MpsBackend;
8use crate::backend::product::ProductStateBackend;
9use crate::backend::sparse::{MAX_SPARSE_INDEX_QUBITS, SparseBackend};
10use crate::backend::stabilizer::StabilizerBackend;
11use crate::backend::statevector::StatevectorBackend;
12use crate::backend::tensornetwork::TensorNetworkBackend;
13use crate::backend::{
14 Backend, DM_QUBIT_CAP_ENV, check_state_allocation, max_density_matrix_qubits,
15 max_statevector_qubits,
16};
17use crate::circuit::{Circuit, Instruction};
18use crate::error::{PrismError, Result};
19
20#[cfg(any(feature = "gpu", feature = "distributed"))]
21use std::sync::Arc;
22
23#[cfg(feature = "gpu")]
24use crate::gpu::GpuContext;
25
26#[cfg(feature = "distributed")]
27use crate::backend::distributed_statevector::DistributedStatevectorBackend;
28#[cfg(feature = "distributed")]
29use crate::distributed::DistributedContext;
30
31use super::metadata::ResolvedBackend;
32use super::{RunOutcome, try_backend_probabilities};
33
34pub(super) const AUTO_MPS_BOND_DIM: usize = 256;
35
36pub(super) const MAX_AUTO_T_COUNT_EXACT: usize = 18;
37
38pub(super) const MAX_AUTO_T_COUNT_SHOTS: usize = 40;
39
40pub(super) const MAX_STABILIZER_RANK_QUBITS: usize = 25;
41
42pub(super) const MIN_QUBITS_FOR_SPD_AUTO: usize = 12;
43
44pub(super) const AUTO_SPD_MAX_TERMS: usize = 65536;
45
46pub(super) const MIN_FACTORED_STABILIZER_QUBITS: usize = 128;
47
48pub(super) const MIN_BLOCK_FOR_FACTORED_STAB: usize = 16;
49
50#[inline]
51pub(super) fn stabilizer_rank_budget(num_qubits: usize) -> usize {
52 let log2n = if num_qubits >= 2 {
53 (num_qubits as f64).log2().ceil() as usize * 2
54 } else {
55 0
56 };
57 num_qubits.saturating_sub(log2n)
58}
59
60#[derive(Debug, Clone)]
88pub enum BackendKind {
89 Auto,
90 Statevector,
91 Stabilizer,
93 Sparse,
95 Mps {
97 max_bond_dim: usize,
98 },
99 ProductState,
101 TensorNetwork,
103 Factored,
105 StabilizerRank,
107 FactoredStabilizer,
109 DensityMatrix,
122 StochasticPauli {
125 num_samples: usize,
126 },
127 DeterministicPauli {
131 epsilon: f64,
132 max_terms: usize,
133 },
134 PauliPath {
147 epsilon: f64,
148 max_terms: usize,
149 },
150 #[cfg(feature = "gpu")]
168 AutoGpu {
169 context: Arc<GpuContext>,
170 },
171 #[cfg(feature = "gpu")]
185 StatevectorGpu {
186 context: Arc<GpuContext>,
187 },
188 #[cfg(feature = "gpu")]
199 DensityMatrixGpu {
200 context: Arc<GpuContext>,
201 },
202 #[cfg(feature = "gpu")]
216 StabilizerGpu {
217 context: Arc<GpuContext>,
218 },
219 #[cfg(feature = "distributed")]
226 StatevectorDistributed {
227 context: Arc<DistributedContext>,
228 },
229}
230
231impl BackendKind {
232 #[inline]
238 pub(crate) fn is_auto(&self) -> bool {
239 match self {
240 BackendKind::Auto => true,
241 #[cfg(feature = "gpu")]
242 BackendKind::AutoGpu { .. } => true,
243 _ => false,
244 }
245 }
246
247 pub fn supports_noisy_per_shot(&self) -> bool {
253 !matches!(
254 self,
255 BackendKind::StabilizerRank
256 | BackendKind::StochasticPauli { .. }
257 | BackendKind::DeterministicPauli { .. }
258 | BackendKind::PauliPath { .. }
259 ) && !self.is_density_matrix()
260 }
261
262 pub(crate) fn is_density_matrix(&self) -> bool {
264 match self {
265 BackendKind::DensityMatrix => true,
266 #[cfg(feature = "gpu")]
267 BackendKind::DensityMatrixGpu { .. } => true,
268 _ => false,
269 }
270 }
271
272 pub fn supports_general_noise(&self) -> bool {
276 match self {
277 BackendKind::Auto
278 | BackendKind::Statevector
279 | BackendKind::Sparse
280 | BackendKind::Mps { .. }
281 | BackendKind::ProductState
282 | BackendKind::Factored
283 | BackendKind::TensorNetwork
284 | BackendKind::DensityMatrix => true,
285 #[cfg(feature = "gpu")]
286 BackendKind::AutoGpu { .. }
287 | BackendKind::StatevectorGpu { .. }
288 | BackendKind::DensityMatrixGpu { .. } => true,
289 _ => false,
290 }
291 }
292
293 pub(crate) fn is_stabilizer_family(&self) -> bool {
294 matches!(
295 self,
296 BackendKind::Stabilizer | BackendKind::FactoredStabilizer
297 ) || {
298 #[cfg(feature = "gpu")]
299 {
300 matches!(self, BackendKind::StabilizerGpu { .. })
301 }
302 #[cfg(not(feature = "gpu"))]
303 {
304 false
305 }
306 }
307 }
308
309 pub(crate) fn general_noise_backend_names() -> &'static str {
310 #[cfg(feature = "gpu")]
311 {
312 "Auto, Statevector, StatevectorGpu, Sparse, Mps, ProductState, Factored, TensorNetwork, DensityMatrix, or DensityMatrixGpu"
313 }
314 #[cfg(not(feature = "gpu"))]
315 {
316 "Auto, Statevector, Sparse, Mps, ProductState, Factored, TensorNetwork, or DensityMatrix"
317 }
318 }
319}
320
321pub(super) fn validate_explicit_backend(kind: &BackendKind, circuit: &Circuit) -> Result<()> {
322 if kind.is_stabilizer_family() && !circuit.is_clifford_only() {
323 return Err(PrismError::IncompatibleBackend {
324 backend: "stabilizer".into(),
325 reason: "circuit contains non-Clifford gates".into(),
326 });
327 }
328 match kind {
329 BackendKind::ProductState if circuit.has_entangling_gates() => {
330 return Err(PrismError::IncompatibleBackend {
331 backend: "productstate".into(),
332 reason: "circuit contains entangling gates".into(),
333 });
334 }
335 BackendKind::StabilizerRank if !circuit.has_t_gates() => {
336 return Err(PrismError::IncompatibleBackend {
337 backend: "stabilizer_rank".into(),
338 reason: "circuit has no T gates; use Stabilizer instead".into(),
339 });
340 }
341 BackendKind::DensityMatrix => {
342 check_state_allocation(
343 "density_matrix",
344 circuit.num_qubits,
345 max_density_matrix_qubits(),
346 DM_QUBIT_CAP_ENV,
347 )?;
348 }
349 _ => {}
350 }
351 Ok(())
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub(super) enum Family {
358 ProductState,
359 Stabilizer,
360 Sparse,
361 Mps,
362 Factored,
363 FactoredStabilizer,
364 TensorNetwork,
365 Statevector,
366 DensityMatrix,
367}
368
369fn select_auto_backend_choice(circuit: &Circuit, has_partial_independence: bool) -> Family {
370 if !circuit.has_entangling_gates() {
371 Family::ProductState
372 } else if circuit.is_clifford_only() {
373 Family::Stabilizer
374 } else if circuit.num_qubits > max_statevector_qubits() {
375 if circuit.is_sparse_friendly() && circuit.num_qubits <= MAX_SPARSE_INDEX_QUBITS {
376 Family::Sparse
377 } else {
378 Family::Mps
379 }
380 } else if has_partial_independence {
381 Family::Factored
382 } else {
383 Family::Statevector
384 }
385}
386
387pub(super) fn auto_selects_cpu_statevector(
388 circuit: &Circuit,
389 has_partial_independence: bool,
390) -> bool {
391 matches!(
392 select_auto_backend_choice(circuit, has_partial_independence),
393 Family::Statevector
394 )
395}
396
397#[derive(Clone)]
401pub(super) enum Accel {
402 Cpu,
403 #[cfg(feature = "gpu")]
404 Gpu {
405 context: Arc<GpuContext>,
406 soft: bool,
407 },
408}
409
410#[cfg(feature = "gpu")]
415struct GpuCapability {
416 min_qubits: fn() -> usize,
417 fits: fn(&Arc<GpuContext>, usize) -> bool,
418}
419
420#[cfg(feature = "gpu")]
423const GPU_CAPABLE_FAMILIES: [Family; 3] = [
424 Family::Statevector,
425 Family::Stabilizer,
426 Family::DensityMatrix,
427];
428
429#[cfg(feature = "gpu")]
430fn gpu_capability(family: Family) -> Option<GpuCapability> {
431 match family {
432 Family::Statevector => Some(GpuCapability {
433 min_qubits: crate::gpu::min_qubits,
434 fits: |ctx, n| ctx.fits_statevector_with_scratch(n).unwrap_or(false),
435 }),
436 Family::Stabilizer => Some(GpuCapability {
437 min_qubits: crate::gpu::stabilizer_min_qubits,
438 fits: |ctx, n| ctx.fits_tableau(n).unwrap_or(false),
439 }),
440 Family::DensityMatrix => Some(GpuCapability {
444 min_qubits: || 0,
445 fits: |ctx, n| ctx.fits_statevector_with_scratch(2 * n).unwrap_or(false),
446 }),
447 Family::ProductState
448 | Family::Sparse
449 | Family::Mps
450 | Family::Factored
451 | Family::FactoredStabilizer
452 | Family::TensorNetwork => None,
453 }
454}
455
456#[cfg(feature = "gpu")]
469pub(super) fn accel_for(kind: &BackendKind, family: Family, num_qubits: usize) -> Accel {
470 let Some((context, soft)) = gpu_request(kind, family) else {
471 return Accel::Cpu;
472 };
473 let Some(cap) = gpu_capability(family) else {
474 return Accel::Cpu;
475 };
476 if num_qubits < (cap.min_qubits)() {
477 return Accel::Cpu;
478 }
479 if soft && !(cap.fits)(context, num_qubits) {
480 return Accel::Cpu;
481 }
482 Accel::Gpu {
483 context: context.clone(),
484 soft,
485 }
486}
487
488#[cfg(feature = "gpu")]
492fn gpu_request(kind: &BackendKind, family: Family) -> Option<(&Arc<GpuContext>, bool)> {
493 match (kind, family) {
494 (BackendKind::AutoGpu { context }, _) => Some((context, true)),
495 (BackendKind::StatevectorGpu { context }, Family::Statevector) => Some((context, false)),
496 (BackendKind::StabilizerGpu { context }, Family::Stabilizer) => Some((context, false)),
497 (BackendKind::DensityMatrixGpu { context }, Family::DensityMatrix) => {
498 Some((context, false))
499 }
500 _ => None,
501 }
502}
503
504#[cfg(feature = "gpu")]
511pub(super) fn may_resolve_to_gpu(kind: &BackendKind, num_qubits: usize) -> bool {
512 GPU_CAPABLE_FAMILIES.into_iter().any(|family| {
513 gpu_request(kind, family).is_some()
514 && gpu_capability(family).is_some_and(|cap| num_qubits >= (cap.min_qubits)())
515 })
516}
517
518#[cfg(not(feature = "gpu"))]
519pub(super) fn accel_for(_kind: &BackendKind, _family: Family, _num_qubits: usize) -> Accel {
520 Accel::Cpu
521}
522
523pub(super) fn build_statevector(accel: &Accel, seed: u64) -> StatevectorBackend {
524 match accel {
525 Accel::Cpu => StatevectorBackend::new(seed),
526 #[cfg(feature = "gpu")]
527 Accel::Gpu {
528 context,
529 soft: true,
530 } => StatevectorBackend::new(seed).with_gpu_auto(context.clone()),
531 #[cfg(feature = "gpu")]
532 Accel::Gpu {
533 context,
534 soft: false,
535 } => StatevectorBackend::new(seed).with_gpu(context.clone()),
536 }
537}
538
539pub(super) fn build_density_matrix(accel: &Accel, seed: u64) -> DensityMatrixBackend {
540 match accel {
541 Accel::Cpu => DensityMatrixBackend::new(seed),
542 #[cfg(feature = "gpu")]
543 Accel::Gpu { context, .. } => DensityMatrixBackend::new(seed).with_gpu(context.clone()),
544 }
545}
546
547fn stabilizer_can_run_lazy(circuit: &Circuit) -> bool {
551 circuit.instructions.iter().all(|inst| {
552 matches!(
553 inst,
554 Instruction::Gate { .. }
555 | Instruction::Conditional { .. }
556 | Instruction::Barrier { .. }
557 )
558 })
559}
560
561fn build_stabilizer(accel: &Accel, lazy: bool, seed: u64) -> StabilizerBackend {
562 let stab = if lazy {
570 StabilizerBackend::new_lazy(seed)
571 } else {
572 StabilizerBackend::new(seed)
573 };
574 match accel {
575 Accel::Cpu => stab,
576 #[cfg(feature = "gpu")]
577 Accel::Gpu {
578 context,
579 soft: true,
580 } => stab.with_gpu_auto(context.clone()),
581 #[cfg(feature = "gpu")]
582 Accel::Gpu {
583 context,
584 soft: false,
585 } => stab.with_gpu(context.clone()),
586 }
587}
588
589#[derive(Clone)]
594pub(super) enum BackendPlan {
595 ProductState,
596 Sparse,
597 TensorNetwork,
598 Factored,
599 FactoredStabilizer,
600 Mps {
601 max_bond_dim: usize,
602 },
603 Stabilizer {
604 accel: Accel,
605 lazy: bool,
606 },
607 Statevector {
608 accel: Accel,
609 },
610 DensityMatrix {
611 accel: Accel,
612 },
613 #[cfg(feature = "distributed")]
614 Distributed(Arc<DistributedContext>),
615}
616
617impl BackendPlan {
618 pub(super) fn resolved(&self) -> ResolvedBackend {
621 match self {
622 BackendPlan::ProductState => ResolvedBackend::ProductState,
623 BackendPlan::Sparse => ResolvedBackend::Sparse,
624 BackendPlan::TensorNetwork => ResolvedBackend::TensorNetwork,
625 BackendPlan::Factored => ResolvedBackend::Factored,
626 BackendPlan::FactoredStabilizer => ResolvedBackend::FactoredStabilizer,
627 BackendPlan::Mps { .. } => ResolvedBackend::Mps,
628 BackendPlan::Stabilizer { .. } => ResolvedBackend::Stabilizer,
629 BackendPlan::Statevector { .. } => ResolvedBackend::Statevector,
630 BackendPlan::DensityMatrix { .. } => ResolvedBackend::DensityMatrix,
631 #[cfg(feature = "distributed")]
632 BackendPlan::Distributed(_) => ResolvedBackend::Distributed,
633 }
634 }
635
636 pub(super) fn build(&self, seed: u64) -> Box<dyn Backend + Send> {
637 match self {
638 BackendPlan::ProductState => Box::new(ProductStateBackend::new(seed)),
639 BackendPlan::Sparse => Box::new(SparseBackend::new(seed)),
640 BackendPlan::TensorNetwork => Box::new(TensorNetworkBackend::new(seed)),
641 BackendPlan::Factored => Box::new(crate::backend::factored::FactoredBackend::new(seed)),
642 BackendPlan::FactoredStabilizer => {
643 Box::new(crate::backend::factored_stabilizer::FactoredStabilizerBackend::new(seed))
644 }
645 BackendPlan::Mps { max_bond_dim } => Box::new(MpsBackend::new(seed, *max_bond_dim)),
646 BackendPlan::Stabilizer { accel, lazy } => {
647 Box::new(build_stabilizer(accel, *lazy, seed))
648 }
649 BackendPlan::Statevector { accel } => Box::new(build_statevector(accel, seed)),
650 BackendPlan::DensityMatrix { accel } => Box::new(build_density_matrix(accel, seed)),
651 #[cfg(feature = "distributed")]
652 BackendPlan::Distributed(context) => {
653 Box::new(DistributedStatevectorBackend::new(context.clone(), seed))
654 }
655 }
656 }
657
658 pub(super) fn is_gpu(&self) -> bool {
659 match self {
660 BackendPlan::Stabilizer { accel, .. }
661 | BackendPlan::Statevector { accel }
662 | BackendPlan::DensityMatrix { accel } => !matches!(accel, Accel::Cpu),
663 _ => false,
664 }
665 }
666
667 #[cfg(test)]
668 pub(super) fn family(&self) -> Family {
669 match self {
670 BackendPlan::ProductState => Family::ProductState,
671 BackendPlan::Sparse => Family::Sparse,
672 BackendPlan::TensorNetwork => Family::TensorNetwork,
673 BackendPlan::Factored => Family::Factored,
674 BackendPlan::FactoredStabilizer => Family::FactoredStabilizer,
675 BackendPlan::Mps { .. } => Family::Mps,
676 BackendPlan::Stabilizer { .. } => Family::Stabilizer,
677 BackendPlan::Statevector { .. } => Family::Statevector,
678 BackendPlan::DensityMatrix { .. } => Family::DensityMatrix,
679 #[cfg(feature = "distributed")]
680 BackendPlan::Distributed(_) => Family::Statevector,
681 }
682 }
683
684 #[cfg(test)]
685 pub(super) fn accel(&self) -> &Accel {
686 match self {
687 BackendPlan::Stabilizer { accel, .. }
688 | BackendPlan::Statevector { accel }
689 | BackendPlan::DensityMatrix { accel } => accel,
690 _ => &Accel::Cpu,
691 }
692 }
693}
694
695pub(super) enum ExecutionPlan {
700 Backend(BackendPlan),
701 StabilizerRank,
702 StochasticPauli { num_samples: usize },
703 DeterministicPauli { epsilon: f64, max_terms: usize },
704 PauliPath,
705}
706
707pub(super) fn approximate_route_name(
716 kind: &BackendKind,
717 circuit: &Circuit,
718) -> Option<&'static str> {
719 match kind {
720 BackendKind::Mps { .. } => return Some("Mps"),
721 BackendKind::StochasticPauli { .. } => return Some("StochasticPauli"),
722 BackendKind::DeterministicPauli { epsilon, max_terms } => {
723 if *epsilon > 0.0 || *max_terms > 0 {
724 return Some("DeterministicPauli");
725 }
726 return None;
727 }
728 BackendKind::PauliPath { epsilon, max_terms } => {
729 if *epsilon > 0.0 || *max_terms > 0 {
730 return Some("PauliPath");
731 }
732 return None;
733 }
734 _ => {}
735 }
736 if !kind.is_auto() {
737 return None;
738 }
739 let (_, has_partial_independence) = crate::sim::analyze_independence(circuit);
740 match select_auto_backend_choice(circuit, has_partial_independence) {
741 Family::Mps => Some("Mps"),
742 _ => None,
743 }
744}
745
746pub(super) fn plan_for_family(
747 kind: &BackendKind,
748 family: Family,
749 num_qubits: usize,
750) -> BackendPlan {
751 match family {
752 Family::ProductState => BackendPlan::ProductState,
753 Family::Sparse => BackendPlan::Sparse,
754 Family::TensorNetwork => BackendPlan::TensorNetwork,
755 Family::Factored => BackendPlan::Factored,
756 Family::FactoredStabilizer => BackendPlan::FactoredStabilizer,
757 Family::Mps => BackendPlan::Mps {
758 max_bond_dim: AUTO_MPS_BOND_DIM,
759 },
760 Family::Stabilizer => BackendPlan::Stabilizer {
761 accel: accel_for(kind, Family::Stabilizer, num_qubits),
762 lazy: false,
763 },
764 Family::Statevector => BackendPlan::Statevector {
765 accel: accel_for(kind, Family::Statevector, num_qubits),
766 },
767 Family::DensityMatrix => BackendPlan::DensityMatrix {
768 accel: accel_for(kind, Family::DensityMatrix, num_qubits),
769 },
770 }
771}
772
773pub(super) fn resolve(
779 kind: &BackendKind,
780 circuit: &Circuit,
781 has_partial_independence: bool,
782) -> ExecutionPlan {
783 let family = match kind {
784 BackendKind::Auto => select_auto_backend_choice(circuit, has_partial_independence),
785 #[cfg(feature = "gpu")]
786 BackendKind::AutoGpu { .. } => {
787 select_auto_backend_choice(circuit, has_partial_independence)
788 }
789 BackendKind::Statevector => Family::Statevector,
790 BackendKind::Stabilizer => Family::Stabilizer,
791 BackendKind::Sparse => Family::Sparse,
792 BackendKind::Mps { max_bond_dim } => {
793 return ExecutionPlan::Backend(BackendPlan::Mps {
794 max_bond_dim: *max_bond_dim,
795 });
796 }
797 BackendKind::ProductState => Family::ProductState,
798 BackendKind::TensorNetwork => Family::TensorNetwork,
799 BackendKind::Factored => Family::Factored,
800 BackendKind::FactoredStabilizer => Family::FactoredStabilizer,
801 BackendKind::DensityMatrix => Family::DensityMatrix,
802 BackendKind::StabilizerRank => return ExecutionPlan::StabilizerRank,
803 BackendKind::StochasticPauli { num_samples } => {
804 return ExecutionPlan::StochasticPauli {
805 num_samples: *num_samples,
806 };
807 }
808 BackendKind::PauliPath { .. } => return ExecutionPlan::PauliPath,
809 BackendKind::DeterministicPauli { epsilon, max_terms } => {
810 return ExecutionPlan::DeterministicPauli {
811 epsilon: *epsilon,
812 max_terms: *max_terms,
813 };
814 }
815 #[cfg(feature = "gpu")]
816 BackendKind::StatevectorGpu { .. } => Family::Statevector,
817 #[cfg(feature = "gpu")]
818 BackendKind::StabilizerGpu { .. } => Family::Stabilizer,
819 #[cfg(feature = "gpu")]
820 BackendKind::DensityMatrixGpu { .. } => Family::DensityMatrix,
821 #[cfg(feature = "distributed")]
822 BackendKind::StatevectorDistributed { context } => {
823 return ExecutionPlan::Backend(BackendPlan::Distributed(context.clone()));
824 }
825 };
826 let mut plan = plan_for_family(kind, family, circuit.num_qubits);
827 if let BackendPlan::Stabilizer { lazy, .. } = &mut plan {
828 *lazy = stabilizer_can_run_lazy(circuit);
829 }
830 ExecutionPlan::Backend(plan)
831}
832
833pub(super) fn initial_state_plan(kind: &BackendKind, num_qubits: usize) -> Result<BackendPlan> {
847 match kind {
848 BackendKind::Auto | BackendKind::Statevector => {
849 Ok(plan_for_family(kind, Family::Statevector, num_qubits))
850 }
851 #[cfg(feature = "gpu")]
852 BackendKind::AutoGpu { .. } | BackendKind::StatevectorGpu { .. } => {
853 Ok(plan_for_family(kind, Family::Statevector, num_qubits))
854 }
855 BackendKind::DensityMatrix => Ok(plan_for_family(kind, Family::DensityMatrix, num_qubits)),
856 #[cfg(feature = "gpu")]
857 BackendKind::DensityMatrixGpu { .. } => {
858 Ok(plan_for_family(kind, Family::DensityMatrix, num_qubits))
859 }
860 #[cfg(feature = "distributed")]
861 BackendKind::StatevectorDistributed { context } => {
862 Ok(BackendPlan::Distributed(context.clone()))
863 }
864 other => Err(PrismError::IncompatibleBackend {
865 backend: format!("{other:?}"),
866 reason: "a start state other than |0...0> runs on the statevector or the density \
867 matrix; every other representation is derived from that start"
868 .into(),
869 }),
870 }
871}
872
873pub(super) fn resolve_backend(
874 kind: &BackendKind,
875 circuit: &Circuit,
876 has_partial_independence: bool,
877) -> BackendPlan {
878 match resolve(kind, circuit, has_partial_independence) {
879 ExecutionPlan::Backend(plan) => plan,
880 _ => unreachable!("non-backend dispatch should be handled by caller"),
881 }
882}
883
884#[inline]
885pub(super) fn min_clifford_prefix_gates(num_qubits: usize) -> usize {
886 (num_qubits * 2).max(16)
887}
888
889pub(super) fn has_temporal_clifford_opportunity(kind: &BackendKind, circuit: &Circuit) -> bool {
890 if !kind.is_auto() {
891 return false;
892 }
893 if circuit.num_qubits > max_statevector_qubits() {
894 return false;
895 }
896 if circuit.is_clifford_only() {
900 return false;
901 }
902 let min_gates = min_clifford_prefix_gates(circuit.num_qubits);
903 let mut prefix_gates = 0;
904 for inst in &circuit.instructions {
905 match inst {
906 Instruction::Gate { gate, .. } => {
907 if !gate.is_clifford() {
908 break;
909 }
910 prefix_gates += 1;
911 }
912 Instruction::Measure { .. }
913 | Instruction::Reset { .. }
914 | Instruction::Conditional { .. }
915 | Instruction::Region(_) => break,
916 Instruction::Barrier { .. } => {}
917 }
918 }
919 prefix_gates >= min_gates && prefix_gates < circuit.instructions.len()
920}
921
922pub(super) struct TemporalCliffordPlan {
929 prefix: Circuit,
930 fused_tail: Circuit,
931 tail_num_classical_bits: usize,
932 tail_accel: Accel,
933}
934
935pub(super) fn plan_temporal_clifford(
936 kind: &BackendKind,
937 circuit: &Circuit,
938) -> Option<TemporalCliffordPlan> {
939 if !kind.is_auto() {
940 return None;
941 }
942 if circuit.num_qubits > max_statevector_qubits() {
943 return None;
944 }
945 if circuit.is_clifford_only() {
946 return None;
947 }
948 let (prefix, tail) = circuit.clifford_prefix_split()?;
949 if prefix.gate_count() < min_clifford_prefix_gates(circuit.num_qubits) {
950 return None;
951 }
952 let tail_accel = accel_for(kind, Family::Statevector, circuit.num_qubits);
953 let tail_num_classical_bits = tail.num_classical_bits;
954 let fused_tail = match &tail_accel {
955 Accel::Cpu => crate::circuit::fusion::fuse_circuit(&tail, true).into_owned(),
956 #[cfg(feature = "gpu")]
957 Accel::Gpu { .. } => {
958 let expanded = crate::circuit::expand_qft_blocks(&tail);
959 let expanded = crate::circuit::expand_pauli_rotations(&expanded).into_owned();
960 crate::circuit::fusion::fuse_circuit(&expanded, true).into_owned()
961 }
962 };
963 Some(TemporalCliffordPlan {
964 prefix,
965 fused_tail,
966 tail_num_classical_bits,
967 tail_accel,
968 })
969}
970
971pub(super) fn run_temporal_clifford(
972 plan: &TemporalCliffordPlan,
973 seed: u64,
974 want_probabilities: bool,
975) -> Result<RunOutcome> {
976 let mut stab = StabilizerBackend::new(seed);
977 stab.init(plan.prefix.num_qubits, plan.prefix.num_classical_bits)?;
978 stab.enable_lazy_destab();
979 for inst in &plan.prefix.instructions {
980 stab.apply(inst)?;
981 }
982
983 let state = stab.export_statevector()?;
984
985 let mut sv = build_statevector(&plan.tail_accel, seed);
986 sv.init_from_state(state, plan.tail_num_classical_bits)?;
987 for inst in &plan.fused_tail.instructions {
988 sv.apply(inst)?;
989 }
990
991 let probabilities = if want_probabilities {
992 try_backend_probabilities(&sv)?
993 } else {
994 None
995 };
996
997 Ok(RunOutcome {
998 classical_bits: sv.classical_results().to_vec(),
999 probabilities,
1000 metadata: crate::sim::backend_metadata(&sv),
1001 })
1002}
1003
1004#[cfg(all(test, feature = "gpu"))]
1005mod gpu_crossover_tests {
1006 use super::*;
1007 use crate::gates::Gate;
1008
1009 fn stub_kind() -> BackendKind {
1010 BackendKind::StatevectorGpu {
1011 context: GpuContext::stub_for_tests(),
1012 }
1013 }
1014
1015 fn run_query(kind: BackendKind, circuit: &Circuit, seed: u64) -> Result<RunOutcome> {
1016 crate::sim::simulate(circuit).backend(kind).seed(seed).run()
1017 }
1018
1019 #[test]
1023 fn builder_gpu_wraps_statevector_gpu_variant() {
1024 let ctx = GpuContext::stub_for_tests();
1025 let mut circuit = Circuit::new(4, 0);
1026 circuit.add_gate(Gate::H, &[0]);
1027 circuit.add_gate(Gate::Cx, &[0, 1]);
1028
1029 let direct = crate::sim::simulate(&circuit)
1030 .gpu(ctx.clone())
1031 .seed(42)
1032 .run()
1033 .expect("builder GPU shortcut must honor crossover and route to CPU");
1034 let manual = crate::sim::simulate(&circuit)
1035 .backend(stub_kind())
1036 .seed(42)
1037 .run()
1038 .expect("manual variant reference");
1039
1040 let dp = direct.probabilities.expect("direct probs").to_vec();
1041 let mp = manual.probabilities.expect("manual probs").to_vec();
1042 assert_eq!(dp, mp);
1043 }
1044
1045 #[test]
1051 fn small_circuit_routes_to_cpu() {
1052 let mut circuit = Circuit::new(4, 0);
1053 circuit.add_gate(Gate::H, &[0]);
1054 circuit.add_gate(Gate::Cx, &[0, 1]);
1055 circuit.add_gate(Gate::H, &[2]);
1056 circuit.add_gate(Gate::Cx, &[2, 3]);
1057
1058 let result = run_query(stub_kind(), &circuit, 42)
1059 .expect("stub context must not be touched for a 4q circuit");
1060 let probs = result
1061 .probabilities
1062 .expect("probabilities missing")
1063 .to_vec();
1064
1065 let mut expected = [0.0_f64; 16];
1066 expected[0b0000] = 0.25;
1067 expected[0b0011] = 0.25;
1068 expected[0b1100] = 0.25;
1069 expected[0b1111] = 0.25;
1070 for (i, (p, e)) in probs.iter().zip(&expected).enumerate() {
1071 assert!((p - e).abs() < 1e-10, "p[{i}] = {p}, expected {e}");
1072 }
1073 }
1074
1075 #[test]
1082 fn decomposable_16q_circuit_runs_per_block_on_cpu() {
1083 let circuit = crate::circuits::independent_bell_pairs(8);
1084 assert_eq!(circuit.num_qubits, 16);
1085
1086 let cpu = run_query(BackendKind::Statevector, &circuit, 42).expect("cpu baseline");
1087 let gpu = run_query(stub_kind(), &circuit, 42).expect("stub must stay out of the way");
1088
1089 let cpu_p = cpu.probabilities.expect("cpu probs").to_vec();
1090 let gpu_p = gpu.probabilities.expect("gpu probs").to_vec();
1091 assert_eq!(cpu_p.len(), gpu_p.len());
1092 for (i, (c, g)) in cpu_p.iter().zip(gpu_p.iter()).enumerate() {
1093 assert!(
1094 (c - g).abs() < 1e-10,
1095 "prob[{i}] cpu={c}, gpu={g}, diff={}",
1096 (c - g).abs()
1097 );
1098 }
1099 }
1100
1101 fn stabilizer_stub_kind() -> BackendKind {
1102 BackendKind::StabilizerGpu {
1103 context: GpuContext::stub_for_tests(),
1104 }
1105 }
1106
1107 #[test]
1111 fn stabilizer_gpu_small_circuit_routes_to_cpu() {
1112 let mut circuit = Circuit::new(4, 4);
1113 circuit.add_gate(Gate::H, &[0]);
1114 circuit.add_gate(Gate::Cx, &[0, 1]);
1115 circuit.add_gate(Gate::Cx, &[1, 2]);
1116 circuit.add_gate(Gate::Cx, &[2, 3]);
1117 circuit.add_measure(0, 0);
1118 circuit.add_measure(1, 1);
1119 circuit.add_measure(2, 2);
1120 circuit.add_measure(3, 3);
1121
1122 let cpu_run = run_query(BackendKind::Stabilizer, &circuit, 42).expect("cpu baseline");
1123 let gpu_run = run_query(stabilizer_stub_kind(), &circuit, 42)
1124 .expect("stub must stay out of the way for small circuits");
1125 assert_eq!(cpu_run.classical_bits, gpu_run.classical_bits);
1126 }
1127
1128 #[test]
1131 fn stabilizer_gpu_rejects_non_clifford_at_dispatch() {
1132 let mut circuit = Circuit::new(2, 0);
1133 circuit.add_gate(Gate::T, &[0]);
1134 let err = run_query(stabilizer_stub_kind(), &circuit, 42).unwrap_err();
1135 assert!(matches!(err, PrismError::IncompatibleBackend { .. }));
1136 }
1137
1138 fn auto_gpu_stub_kind() -> BackendKind {
1139 BackendKind::AutoGpu {
1140 context: GpuContext::stub_for_tests(),
1141 }
1142 }
1143
1144 fn assert_probs_match(a: &RunOutcome, b: &RunOutcome) {
1145 let ap = a.probabilities.as_ref().expect("probs a").to_vec();
1146 let bp = b.probabilities.as_ref().expect("probs b").to_vec();
1147 assert_eq!(ap.len(), bp.len());
1148 for (i, (x, y)) in ap.iter().zip(bp.iter()).enumerate() {
1149 assert!((x - y).abs() < 1e-10, "prob[{i}]: {x} vs {y}");
1150 }
1151 }
1152
1153 #[test]
1157 fn auto_gpu_small_clifford_routes_to_cpu() {
1158 let mut circuit = Circuit::new(4, 0);
1159 circuit.add_gate(Gate::H, &[0]);
1160 circuit.add_gate(Gate::Cx, &[0, 1]);
1161 circuit.add_gate(Gate::Cx, &[1, 2]);
1162 circuit.add_gate(Gate::Cx, &[2, 3]);
1163
1164 let cpu = run_query(BackendKind::Auto, &circuit, 42).expect("cpu auto baseline");
1165 let gpu = run_query(auto_gpu_stub_kind(), &circuit, 42)
1166 .expect("stub must stay out of the way below the stabilizer crossover");
1167 assert_probs_match(&cpu, &gpu);
1168 }
1169
1170 #[test]
1175 fn auto_gpu_decomposable_16q_runs_per_block_on_cpu() {
1176 let circuit = crate::circuits::independent_bell_pairs(8);
1177 assert_eq!(circuit.num_qubits, 16);
1178
1179 let cpu = run_query(BackendKind::Auto, &circuit, 42).expect("cpu auto baseline");
1180 let gpu = run_query(auto_gpu_stub_kind(), &circuit, 42).expect("stub stays out of the way");
1181 assert_probs_match(&cpu, &gpu);
1182 }
1183
1184 #[test]
1191 fn auto_gpu_large_block_falls_back_to_cpu_on_stub() {
1192 let mut circuit = Circuit::new(16, 0);
1193 for q in 0..16 {
1194 circuit.add_gate(Gate::Rx(0.3), &[q]);
1195 }
1196 for q in 0..15 {
1197 circuit.add_gate(Gate::Cx, &[q, q + 1]);
1198 }
1199
1200 let cpu = run_query(BackendKind::Auto, &circuit, 42).expect("cpu auto baseline");
1201 let gpu = run_query(auto_gpu_stub_kind(), &circuit, 42)
1202 .expect("stub VRAM query fails closed, so AutoGpu must fall back to CPU without error");
1203 assert_probs_match(&cpu, &gpu);
1204
1205 let explicit = BackendKind::StatevectorGpu {
1206 context: GpuContext::stub_for_tests(),
1207 };
1208 assert!(matches!(
1209 run_query(explicit, &circuit, 42).unwrap_err(),
1210 PrismError::BackendUnsupported { .. }
1211 ));
1212 }
1213}
1214
1215#[cfg(all(test, feature = "gpu"))]
1216mod accel_tests {
1217 use super::*;
1218
1219 fn stub() -> Arc<GpuContext> {
1220 GpuContext::stub_for_tests()
1221 }
1222
1223 fn is_cpu(accel: &Accel) -> bool {
1224 matches!(accel, Accel::Cpu)
1225 }
1226
1227 #[test]
1228 fn auto_gpu_statevector_below_crossover_is_cpu() {
1229 let kind = BackendKind::AutoGpu { context: stub() };
1230 let n = crate::gpu::min_qubits() - 1;
1231 assert!(is_cpu(&accel_for(&kind, Family::Statevector, n)));
1232 }
1233
1234 #[test]
1237 fn auto_gpu_statevector_fits_fails_closed_on_stub() {
1238 let kind = BackendKind::AutoGpu { context: stub() };
1239 let n = crate::gpu::min_qubits() + 2;
1240 assert!(is_cpu(&accel_for(&kind, Family::Statevector, n)));
1241 }
1242
1243 #[test]
1244 fn auto_gpu_cpu_only_families_stay_cpu() {
1245 let kind = BackendKind::AutoGpu { context: stub() };
1246 for family in [
1247 Family::ProductState,
1248 Family::Sparse,
1249 Family::Mps,
1250 Family::Factored,
1251 Family::FactoredStabilizer,
1252 Family::TensorNetwork,
1253 ] {
1254 assert!(is_cpu(&accel_for(&kind, family, 1 << 10)), "{family:?}");
1255 }
1256 }
1257
1258 #[test]
1259 fn explicit_statevector_gpu_is_hard_at_crossover_and_cpu_below() {
1260 let kind = BackendKind::StatevectorGpu { context: stub() };
1261 let n = crate::gpu::min_qubits();
1262 assert!(matches!(
1263 accel_for(&kind, Family::Statevector, n),
1264 Accel::Gpu { soft: false, .. }
1265 ));
1266 assert!(is_cpu(&accel_for(&kind, Family::Statevector, n - 1)));
1267 }
1268
1269 #[test]
1270 fn explicit_stabilizer_gpu_is_hard_at_crossover_and_cpu_below() {
1271 let kind = BackendKind::StabilizerGpu { context: stub() };
1272 let n = crate::gpu::stabilizer_min_qubits();
1273 assert!(matches!(
1274 accel_for(&kind, Family::Stabilizer, n),
1275 Accel::Gpu { soft: false, .. }
1276 ));
1277 assert!(is_cpu(&accel_for(&kind, Family::Stabilizer, n - 1)));
1278 }
1279
1280 #[test]
1283 fn explicit_kind_other_family_is_cpu() {
1284 let sv = BackendKind::StatevectorGpu { context: stub() };
1285 assert!(is_cpu(&accel_for(&sv, Family::Stabilizer, 1 << 20)));
1286 let stab = BackendKind::StabilizerGpu { context: stub() };
1287 assert!(is_cpu(&accel_for(&stab, Family::Statevector, 1 << 20)));
1288 }
1289
1290 #[test]
1291 fn cpu_kinds_are_cpu_everywhere() {
1292 for family in [Family::Statevector, Family::Stabilizer] {
1293 assert!(is_cpu(&accel_for(&BackendKind::Auto, family, 1 << 20)));
1294 assert!(is_cpu(&accel_for(
1295 &BackendKind::Statevector,
1296 family,
1297 1 << 20
1298 )));
1299 }
1300 }
1301
1302 #[test]
1305 fn init_from_state_soft_falls_back_to_host_on_stub() {
1306 use num_complex::Complex64;
1307 let mut sv = StatevectorBackend::new(42).with_gpu_auto(stub());
1308 let amp = Complex64::new(std::f64::consts::FRAC_1_SQRT_2, 0.0);
1309 let state = vec![amp, Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), amp];
1310 sv.init_from_state(state.clone(), 0).unwrap();
1311 let exported = sv.export_statevector().unwrap();
1312 for (e, s) in exported.iter().zip(&state) {
1313 assert!((e - s).norm() < 1e-12);
1314 }
1315 }
1316
1317 #[test]
1318 fn init_from_state_hard_errors_on_stub() {
1319 use num_complex::Complex64;
1320 let mut sv = StatevectorBackend::new(42).with_gpu(stub());
1321 let err = sv
1322 .init_from_state(vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], 0)
1323 .unwrap_err();
1324 assert!(matches!(err, PrismError::BackendUnsupported { .. }));
1325 }
1326}
1327
1328#[cfg(test)]
1333mod dispatch_matrix_tests {
1334 use super::*;
1335 use crate::gates::Gate;
1336
1337 fn product(n: usize) -> Circuit {
1338 let mut c = Circuit::new(n, 0);
1339 for q in 0..n {
1340 c.add_gate(Gate::Rx(0.3), &[q]);
1341 }
1342 c
1343 }
1344
1345 fn clifford(n: usize) -> Circuit {
1346 let mut c = Circuit::new(n, 0);
1347 c.add_gate(Gate::H, &[0]);
1348 for q in 0..n - 1 {
1349 c.add_gate(Gate::Cx, &[q, q + 1]);
1350 }
1351 c
1352 }
1353
1354 fn dense(n: usize) -> Circuit {
1355 let mut c = Circuit::new(n, 0);
1356 for q in 0..n {
1357 c.add_gate(Gate::Rx(0.3), &[q]);
1358 }
1359 for q in 0..n - 1 {
1360 c.add_gate(Gate::Cx, &[q, q + 1]);
1361 }
1362 c
1363 }
1364
1365 fn oversize_qubits() -> Option<usize> {
1368 let cap = max_statevector_qubits();
1369 if cap >= usize::BITS as usize {
1370 eprintln!(
1371 "SKIP: statevector qubit cap disabled on this host; skipping oversize checks"
1372 );
1373 return None;
1374 }
1375 Some(cap + 1)
1376 }
1377
1378 fn oversize_sparse(n: usize) -> Circuit {
1379 let mut c = Circuit::new(n, 0);
1380 for q in 0..n {
1381 c.add_gate(Gate::T, &[q]);
1382 }
1383 for q in 0..n - 1 {
1384 c.add_gate(Gate::Cx, &[q, q + 1]);
1385 }
1386 c
1387 }
1388
1389 fn oversize_dense(n: usize) -> Circuit {
1390 let mut c = Circuit::new(n, 0);
1391 for q in 0..n {
1392 c.add_gate(Gate::Rx(0.3), &[q]);
1393 }
1394 for q in 0..n - 1 {
1395 c.add_gate(Gate::Cx, &[q, q + 1]);
1396 }
1397 c
1398 }
1399
1400 fn resolved(kind: &BackendKind, circuit: &Circuit, hpi: bool) -> BackendPlan {
1401 match resolve(kind, circuit, hpi) {
1402 ExecutionPlan::Backend(plan) => plan,
1403 _ => panic!("expected a backend plan"),
1404 }
1405 }
1406
1407 fn assert_cpu_family(kind: &BackendKind, circuit: &Circuit, hpi: bool, family: Family) {
1408 let plan = resolved(kind, circuit, hpi);
1409 assert_eq!(plan.family(), family, "kind {kind:?}");
1410 assert!(
1411 matches!(plan.accel(), Accel::Cpu),
1412 "kind {kind:?} family {family:?} must resolve to the host"
1413 );
1414 }
1415
1416 fn auto_kinds() -> Vec<BackendKind> {
1417 #[cfg(feature = "gpu")]
1418 {
1419 vec![
1420 BackendKind::Auto,
1421 BackendKind::AutoGpu {
1422 context: GpuContext::stub_for_tests(),
1423 },
1424 ]
1425 }
1426 #[cfg(not(feature = "gpu"))]
1427 {
1428 vec![BackendKind::Auto]
1429 }
1430 }
1431
1432 #[test]
1436 fn auto_family_matrix() {
1437 let oversize = oversize_qubits();
1438 for kind in auto_kinds() {
1439 assert_cpu_family(&kind, &product(6), false, Family::ProductState);
1440 assert_cpu_family(&kind, &clifford(6), false, Family::Stabilizer);
1441 assert_cpu_family(&kind, &clifford(16), false, Family::Stabilizer);
1442 assert_cpu_family(&kind, &dense(8), false, Family::Statevector);
1443 assert_cpu_family(&kind, &dense(16), false, Family::Statevector);
1444 assert_cpu_family(&kind, &dense(8), true, Family::Factored);
1445 if let Some(n) = oversize {
1446 assert_cpu_family(&kind, &oversize_sparse(n), false, Family::Sparse);
1447 assert_cpu_family(&kind, &oversize_dense(n), false, Family::Mps);
1448 }
1449 }
1450 }
1451
1452 #[test]
1453 fn auto_oversize_mps_uses_auto_bond_dim() {
1454 let Some(n) = oversize_qubits() else { return };
1455 for kind in auto_kinds() {
1456 let plan = resolved(&kind, &oversize_dense(n), false);
1457 assert!(matches!(
1458 plan,
1459 BackendPlan::Mps {
1460 max_bond_dim: AUTO_MPS_BOND_DIM
1461 }
1462 ));
1463 }
1464 }
1465
1466 #[test]
1469 fn explicit_cpu_kind_matrix() {
1470 let circuit = dense(6);
1471 let cases = [
1472 (BackendKind::Statevector, Family::Statevector),
1473 (BackendKind::Stabilizer, Family::Stabilizer),
1474 (BackendKind::Sparse, Family::Sparse),
1475 (BackendKind::ProductState, Family::ProductState),
1476 (BackendKind::TensorNetwork, Family::TensorNetwork),
1477 (BackendKind::Factored, Family::Factored),
1478 (BackendKind::FactoredStabilizer, Family::FactoredStabilizer),
1479 ];
1480 for (kind, family) in cases {
1481 assert_cpu_family(&kind, &circuit, false, family);
1482 }
1483
1484 let plan = resolved(&BackendKind::Mps { max_bond_dim: 77 }, &circuit, false);
1485 assert!(matches!(plan, BackendPlan::Mps { max_bond_dim: 77 }));
1486 }
1487
1488 #[test]
1489 fn non_backend_kinds_resolve_to_their_engines() {
1490 let circuit = dense(6);
1491 assert!(matches!(
1492 resolve(&BackendKind::StabilizerRank, &circuit, false),
1493 ExecutionPlan::StabilizerRank
1494 ));
1495 assert!(matches!(
1496 resolve(
1497 &BackendKind::StochasticPauli { num_samples: 9 },
1498 &circuit,
1499 false
1500 ),
1501 ExecutionPlan::StochasticPauli { num_samples: 9 }
1502 ));
1503 assert!(matches!(
1504 resolve(
1505 &BackendKind::DeterministicPauli {
1506 epsilon: 0.5,
1507 max_terms: 3
1508 },
1509 &circuit,
1510 false
1511 ),
1512 ExecutionPlan::DeterministicPauli {
1513 epsilon: e,
1514 max_terms: 3
1515 } if e == 0.5
1516 ));
1517 }
1518
1519 #[test]
1520 fn density_matrix_is_explicit_only() {
1521 for circuit in [product(6), clifford(6), dense(6)] {
1523 for hpi in [false, true] {
1524 assert_ne!(
1525 resolved(&BackendKind::Auto, &circuit, hpi).family(),
1526 Family::DensityMatrix
1527 );
1528 }
1529 }
1530 let plan = resolved(&BackendKind::DensityMatrix, &dense(6), false);
1533 assert_eq!(plan.family(), Family::DensityMatrix);
1534 assert!(plan.build(42).supports_fused_gates());
1535 assert!(matches!(plan.accel(), Accel::Cpu));
1536 }
1537
1538 #[test]
1539 fn density_matrix_rejects_over_cap() {
1540 let cap = max_density_matrix_qubits();
1541 if cap >= usize::BITS as usize {
1542 eprintln!("SKIP: density-matrix cap disabled on this host");
1543 return;
1544 }
1545 let circuit = dense(cap + 1);
1546 let err = validate_explicit_backend(&BackendKind::DensityMatrix, &circuit).unwrap_err();
1547 assert!(matches!(err, PrismError::IncompatibleBackend { .. }));
1548 assert!(validate_explicit_backend(&BackendKind::DensityMatrix, &dense(cap)).is_ok());
1550 }
1551
1552 #[cfg(feature = "gpu")]
1556 #[test]
1557 fn explicit_gpu_kind_matrix() {
1558 let sv_kind = BackendKind::StatevectorGpu {
1559 context: GpuContext::stub_for_tests(),
1560 };
1561 let large = dense(crate::gpu::min_qubits());
1562 let plan = resolved(&sv_kind, &large, false);
1563 assert_eq!(plan.family(), Family::Statevector);
1564 assert!(matches!(plan.accel(), Accel::Gpu { soft: false, .. }));
1565 assert_cpu_family(
1566 &sv_kind,
1567 &dense(crate::gpu::min_qubits() - 1),
1568 false,
1569 Family::Statevector,
1570 );
1571
1572 let stab_kind = BackendKind::StabilizerGpu {
1573 context: GpuContext::stub_for_tests(),
1574 };
1575 let huge = Circuit::new(crate::gpu::stabilizer_min_qubits(), 0);
1576 let plan = resolved(&stab_kind, &huge, false);
1577 assert_eq!(plan.family(), Family::Stabilizer);
1578 assert!(matches!(plan.accel(), Accel::Gpu { soft: false, .. }));
1579 assert_cpu_family(&stab_kind, &clifford(8), false, Family::Stabilizer);
1580 }
1581
1582 #[test]
1588 fn auto_stabilizer_rank_budget_stays_inside_the_exact_ceiling() {
1589 for n in 1..=MAX_STABILIZER_RANK_QUBITS {
1590 let budget = stabilizer_rank_budget(n);
1591 assert!(
1592 budget <= MAX_AUTO_T_COUNT_EXACT,
1593 "width {n}: budget {budget} exceeds the exact ceiling {MAX_AUTO_T_COUNT_EXACT}"
1594 );
1595 }
1596 }
1597}