Skip to main content

ruqu_core/
planner.rs

1//! Cost-model circuit execution planner.
2//!
3//! Replaces the simple heuristic backend selector in [`crate::backend`] with a
4//! full cost-model planner that produces a concrete [`ExecutionPlan`] -- not
5//! just a backend enum. The planner predicts memory usage, runtime, selects
6//! verification policies, mitigation strategies, and computes entanglement
7//! budgets for tensor-network simulation.
8//!
9//! # Cost Model
10//!
11//! | Backend | Memory | Runtime |
12//! |---------|--------|---------|
13//! | StateVector | 2^n * 16 bytes | 2^n * gates * 4ns (SIMD, n<=25) |
14//! | Stabilizer | n^2 / 4 bytes | n^2 * gates * 0.1ns |
15//! | TensorNetwork | n * chi^2 * 16 bytes | n * chi^3 * gates * 2ns |
16//!
17//! # Example
18//!
19//! ```
20//! use ruqu_core::circuit::QuantumCircuit;
21//! use ruqu_core::planner::{plan_execution, PlannerConfig};
22//! use ruqu_core::backend::BackendType;
23//!
24//! let mut circ = QuantumCircuit::new(5);
25//! circ.h(0).cnot(0, 1).t(2);
26//!
27//! let config = PlannerConfig::default();
28//! let plan = plan_execution(&circ, &config);
29//! assert_eq!(plan.backend, BackendType::StateVector);
30//! assert!(plan.predicted_memory_bytes < config.available_memory_bytes);
31//! ```
32
33use crate::backend::{analyze_circuit, BackendType, CircuitAnalysis};
34use crate::circuit::QuantumCircuit;
35
36// ---------------------------------------------------------------------------
37// Public types
38// ---------------------------------------------------------------------------
39
40/// A concrete execution plan produced by the cost-model planner.
41///
42/// Contains the selected backend, predicted resource usage, verification and
43/// mitigation policies, and an optional entanglement budget for tensor-network
44/// simulation.
45#[derive(Debug, Clone)]
46pub struct ExecutionPlan {
47    /// Selected simulation backend.
48    pub backend: BackendType,
49    /// Predicted peak memory usage in bytes.
50    pub predicted_memory_bytes: u64,
51    /// Predicted wall-clock runtime in milliseconds.
52    pub predicted_runtime_ms: f64,
53    /// Confidence in the plan (0.0 to 1.0).
54    pub confidence: f64,
55    /// How to verify the simulation result.
56    pub verification_policy: VerificationPolicy,
57    /// Error mitigation strategy to apply.
58    pub mitigation_strategy: MitigationStrategy,
59    /// Entanglement budget for tensor-network backends.
60    pub entanglement_budget: Option<EntanglementBudget>,
61    /// Human-readable explanation of the planning decisions.
62    pub explanation: String,
63    /// Breakdown of computational costs.
64    pub cost_breakdown: CostBreakdown,
65}
66
67/// Policy for verifying simulation results.
68///
69/// Higher-confidence plans may skip verification entirely, while lower
70/// confidence triggers cross-checks against a different backend or sampling.
71#[derive(Debug, Clone, PartialEq)]
72pub enum VerificationPolicy {
73    /// Pure Clifford circuit: verify by running the stabilizer backend and
74    /// comparing results exactly.
75    ExactCliffordCheck,
76    /// Run a reduced-qubit version of the circuit on state-vector for a spot
77    /// check. The `u32` is the number of qubits in the downscaled version.
78    DownscaledStateVector(u32),
79    /// Compare a subset of observables between backends. The `u32` is the
80    /// number of observables to sample.
81    StatisticalSampling(u32),
82    /// No verification needed (high confidence in the result).
83    None,
84}
85
86/// Strategy for mitigating simulation or hardware noise.
87#[derive(Debug, Clone, PartialEq)]
88pub enum MitigationStrategy {
89    /// No mitigation needed (noiseless simulation).
90    None,
91    /// Apply measurement error correction only.
92    MeasurementCorrectionOnly,
93    /// Zero-noise extrapolation with the given noise scale factors.
94    ZneWithScales(Vec<f64>),
95    /// ZNE combined with measurement error correction.
96    ZnePlusMeasurementCorrection(Vec<f64>),
97    /// Full mitigation pipeline: ZNE + CDR training circuits.
98    Full {
99        /// Noise scale factors for ZNE.
100        zne_scales: Vec<f64>,
101        /// Number of Clifford Data Regression training circuits.
102        cdr_circuits: usize,
103    },
104}
105
106/// Entanglement budget for tensor-network simulation.
107///
108/// Controls the maximum bond dimension and whether truncation is needed.
109#[derive(Debug, Clone, PartialEq)]
110pub struct EntanglementBudget {
111    /// Maximum bond dimension the simulator should allow.
112    pub max_bond_dimension: u32,
113    /// Predicted peak bond dimension based on circuit analysis.
114    pub predicted_peak_bond: u32,
115    /// Whether truncation will be needed to stay within budget.
116    pub truncation_needed: bool,
117}
118
119/// Breakdown of computational costs for the execution plan.
120#[derive(Debug, Clone)]
121pub struct CostBreakdown {
122    /// Estimated floating-point operations in units of 10^9 (GFLOPs).
123    pub simulation_cost: f64,
124    /// Multiplier overhead from ZNE (e.g., 3.0x for 3 scale factors).
125    pub mitigation_overhead: f64,
126    /// Multiplier overhead from verification.
127    pub verification_overhead: f64,
128    /// Total number of shots needed (including mitigation overhead).
129    pub total_shots_needed: u32,
130}
131
132/// Configuration for the execution planner.
133#[derive(Debug, Clone)]
134pub struct PlannerConfig {
135    /// Available system memory in bytes (default: 8 GiB).
136    pub available_memory_bytes: u64,
137    /// Optional noise level from 0.0 (noiseless) to 1.0 (fully depolarized).
138    /// `None` means noiseless simulation.
139    pub noise_level: Option<f64>,
140    /// Maximum total shots the user is willing to spend.
141    pub shot_budget: u32,
142    /// Target precision for observable estimation (standard error).
143    pub target_precision: f64,
144}
145
146impl Default for PlannerConfig {
147    fn default() -> Self {
148        Self {
149            available_memory_bytes: 8 * 1024 * 1024 * 1024, // 8 GiB
150            noise_level: Option::None,
151            shot_budget: 10_000,
152            target_precision: 0.01,
153        }
154    }
155}
156
157// ---------------------------------------------------------------------------
158// Cost model constants
159// ---------------------------------------------------------------------------
160
161/// Nanoseconds per state-vector gate application (SIMD-optimized).
162const SV_NS_PER_GATE: f64 = 4.0;
163
164/// Nanoseconds per stabilizer gate application.
165const STAB_NS_PER_GATE: f64 = 0.1;
166
167/// Nanoseconds per tensor-network contraction step.
168const TN_NS_PER_GATE: f64 = 2.0;
169
170/// Maximum qubit count for comfortable state-vector simulation.
171const SV_COMFORT_QUBITS: u32 = 25;
172
173/// Default bond dimension cap for tensor networks when no better estimate
174/// is available.
175const DEFAULT_MAX_BOND_DIM: u32 = 256;
176
177/// Maximum bond dimension the simulator can practically handle.
178const ABSOLUTE_MAX_BOND_DIM: u32 = 4096;
179
180/// Nanoseconds per Clifford+T gate application (per stabilizer term).
181const CT_NS_PER_GATE: f64 = 0.15;
182
183/// Maximum T-count where Clifford+T is practical (2^40 terms is too many).
184const CT_MAX_T_COUNT: usize = 40;
185
186// ---------------------------------------------------------------------------
187// Public API
188// ---------------------------------------------------------------------------
189
190/// Plan the execution of a quantum circuit.
191///
192/// Analyzes the circuit structure, predicts resource costs for each candidate
193/// backend, and selects the optimal backend subject to the memory and shot
194/// budget constraints in `config`. Returns a complete [`ExecutionPlan`].
195///
196/// # Arguments
197///
198/// * `circuit` -- The quantum circuit to plan for.
199/// * `config` -- Planner constraints (memory, noise, shots, precision).
200///
201/// # Example
202///
203/// ```
204/// use ruqu_core::circuit::QuantumCircuit;
205/// use ruqu_core::planner::{plan_execution, PlannerConfig};
206/// use ruqu_core::backend::BackendType;
207///
208/// let mut circ = QuantumCircuit::new(3);
209/// circ.h(0).cnot(0, 1);
210/// let plan = plan_execution(&circ, &PlannerConfig::default());
211/// assert_eq!(plan.backend, BackendType::Stabilizer);
212/// ```
213pub 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    // --- Candidate evaluation ---
220
221    // Evaluate Stabilizer backend.
222    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    // Evaluate StateVector backend.
227    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    // Evaluate TensorNetwork backend.
232    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    // Evaluate CliffordT backend.
238    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    // --- Backend selection ---
245
246    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    // --- Verification policy ---
268    let verification_policy = select_verification_policy(&analysis, backend, num_qubits);
269
270    // --- Mitigation strategy ---
271    let mitigation_strategy =
272        select_mitigation_strategy(config.noise_level, config.shot_budget, &analysis);
273
274    // --- Entanglement budget ---
275    let entanglement_budget = if backend == BackendType::TensorNetwork {
276        Some(entanglement)
277    } else {
278        Option::None
279    };
280
281    // --- Cost breakdown ---
282    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
304/// Estimate the entanglement budget for a quantum circuit.
305///
306/// Walks the circuit gate-by-gate, tracking cumulative two-qubit gate count
307/// across each possible bipartition of the qubit register. The peak bond
308/// dimension is derived from the worst-case cut.
309///
310/// # Arguments
311///
312/// * `circuit` -- The quantum circuit to analyze.
313///
314/// # Returns
315///
316/// An [`EntanglementBudget`] with the predicted peak bond dimension and
317/// whether truncation would be needed.
318pub 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    // Track cumulative entangling-gate count crossing each cut position.
329    // cut_counts[k] counts gates straddling the partition [0..k) | [k..n).
330    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            // This gate crosses every cut between lo and hi.
342            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    // Bond dimension grows as 2^(gates across cut), but we cap it sensibly.
353    // For circuits where max_gates_across_cut is large, the bond dimension
354    // is effectively 2^(n/2) (the maximum for n qubits).
355    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    // Allow up to the absolute maximum or 2x the predicted peak.
360    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
373// ---------------------------------------------------------------------------
374// Memory prediction
375// ---------------------------------------------------------------------------
376
377/// Predict memory usage for state-vector simulation: 2^n * 16 bytes.
378fn 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
385/// Predict memory usage for stabilizer simulation: n^2 / 4 bytes.
386fn predict_memory_stabilizer(num_qubits: u32) -> u64 {
387    let n = num_qubits as u64;
388    // Stabilizer tableau stores 2n rows of n bits each, packed.
389    // Approximately n^2 / 4 bytes.
390    n.saturating_mul(n) / 4
391}
392
393/// Predict memory usage for tensor-network simulation: n * chi^2 * 16 bytes.
394fn 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
402// ---------------------------------------------------------------------------
403// Runtime prediction
404// ---------------------------------------------------------------------------
405
406/// Predict runtime for state-vector simulation in milliseconds.
407///
408/// Base: 2^n * gates * 4ns for n <= 25.
409/// Each qubit above 25 doubles the runtime (cache pressure, no SIMD benefit).
410fn 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    // Scale up for qubits beyond the SIMD-comfortable threshold.
418    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 // Convert ns to ms
425}
426
427/// Predict runtime for stabilizer simulation in milliseconds.
428///
429/// n^2 * gates * 0.1ns.
430fn 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
436/// Predict runtime for tensor-network simulation in milliseconds.
437///
438/// n * chi^3 * gates * 2ns.
439fn 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
446/// Predict memory for Clifford+T: terms * n^2 / 4 bytes.
447fn predict_memory_clifford_t(num_qubits: u32, terms: u64) -> u64 {
448    let n = num_qubits as u64;
449    // Each stabilizer term needs a tableau of ~n^2/4 bytes + 16 bytes for the coefficient.
450    let per_term = n.saturating_mul(n) / 4 + 16;
451    terms.saturating_mul(per_term)
452}
453
454/// Predict runtime for Clifford+T in milliseconds.
455///
456/// terms * n^2 * gates * 0.15ns.
457fn 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// ---------------------------------------------------------------------------
464// Backend selection logic
465// ---------------------------------------------------------------------------
466
467/// Select the optimal backend given cost estimates and constraints.
468///
469/// Priority order:
470/// 1. Stabilizer for pure-Clifford circuits (any qubit count).
471/// 2. StateVector when it fits in memory and qubit count is manageable.
472/// 3. TensorNetwork when StateVector exceeds memory.
473/// 4. TensorNetwork as last resort for large circuits.
474#[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    // Rule 1: Pure Clifford -> Stabilizer (efficient for any qubit count).
497    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    // Rule 2: Mostly Clifford with very few non-Clifford on large circuits.
512    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    // Rule 2b: Moderate T-count on large circuits -> CliffordT.
532    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    // Rule 3: StateVector fits in available memory.
547    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    // Rule 4: StateVector would exceed memory -> fall back to TensorNetwork.
562    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    // Fallback: state vector.
599    (
600        BackendType::StateVector,
601        sv_memory,
602        sv_runtime,
603        0.70,
604        "Default to exact state vector simulation.".into(),
605    )
606}
607
608// ---------------------------------------------------------------------------
609// Verification policy selection
610// ---------------------------------------------------------------------------
611
612/// Select a verification policy based on circuit properties.
613///
614/// - Pure Clifford: exact cross-check with stabilizer.
615/// - High confidence and small circuits: no verification.
616/// - Medium confidence: downscaled state-vector spot check.
617/// - Low confidence: statistical sampling.
618fn select_verification_policy(
619    analysis: &CircuitAnalysis,
620    backend: BackendType,
621    num_qubits: u32,
622) -> VerificationPolicy {
623    // Pure Clifford: always verify with stabilizer (it's cheap).
624    if analysis.clifford_fraction >= 1.0 {
625        return VerificationPolicy::ExactCliffordCheck;
626    }
627
628    // High Clifford fraction on a non-stabilizer backend: downscale check.
629    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    // Small state-vector circuits: no verification needed.
635    if backend == BackendType::StateVector && num_qubits <= SV_COMFORT_QUBITS {
636        return VerificationPolicy::None;
637    }
638
639    // Medium-sized state-vector: statistical sampling with a few observables.
640    if backend == BackendType::StateVector && num_qubits <= 32 {
641        return VerificationPolicy::StatisticalSampling(10);
642    }
643
644    // Tensor network: always verify since results may be approximate.
645    if backend == BackendType::TensorNetwork {
646        if num_qubits <= 20 {
647            // Small enough to cross-check with state vector.
648            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
658// ---------------------------------------------------------------------------
659// Mitigation strategy selection
660// ---------------------------------------------------------------------------
661
662/// Select the error mitigation strategy based on noise level and shot budget.
663///
664/// - No noise: no mitigation.
665/// - Low noise (< 0.01): measurement correction only.
666/// - Medium noise (0.01-0.1): ZNE with 3 scale factors.
667/// - High noise (0.1-0.5): ZNE + measurement correction.
668/// - Very high noise (> 0.5): full pipeline with CDR.
669fn 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    // Low noise: measurement correction is sufficient.
680    if noise < 0.01 {
681        return MitigationStrategy::MeasurementCorrectionOnly;
682    }
683
684    // Standard ZNE scale factors.
685    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    // Medium noise: ZNE with 3 scale factors.
689    if noise < 0.1 {
690        // If we have enough shots, use 5 scale factors for better extrapolation.
691        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    // High noise: ZNE + measurement correction.
700    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    // Very high noise: full pipeline with CDR.
710    // CDR circuits scale with circuit complexity.
711    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
718// ---------------------------------------------------------------------------
719// Cost breakdown computation
720// ---------------------------------------------------------------------------
721
722/// Compute a cost breakdown for the execution plan.
723fn 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    // Simulation cost in GFLOPs (rough estimate from runtime).
732    // Assume ~1 GFLOP/ms on a modern CPU.
733    let simulation_cost = predicted_runtime_ms.max(0.001);
734
735    // Mitigation overhead multiplier.
736    let mitigation_overhead = match mitigation {
737        MitigationStrategy::None => 1.0,
738        MitigationStrategy::MeasurementCorrectionOnly => 1.1, // slight overhead
739        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    // Verification overhead multiplier.
749    let verification_overhead = match verification {
750        VerificationPolicy::None => 1.0,
751        VerificationPolicy::ExactCliffordCheck => 1.05, // cheap stabilizer check
752        VerificationPolicy::DownscaledStateVector(_) => 1.1,
753        VerificationPolicy::StatisticalSampling(n) => {
754            1.0 + (*n as f64) * 0.01
755        }
756    };
757
758    // Total shots: base shots * mitigation overhead.
759    // Base shots from precision: 1 / precision^2 (Hoeffding bound).
760    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// ---------------------------------------------------------------------------
774// Tests
775// ---------------------------------------------------------------------------
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780    use crate::circuit::QuantumCircuit;
781
782    /// Helper to build a default planner config.
783    fn default_config() -> PlannerConfig {
784        PlannerConfig::default()
785    }
786
787    // -----------------------------------------------------------------------
788    // test_pure_clifford_plan
789    // -----------------------------------------------------------------------
790
791    #[test]
792    fn test_pure_clifford_plan() {
793        // A pure Clifford circuit should route to Stabilizer with
794        // ExactCliffordCheck verification.
795        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    // -----------------------------------------------------------------------
832    // test_small_circuit_plan
833    // -----------------------------------------------------------------------
834
835    #[test]
836    fn test_small_circuit_plan() {
837        // A small circuit with non-Clifford gates should route to StateVector
838        // with no mitigation.
839        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        // Memory should be 2^5 * 16 = 512 bytes
863        assert_eq!(plan.predicted_memory_bytes, 512);
864        assert!(plan.predicted_runtime_ms > 0.0);
865        assert!(plan.confidence >= 0.9);
866    }
867
868    // -----------------------------------------------------------------------
869    // test_large_mps_plan
870    // -----------------------------------------------------------------------
871
872    #[test]
873    fn test_large_mps_plan() {
874        // A large circuit with nearest-neighbor connectivity and many
875        // non-Clifford gates (exceeding CT_MAX_T_COUNT) should route to
876        // TensorNetwork with an entanglement budget.
877        let mut circ = QuantumCircuit::new(64);
878        // Build a nearest-neighbor circuit with non-Clifford gates.
879        for q in 0..63 {
880            circ.cnot(q, q + 1);
881        }
882        // Use 50 T-gates to exceed CT_MAX_T_COUNT (40), forcing TensorNetwork.
883        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    // -----------------------------------------------------------------------
916    // test_memory_overflow_fallback
917    // -----------------------------------------------------------------------
918
919    #[test]
920    fn test_memory_overflow_fallback() {
921        // When StateVector would exceed available memory, the planner should
922        // fall back to TensorNetwork.
923        let mut circ = QuantumCircuit::new(30);
924        circ.h(0).t(1).cnot(0, 1);
925
926        // Give only 1 MiB of memory -- not enough for 2^30 * 16 = 16 GiB.
927        let config = PlannerConfig {
928            available_memory_bytes: 1024 * 1024, // 1 MiB
929            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        // The predicted memory for TN should fit within the budget.
941        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    // -----------------------------------------------------------------------
950    // test_noisy_circuit_plan
951    // -----------------------------------------------------------------------
952
953    #[test]
954    fn test_noisy_circuit_plan() {
955        // When noise_level > 0, the planner should add ZNE mitigation.
956        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), // medium noise
962            shot_budget: 10_000,
963            target_precision: 0.01,
964        };
965        let plan = plan_execution(&circ, &config);
966
967        // Should have ZNE mitigation.
968        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    // -----------------------------------------------------------------------
992    // test_entanglement_estimate
993    // -----------------------------------------------------------------------
994
995    #[test]
996    fn test_entanglement_estimate() {
997        // Bell state circuit: H on qubit 0, CNOT(0,1).
998        // One two-qubit gate crossing the single cut -> chi = 2.
999        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        // Single-qubit circuit: no entanglement possible.
1016        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        // Multi-qubit circuit but no two-qubit gates: bond dim = 1.
1028        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        // GHZ-like circuit: H(0), CNOT(0,1), CNOT(1,2), CNOT(2,3).
1040        // Each gate crosses one additional cut.
1041        // Cut 0-1: gates CNOT(0,1) = 1 crossing -> chi=2
1042        // Cut 1-2: gates CNOT(0,1) does not cross (both on same side),
1043        //          CNOT(1,2) crosses = 1 -> chi=2
1044        // Cut 2-3: CNOT(2,3) crosses = 1 -> chi=2
1045        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    // -----------------------------------------------------------------------
1056    // test_workload_routing_accuracy
1057    // -----------------------------------------------------------------------
1058
1059    #[test]
1060    fn test_workload_routing_accuracy() {
1061        let config = default_config();
1062
1063        // 1. Empty circuit: pure Clifford -> Stabilizer
1064        let circ_empty = QuantumCircuit::new(10);
1065        let plan = plan_execution(&circ_empty, &config);
1066        assert_eq!(plan.backend, BackendType::Stabilizer);
1067
1068        // 2. Single H gate: Clifford -> Stabilizer
1069        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        // 3. Bell state (Clifford) -> Stabilizer
1075        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        // 4. Small with T gate -> StateVector
1081        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        // 5. 20-qubit variational ansatz -> StateVector
1087        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        // 6. 40-qubit pure Clifford -> Stabilizer
1098        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        // 7. 100-qubit nearest-neighbor with many non-Clifford -> TensorNetwork
1109        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        // 8. 50-qubit mostly-Clifford (few non-Clifford) -> Stabilizer
1120        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        // Add only a handful of non-Clifford gates (< 10).
1128        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        // 9. Medium circuit (25 qubits) with non-Clifford -> StateVector
1139        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        // 10. Large circuit forced into TN by memory constraint.
1151        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, // absurdly small
1155            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        // 11. Very high noise should trigger full mitigation.
1167        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    // -----------------------------------------------------------------------
1192    // Memory prediction tests
1193    // -----------------------------------------------------------------------
1194
1195    #[test]
1196    fn test_memory_prediction_statevector() {
1197        assert_eq!(predict_memory_statevector(1), 32); // 2 * 16
1198        assert_eq!(predict_memory_statevector(10), 1024 * 16); // 2^10 * 16
1199        assert_eq!(predict_memory_statevector(20), 1048576 * 16); // 2^20 * 16
1200    }
1201
1202    #[test]
1203    fn test_memory_prediction_stabilizer() {
1204        // n^2 / 4
1205        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        // n * chi^2 * 16
1212        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    // -----------------------------------------------------------------------
1217    // Runtime prediction tests
1218    // -----------------------------------------------------------------------
1219
1220    #[test]
1221    fn test_runtime_prediction_statevector() {
1222        let rt = predict_runtime_statevector(10, 100);
1223        // 2^10 * 100 * 4ns = 409600 ns = ~0.41 ms
1224        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        // 100^2 * 200 * 0.1 ns = 200000 ns = 0.2 ms
1235        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        // 26 qubits: 2x the amplitudes and 2x the scaling factor = 4x total.
1247        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    // -----------------------------------------------------------------------
1255    // Cost breakdown tests
1256    // -----------------------------------------------------------------------
1257
1258    #[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    // -----------------------------------------------------------------------
1292    // Mitigation strategy selection tests
1293    // -----------------------------------------------------------------------
1294
1295    #[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    // -----------------------------------------------------------------------
1336    // Verification policy tests
1337    // -----------------------------------------------------------------------
1338
1339    #[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    // -----------------------------------------------------------------------
1381    // PlannerConfig default test
1382    // -----------------------------------------------------------------------
1383
1384    #[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    // -----------------------------------------------------------------------
1394    // ExecutionPlan explanation test
1395    // -----------------------------------------------------------------------
1396
1397    #[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    // -----------------------------------------------------------------------
1409    // Edge case: 0-qubit circuit
1410    // -----------------------------------------------------------------------
1411
1412    #[test]
1413    fn test_zero_qubit_circuit() {
1414        let circ = QuantumCircuit::new(0);
1415        let plan = plan_execution(&circ, &default_config());
1416        // Should not panic; stabilizer since it's "pure Clifford" (no gates).
1417        assert_eq!(plan.backend, BackendType::Stabilizer);
1418    }
1419
1420    // -----------------------------------------------------------------------
1421    // Helper: build a CircuitAnalysis stub for unit tests of sub-functions
1422    // -----------------------------------------------------------------------
1423
1424    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    // -----------------------------------------------------------------------
1450    // CliffordT routing test
1451    // -----------------------------------------------------------------------
1452
1453    #[test]
1454    fn test_clifford_t_routing() {
1455        // A large circuit with moderate T-count should route to CliffordT.
1456        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        // Add 15 T-gates (moderate count, 2^15 = 32768 terms).
1464        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}