1use crate::backend::{analyze_circuit, BackendType, CircuitAnalysis};
34use crate::circuit::QuantumCircuit;
35
36#[derive(Debug, Clone)]
46pub struct ExecutionPlan {
47 pub backend: BackendType,
49 pub predicted_memory_bytes: u64,
51 pub predicted_runtime_ms: f64,
53 pub confidence: f64,
55 pub verification_policy: VerificationPolicy,
57 pub mitigation_strategy: MitigationStrategy,
59 pub entanglement_budget: Option<EntanglementBudget>,
61 pub explanation: String,
63 pub cost_breakdown: CostBreakdown,
65}
66
67#[derive(Debug, Clone, PartialEq)]
72pub enum VerificationPolicy {
73 ExactCliffordCheck,
76 DownscaledStateVector(u32),
79 StatisticalSampling(u32),
82 None,
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub enum MitigationStrategy {
89 None,
91 MeasurementCorrectionOnly,
93 ZneWithScales(Vec<f64>),
95 ZnePlusMeasurementCorrection(Vec<f64>),
97 Full {
99 zne_scales: Vec<f64>,
101 cdr_circuits: usize,
103 },
104}
105
106#[derive(Debug, Clone, PartialEq)]
110pub struct EntanglementBudget {
111 pub max_bond_dimension: u32,
113 pub predicted_peak_bond: u32,
115 pub truncation_needed: bool,
117}
118
119#[derive(Debug, Clone)]
121pub struct CostBreakdown {
122 pub simulation_cost: f64,
124 pub mitigation_overhead: f64,
126 pub verification_overhead: f64,
128 pub total_shots_needed: u32,
130}
131
132#[derive(Debug, Clone)]
134pub struct PlannerConfig {
135 pub available_memory_bytes: u64,
137 pub noise_level: Option<f64>,
140 pub shot_budget: u32,
142 pub target_precision: f64,
144}
145
146impl Default for PlannerConfig {
147 fn default() -> Self {
148 Self {
149 available_memory_bytes: 8 * 1024 * 1024 * 1024, noise_level: Option::None,
151 shot_budget: 10_000,
152 target_precision: 0.01,
153 }
154 }
155}
156
157const SV_NS_PER_GATE: f64 = 4.0;
163
164const STAB_NS_PER_GATE: f64 = 0.1;
166
167const TN_NS_PER_GATE: f64 = 2.0;
169
170const SV_COMFORT_QUBITS: u32 = 25;
172
173const DEFAULT_MAX_BOND_DIM: u32 = 256;
176
177const ABSOLUTE_MAX_BOND_DIM: u32 = 4096;
179
180const CT_NS_PER_GATE: f64 = 0.15;
182
183const CT_MAX_T_COUNT: usize = 40;
185
186pub fn plan_execution(circuit: &QuantumCircuit, config: &PlannerConfig) -> ExecutionPlan {
214 let analysis = analyze_circuit(circuit);
215 let entanglement = estimate_entanglement(circuit);
216 let num_qubits = analysis.num_qubits;
217 let total_gates = analysis.total_gates;
218
219 let stab_viable = analysis.clifford_fraction >= 1.0;
223 let stab_memory = predict_memory_stabilizer(num_qubits);
224 let stab_runtime = predict_runtime_stabilizer(num_qubits, total_gates);
225
226 let sv_memory = predict_memory_statevector(num_qubits);
228 let sv_viable = sv_memory <= config.available_memory_bytes;
229 let sv_runtime = predict_runtime_statevector(num_qubits, total_gates);
230
231 let chi = entanglement.predicted_peak_bond.min(ABSOLUTE_MAX_BOND_DIM);
233 let tn_memory = predict_memory_tensor_network(num_qubits, chi);
234 let tn_viable = tn_memory <= config.available_memory_bytes;
235 let tn_runtime = predict_runtime_tensor_network(num_qubits, total_gates, chi);
236
237 let t_count = analysis.non_clifford_gates;
239 let ct_viable = t_count > 0 && t_count <= CT_MAX_T_COUNT && num_qubits > 32;
240 let ct_terms = if ct_viable { 1u64.checked_shl(t_count as u32).unwrap_or(u64::MAX) } else { u64::MAX };
241 let ct_memory = predict_memory_clifford_t(num_qubits, ct_terms);
242 let ct_runtime = predict_runtime_clifford_t(num_qubits, total_gates, ct_terms);
243
244 let (backend, predicted_memory, predicted_runtime, confidence, explanation) =
247 select_optimal_backend(
248 &analysis,
249 &entanglement,
250 config,
251 stab_viable,
252 stab_memory,
253 stab_runtime,
254 sv_viable,
255 sv_memory,
256 sv_runtime,
257 tn_viable,
258 tn_memory,
259 tn_runtime,
260 chi,
261 ct_viable,
262 ct_memory,
263 ct_runtime,
264 ct_terms,
265 );
266
267 let verification_policy = select_verification_policy(&analysis, backend, num_qubits);
269
270 let mitigation_strategy =
272 select_mitigation_strategy(config.noise_level, config.shot_budget, &analysis);
273
274 let entanglement_budget = if backend == BackendType::TensorNetwork {
276 Some(entanglement)
277 } else {
278 Option::None
279 };
280
281 let cost_breakdown = compute_cost_breakdown(
283 backend,
284 predicted_runtime,
285 &mitigation_strategy,
286 &verification_policy,
287 config.shot_budget,
288 config.target_precision,
289 );
290
291 ExecutionPlan {
292 backend,
293 predicted_memory_bytes: predicted_memory,
294 predicted_runtime_ms: predicted_runtime,
295 confidence,
296 verification_policy,
297 mitigation_strategy,
298 entanglement_budget,
299 explanation,
300 cost_breakdown,
301 }
302}
303
304pub fn estimate_entanglement(circuit: &QuantumCircuit) -> EntanglementBudget {
319 let n = circuit.num_qubits();
320 if n <= 1 {
321 return EntanglementBudget {
322 max_bond_dimension: 1,
323 predicted_peak_bond: 1,
324 truncation_needed: false,
325 };
326 }
327
328 let num_cuts = (n - 1) as usize;
331 let mut cut_counts = vec![0u32; num_cuts];
332
333 for gate in circuit.gates() {
334 let qubits = gate.qubits();
335 if qubits.len() == 2 {
336 let (lo, hi) = if qubits[0] < qubits[1] {
337 (qubits[0], qubits[1])
338 } else {
339 (qubits[1], qubits[0])
340 };
341 for cut_idx in (lo as usize)..(hi as usize) {
343 if cut_idx < num_cuts {
344 cut_counts[cut_idx] += 1;
345 }
346 }
347 }
348 }
349
350 let max_gates_across_cut = cut_counts.iter().copied().max().unwrap_or(0);
351
352 let half_n = n / 2;
356 let effective_exponent = max_gates_across_cut.min(half_n).min(30);
357 let predicted_peak_bond = 1u32.checked_shl(effective_exponent).unwrap_or(u32::MAX);
358
359 let max_bond_dimension = predicted_peak_bond
361 .saturating_mul(2)
362 .min(ABSOLUTE_MAX_BOND_DIM);
363
364 let truncation_needed = predicted_peak_bond > DEFAULT_MAX_BOND_DIM;
365
366 EntanglementBudget {
367 max_bond_dimension,
368 predicted_peak_bond,
369 truncation_needed,
370 }
371}
372
373fn predict_memory_statevector(num_qubits: u32) -> u64 {
379 if num_qubits >= 64 {
380 return u64::MAX;
381 }
382 (1u64 << num_qubits).saturating_mul(16)
383}
384
385fn predict_memory_stabilizer(num_qubits: u32) -> u64 {
387 let n = num_qubits as u64;
388 n.saturating_mul(n) / 4
391}
392
393fn predict_memory_tensor_network(num_qubits: u32, chi: u32) -> u64 {
395 let n = num_qubits as u64;
396 let c = chi as u64;
397 n.saturating_mul(c)
398 .saturating_mul(c)
399 .saturating_mul(16)
400}
401
402fn predict_runtime_statevector(num_qubits: u32, total_gates: usize) -> f64 {
411 if num_qubits >= 64 {
412 return f64::INFINITY;
413 }
414 let base_ops = (1u64 << num_qubits) as f64 * total_gates as f64;
415 let ns = base_ops * SV_NS_PER_GATE;
416
417 let scaling = if num_qubits > SV_COMFORT_QUBITS {
419 2.0_f64.powi((num_qubits - SV_COMFORT_QUBITS) as i32)
420 } else {
421 1.0
422 };
423
424 ns * scaling / 1_000_000.0 }
426
427fn predict_runtime_stabilizer(num_qubits: u32, total_gates: usize) -> f64 {
431 let n = num_qubits as f64;
432 let ns = n * n * total_gates as f64 * STAB_NS_PER_GATE;
433 ns / 1_000_000.0
434}
435
436fn predict_runtime_tensor_network(num_qubits: u32, total_gates: usize, chi: u32) -> f64 {
440 let n = num_qubits as f64;
441 let c = chi as f64;
442 let ns = n * c * c * c * total_gates as f64 * TN_NS_PER_GATE;
443 ns / 1_000_000.0
444}
445
446fn predict_memory_clifford_t(num_qubits: u32, terms: u64) -> u64 {
448 let n = num_qubits as u64;
449 let per_term = n.saturating_mul(n) / 4 + 16;
451 terms.saturating_mul(per_term)
452}
453
454fn predict_runtime_clifford_t(num_qubits: u32, total_gates: usize, terms: u64) -> f64 {
458 let n = num_qubits as f64;
459 let ns = terms as f64 * n * n * total_gates as f64 * CT_NS_PER_GATE;
460 ns / 1_000_000.0
461}
462
463#[allow(clippy::too_many_arguments)]
475fn select_optimal_backend(
476 analysis: &CircuitAnalysis,
477 entanglement: &EntanglementBudget,
478 config: &PlannerConfig,
479 stab_viable: bool,
480 stab_memory: u64,
481 stab_runtime: f64,
482 sv_viable: bool,
483 sv_memory: u64,
484 sv_runtime: f64,
485 _tn_viable: bool,
486 tn_memory: u64,
487 tn_runtime: f64,
488 chi: u32,
489 ct_viable: bool,
490 ct_memory: u64,
491 ct_runtime: f64,
492 ct_terms: u64,
493) -> (BackendType, u64, f64, f64, String) {
494 let n = analysis.num_qubits;
495
496 if stab_viable {
498 return (
499 BackendType::Stabilizer,
500 stab_memory,
501 stab_runtime,
502 0.99,
503 format!(
504 "Pure Clifford circuit ({} qubits, {} gates): stabilizer simulation in \
505 O(n^2) per gate. Predicted {:.1} ms, {} bytes memory.",
506 n, analysis.total_gates, stab_runtime, stab_memory
507 ),
508 );
509 }
510
511 if analysis.clifford_fraction >= 0.95
513 && n > 32
514 && analysis.non_clifford_gates <= 10
515 {
516 return (
517 BackendType::Stabilizer,
518 stab_memory,
519 stab_runtime,
520 0.85,
521 format!(
522 "{:.0}% Clifford with only {} non-Clifford gates on {} qubits: \
523 stabilizer backend with approximate decomposition.",
524 analysis.clifford_fraction * 100.0,
525 analysis.non_clifford_gates,
526 n
527 ),
528 );
529 }
530
531 if ct_viable && ct_memory <= config.available_memory_bytes {
533 return (
534 BackendType::CliffordT,
535 ct_memory,
536 ct_runtime,
537 0.90,
538 format!(
539 "{} qubits with {} T-gates: Clifford+T decomposition with {} stabilizer terms. \
540 Predicted {:.2} ms, {} bytes.",
541 n, analysis.non_clifford_gates, ct_terms, ct_runtime, ct_memory
542 ),
543 );
544 }
545
546 if sv_viable && n <= 32 {
548 let conf = if n <= SV_COMFORT_QUBITS { 0.95 } else { 0.80 };
549 return (
550 BackendType::StateVector,
551 sv_memory,
552 sv_runtime,
553 conf,
554 format!(
555 "{} qubits fits in state vector ({} bytes). Predicted {:.2} ms runtime.",
556 n, sv_memory, sv_runtime
557 ),
558 );
559 }
560
561 if !sv_viable || n > 32 {
563 let conf = if analysis.is_nearest_neighbor && analysis.depth < n * 2 {
564 0.85
565 } else if analysis.is_nearest_neighbor {
566 0.75
567 } else {
568 0.55
569 };
570
571 let used_memory = tn_memory;
572 let used_runtime = tn_runtime;
573
574 let truncation_note = if entanglement.truncation_needed {
575 " Results will be approximate due to bond dimension truncation."
576 } else {
577 ""
578 };
579
580 return (
581 BackendType::TensorNetwork,
582 used_memory,
583 used_runtime,
584 conf,
585 format!(
586 "{} qubits exceeds state vector capacity ({} bytes > {} bytes available). \
587 Using tensor network with chi={}.{} Predicted {:.2} ms.",
588 n,
589 predict_memory_statevector(n),
590 config.available_memory_bytes,
591 chi,
592 truncation_note,
593 used_runtime
594 ),
595 );
596 }
597
598 (
600 BackendType::StateVector,
601 sv_memory,
602 sv_runtime,
603 0.70,
604 "Default to exact state vector simulation.".into(),
605 )
606}
607
608fn select_verification_policy(
619 analysis: &CircuitAnalysis,
620 backend: BackendType,
621 num_qubits: u32,
622) -> VerificationPolicy {
623 if analysis.clifford_fraction >= 1.0 {
625 return VerificationPolicy::ExactCliffordCheck;
626 }
627
628 if analysis.clifford_fraction >= 0.9 && num_qubits > 20 {
630 let downscale_qubits = num_qubits.min(16);
631 return VerificationPolicy::DownscaledStateVector(downscale_qubits);
632 }
633
634 if backend == BackendType::StateVector && num_qubits <= SV_COMFORT_QUBITS {
636 return VerificationPolicy::None;
637 }
638
639 if backend == BackendType::StateVector && num_qubits <= 32 {
641 return VerificationPolicy::StatisticalSampling(10);
642 }
643
644 if backend == BackendType::TensorNetwork {
646 if num_qubits <= 20 {
647 return VerificationPolicy::DownscaledStateVector(num_qubits);
649 }
650 return VerificationPolicy::StatisticalSampling(
651 (num_qubits / 2).max(5).min(50),
652 );
653 }
654
655 VerificationPolicy::None
656}
657
658fn select_mitigation_strategy(
670 noise_level: Option<f64>,
671 shot_budget: u32,
672 analysis: &CircuitAnalysis,
673) -> MitigationStrategy {
674 let noise = match noise_level {
675 Some(n) if n > 0.0 => n,
676 _ => return MitigationStrategy::None,
677 };
678
679 if noise < 0.01 {
681 return MitigationStrategy::MeasurementCorrectionOnly;
682 }
683
684 let zne_scales_3 = vec![1.0, 1.5, 2.0];
686 let zne_scales_5 = vec![1.0, 1.25, 1.5, 1.75, 2.0];
687
688 if noise < 0.1 {
690 let scales = if shot_budget >= 50_000 {
692 zne_scales_5
693 } else {
694 zne_scales_3.clone()
695 };
696 return MitigationStrategy::ZneWithScales(scales);
697 }
698
699 if noise < 0.5 {
701 let scales = if shot_budget >= 50_000 {
702 zne_scales_5
703 } else {
704 zne_scales_3
705 };
706 return MitigationStrategy::ZnePlusMeasurementCorrection(scales);
707 }
708
709 let cdr_circuits = (analysis.non_clifford_gates * 2).max(10).min(100);
712 MitigationStrategy::Full {
713 zne_scales: vec![1.0, 1.5, 2.0, 2.5, 3.0],
714 cdr_circuits,
715 }
716}
717
718fn compute_cost_breakdown(
724 _backend: BackendType,
725 predicted_runtime_ms: f64,
726 mitigation: &MitigationStrategy,
727 verification: &VerificationPolicy,
728 shot_budget: u32,
729 target_precision: f64,
730) -> CostBreakdown {
731 let simulation_cost = predicted_runtime_ms.max(0.001);
734
735 let mitigation_overhead = match mitigation {
737 MitigationStrategy::None => 1.0,
738 MitigationStrategy::MeasurementCorrectionOnly => 1.1, MitigationStrategy::ZneWithScales(scales) => scales.len() as f64,
740 MitigationStrategy::ZnePlusMeasurementCorrection(scales) => {
741 scales.len() as f64 * 1.1
742 }
743 MitigationStrategy::Full { zne_scales, cdr_circuits } => {
744 zne_scales.len() as f64 + *cdr_circuits as f64 * 0.5
745 }
746 };
747
748 let verification_overhead = match verification {
750 VerificationPolicy::None => 1.0,
751 VerificationPolicy::ExactCliffordCheck => 1.05, VerificationPolicy::DownscaledStateVector(_) => 1.1,
753 VerificationPolicy::StatisticalSampling(n) => {
754 1.0 + (*n as f64) * 0.01
755 }
756 };
757
758 let base_shots = (1.0 / (target_precision * target_precision)).ceil() as u32;
761 let mitigated_shots =
762 (base_shots as f64 * mitigation_overhead).ceil() as u32;
763 let total_shots_needed = mitigated_shots.min(shot_budget);
764
765 CostBreakdown {
766 simulation_cost,
767 mitigation_overhead,
768 verification_overhead,
769 total_shots_needed,
770 }
771}
772
773#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::circuit::QuantumCircuit;
781
782 fn default_config() -> PlannerConfig {
784 PlannerConfig::default()
785 }
786
787 #[test]
792 fn test_pure_clifford_plan() {
793 let mut circ = QuantumCircuit::new(50);
796 for q in 0..50 {
797 circ.h(q);
798 }
799 for q in 0..49 {
800 circ.cnot(q, q + 1);
801 }
802
803 let config = default_config();
804 let plan = plan_execution(&circ, &config);
805
806 assert_eq!(
807 plan.backend,
808 BackendType::Stabilizer,
809 "Pure Clifford circuit should use Stabilizer backend"
810 );
811 assert_eq!(
812 plan.verification_policy,
813 VerificationPolicy::ExactCliffordCheck,
814 "Pure Clifford should use ExactCliffordCheck verification"
815 );
816 assert_eq!(
817 plan.mitigation_strategy,
818 MitigationStrategy::None,
819 "Noiseless config should have no mitigation"
820 );
821 assert!(
822 plan.confidence > 0.9,
823 "Confidence should be high for pure Clifford"
824 );
825 assert!(
826 plan.entanglement_budget.is_none(),
827 "Stabilizer backend should not have entanglement budget"
828 );
829 }
830
831 #[test]
836 fn test_small_circuit_plan() {
837 let mut circ = QuantumCircuit::new(5);
840 circ.h(0).t(1).cnot(0, 1).rx(2, 0.5);
841
842 let config = default_config();
843 let plan = plan_execution(&circ, &config);
844
845 assert_eq!(
846 plan.backend,
847 BackendType::StateVector,
848 "Small non-Clifford circuit should use StateVector"
849 );
850 assert_eq!(
851 plan.mitigation_strategy,
852 MitigationStrategy::None,
853 "Noiseless config should have no mitigation"
854 );
855 assert_eq!(
856 plan.verification_policy,
857 VerificationPolicy::None,
858 "Small SV circuit should not need verification"
859 );
860 assert!(plan.entanglement_budget.is_none());
861
862 assert_eq!(plan.predicted_memory_bytes, 512);
864 assert!(plan.predicted_runtime_ms > 0.0);
865 assert!(plan.confidence >= 0.9);
866 }
867
868 #[test]
873 fn test_large_mps_plan() {
874 let mut circ = QuantumCircuit::new(64);
878 for q in 0..63 {
880 circ.cnot(q, q + 1);
881 }
882 for q in 0..50 {
884 circ.t(q % 64);
885 }
886
887 let config = PlannerConfig {
888 available_memory_bytes: 8 * 1024 * 1024 * 1024,
889 noise_level: Option::None,
890 shot_budget: 10_000,
891 target_precision: 0.01,
892 };
893 let plan = plan_execution(&circ, &config);
894
895 assert_eq!(
896 plan.backend,
897 BackendType::TensorNetwork,
898 "Large non-Clifford circuit should use TensorNetwork"
899 );
900 assert!(
901 plan.entanglement_budget.is_some(),
902 "TensorNetwork backend should have entanglement budget"
903 );
904 let budget = plan.entanglement_budget.as_ref().unwrap();
905 assert!(
906 budget.predicted_peak_bond >= 2,
907 "Entangling gates should produce bond dimension >= 2"
908 );
909 assert!(
910 budget.max_bond_dimension >= budget.predicted_peak_bond,
911 "Max bond dimension should be >= predicted peak"
912 );
913 }
914
915 #[test]
920 fn test_memory_overflow_fallback() {
921 let mut circ = QuantumCircuit::new(30);
924 circ.h(0).t(1).cnot(0, 1);
925
926 let config = PlannerConfig {
928 available_memory_bytes: 1024 * 1024, noise_level: Option::None,
930 shot_budget: 10_000,
931 target_precision: 0.01,
932 };
933 let plan = plan_execution(&circ, &config);
934
935 assert_eq!(
936 plan.backend,
937 BackendType::TensorNetwork,
938 "When SV exceeds memory, should fall back to TensorNetwork"
939 );
940 assert!(
942 plan.predicted_memory_bytes <= config.available_memory_bytes,
943 "TensorNetwork memory ({}) should fit within budget ({})",
944 plan.predicted_memory_bytes,
945 config.available_memory_bytes
946 );
947 }
948
949 #[test]
954 fn test_noisy_circuit_plan() {
955 let mut circ = QuantumCircuit::new(5);
957 circ.h(0).cnot(0, 1).t(2);
958
959 let config = PlannerConfig {
960 available_memory_bytes: 8 * 1024 * 1024 * 1024,
961 noise_level: Some(0.05), shot_budget: 10_000,
963 target_precision: 0.01,
964 };
965 let plan = plan_execution(&circ, &config);
966
967 match &plan.mitigation_strategy {
969 MitigationStrategy::ZneWithScales(scales) => {
970 assert!(
971 scales.len() >= 3,
972 "ZNE should have at least 3 scale factors"
973 );
974 assert!(
975 scales.contains(&1.0),
976 "ZNE scales must include the baseline 1.0"
977 );
978 }
979 other => panic!(
980 "Expected ZneWithScales for noise=0.05, got {:?}",
981 other
982 ),
983 }
984
985 assert!(
986 plan.cost_breakdown.mitigation_overhead > 1.0,
987 "Mitigation should add overhead"
988 );
989 }
990
991 #[test]
996 fn test_entanglement_estimate() {
997 let mut circ = QuantumCircuit::new(2);
1000 circ.h(0).cnot(0, 1);
1001
1002 let budget = estimate_entanglement(&circ);
1003 assert_eq!(
1004 budget.predicted_peak_bond, 2,
1005 "Bell state should have bond dimension 2"
1006 );
1007 assert!(
1008 !budget.truncation_needed,
1009 "Bell state bond dimension 2 should not need truncation"
1010 );
1011 }
1012
1013 #[test]
1014 fn test_entanglement_estimate_single_qubit() {
1015 let mut circ = QuantumCircuit::new(1);
1017 circ.h(0);
1018
1019 let budget = estimate_entanglement(&circ);
1020 assert_eq!(budget.predicted_peak_bond, 1);
1021 assert_eq!(budget.max_bond_dimension, 1);
1022 assert!(!budget.truncation_needed);
1023 }
1024
1025 #[test]
1026 fn test_entanglement_estimate_no_two_qubit_gates() {
1027 let mut circ = QuantumCircuit::new(10);
1029 for q in 0..10 {
1030 circ.h(q);
1031 }
1032
1033 let budget = estimate_entanglement(&circ);
1034 assert_eq!(budget.predicted_peak_bond, 1);
1035 }
1036
1037 #[test]
1038 fn test_entanglement_estimate_ghz_chain() {
1039 let mut circ = QuantumCircuit::new(4);
1046 circ.h(0).cnot(0, 1).cnot(1, 2).cnot(2, 3);
1047
1048 let budget = estimate_entanglement(&circ);
1049 assert_eq!(
1050 budget.predicted_peak_bond, 2,
1051 "GHZ chain should have peak bond dim 2 (nearest-neighbor only)"
1052 );
1053 }
1054
1055 #[test]
1060 fn test_workload_routing_accuracy() {
1061 let config = default_config();
1062
1063 let circ_empty = QuantumCircuit::new(10);
1065 let plan = plan_execution(&circ_empty, &config);
1066 assert_eq!(plan.backend, BackendType::Stabilizer);
1067
1068 let mut circ_h = QuantumCircuit::new(3);
1070 circ_h.h(0);
1071 let plan = plan_execution(&circ_h, &config);
1072 assert_eq!(plan.backend, BackendType::Stabilizer);
1073
1074 let mut circ_bell = QuantumCircuit::new(2);
1076 circ_bell.h(0).cnot(0, 1);
1077 let plan = plan_execution(&circ_bell, &config);
1078 assert_eq!(plan.backend, BackendType::Stabilizer);
1079
1080 let mut circ_small_t = QuantumCircuit::new(5);
1082 circ_small_t.h(0).t(1).cnot(0, 1);
1083 let plan = plan_execution(&circ_small_t, &config);
1084 assert_eq!(plan.backend, BackendType::StateVector);
1085
1086 let mut circ_vqe = QuantumCircuit::new(20);
1088 for q in 0..20 {
1089 circ_vqe.ry(q, 0.5);
1090 }
1091 for q in 0..19 {
1092 circ_vqe.cnot(q, q + 1);
1093 }
1094 let plan = plan_execution(&circ_vqe, &config);
1095 assert_eq!(plan.backend, BackendType::StateVector);
1096
1097 let mut circ_40_cliff = QuantumCircuit::new(40);
1099 for q in 0..40 {
1100 circ_40_cliff.h(q);
1101 }
1102 for q in 0..39 {
1103 circ_40_cliff.cnot(q, q + 1);
1104 }
1105 let plan = plan_execution(&circ_40_cliff, &config);
1106 assert_eq!(plan.backend, BackendType::Stabilizer);
1107
1108 let mut circ_100 = QuantumCircuit::new(100);
1110 for q in 0..99 {
1111 circ_100.cnot(q, q + 1);
1112 }
1113 for q in 0..50 {
1114 circ_100.rx(q, 1.0);
1115 }
1116 let plan = plan_execution(&circ_100, &config);
1117 assert_eq!(plan.backend, BackendType::TensorNetwork);
1118
1119 let mut circ_mostly_cliff = QuantumCircuit::new(50);
1121 for q in 0..50 {
1122 circ_mostly_cliff.h(q);
1123 }
1124 for q in 0..49 {
1125 circ_mostly_cliff.cnot(q, q + 1);
1126 }
1127 for q in 0..5 {
1129 circ_mostly_cliff.t(q);
1130 }
1131 let plan = plan_execution(&circ_mostly_cliff, &config);
1132 assert_eq!(
1133 plan.backend,
1134 BackendType::Stabilizer,
1135 "Mostly-Clifford 50-qubit circuit should use Stabilizer"
1136 );
1137
1138 let mut circ_25 = QuantumCircuit::new(25);
1140 for q in 0..25 {
1141 circ_25.h(q);
1142 }
1143 for q in 0..24 {
1144 circ_25.cnot(q, q + 1);
1145 }
1146 circ_25.t(0).t(1).rx(2, 0.5);
1147 let plan = plan_execution(&circ_25, &config);
1148 assert_eq!(plan.backend, BackendType::StateVector);
1149
1150 let mut circ_28 = QuantumCircuit::new(28);
1152 circ_28.h(0).t(1).cnot(0, 1);
1153 let tight_config = PlannerConfig {
1154 available_memory_bytes: 1024, noise_level: Option::None,
1156 shot_budget: 1000,
1157 target_precision: 0.01,
1158 };
1159 let plan = plan_execution(&circ_28, &tight_config);
1160 assert_eq!(
1161 plan.backend,
1162 BackendType::TensorNetwork,
1163 "Should fall back to TN when memory is too tight for SV"
1164 );
1165
1166 let mut circ_noisy = QuantumCircuit::new(5);
1168 circ_noisy.h(0).t(0).cnot(0, 1);
1169 let noisy_config = PlannerConfig {
1170 available_memory_bytes: 8 * 1024 * 1024 * 1024,
1171 noise_level: Some(0.7),
1172 shot_budget: 100_000,
1173 target_precision: 0.01,
1174 };
1175 let plan = plan_execution(&circ_noisy, &noisy_config);
1176 match &plan.mitigation_strategy {
1177 MitigationStrategy::Full {
1178 zne_scales,
1179 cdr_circuits,
1180 } => {
1181 assert!(zne_scales.len() >= 3);
1182 assert!(*cdr_circuits >= 2);
1183 }
1184 other => panic!(
1185 "Expected Full mitigation for noise=0.7, got {:?}",
1186 other
1187 ),
1188 }
1189 }
1190
1191 #[test]
1196 fn test_memory_prediction_statevector() {
1197 assert_eq!(predict_memory_statevector(1), 32); assert_eq!(predict_memory_statevector(10), 1024 * 16); assert_eq!(predict_memory_statevector(20), 1048576 * 16); }
1201
1202 #[test]
1203 fn test_memory_prediction_stabilizer() {
1204 assert_eq!(predict_memory_stabilizer(100), 2500);
1206 assert_eq!(predict_memory_stabilizer(1000), 250_000);
1207 }
1208
1209 #[test]
1210 fn test_memory_prediction_tensor_network() {
1211 assert_eq!(predict_memory_tensor_network(10, 4), 10 * 16 * 16);
1213 assert_eq!(predict_memory_tensor_network(100, 32), 100 * 1024 * 16);
1214 }
1215
1216 #[test]
1221 fn test_runtime_prediction_statevector() {
1222 let rt = predict_runtime_statevector(10, 100);
1223 let expected = (1024.0 * 100.0 * 4.0) / 1_000_000.0;
1225 assert!(
1226 (rt - expected).abs() < 1e-6,
1227 "SV runtime for 10 qubits: expected {expected}, got {rt}"
1228 );
1229 }
1230
1231 #[test]
1232 fn test_runtime_prediction_stabilizer() {
1233 let rt = predict_runtime_stabilizer(100, 200);
1234 let expected = (10000.0 * 200.0 * 0.1) / 1_000_000.0;
1236 assert!(
1237 (rt - expected).abs() < 1e-6,
1238 "Stabilizer runtime: expected {expected}, got {rt}"
1239 );
1240 }
1241
1242 #[test]
1243 fn test_runtime_scales_above_25_qubits() {
1244 let rt_25 = predict_runtime_statevector(25, 100);
1245 let rt_26 = predict_runtime_statevector(26, 100);
1246 let ratio = rt_26 / rt_25;
1248 assert!(
1249 (ratio - 4.0).abs() < 0.1,
1250 "Going from 25 to 26 qubits should ~4x the runtime, got {ratio}x"
1251 );
1252 }
1253
1254 #[test]
1259 fn test_cost_breakdown_no_mitigation() {
1260 let breakdown = compute_cost_breakdown(
1261 BackendType::StateVector,
1262 1.0,
1263 &MitigationStrategy::None,
1264 &VerificationPolicy::None,
1265 10_000,
1266 0.01,
1267 );
1268 assert_eq!(breakdown.mitigation_overhead, 1.0);
1269 assert_eq!(breakdown.verification_overhead, 1.0);
1270 assert!(breakdown.total_shots_needed <= 10_000);
1271 }
1272
1273 #[test]
1274 fn test_cost_breakdown_with_zne() {
1275 let scales = vec![1.0, 1.5, 2.0];
1276 let breakdown = compute_cost_breakdown(
1277 BackendType::StateVector,
1278 1.0,
1279 &MitigationStrategy::ZneWithScales(scales),
1280 &VerificationPolicy::None,
1281 100_000,
1282 0.01,
1283 );
1284 assert_eq!(
1285 breakdown.mitigation_overhead, 3.0,
1286 "3 ZNE scales -> 3x overhead"
1287 );
1288 assert!(breakdown.total_shots_needed > 10_000);
1289 }
1290
1291 #[test]
1296 fn test_mitigation_none_for_noiseless() {
1297 let analysis = make_analysis(5, 10, 1.0);
1298 let strat = select_mitigation_strategy(Option::None, 10_000, &analysis);
1299 assert_eq!(strat, MitigationStrategy::None);
1300 }
1301
1302 #[test]
1303 fn test_mitigation_measurement_correction_low_noise() {
1304 let analysis = make_analysis(5, 10, 0.5);
1305 let strat = select_mitigation_strategy(Some(0.005), 10_000, &analysis);
1306 assert_eq!(strat, MitigationStrategy::MeasurementCorrectionOnly);
1307 }
1308
1309 #[test]
1310 fn test_mitigation_zne_medium_noise() {
1311 let analysis = make_analysis(5, 10, 0.5);
1312 let strat = select_mitigation_strategy(Some(0.05), 10_000, &analysis);
1313 match strat {
1314 MitigationStrategy::ZneWithScales(scales) => {
1315 assert!(scales.contains(&1.0));
1316 assert!(scales.len() >= 3);
1317 }
1318 other => panic!("Expected ZneWithScales, got {:?}", other),
1319 }
1320 }
1321
1322 #[test]
1323 fn test_mitigation_full_for_high_noise() {
1324 let analysis = make_analysis(5, 10, 0.5);
1325 let strat = select_mitigation_strategy(Some(0.7), 100_000, &analysis);
1326 match strat {
1327 MitigationStrategy::Full { zne_scales, cdr_circuits } => {
1328 assert!(zne_scales.len() >= 3);
1329 assert!(cdr_circuits >= 2);
1330 }
1331 other => panic!("Expected Full mitigation, got {:?}", other),
1332 }
1333 }
1334
1335 #[test]
1340 fn test_verification_clifford_check() {
1341 let analysis = make_analysis(10, 50, 1.0);
1342 let policy = select_verification_policy(
1343 &analysis,
1344 BackendType::Stabilizer,
1345 10,
1346 );
1347 assert_eq!(policy, VerificationPolicy::ExactCliffordCheck);
1348 }
1349
1350 #[test]
1351 fn test_verification_none_for_small_sv() {
1352 let analysis = make_analysis(5, 10, 0.5);
1353 let policy = select_verification_policy(
1354 &analysis,
1355 BackendType::StateVector,
1356 5,
1357 );
1358 assert_eq!(policy, VerificationPolicy::None);
1359 }
1360
1361 #[test]
1362 fn test_verification_statistical_for_tn() {
1363 let analysis = make_analysis(50, 100, 0.5);
1364 let policy = select_verification_policy(
1365 &analysis,
1366 BackendType::TensorNetwork,
1367 50,
1368 );
1369 match policy {
1370 VerificationPolicy::StatisticalSampling(n) => {
1371 assert!(n >= 5, "Should sample at least 5 observables");
1372 }
1373 other => panic!(
1374 "Expected StatisticalSampling for TN, got {:?}",
1375 other
1376 ),
1377 }
1378 }
1379
1380 #[test]
1385 fn test_planner_config_default() {
1386 let config = PlannerConfig::default();
1387 assert_eq!(config.available_memory_bytes, 8 * 1024 * 1024 * 1024);
1388 assert!(config.noise_level.is_none());
1389 assert_eq!(config.shot_budget, 10_000);
1390 assert!((config.target_precision - 0.01).abs() < 1e-12);
1391 }
1392
1393 #[test]
1398 fn test_plan_has_nonempty_explanation() {
1399 let mut circ = QuantumCircuit::new(3);
1400 circ.h(0).cnot(0, 1);
1401 let plan = plan_execution(&circ, &default_config());
1402 assert!(
1403 !plan.explanation.is_empty(),
1404 "Plan explanation should not be empty"
1405 );
1406 }
1407
1408 #[test]
1413 fn test_zero_qubit_circuit() {
1414 let circ = QuantumCircuit::new(0);
1415 let plan = plan_execution(&circ, &default_config());
1416 assert_eq!(plan.backend, BackendType::Stabilizer);
1418 }
1419
1420 fn make_analysis(
1425 num_qubits: u32,
1426 total_gates: usize,
1427 clifford_fraction: f64,
1428 ) -> CircuitAnalysis {
1429 let clifford_gates =
1430 (total_gates as f64 * clifford_fraction).round() as usize;
1431 let non_clifford_gates = total_gates - clifford_gates;
1432
1433 CircuitAnalysis {
1434 num_qubits,
1435 total_gates,
1436 clifford_gates,
1437 non_clifford_gates,
1438 clifford_fraction,
1439 measurement_gates: 0,
1440 depth: total_gates as u32,
1441 max_connectivity: 1,
1442 is_nearest_neighbor: true,
1443 recommended_backend: BackendType::Auto,
1444 confidence: 0.5,
1445 explanation: String::new(),
1446 }
1447 }
1448
1449 #[test]
1454 fn test_clifford_t_routing() {
1455 let mut circ = QuantumCircuit::new(50);
1457 for q in 0..50 {
1458 circ.h(q);
1459 }
1460 for q in 0..49 {
1461 circ.cnot(q, q + 1);
1462 }
1463 for q in 0..15 {
1465 circ.t(q);
1466 }
1467
1468 let config = default_config();
1469 let plan = plan_execution(&circ, &config);
1470 assert_eq!(
1471 plan.backend,
1472 BackendType::CliffordT,
1473 "50 qubits with 15 T-gates should use CliffordT backend"
1474 );
1475 assert!(plan.confidence >= 0.85);
1476 }
1477}