Skip to main content

ruqu_core/
backend.rs

1//! Unified simulation backend trait and automatic backend selection.
2//!
3//! ruqu-core supports multiple simulation backends, each optimal for
4//! different circuit structures:
5//!
6//! | Backend | Qubits | Best for |
7//! |---------|--------|----------|
8//! | StateVector | up to ~32 | General circuits, exact simulation |
9//! | Stabilizer | millions | Clifford circuits + measurement |
10//! | TensorNetwork | hundreds-thousands | Low-depth, local connectivity |
11
12use crate::circuit::QuantumCircuit;
13use crate::gate::Gate;
14
15// ---------------------------------------------------------------------------
16// Backend type enum
17// ---------------------------------------------------------------------------
18
19/// Which backend to use for simulation.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum BackendType {
22    /// Dense state-vector (exact, up to ~32 qubits).
23    StateVector,
24    /// Aaronson-Gottesman stabilizer tableau (Clifford-only, millions of qubits).
25    Stabilizer,
26    /// Matrix Product State tensor network (bounded entanglement, hundreds+).
27    TensorNetwork,
28    /// Clifford+T stabilizer rank decomposition (moderate T-count, many qubits).
29    CliffordT,
30    /// Automatically select the best backend based on circuit analysis.
31    Auto,
32}
33
34// ---------------------------------------------------------------------------
35// Circuit analysis result
36// ---------------------------------------------------------------------------
37
38/// Result of circuit analysis, used for backend selection.
39///
40/// Produced by [`analyze_circuit`] and contains both raw statistics about the
41/// circuit (gate counts, depth, connectivity) and a recommended backend with
42/// a confidence score and human-readable explanation.
43#[derive(Debug, Clone)]
44pub struct CircuitAnalysis {
45    /// Number of qubits in the circuit.
46    pub num_qubits: u32,
47    /// Total number of gates.
48    pub total_gates: usize,
49    /// Number of Clifford gates (H, S, CNOT, CZ, SWAP, X, Y, Z, Sdg).
50    pub clifford_gates: usize,
51    /// Number of non-Clifford gates (T, Tdg, Rx, Ry, Rz, Phase, Rzz, Unitary1Q).
52    pub non_clifford_gates: usize,
53    /// Fraction of unitary gates that are Clifford (0.0 to 1.0).
54    pub clifford_fraction: f64,
55    /// Number of measurement gates.
56    pub measurement_gates: usize,
57    /// Circuit depth (longest qubit timeline).
58    pub depth: u32,
59    /// Maximum qubit distance in any two-qubit gate.
60    pub max_connectivity: u32,
61    /// Whether all two-qubit gates are between adjacent qubits.
62    pub is_nearest_neighbor: bool,
63    /// Recommended backend based on the analysis heuristics.
64    pub recommended_backend: BackendType,
65    /// Confidence in the recommendation (0.0 to 1.0).
66    pub confidence: f64,
67    /// Human-readable explanation of the recommendation.
68    pub explanation: String,
69}
70
71// ---------------------------------------------------------------------------
72// Public analysis entry point
73// ---------------------------------------------------------------------------
74
75/// Analyze a quantum circuit to determine the optimal simulation backend.
76///
77/// Walks the gate list once to collect statistics, then applies a series of
78/// heuristic rules to recommend a [`BackendType`]. The returned
79/// [`CircuitAnalysis`] contains both the raw numbers and the recommendation.
80///
81/// # Example
82///
83/// ```
84/// use ruqu_core::circuit::QuantumCircuit;
85/// use ruqu_core::backend::{analyze_circuit, BackendType};
86///
87/// // A small circuit with a non-Clifford gate routes to StateVector.
88/// let mut circ = QuantumCircuit::new(3);
89/// circ.h(0).t(1).cnot(0, 1);
90/// let analysis = analyze_circuit(&circ);
91/// assert_eq!(analysis.recommended_backend, BackendType::StateVector);
92/// ```
93pub fn analyze_circuit(circuit: &QuantumCircuit) -> CircuitAnalysis {
94    let num_qubits = circuit.num_qubits();
95    let gates = circuit.gates();
96    let total_gates = gates.len();
97
98    let mut clifford_gates = 0usize;
99    let mut non_clifford_gates = 0usize;
100    let mut measurement_gates = 0usize;
101    let mut max_connectivity: u32 = 0;
102    let mut is_nearest_neighbor = true;
103
104    for gate in gates {
105        match gate {
106            // Clifford gates
107            Gate::H(_)
108            | Gate::X(_)
109            | Gate::Y(_)
110            | Gate::Z(_)
111            | Gate::S(_)
112            | Gate::Sdg(_)
113            | Gate::CNOT(_, _)
114            | Gate::CZ(_, _)
115            | Gate::SWAP(_, _) => {
116                clifford_gates += 1;
117            }
118            // Non-Clifford gates
119            Gate::T(_)
120            | Gate::Tdg(_)
121            | Gate::Rx(_, _)
122            | Gate::Ry(_, _)
123            | Gate::Rz(_, _)
124            | Gate::Phase(_, _)
125            | Gate::Rzz(_, _, _)
126            | Gate::Unitary1Q(_, _) => {
127                non_clifford_gates += 1;
128            }
129            Gate::Measure(_) => {
130                measurement_gates += 1;
131            }
132            Gate::Reset(_) | Gate::Barrier => {}
133        }
134
135        // Check connectivity for two-qubit gates.
136        let qubits = gate.qubits();
137        if qubits.len() == 2 {
138            let dist = if qubits[0] > qubits[1] {
139                qubits[0] - qubits[1]
140            } else {
141                qubits[1] - qubits[0]
142            };
143            if dist > max_connectivity {
144                max_connectivity = dist;
145            }
146            if dist > 1 {
147                is_nearest_neighbor = false;
148            }
149        }
150    }
151
152    let unitary_gates = clifford_gates + non_clifford_gates;
153    let clifford_fraction = if unitary_gates > 0 {
154        clifford_gates as f64 / unitary_gates as f64
155    } else {
156        1.0
157    };
158
159    let depth = circuit.depth();
160
161    // Decide which backend fits best.
162    let (recommended_backend, confidence, explanation) = select_backend(
163        num_qubits,
164        clifford_fraction,
165        non_clifford_gates,
166        depth,
167        is_nearest_neighbor,
168        max_connectivity,
169    );
170
171    CircuitAnalysis {
172        num_qubits,
173        total_gates,
174        clifford_gates,
175        non_clifford_gates,
176        clifford_fraction,
177        measurement_gates,
178        depth,
179        max_connectivity,
180        is_nearest_neighbor,
181        recommended_backend,
182        confidence,
183        explanation,
184    }
185}
186
187// ---------------------------------------------------------------------------
188// Internal selection heuristics
189// ---------------------------------------------------------------------------
190
191/// Internal backend selection logic.
192///
193/// Returns `(backend, confidence, explanation)` based on a priority-ordered
194/// set of heuristic rules.
195fn select_backend(
196    num_qubits: u32,
197    clifford_fraction: f64,
198    non_clifford_gates: usize,
199    depth: u32,
200    is_nearest_neighbor: bool,
201    max_connectivity: u32,
202) -> (BackendType, f64, String) {
203    // Rule 1: Pure Clifford circuits -> Stabilizer (any size).
204    if clifford_fraction >= 1.0 {
205        return (
206            BackendType::Stabilizer,
207            0.99,
208            format!(
209                "Pure Clifford circuit: stabilizer backend handles {} qubits in O(n^2) per gate",
210                num_qubits
211            ),
212        );
213    }
214
215    // Rule 2: Mostly Clifford with very few non-Clifford gates and too many
216    // qubits for state vector -> Stabilizer with approximate decomposition.
217    if clifford_fraction >= 0.95 && num_qubits > 32 && non_clifford_gates <= 10 {
218        return (
219            BackendType::Stabilizer,
220            0.85,
221            format!(
222                "{}% Clifford with only {} non-Clifford gates: \
223                 stabilizer backend recommended for {} qubits",
224                (clifford_fraction * 100.0) as u32,
225                non_clifford_gates,
226                num_qubits
227            ),
228        );
229    }
230
231    // Rule 3: Small enough for state vector -> use it (exact, comfortable).
232    if num_qubits <= 25 {
233        return (
234            BackendType::StateVector,
235            0.95,
236            format!(
237                "{} qubits fits comfortably in state vector ({})",
238                num_qubits,
239                format_memory(num_qubits)
240            ),
241        );
242    }
243
244    // Rule 4: State vector possible but tight on memory.
245    if num_qubits <= 32 {
246        return (
247            BackendType::StateVector,
248            0.80,
249            format!(
250                "{} qubits requires {} for state vector - verify available memory",
251                num_qubits,
252                format_memory(num_qubits)
253            ),
254        );
255    }
256
257    // Rule 5: Low depth, local connectivity -> tensor network.
258    if is_nearest_neighbor && depth < num_qubits * 2 {
259        return (
260            BackendType::TensorNetwork,
261            0.85,
262            format!(
263                "Nearest-neighbor connectivity with depth {} on {} qubits: \
264                 tensor network efficient",
265                depth, num_qubits
266            ),
267        );
268    }
269
270    // Rule 6: General large circuit -> tensor network as best approximation.
271    if num_qubits > 32 {
272        let conf = if is_nearest_neighbor { 0.75 } else { 0.55 };
273        return (
274            BackendType::TensorNetwork,
275            conf,
276            format!(
277                "{} qubits exceeds state vector capacity. \
278                 Tensor network with connectivity {} - results are approximate",
279                num_qubits, max_connectivity
280            ),
281        );
282    }
283
284    // Fallback: exact state vector simulation.
285    (
286        BackendType::StateVector,
287        0.70,
288        "Default to exact state vector simulation".into(),
289    )
290}
291
292// ---------------------------------------------------------------------------
293// Memory formatting helper
294// ---------------------------------------------------------------------------
295
296/// Format the state-vector memory requirement for a given qubit count.
297///
298/// Each amplitude is a `Complex` (16 bytes), and there are `2^n` of them.
299fn format_memory(num_qubits: u32) -> String {
300    // Use u128 to avoid overflow for up to 127 qubits.
301    let bytes = (1u128 << num_qubits) * 16;
302    if bytes >= 1 << 40 {
303        format!("{:.1} TiB", bytes as f64 / (1u128 << 40) as f64)
304    } else if bytes >= 1 << 30 {
305        format!("{:.1} GiB", bytes as f64 / (1u128 << 30) as f64)
306    } else if bytes >= 1 << 20 {
307        format!("{:.1} MiB", bytes as f64 / (1u128 << 20) as f64)
308    } else {
309        format!("{} bytes", bytes)
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Scaling information
315// ---------------------------------------------------------------------------
316
317/// Scaling characteristics for a single simulation backend.
318#[derive(Debug, Clone)]
319pub struct ScalingInfo {
320    /// The backend this info describes.
321    pub backend: BackendType,
322    /// Maximum qubits for exact (zero-error) simulation.
323    pub max_qubits_exact: u32,
324    /// Maximum qubits for approximate simulation with truncation.
325    pub max_qubits_approximate: u32,
326    /// Time complexity in big-O notation.
327    pub time_complexity: String,
328    /// Space complexity in big-O notation.
329    pub space_complexity: String,
330}
331
332/// Get scaling information for all supported backends.
333///
334/// Returns a `Vec` with one [`ScalingInfo`] per backend (StateVector,
335/// Stabilizer, TensorNetwork, CliffordT) in that order.
336pub fn scaling_report() -> Vec<ScalingInfo> {
337    vec![
338        ScalingInfo {
339            backend: BackendType::StateVector,
340            max_qubits_exact: 32,
341            max_qubits_approximate: 36,
342            time_complexity: "O(2^n * gates)".into(),
343            space_complexity: "O(2^n)".into(),
344        },
345        ScalingInfo {
346            backend: BackendType::Stabilizer,
347            max_qubits_exact: 10_000_000,
348            max_qubits_approximate: 10_000_000,
349            time_complexity: "O(n^2 * gates) for Clifford".into(),
350            space_complexity: "O(n^2)".into(),
351        },
352        ScalingInfo {
353            backend: BackendType::TensorNetwork,
354            max_qubits_exact: 100,
355            max_qubits_approximate: 10_000,
356            time_complexity: "O(n * chi^3 * gates)".into(),
357            space_complexity: "O(n * chi^2)".into(),
358        },
359        ScalingInfo {
360            backend: BackendType::CliffordT,
361            max_qubits_exact: 1000,
362            max_qubits_approximate: 10_000,
363            time_complexity: "O(2^t * n^2 * gates) for t T-gates".into(),
364            space_complexity: "O(2^t * n^2)".into(),
365        },
366    ]
367}
368
369// ---------------------------------------------------------------------------
370// Tests
371// ---------------------------------------------------------------------------
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::circuit::QuantumCircuit;
377
378    #[test]
379    fn pure_clifford_selects_stabilizer() {
380        let mut circ = QuantumCircuit::new(50);
381        for q in 0..50 {
382            circ.h(q);
383        }
384        for q in 0..49 {
385            circ.cnot(q, q + 1);
386        }
387        let analysis = analyze_circuit(&circ);
388        assert_eq!(analysis.recommended_backend, BackendType::Stabilizer);
389        assert!(analysis.clifford_fraction >= 1.0);
390        assert!(analysis.confidence > 0.9);
391    }
392
393    #[test]
394    fn small_circuit_selects_state_vector() {
395        let mut circ = QuantumCircuit::new(5);
396        circ.h(0).t(1).cnot(0, 1);
397        let analysis = analyze_circuit(&circ);
398        assert_eq!(analysis.recommended_backend, BackendType::StateVector);
399        assert!(analysis.confidence > 0.9);
400    }
401
402    #[test]
403    fn medium_circuit_selects_state_vector() {
404        let mut circ = QuantumCircuit::new(30);
405        circ.h(0).rx(1, 1.0).cnot(0, 1);
406        let analysis = analyze_circuit(&circ);
407        assert_eq!(analysis.recommended_backend, BackendType::StateVector);
408        assert!(analysis.confidence >= 0.80);
409    }
410
411    #[test]
412    fn large_nearest_neighbor_selects_tensor_network() {
413        let mut circ = QuantumCircuit::new(64);
414        // Low depth, nearest-neighbor only.
415        for q in 0..63 {
416            circ.cnot(q, q + 1);
417        }
418        // Add enough non-Clifford gates to avoid the "mostly Clifford" Rule 2
419        // (which requires non_clifford_gates <= 10).
420        for q in 0..12 {
421            circ.t(q);
422        }
423        let analysis = analyze_circuit(&circ);
424        assert_eq!(analysis.recommended_backend, BackendType::TensorNetwork);
425    }
426
427    #[test]
428    fn empty_circuit_defaults() {
429        let circ = QuantumCircuit::new(10);
430        let analysis = analyze_circuit(&circ);
431        // Empty circuit is "pure Clifford" (no non-Clifford gates).
432        assert_eq!(analysis.total_gates, 0);
433        assert!(analysis.clifford_fraction >= 1.0);
434    }
435
436    #[test]
437    fn measurement_counted() {
438        let mut circ = QuantumCircuit::new(3);
439        circ.h(0).measure(0).measure(1).measure(2);
440        let analysis = analyze_circuit(&circ);
441        assert_eq!(analysis.measurement_gates, 3);
442    }
443
444    #[test]
445    fn connectivity_detected() {
446        let mut circ = QuantumCircuit::new(10);
447        circ.cnot(0, 5); // distance = 5
448        let analysis = analyze_circuit(&circ);
449        assert_eq!(analysis.max_connectivity, 5);
450        assert!(!analysis.is_nearest_neighbor);
451    }
452
453    #[test]
454    fn scaling_report_has_four_entries() {
455        let report = scaling_report();
456        assert_eq!(report.len(), 4);
457        assert_eq!(report[0].backend, BackendType::StateVector);
458        assert_eq!(report[1].backend, BackendType::Stabilizer);
459        assert_eq!(report[2].backend, BackendType::TensorNetwork);
460        assert_eq!(report[3].backend, BackendType::CliffordT);
461    }
462
463    #[test]
464    fn format_memory_values() {
465        // 10 qubits => 2^10 * 16 = 16384 bytes
466        assert_eq!(format_memory(10), "16384 bytes");
467        // 20 qubits => 2^20 * 16 = 16 MiB
468        assert_eq!(format_memory(20), "16.0 MiB");
469        // 30 qubits => 2^30 * 16 = 16 GiB
470        assert_eq!(format_memory(30), "16.0 GiB");
471    }
472}