Skip to main content

quantrs2_core/qml/
simulator.rs

1//! Lightweight exact state-vector simulator for QML circuits.
2//!
3//! QML primitives (variational classifiers, reinforcement-learning value/policy
4//! circuits, NLP models) build sequences of [`GateOp`] objects and need their
5//! *real* output statistics (measurement probabilities, Pauli-Z expectations)
6//! to compute losses and parameter-shift gradients. This module provides a
7//! small, dependency-free, exact simulator that applies arbitrary one- and
8//! two-qubit gates (via their dense `matrix()`) to a full `2^n` amplitude
9//! vector.
10//!
11//! # Qubit convention
12//!
13//! Qubit `q` corresponds to bit `q` of the basis-state index (little-endian),
14//! i.e. amplitude index `i` has qubit `q` in state `(i >> q) & 1`. This matches
15//! the convention used by the per-gate applicators in
16//! [`crate::qml::advanced_algorithms`] and the gate `matrix()` definitions in
17//! [`crate::gate::functions`].
18
19use crate::error::{QuantRS2Error, QuantRS2Result};
20use crate::gate::GateOp;
21use scirs2_core::ndarray::Array1;
22use scirs2_core::Complex64;
23
24/// Allocate the `|0…0⟩` computational-basis state for `num_qubits` qubits.
25#[must_use]
26pub fn zero_state(num_qubits: usize) -> Array1<Complex64> {
27    let dim = 1usize << num_qubits;
28    let mut state = Array1::zeros(dim);
29    state[0] = Complex64::new(1.0, 0.0);
30    state
31}
32
33/// Apply a single one- or two-qubit gate to `state` in place.
34///
35/// The gate's dense matrix (row-major, `2^k × 2^k` for a `k`-qubit gate) is
36/// applied to the sub-amplitudes selected by the gate's target qubits. Only
37/// one- and two-qubit gates are supported; larger gates return an honest error
38/// rather than silently leaving the state unchanged.
39pub fn apply_gate(state: &mut Array1<Complex64>, gate: &dyn GateOp) -> QuantRS2Result<()> {
40    let qubits = gate.qubits();
41    let matrix = gate.matrix()?;
42
43    match qubits.len() {
44        1 => apply_one_qubit(state, qubits[0].0 as usize, &matrix),
45        2 => apply_two_qubit(state, qubits[0].0 as usize, qubits[1].0 as usize, &matrix),
46        k => Err(QuantRS2Error::UnsupportedOperation(format!(
47            "state-vector simulator only supports 1- and 2-qubit gates, got {k}-qubit gate '{}'",
48            gate.name()
49        ))),
50    }
51}
52
53/// Apply a sequence of gates to a fresh `|0…0⟩` state and return the final
54/// amplitude vector.
55pub fn simulate(num_qubits: usize, gates: &[Box<dyn GateOp>]) -> QuantRS2Result<Array1<Complex64>> {
56    let mut state = zero_state(num_qubits);
57    for gate in gates {
58        apply_gate(&mut state, gate.as_ref())?;
59    }
60    Ok(state)
61}
62
63/// Apply a `2x2` matrix `[[m0, m1], [m2, m3]]` (row-major) to qubit `target`.
64fn apply_one_qubit(
65    state: &mut Array1<Complex64>,
66    target: usize,
67    matrix: &[Complex64],
68) -> QuantRS2Result<()> {
69    if matrix.len() != 4 {
70        return Err(QuantRS2Error::InvalidInput(format!(
71            "one-qubit gate matrix must have 4 entries, got {}",
72            matrix.len()
73        )));
74    }
75    let dim = state.len();
76    let bit = 1usize << target;
77    if bit >= dim {
78        return Err(QuantRS2Error::InvalidInput(format!(
79            "qubit index {target} out of range for {dim}-amplitude state"
80        )));
81    }
82
83    let mut idx = 0;
84    while idx < dim {
85        if idx & bit == 0 {
86            let i0 = idx;
87            let i1 = idx | bit;
88            let a = state[i0];
89            let b = state[i1];
90            state[i0] = matrix[0] * a + matrix[1] * b;
91            state[i1] = matrix[2] * a + matrix[3] * b;
92        }
93        idx += 1;
94    }
95    Ok(())
96}
97
98/// Apply a `4x4` matrix (row-major) to qubits `q_high`/`q_low`.
99///
100/// The matrix is indexed by the two-bit value `(b_first << 1) | b_second`,
101/// where `b_first` is the bit of `q_first` and `b_second` is the bit of
102/// `q_second` (matching the ordering produced by `gate.qubits()`).
103fn apply_two_qubit(
104    state: &mut Array1<Complex64>,
105    q_first: usize,
106    q_second: usize,
107    matrix: &[Complex64],
108) -> QuantRS2Result<()> {
109    if matrix.len() != 16 {
110        return Err(QuantRS2Error::InvalidInput(format!(
111            "two-qubit gate matrix must have 16 entries, got {}",
112            matrix.len()
113        )));
114    }
115    if q_first == q_second {
116        return Err(QuantRS2Error::InvalidInput(
117            "two-qubit gate requires two distinct qubits".to_string(),
118        ));
119    }
120    let dim = state.len();
121    let bit_first = 1usize << q_first;
122    let bit_second = 1usize << q_second;
123    if bit_first >= dim || bit_second >= dim {
124        return Err(QuantRS2Error::InvalidInput(format!(
125            "qubit index ({q_first},{q_second}) out of range for {dim}-amplitude state"
126        )));
127    }
128
129    let mut idx = 0;
130    while idx < dim {
131        // Only process indices where both target bits are 0; the other three
132        // members of the 2-qubit subspace are derived from this base.
133        if idx & bit_first == 0 && idx & bit_second == 0 {
134            let i00 = idx;
135            let i01 = idx | bit_second;
136            let i10 = idx | bit_first;
137            let i11 = idx | bit_first | bit_second;
138            let amps = [state[i00], state[i01], state[i10], state[i11]];
139            for (row, target_idx) in [i00, i01, i10, i11].into_iter().enumerate() {
140                let mut acc = Complex64::new(0.0, 0.0);
141                for (col, amp) in amps.iter().enumerate() {
142                    acc += matrix[row * 4 + col] * *amp;
143                }
144                state[target_idx] = acc;
145            }
146        }
147        idx += 1;
148    }
149    Ok(())
150}
151
152/// Probability of measuring qubit `target` in state `|1⟩`.
153#[must_use]
154pub fn probability_one(state: &Array1<Complex64>, target: usize) -> f64 {
155    let bit = 1usize << target;
156    state
157        .iter()
158        .enumerate()
159        .filter(|(i, _)| i & bit != 0)
160        .map(|(_, a)| a.norm_sqr())
161        .sum()
162}
163
164/// Expectation value of the Pauli-Z operator on qubit `target`,
165/// `⟨Z⟩ = P(|0⟩) − P(|1⟩) ∈ [−1, 1]`.
166#[must_use]
167pub fn expectation_z(state: &Array1<Complex64>, target: usize) -> f64 {
168    let p1 = probability_one(state, target);
169    1.0 - 2.0 * p1
170}
171
172/// Full probability distribution over all `2^n` computational basis states.
173#[must_use]
174pub fn probabilities(state: &Array1<Complex64>) -> Vec<f64> {
175    state.iter().map(scirs2_core::Complex::norm_sqr).collect()
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::gate::multi::CNOT;
182    use crate::gate::single::{Hadamard, PauliX, RotationY};
183    use crate::qubit::QubitId;
184
185    #[test]
186    fn test_pauli_x_flips_qubit() {
187        let mut state = zero_state(1);
188        let x = PauliX { target: QubitId(0) };
189        apply_gate(&mut state, &x).expect("apply X");
190        // |0> -> |1>
191        assert!((state[1].norm() - 1.0).abs() < 1e-12);
192        assert!(state[0].norm() < 1e-12);
193        assert!((expectation_z(&state, 0) + 1.0).abs() < 1e-12);
194    }
195
196    #[test]
197    fn test_hadamard_superposition() {
198        let mut state = zero_state(1);
199        let h = Hadamard { target: QubitId(0) };
200        apply_gate(&mut state, &h).expect("apply H");
201        assert!((probability_one(&state, 0) - 0.5).abs() < 1e-12);
202        assert!(expectation_z(&state, 0).abs() < 1e-12);
203    }
204
205    #[test]
206    fn test_bell_state_entanglement() {
207        // H on q0, CNOT(0->1) yields (|00> + |11>)/sqrt(2)
208        let gates: Vec<Box<dyn GateOp>> = vec![
209            Box::new(Hadamard { target: QubitId(0) }),
210            Box::new(CNOT {
211                control: QubitId(0),
212                target: QubitId(1),
213            }),
214        ];
215        let state = simulate(2, &gates).expect("simulate bell");
216        let probs = probabilities(&state);
217        assert!((probs[0b00] - 0.5).abs() < 1e-12);
218        assert!((probs[0b11] - 0.5).abs() < 1e-12);
219        assert!(probs[0b01] < 1e-12);
220        assert!(probs[0b10] < 1e-12);
221    }
222
223    #[test]
224    fn test_rotation_y_expectation_is_continuous() {
225        // RY(theta)|0> gives <Z> = cos(theta); verify it is a real, theta-dependent value.
226        let theta = 0.7;
227        let mut state = zero_state(1);
228        let ry = RotationY {
229            target: QubitId(0),
230            theta,
231        };
232        apply_gate(&mut state, &ry).expect("apply RY");
233        assert!((expectation_z(&state, 0) - theta.cos()).abs() < 1e-10);
234    }
235
236    #[test]
237    fn test_two_qubit_gate_on_high_index_qubits() {
238        // CNOT with control=1, target=0 on |10> (q1=1) -> |11>.
239        let mut state = zero_state(2);
240        // set |10>: q1 = 1
241        state[0] = Complex64::new(0.0, 0.0);
242        state[0b10] = Complex64::new(1.0, 0.0);
243        let cnot = CNOT {
244            control: QubitId(1),
245            target: QubitId(0),
246        };
247        apply_gate(&mut state, &cnot).expect("apply CNOT");
248        assert!((state[0b11].norm() - 1.0).abs() < 1e-12);
249    }
250}