Skip to main content

ruqu_core/
simulator.rs

1//! High-level simulator that executes quantum circuits
2
3use crate::circuit::QuantumCircuit;
4use crate::gate::Gate;
5use crate::state::QuantumState;
6use crate::types::*;
7use crate::error::Result;
8
9use rand::Rng;
10use std::collections::HashMap;
11use std::time::Instant;
12
13/// Configuration for a simulation run.
14pub struct SimConfig {
15    /// Deterministic seed. `None` uses OS entropy.
16    pub seed: Option<u64>,
17    /// Optional noise model applied after every gate.
18    pub noise: Option<NoiseModel>,
19    /// Number of repeated shots (`None` = single run returning state).
20    pub shots: Option<u32>,
21}
22
23impl Default for SimConfig {
24    fn default() -> Self {
25        Self {
26            seed: None,
27            noise: None,
28            shots: None,
29        }
30    }
31}
32
33/// Result of a single simulation run (state + measurements).
34pub struct SimulationResult {
35    pub state: QuantumState,
36    pub measurements: Vec<MeasurementOutcome>,
37    pub metrics: SimulationMetrics,
38}
39
40/// Result of a multi-shot simulation (histogram of outcomes).
41pub struct ShotResult {
42    pub counts: HashMap<Vec<bool>, usize>,
43    pub metrics: SimulationMetrics,
44}
45
46/// Stateless simulator entry-point.
47pub struct Simulator;
48
49impl Simulator {
50    /// Run a circuit once with default configuration.
51    pub fn run(circuit: &QuantumCircuit) -> Result<SimulationResult> {
52        Self::run_with_config(circuit, &SimConfig::default())
53    }
54
55    /// Run a circuit once with explicit configuration.
56    pub fn run_with_config(
57        circuit: &QuantumCircuit,
58        config: &SimConfig,
59    ) -> Result<SimulationResult> {
60        let start = Instant::now();
61
62        let mut state = match config.seed {
63            Some(seed) => QuantumState::new_with_seed(circuit.num_qubits(), seed)?,
64            None => QuantumState::new(circuit.num_qubits())?,
65        };
66
67        let mut measurements = Vec::new();
68        let mut gate_count: usize = 0;
69
70        for gate in circuit.gates() {
71            let outcomes = state.apply_gate(gate)?;
72            measurements.extend(outcomes);
73            if !gate.is_non_unitary() {
74                gate_count += 1;
75            }
76            // Apply noise channel after each gate when a model is provided.
77            if let Some(ref noise) = config.noise {
78                apply_noise(&mut state, gate, noise);
79            }
80        }
81
82        let elapsed = start.elapsed();
83        let metrics = SimulationMetrics {
84            num_qubits: circuit.num_qubits(),
85            gate_count,
86            execution_time_ns: elapsed.as_nanos() as u64,
87            peak_memory_bytes: QuantumState::estimate_memory(circuit.num_qubits()),
88            gates_per_second: if elapsed.as_secs_f64() > 0.0 {
89                gate_count as f64 / elapsed.as_secs_f64()
90            } else {
91                0.0
92            },
93            gates_fused: 0,
94        };
95
96        Ok(SimulationResult {
97            state,
98            measurements,
99            metrics,
100        })
101    }
102
103    /// Run a circuit `shots` times, collecting a histogram of measurement outcomes.
104    ///
105    /// If the circuit contains no `Measure` gates, all qubits are measured
106    /// automatically at the end of each shot.
107    pub fn run_shots(
108        circuit: &QuantumCircuit,
109        shots: u32,
110        seed: Option<u64>,
111    ) -> Result<ShotResult> {
112        let start = Instant::now();
113        let mut counts: HashMap<Vec<bool>, usize> = HashMap::new();
114        let base_seed = seed.unwrap_or(42);
115        let mut total_gates: usize = 0;
116        let n_qubits = circuit.num_qubits();
117
118        let has_measurements = circuit
119            .gates()
120            .iter()
121            .any(|g| matches!(g, Gate::Measure(_)));
122
123        for shot in 0..shots {
124            let config = SimConfig {
125                seed: Some(base_seed.wrapping_add(shot as u64)),
126                noise: None,
127                shots: None,
128            };
129
130            let mut result = Self::run_with_config(circuit, &config)?;
131            total_gates += result.metrics.gate_count;
132
133            // Implicit measurement when the circuit has none.
134            if !has_measurements {
135                let outcomes = result.state.measure_all()?;
136                result.measurements.extend(outcomes);
137            }
138
139            // Build a bit-vector keyed by qubit index.
140            let mut bits = vec![false; n_qubits as usize];
141            for m in &result.measurements {
142                if (m.qubit as usize) < bits.len() {
143                    bits[m.qubit as usize] = m.result;
144                }
145            }
146            *counts.entry(bits).or_insert(0) += 1;
147        }
148
149        let elapsed = start.elapsed();
150        let metrics = SimulationMetrics {
151            num_qubits: n_qubits,
152            gate_count: total_gates,
153            execution_time_ns: elapsed.as_nanos() as u64,
154            peak_memory_bytes: QuantumState::estimate_memory(n_qubits),
155            gates_per_second: if elapsed.as_secs_f64() > 0.0 {
156                total_gates as f64 / elapsed.as_secs_f64()
157            } else {
158                0.0
159            },
160            gates_fused: 0,
161        };
162
163        Ok(ShotResult { counts, metrics })
164    }
165}
166
167// ---------------------------------------------------------------------------
168// Noise channel
169// ---------------------------------------------------------------------------
170
171/// Apply a stochastic noise channel to the state after a gate.
172///
173/// For each qubit that the gate touches:
174///   - with probability `depolarizing_rate`, apply a random Pauli (X, Y, or Z
175///     each with probability 1/3);
176///   - with probability `bit_flip_rate`, apply X;
177///   - with probability `phase_flip_rate`, apply Z.
178fn apply_noise(state: &mut QuantumState, gate: &Gate, noise: &NoiseModel) {
179    let qubits = gate.qubits();
180    if qubits.is_empty() {
181        return;
182    }
183
184    for &qubit in &qubits {
185        // Depolarising channel
186        if noise.depolarizing_rate > 0.0 {
187            let r: f64 = state.rng_mut().gen();
188            if r < noise.depolarizing_rate {
189                let choice: f64 = state.rng_mut().gen();
190                let pauli = if choice < 1.0 / 3.0 {
191                    Gate::X(qubit)
192                } else if choice < 2.0 / 3.0 {
193                    Gate::Y(qubit)
194                } else {
195                    Gate::Z(qubit)
196                };
197                if let Some(m) = pauli.matrix_1q() {
198                    state.apply_single_qubit_gate(qubit, &m);
199                }
200            }
201        }
202
203        // Bit-flip channel
204        if noise.bit_flip_rate > 0.0 {
205            let r: f64 = state.rng_mut().gen();
206            if r < noise.bit_flip_rate {
207                let m = Gate::X(qubit).matrix_1q().unwrap();
208                state.apply_single_qubit_gate(qubit, &m);
209            }
210        }
211
212        // Phase-flip channel
213        if noise.phase_flip_rate > 0.0 {
214            let r: f64 = state.rng_mut().gen();
215            if r < noise.phase_flip_rate {
216                let m = Gate::Z(qubit).matrix_1q().unwrap();
217                state.apply_single_qubit_gate(qubit, &m);
218            }
219        }
220    }
221}