From 1865590576f891d696d6dc4810bf1b1e51a66c64 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 10 Dec 2024 15:43:08 -0700 Subject: [PATCH 01/33] Initial Pauli prop code add --- crates/pecos-qsim/src/pauli_prop.rs | 223 ++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 crates/pecos-qsim/src/pauli_prop.rs diff --git a/crates/pecos-qsim/src/pauli_prop.rs b/crates/pecos-qsim/src/pauli_prop.rs new file mode 100644 index 00000000..d9928ddb --- /dev/null +++ b/crates/pecos-qsim/src/pauli_prop.rs @@ -0,0 +1,223 @@ +// Copyright 2024 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. +#![allow(unused_variables)] + +use super::clifford_simulator::CliffordSimulator; +use crate::quantum_simulator::QuantumSimulator; +use core::marker::PhantomData; +use pecos_core::{IndexableElement, Set, VecSet}; + +// TODO: Allow for the use of sets of elements of types other than usize + +#[expect(clippy::module_name_repetitions)] +pub type StdPauliProp = PauliProp, usize>; + +#[derive(Clone, Debug)] +pub struct PauliProp +where + T: for<'a> Set<'a, Element = E>, + E: IndexableElement, +{ + num_qubits: usize, + xs: T, + zs: T, + _marker: PhantomData, +} + +impl QuantumSimulator for PauliProp +where + E: IndexableElement, + T: for<'a> Set<'a, Element = E>, +{ + #[inline] + #[must_use] + fn new(num_qubits: usize) -> PauliProp { + Self { + num_qubits, + xs: T::new(), + zs: T::new(), + _marker: PhantomData, + } + } + + #[inline] + fn num_qubits(&self) -> usize { + self.num_qubits + } + + #[inline] + fn reset(&mut self) -> &mut Self { + todo!() + } +} + +impl PauliProp +where + T: for<'a> Set<'a, Element = E>, + E: IndexableElement, +{ + #[inline] + pub fn insert_x(&mut self, item: E) { + self.xs.insert(item); + } + + #[inline] + pub fn insert_z(&mut self, item: E) { + self.zs.insert(item); + } + + #[inline] + pub fn insert_y(&mut self, item: E) { + self.insert_x(item); + self.insert_z(item); + } + + #[inline] + #[expect(clippy::similar_names)] + pub fn h_v2(&mut self, q: E) { + let in_xs = self.xs.contains(&q); + let in_zs = self.zs.contains(&q); + + if in_xs && !in_zs { + self.xs.remove(&q); + self.zs.insert(q); + } else if !in_xs && in_zs { + self.zs.remove(&q); + self.xs.insert(q); + } else if in_xs && in_zs { + return; + } + } +} + +impl CliffordSimulator for PauliProp +where + T: for<'a> Set<'a, Element = E>, + E: IndexableElement, +{ + #[inline] + fn px(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn pnx(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn py(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn pny(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn pz(&mut self, q: E) -> (bool, bool) { + self.xs.remove(&q); + self.zs.remove(&q); + (true, true) // TODO: fix... + } + + #[inline] + fn pnz(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn mx(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn mnx(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn my(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn mny(&mut self, q: E) -> (bool, bool) { + todo!() + } + + /// Output true if there is an X on the qubit. + #[inline] + fn mz(&mut self, q: E) -> (bool, bool) { + (self.xs.contains(&q), true) // TODO: Is deterministic a useful value... maybe, maybe not... fix + } + + #[inline] + fn mnz(&mut self, q: E) -> (bool, bool) { + todo!() + } + + #[inline] + fn identity(&mut self, _q: E) {} + + #[inline] + fn x(&mut self, q: E) { + todo!() + } + + #[inline] + fn y(&mut self, q: E) { + todo!() + } + + #[inline] + fn z(&mut self, q: E) { + todo!() + } + + /// X -> Y, Z -> Z, Y -> -X + #[inline] + fn sz(&mut self, q: E) { + if self.xs.contains(&q) { + self.zs.symmetric_difference_item_update(&q); + } + } + + /// X -> Z, Z -> X, Y -> -Y + #[inline] + #[expect(clippy::similar_names)] + fn h(&mut self, q: E) { + let in_xs = self.xs.contains(&q); + let in_zs = self.zs.contains(&q); + + if in_xs && in_zs { + } else if in_xs { + self.xs.remove(&q); + self.zs.insert(q); + } else if in_zs { + self.zs.remove(&q); + self.xs.insert(q); + } + } + + /// XI -> XX, ZI -> ZI, IX -> IX, IZ -> ZZ + #[inline] + fn cx(&mut self, q1: E, q2: E) { + if self.xs.contains(&q1) { + self.xs.symmetric_difference_item_update(&q2); + } + if self.zs.contains(&q2) { + self.zs.symmetric_difference_item_update(&q1); + } + } +} From 00a9064d75984245225de95b447806f4e85eea3e Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 17 Dec 2024 19:19:19 -0700 Subject: [PATCH 02/33] Add Rust state vector --- Cargo.lock | 21 + Cargo.toml | 1 + crates/pecos-qsim/Cargo.toml | 6 + crates/pecos-qsim/src/lib.rs | 2 + crates/pecos-qsim/src/statevec.rs | 1517 +++++++++++++++++++++++++++++ 5 files changed, 1547 insertions(+) create mode 100644 crates/pecos-qsim/src/statevec.rs diff --git a/Cargo.lock b/Cargo.lock index eca8f7c8..0cb6b1c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,6 +115,15 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -1040,6 +1049,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1187,7 +1205,10 @@ dependencies = [ name = "pecos-qsim" version = "0.1.1" dependencies = [ + "approx", + "num-complex", "pecos-core", + "rand", "rand_chacha", ] diff --git a/Cargo.toml b/Cargo.toml index 39b90cc8..92334b2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ pyo3 = "0.23" rand = "0.8" rand_chacha = "0.3" rand_xoshiro = "0.6" +num-complex = "0.4" pecos-core = { version = "0.1.1", path = "crates/pecos-core" } pecos-qsim = { version = "0.1.1", path = "crates/pecos-qsim" } diff --git a/crates/pecos-qsim/Cargo.toml b/crates/pecos-qsim/Cargo.toml index d2a0a8fa..a23a8c00 100644 --- a/crates/pecos-qsim/Cargo.toml +++ b/crates/pecos-qsim/Cargo.toml @@ -12,7 +12,13 @@ description = "Provides simulators and related elements for PECOS simulations." [dependencies] pecos-core = { workspace = true } +rand = { workspace = true } rand_chacha = { workspace = true } +num-complex = { workspace = true } + +[dev-dependencies] +approx = "0.5" [lints] workspace = true + diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index 6f06c952..0dec2ddd 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -18,6 +18,7 @@ pub mod gens; // pub mod paulis; pub mod quantum_simulator; pub mod sparse_stab; +pub mod statevec; pub use clifford_simulator::CliffordSimulator; pub use gens::Gens; @@ -27,3 +28,4 @@ pub use gens::Gens; // pub use paulis::Paulis; pub use quantum_simulator::QuantumSimulator; pub use sparse_stab::SparseStab; +pub use statevec::StateVec; diff --git a/crates/pecos-qsim/src/statevec.rs b/crates/pecos-qsim/src/statevec.rs new file mode 100644 index 00000000..da0eccb5 --- /dev/null +++ b/crates/pecos-qsim/src/statevec.rs @@ -0,0 +1,1517 @@ +use num_complex::Complex64; +use rand::Rng; + +pub struct StateVec { + num_qubits: usize, + state: Vec, +} + +impl StateVec { + /// Create a new state initialized to |0...0⟩ + #[must_use] pub fn new(num_qubits: usize) -> Self { + let size = 1 << num_qubits; // 2^n + let mut state = vec![Complex64::new(0.0, 0.0); size]; + state[0] = Complex64::new(1.0, 0.0); // Prep |0...0> + StateVec { num_qubits, state } + } + + /// Initialize from a custom state vector + #[must_use] pub fn from_state(state: Vec) -> Self { + let num_qubits = (state.len() as f64).log2() as usize; + assert_eq!(1 << num_qubits, state.len(), "Invalid state vector size"); + StateVec { num_qubits, state } + } + + /// Apply Hadamard gate + pub fn hadamard(&mut self, target: usize) { + assert!(target < self.num_qubits); + let factor = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0); + let step = 1 << target; + + for i in (0..self.state.len()).step_by(2 * step) { + for offset in 0..step { + let j = i + offset; + let paired_j = j ^ step; + + let a = self.state[j]; + let b = self.state[paired_j]; + + self.state[j] = factor * (a + b); + self.state[paired_j] = factor * (a - b); + } + } + } + + /// Apply Pauli-X gate + pub fn x(&mut self, target: usize) { + assert!(target < self.num_qubits); + let step = 1 << target; + + for i in (0..self.state.len()).step_by(2 * step) { + for offset in 0..step { + self.state.swap(i + offset, i + offset + step); + } + } + } + + /// Apply Y = [[0, -i], [i, 0]] gate to target qubit + pub fn y(&mut self, target: usize) { + assert!(target < self.num_qubits); + + for i in 0..self.state.len() { + if (i >> target) & 1 == 0 { + let flipped_i = i ^ (1 << target); + let temp = self.state[i]; + self.state[i] = -Complex64::i() * self.state[flipped_i]; + self.state[flipped_i] = Complex64::i() * temp; + } + } + } + + /// Apply Z = [[1, 0], [0, -1]] gate to target qubit + pub fn z(&mut self, target: usize) { + assert!(target < self.num_qubits); + + for i in 0..self.state.len() { + if (i >> target) & 1 == 1 { + self.state[i] = -self.state[i]; + } + } + } + + /// Gate RX(θ) = exp(-i θ X/2) = cos(θ/2) I - i*sin(θ/2) X + /// RX(θ) = [[cos(θ/2), -i*sin(θ/2)], + /// [-i*sin(θ/2), cos(θ/2)]] + pub fn rx(&mut self, theta: f64, target: usize) { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + let neg_i_sin = Complex64::new(0.0, -sin); + + self.single_qubit_rotation( + target, + Complex64::new(cos, 0.0), // u00 + neg_i_sin, // u01 + neg_i_sin, // u10 + Complex64::new(cos, 0.0), // u11 + ); + } + + /// Gate RY(θ) = exp(-i θ Y/2) = cos(θ/2) I - i*sin(θ/2) Y + /// RY(θ) = [[cos(θ/2), -sin(θ/2)], + /// [-sin(θ/2), cos(θ/2)]] + pub fn ry(&mut self, theta: f64, target: usize) { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + + self.single_qubit_rotation( + target, + Complex64::new(cos, 0.0), // u00 + Complex64::new(-sin, 0.0), // u01 + Complex64::new(sin, 0.0), // u10 + Complex64::new(cos, 0.0), // u11 + ); + } + + /// Gate RZ(θ) = exp(-i θ Z/2) = cos(θ/2) I - i*sin(θ/2) Z + /// RZ(θ) = [[cos(θ/2)-i*sin(θ/2), 0], + /// [0, cos(θ/2)+i*sin(θ/2)]] + pub fn rz(&mut self, theta: f64, target: usize) { + let exp_minus_i_theta_2 = Complex64::from_polar(1.0, -theta / 2.0); + let exp_plus_i_theta_2 = Complex64::from_polar(1.0, theta / 2.0); + + self.single_qubit_rotation( + target, + exp_minus_i_theta_2, // u00 + Complex64::new(0.0, 0.0), // u01 + Complex64::new(0.0, 0.0), // u10 + exp_plus_i_theta_2, // u11 + ); + } + + pub fn single_qubit_rotation( + &mut self, + target: usize, + u00: Complex64, + u01: Complex64, + u10: Complex64, + u11: Complex64, + ) { + assert!(target < self.num_qubits); + + let step = 1 << target; + for i in (0..self.state.len()).step_by(2 * step) { + for offset in 0..step { + let j = i + offset; + let k = j ^ step; + + let a = self.state[j]; + let b = self.state[k]; + + self.state[j] = u00 * a + u01 * b; + self.state[k] = u10 * a + u11 * b; + } + } + } + + /// Apply a SWAP gate between two qubits + pub fn swap(&mut self, qubit1: usize, qubit2: usize) { + assert!(qubit1 < self.num_qubits && qubit2 < self.num_qubits); + if qubit1 == qubit2 { + return; // No-op if qubits are the same + } + + let step1 = 1 << qubit1; + let step2 = 1 << qubit2; + + for i in 0..self.state.len() { + let bit1 = (i >> qubit1) & 1; + let bit2 = (i >> qubit2) & 1; + + if bit1 != bit2 { + let swapped_index = i ^ step1 ^ step2; + if i < swapped_index { + self.state.swap(i, swapped_index); + } + } + } + } + + /// Apply U3(theta, phi, lambda) gate + /// U3 = [[cos(θ/2), -e^(iλ)sin(θ/2)], + /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] + pub fn u3(&mut self, target: usize, theta: f64, phi: f64, lambda: f64) { + assert!(target < self.num_qubits); + + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + + // Calculate matrix elements + let u00 = Complex64::new(cos, 0.0); + let u01 = -Complex64::from_polar(sin, lambda); + let u10 = Complex64::from_polar(sin, phi); + let u11 = Complex64::from_polar(cos, phi + lambda); + + // Apply the unitary + for i in 0..self.state.len() { + if (i >> target) & 1 == 0 { + let i1 = i ^ (1 << target); + let a0 = self.state[i]; + let a1 = self.state[i1]; + + self.state[i] = u00 * a0 + u01 * a1; + self.state[i1] = u10 * a0 + u11 * a1; + } + } + } + + /// Apply controlled-X gate + /// CX = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ X + pub fn cx(&mut self, control: usize, target: usize) { + assert!(control < self.num_qubits); + assert!(target < self.num_qubits); + assert!(control != target); + + for i in 0..self.state.len() { + let control_val = (i >> control) & 1; + let target_val = (i >> target) & 1; + if control_val == 1 && target_val == 0 { + let flipped_i = i ^ (1 << target); + let i = i; + self.state.swap(i, flipped_i); + // self.state.swap(i, flipped_i); + } + } + } + + /// Apply controlled-Y gate + /// CY = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Y + pub fn cy(&mut self, control: usize, target: usize) { + assert!(control < self.num_qubits); + assert!(target < self.num_qubits); + assert!(control != target); + + // Only process when control bit is 1 and target bit is 0 + for i in 0..self.state.len() { + let control_val = (i >> control) & 1; + let target_val = (i >> target) & 1; + + if control_val == 1 && target_val == 0 { + let flipped_i = i ^ (1 << target); + + // Y gate has different phases than X + // Y = [[0, -i], [i, 0]] + let temp = self.state[i]; + self.state[i] = -Complex64::i() * self.state[flipped_i]; + self.state[flipped_i] = Complex64::i() * temp; + } + } + } + + /// Apply controlled-Z gate + /// CZ = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Z + pub fn cz(&mut self, control: usize, target: usize) { + assert!(control < self.num_qubits); + assert!(target < self.num_qubits); + assert!(control != target); + + // CZ is simpler - just add phase when both control and target are 1 + for i in 0..self.state.len() { + let control_val = (i >> control) & 1; + let target_val = (i >> target) & 1; + + if control_val == 1 && target_val == 1 { + self.state[i] = -self.state[i]; + } + } + } + + /// Apply RXX(θ) = exp(-i θ XX/2) gate + /// This implements evolution under the XX coupling between two qubits + pub fn rxx(&mut self, theta: f64, qubit1: usize, qubit2: usize) { + assert!(qubit1 < self.num_qubits); + assert!(qubit2 < self.num_qubits); + assert!(qubit1 != qubit2); + + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + let neg_i_sin = Complex64::new(0.0, -sin); // -i*sin + + // Make sure qubit1 < qubit2 for consistent ordering + let (q1, q2) = if qubit1 < qubit2 { + (qubit1, qubit2) + } else { + (qubit2, qubit1) + }; + + for i in 0..self.state.len() { + let bit1 = (i >> q1) & 1; + let bit2 = (i >> q2) & 1; + + if bit1 == 0 && bit2 == 0 { + let i01 = i ^ (1 << q2); + let i10 = i ^ (1 << q1); + let i11 = i ^ (1 << q1) ^ (1 << q2); + + let a00 = self.state[i]; + let a01 = self.state[i01]; + let a10 = self.state[i10]; + let a11 = self.state[i11]; + + // Apply the correct RXX matrix + self.state[i] = cos * a00 + neg_i_sin * a11; + self.state[i01] = cos * a01 + neg_i_sin * a10; + self.state[i10] = cos * a10 + neg_i_sin * a01; + self.state[i11] = cos * a11 + neg_i_sin * a00; + } + } + } + + /// Apply RYY(θ) = exp(-i θ YY/2) gate + pub fn ryy(&mut self, theta: f64, qubit1: usize, qubit2: usize) { + assert!(qubit1 < self.num_qubits); + assert!(qubit2 < self.num_qubits); + assert!(qubit1 != qubit2); + + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + let neg_i_sin = Complex64::new(0.0, -sin); + + let (q1, q2) = if qubit1 < qubit2 { + (qubit1, qubit2) + } else { + (qubit2, qubit1) + }; + + for i in 0..self.state.len() { + let bit1 = (i >> q1) & 1; + let bit2 = (i >> q2) & 1; + + if bit1 == 0 && bit2 == 0 { + let i01 = i ^ (1 << q2); + let i10 = i ^ (1 << q1); + let i11 = i ^ (1 << q1) ^ (1 << q2); + + let a00 = self.state[i]; + let a01 = self.state[i01]; + let a10 = self.state[i10]; + let a11 = self.state[i11]; + + // YY has an extra minus sign compared to XX when acting on |01⟩ and |10⟩ + self.state[i] = cos * a00 + neg_i_sin * a11; + self.state[i01] = cos * a01 - neg_i_sin * a10; + self.state[i10] = cos * a10 - neg_i_sin * a01; + self.state[i11] = cos * a11 + neg_i_sin * a00; + } + } + } + + /// Apply RZZ(θ) = exp(-i θ ZZ/2) gate + pub fn rzz(&mut self, theta: f64, qubit1: usize, qubit2: usize) { + assert!(qubit1 < self.num_qubits); + assert!(qubit2 < self.num_qubits); + assert!(qubit1 != qubit2); + + // RZZ is diagonal in computational basis - just add phases + for i in 0..self.state.len() { + let bit1 = (i >> qubit1) & 1; + let bit2 = (i >> qubit2) & 1; + + // Phase depends on parity of bits + let phase = if bit1 ^ bit2 == 0 { + // Same bits (00 or 11) -> e^(-iθ/2) + Complex64::from_polar(1.0, -theta / 2.0) + } else { + // Different bits (01 or 10) -> e^(iθ/2) + Complex64::from_polar(1.0, theta / 2.0) + }; + + self.state[i] *= phase; + } + } + + /// Apply a general two-qubit unitary given by a 4x4 complex matrix + /// U = [[u00, u01, u02, u03], + /// [u10, u11, u12, u13], + /// [u20, u21, u22, u23], + /// [u30, u31, u32, u33]] + pub fn two_qubit_unitary(&mut self, qubit1: usize, qubit2: usize, matrix: [[Complex64; 4]; 4]) { + assert!(qubit1 < self.num_qubits); + assert!(qubit2 < self.num_qubits); + assert!(qubit1 != qubit2); + + // Make sure qubit1 < qubit2 for consistent ordering + let (q1, q2) = if qubit1 < qubit2 { + (qubit1, qubit2) + } else { + (qubit2, qubit1) + }; + + // Process state vector in groups of 4 amplitudes + for i in 0..self.state.len() { + let bit1 = (i >> q1) & 1; + let bit2 = (i >> q2) & 1; + + // Only process each set of 4 states once + if bit1 == 0 && bit2 == 0 { + // Calculate indices for all four basis states + let i00 = i; + let i01 = i ^ (1 << q2); + let i10 = i ^ (1 << q1); + let i11 = i ^ (1 << q1) ^ (1 << q2); + + // Store original amplitudes + let a00 = self.state[i00]; + let a01 = self.state[i01]; + let a10 = self.state[i10]; + let a11 = self.state[i11]; + + // Apply the 4x4 unitary transformation + self.state[i00] = matrix[0][0] * a00 + + matrix[0][1] * a01 + + matrix[0][2] * a10 + + matrix[0][3] * a11; + self.state[i01] = matrix[1][0] * a00 + + matrix[1][1] * a01 + + matrix[1][2] * a10 + + matrix[1][3] * a11; + self.state[i10] = matrix[2][0] * a00 + + matrix[2][1] * a01 + + matrix[2][2] * a10 + + matrix[2][3] * a11; + self.state[i11] = matrix[3][0] * a00 + + matrix[3][1] * a01 + + matrix[3][2] * a10 + + matrix[3][3] * a11; + } + } + } + + /// Measure a single qubit in the Z basis and collapse the state + pub fn measure(&mut self, target: usize) -> usize { + assert!(target < self.num_qubits); + let mut rng = rand::thread_rng(); + + let step = 1 << target; + let mut prob_one = 0.0; + + // Calculate probability of measuring |1⟩ + for i in (0..self.state.len()).step_by(2 * step) { + for offset in 0..step { + let idx = i + offset + step; // Target bit = 1 positions + prob_one += self.state[idx].norm_sqr(); + } + } + + // Decide measurement outcome + let result = if rng.gen::() < prob_one { 1 } else { 0 }; + + // Collapse and normalize state + let mut norm = 0.0; + for i in 0..self.state.len() { + let bit = (i >> target) & 1; + if bit != result { + self.state[i] = Complex64::new(0.0, 0.0); + } else { + norm += self.state[i].norm_sqr(); + } + } + + let norm_inv = 1.0 / norm.sqrt(); + for amp in &mut self.state { + *amp *= norm_inv; + } + + result + } + + /// Reset a qubit to the |0⟩ state + pub fn reset(&mut self, target: usize) { + assert!(target < self.num_qubits); + + // Measure the qubit + let result = self.measure(target); + + // If we got |1⟩, apply X to flip it to |0⟩ + if result == 1 { + self.x(target); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, PI, TAU}; + + #[test] + fn test_new_state() { + let q = StateVec::new(2); + assert_eq!(q.state[0], Complex64::new(1.0, 0.0)); + for i in 1..4 { + assert_eq!(q.state[i], Complex64::new(0.0, 0.0)); + } + } + + #[test] + fn test_rx() { + let mut q = StateVec::new(1); + + // RX(π) should be equivalent to X up to global phase + q.rx(PI, 0); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].norm() - 1.0).abs() < 1e-10); + + // RX(2π) should return to initial state up to global phase + let mut q = StateVec::new(1); + q.rx(2.0 * PI, 0); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + } + + #[test] + fn test_ry() { + let mut q = StateVec::new(1); + + // RY(π) should be equivalent to X up to global phase + q.ry(PI, 0); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].norm() - 1.0).abs() < 1e-10); + + // RY(2π) should return to initial state up to global phase + let mut q = StateVec::new(1); + q.ry(2.0 * PI, 0); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + } + + #[test] + fn test_rz() { + let mut q = StateVec::new(1); + + // RZ should only add phases, not change probabilities + q.hadamard(0); // Put in superposition first + let probs_before: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); + + q.rz(FRAC_PI_2, 0); + let probs_after: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); + + // Probabilities should remain unchanged + for (p1, p2) in probs_before.iter().zip(probs_after.iter()) { + assert!((p1 - p2).abs() < 1e-10); + } + + // RZ(2π) should return to initial state up to global phase + let mut q = StateVec::new(1); + q.hadamard(0); + let state_before = q.state.clone(); + q.rz(TAU, 0); + + // States should be identical up to global phase + let phase = q.state[0] / state_before[0]; + for (a, b) in q.state.iter().zip(state_before.iter()) { + assert!((a - b * phase).norm() < 1e-10); + } + } + + #[test] + fn test_hadamard() { + let mut q = StateVec::new(1); + q.hadamard(0); + + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + } + + #[test] + fn test_swap() { + let mut q = StateVec::new(2); + q.x(0); + q.swap(0, 1); + + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[2].re - 1.0).abs() < 1e-10); + } + + #[test] + fn test_cx() { + let mut q = StateVec::new(2); + // Prep |+> + q.hadamard(0); + q.cx(0, 1); + + // Should be in Bell state (|00> + |11>)/sqrt(2) + let expected = 1.0 / 2.0_f64.sqrt(); + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!((q.state[3].re - expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + } + + #[test] + fn test_cy() { + let mut q = StateVec::new(2); + + // Create |+0⟩ state + q.hadamard(0); + + // Apply CY to get entangled state + q.cy(0, 1); + + // Should be (|00⟩ + i|11⟩)/√2 + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].re - expected).abs() < 1e-10); // |00⟩ amplitude + assert!(q.state[1].norm() < 1e-10); // |01⟩ amplitude + assert!(q.state[2].norm() < 1e-10); // |10⟩ amplitude + assert!((q.state[3].im - expected).abs() < 1e-10); // |11⟩ amplitude + } + + #[test] + fn test_cz() { + let mut q = StateVec::new(2); + + // Create |++⟩ state + q.hadamard(0); + q.hadamard(1); + + // Apply CZ + q.cz(0, 1); + + // Should be (|00⟩ + |01⟩ + |10⟩ - |11⟩)/2 + let expected = 0.5; + assert!((q.state[0].re - expected).abs() < 1e-10); // |00⟩ amplitude + assert!((q.state[1].re - expected).abs() < 1e-10); // |01⟩ amplitude + assert!((q.state[2].re - expected).abs() < 1e-10); // |10⟩ amplitude + assert!((q.state[3].re + expected).abs() < 1e-10); // |11⟩ amplitude + } + + #[test] + fn test_control_target_independence() { + // Test that CY and CZ work regardless of which qubit is control/target + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Prepare same initial state + q1.hadamard(0); + q1.hadamard(1); + q2.hadamard(0); + q2.hadamard(1); + + // Apply gates with different control/target + q1.cz(0, 1); + q2.cz(1, 0); + + // Results should be identical + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_x() { + let mut q = StateVec::new(1); + + // Check initial state is |0> + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Test X on |0> -> |1> + q.x(0); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - 1.0).abs() < 1e-10); + + // Test X on |1> -> |0> + q.x(0); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Test X on superposition + q.hadamard(0); + let initial_state = q.state.clone(); + q.x(0); // X|+> = |+> + for i in 0..2 { + assert!((q.state[i] - initial_state[i]).norm() < 1e-10); + } + + // Test X on second qubit of two-qubit system + let mut q = StateVec::new(2); + q.x(1); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[2].re - 1.0).abs() < 1e-10); + } + + #[test] + fn test_y() { + let mut q = StateVec::new(1); + + // Test Y on |0⟩ -> i|1⟩ + q.y(0); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1] - Complex64::i()).norm() < 1e-10); + + // Test Y on i|1⟩ -> |0⟩ + q.y(0); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Test Y on |+⟩ + let mut q = StateVec::new(1); + q.hadamard(0); // Create |+⟩ + q.y(0); // Should give i|-⟩ + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].im + expected).abs() < 1e-10); + assert!((q.state[1].im - expected).abs() < 1e-10); + } + + #[test] + fn test_z() { + let mut q = StateVec::new(1); + + // Test Z on |0⟩ -> |0⟩ + q.z(0); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Test Z on |1⟩ -> -|1⟩ + q.x(0); // Prepare |1⟩ + q.z(0); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re + 1.0).abs() < 1e-10); + + // Test Z on |+⟩ -> |-⟩ + let mut q = StateVec::new(1); + q.hadamard(0); // Create |+⟩ + q.z(0); // Should give |-⟩ + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!((q.state[1].re + expected).abs() < 1e-10); + } + + #[test] + fn test_pauli_relations() { + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + // Store initial state + let initial_state = q1.state.clone(); + + // Test XYZ sequence + q1.x(0); + q1.y(0); + q1.z(0); + + // XYZ = -iI, so state should be -i times initial state + if initial_state[0].norm() > 1e-10 { + let phase = q1.state[0] / initial_state[0]; + assert!((phase + Complex64::i()).norm() < 1e-10); // Changed to +Complex64::i() + } + + // Test YZX sequence - should give same result + q2.y(0); + q2.z(0); + q2.x(0); + + // Compare q1 and q2 up to global phase + if q1.state[0].norm() > 1e-10 { + let phase = q2.state[0] / q1.state[0]; + let phase_norm = phase.norm(); + assert!((phase_norm - 1.0).abs() < 1e-10); + + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a * phase - b).norm() < 1e-10); + } + } + } + + #[test] + fn test_rxx() { + // Test 1: RXX(π/2) on |00⟩ should give (|00⟩ - i|11⟩)/√2 + let mut q = StateVec::new(2); + q.rxx(FRAC_PI_2, 0, 1); + + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3].im + expected).abs() < 1e-10); + + // Test 2: RXX(2π) should return to original state up to global phase + let mut q = StateVec::new(2); + q.hadamard(0); // Create some initial state + let initial = q.state.clone(); + q.rxx(TAU, 0, 1); + + // Compare up to global phase + if q.state[0].norm() > 1e-10 { + let phase = q.state[0] / initial[0]; + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!((a - b * phase).norm() < 1e-10); + } + } + + // Test 3: RXX(π) should flip |00⟩ to |11⟩ up to phase + let mut q = StateVec::new(2); + q.rxx(PI, 0, 1); + + // Should get -i|11⟩ + assert!(q.state[0].norm() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3] - Complex64::new(0.0, -1.0)).norm() < 1e-10); + } + + #[test] + fn test_rxx_symmetry() { + // Test that RXX is symmetric under exchange of qubits + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Prepare same non-trivial initial state + q1.hadamard(0); + q1.hadamard(1); + q2.hadamard(0); + q2.hadamard(1); + + // Apply RXX with different qubit orders + q1.rxx(FRAC_PI_3, 0, 1); + q2.rxx(FRAC_PI_3, 1, 0); + + // Results should be identical + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_ryy() { + // Test 1: RYY(π/2) on |00⟩ should give (|00⟩ - i|11⟩)/√2 + let mut q = StateVec::new(2); + q.ryy(FRAC_PI_2, 0, 1); + + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3].im + expected).abs() < 1e-10); + + // Test 2: RYY(2π) should return to original state up to global phase + let mut q = StateVec::new(2); + q.hadamard(0); // Create some initial state + let initial = q.state.clone(); + q.ryy(TAU, 0, 1); + + if q.state[0].norm() > 1e-10 { + let phase = q.state[0] / initial[0]; + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!((a - b * phase).norm() < 1e-10); + } + } + } + + #[test] + fn test_rzz() { + // Test 1: RZZ(π) on (|00⟩ + |11⟩)/√2 should give itself + let mut q = StateVec::new(2); + // Create Bell state + q.hadamard(0); + q.cx(0, 1); + let initial = q.state.clone(); + + q.rzz(PI, 0, 1); + + // Compare up to global phase + if q.state[0].norm() > 1e-10 { + let phase = q.state[0] / initial[0]; + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!((a - b * phase).norm() < 1e-10); + } + } + + // Test 2: RZZ(π/2) on |++⟩ + let mut q = StateVec::new(2); + q.hadamard(0); + q.hadamard(1); + q.rzz(FRAC_PI_2, 0, 1); + + // e^(-iπ/4) = (1-i)/√2 + // e^(iπ/4) = (1+i)/√2 + let factor = 0.5; // 1/2 for the |++⟩ normalization + let exp_minus_i_pi_4 = Complex64::new(1.0, -1.0) / (2.0_f64.sqrt()); + let exp_plus_i_pi_4 = Complex64::new(1.0, 1.0) / (2.0_f64.sqrt()); + + assert!((q.state[0] - factor * exp_minus_i_pi_4).norm() < 1e-10); // |00⟩ + assert!((q.state[1] - factor * exp_plus_i_pi_4).norm() < 1e-10); // |01⟩ + assert!((q.state[2] - factor * exp_plus_i_pi_4).norm() < 1e-10); // |10⟩ + assert!((q.state[3] - factor * exp_minus_i_pi_4).norm() < 1e-10); // |11⟩ + } + + #[test] + fn test_rotation_symmetries() { + // Test that all rotations are symmetric under exchange of qubits + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Prepare same non-trivial initial state + q1.hadamard(0); + q1.hadamard(1); + q2.hadamard(0); + q2.hadamard(1); + + let theta = PI / 3.0; + + // Test RYY symmetry + q1.ryy(theta, 0, 1); + q2.ryy(theta, 1, 0); + + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10); + } + + // Test RZZ symmetry + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + q1.hadamard(0); + q1.hadamard(1); + q2.hadamard(0); + q2.hadamard(1); + + q1.rzz(theta, 0, 1); + q2.rzz(theta, 1, 0); + + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_measure2() { + let mut q = StateVec::new(1); + q.hadamard(0); + let result = q.measure(0); + + // Check collapse to |0⟩ or |1⟩ + assert!(result == 0 || result == 1); + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); + } + + #[test] + fn test_measure() { + // Test 1: Meauring |0> state + let mut q = StateVec::new(1); + let result = q.measure(0); + assert_eq!(result, 0); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Test 2: Measuring |1> state + let mut q = StateVec::new(1); + q.x(0); + let result = q.measure(0); + assert_eq!(result, 1); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - 1.0).abs() < 1e-10); + + // Test 3: Measuring superposition state multiple times + let mut zeros = 0; + let trials = 1000; + + for _ in 0..trials { + let mut q = StateVec::new(1); + q.hadamard(0); + let result = q.measure(0); + if result == 0 { + zeros += 1; + } + } + + // Check if measurements are roughly equally distributed + let ratio = f64::from(zeros) / f64::from(trials); + assert!((ratio - 0.5).abs() < 0.1); // Should be close to 0.5... + + // Test 4: Measuring one qubit of a Bell state + let mut q = StateVec::new(2); + q.hadamard(0); + q.cx(0, 1); + + // Measure first qubit + let result1 = q.measure(0); + // Measure second qubit - should match first + let result2 = q.measure(1); + assert_eq!(result1, result2); + } + + #[test] + fn test_reset() { + let mut q = StateVec::new(1); + + q.hadamard(0); + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + + q.reset(0); + + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + } + + #[test] + fn test_reset_multiple_qubits() { + let mut q = StateVec::new(2); + + q.hadamard(0); + q.cx(0, 1); + + q.reset(0); + + let prob_0 = q.state[0].norm_sqr() + q.state[2].norm_sqr(); + let prob_1 = q.state[1].norm_sqr() + q.state[3].norm_sqr(); + + assert!((prob_0 - 1.0).abs() < 1e-10); + assert!(prob_1 < 1e-10); + } + + #[test] + fn test_single_qubit_rotation() { + let mut q = StateVec::new(1); + + // Test 1: Hadamard gate + let h00 = Complex64::new(FRAC_1_SQRT_2, 0.0); + let h01 = Complex64::new(FRAC_1_SQRT_2, 0.0); + let h10 = Complex64::new(FRAC_1_SQRT_2, 0.0); + let h11 = Complex64::new(-FRAC_1_SQRT_2, 0.0); + + q.single_qubit_rotation(0, h00, h01, h10, h11); + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + + // Test 2: X gate + let mut q = StateVec::new(1); + let x00 = Complex64::new(0.0, 0.0); + let x01 = Complex64::new(1.0, 0.0); + let x10 = Complex64::new(1.0, 0.0); + let x11 = Complex64::new(0.0, 0.0); + + q.single_qubit_rotation(0, x00, x01, x10, x11); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - 1.0).abs() < 1e-10); + + // Test 3: Phase gate + let mut q = StateVec::new(1); + let p00 = Complex64::new(1.0, 0.0); + let p01 = Complex64::new(0.0, 0.0); + let p10 = Complex64::new(0.0, 0.0); + let p11 = Complex64::new(0.0, 1.0); + + q.single_qubit_rotation(0, p00, p01, p10, p11); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Test 4: Y gate using unitary + let mut q = StateVec::new(1); + let y00 = Complex64::new(0.0, 0.0); + let y01 = Complex64::new(0.0, -1.0); + let y10 = Complex64::new(0.0, 1.0); + let y11 = Complex64::new(0.0, 0.0); + + q.single_qubit_rotation(0, y00, y01, y10, y11); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].im - 1.0).abs() < 1e-10); + } + + #[test] + fn test_unitary_properties() { + let mut q = StateVec::new(1); + + // Create random state with Hadamard + q.hadamard(0); + + // Apply Z gate as unitary + let z00 = Complex64::new(1.0, 0.0); + let z01 = Complex64::new(0.0, 0.0); + let z10 = Complex64::new(0.0, 0.0); + let z11 = Complex64::new(-1.0, 0.0); + + let initial = q.state.clone(); + q.single_qubit_rotation(0, z00, z01, z10, z11); + + // Check normalization is preserved + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); + + // Apply Z again - should get back original state + q.single_qubit_rotation(0, z00, z01, z10, z11); + + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_u3_special_cases() { + // Test 1: U3(π, 0, π) should be X gate + let mut q = StateVec::new(1); + q.u3(0, PI, 0.0, PI); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - 1.0).abs() < 1e-10); + + // Test 2: Hadamard gate + // H = U3(π/2, 0, π) + let mut q = StateVec::new(1); + q.u3(0, PI / 2.0, 0.0, PI); + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + + // Test 3: U3(0, 0, π) should be Z gate + let mut q = StateVec::new(1); + q.hadamard(0); // First put in superposition + let initial = q.state.clone(); + q.u3(0, 0.0, 0.0, PI); + assert!((q.state[0] - initial[0]).norm() < 1e-10); + assert!((q.state[1] + initial[1]).norm() < 1e-10); + + // Additional test: U3(π/2, π/2, -π/2) should be S†H + let mut q = StateVec::new(1); + q.u3(0, PI / 2.0, PI / 2.0, -PI / 2.0); + // This creates the state (|0⟩ + i|1⟩)/√2 + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].im - FRAC_1_SQRT_2).abs() < 1e-10); + } + + #[test] + fn test_u3_composition() { + let mut q1 = StateVec::new(1); + let q2 = StateVec::new(1); + + // Two U3 gates that should multiply to identity + q1.u3(0, PI / 3.0, PI / 4.0, PI / 6.0); + q1.u3(0, -PI / 3.0, -PI / 6.0, -PI / 4.0); + + // Compare with initial state + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_u3_arbitrary() { + let mut q = StateVec::new(1); + + // Apply some arbitrary rotation + let theta = PI / 5.0; + let phi = PI / 7.0; + let lambda = PI / 3.0; + q.u3(0, theta, phi, lambda); + + // Verify normalization is preserved + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); + + // Verify expected amplitudes + let expected_0 = (theta / 2.0).cos(); + assert!((q.state[0].re - expected_0).abs() < 1e-10); + + let expected_1_mag = (theta / 2.0).sin(); + assert!((q.state[1].norm() - expected_1_mag).abs() < 1e-10); + } + + #[test] + fn test_two_qubit_unitary_cnot() { + // Test that we can implement CNOT using the general unitary + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // CNOT matrix + let cnot = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ], + ]; + + // Create Bell state using both methods + q1.hadamard(0); + q1.cx(0, 1); + + q2.hadamard(0); + q2.two_qubit_unitary(0, 1, cnot); + + // Compare results + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_two_qubit_unitary_swap() { + // Test SWAP gate + let mut q = StateVec::new(2); + + // Prepare |10⟩ state + q.x(0); + + // SWAP matrix + let swap = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + ]; + + q.two_qubit_unitary(0, 1, swap); + + // Should be in |01⟩ state + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[2].re - 1.0).abs() < 1e-10); + } + + #[test] + fn test_two_qubit_unitary_properties() { + let mut q = StateVec::new(2); + + // Create a non-trivial state + q.hadamard(0); + q.hadamard(1); + + // iSWAP matrix + let iswap = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 1.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 1.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + ]; + + q.two_qubit_unitary(0, 1, iswap); + + // Verify normalization is preserved + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); + } + + #[test] + fn test_single_qubit_locality() { + // Test on 3 qubit system that gates only affect their target + let mut q = StateVec::new(3); + + // Prepare state |+⟩|0⟩|0⟩ + q.hadamard(0); // Affects least significant bit + + // Apply X to qubit 2 (most significant bit) + q.x(2); + + // Check that qubit 0 is still in |+⟩ state + // When qubit 2 is |1⟩, check LSB still shows |+⟩ + assert!((q.state[4].re - FRAC_1_SQRT_2).abs() < 1e-10); // |100⟩ + assert!((q.state[5].re - FRAC_1_SQRT_2).abs() < 1e-10); // |101⟩ + } + + #[test] + fn test_bit_indexing() { + let mut q = StateVec::new(3); + + println!("Initial state (|000⟩):"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state[i]); + } + + // Put |+⟩ on qubit 0 (LSB) + q.hadamard(0); + + println!("\nAfter H on qubit 0:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state[i]); + } + + // Check state is |+⟩|0⟩|0⟩ + // Only indices that differ in LSB (qubit 0) should be FRAC_1_SQRT_2 + for i in 0..8 { + let qubit0 = i & 1; + let qubit1 = (i >> 1) & 1; + let qubit2 = (i >> 2) & 1; + + let expected = if qubit1 == 0 && qubit2 == 0 { + FRAC_1_SQRT_2 + } else { + 0.0 + }; + + if (q.state[i].re - expected).abs() >= 1e-10 { + println!("\nMismatch at index {i}: {i:03b}"); + println!("Qubit values: q2={qubit2}, q1={qubit1}, q0={qubit0}"); + println!("Expected {}, got {}", expected, q.state[i].re); + } + assert!((q.state[i].re - expected).abs() < 1e-10); + } + } + + #[test] + fn test_two_qubit_locality() { + let mut q = StateVec::new(4); + + println!("Initial state:"); + for i in 0..16 { + println!(" {:04b}: {:.3}", i, q.state[i]); + } + + // Prepare |+⟩ on qubit 0 (LSB) + q.hadamard(0); + + println!("\nAfter H on qubit 0:"); + for i in 0..16 { + println!(" {:04b}: {:.3}", i, q.state[i]); + } + + // Apply CX between qubits 2,3 + q.cx(2, 3); + + println!("\nAfter CX on qubits 2,3:"); + for i in 0..16 { + println!(" {:04b}: {:.3}", i, q.state[i]); + + // Extract qubit values + // let _q0 = i & 1; + let q1 = (i >> 1) & 1; + let q2 = (i >> 2) & 1; + let q3 = (i >> 3) & 1; + + // Only states with q0=0 or q0=1 and q1=q2=q3=0 should have amplitude + let expected = if q1 == 0 && q2 == 0 && q3 == 0 { + FRAC_1_SQRT_2 + } else { + 0.0 + }; + + if (q.state[i].re - expected).abs() >= 1e-10 { + println!("Mismatch at {i:04b}"); + println!("Expected {}, got {}", expected, q.state[i].re); + } + assert!((q.state[i].re - expected).abs() < 1e-10); + } + } + + #[test] + fn test_two_qubit_gate_locality() { + let mut q = StateVec::new(3); + + // Prepare state |+⟩|0⟩|0⟩ + q.hadamard(0); + + // Apply CX on qubits 1 and 2 (no effect on qubit 0) + q.cx(1, 2); + + // Qubit 0 should remain in superposition + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state[0].re - expected_amp).abs() < 1e-10); + assert!((q.state[1].re - expected_amp).abs() < 1e-10); + } + + #[test] + fn test_rotation_locality() { + let mut q = StateVec::new(3); + + println!("Initial state:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state[i]); + } + + // Prepare |+⟩ on qubit 0 (LSB) + q.hadamard(0); + + println!("\nAfter H on qubit 0:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state[i]); + } + + // Apply rotation to qubit 1 + q.rx(PI / 2.0, 1); + + println!("\nAfter RX on qubit 1:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state[i]); + } + + // Check each basis state contribution + for i in 0..8 { + let expected = if i & 1 == 0 { + FRAC_1_SQRT_2 + } else { + FRAC_1_SQRT_2 + }; + if (q.state[i].norm() - expected).abs() >= 1e-10 { + println!("\nMismatch at index {i}: {i:03b}"); + println!("Expected norm {}, got {}", expected, q.state[i].norm()); + } + } + } + + #[test] + fn test_large_system_hadamard() { + let num_qubits = 10; // 10 qubits => state vector size = 1024 + let mut q = StateVec::new(num_qubits); + + // Apply Hadamard gate to the 0th qubit + q.hadamard(0); + + // Check that |0...0> and |1...0> have equal amplitude + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state[0].re - expected_amp).abs() < 1e-10); + assert!((q.state[1].re - expected_amp).abs() < 1e-10); + + // Ensure all other amplitudes remain zero + for i in 2..q.state.len() { + assert!(q.state[i].norm() < 1e-10); + } + } + + #[test] + #[should_panic] + fn test_invalid_qubit_index_single() { + let mut q = StateVec::new(2); + q.x(3); // Invalid qubit index + } + + #[test] + #[should_panic] + fn test_invalid_qubit_index_controlled() { + let mut q = StateVec::new(2); + q.cx(1, 2); // Invalid target qubit + } + + #[test] + #[should_panic] + fn test_control_equals_target() { + let mut q = StateVec::new(2); + q.cx(0, 0); // Control and target are the same + } + + #[test] + fn test_measurement_consistency() { + let mut q = StateVec::new(1); + + // Put qubit in |1⟩ state + q.x(0); + + // Measure twice - result should be the same + let result1 = q.measure(0); + let result2 = q.measure(0); + + assert_eq!(result1, 1); + assert_eq!(result2, 1); + } + + #[test] + fn test_measurement_on_entangled_state() { + let mut q = StateVec::new(2); + + // Create Bell state (|00⟩ + |11⟩) / sqrt(2) + q.hadamard(0); + q.cx(0, 1); + + // Measure the first qubit + let result1 = q.measure(0); + + // Measure the second qubit - should match the first + let result2 = q.measure(1); + + assert_eq!(result1, result2); + } +} From bbaa88e73998e351207ab10905468293d334cba0 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 17 Dec 2024 19:50:16 -0700 Subject: [PATCH 03/33] Making clippy happy --- crates/pecos-qsim/src/statevec.rs | 111 +++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 18 deletions(-) diff --git a/crates/pecos-qsim/src/statevec.rs b/crates/pecos-qsim/src/statevec.rs index da0eccb5..de8f8bd7 100644 --- a/crates/pecos-qsim/src/statevec.rs +++ b/crates/pecos-qsim/src/statevec.rs @@ -16,13 +16,21 @@ impl StateVec { } /// Initialize from a custom state vector + /// + /// # Panics + /// + /// Panics if the input state requires more qubits then `StateVec` has. #[must_use] pub fn from_state(state: Vec) -> Self { - let num_qubits = (state.len() as f64).log2() as usize; + let num_qubits = state.len().trailing_zeros() as usize; assert_eq!(1 << num_qubits, state.len(), "Invalid state vector size"); StateVec { num_qubits, state } } - /// Apply Hadamard gate + /// Apply Hadamard gate to the target qubit + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn hadamard(&mut self, target: usize) { assert!(target < self.num_qubits); let factor = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0); @@ -43,6 +51,10 @@ impl StateVec { } /// Apply Pauli-X gate + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn x(&mut self, target: usize) { assert!(target < self.num_qubits); let step = 1 << target; @@ -55,6 +67,10 @@ impl StateVec { } /// Apply Y = [[0, -i], [i, 0]] gate to target qubit + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn y(&mut self, target: usize) { assert!(target < self.num_qubits); @@ -69,6 +85,10 @@ impl StateVec { } /// Apply Z = [[1, 0], [0, -1]] gate to target qubit + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn z(&mut self, target: usize) { assert!(target < self.num_qubits); @@ -82,6 +102,10 @@ impl StateVec { /// Gate RX(θ) = exp(-i θ X/2) = cos(θ/2) I - i*sin(θ/2) X /// RX(θ) = [[cos(θ/2), -i*sin(θ/2)], /// [-i*sin(θ/2), cos(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn rx(&mut self, theta: f64, target: usize) { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); @@ -99,6 +123,10 @@ impl StateVec { /// Gate RY(θ) = exp(-i θ Y/2) = cos(θ/2) I - i*sin(θ/2) Y /// RY(θ) = [[cos(θ/2), -sin(θ/2)], /// [-sin(θ/2), cos(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn ry(&mut self, theta: f64, target: usize) { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); @@ -115,6 +143,10 @@ impl StateVec { /// Gate RZ(θ) = exp(-i θ Z/2) = cos(θ/2) I - i*sin(θ/2) Z /// RZ(θ) = [[cos(θ/2)-i*sin(θ/2), 0], /// [0, cos(θ/2)+i*sin(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn rz(&mut self, theta: f64, target: usize) { let exp_minus_i_theta_2 = Complex64::from_polar(1.0, -theta / 2.0); let exp_plus_i_theta_2 = Complex64::from_polar(1.0, theta / 2.0); @@ -128,6 +160,11 @@ impl StateVec { ); } + /// General single-qubit unitary that takes in matrix elements + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn single_qubit_rotation( &mut self, target: usize, @@ -154,6 +191,10 @@ impl StateVec { } /// Apply a SWAP gate between two qubits + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits pub fn swap(&mut self, qubit1: usize, qubit2: usize) { assert!(qubit1 < self.num_qubits && qubit2 < self.num_qubits); if qubit1 == qubit2 { @@ -179,6 +220,10 @@ impl StateVec { /// Apply U3(theta, phi, lambda) gate /// U3 = [[cos(θ/2), -e^(iλ)sin(θ/2)], /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn u3(&mut self, target: usize, theta: f64, phi: f64, lambda: f64) { assert!(target < self.num_qubits); @@ -206,6 +251,10 @@ impl StateVec { /// Apply controlled-X gate /// CX = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ X + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 pub fn cx(&mut self, control: usize, target: usize) { assert!(control < self.num_qubits); assert!(target < self.num_qubits); @@ -216,15 +265,17 @@ impl StateVec { let target_val = (i >> target) & 1; if control_val == 1 && target_val == 0 { let flipped_i = i ^ (1 << target); - let i = i; self.state.swap(i, flipped_i); - // self.state.swap(i, flipped_i); } } } /// Apply controlled-Y gate /// CY = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Y + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 pub fn cy(&mut self, control: usize, target: usize) { assert!(control < self.num_qubits); assert!(target < self.num_qubits); @@ -249,6 +300,10 @@ impl StateVec { /// Apply controlled-Z gate /// CZ = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Z + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 pub fn cz(&mut self, control: usize, target: usize) { assert!(control < self.num_qubits); assert!(target < self.num_qubits); @@ -267,6 +322,10 @@ impl StateVec { /// Apply RXX(θ) = exp(-i θ XX/2) gate /// This implements evolution under the XX coupling between two qubits + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 pub fn rxx(&mut self, theta: f64, qubit1: usize, qubit2: usize) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); @@ -307,6 +366,10 @@ impl StateVec { } /// Apply RYY(θ) = exp(-i θ YY/2) gate + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 pub fn ryy(&mut self, theta: f64, qubit1: usize, qubit2: usize) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); @@ -346,6 +409,10 @@ impl StateVec { } /// Apply RZZ(θ) = exp(-i θ ZZ/2) gate + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 pub fn rzz(&mut self, theta: f64, qubit1: usize, qubit2: usize) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); @@ -374,6 +441,10 @@ impl StateVec { /// [u10, u11, u12, u13], /// [u20, u21, u22, u23], /// [u30, u31, u32, u33]] + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 pub fn two_qubit_unitary(&mut self, qubit1: usize, qubit2: usize, matrix: [[Complex64; 4]; 4]) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); @@ -427,6 +498,10 @@ impl StateVec { } /// Measure a single qubit in the Z basis and collapse the state + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn measure(&mut self, target: usize) -> usize { assert!(target < self.num_qubits); let mut rng = rand::thread_rng(); @@ -443,16 +518,16 @@ impl StateVec { } // Decide measurement outcome - let result = if rng.gen::() < prob_one { 1 } else { 0 }; + let result = usize::from(rng.gen::() < prob_one); // Collapse and normalize state let mut norm = 0.0; for i in 0..self.state.len() { let bit = (i >> target) & 1; - if bit != result { - self.state[i] = Complex64::new(0.0, 0.0); - } else { + if bit == result { norm += self.state[i].norm_sqr(); + } else { + self.state[i] = Complex64::new(0.0, 0.0); } } @@ -465,6 +540,10 @@ impl StateVec { } /// Reset a qubit to the |0⟩ state + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits pub fn reset(&mut self, target: usize) { assert!(target < self.num_qubits); @@ -668,8 +747,8 @@ mod tests { q.hadamard(0); let initial_state = q.state.clone(); q.x(0); // X|+> = |+> - for i in 0..2 { - assert!((q.state[i] - initial_state[i]).norm() < 1e-10); + for (state, initial) in q.state.iter().zip(initial_state.iter()) { + assert!((state - initial).norm() < 1e-10); } // Test X on second qubit of two-qubit system @@ -1431,11 +1510,7 @@ mod tests { // Check each basis state contribution for i in 0..8 { - let expected = if i & 1 == 0 { - FRAC_1_SQRT_2 - } else { - FRAC_1_SQRT_2 - }; + let expected = FRAC_1_SQRT_2; if (q.state[i].norm() - expected).abs() >= 1e-10 { println!("\nMismatch at index {i}: {i:03b}"); println!("Expected norm {}, got {}", expected, q.state[i].norm()); @@ -1463,21 +1538,21 @@ mod tests { } #[test] - #[should_panic] + #[should_panic(expected = "assertion failed: target < self.num_qubits")] fn test_invalid_qubit_index_single() { let mut q = StateVec::new(2); q.x(3); // Invalid qubit index } #[test] - #[should_panic] + #[should_panic(expected = "assertion failed: target < self.num_qubits")] fn test_invalid_qubit_index_controlled() { let mut q = StateVec::new(2); q.cx(1, 2); // Invalid target qubit } #[test] - #[should_panic] + #[should_panic(expected = "assertion failed: control != target")] fn test_control_equals_target() { let mut q = StateVec::new(2); q.cx(0, 0); // Control and target are the same From e6c430acfb434c3c51e3259e0b27be1c812433f2 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 17 Dec 2024 20:26:37 -0700 Subject: [PATCH 04/33] Small tweaks --- crates/pecos-qsim/src/statevec.rs | 69 +++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/crates/pecos-qsim/src/statevec.rs b/crates/pecos-qsim/src/statevec.rs index de8f8bd7..59be6ea8 100644 --- a/crates/pecos-qsim/src/statevec.rs +++ b/crates/pecos-qsim/src/statevec.rs @@ -1,6 +1,7 @@ use num_complex::Complex64; use rand::Rng; +#[derive(Clone, Debug)] pub struct StateVec { num_qubits: usize, state: Vec, @@ -26,6 +27,43 @@ impl StateVec { StateVec { num_qubits, state } } + /// Prepare a specific computational basis state + /// + /// # Panics + /// + /// Panics if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) + pub fn prepare_computational_basis(&mut self, basis_state: usize) { + assert!(basis_state < 1 << self.num_qubits); + self.state.fill(Complex64::new(0.0, 0.0)); + self.state[basis_state] = Complex64::new(1.0, 0.0); + } + + /// Prepare all qubits in |+⟩ state + pub fn prepare_plus_state(&mut self) { + let factor = Complex64::new(1.0 / f64::from(1 << self.num_qubits), 0.0); + self.state.fill(factor); + } + + /// Returns the number of qubits in the system + #[must_use] pub fn num_qubits(&self) -> usize { + self.num_qubits + } + + /// Returns reference to the state vector + #[must_use] pub fn state(&self) -> &[Complex64] { + &self.state + } + + /// Returns the probability of measuring a specific basis state + /// + /// # Panics + /// + /// Panics if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) + #[must_use] pub fn probability(&self, basis_state: usize) -> f64 { + assert!(basis_state < 1 << self.num_qubits); + self.state[basis_state].norm_sqr() + } + /// Apply Hadamard gate to the target qubit /// /// # Panics @@ -160,7 +198,22 @@ impl StateVec { ); } - /// General single-qubit unitary that takes in matrix elements + /// Apply a general single-qubit unitary gate + /// + /// # Examples + /// ``` + /// use pecos_qsim::statevec::StateVec; + /// use std::f64::consts::FRAC_1_SQRT_2; + /// use num_complex::Complex64; + /// let mut q = StateVec::new(1); + /// // Apply Hadamard gate + /// q.single_qubit_rotation(0, + /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u00 + /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u01 + /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u10 + /// Complex64::new(-FRAC_1_SQRT_2, 0.0), // u11 + /// ); + /// ``` /// /// # Panics /// @@ -258,7 +311,7 @@ impl StateVec { pub fn cx(&mut self, control: usize, target: usize) { assert!(control < self.num_qubits); assert!(target < self.num_qubits); - assert!(control != target); + assert_ne!(control, target); for i in 0..self.state.len() { let control_val = (i >> control) & 1; @@ -279,7 +332,7 @@ impl StateVec { pub fn cy(&mut self, control: usize, target: usize) { assert!(control < self.num_qubits); assert!(target < self.num_qubits); - assert!(control != target); + assert_ne!(control, target); // Only process when control bit is 1 and target bit is 0 for i in 0..self.state.len() { @@ -307,7 +360,7 @@ impl StateVec { pub fn cz(&mut self, control: usize, target: usize) { assert!(control < self.num_qubits); assert!(target < self.num_qubits); - assert!(control != target); + assert_ne!(control, target); // CZ is simpler - just add phase when both control and target are 1 for i in 0..self.state.len() { @@ -329,7 +382,7 @@ impl StateVec { pub fn rxx(&mut self, theta: f64, qubit1: usize, qubit2: usize) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); - assert!(qubit1 != qubit2); + assert_ne!(qubit1, qubit2); let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); @@ -373,7 +426,7 @@ impl StateVec { pub fn ryy(&mut self, theta: f64, qubit1: usize, qubit2: usize) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); - assert!(qubit1 != qubit2); + assert_ne!(qubit1, qubit2); let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); @@ -416,7 +469,7 @@ impl StateVec { pub fn rzz(&mut self, theta: f64, qubit1: usize, qubit2: usize) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); - assert!(qubit1 != qubit2); + assert_ne!(qubit1, qubit2); // RZZ is diagonal in computational basis - just add phases for i in 0..self.state.len() { @@ -448,7 +501,7 @@ impl StateVec { pub fn two_qubit_unitary(&mut self, qubit1: usize, qubit2: usize, matrix: [[Complex64; 4]; 4]) { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); - assert!(qubit1 != qubit2); + assert_ne!(qubit1, qubit2); // Make sure qubit1 < qubit2 for consistent ordering let (q1, q2) = if qubit1 < qubit2 { From ef6b257da9122cb285d6e315f7f26af111720623 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 17 Dec 2024 21:45:46 -0700 Subject: [PATCH 05/33] More documentation for stab sim --- crates/pecos-qsim/src/sparse_stab.rs | 137 ++++++++++++++++++++++++++- crates/pecos-qsim/src/statevec.rs | 2 +- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index dcd98b55..ad700832 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -18,6 +18,83 @@ use pecos_core::{IndexableElement, Set}; use rand_chacha::ChaCha8Rng; // TODO: Look into seeing if a dense bool for signs_minus and signs_i is more efficient +/// A sparse representation of a stabilizer state using the stabilizer/destabilizer formalism. +/// +/// This implementation is based on the work found in the thesis "Quantum Algorithms, Architecture, +/// and Error Correction" by Ciarán Ryan-Anderson (). +/// +/// # State Representation +/// The quantum state is represented by: +/// - A set of n stabilizer generators that mutually commute +/// - A set of n destabilizer generators where destab[i] anti-commutes with stab[i] and +/// commutes with all other stabilizers +/// +/// The implementation uses a sparse matrix representation for efficiency and speed, storing: +/// - Row-wise X and Z Pauli operators +/// - Column-wise X and Z Pauli operators +/// - Signs (± and ±i) for each generator +/// +/// # Type Parameters +/// - T: A set type that implements the Set trait, used for storing operator locations +/// - E: An indexable element type that can convert between usize indices +/// - R: A random number generator type, defaults to `ChaCha8Rng` +/// +/// # Examples +/// ```rust +/// use pecos_core::VecSet; +/// use pecos_qsim::{QuantumSimulator, CliffordSimulator, SparseStab}; +/// +/// // Create a new 2-qubit stabilizer state +/// let mut sim = SparseStab::, u32>::new(2); +/// +/// // Initialize to |+> ⊗ |0> +/// sim.h(0); // Apply Hadamard to first qubit +/// +/// // Create Bell state |Φ+> = (|00> + |11>)/√2 +/// sim.cx(0, 1); +/// +/// // Measure first qubit in Z basis +/// let (outcome, determined) = sim.mz(0); +/// ``` +/// +/// # Measurement Behavior +/// Measurements can be either: +/// - Deterministic: The outcome is predetermined by the current stabilizer state +/// - Non-deterministic: The outcome is random with 50-50 probability +/// +/// The measurement functions return both the outcome and whether it was deterministic. +/// +/// # Gate Operations +/// The simulator supports common Clifford gates: +/// - Pauli gates (X, Y, Z) +/// - Hadamard (H) +/// - Phase gates (S = SZ = √Z) +/// - CX and other 2-qubit Clifford gates +/// +/// Each gate operation updates the stabilizer and destabilizer generators according to +/// the appropriate Heisenberg representation transformations. +/// +/// # Memory Efficiency +/// The sparse representation is memory efficient for: +/// - States with local correlations +/// - Circuit intermediates with limited entanglement +/// - Error correction scenarios where most stabilizers are low-weight +/// +/// # Performance Considerations +/// - Row/column access patterns are optimized for common operations +/// - Signs are stored separately from Pauli operators +/// - Non-deterministic measurements require tableau updates +/// +/// # Limitations +/// - Only supports Clifford operations +/// - Cannot represent arbitrary quantum states +/// - Measurement outcomes are truly random (not pseudo-random) +/// +/// # References +/// 1. Aaronson & Gottesman, "Improved Simulation of Stabilizer Circuits" +/// +/// 2. Ryan-Anderson, "Quantum Algorithms, Architecture, and Error Correction" +/// #[derive(Clone, Debug)] pub struct SparseStab where @@ -38,7 +115,7 @@ where { #[inline] #[must_use] - fn new(num_qubits: usize) -> Self { + pub fn new(num_qubits: usize) -> Self { let rng = SimRng::from_entropy(); Self::with_rng(num_qubits, rng) } @@ -387,8 +464,37 @@ where // TODO: pub fun p(&mut self, pauli: &pauli, q: U) { todo!() } // TODO: pub fun m(&mut self, pauli: &pauli, q: U) -> bool { todo!() } - /// Measurement of the +`Z_q` operator. + + /// Measures a qubit in the Z basis. + /// + /// Returns a tuple containing: + /// - The measurement outcome (true = |1>, false = |0>) + /// - Whether the measurement was deterministic + /// + /// The measurement can be: + /// - Deterministic: The outcome is fixed by the current stabilizer state + /// - Non-deterministic: The outcome is random with 50% probability for each result + /// + /// # Arguments + /// * q - The qubit index to measure + /// + /// # Returns + /// * (bool, bool) - (`measurement_outcome`, `is_deterministic`) + /// + /// # Example + /// ```rust + /// use pecos_core::VecSet; + /// use pecos_qsim::{QuantumSimulator, CliffordSimulator, SparseStab}; + /// let mut state = SparseStab::, u32>::new(2); + /// + /// let (outcome, deterministic) = state.mz(0); + /// if deterministic { + /// println!("Measurement was deterministic with outcome: {}", outcome); + /// } + /// ``` + /// /// # Panics + /// /// Will panic if qubit ids don't convert to usize. #[inline] fn mz(&mut self, q: E) -> (bool, bool) { @@ -502,6 +608,33 @@ where } } + /// Applies a CX or CNOT (Controlled-X) gate between two qubits. + /// + /// The CX performs the transformation: + /// - |0⟩|b⟩ → |0⟩|b⟩ + /// - |1⟩|b⟩ → |1⟩|b⊕1⟩ + /// + /// In the Heisenberg picture, it transforms the Pauli operators as: + /// - IX → IX + /// - XI → XX + /// - IZ → ZZ + /// - ZI → ZI + /// + /// # Arguments + /// * q1 - Control qubit index + /// * q2 - Target qubit index + /// + /// # Example + /// ```rust + /// use pecos_core::VecSet; + /// use pecos_qsim::{QuantumSimulator, CliffordSimulator, SparseStab}; + /// let mut state = SparseStab::, u32>::new(2); + /// + /// // Create Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2 + /// state.h(0); // Put first qubit in |+⟩ + /// state.cx(0, 1); // Entangle qubits + /// ``` + /// /// CX: +IX -> +IX; +IZ -> +ZZ; +XI -> +XX; +ZI -> +ZI /// # Panics /// Will panic if qubit ids don't convert to usize. diff --git a/crates/pecos-qsim/src/statevec.rs b/crates/pecos-qsim/src/statevec.rs index 59be6ea8..30849f7c 100644 --- a/crates/pecos-qsim/src/statevec.rs +++ b/crates/pecos-qsim/src/statevec.rs @@ -1605,7 +1605,7 @@ mod tests { } #[test] - #[should_panic(expected = "assertion failed: control != target")] + #[should_panic(expected = "assertion `left != right` failed\n left: 0\n right: 0")] fn test_control_equals_target() { let mut q = StateVec::new(2); q.cx(0, 0); // Control and target are the same From eec20ecd3ba4b7e441f99a163f5f36d06eb5833b Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 17 Dec 2024 22:47:16 -0700 Subject: [PATCH 06/33] More docs --- Cargo.lock | 10 - crates/pecos-qsim/Cargo.toml | 3 - crates/pecos-qsim/src/clifford_simulator.rs | 304 ++++++++++++++++---- 3 files changed, 251 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cb6b1c7..480cbf9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,15 +115,6 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "arrayvec" version = "0.7.6" @@ -1205,7 +1196,6 @@ dependencies = [ name = "pecos-qsim" version = "0.1.1" dependencies = [ - "approx", "num-complex", "pecos-core", "rand", diff --git a/crates/pecos-qsim/Cargo.toml b/crates/pecos-qsim/Cargo.toml index a23a8c00..9b1660cf 100644 --- a/crates/pecos-qsim/Cargo.toml +++ b/crates/pecos-qsim/Cargo.toml @@ -16,9 +16,6 @@ rand = { workspace = true } rand_chacha = { workspace = true } num-complex = { workspace = true } -[dev-dependencies] -approx = "0.5" - [lints] workspace = true diff --git a/crates/pecos-qsim/src/clifford_simulator.rs b/crates/pecos-qsim/src/clifford_simulator.rs index 2008d565..1a9924b2 100644 --- a/crates/pecos-qsim/src/clifford_simulator.rs +++ b/crates/pecos-qsim/src/clifford_simulator.rs @@ -13,7 +13,87 @@ use super::quantum_simulator::QuantumSimulator; use pecos_core::IndexableElement; -/// A simulator that implements Clifford gates. +/// A simulator trait for quantum systems that implement Clifford operations. +/// +/// # Overview +/// The Clifford group is a set of quantum operations that map Pauli operators to Pauli operators +/// under conjugation. A Clifford operation C transforms a Pauli operator P as: +/// ```text +/// C P C† = P' +/// ``` +/// where P' is another Pauli operator (possibly with a phase ±1 or ±i). +/// +/// # Gate Set +/// This trait provides: +/// +/// ## Single-qubit gates +/// - Pauli gates (X, Y, Z) +/// - Hadamard (H) and variants (H2-H6) +/// - Phase gates (SX, SY, SZ) and their adjoints +/// - Face (F) gates and variants (F, F2-F4) and their adjoints +/// +/// ## Two-qubit gates +/// - CNOT (CX) +/// - Controlled-Y (CY) +/// - Controlled-Z (CZ) +/// - SWAP +/// - √XX, √YY, √ZZ and their adjoints +/// - G2 (a two-qubit Clifford) +/// +/// ## Measurements and Preparations +/// - Measurements in X, Y, Z bases (including ± variants) +/// - State preparations in X, Y, Z bases (including ± variants) +/// +/// # Type Parameters +/// - T: An indexable element type that can convert between qubit indices and usizes +/// +/// # Gate Transformations +/// Gates transform Pauli operators according to their Heisenberg representation. For example: +/// +/// Hadamard (H): +/// ```text +/// X → Z +/// Z → X +/// Y → -Y +/// ``` +/// +/// CNOT (with control c and target t): +/// ```text +/// Xc → Xc⊗Xt +/// Xt → Xt +/// Zc → Zc +/// Zt → Zc⊗Zt +/// ``` +/// +/// # Measurement Semantics +/// - All measurements return (outcome, deterministic) +/// - outcome: true = +1 eigenstate, false = -1 eigenstate +/// - deterministic: true if state was already in an eigenstate +/// +/// # Examples +/// ```rust +/// use pecos_core::VecSet; +/// use pecos_qsim::{CliffordSimulator, SparseStab}; +/// +/// let mut sim = SparseStab::, u32>::new(2); +/// +/// // Create Bell state +/// sim.h(0); +/// sim.cx(0, 1); +/// +/// // Measure in Z basis +/// let (outcome, deterministic) = sim.mz(0); +/// ``` +/// +/// # Implementation Notes +/// - Most multi-qubit gates are implemented in terms of simpler gates +/// - The trait provides default implementations where possible +/// - Implementors must provide: `mz()`, `x()`, `y()`, `z()`, `h()`, `sz()`, `cx()` +/// - All other operations have default implementations +/// +/// # References +/// - Gottesman, "The Heisenberg Representation of Quantum Computers" +/// #[expect(clippy::min_ident_chars)] pub trait CliffordSimulator: QuantumSimulator { #[inline] @@ -35,7 +115,16 @@ pub trait CliffordSimulator: QuantumSimulator { ::reset(self) } - /// Preparation of the +`X_q` operator. + /// Prepares a qubit in the +1 eigenstate of the X operator. + /// + /// Equivalent to preparing |+⟩ = (|0⟩ + |1⟩)/√2 + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] @@ -44,12 +133,21 @@ pub trait CliffordSimulator: QuantumSimulator { if meas { self.z(q); } - (meas, deter) } - /// Preparation of the -`X_q` operator. + /// Prepares a qubit in the -1 eigenstate of the X operator. + /// + /// Equivalent to preparing |-⟩ = (|0⟩ - |1⟩)/√2 + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// /// # Panics + /// /// Will panic if qubit ids don't convert to usize. #[inline] fn pnx(&mut self, q: T) -> (bool, bool) { @@ -69,8 +167,6 @@ pub trait CliffordSimulator: QuantumSimulator { if meas { self.z(q); } - // let (meas, deter) = self.pz(q); - // self.h5(q); (meas, deter) } @@ -83,13 +179,21 @@ pub trait CliffordSimulator: QuantumSimulator { if meas { self.z(q); } - // let (meas, deter) = self.pz(q); - // self.h6(q); (meas, deter) } - /// Preparation of the +`Z_q` operator. + /// Prepares a qubit in the +1 eigenstate of the Z operator. + /// + /// Equivalent to preparing |0⟩ + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// /// # Panics + /// /// Will panic if qubit ids don't convert to usize. #[inline] fn pz(&mut self, q: T) -> (bool, bool) { @@ -100,8 +204,18 @@ pub trait CliffordSimulator: QuantumSimulator { (meas, deter) } - /// Preparation of the -`Z_q` operator. + /// Prepares a qubit in the -1 eigenstate of the Z operator. + /// + /// Equivalent to preparing |1⟩ + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// /// # Panics + /// /// Will panic if qubit ids don't convert to usize. #[inline] fn pnz(&mut self, q: T) -> (bool, bool) { @@ -145,11 +259,9 @@ pub trait CliffordSimulator: QuantumSimulator { fn my(&mut self, q: T) -> (bool, bool) { // +Y -> +Z self.sx(q); - // self.h5(q); let (meas, deter) = self.mz(q); // +Z -> +Y self.sxdg(q); - // self.h5(q); (meas, deter) } @@ -161,11 +273,9 @@ pub trait CliffordSimulator: QuantumSimulator { fn mny(&mut self, q: T) -> (bool, bool) { // -Y -> +Z self.sxdg(q); - // self.h6(q); let (meas, deter) = self.mz(q); // +Z -> -Y self.sx(q); - // self.h6(q); (meas, deter) } @@ -207,9 +317,6 @@ pub trait CliffordSimulator: QuantumSimulator { /// Y -> Z #[inline] fn sx(&mut self, q: T) { - // X -H-> Z -SZ-> Z -H-> X - // Z -H-> X -SZ-> Y -H-> -Y - // Y -H-> -Y -SZ-> X -H-> Z self.h(q); self.sz(q); self.h(q); @@ -222,9 +329,6 @@ pub trait CliffordSimulator: QuantumSimulator { /// Y -> -Z #[inline] fn sxdg(&mut self, q: T) { - // X -H-> Z -Z-> Z -SZ-> Z -H-> X - // Z -H-> X -Z-> -X -SZ-> -Y -H-> Y - // Y -H-> -Y -Z-> Y -SZ-> -X -H-> -Z self.h(q); self.szdg(q); self.h(q); @@ -266,9 +370,6 @@ pub trait CliffordSimulator: QuantumSimulator { /// Y -> X #[inline] fn szdg(&mut self, q: T) { - // X -Z-> -X -SZ-> -Y - // Z -Z-> Z -SZ-> Z - // Y -Z-> -Y -SZ-> X self.z(q); self.sz(q); } @@ -367,7 +468,25 @@ pub trait CliffordSimulator: QuantumSimulator { self.szdg(q); } - /// Controlled-not gate. IX -> IX, IZ -> ZZ, XI -> XX, ZI -> ZZ + /// Performs a controlled-NOT operation between two qubits. + /// + /// The operation performs: + /// ```text + /// |0⟩|b⟩ → |0⟩|b⟩ + /// |1⟩|b⟩ → |1⟩|b⊕1⟩ + /// ``` + /// + /// In the Heisenberg picture, transforms Pauli operators as: + /// ```text + /// IX → IX (X on target unchanged) + /// XI → XX (X on control propagates to target) + /// IZ → ZZ (Z on target propagates to control) + /// ZI → ZI (Z on control unchanged) + /// ``` + /// + /// # Arguments + /// * `q1` - Control qubit + /// * `q2` - Target qubit fn cx(&mut self, q1: T, q2: T); /// CY: +IX -> +ZX; +IZ -> +ZZ; +XI -> -XY; +ZI -> +ZI; @@ -386,10 +505,24 @@ pub trait CliffordSimulator: QuantumSimulator { self.h(q2); } - /// SXX: XI -> XI - /// IX -> IX - /// ZI -> -YX - /// IZ -> -XY + /// Performs a square root of XX operation (√XX). + /// + /// This is a symmetric two-qubit Clifford that implements: + /// ```text + /// SXX = exp(-iπ X⊗X/4) + /// ``` + /// + /// Transforms Pauli operators as: + /// ```text + /// XI → XI + /// IX → IX + /// ZI → -YX + /// IZ → -XY + /// ``` + /// + /// # Arguments + /// * q1 - First qubit + /// * q2 - Second qubit #[inline] fn sxx(&mut self, q1: T, q2: T) { self.sx(q1); @@ -399,10 +532,24 @@ pub trait CliffordSimulator: QuantumSimulator { self.sy(q1); } - /// `SXXdg`: XI -> XI - /// IX -> IX - /// ZI -> YX - /// IZ -> XY + /// Performs an adjoint of the square root of XX operation (√XX†). + /// + /// This is a symmetric two-qubit Clifford that implements: + /// ```text + /// SXXdg = exp(+iπ X⊗X/4) + /// ``` + /// + /// Transforms Pauli operators as: + /// ```text + /// XI → XI + /// IX → IX + /// ZI → YX + /// IZ → XY + /// ``` + /// + /// # Arguments + /// * `q1` - First qubit + /// * `q2` - Second qubit #[inline] fn sxxdg(&mut self, q1: T, q2: T) { self.x(q1); @@ -410,10 +557,24 @@ pub trait CliffordSimulator: QuantumSimulator { self.sxx(q1, q2); } - /// SYY: XI -> -ZY - /// IX -> -YZ - /// ZI -> XY - /// IZ -> YX + /// Performs a square root of YY operation (√YY). + /// + /// This is a symmetric two-qubit Clifford that implements: + /// ```text + /// SYY = exp(-iπ Y⊗Y/4) + /// ``` + /// + /// Transforms Pauli operators as: + /// ```text + /// XI → -ZY + /// IX → -YZ + /// ZI → XY + /// IZ → YX + /// ``` + /// + /// # Arguments + /// * `q1` - First qubit + /// * `q2` - Second qubit #[inline] fn syy(&mut self, q1: T, q2: T) { self.szdg(q1); @@ -423,10 +584,24 @@ pub trait CliffordSimulator: QuantumSimulator { self.sz(q2); } - /// `SYYdg`: XI -> ZY - /// IX -> YZ - /// ZI -> -XY - /// IZ -> -YX + /// Performs an adjoint of the square root of YY operation (√YY†). + /// + /// This is a symmetric two-qubit Clifford that implements: + /// ```text + /// SYY = exp(+iπ Y⊗Y/4) + /// ``` + /// + /// Transforms Pauli operators as: + /// ```text + /// XI → ZY + /// IX → YZ + /// ZI → -XY + /// IZ → -YX + /// ``` + /// + /// # Arguments + /// * `q1` - First qubit + /// * `q2` - Second qubit #[inline] fn syydg(&mut self, q1: T, q2: T) { self.y(q1); @@ -434,10 +609,24 @@ pub trait CliffordSimulator: QuantumSimulator { self.syy(q1, q2); } - /// SZZ: +IX -> +ZY; - /// +IZ -> +IZ; - /// +XI -> +YZ; - /// +ZI -> +ZI; + /// Performs a square root of ZZ operation (√ZZ). + /// + /// This is a symmetric two-qubit Clifford that implements: + /// ```text + /// SZZ = exp(-iπ Z⊗Z/4) + /// ``` + /// + /// Transforms Pauli operators as: + /// ```text + /// XI → YZ + /// IX → ZY + /// ZI → ZI + /// IZ → IZ + /// ``` + /// + /// # Arguments + /// * `q1` - First qubit + /// * `q2` - Second qubit #[inline] fn szz(&mut self, q1: T, q2: T) { self.sydg(q1); @@ -447,20 +636,29 @@ pub trait CliffordSimulator: QuantumSimulator { self.sy(q2); } - /// `SZZdg`: +IX -> -ZY; - /// +IZ -> +IZ; - /// +XI -> -ZY; - /// +ZI -> +ZI; + /// Performs an adjoint of the square root of ZZ operation (√ZZ). + /// + /// This is a symmetric two-qubit Clifford that implements: + /// ```text + /// SZZdg = exp(+iπ Z⊗Z/4) + /// ``` + /// + /// Transforms Pauli operators as: + /// ```text + /// XI → -ZY + /// IX → -YZ TODO: verify + /// ZI → ZI + /// IZ → IZ + /// ``` + /// + /// # Arguments + /// * `q1` - First qubit + /// * `q2` - Second qubit #[inline] fn szzdg(&mut self, q1: T, q2: T) { self.z(q1); self.z(q2); self.szz(q1, q2); - // self.sy(q1); - // self.sy(q2); - // self.sxxdg(q1, q2); - // self.sydg(q1); - // self.sydg(q2); } /// SWAP: +IX -> XI; From 0cade4ced3b413ebf24432edba4395e5ea9de670 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Fri, 20 Dec 2024 02:16:46 -0700 Subject: [PATCH 07/33] qsim documenting + tweaks --- crates/pecos-qsim/src/clifford_simulator.rs | 283 +++++++++++++++--- crates/pecos-qsim/src/lib.rs | 9 +- crates/pecos-qsim/src/pauli_prop.rs | 11 - crates/pecos-qsim/src/prelude.rs | 6 + crates/pecos-qsim/src/quantum_simulator.rs | 2 - crates/pecos-qsim/src/sparse_stab.rs | 14 +- .../src/{statevec.rs => state_vec.rs} | 17 +- 7 files changed, 270 insertions(+), 72 deletions(-) create mode 100644 crates/pecos-qsim/src/prelude.rs rename crates/pecos-qsim/src/{statevec.rs => state_vec.rs} (99%) diff --git a/crates/pecos-qsim/src/clifford_simulator.rs b/crates/pecos-qsim/src/clifford_simulator.rs index 1a9924b2..39de73ed 100644 --- a/crates/pecos-qsim/src/clifford_simulator.rs +++ b/crates/pecos-qsim/src/clifford_simulator.rs @@ -45,7 +45,7 @@ use pecos_core::IndexableElement; /// - State preparations in X, Y, Z bases (including ± variants) /// /// # Type Parameters -/// - T: An indexable element type that can convert between qubit indices and usizes +/// - `T`: An indexable element type that can convert between qubit indices and usizes /// /// # Gate Transformations /// Gates transform Pauli operators according to their Heisenberg representation. For example: @@ -59,10 +59,10 @@ use pecos_core::IndexableElement; /// /// CNOT (with control c and target t): /// ```text -/// Xc → Xc⊗Xt -/// Xt → Xt -/// Zc → Zc -/// Zt → Zc⊗Zt +/// Xc⊗It → Xc⊗Xt +/// Ic⊗Xt → Ic⊗Xt +/// Zc⊗It → Zc⊗It +/// Ic⊗Zt → Zc⊗Zt /// ``` /// /// # Measurement Semantics @@ -72,10 +72,8 @@ use pecos_core::IndexableElement; /// /// # Examples /// ```rust -/// use pecos_core::VecSet; -/// use pecos_qsim::{CliffordSimulator, SparseStab}; -/// -/// let mut sim = SparseStab::, u32>::new(2); +/// use pecos_qsim::{CliffordSimulator, StdSparseStab}; +/// let mut sim = StdSparseStab::new(2); /// /// // Create Bell state /// sim.h(0); @@ -85,31 +83,43 @@ use pecos_core::IndexableElement; /// let (outcome, deterministic) = sim.mz(0); /// ``` /// -/// # Implementation Notes -/// - Most multi-qubit gates are implemented in terms of simpler gates -/// - The trait provides default implementations where possible -/// - Implementors must provide: `mz()`, `x()`, `y()`, `z()`, `h()`, `sz()`, `cx()` -/// - All other operations have default implementations +/// # Required Implementations +/// When implementing this trait, the following methods must be provided at minimum: +/// - `mz()`: Z-basis measurement +/// - `x()`: Pauli X gate +/// - `y()`: Pauli Y gate +/// - `z()`: Pauli Z gate +/// - `h()`: Hadamard gate +/// - `sz()`: Square root of Z gate +/// - `cx()`: Controlled-NOT gate +/// +/// All other operations have default implementations in terms of these basic gates. +/// Implementors may override any default implementation for efficiency. /// /// # References /// - Gottesman, "The Heisenberg Representation of Quantum Computers" /// #[expect(clippy::min_ident_chars)] pub trait CliffordSimulator: QuantumSimulator { - #[inline] - #[must_use] - fn new(num_qubits: usize) -> Self - where - Self: Sized, - { - ::new(num_qubits) - } - + /// Returns the number of qubits in the system. + /// + /// This method is inherited from `QuantumSimulator` and returns the total number + /// of qubits that this simulator instance is configured to handle. + /// + /// # Returns + /// * `usize` - The number of qubits in the system #[inline] fn num_qubits(&self) -> usize { ::num_qubits(self) } + /// Resets the quantum system to its initial state. + /// + /// This method returns all qubits to the |0⟩ state and clears any entanglement + /// or quantum correlations between qubits. + /// + /// # Returns + /// * `&mut Self` - A mutable reference to the simulator for method chaining #[inline] fn reset(&mut self) -> &mut Self { ::reset(self) @@ -280,7 +290,31 @@ pub trait CliffordSimulator: QuantumSimulator { (meas, deter) } - /// Measurement of the +`Z_q` operator. + /// Performs a measurement in the Z basis on the specified qubit. + /// + /// This measurement projects the qubit state onto the +1 or -1 eigenstate + /// of the Z operator (corresponding to |0⟩ or |1⟩ respectively). + /// + /// # Arguments + /// * `q` - The qubit to measure + /// + /// For all measurement operations (mx, my, mz, mnx, mny, mnz): + /// + /// # Return Values + /// Returns a tuple `(outcome, deterministic)` where: + /// * `outcome`: + /// - `true` indicates projection onto the +1 eigenstate + /// - `false` indicates projection onto the -1 eigenstate + /// * `deterministic`: + /// - `true` if the state was already in an eigenstate of the measured operator + /// - `false` if the measurement result was random (state was in superposition) + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let (outcome, deterministic) = sim.mz(0); + /// ``` fn mz(&mut self, q: T) -> (bool, bool); /// Measurement of the -`Z_q` operator. @@ -301,13 +335,67 @@ pub trait CliffordSimulator: QuantumSimulator { #[inline] fn identity(&mut self, _q: T) {} - /// Pauli X gate. X -> X, Z -> -Z + /// Applies a Pauli X (NOT) gate to the specified qubit. + /// + /// The X gate is equivalent to a classical NOT operation in the computational basis. + /// It transforms the Pauli operators as follows: + /// ```text + /// X → X + /// Y → -Y + /// Z → -Z + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.x(0); // Apply X gate to qubit 0 + /// ``` fn x(&mut self, q: T); - /// Pauli Y gate. X -> -X, Z -> -Z + /// Applies a Pauli Y gate to the specified qubit. + /// + /// The Y gate is a rotation by π radians around the Y axis of the Bloch sphere. + /// It transforms the Pauli operators as follows: + /// ```text + /// X → -X + /// Y → Y + /// Z → -Z + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.y(0); // Apply Y gate to qubit 0 + /// ``` fn y(&mut self, q: T); - /// Pauli Z gate. X -> -X, Z -> Z + /// Applies a Pauli Z gate to the specified qubit. + /// + /// The Z gate applies a phase flip in the computational basis. + /// It transforms the Pauli operators as follows: + /// ```text + /// X → -X + /// Y → -Y + /// Z → Z + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.z(0); // Apply X gate to qubit 0 + /// ``` fn z(&mut self, q: T); /// Sqrt of X gate. @@ -334,11 +422,33 @@ pub trait CliffordSimulator: QuantumSimulator { self.h(q); } - /// Sqrt of Y gate. - /// X -> -Z - /// Z -> X - /// W -> W - /// Y -> Y + /// Applies a square root of Y gate to the specified qubit. + /// + /// The SY gate is equivalent to a rotation by π/2 radians around the Y axis + /// of the Bloch sphere. It transforms the Pauli operators as follows: + /// ```text + /// X → -Z + /// Z → X + /// Y → Y + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Mathematical Details + /// The SY gate has the following matrix representation: + /// ```text + /// SY = 1/√2 [1 -1] + /// [1 1] + /// ``` + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.sy(0); // Apply square root of Y gate to qubit 0 + /// ``` + /// /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] @@ -360,7 +470,25 @@ pub trait CliffordSimulator: QuantumSimulator { self.h(q); } - /// Sqrt of Z gate. +X -> +Y; +Z -> +Z; +Y -> -X; + /// Applies a square root of Z gate (S or SZ gate) to the specified qubit. + /// + /// The SZ gate is equivalent to a rotation by π/2 radians around the Z axis + /// of the Bloch sphere. It transforms the Pauli operators as follows: + /// ```text + /// X → Y + /// Y → -X + /// Z → Z + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Mathematical Details + /// The S gate has the following matrix representation: + /// ```text + /// S = [1 0] + /// [0 i] + /// ``` fn sz(&mut self, q: T); /// Adjoint of Sqrt of Z gate. X -> ..., Z -> ... @@ -374,10 +502,44 @@ pub trait CliffordSimulator: QuantumSimulator { self.sz(q); } - /// Hadamard gate. X -> Z, Z -> X + /// Applies a Hadamard gate to the specified qubit. + /// + /// The Hadamard gate creates an equal superposition of the computational basis + /// states and is fundamental to many quantum algorithms. It transforms the + /// Pauli operators as follows: + /// ```text + /// X → Z + /// Y → -Y + /// Z → X + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Mathematical Details + /// The Hadamard gate has the following matrix representation: + /// ```text + /// H = 1/√2 [1 1] + /// [1 -1] + /// ``` fn h(&mut self, q: T); - /// X -> -Z, Z -> -X, Y -> -Y + /// Applies a variant of the Hadamard gate (H2) to the specified qubit. + /// + /// H2 is part of the family of Hadamard-like gates that map X to Z and Z to X + /// with different sign combinations. It transforms the Pauli operators as follows: + /// ```text + /// X → -Z + /// Z → -X + /// Y → -Y + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Notes + /// - H2 is equivalent to the composition SY • Z # TODO: Verify + /// - H2 differs from H by introducing additional minus signs #[inline] fn h2(&mut self, q: T) { self.sy(q); @@ -412,7 +574,28 @@ pub trait CliffordSimulator: QuantumSimulator { self.y(q); } - /// X -> Y, Z -> X, Y -> Z + /// Applies a Face gate (F) to the specified qubit. + /// + /// The F gate is a member of the Clifford group that cyclically permutes + /// the Pauli operators. It transforms them as follows: + /// ```text + /// X → Y + /// Y → Z + /// Z → X + /// ``` + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Mathematical Details + /// The F gate can be implemented as F = SX • SZ # TODO: verify + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.f(0); // Apply F gate to qubit 0 + /// ``` #[inline] fn f(&mut self, q: T) { self.sx(q); @@ -672,10 +855,30 @@ pub trait CliffordSimulator: QuantumSimulator { self.cx(q1, q2); } - /// G2: +XI -> +IX - /// +IX -> +XI - /// +ZI -> +XZ - /// +IZ -> +ZX + /// Applies the G2 two-qubit Clifford operation. + /// + /// G2 is a symmetric two-qubit operation that implements a particular permutation + /// of single-qubit Paulis. It transforms the Pauli operators as follows: + /// ```text + /// XI → IX + /// IX → XI + /// ZI → XZ + /// IZ → ZX + /// ``` + /// + /// # Arguments + /// * `q1` - First qubit + /// * `q2` - Second qubit + /// + /// # Implementation Details + /// G2 can be implemented as: CZ • (H⊗H) • CZ + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.g2(0, 1); // Apply G2 operation between qubits 0 and 1 + /// ``` #[inline] fn g2(&mut self, q1: T, q2: T) { self.cz(q1, q2); diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index 0dec2ddd..bd3b1109 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -14,11 +14,12 @@ pub mod clifford_simulator; pub mod gens; // pub mod measurement; // pub mod nonclifford_simulator; -// pub mod pauli_prop; +pub mod pauli_prop; // pub mod paulis; +pub mod prelude; pub mod quantum_simulator; pub mod sparse_stab; -pub mod statevec; +pub mod state_vec; pub use clifford_simulator::CliffordSimulator; pub use gens::Gens; @@ -27,5 +28,5 @@ pub use gens::Gens; // pub use pauli_prop::{PauliProp, StdPauliProp}; // pub use paulis::Paulis; pub use quantum_simulator::QuantumSimulator; -pub use sparse_stab::SparseStab; -pub use statevec::StateVec; +pub use sparse_stab::{SparseStab, StdSparseStab}; +pub use state_vec::StateVec; diff --git a/crates/pecos-qsim/src/pauli_prop.rs b/crates/pecos-qsim/src/pauli_prop.rs index d9928ddb..e3740a1c 100644 --- a/crates/pecos-qsim/src/pauli_prop.rs +++ b/crates/pecos-qsim/src/pauli_prop.rs @@ -38,17 +38,6 @@ where E: IndexableElement, T: for<'a> Set<'a, Element = E>, { - #[inline] - #[must_use] - fn new(num_qubits: usize) -> PauliProp { - Self { - num_qubits, - xs: T::new(), - zs: T::new(), - _marker: PhantomData, - } - } - #[inline] fn num_qubits(&self) -> usize { self.num_qubits diff --git a/crates/pecos-qsim/src/prelude.rs b/crates/pecos-qsim/src/prelude.rs new file mode 100644 index 00000000..ef9a8dfe --- /dev/null +++ b/crates/pecos-qsim/src/prelude.rs @@ -0,0 +1,6 @@ +pub use crate::clifford_simulator::CliffordSimulator; +pub use crate::pauli_prop::{PauliProp, StdPauliProp}; +pub use crate::quantum_simulator::QuantumSimulator; +pub use crate::sparse_stab::SparseStab; +pub use crate::state_vec::StateVec; +pub use pecos_core::VecSet; diff --git a/crates/pecos-qsim/src/quantum_simulator.rs b/crates/pecos-qsim/src/quantum_simulator.rs index 06dc27fa..4497532f 100644 --- a/crates/pecos-qsim/src/quantum_simulator.rs +++ b/crates/pecos-qsim/src/quantum_simulator.rs @@ -11,8 +11,6 @@ // the License. pub trait QuantumSimulator { - fn new(num_qubits: usize) -> Self; - fn num_qubits(&self) -> usize; fn reset(&mut self) -> &mut Self; diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index ad700832..f5934b89 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -13,11 +13,14 @@ use crate::{CliffordSimulator, Gens, QuantumSimulator}; use core::fmt::Debug; use core::mem; -use pecos_core::SimRng; use pecos_core::{IndexableElement, Set}; +use pecos_core::{SimRng, VecSet}; use rand_chacha::ChaCha8Rng; // TODO: Look into seeing if a dense bool for signs_minus and signs_i is more efficient +#[expect(clippy::module_name_repetitions)] +pub type StdSparseStab = SparseStab, usize>; + /// A sparse representation of a stabilizer state using the stabilizer/destabilizer formalism. /// /// This implementation is based on the work found in the thesis "Quantum Algorithms, Architecture, @@ -438,12 +441,6 @@ where R: SimRng, T: for<'a> Set<'a, Element = E>, { - #[inline] - #[must_use] - fn new(num_qubits: usize) -> Self { - Self::new(num_qubits) - } - #[inline] fn num_qubits(&self) -> usize { self.num_qubits @@ -464,7 +461,6 @@ where // TODO: pub fun p(&mut self, pauli: &pauli, q: U) { todo!() } // TODO: pub fun m(&mut self, pauli: &pauli, q: U) -> bool { todo!() } - /// Measures a qubit in the Z basis. /// /// Returns a tuple containing: @@ -629,7 +625,7 @@ where /// use pecos_core::VecSet; /// use pecos_qsim::{QuantumSimulator, CliffordSimulator, SparseStab}; /// let mut state = SparseStab::, u32>::new(2); - /// + /// /// // Create Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2 /// state.h(0); // Put first qubit in |+⟩ /// state.cx(0, 1); // Entangle qubits diff --git a/crates/pecos-qsim/src/statevec.rs b/crates/pecos-qsim/src/state_vec.rs similarity index 99% rename from crates/pecos-qsim/src/statevec.rs rename to crates/pecos-qsim/src/state_vec.rs index 30849f7c..aacd8ff9 100644 --- a/crates/pecos-qsim/src/statevec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -9,7 +9,8 @@ pub struct StateVec { impl StateVec { /// Create a new state initialized to |0...0⟩ - #[must_use] pub fn new(num_qubits: usize) -> Self { + #[must_use] + pub fn new(num_qubits: usize) -> Self { let size = 1 << num_qubits; // 2^n let mut state = vec![Complex64::new(0.0, 0.0); size]; state[0] = Complex64::new(1.0, 0.0); // Prep |0...0> @@ -21,7 +22,8 @@ impl StateVec { /// # Panics /// /// Panics if the input state requires more qubits then `StateVec` has. - #[must_use] pub fn from_state(state: Vec) -> Self { + #[must_use] + pub fn from_state(state: Vec) -> Self { let num_qubits = state.len().trailing_zeros() as usize; assert_eq!(1 << num_qubits, state.len(), "Invalid state vector size"); StateVec { num_qubits, state } @@ -45,12 +47,14 @@ impl StateVec { } /// Returns the number of qubits in the system - #[must_use] pub fn num_qubits(&self) -> usize { + #[must_use] + pub fn num_qubits(&self) -> usize { self.num_qubits } /// Returns reference to the state vector - #[must_use] pub fn state(&self) -> &[Complex64] { + #[must_use] + pub fn state(&self) -> &[Complex64] { &self.state } @@ -59,7 +63,8 @@ impl StateVec { /// # Panics /// /// Panics if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) - #[must_use] pub fn probability(&self, basis_state: usize) -> f64 { + #[must_use] + pub fn probability(&self, basis_state: usize) -> f64 { assert!(basis_state < 1 << self.num_qubits); self.state[basis_state].norm_sqr() } @@ -202,7 +207,7 @@ impl StateVec { /// /// # Examples /// ``` - /// use pecos_qsim::statevec::StateVec; + /// use pecos_qsim::state_vec::StateVec; /// use std::f64::consts::FRAC_1_SQRT_2; /// use num_complex::Complex64; /// let mut q = StateVec::new(1); From e39cf4c3933dc99f08fa7cf8735c3656a1c72256 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Fri, 20 Dec 2024 17:41:51 -0700 Subject: [PATCH 08/33] Refactoring: CliffordGateable --- crates/pecos-python/src/sparse_sim.rs | 68 +- crates/pecos-qsim/Cargo.toml | 1 - ...ford_simulator.rs => clifford_gateable.rs} | 610 ++++++++---------- crates/pecos-qsim/src/lib.rs | 9 +- crates/pecos-qsim/src/pauli_prop.rs | 125 +--- crates/pecos-qsim/src/prelude.rs | 4 +- crates/pecos-qsim/src/quantum_simulator.rs | 17 - .../pecos-qsim/src/quantum_simulator_state.rs | 40 ++ crates/pecos-qsim/src/sparse_stab.rs | 405 ++++++++---- crates/pecos-qsim/src/state_vec.rs | 2 +- crates/pecos/src/prelude.rs | 2 +- 11 files changed, 669 insertions(+), 614 deletions(-) rename crates/pecos-qsim/src/{clifford_simulator.rs => clifford_gateable.rs} (77%) delete mode 100644 crates/pecos-qsim/src/quantum_simulator.rs create mode 100644 crates/pecos-qsim/src/quantum_simulator_state.rs diff --git a/crates/pecos-python/src/sparse_sim.rs b/crates/pecos-python/src/sparse_sim.rs index 213b6012..cb56d38a 100644 --- a/crates/pecos-python/src/sparse_sim.rs +++ b/crates/pecos-python/src/sparse_sim.rs @@ -133,9 +133,48 @@ impl SparseSim { self.inner.szdg(location); Ok(None) } - "MZ" | "MX" | "MY" | "MZForced" | "PZ" | "PX" | "PY" | "PZForced" | "PnZ" | "PnX" - | "PnY" => { - let (result, _) = match symbol { + "PZ" => { + self.inner.pz(location); + Ok(None) + } + "PX" => { + self.inner.px(location); + Ok(None) + } + "PY" => { + self.inner.py(location); + Ok(None) + } + "PnZ" => { + self.inner.pnz(location); + Ok(None) + } + "PnX" => { + self.inner.pnx(location); + Ok(None) + } + "PnY" => { + self.inner.pny(location); + Ok(None) + } + "PZForced" => { + let forced_value = params + .ok_or_else(|| { + PyErr::new::("PZForced requires params") + })? + .get_item("forced_outcome")? + .ok_or_else(|| { + PyErr::new::( + "PZForced requires a 'forced_outcome' parameter", + ) + })? + .call_method0("__bool__")? + .extract::()?; + self.inner.pz_forced(location, forced_value); + Ok(None) + } + "MZ" | "MX" | "MY" | "MZForced" => { + let result = match symbol { "MZ" => self.inner.mz(location), "MX" => self.inner.mx(location), "MY" => self.inner.my(location), @@ -156,29 +195,6 @@ impl SparseSim { .extract::()?; self.inner.mz_forced(location, forced_value) } - "PZ" => self.inner.pz(location), - "PX" => self.inner.px(location), - "PY" => self.inner.py(location), - "PZForced" => { - let forced_value = params - .ok_or_else(|| { - PyErr::new::( - "PZForced requires params", - ) - })? - .get_item("forced_outcome")? - .ok_or_else(|| { - PyErr::new::( - "PZForced requires a 'forced_outcome' parameter", - ) - })? - .call_method0("__bool__")? - .extract::()?; - self.inner.pz_forced(location, forced_value) - } - "PnZ" => self.inner.pnz(location), - "PnX" => self.inner.pnx(location), - "PnY" => self.inner.pny(location), _ => unreachable!(), }; Ok(Some(u8::from(result))) diff --git a/crates/pecos-qsim/Cargo.toml b/crates/pecos-qsim/Cargo.toml index 9b1660cf..3bcc2ed6 100644 --- a/crates/pecos-qsim/Cargo.toml +++ b/crates/pecos-qsim/Cargo.toml @@ -18,4 +18,3 @@ num-complex = { workspace = true } [lints] workspace = true - diff --git a/crates/pecos-qsim/src/clifford_simulator.rs b/crates/pecos-qsim/src/clifford_gateable.rs similarity index 77% rename from crates/pecos-qsim/src/clifford_simulator.rs rename to crates/pecos-qsim/src/clifford_gateable.rs index 39de73ed..0033e1f3 100644 --- a/crates/pecos-qsim/src/clifford_simulator.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -10,7 +10,7 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -use super::quantum_simulator::QuantumSimulator; +use super::quantum_simulator_state::QuantumSimulatorState; use pecos_core::IndexableElement; /// A simulator trait for quantum systems that implement Clifford operations. @@ -72,7 +72,7 @@ use pecos_core::IndexableElement; /// /// # Examples /// ```rust -/// use pecos_qsim::{CliffordSimulator, StdSparseStab}; +/// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(2); /// /// // Create Bell state @@ -80,7 +80,7 @@ use pecos_core::IndexableElement; /// sim.cx(0, 1); /// /// // Measure in Z basis -/// let (outcome, deterministic) = sim.mz(0); +/// let outcome = sim.mz(0); /// ``` /// /// # Required Implementations @@ -100,240 +100,12 @@ use pecos_core::IndexableElement; /// - Gottesman, "The Heisenberg Representation of Quantum Computers" /// #[expect(clippy::min_ident_chars)] -pub trait CliffordSimulator: QuantumSimulator { - /// Returns the number of qubits in the system. - /// - /// This method is inherited from `QuantumSimulator` and returns the total number - /// of qubits that this simulator instance is configured to handle. - /// - /// # Returns - /// * `usize` - The number of qubits in the system - #[inline] - fn num_qubits(&self) -> usize { - ::num_qubits(self) - } - - /// Resets the quantum system to its initial state. - /// - /// This method returns all qubits to the |0⟩ state and clears any entanglement - /// or quantum correlations between qubits. - /// - /// # Returns - /// * `&mut Self` - A mutable reference to the simulator for method chaining - #[inline] - fn reset(&mut self) -> &mut Self { - ::reset(self) - } - - /// Prepares a qubit in the +1 eigenstate of the X operator. - /// - /// Equivalent to preparing |+⟩ = (|0⟩ + |1⟩)/√2 - /// - /// # Arguments - /// * `q` - Target qubit - /// - /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) - /// - /// # Panics - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn px(&mut self, q: T) -> (bool, bool) { - let (meas, deter) = self.mx(q); - if meas { - self.z(q); - } - (meas, deter) - } - - /// Prepares a qubit in the -1 eigenstate of the X operator. - /// - /// Equivalent to preparing |-⟩ = (|0⟩ - |1⟩)/√2 - /// - /// # Arguments - /// * `q` - Target qubit - /// - /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) - /// - /// # Panics - /// - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn pnx(&mut self, q: T) -> (bool, bool) { - let (meas, deter) = self.mnx(q); - if meas { - self.z(q); - } - (meas, deter) - } - - /// Preparation of the +`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn py(&mut self, q: T) -> (bool, bool) { - let (meas, deter) = self.my(q); - if meas { - self.z(q); - } - (meas, deter) - } - - /// Preparation of the -`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn pny(&mut self, q: T) -> (bool, bool) { - let (meas, deter) = self.mny(q); - if meas { - self.z(q); - } - (meas, deter) - } - - /// Prepares a qubit in the +1 eigenstate of the Z operator. - /// - /// Equivalent to preparing |0⟩ - /// - /// # Arguments - /// * `q` - Target qubit - /// - /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) - /// - /// # Panics - /// - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn pz(&mut self, q: T) -> (bool, bool) { - let (meas, deter) = self.mz(q); - if meas { - self.x(q); - } - (meas, deter) - } - - /// Prepares a qubit in the -1 eigenstate of the Z operator. - /// - /// Equivalent to preparing |1⟩ - /// - /// # Arguments - /// * `q` - Target qubit - /// - /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) - /// - /// # Panics - /// - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn pnz(&mut self, q: T) -> (bool, bool) { - let (meas, deter) = self.mnz(q); - if meas { - self.x(q); - } - (meas, deter) - } - - /// Measurement of the +`X_q` operator. - #[inline] - fn mx(&mut self, q: T) -> (bool, bool) { - // +X -> +Z - self.h(q); - let (meas, deter) = self.mz(q); - // +Z -> +X - self.h(q); - - (meas, deter) - } - - /// Measurement of the -`X_q` operator. - #[inline] - fn mnx(&mut self, q: T) -> (bool, bool) { - // -X -> +Z - self.h(q); - self.x(q); - let (meas, deter) = self.mz(q); - // +Z -> -X - self.x(q); - self.h(q); - - (meas, deter) - } - - /// Measurement of the +`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn my(&mut self, q: T) -> (bool, bool) { - // +Y -> +Z - self.sx(q); - let (meas, deter) = self.mz(q); - // +Z -> +Y - self.sxdg(q); - - (meas, deter) - } - - /// Measurement of the -`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn mny(&mut self, q: T) -> (bool, bool) { - // -Y -> +Z - self.sxdg(q); - let (meas, deter) = self.mz(q); - // +Z -> -Y - self.sx(q); - - (meas, deter) - } - - /// Performs a measurement in the Z basis on the specified qubit. - /// - /// This measurement projects the qubit state onto the +1 or -1 eigenstate - /// of the Z operator (corresponding to |0⟩ or |1⟩ respectively). - /// - /// # Arguments - /// * `q` - The qubit to measure - /// - /// For all measurement operations (mx, my, mz, mnx, mny, mnz): - /// - /// # Return Values - /// Returns a tuple `(outcome, deterministic)` where: - /// * `outcome`: - /// - `true` indicates projection onto the +1 eigenstate - /// - `false` indicates projection onto the -1 eigenstate - /// * `deterministic`: - /// - `true` if the state was already in an eigenstate of the measured operator - /// - `false` if the measurement result was random (state was in superposition) - /// - /// # Examples - /// ```rust - /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; - /// let mut sim = StdSparseStab::new(1); - /// let (outcome, deterministic) = sim.mz(0); - /// ``` - fn mz(&mut self, q: T) -> (bool, bool); - - /// Measurement of the -`Z_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn mnz(&mut self, q: T) -> (bool, bool) { - // -Z -> +Z - self.x(q); - let (meas, deter) = self.mz(q); - // +Z -> -Z - self.x(q); - - (meas, deter) - } - +pub trait CliffordGateable: QuantumSimulatorState { /// Identity on qubit q. X -> X, Z -> Z #[inline] - fn identity(&mut self, _q: T) {} + fn identity(&mut self, _q: T) -> &mut Self { + self + } /// Applies a Pauli X (NOT) gate to the specified qubit. /// @@ -350,11 +122,13 @@ pub trait CliffordSimulator: QuantumSimulator { /// /// # Examples /// ```rust - /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); /// sim.x(0); // Apply X gate to qubit 0 /// ``` - fn x(&mut self, q: T); + fn x(&mut self, q: T) -> &mut Self { + self.h(q).z(q).h(q) + } /// Applies a Pauli Y gate to the specified qubit. /// @@ -371,11 +145,13 @@ pub trait CliffordSimulator: QuantumSimulator { /// /// # Examples /// ```rust - /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); /// sim.y(0); // Apply Y gate to qubit 0 /// ``` - fn y(&mut self, q: T); + fn y(&mut self, q: T) -> &mut Self { + self.z(q).x(q) + } /// Applies a Pauli Z gate to the specified qubit. /// @@ -392,11 +168,14 @@ pub trait CliffordSimulator: QuantumSimulator { /// /// # Examples /// ```rust - /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); /// sim.z(0); // Apply X gate to qubit 0 /// ``` - fn z(&mut self, q: T); + fn z(&mut self, q: T) -> &mut Self { + self.sz(q).sz(q); + self + } /// Sqrt of X gate. /// X -> X @@ -404,10 +183,11 @@ pub trait CliffordSimulator: QuantumSimulator { /// W -> -iZ /// Y -> Z #[inline] - fn sx(&mut self, q: T) { + fn sx(&mut self, q: T) -> &mut Self { self.h(q); self.sz(q); self.h(q); + self } /// Adjoint of Sqrt X gate. @@ -416,10 +196,11 @@ pub trait CliffordSimulator: QuantumSimulator { /// W -> iZ /// Y -> -Z #[inline] - fn sxdg(&mut self, q: T) { + fn sxdg(&mut self, q: T) -> &mut Self { self.h(q); self.szdg(q); self.h(q); + self } /// Applies a square root of Y gate to the specified qubit. @@ -444,7 +225,7 @@ pub trait CliffordSimulator: QuantumSimulator { /// /// # Examples /// ```rust - /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); /// sim.sy(0); // Apply square root of Y gate to qubit 0 /// ``` @@ -452,9 +233,10 @@ pub trait CliffordSimulator: QuantumSimulator { /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn sy(&mut self, q: T) { + fn sy(&mut self, q: T) -> &mut Self { self.h(q); self.x(q); + self } /// Adjoint of sqrt of Y gate. @@ -465,9 +247,10 @@ pub trait CliffordSimulator: QuantumSimulator { /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn sydg(&mut self, q: T) { + fn sydg(&mut self, q: T) -> &mut Self { self.x(q); self.h(q); + self } /// Applies a square root of Z gate (S or SZ gate) to the specified qubit. @@ -489,7 +272,7 @@ pub trait CliffordSimulator: QuantumSimulator { /// S = [1 0] /// [0 i] /// ``` - fn sz(&mut self, q: T); + fn sz(&mut self, q: T) -> &mut Self; /// Adjoint of Sqrt of Z gate. X -> ..., Z -> ... /// X -> -iW = -Y @@ -497,9 +280,10 @@ pub trait CliffordSimulator: QuantumSimulator { /// W -> -iX /// Y -> X #[inline] - fn szdg(&mut self, q: T) { + fn szdg(&mut self, q: T) -> &mut Self { self.z(q); self.sz(q); + self } /// Applies a Hadamard gate to the specified qubit. @@ -522,7 +306,7 @@ pub trait CliffordSimulator: QuantumSimulator { /// H = 1/√2 [1 1] /// [1 -1] /// ``` - fn h(&mut self, q: T); + fn h(&mut self, q: T) -> &mut Self; /// Applies a variant of the Hadamard gate (H2) to the specified qubit. /// @@ -541,37 +325,42 @@ pub trait CliffordSimulator: QuantumSimulator { /// - H2 is equivalent to the composition SY • Z # TODO: Verify /// - H2 differs from H by introducing additional minus signs #[inline] - fn h2(&mut self, q: T) { + fn h2(&mut self, q: T) -> &mut Self { self.sy(q); self.z(q); + self } /// X -> Y, Z -> -Z, Y -> X #[inline] - fn h3(&mut self, q: T) { + fn h3(&mut self, q: T) -> &mut Self { self.sz(q); self.y(q); + self } /// X -> -Y, Z -> -Z, Y -> -X #[inline] - fn h4(&mut self, q: T) { + fn h4(&mut self, q: T) -> &mut Self { self.sz(q); self.x(q); + self } /// X -> -X, Z -> Y, Y -> Z #[inline] - fn h5(&mut self, q: T) { + fn h5(&mut self, q: T) -> &mut Self { self.sx(q); self.z(q); + self } /// X -> -X, Z -> -Y, Y -> -Z #[inline] - fn h6(&mut self, q: T) { + fn h6(&mut self, q: T) -> &mut Self { self.sx(q); self.y(q); + self } /// Applies a Face gate (F) to the specified qubit. @@ -592,63 +381,55 @@ pub trait CliffordSimulator: QuantumSimulator { /// /// # Examples /// ```rust - /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); /// sim.f(0); // Apply F gate to qubit 0 /// ``` #[inline] - fn f(&mut self, q: T) { - self.sx(q); - self.sz(q); + fn f(&mut self, q: T) -> &mut Self { + self.sx(q).sz(q) } /// X -> Z, Z -> Y, Y -> X #[inline] - fn fdg(&mut self, q: T) { - self.szdg(q); - self.sxdg(q); + fn fdg(&mut self, q: T) -> &mut Self { + self.szdg(q).sxdg(q) } /// X -> -Z, Z -> Y, Y -> -X #[inline] - fn f2(&mut self, q: T) { - self.sxdg(q); - self.sy(q); + fn f2(&mut self, q: T) -> &mut Self { + self.sxdg(q).sy(q) } /// X -> -Y, Z -> -X, Y -> Z #[inline] - fn f2dg(&mut self, q: T) { - self.sydg(q); - self.sx(q); + fn f2dg(&mut self, q: T) -> &mut Self { + self.sydg(q).sx(q) } /// X -> Y, Z -> -X, Y -> -Z #[inline] - fn f3(&mut self, q: T) { - self.sxdg(q); - self.sz(q); + fn f3(&mut self, q: T) -> &mut Self { + self.sxdg(q).sz(q) } /// X -> -Z, Z -> -Y, Y -> X #[inline] - fn f3dg(&mut self, q: T) { - self.szdg(q); - self.sx(q); + fn f3dg(&mut self, q: T) -> &mut Self { + self.szdg(q).sx(q) } /// X -> Z, Z -> -Y, Y -> -X #[inline] - fn f4(&mut self, q: T) { - self.sz(q); - self.sx(q); + fn f4(&mut self, q: T) -> &mut Self { + self.sz(q).sx(q) } /// X -> -Y, Z -> X, Y -> -Z #[inline] - fn f4dg(&mut self, q: T) { - self.sxdg(q); - self.szdg(q); + fn f4dg(&mut self, q: T) -> &mut Self { + self.sxdg(q).szdg(q) } /// Performs a controlled-NOT operation between two qubits. @@ -670,22 +451,18 @@ pub trait CliffordSimulator: QuantumSimulator { /// # Arguments /// * `q1` - Control qubit /// * `q2` - Target qubit - fn cx(&mut self, q1: T, q2: T); + fn cx(&mut self, q1: T, q2: T) -> &mut Self; /// CY: +IX -> +ZX; +IZ -> +ZZ; +XI -> -XY; +ZI -> +ZI; #[inline] - fn cy(&mut self, q1: T, q2: T) { - self.sz(q2); - self.cx(q1, q2); - self.szdg(q2); + fn cy(&mut self, q1: T, q2: T) -> &mut Self { + self.sz(q2).cx(q1, q2).szdg(q2) } /// CZ: +IX -> +ZX; +IZ -> +IZ; +XI -> +XZ; +ZI -> +ZI; #[inline] - fn cz(&mut self, q1: T, q2: T) { - self.h(q2); - self.cx(q1, q2); - self.h(q2); + fn cz(&mut self, q1: T, q2: T) -> &mut Self { + self.h(q2).cx(q1, q2).h(q2) } /// Performs a square root of XX operation (√XX). @@ -707,12 +484,8 @@ pub trait CliffordSimulator: QuantumSimulator { /// * q1 - First qubit /// * q2 - Second qubit #[inline] - fn sxx(&mut self, q1: T, q2: T) { - self.sx(q1); - self.sx(q2); - self.sydg(q1); - self.cx(q1, q2); - self.sy(q1); + fn sxx(&mut self, q1: T, q2: T) -> &mut Self { + self.sx(q1).sx(q2).sydg(q1).cx(q1, q2).sy(q1) } /// Performs an adjoint of the square root of XX operation (√XX†). @@ -734,10 +507,8 @@ pub trait CliffordSimulator: QuantumSimulator { /// * `q1` - First qubit /// * `q2` - Second qubit #[inline] - fn sxxdg(&mut self, q1: T, q2: T) { - self.x(q1); - self.x(q2); - self.sxx(q1, q2); + fn sxxdg(&mut self, q1: T, q2: T) -> &mut Self { + self.x(q1).x(q2).sxx(q1, q2) } /// Performs a square root of YY operation (√YY). @@ -759,12 +530,8 @@ pub trait CliffordSimulator: QuantumSimulator { /// * `q1` - First qubit /// * `q2` - Second qubit #[inline] - fn syy(&mut self, q1: T, q2: T) { - self.szdg(q1); - self.szdg(q2); - self.sxx(q1, q2); - self.sz(q1); - self.sz(q2); + fn syy(&mut self, q1: T, q2: T) -> &mut Self { + self.szdg(q1).szdg(q2).sxx(q1, q2).sz(q1).sz(q2) } /// Performs an adjoint of the square root of YY operation (√YY†). @@ -786,10 +553,8 @@ pub trait CliffordSimulator: QuantumSimulator { /// * `q1` - First qubit /// * `q2` - Second qubit #[inline] - fn syydg(&mut self, q1: T, q2: T) { - self.y(q1); - self.y(q2); - self.syy(q1, q2); + fn syydg(&mut self, q1: T, q2: T) -> &mut Self { + self.y(q1).y(q2).syy(q1, q2) } /// Performs a square root of ZZ operation (√ZZ). @@ -811,12 +576,8 @@ pub trait CliffordSimulator: QuantumSimulator { /// * `q1` - First qubit /// * `q2` - Second qubit #[inline] - fn szz(&mut self, q1: T, q2: T) { - self.sydg(q1); - self.sydg(q2); - self.sxx(q1, q2); - self.sy(q1); - self.sy(q2); + fn szz(&mut self, q1: T, q2: T) -> &mut Self { + self.sydg(q1).sydg(q2).sxx(q1, q2).sy(q1).sy(q2) } /// Performs an adjoint of the square root of ZZ operation (√ZZ). @@ -838,10 +599,8 @@ pub trait CliffordSimulator: QuantumSimulator { /// * `q1` - First qubit /// * `q2` - Second qubit #[inline] - fn szzdg(&mut self, q1: T, q2: T) { - self.z(q1); - self.z(q2); - self.szz(q1, q2); + fn szzdg(&mut self, q1: T, q2: T) -> &mut Self { + self.z(q1).z(q2).szz(q1, q2) } /// SWAP: +IX -> XI; @@ -849,10 +608,8 @@ pub trait CliffordSimulator: QuantumSimulator { /// +XI -> IX; /// +ZI -> IZ; #[inline] - fn swap(&mut self, q1: T, q2: T) { - self.cx(q1, q2); - self.cx(q2, q1); - self.cx(q1, q2); + fn swap(&mut self, q1: T, q2: T) -> &mut Self { + self.cx(q1, q2).cx(q2, q1).cx(q1, q2) } /// Applies the G2 two-qubit Clifford operation. @@ -875,15 +632,212 @@ pub trait CliffordSimulator: QuantumSimulator { /// /// # Examples /// ```rust - /// use pecos_qsim::{CliffordSimulator, StdSparseStab}; + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(2); /// sim.g2(0, 1); // Apply G2 operation between qubits 0 and 1 /// ``` #[inline] - fn g2(&mut self, q1: T, q2: T) { - self.cz(q1, q2); - self.h(q1); - self.h(q2); - self.cz(q1, q2); + fn g2(&mut self, q1: T, q2: T) -> &mut Self { + self.cz(q1, q2).h(q1).h(q2).cz(q1, q2) + } + + /// Measurement of the +`X_q` operator. + #[inline] + fn mx(&mut self, q: T) -> bool { + // +X -> +Z + self.h(q); + let meas = self.mz(q); + // +Z -> +X + self.h(q); + + meas + } + + /// Measurement of the -`X_q` operator. + #[inline] + fn mnx(&mut self, q: T) -> bool { + // -X -> +Z + self.h(q); + self.x(q); + let meas = self.mz(q); + // +Z -> -X + self.x(q); + self.h(q); + + meas + } + + /// Measurement of the +`Y_q` operator. + /// # Panics + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn my(&mut self, q: T) -> bool { + // +Y -> +Z + self.sx(q); + let meas = self.mz(q); + // +Z -> +Y + self.sxdg(q); + + meas + } + + /// Measurement of the -`Y_q` operator. + /// # Panics + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn mny(&mut self, q: T) -> bool { + // -Y -> +Z + self.sxdg(q); + let meas = self.mz(q); + // +Z -> -Y + self.sx(q); + + meas + } + + /// Performs a measurement in the Z basis on the specified qubit. + /// + /// This measurement projects the qubit state onto the +1 or -1 eigenstate + /// of the Z operator (corresponding to |0⟩ or |1⟩ respectively). + /// + /// # Arguments + /// * `q` - The qubit to measure + /// + /// For all measurement operations (mx, my, mz, mnx, mny, mnz): + /// + /// # Return Values + /// Returns a tuple `(outcome, deterministic)` where: + /// * `outcome`: + /// - `true` indicates projection onto the +1 eigenstate + /// - `false` indicates projection onto the -1 eigenstate + /// * `deterministic`: + /// - `true` if the state was already in an eigenstate of the measured operator + /// - `false` if the measurement result was random (state was in superposition) + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let outcome = sim.mz(0); + /// ``` + fn mz(&mut self, q: T) -> bool; + + /// Measurement of the -`Z_q` operator. + /// # Panics + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn mnz(&mut self, q: T) -> bool { + // -Z -> +Z + self.x(q); + let meas = self.mz(q); + // +Z -> -Z + self.x(q); + + meas + } + + /// Prepares a qubit in the +1 eigenstate of the X operator. + /// + /// Equivalent to preparing |+⟩ = (|0⟩ + |1⟩)/√2 + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// + /// # Panics + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn px(&mut self, q: T) -> &mut Self { + if self.mx(q) { + self.z(q); + } + self + } + + /// Prepares a qubit in the -1 eigenstate of the X operator. + /// + /// Equivalent to preparing |-⟩ = (|0⟩ - |1⟩)/√2 + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// + /// # Panics + /// + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn pnx(&mut self, q: T) -> &mut Self { + if self.mnx(q) { + self.z(q); + } + self + } + + /// Preparation of the +`Y_q` operator. + /// # Panics + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn py(&mut self, q: T) -> &mut Self { + if self.my(q) { + self.z(q); + } + self + } + + /// Preparation of the -`Y_q` operator. + /// # Panics + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn pny(&mut self, q: T) -> &mut Self { + if self.mny(q) { + self.z(q); + } + self + } + + /// Prepares a qubit in the +1 eigenstate of the Z operator. + /// + /// Equivalent to preparing |0⟩ + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// + /// # Panics + /// + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn pz(&mut self, q: T) -> &mut Self { + if self.mz(q) { + self.x(q); + } + self + } + + /// Prepares a qubit in the -1 eigenstate of the Z operator. + /// + /// Equivalent to preparing |1⟩ + /// + /// # Arguments + /// * `q` - Target qubit + /// + /// # Returns + /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// + /// # Panics + /// + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn pnz(&mut self, q: T) -> &mut Self { + if self.mnz(q) { + self.x(q); + } + self } } diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index bd3b1109..89447ca9 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -10,23 +10,22 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -pub mod clifford_simulator; +pub mod clifford_gateable; pub mod gens; // pub mod measurement; // pub mod nonclifford_simulator; pub mod pauli_prop; // pub mod paulis; pub mod prelude; -pub mod quantum_simulator; +pub mod quantum_simulator_state; pub mod sparse_stab; pub mod state_vec; - -pub use clifford_simulator::CliffordSimulator; +pub use clifford_gateable::CliffordGateable; pub use gens::Gens; // pub use measurement::{MeasBitValue, MeasValue, Measurement}; // TODO: Distinguish between trait and struct/enum // pub use nonclifford_simulator::NonCliffordSimulator; // pub use pauli_prop::{PauliProp, StdPauliProp}; // pub use paulis::Paulis; -pub use quantum_simulator::QuantumSimulator; +pub use quantum_simulator_state::QuantumSimulatorState; pub use sparse_stab::{SparseStab, StdSparseStab}; pub use state_vec::StateVec; diff --git a/crates/pecos-qsim/src/pauli_prop.rs b/crates/pecos-qsim/src/pauli_prop.rs index e3740a1c..17f517db 100644 --- a/crates/pecos-qsim/src/pauli_prop.rs +++ b/crates/pecos-qsim/src/pauli_prop.rs @@ -11,8 +11,8 @@ // the License. #![allow(unused_variables)] -use super::clifford_simulator::CliffordSimulator; -use crate::quantum_simulator::QuantumSimulator; +use super::clifford_gateable::CliffordGateable; +use crate::quantum_simulator_state::QuantumSimulatorState; use core::marker::PhantomData; use pecos_core::{IndexableElement, Set, VecSet}; @@ -33,7 +33,7 @@ where _marker: PhantomData, } -impl QuantumSimulator for PauliProp +impl QuantumSimulatorState for PauliProp where E: IndexableElement, T: for<'a> Set<'a, Element = E>, @@ -45,7 +45,9 @@ where #[inline] fn reset(&mut self) -> &mut Self { - todo!() + self.xs.clear(); + self.zs.clear(); + self } } @@ -69,123 +71,26 @@ where self.insert_x(item); self.insert_z(item); } - - #[inline] - #[expect(clippy::similar_names)] - pub fn h_v2(&mut self, q: E) { - let in_xs = self.xs.contains(&q); - let in_zs = self.zs.contains(&q); - - if in_xs && !in_zs { - self.xs.remove(&q); - self.zs.insert(q); - } else if !in_xs && in_zs { - self.zs.remove(&q); - self.xs.insert(q); - } else if in_xs && in_zs { - return; - } - } } -impl CliffordSimulator for PauliProp +impl CliffordGateable for PauliProp where T: for<'a> Set<'a, Element = E>, E: IndexableElement, { - #[inline] - fn px(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn pnx(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn py(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn pny(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn pz(&mut self, q: E) -> (bool, bool) { - self.xs.remove(&q); - self.zs.remove(&q); - (true, true) // TODO: fix... - } - - #[inline] - fn pnz(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn mx(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn mnx(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn my(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn mny(&mut self, q: E) -> (bool, bool) { - todo!() - } - - /// Output true if there is an X on the qubit. - #[inline] - fn mz(&mut self, q: E) -> (bool, bool) { - (self.xs.contains(&q), true) // TODO: Is deterministic a useful value... maybe, maybe not... fix - } - - #[inline] - fn mnz(&mut self, q: E) -> (bool, bool) { - todo!() - } - - #[inline] - fn identity(&mut self, _q: E) {} - - #[inline] - fn x(&mut self, q: E) { - todo!() - } - - #[inline] - fn y(&mut self, q: E) { - todo!() - } - - #[inline] - fn z(&mut self, q: E) { - todo!() - } - /// X -> Y, Z -> Z, Y -> -X #[inline] - fn sz(&mut self, q: E) { + fn sz(&mut self, q: E) -> &mut Self { if self.xs.contains(&q) { self.zs.symmetric_difference_item_update(&q); } + self } /// X -> Z, Z -> X, Y -> -Y #[inline] #[expect(clippy::similar_names)] - fn h(&mut self, q: E) { + fn h(&mut self, q: E) -> &mut Self { let in_xs = self.xs.contains(&q); let in_zs = self.zs.contains(&q); @@ -197,16 +102,24 @@ where self.zs.remove(&q); self.xs.insert(q); } + self } /// XI -> XX, ZI -> ZI, IX -> IX, IZ -> ZZ #[inline] - fn cx(&mut self, q1: E, q2: E) { + fn cx(&mut self, q1: E, q2: E) -> &mut Self { if self.xs.contains(&q1) { self.xs.symmetric_difference_item_update(&q2); } if self.zs.contains(&q2) { self.zs.symmetric_difference_item_update(&q1); } + self + } + + /// Output true if there is an X on the qubit. + #[inline] + fn mz(&mut self, q: E) -> bool { + self.xs.contains(&q) } } diff --git a/crates/pecos-qsim/src/prelude.rs b/crates/pecos-qsim/src/prelude.rs index ef9a8dfe..19604118 100644 --- a/crates/pecos-qsim/src/prelude.rs +++ b/crates/pecos-qsim/src/prelude.rs @@ -1,6 +1,6 @@ -pub use crate::clifford_simulator::CliffordSimulator; +pub use crate::clifford_gateable::CliffordGateable; pub use crate::pauli_prop::{PauliProp, StdPauliProp}; -pub use crate::quantum_simulator::QuantumSimulator; +pub use crate::quantum_simulator_state::QuantumSimulatorState; pub use crate::sparse_stab::SparseStab; pub use crate::state_vec::StateVec; pub use pecos_core::VecSet; diff --git a/crates/pecos-qsim/src/quantum_simulator.rs b/crates/pecos-qsim/src/quantum_simulator.rs deleted file mode 100644 index 4497532f..00000000 --- a/crates/pecos-qsim/src/quantum_simulator.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 The PECOS Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License.You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software distributed under the License -// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express -// or implied. See the License for the specific language governing permissions and limitations under -// the License. - -pub trait QuantumSimulator { - fn num_qubits(&self) -> usize; - - fn reset(&mut self) -> &mut Self; -} diff --git a/crates/pecos-qsim/src/quantum_simulator_state.rs b/crates/pecos-qsim/src/quantum_simulator_state.rs new file mode 100644 index 00000000..712450f8 --- /dev/null +++ b/crates/pecos-qsim/src/quantum_simulator_state.rs @@ -0,0 +1,40 @@ +// Copyright 2024 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +// Base trait for quantum simulator state management +pub trait QuantumSimulatorState { + /// Returns the number of qubits in the system + /// + /// # Returns + /// * `usize` - The total number of qubits this simulator is configured to handle + fn num_qubits(&self) -> usize; + + /// Resets all qubits in the system to the |0⟩ state + /// + /// This is the standard computational basis state where all qubits are in their + /// ground state. Any entanglement or quantum correlations between qubits are removed. + /// + /// # Returns + /// * `&mut Self` - Returns self for method chaining (builder pattern) + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, StdSparseStab}; + /// + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) + /// .cx(0, 1) + /// .reset() // Return to |00⟩ state + /// .h(1); // Can continue chaining methods + /// ``` + fn reset(&mut self) -> &mut Self; +} diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index f5934b89..9749c6f5 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -10,7 +10,7 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -use crate::{CliffordSimulator, Gens, QuantumSimulator}; +use crate::{CliffordGateable, Gens, QuantumSimulatorState}; use core::fmt::Debug; use core::mem; use pecos_core::{IndexableElement, Set}; @@ -45,7 +45,7 @@ pub type StdSparseStab = SparseStab, usize>; /// # Examples /// ```rust /// use pecos_core::VecSet; -/// use pecos_qsim::{QuantumSimulator, CliffordSimulator, SparseStab}; +/// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; /// /// // Create a new 2-qubit stabilizer state /// let mut sim = SparseStab::, u32>::new(2); @@ -57,7 +57,7 @@ pub type StdSparseStab = SparseStab, usize>; /// sim.cx(0, 1); /// /// // Measure first qubit in Z basis -/// let (outcome, determined) = sim.mz(0); +/// let outcome = sim.mz(0); /// ``` /// /// # Measurement Behavior @@ -135,9 +135,8 @@ where stab } - #[expect(clippy::single_call_fn)] #[inline] - fn reset(&mut self) -> &mut Self { + pub fn reset(&mut self) -> &mut Self { self.stabs.init_all_z(); self.destabs.init_all_x(); self @@ -254,7 +253,7 @@ where /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn nondeterministic_meas(&mut self, q: E) -> E { + fn nondeterministic_meas(&mut self, q: E, result: bool) -> bool { let qu = q.to_usize(); let mut anticom_stabs_col = self.stabs.col_x[qu].clone(); @@ -388,39 +387,177 @@ where self.destabs.row_x[id_usize] = removed_row_x; self.destabs.row_z[id_usize] = removed_row_z; - id + self.apply_outcome(id, result) } - /// Measurement of the +`Z_q` operator where random outcomes are forced to a particular value. + /// Measures a qubit in the Z basis and get if deterministic result. + /// + /// Returns a tuple containing: + /// - The measurement outcome (true = |1>, false = |0>) + /// - Whether the measurement was deterministic + /// + /// The measurement can be: + /// - Deterministic: The outcome is fixed by the current stabilizer state + /// - Non-deterministic: The outcome is random with 50% probability for each result + /// + /// # Arguments + /// * q - The qubit index to measure + /// + /// # Returns + /// * (bool, bool) - (`measurement_outcome`, `is_deterministic`) + /// + /// # Example + /// ```rust + /// use pecos_core::VecSet; + /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; + /// let mut state = SparseStab::, u32>::new(2); + /// + /// let (outcome, deterministic) = state.mz_determine(0); + /// if deterministic { + /// println!("Measurement was deterministic with outcome: {}", outcome); + /// } + /// ``` + /// /// # Panics + /// /// Will panic if qubit ids don't convert to usize. #[inline] - pub fn mz_forced(&mut self, q: E, forced_outcome: bool) -> (bool, bool) { + pub fn mz_determine(&mut self, q: E) -> (bool, bool) { let qu = q.to_usize(); let deterministic = self.stabs.col_x[qu].is_empty(); - // There are no stabilizers that anti-commute with Z_q let meas_out = if deterministic { + // There are no stabilizers that anti-commute with Z_q self.deterministic_meas(q) } else { - let id = self.nondeterministic_meas(q); - - self.apply_outcome(id, forced_outcome) + let result = self.rng.gen_bool(0.5); + self.nondeterministic_meas(q, result) }; (meas_out, deterministic) } + /// Measurement of the +`Z_q` operator where random outcomes are forced to a particular value. + /// # Panics + /// Will panic if qubit ids don't convert to usize. + #[inline] + pub fn mz_forced(&mut self, q: E, forced_outcome: bool) -> bool { + let qu = q.to_usize(); + + if self.stabs.col_x[qu].is_empty() { + // There are no stabilizers that anti-commute with Z_q + self.deterministic_meas(q) + } else { + self.nondeterministic_meas(q, forced_outcome) + } + } + + #[inline] + pub fn mx_determine(&mut self, q: E) -> (bool, bool) { + self.h(q); + let result = self.mz_determine(q); + self.h(q); + result + } + + #[inline] + pub fn mnx_determine(&mut self, q: E) -> (bool, bool) { + self.h(q); + let result = self.mnz_determine(q); + self.h(q); + result + } + + #[inline] + pub fn my_determine(&mut self, q: E) -> (bool, bool) { + self.sx(q); + let result = self.mz_determine(q); + self.sxdg(q); + result + } + + #[inline] + pub fn mny_determine(&mut self, q: E) -> (bool, bool) { + self.sxdg(q); + let result = self.mz_determine(q); + self.sx(q); + result + } + + #[inline] + pub fn mnz_determine(&mut self, q: E) -> (bool, bool) { + self.x(q); + let result = self.mz_determine(q); + self.x(q); + + result + } + + #[inline] + pub fn px_determine(&mut self, q: E) -> (bool, bool) { + let result = self.mx_determine(q); + if result.0 { + self.z(q); + } + result + } + + #[inline] + pub fn pnx_determine(&mut self, q: E) -> (bool, bool) { + let result = self.mnx_determine(q); + if result.0 { + self.z(q); + } + result + } + + #[inline] + pub fn py_determine(&mut self, q: E) -> (bool, bool) { + let result = self.my_determine(q); + if result.0 { + self.z(q); + } + result + } + + #[inline] + pub fn pny_determine(&mut self, q: E) -> (bool, bool) { + let result = self.mny_determine(q); + if result.0 { + self.z(q); + } + result + } + + #[inline] + pub fn pz_determine(&mut self, q: E) -> (bool, bool) { + let result = self.mz_determine(q); + if result.0 { + self.x(q); + } + result + } + + #[inline] + pub fn pnz_determine(&mut self, q: E) -> (bool, bool) { + let result = self.mnz_determine(q); + if result.0 { + self.x(q); + } + result + } + /// Preparation of the +`Z_q` operator where random outcomes are forced to a particular value. + /// /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - pub fn pz_forced(&mut self, q: E, forced_outcome: bool) -> (bool, bool) { - let (meas, deter) = self.mz_forced(q, forced_outcome); - if meas { + pub fn pz_forced(&mut self, q: E, forced_outcome: bool) -> &mut Self { + let result = self.mz_forced(q, forced_outcome); + if result { self.x(q); } - (meas, deter) + self } /// Apply measurement outcome @@ -435,7 +572,7 @@ where } } -impl QuantumSimulator for SparseStab +impl QuantumSimulatorState for SparseStab where E: IndexableElement, R: SimRng, @@ -452,7 +589,7 @@ where } } -impl CliffordSimulator for SparseStab +impl CliffordGateable for SparseStab where T: for<'a> Set<'a, Element = E>, E: IndexableElement, @@ -461,85 +598,38 @@ where // TODO: pub fun p(&mut self, pauli: &pauli, q: U) { todo!() } // TODO: pub fun m(&mut self, pauli: &pauli, q: U) -> bool { todo!() } - /// Measures a qubit in the Z basis. - /// - /// Returns a tuple containing: - /// - The measurement outcome (true = |1>, false = |0>) - /// - Whether the measurement was deterministic - /// - /// The measurement can be: - /// - Deterministic: The outcome is fixed by the current stabilizer state - /// - Non-deterministic: The outcome is random with 50% probability for each result - /// - /// # Arguments - /// * q - The qubit index to measure - /// - /// # Returns - /// * (bool, bool) - (`measurement_outcome`, `is_deterministic`) - /// - /// # Example - /// ```rust - /// use pecos_core::VecSet; - /// use pecos_qsim::{QuantumSimulator, CliffordSimulator, SparseStab}; - /// let mut state = SparseStab::, u32>::new(2); - /// - /// let (outcome, deterministic) = state.mz(0); - /// if deterministic { - /// println!("Measurement was deterministic with outcome: {}", outcome); - /// } - /// ``` - /// - /// # Panics - /// - /// Will panic if qubit ids don't convert to usize. - #[inline] - fn mz(&mut self, q: E) -> (bool, bool) { - let qu = q.to_usize(); - - let deterministic = self.stabs.col_x[qu].is_empty(); - - // There are no stabilizers that anti-commute with Z_q - let meas_out = if deterministic { - self.deterministic_meas(q) - } else { - let id = self.nondeterministic_meas(q); - - let meas_outcome = self.rng.gen_bool(0.5); - - self.apply_outcome(id, meas_outcome) - }; - (meas_out, deterministic) - } - /// Pauli X gate. X -> X, Z -> -Z /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn x(&mut self, q: E) { + fn x(&mut self, q: E) -> &mut Self { let qu = q.to_usize(); self.stabs.signs_minus ^= &self.stabs.col_z[qu]; + self } /// Pauli Y gate. X -> -X, Z -> -Z /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn y(&mut self, q: E) { + fn y(&mut self, q: E) -> &mut Self { // TODO: Add test let qu = q.to_usize(); // stabs.signs_minus ^= stabs.col_x[qubit] ^ stabs.col_z[qubit] for i in self.stabs.col_x[qu].symmetric_difference(&self.stabs.col_z[qu]) { self.stabs.signs_minus ^= i; } + self } /// Pauli Z gate. X -> -X, Z -> Z /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn z(&mut self, q: E) { + fn z(&mut self, q: E) -> &mut Self { // TODO: Add test self.stabs.signs_minus ^= &self.stabs.col_x[q.to_usize()]; + self } /// Sqrt of Z gate. @@ -550,7 +640,7 @@ where /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn sz(&mut self, q: E) { + fn sz(&mut self, q: E) -> &mut Self { let qu = q.to_usize(); // X -> i @@ -572,13 +662,14 @@ where g.row_z[iu] ^= &q; } } + self } /// Hadamard gate. X -> Z, Z -> X /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn h(&mut self, q: E) { + fn h(&mut self, q: E) -> &mut Self { let qu = q.to_usize(); // self.stabs.signs_minus.symmetric_difference_update(self.stabs.col_x[qu].intersection()) @@ -602,6 +693,7 @@ where mem::swap(&mut g.col_x[qu], &mut g.col_z[qu]); } + self } /// Applies a CX or CNOT (Controlled-X) gate between two qubits. @@ -623,7 +715,7 @@ where /// # Example /// ```rust /// use pecos_core::VecSet; - /// use pecos_qsim::{QuantumSimulator, CliffordSimulator, SparseStab}; + /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; /// let mut state = SparseStab::, u32>::new(2); /// /// // Create Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2 @@ -635,7 +727,7 @@ where /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn cx(&mut self, q1: E, q2: E) { + fn cx(&mut self, q1: E, q2: E) -> &mut Self { let qu1 = q1.to_usize(); let qu2 = q2.to_usize(); @@ -688,6 +780,65 @@ where col_z_qu1.symmetric_difference_update(col_z_qu2); } } + self + } + + /// Measures a qubit in the Z basis. + /// + /// Returns a tuple containing: + /// - The measurement outcome (true = |1>, false = |0>) + /// - Whether the measurement was deterministic + /// + /// The measurement can be: + /// - Deterministic: The outcome is fixed by the current stabilizer state + /// - Non-deterministic: The outcome is random with 50% probability for each result + /// + /// # Arguments + /// * q - The qubit index to measure + /// + /// # Returns + /// * (bool, bool) - (`measurement_outcome`, `is_deterministic`) + /// + /// # Example + /// ```rust + /// use pecos_core::VecSet; + /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; + /// let mut state = SparseStab::, u32>::new(2); + /// + /// let outcome = state.mz(0); + /// ``` + /// + /// # Panics + /// + /// Will panic if qubit ids don't convert to usize. + #[inline] + fn mz(&mut self, q: E) -> bool { + self.mz_determine(q).0 + } + + #[inline] + fn mnz(&mut self, q: E) -> bool { + self.mnz_determine(q).0 + } + + #[inline] + fn mx(&mut self, q: E) -> bool { + self.mx_determine(q).0 + } + + #[inline] + fn mnx(&mut self, q: E) -> bool { + self.mnx_determine(q).0 + } + + #[inline] + fn my(&mut self, q: E) -> bool { + self.my_determine(q).0 + } + + #[inline] + fn mny(&mut self, q: E) -> bool { + self.mny_determine(q).0 } } @@ -873,8 +1024,8 @@ mod tests { fn test_nondeterministic_px() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.px(0); - let (m1, d1) = state.mx(0); + let (_m0, d0) = state.px_determine(0); + let (m1, d1) = state.mx_determine(0); let m1_int = u8::from(m1); assert_eq!(m1_int, 0); // |+X> @@ -886,7 +1037,7 @@ mod tests { #[test] fn test_deterministic_px() { let mut state = prep_state(&["X"], &["Z"]); - let (m0, d0) = state.px(0); + let (m0, d0) = state.px_determine(0); let m0_int = u8::from(m0); assert!(d0); // Deterministic @@ -897,8 +1048,8 @@ mod tests { fn test_nondeterministic_pnx() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.pnx(0); - let (m1, d1) = state.mx(0); + let (_m0, d0) = state.pnx_determine(0); + let (m1, d1) = state.mx_determine(0); let m1_int = u8::from(m1); assert_eq!(m1_int, 1); // |-X> @@ -910,7 +1061,7 @@ mod tests { #[test] fn test_deterministic_pnx() { let mut state = prep_state(&["-X"], &["Z"]); - let (m0, d0) = state.pnx(0); + let (m0, d0) = state.pnx_determine(0); let m0_int = u8::from(m0); assert!(d0); // Deterministic @@ -921,8 +1072,8 @@ mod tests { fn test_nondeterministic_py() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.py(0); - let (m1, d1) = state.my(0); + let (_m0, d0) = state.py_determine(0); + let (m1, d1) = state.my_determine(0); let m1_int = u8::from(m1); assert_eq!(m1_int, 0); // |+Y> @@ -934,7 +1085,7 @@ mod tests { #[test] fn test_deterministic_py() { let mut state = prep_state(&["iW"], &["Z"]); - let (m0, d0) = state.py(0); + let (m0, d0) = state.py_determine(0); let m0_int = u8::from(m0); assert!(d0); // Deterministic @@ -945,8 +1096,8 @@ mod tests { fn test_nondeterministic_pny() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.pny(0); - let (m1, d1) = state.my(0); + let (_m0, d0) = state.pny_determine(0); + let (m1, d1) = state.my_determine(0); let m1_int = u8::from(m1); assert_eq!(m1_int, 1); // |-Y> @@ -958,7 +1109,7 @@ mod tests { #[test] fn test_deterministic_pny() { let mut state = prep_state(&["-iW"], &["Z"]); - let (m0, d0) = state.pny(0); + let (m0, d0) = state.pny_determine(0); let m0_int = u8::from(m0); assert!(d0); // Deterministic @@ -969,8 +1120,8 @@ mod tests { fn test_nondeterministic_pz() { for _ in 1_u32..=100 { let mut state = prep_state(&["X"], &["Z"]); - let (_m0, d0) = state.pz(0); - let (m1, d1) = state.mz(0); + let (_m0, d0) = state.pz_determine(0); + let (m1, d1) = state.mz_determine(0); let m1_int = u8::from(m1); assert_eq!(m1_int, 0); // |0> @@ -982,7 +1133,7 @@ mod tests { #[test] fn test_deterministic_pz() { let mut state = prep_state(&["Z"], &["X"]); - let (m0, d0) = state.pz(0); + let (m0, d0) = state.pz_determine(0); let m0_int = u8::from(m0); assert!(d0); // Deterministic @@ -993,8 +1144,8 @@ mod tests { fn test_nondeterministic_pnz() { for _ in 1_u32..=100 { let mut state = prep_state(&["X"], &["Z"]); - let (_m0, d0) = state.pnz(0); - let (m1, d1) = state.mz(0); + let (_m0, d0) = state.pnz_determine(0); + let (m1, d1) = state.mz_determine(0); let m1_int = u8::from(m1); assert_eq!(m1_int, 1); // |1> @@ -1006,7 +1157,7 @@ mod tests { #[test] fn test_deterministic_pnz() { let mut state = prep_state(&["-Z"], &["X"]); - let (m0, d0) = state.pnz(0); + let (m0, d0) = state.pnz_determine(0); let m0_int = u8::from(m0); assert!(d0); // Deterministic @@ -1016,19 +1167,19 @@ mod tests { #[test] fn test_nondeterministic_mx() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.mx(0); + let (_meas, determined) = state.mx_determine(0); assert!(!determined); } #[test] fn test_deterministic_mx() { let mut state0 = prep_state(&["X"], &["Z"]); - let (meas0, determined0) = state0.mx(0); + let (meas0, determined0) = state0.mx_determine(0); assert!(determined0); assert!(!meas0); let mut state1 = prep_state(&["-X"], &["Z"]); - let (meas1, determined1) = state1.mx(0); + let (meas1, determined1) = state1.mx_determine(0); assert!(determined1); assert!(meas1); } @@ -1036,19 +1187,19 @@ mod tests { #[test] fn test_nondeterministic_mnx() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.mnx(0); + let (_meas, determined) = state.mnx_determine(0); assert!(!determined); } #[test] fn test_deterministic_mnx() { let mut state0 = prep_state(&["-X"], &["Z"]); - let (meas0, determined0) = state0.mnx(0); + let (meas0, determined0) = state0.mnx_determine(0); assert!(determined0); assert!(!meas0); let mut state1 = prep_state(&["X"], &["Z"]); - let (meas1, determined1) = state1.mnx(0); + let (meas1, determined1) = state1.mnx_determine(0); assert!(determined1); assert!(meas1); } @@ -1056,19 +1207,19 @@ mod tests { #[test] fn test_nondeterministic_my() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.my(0); + let (_meas, determined) = state.my_determine(0); assert!(!determined); } #[test] fn test_deterministic_my() { let mut state0 = prep_state(&["iW"], &["Z"]); - let (meas0, determined0) = state0.my(0); + let (meas0, determined0) = state0.my_determine(0); assert!(determined0); assert!(!meas0); let mut state1 = prep_state(&["-iW"], &["Z"]); - let (meas1, determined1) = state1.my(0); + let (meas1, determined1) = state1.my_determine(0); assert!(determined1); assert!(meas1); } @@ -1076,19 +1227,19 @@ mod tests { #[test] fn test_nondeterministic_mny() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.mny(0); + let (_meas, determined) = state.mny_determine(0); assert!(!determined); } #[test] fn test_deterministic_mny() { let mut state0 = prep_state(&["-iW"], &["Z"]); - let (meas0, determined0) = state0.mny(0); + let (meas0, determined0) = state0.mny_determine(0); assert!(determined0); assert!(!meas0); let mut state1 = prep_state(&["iW"], &["Z"]); - let (meas1, determined1) = state1.mny(0); + let (meas1, determined1) = state1.mny_determine(0); assert!(determined1); assert!(meas1); } @@ -1096,19 +1247,19 @@ mod tests { #[test] fn test_nondeterministic_mz() { let mut state = prep_state(&["X"], &["Z"]); - let (_meas, determined) = state.mz(0); + let (_meas, determined) = state.mz_determine(0); assert!(!determined); } #[test] fn test_deterministic_mz() { let mut state0 = prep_state(&["Z"], &["X"]); - let (meas0, determined0) = state0.mz(0); + let (meas0, determined0) = state0.mz_determine(0); assert!(determined0); assert!(!meas0); let mut state1 = prep_state(&["-Z"], &["X"]); - let (meas1, determined1) = state1.mz(0); + let (meas1, determined1) = state1.mz_determine(0); assert!(determined1); assert!(meas1); } @@ -1116,19 +1267,19 @@ mod tests { #[test] fn test_nondeterministic_mnz() { let mut state = prep_state(&["X"], &["Z"]); - let (_meas, determined) = state.mnz(0); + let (_meas, determined) = state.mnz_determine(0); assert!(!determined); } #[test] fn test_deterministic_mnz() { let mut state0 = prep_state(&["Z"], &["X"]); - let (meas0, determined0) = state0.mnz(0); + let (meas0, determined0) = state0.mnz_determine(0); assert!(determined0); assert!(meas0); let mut state1 = prep_state(&["-Z"], &["X"]); - let (meas1, determined1) = state1.mnz(0); + let (meas1, determined1) = state1.mnz_determine(0); assert!(determined1); assert!(!meas1); } @@ -2091,7 +2242,7 @@ mod tests { ) -> (SparseStab, u32>, bool) { state.cx(1, 0); state.h(1); - let (m1, d1) = state.mz(1); + let (m1, d1) = state.mz_determine(1); if m1 { state.z(0); } @@ -2110,7 +2261,7 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // X basis meas state.h(0); - let (m0, d0) = state.mz(0); + let (m0, d0) = state.mz_determine(0); let m0_int = u8::from(m0); assert_eq!(m0_int, 0); // |+> -> 0 == false assert!(!d1); // Not deterministic @@ -2131,7 +2282,7 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // X basis meas state.h(0); - let (m0, d0) = state.mz(0); + let (m0, d0) = state.mz_determine(0); let m0_int = u8::from(m0); assert_eq!(m0_int, 1); // |-> -> 1 == true assert!(!d1); // Not deterministic @@ -2151,7 +2302,7 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // Y basis meas state.sx(0); // Y -> Z - let (m0, d0) = state.mz(0); + let (m0, d0) = state.mz_determine(0); let m0_int = u8::from(m0); assert_eq!(m0_int, 0); // |+X> -> 0 == false assert!(!d1); // Not deterministic @@ -2172,7 +2323,7 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // Y basis meas state.sx(0); // Y -> Z - let (m0, d0) = state.mz(0); + let (m0, d0) = state.mz_determine(0); let m0_int = u8::from(m0); assert_eq!(m0_int, 1); // |-Y> -> 1 == true assert!(!d1); // Not deterministic @@ -2190,7 +2341,7 @@ mod tests { let mut state: SparseStab, u32> = SparseStab::new(2); // Set input to |0> (state, d1) = one_bit_z_teleport(state); - let (m0, d0) = state.mz(0); + let (m0, d0) = state.mz_determine(0); let m0_int = u8::from(m0); assert_eq!(m0_int, 0); // |0> assert!(!d1); // Not deterministic @@ -2208,7 +2359,7 @@ mod tests { let mut state: SparseStab, u32> = SparseStab::new(2); state.x(1); // Set input to |1> (state, d1) = one_bit_z_teleport(state); - let (m0, d0) = state.mz(0); + let (m0, d0) = state.mz_determine(0); let m0_int = u8::from(m0); assert_eq!(m0_int, 1); // |1> -> 1 == true assert!(!d1); // Not deterministic @@ -2229,8 +2380,8 @@ mod tests { state.cx(1, 2); state.cx(0, 1); state.h(0); - let (m0, d0) = state.mz(0); - let (m1, d1) = state.mz(1); + let (m0, d0) = state.mz_determine(0); + let (m1, d1) = state.mz_determine(1); if m1 { state.x(2); } @@ -2249,7 +2400,7 @@ mod tests { state.h(0); (state, d0, d1) = teleport(state); state.h(2); - let (m2, d2) = state.mz(2); + let (m2, d2) = state.mz_determine(2); let m2_int = u8::from(m2); assert_eq!(m2_int, 0); assert!(!d0); @@ -2268,7 +2419,7 @@ mod tests { state.h(0); (state, d0, d1) = teleport(state); state.h(2); - let (m2, d2) = state.mz(2); + let (m2, d2) = state.mz_determine(2); let m2_int = u8::from(m2); assert_eq!(m2_int, 1); @@ -2287,7 +2438,7 @@ mod tests { state.sxdg(0); (state, d0, d1) = teleport(state); state.sx(2); - let (m2, d2) = state.mz(2); + let (m2, d2) = state.mz_determine(2); let m2_int = u8::from(m2); assert_eq!(m2_int, 0); assert!(!d0); @@ -2306,7 +2457,7 @@ mod tests { state.sxdg(0); (state, d0, d1) = teleport(state); state.sx(2); - let (m2, d2) = state.mz(2); + let (m2, d2) = state.mz_determine(2); let m2_int = u8::from(m2); assert_eq!(m2_int, 1); assert!(!d0); @@ -2322,7 +2473,7 @@ mod tests { let d1; let mut state: SparseStab, u32> = SparseStab::new(3); (state, d0, d1) = teleport(state); - let (m2, d2) = state.mz(2); + let (m2, d2) = state.mz_determine(2); let m2_int = u8::from(m2); assert_eq!(m2_int, 0); @@ -2340,7 +2491,7 @@ mod tests { let mut state: SparseStab, u32> = SparseStab::new(3); state.x(0); // input state |-Z> (state, d0, d1) = teleport(state); - let (m2, d2) = state.mz(2); + let (m2, d2) = state.mz_determine(2); let m2_int = u8::from(m2); assert_eq!(m2_int, 1); diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index aacd8ff9..f4a87668 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -1073,7 +1073,7 @@ mod tests { #[test] fn test_measure() { - // Test 1: Meauring |0> state + // Test 1: Measuring |0> state let mut q = StateVec::new(1); let result = q.measure(0); assert_eq!(result, 0); diff --git a/crates/pecos/src/prelude.rs b/crates/pecos/src/prelude.rs index 744c435c..7b3c5847 100644 --- a/crates/pecos/src/prelude.rs +++ b/crates/pecos/src/prelude.rs @@ -14,7 +14,7 @@ pub use pecos_core::VecSet; // re-exporting pecos-qsim -pub use pecos_qsim::CliffordSimulator; +pub use pecos_qsim::CliffordGateable; pub use pecos_qsim::SparseStab; // TODO: add the following in the future as makes sense... // pub use pecos_qsim::clifford_simulator::CliffordSimulator; From bda321b9b6ef444ae6ac0deb04fb8eb6c796dbe1 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Fri, 20 Dec 2024 19:04:43 -0700 Subject: [PATCH 09/33] Refactor state-vec --- .../src/arbitrary_rotation_gateable.rs | 24 + crates/pecos-qsim/src/lib.rs | 2 + crates/pecos-qsim/src/prelude.rs | 3 +- crates/pecos-qsim/src/sparse_stab.rs | 42 +- crates/pecos-qsim/src/state_vec.rs | 762 ++++++++++-------- 5 files changed, 453 insertions(+), 380 deletions(-) create mode 100644 crates/pecos-qsim/src/arbitrary_rotation_gateable.rs diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs new file mode 100644 index 00000000..0977ba9d --- /dev/null +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -0,0 +1,24 @@ +// Copyright 2024 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +use crate::CliffordGateable; +use pecos_core::IndexableElement; + +pub trait ArbitraryRotationGateable: CliffordGateable { + fn rx(&mut self, theta: f64, q: T) -> &mut Self; + fn ry(&mut self, theta: f64, q: T) -> &mut Self; + fn rz(&mut self, theta: f64, q: T) -> &mut Self; + + fn rxx(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; + fn ryy(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; + fn rzz(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; +} diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index 89447ca9..902586de 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -16,10 +16,12 @@ pub mod gens; // pub mod nonclifford_simulator; pub mod pauli_prop; // pub mod paulis; +pub mod arbitrary_rotation_gateable; pub mod prelude; pub mod quantum_simulator_state; pub mod sparse_stab; pub mod state_vec; + pub use clifford_gateable::CliffordGateable; pub use gens::Gens; // pub use measurement::{MeasBitValue, MeasValue, Measurement}; // TODO: Distinguish between trait and struct/enum diff --git a/crates/pecos-qsim/src/prelude.rs b/crates/pecos-qsim/src/prelude.rs index 19604118..6d576ebb 100644 --- a/crates/pecos-qsim/src/prelude.rs +++ b/crates/pecos-qsim/src/prelude.rs @@ -1,6 +1,7 @@ +pub use crate::arbitrary_rotation_gateable::ArbitraryRotationGateable; pub use crate::clifford_gateable::CliffordGateable; pub use crate::pauli_prop::{PauliProp, StdPauliProp}; pub use crate::quantum_simulator_state::QuantumSimulatorState; pub use crate::sparse_stab::SparseStab; pub use crate::state_vec::StateVec; -pub use pecos_core::VecSet; +pub use pecos_core::{IndexableElement, VecSet}; diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index 9749c6f5..94e37f86 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -105,7 +105,7 @@ where E: IndexableElement, R: SimRng, { - num_qubits: usize, + pub(crate) num_qubits: usize, stabs: Gens, destabs: Gens, rng: R, @@ -783,6 +783,26 @@ where self } + #[inline] + fn mx(&mut self, q: E) -> bool { + self.mx_determine(q).0 + } + + #[inline] + fn mnx(&mut self, q: E) -> bool { + self.mnx_determine(q).0 + } + + #[inline] + fn my(&mut self, q: E) -> bool { + self.my_determine(q).0 + } + + #[inline] + fn mny(&mut self, q: E) -> bool { + self.mny_determine(q).0 + } + /// Measures a qubit in the Z basis. /// /// Returns a tuple containing: @@ -820,26 +840,6 @@ where fn mnz(&mut self, q: E) -> bool { self.mnz_determine(q).0 } - - #[inline] - fn mx(&mut self, q: E) -> bool { - self.mx_determine(q).0 - } - - #[inline] - fn mnx(&mut self, q: E) -> bool { - self.mnx_determine(q).0 - } - - #[inline] - fn my(&mut self, q: E) -> bool { - self.my_determine(q).0 - } - - #[inline] - fn mny(&mut self, q: E) -> bool { - self.mny_determine(q).0 - } } #[cfg(test)] diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index f4a87668..ba78d13a 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -1,3 +1,19 @@ +// Copyright 2024 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +use super::arbitrary_rotation_gateable::ArbitraryRotationGateable; +use super::clifford_gateable::CliffordGateable; +use super::quantum_simulator_state::QuantumSimulatorState; + use num_complex::Complex64; use rand::Rng; @@ -10,6 +26,7 @@ pub struct StateVec { impl StateVec { /// Create a new state initialized to |0...0⟩ #[must_use] + #[inline] pub fn new(num_qubits: usize) -> Self { let size = 1 << num_qubits; // 2^n let mut state = vec![Complex64::new(0.0, 0.0); size]; @@ -23,6 +40,7 @@ impl StateVec { /// /// Panics if the input state requires more qubits then `StateVec` has. #[must_use] + #[inline] pub fn from_state(state: Vec) -> Self { let num_qubits = state.len().trailing_zeros() as usize; assert_eq!(1 << num_qubits, state.len(), "Invalid state vector size"); @@ -34,26 +52,25 @@ impl StateVec { /// # Panics /// /// Panics if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) - pub fn prepare_computational_basis(&mut self, basis_state: usize) { + #[inline] + pub fn prepare_computational_basis(&mut self, basis_state: usize) -> &mut Self { assert!(basis_state < 1 << self.num_qubits); self.state.fill(Complex64::new(0.0, 0.0)); self.state[basis_state] = Complex64::new(1.0, 0.0); + self } /// Prepare all qubits in |+⟩ state - pub fn prepare_plus_state(&mut self) { + #[inline] + pub fn prepare_plus_state(&mut self) -> &mut Self { let factor = Complex64::new(1.0 / f64::from(1 << self.num_qubits), 0.0); self.state.fill(factor); - } - - /// Returns the number of qubits in the system - #[must_use] - pub fn num_qubits(&self) -> usize { - self.num_qubits + self } /// Returns reference to the state vector #[must_use] + #[inline] pub fn state(&self) -> &[Complex64] { &self.state } @@ -64,41 +81,165 @@ impl StateVec { /// /// Panics if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) #[must_use] + #[inline] pub fn probability(&self, basis_state: usize) -> f64 { assert!(basis_state < 1 << self.num_qubits); self.state[basis_state].norm_sqr() } - /// Apply Hadamard gate to the target qubit + /// Apply a general single-qubit unitary gate + /// + /// # Examples + /// ``` + /// use pecos_qsim::state_vec::StateVec; + /// use std::f64::consts::FRAC_1_SQRT_2; + /// use num_complex::Complex64; + /// let mut q = StateVec::new(1); + /// // Apply Hadamard gate + /// q.single_qubit_rotation(0, + /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u00 + /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u01 + /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u10 + /// Complex64::new(-FRAC_1_SQRT_2, 0.0), // u11 + /// ); + /// ``` /// /// # Panics /// /// Panics if target qubit index is >= number of qubits - pub fn hadamard(&mut self, target: usize) { + #[inline] + pub fn single_qubit_rotation( + &mut self, + target: usize, + u00: Complex64, + u01: Complex64, + u10: Complex64, + u11: Complex64, + ) -> &mut Self { assert!(target < self.num_qubits); - let factor = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0); - let step = 1 << target; + let step = 1 << target; for i in (0..self.state.len()).step_by(2 * step) { for offset in 0..step { let j = i + offset; - let paired_j = j ^ step; + let k = j ^ step; let a = self.state[j]; - let b = self.state[paired_j]; + let b = self.state[k]; - self.state[j] = factor * (a + b); - self.state[paired_j] = factor * (a - b); + self.state[j] = u00 * a + u01 * b; + self.state[k] = u10 * a + u11 * b; + } + } + self + } + + /// Apply U3(theta, phi, lambda) gate + /// U3 = [[cos(θ/2), -e^(iλ)sin(θ/2)], + /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits + #[inline] + pub fn u3(&mut self, target: usize, theta: f64, phi: f64, lambda: f64) -> &mut Self { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + + // Calculate matrix elements + let u00 = Complex64::new(cos, 0.0); + let u01 = -Complex64::from_polar(sin, lambda); + let u10 = Complex64::from_polar(sin, phi); + let u11 = Complex64::from_polar(cos, phi + lambda); + + self.single_qubit_rotation(target, u00, u01, u10, u11) + } + + /// Apply a general two-qubit unitary given by a 4x4 complex matrix + /// U = [[u00, u01, u02, u03], + /// [u10, u11, u12, u13], + /// [u20, u21, u22, u23], + /// [u30, u31, u32, u33]] + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 + #[inline] + pub fn two_qubit_unitary(&mut self, qubit1: usize, qubit2: usize, matrix: [[Complex64; 4]; 4]) { + assert!(qubit1 < self.num_qubits); + assert!(qubit2 < self.num_qubits); + assert_ne!(qubit1, qubit2); + + // Make sure qubit1 < qubit2 for consistent ordering + let (q1, q2) = if qubit1 < qubit2 { + (qubit1, qubit2) + } else { + (qubit2, qubit1) + }; + + // Process state vector in groups of 4 amplitudes + for i in 0..self.state.len() { + let bit1 = (i >> q1) & 1; + let bit2 = (i >> q2) & 1; + + // Only process each set of 4 states once + if bit1 == 0 && bit2 == 0 { + // Calculate indices for all four basis states + let i00 = i; + let i01 = i ^ (1 << q2); + let i10 = i ^ (1 << q1); + let i11 = i ^ (1 << q1) ^ (1 << q2); + + // Store original amplitudes + let a00 = self.state[i00]; + let a01 = self.state[i01]; + let a10 = self.state[i10]; + let a11 = self.state[i11]; + + // Apply the 4x4 unitary transformation + self.state[i00] = matrix[0][0] * a00 + + matrix[0][1] * a01 + + matrix[0][2] * a10 + + matrix[0][3] * a11; + self.state[i01] = matrix[1][0] * a00 + + matrix[1][1] * a01 + + matrix[1][2] * a10 + + matrix[1][3] * a11; + self.state[i10] = matrix[2][0] * a00 + + matrix[2][1] * a01 + + matrix[2][2] * a10 + + matrix[2][3] * a11; + self.state[i11] = matrix[3][0] * a00 + + matrix[3][1] * a01 + + matrix[3][2] * a10 + + matrix[3][3] * a11; } } } +} +impl QuantumSimulatorState for StateVec { + /// Returns the number of qubits in the system + #[must_use] + #[inline] + fn num_qubits(&self) -> usize { + self.num_qubits + } + + #[inline] + fn reset(&mut self) -> &mut Self { + self.prepare_computational_basis(0) + } +} + +impl CliffordGateable for StateVec { /// Apply Pauli-X gate /// /// # Panics /// /// Panics if target qubit index is >= number of qubits - pub fn x(&mut self, target: usize) { + #[inline] + fn x(&mut self, target: usize) -> &mut Self { assert!(target < self.num_qubits); let step = 1 << target; @@ -107,6 +248,7 @@ impl StateVec { self.state.swap(i + offset, i + offset + step); } } + self } /// Apply Y = [[0, -i], [i, 0]] gate to target qubit @@ -114,7 +256,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit index is >= number of qubits - pub fn y(&mut self, target: usize) { + #[inline] + fn y(&mut self, target: usize) -> &mut Self { assert!(target < self.num_qubits); for i in 0..self.state.len() { @@ -125,6 +268,7 @@ impl StateVec { self.state[flipped_i] = Complex64::i() * temp; } } + self } /// Apply Z = [[1, 0], [0, -1]] gate to target qubit @@ -132,7 +276,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit index is >= number of qubits - pub fn z(&mut self, target: usize) { + #[inline] + fn z(&mut self, target: usize) -> &mut Self { assert!(target < self.num_qubits); for i in 0..self.state.len() { @@ -140,171 +285,44 @@ impl StateVec { self.state[i] = -self.state[i]; } } + self } - /// Gate RX(θ) = exp(-i θ X/2) = cos(θ/2) I - i*sin(θ/2) X - /// RX(θ) = [[cos(θ/2), -i*sin(θ/2)], - /// [-i*sin(θ/2), cos(θ/2)]] - /// - /// # Panics - /// - /// Panics if target qubit index is >= number of qubits - pub fn rx(&mut self, theta: f64, target: usize) { - let cos = (theta / 2.0).cos(); - let sin = (theta / 2.0).sin(); - let neg_i_sin = Complex64::new(0.0, -sin); - - self.single_qubit_rotation( - target, - Complex64::new(cos, 0.0), // u00 - neg_i_sin, // u01 - neg_i_sin, // u10 - Complex64::new(cos, 0.0), // u11 - ); - } - - /// Gate RY(θ) = exp(-i θ Y/2) = cos(θ/2) I - i*sin(θ/2) Y - /// RY(θ) = [[cos(θ/2), -sin(θ/2)], - /// [-sin(θ/2), cos(θ/2)]] - /// - /// # Panics - /// - /// Panics if target qubit index is >= number of qubits - pub fn ry(&mut self, theta: f64, target: usize) { - let cos = (theta / 2.0).cos(); - let sin = (theta / 2.0).sin(); - - self.single_qubit_rotation( - target, - Complex64::new(cos, 0.0), // u00 - Complex64::new(-sin, 0.0), // u01 - Complex64::new(sin, 0.0), // u10 - Complex64::new(cos, 0.0), // u11 - ); - } - - /// Gate RZ(θ) = exp(-i θ Z/2) = cos(θ/2) I - i*sin(θ/2) Z - /// RZ(θ) = [[cos(θ/2)-i*sin(θ/2), 0], - /// [0, cos(θ/2)+i*sin(θ/2)]] - /// - /// # Panics - /// - /// Panics if target qubit index is >= number of qubits - pub fn rz(&mut self, theta: f64, target: usize) { - let exp_minus_i_theta_2 = Complex64::from_polar(1.0, -theta / 2.0); - let exp_plus_i_theta_2 = Complex64::from_polar(1.0, theta / 2.0); - + #[inline] + fn sz(&mut self, q: usize) -> &mut Self { self.single_qubit_rotation( - target, - exp_minus_i_theta_2, // u00 + q, + Complex64::new(1.0, 0.0), // u00 Complex64::new(0.0, 0.0), // u01 Complex64::new(0.0, 0.0), // u10 - exp_plus_i_theta_2, // u11 - ); + Complex64::new(0.0, 1.0), // u11 + ) } - /// Apply a general single-qubit unitary gate - /// - /// # Examples - /// ``` - /// use pecos_qsim::state_vec::StateVec; - /// use std::f64::consts::FRAC_1_SQRT_2; - /// use num_complex::Complex64; - /// let mut q = StateVec::new(1); - /// // Apply Hadamard gate - /// q.single_qubit_rotation(0, - /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u00 - /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u01 - /// Complex64::new(FRAC_1_SQRT_2, 0.0), // u10 - /// Complex64::new(-FRAC_1_SQRT_2, 0.0), // u11 - /// ); - /// ``` + /// Apply Hadamard gate to the target qubit /// /// # Panics /// /// Panics if target qubit index is >= number of qubits - pub fn single_qubit_rotation( - &mut self, - target: usize, - u00: Complex64, - u01: Complex64, - u10: Complex64, - u11: Complex64, - ) { + #[inline] + fn h(&mut self, target: usize) -> &mut Self { assert!(target < self.num_qubits); - + let factor = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0); let step = 1 << target; + for i in (0..self.state.len()).step_by(2 * step) { for offset in 0..step { let j = i + offset; - let k = j ^ step; + let paired_j = j ^ step; let a = self.state[j]; - let b = self.state[k]; - - self.state[j] = u00 * a + u01 * b; - self.state[k] = u10 * a + u11 * b; - } - } - } - - /// Apply a SWAP gate between two qubits - /// - /// # Panics - /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits - pub fn swap(&mut self, qubit1: usize, qubit2: usize) { - assert!(qubit1 < self.num_qubits && qubit2 < self.num_qubits); - if qubit1 == qubit2 { - return; // No-op if qubits are the same - } - - let step1 = 1 << qubit1; - let step2 = 1 << qubit2; - - for i in 0..self.state.len() { - let bit1 = (i >> qubit1) & 1; - let bit2 = (i >> qubit2) & 1; - - if bit1 != bit2 { - let swapped_index = i ^ step1 ^ step2; - if i < swapped_index { - self.state.swap(i, swapped_index); - } - } - } - } - - /// Apply U3(theta, phi, lambda) gate - /// U3 = [[cos(θ/2), -e^(iλ)sin(θ/2)], - /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] - /// - /// # Panics - /// - /// Panics if target qubit index is >= number of qubits - pub fn u3(&mut self, target: usize, theta: f64, phi: f64, lambda: f64) { - assert!(target < self.num_qubits); - - let cos = (theta / 2.0).cos(); - let sin = (theta / 2.0).sin(); - - // Calculate matrix elements - let u00 = Complex64::new(cos, 0.0); - let u01 = -Complex64::from_polar(sin, lambda); - let u10 = Complex64::from_polar(sin, phi); - let u11 = Complex64::from_polar(cos, phi + lambda); - - // Apply the unitary - for i in 0..self.state.len() { - if (i >> target) & 1 == 0 { - let i1 = i ^ (1 << target); - let a0 = self.state[i]; - let a1 = self.state[i1]; + let b = self.state[paired_j]; - self.state[i] = u00 * a0 + u01 * a1; - self.state[i1] = u10 * a0 + u11 * a1; + self.state[j] = factor * (a + b); + self.state[paired_j] = factor * (a - b); } } + self } /// Apply controlled-X gate @@ -313,7 +331,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - pub fn cx(&mut self, control: usize, target: usize) { + #[inline] + fn cx(&mut self, control: usize, target: usize) -> &mut Self { assert!(control < self.num_qubits); assert!(target < self.num_qubits); assert_ne!(control, target); @@ -326,6 +345,7 @@ impl StateVec { self.state.swap(i, flipped_i); } } + self } /// Apply controlled-Y gate @@ -334,7 +354,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - pub fn cy(&mut self, control: usize, target: usize) { + #[inline] + fn cy(&mut self, control: usize, target: usize) -> &mut Self { assert!(control < self.num_qubits); assert!(target < self.num_qubits); assert_ne!(control, target); @@ -354,6 +375,7 @@ impl StateVec { self.state[flipped_i] = Complex64::i() * temp; } } + self } /// Apply controlled-Z gate @@ -362,7 +384,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - pub fn cz(&mut self, control: usize, target: usize) { + #[inline] + fn cz(&mut self, control: usize, target: usize) -> &mut Self { assert!(control < self.num_qubits); assert!(target < self.num_qubits); assert_ne!(control, target); @@ -376,6 +399,143 @@ impl StateVec { self.state[i] = -self.state[i]; } } + self + } + + /// Apply a SWAP gate between two qubits + /// + /// # Panics + /// + /// Panics if target qubit1 or qubit2 index is >= number of qubits + #[inline] + fn swap(&mut self, qubit1: usize, qubit2: usize) -> &mut Self { + assert!(qubit1 < self.num_qubits && qubit2 < self.num_qubits); + assert_ne!(qubit1, qubit2); + + let step1 = 1 << qubit1; + let step2 = 1 << qubit2; + + for i in 0..self.state.len() { + let bit1 = (i >> qubit1) & 1; + let bit2 = (i >> qubit2) & 1; + + if bit1 != bit2 { + let swapped_index = i ^ step1 ^ step2; + if i < swapped_index { + self.state.swap(i, swapped_index); + } + } + } + self + } + + /// Measure a single qubit in the Z basis and collapse the state + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits + #[inline] + fn mz(&mut self, target: usize) -> bool { + assert!(target < self.num_qubits); + let mut rng = rand::thread_rng(); + + let step = 1 << target; + let mut prob_one = 0.0; + + // Calculate probability of measuring |1⟩ + for i in (0..self.state.len()).step_by(2 * step) { + for offset in 0..step { + let idx = i + offset + step; // Target bit = 1 positions + prob_one += self.state[idx].norm_sqr(); + } + } + + // Decide measurement outcome + let result = usize::from(rng.gen::() < prob_one); + + // Collapse and normalize state + let mut norm = 0.0; + for i in 0..self.state.len() { + let bit = (i >> target) & 1; + if bit == result { + norm += self.state[i].norm_sqr(); + } else { + self.state[i] = Complex64::new(0.0, 0.0); + } + } + + let norm_inv = 1.0 / norm.sqrt(); + for amp in &mut self.state { + *amp *= norm_inv; + } + + result != 0 + } +} + +impl ArbitraryRotationGateable for StateVec { + /// Gate RX(θ) = exp(-i θ X/2) = cos(θ/2) I - i*sin(θ/2) X + /// RX(θ) = [[cos(θ/2), -i*sin(θ/2)], + /// [-i*sin(θ/2), cos(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits + #[inline] + fn rx(&mut self, theta: f64, target: usize) -> &mut Self { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + let neg_i_sin = Complex64::new(0.0, -sin); + + self.single_qubit_rotation( + target, + Complex64::new(cos, 0.0), // u00 + neg_i_sin, // u01 + neg_i_sin, // u10 + Complex64::new(cos, 0.0), // u11 + ) + } + + /// Gate RY(θ) = exp(-i θ Y/2) = cos(θ/2) I - i*sin(θ/2) Y + /// RY(θ) = [[cos(θ/2), -sin(θ/2)], + /// [-sin(θ/2), cos(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits + #[inline] + fn ry(&mut self, theta: f64, target: usize) -> &mut Self { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + + self.single_qubit_rotation( + target, + Complex64::new(cos, 0.0), // u00 + Complex64::new(-sin, 0.0), // u01 + Complex64::new(sin, 0.0), // u10 + Complex64::new(cos, 0.0), // u11 + ) + } + + /// Gate RZ(θ) = exp(-i θ Z/2) = cos(θ/2) I - i*sin(θ/2) Z + /// RZ(θ) = [[cos(θ/2)-i*sin(θ/2), 0], + /// [0, cos(θ/2)+i*sin(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits + #[inline] + fn rz(&mut self, theta: f64, target: usize) -> &mut Self { + let exp_minus_i_theta_2 = Complex64::from_polar(1.0, -theta / 2.0); + let exp_plus_i_theta_2 = Complex64::from_polar(1.0, theta / 2.0); + + self.single_qubit_rotation( + target, + exp_minus_i_theta_2, // u00 + Complex64::new(0.0, 0.0), // u01 + Complex64::new(0.0, 0.0), // u10 + exp_plus_i_theta_2, // u11 + ) } /// Apply RXX(θ) = exp(-i θ XX/2) gate @@ -384,7 +544,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - pub fn rxx(&mut self, theta: f64, qubit1: usize, qubit2: usize) { + #[inline] + fn rxx(&mut self, theta: f64, qubit1: usize, qubit2: usize) -> &mut Self { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); assert_ne!(qubit1, qubit2); @@ -421,6 +582,7 @@ impl StateVec { self.state[i11] = cos * a11 + neg_i_sin * a00; } } + self } /// Apply RYY(θ) = exp(-i θ YY/2) gate @@ -428,7 +590,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - pub fn ryy(&mut self, theta: f64, qubit1: usize, qubit2: usize) { + #[inline] + fn ryy(&mut self, theta: f64, qubit1: usize, qubit2: usize) -> &mut Self { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); assert_ne!(qubit1, qubit2); @@ -464,6 +627,7 @@ impl StateVec { self.state[i11] = cos * a11 + neg_i_sin * a00; } } + self } /// Apply RZZ(θ) = exp(-i θ ZZ/2) gate @@ -471,7 +635,8 @@ impl StateVec { /// # Panics /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - pub fn rzz(&mut self, theta: f64, qubit1: usize, qubit2: usize) { + #[inline] + fn rzz(&mut self, theta: f64, qubit1: usize, qubit2: usize) -> &mut Self { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); assert_ne!(qubit1, qubit2); @@ -492,126 +657,7 @@ impl StateVec { self.state[i] *= phase; } - } - - /// Apply a general two-qubit unitary given by a 4x4 complex matrix - /// U = [[u00, u01, u02, u03], - /// [u10, u11, u12, u13], - /// [u20, u21, u22, u23], - /// [u30, u31, u32, u33]] - /// - /// # Panics - /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - pub fn two_qubit_unitary(&mut self, qubit1: usize, qubit2: usize, matrix: [[Complex64; 4]; 4]) { - assert!(qubit1 < self.num_qubits); - assert!(qubit2 < self.num_qubits); - assert_ne!(qubit1, qubit2); - - // Make sure qubit1 < qubit2 for consistent ordering - let (q1, q2) = if qubit1 < qubit2 { - (qubit1, qubit2) - } else { - (qubit2, qubit1) - }; - - // Process state vector in groups of 4 amplitudes - for i in 0..self.state.len() { - let bit1 = (i >> q1) & 1; - let bit2 = (i >> q2) & 1; - - // Only process each set of 4 states once - if bit1 == 0 && bit2 == 0 { - // Calculate indices for all four basis states - let i00 = i; - let i01 = i ^ (1 << q2); - let i10 = i ^ (1 << q1); - let i11 = i ^ (1 << q1) ^ (1 << q2); - - // Store original amplitudes - let a00 = self.state[i00]; - let a01 = self.state[i01]; - let a10 = self.state[i10]; - let a11 = self.state[i11]; - - // Apply the 4x4 unitary transformation - self.state[i00] = matrix[0][0] * a00 - + matrix[0][1] * a01 - + matrix[0][2] * a10 - + matrix[0][3] * a11; - self.state[i01] = matrix[1][0] * a00 - + matrix[1][1] * a01 - + matrix[1][2] * a10 - + matrix[1][3] * a11; - self.state[i10] = matrix[2][0] * a00 - + matrix[2][1] * a01 - + matrix[2][2] * a10 - + matrix[2][3] * a11; - self.state[i11] = matrix[3][0] * a00 - + matrix[3][1] * a01 - + matrix[3][2] * a10 - + matrix[3][3] * a11; - } - } - } - - /// Measure a single qubit in the Z basis and collapse the state - /// - /// # Panics - /// - /// Panics if target qubit index is >= number of qubits - pub fn measure(&mut self, target: usize) -> usize { - assert!(target < self.num_qubits); - let mut rng = rand::thread_rng(); - - let step = 1 << target; - let mut prob_one = 0.0; - - // Calculate probability of measuring |1⟩ - for i in (0..self.state.len()).step_by(2 * step) { - for offset in 0..step { - let idx = i + offset + step; // Target bit = 1 positions - prob_one += self.state[idx].norm_sqr(); - } - } - - // Decide measurement outcome - let result = usize::from(rng.gen::() < prob_one); - - // Collapse and normalize state - let mut norm = 0.0; - for i in 0..self.state.len() { - let bit = (i >> target) & 1; - if bit == result { - norm += self.state[i].norm_sqr(); - } else { - self.state[i] = Complex64::new(0.0, 0.0); - } - } - - let norm_inv = 1.0 / norm.sqrt(); - for amp in &mut self.state { - *amp *= norm_inv; - } - - result - } - - /// Reset a qubit to the |0⟩ state - /// - /// # Panics - /// - /// Panics if target qubit index is >= number of qubits - pub fn reset(&mut self, target: usize) { - assert!(target < self.num_qubits); - - // Measure the qubit - let result = self.measure(target); - - // If we got |1⟩, apply X to flip it to |0⟩ - if result == 1 { - self.x(target); - } + self } } @@ -666,7 +712,7 @@ mod tests { let mut q = StateVec::new(1); // RZ should only add phases, not change probabilities - q.hadamard(0); // Put in superposition first + q.h(0); // Put in superposition first let probs_before: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); q.rz(FRAC_PI_2, 0); @@ -679,7 +725,7 @@ mod tests { // RZ(2π) should return to initial state up to global phase let mut q = StateVec::new(1); - q.hadamard(0); + q.h(0); let state_before = q.state.clone(); q.rz(TAU, 0); @@ -693,7 +739,7 @@ mod tests { #[test] fn test_hadamard() { let mut q = StateVec::new(1); - q.hadamard(0); + q.h(0); assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); @@ -713,7 +759,7 @@ mod tests { fn test_cx() { let mut q = StateVec::new(2); // Prep |+> - q.hadamard(0); + q.h(0); q.cx(0, 1); // Should be in Bell state (|00> + |11>)/sqrt(2) @@ -729,7 +775,7 @@ mod tests { let mut q = StateVec::new(2); // Create |+0⟩ state - q.hadamard(0); + q.h(0); // Apply CY to get entangled state q.cy(0, 1); @@ -747,8 +793,8 @@ mod tests { let mut q = StateVec::new(2); // Create |++⟩ state - q.hadamard(0); - q.hadamard(1); + q.h(0); + q.h(1); // Apply CZ q.cz(0, 1); @@ -768,10 +814,10 @@ mod tests { let mut q2 = StateVec::new(2); // Prepare same initial state - q1.hadamard(0); - q1.hadamard(1); - q2.hadamard(0); - q2.hadamard(1); + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); // Apply gates with different control/target q1.cz(0, 1); @@ -802,7 +848,7 @@ mod tests { assert!(q.state[1].norm() < 1e-10); // Test X on superposition - q.hadamard(0); + q.h(0); let initial_state = q.state.clone(); q.x(0); // X|+> = |+> for (state, initial) in q.state.iter().zip(initial_state.iter()) { @@ -832,7 +878,7 @@ mod tests { // Test Y on |+⟩ let mut q = StateVec::new(1); - q.hadamard(0); // Create |+⟩ + q.h(0); // Create |+⟩ q.y(0); // Should give i|-⟩ let expected = FRAC_1_SQRT_2; assert!((q.state[0].im + expected).abs() < 1e-10); @@ -856,7 +902,7 @@ mod tests { // Test Z on |+⟩ -> |-⟩ let mut q = StateVec::new(1); - q.hadamard(0); // Create |+⟩ + q.h(0); // Create |+⟩ q.z(0); // Should give |-⟩ let expected = FRAC_1_SQRT_2; assert!((q.state[0].re - expected).abs() < 1e-10); @@ -913,7 +959,7 @@ mod tests { // Test 2: RXX(2π) should return to original state up to global phase let mut q = StateVec::new(2); - q.hadamard(0); // Create some initial state + q.h(0); // Create some initial state let initial = q.state.clone(); q.rxx(TAU, 0, 1); @@ -943,10 +989,10 @@ mod tests { let mut q2 = StateVec::new(2); // Prepare same non-trivial initial state - q1.hadamard(0); - q1.hadamard(1); - q2.hadamard(0); - q2.hadamard(1); + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); // Apply RXX with different qubit orders q1.rxx(FRAC_PI_3, 0, 1); @@ -972,7 +1018,7 @@ mod tests { // Test 2: RYY(2π) should return to original state up to global phase let mut q = StateVec::new(2); - q.hadamard(0); // Create some initial state + q.h(0); // Create some initial state let initial = q.state.clone(); q.ryy(TAU, 0, 1); @@ -989,7 +1035,7 @@ mod tests { // Test 1: RZZ(π) on (|00⟩ + |11⟩)/√2 should give itself let mut q = StateVec::new(2); // Create Bell state - q.hadamard(0); + q.h(0); q.cx(0, 1); let initial = q.state.clone(); @@ -1005,8 +1051,8 @@ mod tests { // Test 2: RZZ(π/2) on |++⟩ let mut q = StateVec::new(2); - q.hadamard(0); - q.hadamard(1); + q.h(0); + q.h(1); q.rzz(FRAC_PI_2, 0, 1); // e^(-iπ/4) = (1-i)/√2 @@ -1028,10 +1074,10 @@ mod tests { let mut q2 = StateVec::new(2); // Prepare same non-trivial initial state - q1.hadamard(0); - q1.hadamard(1); - q2.hadamard(0); - q2.hadamard(1); + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); let theta = PI / 3.0; @@ -1046,10 +1092,10 @@ mod tests { // Test RZZ symmetry let mut q1 = StateVec::new(2); let mut q2 = StateVec::new(2); - q1.hadamard(0); - q1.hadamard(1); - q2.hadamard(0); - q2.hadamard(1); + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); q1.rzz(theta, 0, 1); q2.rzz(theta, 1, 0); @@ -1062,29 +1108,29 @@ mod tests { #[test] fn test_measure2() { let mut q = StateVec::new(1); - q.hadamard(0); - let result = q.measure(0); + q.h(0); + let _result = q.mz(0); // Check collapse to |0⟩ or |1⟩ - assert!(result == 0 || result == 1); + // assert!(result == 0 || result == 1); let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); assert!((norm - 1.0).abs() < 1e-10); } #[test] - fn test_measure() { + fn test_mz() { // Test 1: Measuring |0> state let mut q = StateVec::new(1); - let result = q.measure(0); - assert_eq!(result, 0); + let result = q.mz(0); + assert!(!result); assert!((q.state[0].re - 1.0).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); // Test 2: Measuring |1> state let mut q = StateVec::new(1); q.x(0); - let result = q.measure(0); - assert_eq!(result, 1); + let result = q.mz(0); + assert!(result); assert!(q.state[0].norm() < 1e-10); assert!((q.state[1].re - 1.0).abs() < 1e-10); @@ -1094,9 +1140,9 @@ mod tests { for _ in 0..trials { let mut q = StateVec::new(1); - q.hadamard(0); - let result = q.measure(0); - if result == 0 { + q.h(0); + let result = q.mz(0); + if !result { zeros += 1; } } @@ -1107,13 +1153,13 @@ mod tests { // Test 4: Measuring one qubit of a Bell state let mut q = StateVec::new(2); - q.hadamard(0); + q.h(0); q.cx(0, 1); // Measure first qubit - let result1 = q.measure(0); + let result1 = q.mz(0); // Measure second qubit - should match first - let result2 = q.measure(1); + let result2 = q.mz(1); assert_eq!(result1, result2); } @@ -1121,11 +1167,11 @@ mod tests { fn test_reset() { let mut q = StateVec::new(1); - q.hadamard(0); + q.h(0); assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - q.reset(0); + q.pz(0); assert!((q.state[0].re - 1.0).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); @@ -1135,10 +1181,10 @@ mod tests { fn test_reset_multiple_qubits() { let mut q = StateVec::new(2); - q.hadamard(0); + q.h(0); q.cx(0, 1); - q.reset(0); + q.pz(0); let prob_0 = q.state[0].norm_sqr() + q.state[2].norm_sqr(); let prob_1 = q.state[1].norm_sqr() + q.state[3].norm_sqr(); @@ -1200,7 +1246,7 @@ mod tests { let mut q = StateVec::new(1); // Create random state with Hadamard - q.hadamard(0); + q.h(0); // Apply Z gate as unitary let z00 = Complex64::new(1.0, 0.0); @@ -1240,7 +1286,7 @@ mod tests { // Test 3: U3(0, 0, π) should be Z gate let mut q = StateVec::new(1); - q.hadamard(0); // First put in superposition + q.h(0); // First put in superposition let initial = q.state.clone(); q.u3(0, 0.0, 0.0, PI); assert!((q.state[0] - initial[0]).norm() < 1e-10); @@ -1326,10 +1372,10 @@ mod tests { ]; // Create Bell state using both methods - q1.hadamard(0); + q1.h(0); q1.cx(0, 1); - q2.hadamard(0); + q2.h(0); q2.two_qubit_unitary(0, 1, cnot); // Compare results @@ -1386,8 +1432,8 @@ mod tests { let mut q = StateVec::new(2); // Create a non-trivial state - q.hadamard(0); - q.hadamard(1); + q.h(0); + q.h(1); // iSWAP matrix let iswap = [ @@ -1430,7 +1476,7 @@ mod tests { let mut q = StateVec::new(3); // Prepare state |+⟩|0⟩|0⟩ - q.hadamard(0); // Affects least significant bit + q.h(0); // Affects least significant bit // Apply X to qubit 2 (most significant bit) q.x(2); @@ -1451,7 +1497,7 @@ mod tests { } // Put |+⟩ on qubit 0 (LSB) - q.hadamard(0); + q.h(0); println!("\nAfter H on qubit 0:"); for i in 0..8 { @@ -1490,7 +1536,7 @@ mod tests { } // Prepare |+⟩ on qubit 0 (LSB) - q.hadamard(0); + q.h(0); println!("\nAfter H on qubit 0:"); for i in 0..16 { @@ -1530,7 +1576,7 @@ mod tests { let mut q = StateVec::new(3); // Prepare state |+⟩|0⟩|0⟩ - q.hadamard(0); + q.h(0); // Apply CX on qubits 1 and 2 (no effect on qubit 0) q.cx(1, 2); @@ -1551,7 +1597,7 @@ mod tests { } // Prepare |+⟩ on qubit 0 (LSB) - q.hadamard(0); + q.h(0); println!("\nAfter H on qubit 0:"); for i in 0..8 { @@ -1577,12 +1623,12 @@ mod tests { } #[test] - fn test_large_system_hadamard() { + fn test_large_system_h() { let num_qubits = 10; // 10 qubits => state vector size = 1024 let mut q = StateVec::new(num_qubits); // Apply Hadamard gate to the 0th qubit - q.hadamard(0); + q.h(0); // Check that |0...0> and |1...0> have equal amplitude let expected_amp = 1.0 / 2.0_f64.sqrt(); @@ -1624,11 +1670,11 @@ mod tests { q.x(0); // Measure twice - result should be the same - let result1 = q.measure(0); - let result2 = q.measure(0); + let result1 = q.mz(0); + let result2 = q.mz(0); - assert_eq!(result1, 1); - assert_eq!(result2, 1); + assert!(result1); + assert!(result2); } #[test] @@ -1636,14 +1682,14 @@ mod tests { let mut q = StateVec::new(2); // Create Bell state (|00⟩ + |11⟩) / sqrt(2) - q.hadamard(0); + q.h(0); q.cx(0, 1); // Measure the first qubit - let result1 = q.measure(0); + let result1 = q.mz(0); // Measure the second qubit - should match the first - let result2 = q.measure(1); + let result2 = q.mz(1); assert_eq!(result1, result2); } From 1fc111d1cdc24f9c7bac4fef90344f5f20fb304c Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Fri, 20 Dec 2024 23:31:58 -0700 Subject: [PATCH 10/33] Adding MeasurementResult struct --- crates/pecos-python/src/sparse_sim.rs | 2 +- .../src/arbitrary_rotation_gateable.rs | 12 +- crates/pecos-qsim/src/clifford_gateable.rs | 59 ++- crates/pecos-qsim/src/lib.rs | 7 +- crates/pecos-qsim/src/pauli_prop.rs | 10 +- .../pecos-qsim/src/quantum_simulator_state.rs | 17 +- crates/pecos-qsim/src/sparse_stab.rs | 499 +++++++----------- crates/pecos-qsim/src/state_vec.rs | 152 ++++-- 8 files changed, 364 insertions(+), 394 deletions(-) diff --git a/crates/pecos-python/src/sparse_sim.rs b/crates/pecos-python/src/sparse_sim.rs index cb56d38a..48699a1b 100644 --- a/crates/pecos-python/src/sparse_sim.rs +++ b/crates/pecos-python/src/sparse_sim.rs @@ -197,7 +197,7 @@ impl SparseSim { } _ => unreachable!(), }; - Ok(Some(u8::from(result))) + Ok(Some(u8::from(result.outcome))) } _ => Err(PyErr::new::( "Unsupported single-qubit gate", diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs index 0977ba9d..e94921b2 100644 --- a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -15,10 +15,16 @@ use pecos_core::IndexableElement; pub trait ArbitraryRotationGateable: CliffordGateable { fn rx(&mut self, theta: f64, q: T) -> &mut Self; - fn ry(&mut self, theta: f64, q: T) -> &mut Self; + fn ry(&mut self, theta: f64, q: T) -> &mut Self { + self.sz(q).rx(theta, q).szdg(q) + } fn rz(&mut self, theta: f64, q: T) -> &mut Self; - fn rxx(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; - fn ryy(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; + fn rxx(&mut self, theta: f64, q1: T, q2: T) -> &mut Self { + self.h(q1).h(q2).rzz(theta, q1, q2).h(q1).h(q2) + } + fn ryy(&mut self, theta: f64, q1: T, q2: T) -> &mut Self { + self.sx(q1).sx(q2).rzz(theta, q1, q2).sxdg(q1).sxdg(q2) + } fn rzz(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; } diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index 0033e1f3..c9f0d8c9 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -13,6 +13,11 @@ use super::quantum_simulator_state::QuantumSimulatorState; use pecos_core::IndexableElement; +pub struct MeasurementResult { + pub outcome: bool, + pub is_deterministic: bool, +} + /// A simulator trait for quantum systems that implement Clifford operations. /// /// # Overview @@ -643,7 +648,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// Measurement of the +`X_q` operator. #[inline] - fn mx(&mut self, q: T) -> bool { + fn mx(&mut self, q: T) -> MeasurementResult { // +X -> +Z self.h(q); let meas = self.mz(q); @@ -655,7 +660,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// Measurement of the -`X_q` operator. #[inline] - fn mnx(&mut self, q: T) -> bool { + fn mnx(&mut self, q: T) -> MeasurementResult { // -X -> +Z self.h(q); self.x(q); @@ -671,7 +676,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn my(&mut self, q: T) -> bool { + fn my(&mut self, q: T) -> MeasurementResult { // +Y -> +Z self.sx(q); let meas = self.mz(q); @@ -685,7 +690,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn mny(&mut self, q: T) -> bool { + fn mny(&mut self, q: T) -> MeasurementResult { // -Y -> +Z self.sxdg(q); let meas = self.mz(q); @@ -720,13 +725,13 @@ pub trait CliffordGateable: QuantumSimulatorState { /// let mut sim = StdSparseStab::new(1); /// let outcome = sim.mz(0); /// ``` - fn mz(&mut self, q: T) -> bool; + fn mz(&mut self, q: T) -> MeasurementResult; /// Measurement of the -`Z_q` operator. /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn mnz(&mut self, q: T) -> bool { + fn mnz(&mut self, q: T) -> MeasurementResult { // -Z -> +Z self.x(q); let meas = self.mz(q); @@ -749,11 +754,12 @@ pub trait CliffordGateable: QuantumSimulatorState { /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn px(&mut self, q: T) -> &mut Self { - if self.mx(q) { + fn px(&mut self, q: T) -> MeasurementResult { + let result = self.mx(q); + if result.outcome { self.z(q); } - self + result } /// Prepares a qubit in the -1 eigenstate of the X operator. @@ -770,33 +776,36 @@ pub trait CliffordGateable: QuantumSimulatorState { /// /// Will panic if qubit ids don't convert to usize. #[inline] - fn pnx(&mut self, q: T) -> &mut Self { - if self.mnx(q) { + fn pnx(&mut self, q: T) -> MeasurementResult { + let result = self.mnx(q); + if result.outcome { self.z(q); } - self + result } /// Preparation of the +`Y_q` operator. /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn py(&mut self, q: T) -> &mut Self { - if self.my(q) { + fn py(&mut self, q: T) -> MeasurementResult { + let result = self.my(q); + if result.outcome { self.z(q); } - self + result } /// Preparation of the -`Y_q` operator. /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn pny(&mut self, q: T) -> &mut Self { - if self.mny(q) { + fn pny(&mut self, q: T) -> MeasurementResult { + let result = self.mny(q); + if result.outcome { self.z(q); } - self + result } /// Prepares a qubit in the +1 eigenstate of the Z operator. @@ -813,11 +822,12 @@ pub trait CliffordGateable: QuantumSimulatorState { /// /// Will panic if qubit ids don't convert to usize. #[inline] - fn pz(&mut self, q: T) -> &mut Self { - if self.mz(q) { + fn pz(&mut self, q: T) -> MeasurementResult { + let result = self.mz(q); + if result.outcome { self.x(q); } - self + result } /// Prepares a qubit in the -1 eigenstate of the Z operator. @@ -834,10 +844,11 @@ pub trait CliffordGateable: QuantumSimulatorState { /// /// Will panic if qubit ids don't convert to usize. #[inline] - fn pnz(&mut self, q: T) -> &mut Self { - if self.mnz(q) { + fn pnz(&mut self, q: T) -> MeasurementResult { + let result = self.mnz(q); + if result.outcome { self.x(q); } - self + result } } diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index 902586de..338810e3 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -12,8 +12,6 @@ pub mod clifford_gateable; pub mod gens; -// pub mod measurement; -// pub mod nonclifford_simulator; pub mod pauli_prop; // pub mod paulis; pub mod arbitrary_rotation_gateable; @@ -22,11 +20,8 @@ pub mod quantum_simulator_state; pub mod sparse_stab; pub mod state_vec; -pub use clifford_gateable::CliffordGateable; +pub use clifford_gateable::{CliffordGateable, MeasurementResult}; pub use gens::Gens; -// pub use measurement::{MeasBitValue, MeasValue, Measurement}; // TODO: Distinguish between trait and struct/enum -// pub use nonclifford_simulator::NonCliffordSimulator; -// pub use pauli_prop::{PauliProp, StdPauliProp}; // pub use paulis::Paulis; pub use quantum_simulator_state::QuantumSimulatorState; pub use sparse_stab::{SparseStab, StdSparseStab}; diff --git a/crates/pecos-qsim/src/pauli_prop.rs b/crates/pecos-qsim/src/pauli_prop.rs index 17f517db..91a1e80b 100644 --- a/crates/pecos-qsim/src/pauli_prop.rs +++ b/crates/pecos-qsim/src/pauli_prop.rs @@ -11,7 +11,7 @@ // the License. #![allow(unused_variables)] -use super::clifford_gateable::CliffordGateable; +use super::clifford_gateable::{CliffordGateable, MeasurementResult}; use crate::quantum_simulator_state::QuantumSimulatorState; use core::marker::PhantomData; use pecos_core::{IndexableElement, Set, VecSet}; @@ -119,7 +119,11 @@ where /// Output true if there is an X on the qubit. #[inline] - fn mz(&mut self, q: E) -> bool { - self.xs.contains(&q) + fn mz(&mut self, q: E) -> MeasurementResult { + let outcome = self.xs.contains(&q); + MeasurementResult { + outcome, + is_deterministic: false, + } } } diff --git a/crates/pecos-qsim/src/quantum_simulator_state.rs b/crates/pecos-qsim/src/quantum_simulator_state.rs index 712450f8..ac45cb38 100644 --- a/crates/pecos-qsim/src/quantum_simulator_state.rs +++ b/crates/pecos-qsim/src/quantum_simulator_state.rs @@ -16,6 +16,14 @@ pub trait QuantumSimulatorState { /// /// # Returns /// * `usize` - The total number of qubits this simulator is configured to handle + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulatorState, StdSparseStab}; + /// let state = StdSparseStab::new(2); + /// let num = state.num_qubits(); + /// assert_eq!(num, 2); + /// ``` fn num_qubits(&self) -> usize; /// Resets all qubits in the system to the |0⟩ state @@ -31,10 +39,15 @@ pub trait QuantumSimulatorState { /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, StdSparseStab}; /// /// let mut sim = StdSparseStab::new(2); - /// sim.h(0) + /// sim.x(0) /// .cx(0, 1) /// .reset() // Return to |00⟩ state - /// .h(1); // Can continue chaining methods + /// .z(1); // Can continue chaining methods + /// let r0 = sim.mz(0); + /// let r1 = sim.mz(1); + /// assert_eq!(r0.outcome, r1.outcome); + /// assert!(!r0.outcome); + /// assert!(!r1.outcome); /// ``` fn reset(&mut self) -> &mut Self; } diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index 94e37f86..e45c7562 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -10,7 +10,7 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -use crate::{CliffordGateable, Gens, QuantumSimulatorState}; +use crate::{CliffordGateable, Gens, MeasurementResult, QuantumSimulatorState}; use core::fmt::Debug; use core::mem; use pecos_core::{IndexableElement, Set}; @@ -50,14 +50,18 @@ pub type StdSparseStab = SparseStab, usize>; /// // Create a new 2-qubit stabilizer state /// let mut sim = SparseStab::, u32>::new(2); /// -/// // Initialize to |+> ⊗ |0> -/// sim.h(0); // Apply Hadamard to first qubit -/// /// // Create Bell state |Φ+> = (|00> + |11>)/√2 -/// sim.cx(0, 1); +/// sim.h(0) +/// .cx(0, 1); +/// +/// // Measure the two qubits in the Z basis +/// let r0 = sim.mz(0); +/// let r1 = sim.mz(1); /// -/// // Measure first qubit in Z basis -/// let outcome = sim.mz(0); +/// // Both measurements should equal each other +/// assert_eq!(r0.outcome, r1.outcome); +/// // But should be random +/// assert!(!r0.is_deterministic); /// ``` /// /// # Measurement Behavior @@ -226,7 +230,7 @@ where /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - fn deterministic_meas(&mut self, q: E) -> bool { + fn deterministic_meas(&mut self, q: E) -> MeasurementResult { let qu = q.to_usize(); let mut num_minuses = self.destabs.col_x[qu] @@ -247,13 +251,18 @@ where // num_is % 4 != 0 num_minuses += 1; } - num_minuses & 1 != 0 // num_minuses % 2 != 0 (is odd) + let outcome = num_minuses & 1 != 0; // num_minuses % 2 != 0 (is odd) + MeasurementResult { + outcome, + is_deterministic: true, + } } /// # Panics /// Will panic if qubit ids don't convert to usize. + #[allow(clippy::too_many_lines)] #[inline] - fn nondeterministic_meas(&mut self, q: E, result: bool) -> bool { + fn nondeterministic_meas(&mut self, q: E, result: bool) -> MeasurementResult { let qu = q.to_usize(); let mut anticom_stabs_col = self.stabs.col_x[qu].clone(); @@ -387,61 +396,18 @@ where self.destabs.row_x[id_usize] = removed_row_x; self.destabs.row_z[id_usize] = removed_row_z; - self.apply_outcome(id, result) - } - - /// Measures a qubit in the Z basis and get if deterministic result. - /// - /// Returns a tuple containing: - /// - The measurement outcome (true = |1>, false = |0>) - /// - Whether the measurement was deterministic - /// - /// The measurement can be: - /// - Deterministic: The outcome is fixed by the current stabilizer state - /// - Non-deterministic: The outcome is random with 50% probability for each result - /// - /// # Arguments - /// * q - The qubit index to measure - /// - /// # Returns - /// * (bool, bool) - (`measurement_outcome`, `is_deterministic`) - /// - /// # Example - /// ```rust - /// use pecos_core::VecSet; - /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; - /// let mut state = SparseStab::, u32>::new(2); - /// - /// let (outcome, deterministic) = state.mz_determine(0); - /// if deterministic { - /// println!("Measurement was deterministic with outcome: {}", outcome); - /// } - /// ``` - /// - /// # Panics - /// - /// Will panic if qubit ids don't convert to usize. - #[inline] - pub fn mz_determine(&mut self, q: E) -> (bool, bool) { - let qu = q.to_usize(); - - let deterministic = self.stabs.col_x[qu].is_empty(); - - let meas_out = if deterministic { - // There are no stabilizers that anti-commute with Z_q - self.deterministic_meas(q) - } else { - let result = self.rng.gen_bool(0.5); - self.nondeterministic_meas(q, result) - }; - (meas_out, deterministic) + let outcome = self.apply_outcome(id, result); + MeasurementResult { + outcome, + is_deterministic: false, + } } /// Measurement of the +`Z_q` operator where random outcomes are forced to a particular value. /// # Panics /// Will panic if qubit ids don't convert to usize. #[inline] - pub fn mz_forced(&mut self, q: E, forced_outcome: bool) -> bool { + pub fn mz_forced(&mut self, q: E, forced_outcome: bool) -> MeasurementResult { let qu = q.to_usize(); if self.stabs.col_x[qu].is_empty() { @@ -452,101 +418,6 @@ where } } - #[inline] - pub fn mx_determine(&mut self, q: E) -> (bool, bool) { - self.h(q); - let result = self.mz_determine(q); - self.h(q); - result - } - - #[inline] - pub fn mnx_determine(&mut self, q: E) -> (bool, bool) { - self.h(q); - let result = self.mnz_determine(q); - self.h(q); - result - } - - #[inline] - pub fn my_determine(&mut self, q: E) -> (bool, bool) { - self.sx(q); - let result = self.mz_determine(q); - self.sxdg(q); - result - } - - #[inline] - pub fn mny_determine(&mut self, q: E) -> (bool, bool) { - self.sxdg(q); - let result = self.mz_determine(q); - self.sx(q); - result - } - - #[inline] - pub fn mnz_determine(&mut self, q: E) -> (bool, bool) { - self.x(q); - let result = self.mz_determine(q); - self.x(q); - - result - } - - #[inline] - pub fn px_determine(&mut self, q: E) -> (bool, bool) { - let result = self.mx_determine(q); - if result.0 { - self.z(q); - } - result - } - - #[inline] - pub fn pnx_determine(&mut self, q: E) -> (bool, bool) { - let result = self.mnx_determine(q); - if result.0 { - self.z(q); - } - result - } - - #[inline] - pub fn py_determine(&mut self, q: E) -> (bool, bool) { - let result = self.my_determine(q); - if result.0 { - self.z(q); - } - result - } - - #[inline] - pub fn pny_determine(&mut self, q: E) -> (bool, bool) { - let result = self.mny_determine(q); - if result.0 { - self.z(q); - } - result - } - - #[inline] - pub fn pz_determine(&mut self, q: E) -> (bool, bool) { - let result = self.mz_determine(q); - if result.0 { - self.x(q); - } - result - } - - #[inline] - pub fn pnz_determine(&mut self, q: E) -> (bool, bool) { - let result = self.mnz_determine(q); - if result.0 { - self.x(q); - } - result - } - /// Preparation of the +`Z_q` operator where random outcomes are forced to a particular value. /// /// # Panics @@ -554,7 +425,7 @@ where #[inline] pub fn pz_forced(&mut self, q: E, forced_outcome: bool) -> &mut Self { let result = self.mz_forced(q, forced_outcome); - if result { + if result.outcome { self.x(q); } self @@ -783,26 +654,6 @@ where self } - #[inline] - fn mx(&mut self, q: E) -> bool { - self.mx_determine(q).0 - } - - #[inline] - fn mnx(&mut self, q: E) -> bool { - self.mnx_determine(q).0 - } - - #[inline] - fn my(&mut self, q: E) -> bool { - self.my_determine(q).0 - } - - #[inline] - fn mny(&mut self, q: E) -> bool { - self.mny_determine(q).0 - } - /// Measures a qubit in the Z basis. /// /// Returns a tuple containing: @@ -832,13 +683,18 @@ where /// /// Will panic if qubit ids don't convert to usize. #[inline] - fn mz(&mut self, q: E) -> bool { - self.mz_determine(q).0 - } + fn mz(&mut self, q: E) -> MeasurementResult { + let qu = q.to_usize(); - #[inline] - fn mnz(&mut self, q: E) -> bool { - self.mnz_determine(q).0 + let deterministic = self.stabs.col_x[qu].is_empty(); + + if deterministic { + // There are no stabilizers that anti-commute with Z_q + self.deterministic_meas(q) + } else { + let result = self.rng.gen_bool(0.5); + self.nondeterministic_meas(q, result) + } } } @@ -1024,12 +880,14 @@ mod tests { fn test_nondeterministic_px() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.px_determine(0); - let (m1, d1) = state.mx_determine(0); + let r0 = state.px(0); + let meas = state.mx(0); + let m1 = meas.outcome; + let d1 = meas.is_deterministic; let m1_int = u8::from(m1); assert_eq!(m1_int, 0); // |+X> - assert!(!d0); // Not deterministic + assert!(!r0.is_deterministic); // Not deterministic assert!(d1); // Deterministic } } @@ -1037,10 +895,10 @@ mod tests { #[test] fn test_deterministic_px() { let mut state = prep_state(&["X"], &["Z"]); - let (m0, d0) = state.px_determine(0); - let m0_int = u8::from(m0); + let r0 = state.px(0); + let m0_int = u8::from(r0.outcome); - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic assert_eq!(m0_int, 0); // |+X> } @@ -1048,23 +906,23 @@ mod tests { fn test_nondeterministic_pnx() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.pnx_determine(0); - let (m1, d1) = state.mx_determine(0); - let m1_int = u8::from(m1); + let r0 = state.pnx(0); + let result = state.mx(0); + let m1_int = u8::from(result.outcome); assert_eq!(m1_int, 1); // |-X> - assert!(!d0); // Not deterministic - assert!(d1); // Deterministic + assert!(!r0.is_deterministic); // Not deterministic + assert!(result.is_deterministic); // Deterministic } } #[test] fn test_deterministic_pnx() { let mut state = prep_state(&["-X"], &["Z"]); - let (m0, d0) = state.pnx_determine(0); - let m0_int = u8::from(m0); + let r0 = state.pnx(0); + let m0_int = u8::from(r0.outcome); - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic assert_eq!(m0_int, 0); // |-X> } @@ -1072,23 +930,23 @@ mod tests { fn test_nondeterministic_py() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.py_determine(0); - let (m1, d1) = state.my_determine(0); - let m1_int = u8::from(m1); + let r0 = state.py(0); + let r1 = state.my(0); + let m1_int = u8::from(r1.outcome); assert_eq!(m1_int, 0); // |+Y> - assert!(!d0); // Not deterministic - assert!(d1); // Deterministic + assert!(!r0.is_deterministic); // Not deterministic + assert!(r1.is_deterministic); // Deterministic } } #[test] fn test_deterministic_py() { let mut state = prep_state(&["iW"], &["Z"]); - let (m0, d0) = state.py_determine(0); - let m0_int = u8::from(m0); + let r0 = state.py(0); + let m0_int = u8::from(r0.outcome); - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic assert_eq!(m0_int, 0); // |+Y> } @@ -1096,23 +954,23 @@ mod tests { fn test_nondeterministic_pny() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let (_m0, d0) = state.pny_determine(0); - let (m1, d1) = state.my_determine(0); - let m1_int = u8::from(m1); + let r0 = state.pny(0); + let r1 = state.my(0); + let m1_int = u8::from(r1.outcome); assert_eq!(m1_int, 1); // |-Y> - assert!(!d0); // Not deterministic - assert!(d1); // Deterministic + assert!(!r0.is_deterministic); // Not deterministic + assert!(r1.is_deterministic); // Deterministic } } #[test] fn test_deterministic_pny() { let mut state = prep_state(&["-iW"], &["Z"]); - let (m0, d0) = state.pny_determine(0); - let m0_int = u8::from(m0); + let r0 = state.pny(0); + let m0_int = u8::from(r0.outcome); - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic assert_eq!(m0_int, 0); // |-Y> } @@ -1120,23 +978,23 @@ mod tests { fn test_nondeterministic_pz() { for _ in 1_u32..=100 { let mut state = prep_state(&["X"], &["Z"]); - let (_m0, d0) = state.pz_determine(0); - let (m1, d1) = state.mz_determine(0); - let m1_int = u8::from(m1); + let r0 = state.pz(0); + let r1 = state.mz(0); + let m1_int = u8::from(r1.outcome); assert_eq!(m1_int, 0); // |0> - assert!(!d0); // Not deterministic - assert!(d1); // Deterministic + assert!(!r0.is_deterministic); // Not deterministic + assert!(r1.is_deterministic); // Deterministic } } #[test] fn test_deterministic_pz() { let mut state = prep_state(&["Z"], &["X"]); - let (m0, d0) = state.pz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.pz(0); + let m0_int = u8::from(r0.outcome); - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic assert_eq!(m0_int, 0); // |+Z> } @@ -1144,144 +1002,144 @@ mod tests { fn test_nondeterministic_pnz() { for _ in 1_u32..=100 { let mut state = prep_state(&["X"], &["Z"]); - let (_m0, d0) = state.pnz_determine(0); - let (m1, d1) = state.mz_determine(0); - let m1_int = u8::from(m1); + let r0 = state.pnz(0); + let r1 = state.mz(0); + let m1_int = u8::from(r1.outcome); assert_eq!(m1_int, 1); // |1> - assert!(!d0); // Not deterministic - assert!(d1); // Deterministic + assert!(!r0.is_deterministic); // Not deterministic + assert!(r1.is_deterministic); // Deterministic } } #[test] fn test_deterministic_pnz() { let mut state = prep_state(&["-Z"], &["X"]); - let (m0, d0) = state.pnz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.pnz(0); + let m0_int = u8::from(r0.outcome); - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic assert_eq!(m0_int, 0); // |-Z> } #[test] fn test_nondeterministic_mx() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.mx_determine(0); - assert!(!determined); + let r = state.mx(0); + assert!(!r.is_deterministic); } #[test] fn test_deterministic_mx() { let mut state0 = prep_state(&["X"], &["Z"]); - let (meas0, determined0) = state0.mx_determine(0); - assert!(determined0); - assert!(!meas0); + let r0 = state0.mx(0); + assert!(r0.is_deterministic); + assert!(!r0.outcome); let mut state1 = prep_state(&["-X"], &["Z"]); - let (meas1, determined1) = state1.mx_determine(0); - assert!(determined1); - assert!(meas1); + let r1 = state1.mx(0); + assert!(r1.is_deterministic); + assert!(r1.outcome); } #[test] fn test_nondeterministic_mnx() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.mnx_determine(0); - assert!(!determined); + let r = state.mnx(0); + assert!(!r.is_deterministic); } #[test] fn test_deterministic_mnx() { let mut state0 = prep_state(&["-X"], &["Z"]); - let (meas0, determined0) = state0.mnx_determine(0); - assert!(determined0); - assert!(!meas0); + let r0 = state0.mnx(0); + assert!(r0.is_deterministic); + assert!(!r0.outcome); let mut state1 = prep_state(&["X"], &["Z"]); - let (meas1, determined1) = state1.mnx_determine(0); - assert!(determined1); - assert!(meas1); + let r1 = state1.mnx(0); + assert!(r1.is_deterministic); + assert!(r1.outcome); } #[test] fn test_nondeterministic_my() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.my_determine(0); - assert!(!determined); + let r = state.my(0); + assert!(!r.is_deterministic); } #[test] fn test_deterministic_my() { let mut state0 = prep_state(&["iW"], &["Z"]); - let (meas0, determined0) = state0.my_determine(0); - assert!(determined0); - assert!(!meas0); + let r0 = state0.my(0); + assert!(r0.is_deterministic); + assert!(!r0.outcome); let mut state1 = prep_state(&["-iW"], &["Z"]); - let (meas1, determined1) = state1.my_determine(0); - assert!(determined1); - assert!(meas1); + let r1 = state1.my(0); + assert!(r1.is_deterministic); + assert!(r1.outcome); } #[test] fn test_nondeterministic_mny() { let mut state = prep_state(&["Z"], &["X"]); - let (_meas, determined) = state.mny_determine(0); - assert!(!determined); + let r = state.mny(0); + assert!(!r.is_deterministic); } #[test] fn test_deterministic_mny() { let mut state0 = prep_state(&["-iW"], &["Z"]); - let (meas0, determined0) = state0.mny_determine(0); - assert!(determined0); - assert!(!meas0); + let r0 = state0.mny(0); + assert!(r0.is_deterministic); + assert!(!r0.outcome); let mut state1 = prep_state(&["iW"], &["Z"]); - let (meas1, determined1) = state1.mny_determine(0); - assert!(determined1); - assert!(meas1); + let r1 = state1.mny(0); + assert!(r1.is_deterministic); + assert!(r1.outcome); } #[test] fn test_nondeterministic_mz() { let mut state = prep_state(&["X"], &["Z"]); - let (_meas, determined) = state.mz_determine(0); - assert!(!determined); + let r = state.mz(0); + assert!(!r.is_deterministic); } #[test] fn test_deterministic_mz() { let mut state0 = prep_state(&["Z"], &["X"]); - let (meas0, determined0) = state0.mz_determine(0); - assert!(determined0); - assert!(!meas0); + let r0 = state0.mz(0); + assert!(r0.is_deterministic); + assert!(!r0.outcome); let mut state1 = prep_state(&["-Z"], &["X"]); - let (meas1, determined1) = state1.mz_determine(0); - assert!(determined1); - assert!(meas1); + let r1 = state1.mz(0); + assert!(r1.is_deterministic); + assert!(r1.outcome); } #[test] fn test_nondeterministic_mnz() { let mut state = prep_state(&["X"], &["Z"]); - let (_meas, determined) = state.mnz_determine(0); - assert!(!determined); + let r = state.mnz(0); + assert!(!r.is_deterministic); } #[test] fn test_deterministic_mnz() { let mut state0 = prep_state(&["Z"], &["X"]); - let (meas0, determined0) = state0.mnz_determine(0); - assert!(determined0); - assert!(meas0); + let r0 = state0.mnz(0); + assert!(r0.is_deterministic); + assert!(r0.outcome); let mut state1 = prep_state(&["-Z"], &["X"]); - let (meas1, determined1) = state1.mnz_determine(0); - assert!(determined1); - assert!(!meas1); + let r1 = state1.mnz(0); + assert!(r1.is_deterministic); + assert!(!r1.outcome); } #[test] @@ -2240,13 +2098,12 @@ mod tests { fn one_bit_z_teleport( mut state: SparseStab, u32>, ) -> (SparseStab, u32>, bool) { - state.cx(1, 0); - state.h(1); - let (m1, d1) = state.mz_determine(1); - if m1 { + state.cx(1, 0).h(1); + let r1 = state.mz(1); + if r1.outcome { state.z(0); } - (state, d1) + (state, r1.is_deterministic) } /// Test one-bit Z teleportation of |+X> @@ -2261,11 +2118,11 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // X basis meas state.h(0); - let (m0, d0) = state.mz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.mz(0); + let m0_int = u8::from(r0.outcome); assert_eq!(m0_int, 0); // |+> -> 0 == false assert!(!d1); // Not deterministic - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic } } @@ -2282,11 +2139,11 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // X basis meas state.h(0); - let (m0, d0) = state.mz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.mz(0); + let m0_int = u8::from(r0.outcome); assert_eq!(m0_int, 1); // |-> -> 1 == true assert!(!d1); // Not deterministic - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic } } @@ -2302,11 +2159,11 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // Y basis meas state.sx(0); // Y -> Z - let (m0, d0) = state.mz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.mz(0); + let m0_int = u8::from(r0.outcome); assert_eq!(m0_int, 0); // |+X> -> 0 == false assert!(!d1); // Not deterministic - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic } } @@ -2323,11 +2180,11 @@ mod tests { (state, d1) = one_bit_z_teleport(state); // Y basis meas state.sx(0); // Y -> Z - let (m0, d0) = state.mz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.mz(0); + let m0_int = u8::from(r0.outcome); assert_eq!(m0_int, 1); // |-Y> -> 1 == true assert!(!d1); // Not deterministic - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic } } @@ -2341,11 +2198,11 @@ mod tests { let mut state: SparseStab, u32> = SparseStab::new(2); // Set input to |0> (state, d1) = one_bit_z_teleport(state); - let (m0, d0) = state.mz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.mz(0); + let m0_int = u8::from(r0.outcome); assert_eq!(m0_int, 0); // |0> assert!(!d1); // Not deterministic - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic } } @@ -2359,11 +2216,11 @@ mod tests { let mut state: SparseStab, u32> = SparseStab::new(2); state.x(1); // Set input to |1> (state, d1) = one_bit_z_teleport(state); - let (m0, d0) = state.mz_determine(0); - let m0_int = u8::from(m0); + let r0 = state.mz(0); + let m0_int = u8::from(r0.outcome); assert_eq!(m0_int, 1); // |1> -> 1 == true assert!(!d1); // Not deterministic - assert!(d0); // Deterministic + assert!(r0.is_deterministic); // Deterministic } } @@ -2380,15 +2237,15 @@ mod tests { state.cx(1, 2); state.cx(0, 1); state.h(0); - let (m0, d0) = state.mz_determine(0); - let (m1, d1) = state.mz_determine(1); - if m1 { + let r0 = state.mz(0); + let r1 = state.mz(1); + if r1.outcome { state.x(2); } - if m0 { + if r0.outcome { state.z(2); } - (state, d0, d1) + (state, r0.is_deterministic, r1.is_deterministic) } #[test] @@ -2400,12 +2257,12 @@ mod tests { state.h(0); (state, d0, d1) = teleport(state); state.h(2); - let (m2, d2) = state.mz_determine(2); - let m2_int = u8::from(m2); + let r2 = state.mz(2); + let m2_int = u8::from(r2.outcome); assert_eq!(m2_int, 0); assert!(!d0); assert!(!d1); - assert!(d2); + assert!(r2.is_deterministic); } } @@ -2419,13 +2276,13 @@ mod tests { state.h(0); (state, d0, d1) = teleport(state); state.h(2); - let (m2, d2) = state.mz_determine(2); - let m2_int = u8::from(m2); + let r2 = state.mz(2); + let m2_int = u8::from(r2.outcome); assert_eq!(m2_int, 1); assert!(!d0); assert!(!d1); - assert!(d2); + assert!(r2.is_deterministic); } } @@ -2438,12 +2295,12 @@ mod tests { state.sxdg(0); (state, d0, d1) = teleport(state); state.sx(2); - let (m2, d2) = state.mz_determine(2); - let m2_int = u8::from(m2); + let r2 = state.mz(2); + let m2_int = u8::from(r2.outcome); assert_eq!(m2_int, 0); assert!(!d0); assert!(!d1); - assert!(d2); + assert!(r2.is_deterministic); } } @@ -2457,12 +2314,12 @@ mod tests { state.sxdg(0); (state, d0, d1) = teleport(state); state.sx(2); - let (m2, d2) = state.mz_determine(2); - let m2_int = u8::from(m2); + let r2 = state.mz(2); + let m2_int = u8::from(r2.outcome); assert_eq!(m2_int, 1); assert!(!d0); assert!(!d1); - assert!(d2); + assert!(r2.is_deterministic); } } @@ -2473,13 +2330,13 @@ mod tests { let d1; let mut state: SparseStab, u32> = SparseStab::new(3); (state, d0, d1) = teleport(state); - let (m2, d2) = state.mz_determine(2); - let m2_int = u8::from(m2); + let r2 = state.mz(2); + let m2_int = u8::from(r2.outcome); assert_eq!(m2_int, 0); assert!(!d0); assert!(!d1); - assert!(d2); + assert!(r2.is_deterministic); } } @@ -2491,13 +2348,13 @@ mod tests { let mut state: SparseStab, u32> = SparseStab::new(3); state.x(0); // input state |-Z> (state, d0, d1) = teleport(state); - let (m2, d2) = state.mz_determine(2); - let m2_int = u8::from(m2); + let r2 = state.mz(2); + let m2_int = u8::from(r2.outcome); assert_eq!(m2_int, 1); assert!(!d0); assert!(!d1); - assert!(d2); + assert!(r2.is_deterministic); } } diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index ba78d13a..eb349ec3 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -11,7 +11,7 @@ // the License. use super::arbitrary_rotation_gateable::ArbitraryRotationGateable; -use super::clifford_gateable::CliffordGateable; +use super::clifford_gateable::{CliffordGateable, MeasurementResult}; use super::quantum_simulator_state::QuantumSimulatorState; use num_complex::Complex64; @@ -165,7 +165,12 @@ impl StateVec { /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 #[inline] - pub fn two_qubit_unitary(&mut self, qubit1: usize, qubit2: usize, matrix: [[Complex64; 4]; 4]) { + pub fn two_qubit_unitary( + &mut self, + qubit1: usize, + qubit2: usize, + matrix: [[Complex64; 4]; 4], + ) -> &mut Self { assert!(qubit1 < self.num_qubits); assert!(qubit2 < self.num_qubits); assert_ne!(qubit1, qubit2); @@ -215,6 +220,7 @@ impl StateVec { + matrix[3][3] * a11; } } + self } } @@ -435,7 +441,7 @@ impl CliffordGateable for StateVec { /// /// Panics if target qubit index is >= number of qubits #[inline] - fn mz(&mut self, target: usize) -> bool { + fn mz(&mut self, target: usize) -> MeasurementResult { assert!(target < self.num_qubits); let mut rng = rand::thread_rng(); @@ -469,7 +475,10 @@ impl CliffordGateable for StateVec { *amp *= norm_inv; } - result != 0 + MeasurementResult { + outcome: result != 0, + is_deterministic: false, + } } } @@ -598,7 +607,7 @@ impl ArbitraryRotationGateable for StateVec { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); - let neg_i_sin = Complex64::new(0.0, -sin); + let i_sin = Complex64::new(0.0, sin); // Changed name and sign let (q1, q2) = if qubit1 < qubit2 { (qubit1, qubit2) @@ -606,25 +615,24 @@ impl ArbitraryRotationGateable for StateVec { (qubit2, qubit1) }; - for i in 0..self.state.len() { - let bit1 = (i >> q1) & 1; - let bit2 = (i >> q2) & 1; - - if bit1 == 0 && bit2 == 0 { - let i01 = i ^ (1 << q2); - let i10 = i ^ (1 << q1); - let i11 = i ^ (1 << q1) ^ (1 << q2); + let step1 = 1 << q1; + let step2 = 1 << q2; + for i in (0..self.state.len()).step_by(2 * step2) { + for j in (i..i + step2).step_by(2 * step1) { + let i00 = j; + let i01 = j ^ step2; + let i10 = j ^ step1; + let i11 = i10 ^ step2; - let a00 = self.state[i]; + let a00 = self.state[i00]; let a01 = self.state[i01]; let a10 = self.state[i10]; let a11 = self.state[i11]; - // YY has an extra minus sign compared to XX when acting on |01⟩ and |10⟩ - self.state[i] = cos * a00 + neg_i_sin * a11; - self.state[i01] = cos * a01 - neg_i_sin * a10; - self.state[i10] = cos * a10 - neg_i_sin * a01; - self.state[i11] = cos * a11 + neg_i_sin * a00; + self.state[i00] = cos * a00 - i_sin * a11; + self.state[i01] = cos * a01 - i_sin * a10; + self.state[i10] = cos * a10 - i_sin * a01; + self.state[i11] = cos * a11 - i_sin * a00; } } self @@ -632,6 +640,15 @@ impl ArbitraryRotationGateable for StateVec { /// Apply RZZ(θ) = exp(-i θ ZZ/2) gate /// + /// This is an optimized implementation of the general two-qubit unitary: + /// ```text + /// RZZ(θ) = [[e^(-iθ/2), 0, 0, 0 ], + /// [0, e^(iθ/2), 0, 0 ], + /// [0, 0, e^(iθ/2), 0 ], + /// [0, 0, 0, e^(-iθ/2) ]] + /// ``` + /// Optimized by taking advantage of the diagonal structure. + /// /// # Panics /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 @@ -664,7 +681,7 @@ impl ArbitraryRotationGateable for StateVec { #[cfg(test)] mod tests { use super::*; - use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, PI, TAU}; + use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, FRAC_PI_6, PI, TAU}; #[test] fn test_new_state() { @@ -1006,28 +1023,95 @@ mod tests { #[test] fn test_ryy() { - // Test 1: RYY(π/2) on |00⟩ should give (|00⟩ - i|11⟩)/√2 + let expected = FRAC_1_SQRT_2; + + // Test all basis states for RYY(π/2) + // |00⟩ -> (1/√2)|00⟩ - i(1/√2)|11⟩ let mut q = StateVec::new(2); q.ryy(FRAC_PI_2, 0, 1); - - let expected = FRAC_1_SQRT_2; assert!((q.state[0].re - expected).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); assert!(q.state[2].norm() < 1e-10); assert!((q.state[3].im + expected).abs() < 1e-10); - // Test 2: RYY(2π) should return to original state up to global phase + // |11⟩ -> (1/√2)|11⟩ - i(1/√2)|00⟩ let mut q = StateVec::new(2); - q.h(0); // Create some initial state + q.x(0).x(1); // Prepare |11⟩ + q.ryy(FRAC_PI_2, 0, 1); + assert!((q.state[0].im + expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3].re - expected).abs() < 1e-10); + + // |01⟩ -> (1/√2)|01⟩ + i(1/√2)|10⟩ + let mut q = StateVec::new(2); + q.x(1); // Prepare |01⟩ + q.ryy(FRAC_PI_2, 0, 1); + assert!(q.state[0].norm() < 1e-10); + assert!(q.state[1].re.abs() < 1e-10); + assert!((q.state[1].im + expected).abs() < 1e-10); + assert!((q.state[2].re - expected).abs() < 1e-10); + assert!(q.state[2].im.abs() < 1e-10); + assert!(q.state[3].norm() < 1e-10); + + // |10⟩ -> (1/√2)|10⟩ + i(1/√2)|01⟩ + let mut q = StateVec::new(2); + q.x(0); // Prepare |10⟩ + q.ryy(FRAC_PI_2, 0, 1); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - expected).abs() < 1e-10); + assert!(q.state[1].im.abs() < 1e-10); + assert!(q.state[2].re.abs() < 1e-10); + assert!((q.state[2].im + expected).abs() < 1e-10); + assert!(q.state[3].norm() < 1e-10); + + // Test properties + + // 1. Periodicity: RYY(2π) = I + let mut q = StateVec::new(2); + q.h(0); // Create non-trivial initial state let initial = q.state.clone(); q.ryy(TAU, 0, 1); - + // Need to account for potential global phase if q.state[0].norm() > 1e-10 { let phase = q.state[0] / initial[0]; for (a, b) in q.state.iter().zip(initial.iter()) { - assert!((a - b * phase).norm() < 1e-10); + assert!( + (a - b * phase).norm() < 1e-10, + "Periodicity test failed: a={a}, b={b}" + ); } } + + // 2. Composition: RYY(θ₁)RYY(θ₂) = RYY(θ₁ + θ₂) + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + q1.h(0); // Create non-trivial initial state + q2.h(0); // Same initial state + q1.ryy(FRAC_PI_3, 0, 1).ryy(FRAC_PI_6, 0, 1); + q2.ryy(FRAC_PI_2, 0, 1); + // Compare up to global phase + if q1.state[0].norm() > 1e-10 { + let phase = q1.state[0] / q2.state[0]; + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!( + (a - b * phase).norm() < 1e-10, + "Composition test failed: a={a}, b={b}" + ); + } + } + + // 3. Symmetry: RYY(θ,0,1) = RYY(θ,1,0) + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + q1.h(0).h(1); // Create non-trivial initial state + q2.h(0).h(1); // Same initial state + q1.ryy(FRAC_PI_3, 0, 1); + q2.ryy(FRAC_PI_3, 1, 0); + // States should be exactly equal (no phase difference) + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10, "Symmetry test failed: a={a}, b={b}"); + } } #[test] @@ -1122,7 +1206,7 @@ mod tests { // Test 1: Measuring |0> state let mut q = StateVec::new(1); let result = q.mz(0); - assert!(!result); + assert!(!result.outcome); assert!((q.state[0].re - 1.0).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); @@ -1130,7 +1214,7 @@ mod tests { let mut q = StateVec::new(1); q.x(0); let result = q.mz(0); - assert!(result); + assert!(result.outcome); assert!(q.state[0].norm() < 1e-10); assert!((q.state[1].re - 1.0).abs() < 1e-10); @@ -1142,7 +1226,7 @@ mod tests { let mut q = StateVec::new(1); q.h(0); let result = q.mz(0); - if !result { + if !result.outcome { zeros += 1; } } @@ -1160,7 +1244,7 @@ mod tests { let result1 = q.mz(0); // Measure second qubit - should match first let result2 = q.mz(1); - assert_eq!(result1, result2); + assert_eq!(result1.outcome, result2.outcome); } #[test] @@ -1673,8 +1757,8 @@ mod tests { let result1 = q.mz(0); let result2 = q.mz(0); - assert!(result1); - assert!(result2); + assert!(result1.outcome); + assert!(result2.outcome); } #[test] @@ -1691,6 +1775,6 @@ mod tests { // Measure the second qubit - should match the first let result2 = q.mz(1); - assert_eq!(result1, result2); + assert_eq!(result1.outcome, result2.outcome); } } From f2e45d04d06c235b79400b06a072feabdd1fea0b Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 21 Dec 2024 02:30:31 -0700 Subject: [PATCH 11/33] small tweaks --- .../src/arbitrary_rotation_gateable.rs | 12 ++++ crates/pecos-qsim/src/clifford_gateable.rs | 3 + crates/pecos-qsim/src/gens.rs | 3 +- crates/pecos-qsim/src/sparse_stab.rs | 3 + crates/pecos-qsim/src/state_vec.rs | 70 +++++++++---------- 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs index e94921b2..b0757dee 100644 --- a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -12,17 +12,29 @@ use crate::CliffordGateable; use pecos_core::IndexableElement; +use std::f64::consts::PI; pub trait ArbitraryRotationGateable: CliffordGateable { fn rx(&mut self, theta: f64, q: T) -> &mut Self; + #[inline] fn ry(&mut self, theta: f64, q: T) -> &mut Self { self.sz(q).rx(theta, q).szdg(q) } fn rz(&mut self, theta: f64, q: T) -> &mut Self; + fn u(&mut self, theta: f64, phi: f64, lambda: f64, q: T) -> &mut Self; + + #[inline] + fn r1xy(&mut self, theta: f64, phi: f64, q: T) -> &mut Self { + self.rz(-phi + PI, q).rx(theta, q).rz(phi - PI, q) + } + + #[inline] fn rxx(&mut self, theta: f64, q1: T, q2: T) -> &mut Self { self.h(q1).h(q2).rzz(theta, q1, q2).h(q1).h(q2) } + + #[inline] fn ryy(&mut self, theta: f64, q1: T, q2: T) -> &mut Self { self.sx(q1).sx(q2).rzz(theta, q1, q2).sxdg(q1).sxdg(q2) } diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index c9f0d8c9..b7649e8c 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -131,6 +131,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// let mut sim = StdSparseStab::new(1); /// sim.x(0); // Apply X gate to qubit 0 /// ``` + #[inline] fn x(&mut self, q: T) -> &mut Self { self.h(q).z(q).h(q) } @@ -154,6 +155,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// let mut sim = StdSparseStab::new(1); /// sim.y(0); // Apply Y gate to qubit 0 /// ``` + #[inline] fn y(&mut self, q: T) -> &mut Self { self.z(q).x(q) } @@ -177,6 +179,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// let mut sim = StdSparseStab::new(1); /// sim.z(0); // Apply X gate to qubit 0 /// ``` + #[inline] fn z(&mut self, q: T) -> &mut Self { self.sz(q).sz(q); self diff --git a/crates/pecos-qsim/src/gens.rs b/crates/pecos-qsim/src/gens.rs index 377f3652..95b1637d 100644 --- a/crates/pecos-qsim/src/gens.rs +++ b/crates/pecos-qsim/src/gens.rs @@ -52,12 +52,13 @@ where } } + #[inline] pub fn get_num_qubits(&self) -> usize { self.num_qubits } #[inline] - pub fn clear(&mut self) { + fn clear(&mut self) { self.col_x.clear(); self.col_z.clear(); self.row_x.clear(); diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index e45c7562..08ad2769 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -171,6 +171,7 @@ where } /// Utility that creates a string for the Pauli generates of a `Gens`. + #[inline] fn tableau_string(num_qubits: usize, gens: &Gens) -> String { // TODO: calculate signs so we are really doing Y and not W let mut result = @@ -766,6 +767,7 @@ mod tests { } } + #[inline] fn check_state(state: &SparseStab, u32>, stabs: &[&str], destabs: &[&str]) { check_matrix(stabs, &state.stabs); check_matrix(destabs, &state.destabs); @@ -773,6 +775,7 @@ mod tests { // TODO: Add matrix verification func } + #[inline] fn split_pauli(pauli_str: &str) -> (usize, &str, &str) { let (phase, pauli_str) = if pauli_str.contains("+i") || pauli_str.contains("-i") { pauli_str.split_at(2) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index eb349ec3..f2d48bb7 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -134,27 +134,6 @@ impl StateVec { self } - /// Apply U3(theta, phi, lambda) gate - /// U3 = [[cos(θ/2), -e^(iλ)sin(θ/2)], - /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] - /// - /// # Panics - /// - /// Panics if target qubit index is >= number of qubits - #[inline] - pub fn u3(&mut self, target: usize, theta: f64, phi: f64, lambda: f64) -> &mut Self { - let cos = (theta / 2.0).cos(); - let sin = (theta / 2.0).sin(); - - // Calculate matrix elements - let u00 = Complex64::new(cos, 0.0); - let u01 = -Complex64::from_polar(sin, lambda); - let u10 = Complex64::from_polar(sin, phi); - let u11 = Complex64::from_polar(cos, phi + lambda); - - self.single_qubit_rotation(target, u00, u01, u10, u11) - } - /// Apply a general two-qubit unitary given by a 4x4 complex matrix /// U = [[u00, u01, u02, u03], /// [u10, u11, u12, u13], @@ -547,6 +526,27 @@ impl ArbitraryRotationGateable for StateVec { ) } + /// Apply U(theta, phi, lambda) gate + /// U1_3 = [[cos(θ/2), -e^(iλ)sin(θ/2)], + /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] + /// + /// # Panics + /// + /// Panics if target qubit index is >= number of qubits + #[inline] + fn u(&mut self, theta: f64, phi: f64, lambda: f64, target: usize) -> &mut Self { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + + // Calculate matrix elements + let u00 = Complex64::new(cos, 0.0); + let u01 = -Complex64::from_polar(sin, lambda); + let u10 = Complex64::from_polar(sin, phi); + let u11 = Complex64::from_polar(cos, phi + lambda); + + self.single_qubit_rotation(target, u00, u01, u10, u11) + } + /// Apply RXX(θ) = exp(-i θ XX/2) gate /// This implements evolution under the XX coupling between two qubits /// @@ -1354,44 +1354,44 @@ mod tests { } #[test] - fn test_u3_special_cases() { - // Test 1: U3(π, 0, π) should be X gate + fn test_u_special_cases() { + // Test 1: U(π, 0, π) should be X gate let mut q = StateVec::new(1); - q.u3(0, PI, 0.0, PI); + q.u(PI, 0.0, PI, 0); assert!(q.state[0].norm() < 1e-10); assert!((q.state[1].re - 1.0).abs() < 1e-10); // Test 2: Hadamard gate - // H = U3(π/2, 0, π) + // H = U(π/2, 0, π) let mut q = StateVec::new(1); - q.u3(0, PI / 2.0, 0.0, PI); + q.u(PI / 2.0, 0.0, PI, 0); assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - // Test 3: U3(0, 0, π) should be Z gate + // Test 3: U(0, 0, π) should be Z gate let mut q = StateVec::new(1); q.h(0); // First put in superposition let initial = q.state.clone(); - q.u3(0, 0.0, 0.0, PI); + q.u(0.0, 0.0, PI, 0); assert!((q.state[0] - initial[0]).norm() < 1e-10); assert!((q.state[1] + initial[1]).norm() < 1e-10); // Additional test: U3(π/2, π/2, -π/2) should be S†H let mut q = StateVec::new(1); - q.u3(0, PI / 2.0, PI / 2.0, -PI / 2.0); + q.u(PI / 2.0, PI / 2.0, -PI / 2.0, 0); // This creates the state (|0⟩ + i|1⟩)/√2 assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); assert!((q.state[1].im - FRAC_1_SQRT_2).abs() < 1e-10); } #[test] - fn test_u3_composition() { + fn test_u_composition() { let mut q1 = StateVec::new(1); let q2 = StateVec::new(1); - // Two U3 gates that should multiply to identity - q1.u3(0, PI / 3.0, PI / 4.0, PI / 6.0); - q1.u3(0, -PI / 3.0, -PI / 6.0, -PI / 4.0); + // Two U gates that should multiply to identity + q1.u(PI / 3.0, PI / 4.0, PI / 6.0, 0); + q1.u(-PI / 3.0, -PI / 6.0, -PI / 4.0, 0); // Compare with initial state for (a, b) in q1.state.iter().zip(q2.state.iter()) { @@ -1400,14 +1400,14 @@ mod tests { } #[test] - fn test_u3_arbitrary() { + fn test_u_arbitrary() { let mut q = StateVec::new(1); // Apply some arbitrary rotation let theta = PI / 5.0; let phi = PI / 7.0; let lambda = PI / 3.0; - q.u3(0, theta, phi, lambda); + q.u(theta, phi, lambda, 0); // Verify normalization is preserved let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); From c3dd2adb16a30cbb22e9a75e9b35f3bb42f3eafa Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 21 Dec 2024 12:41:24 -0700 Subject: [PATCH 12/33] Improved testing of RX, RY, RZ. --- crates/pecos-qsim/src/state_vec.rs | 367 ++++++++++++++++++++++++++--- 1 file changed, 328 insertions(+), 39 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index f2d48bb7..24a7ffae 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -473,14 +473,12 @@ impl ArbitraryRotationGateable for StateVec { fn rx(&mut self, theta: f64, target: usize) -> &mut Self { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); - let neg_i_sin = Complex64::new(0.0, -sin); - self.single_qubit_rotation( target, - Complex64::new(cos, 0.0), // u00 - neg_i_sin, // u01 - neg_i_sin, // u10 - Complex64::new(cos, 0.0), // u11 + Complex64::new(cos, 0.0), + Complex64::new(0.0, -sin), + Complex64::new(0.0, -sin), + Complex64::new(cos, 0.0), ) } @@ -495,13 +493,12 @@ impl ArbitraryRotationGateable for StateVec { fn ry(&mut self, theta: f64, target: usize) -> &mut Self { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); - self.single_qubit_rotation( target, - Complex64::new(cos, 0.0), // u00 - Complex64::new(-sin, 0.0), // u01 - Complex64::new(sin, 0.0), // u10 - Complex64::new(cos, 0.0), // u11 + Complex64::new(cos, 0.0), + Complex64::new(-sin, 0.0), + Complex64::new(sin, 0.0), + Complex64::new(cos, 0.0), ) } @@ -514,20 +511,20 @@ impl ArbitraryRotationGateable for StateVec { /// Panics if target qubit index is >= number of qubits #[inline] fn rz(&mut self, theta: f64, target: usize) -> &mut Self { - let exp_minus_i_theta_2 = Complex64::from_polar(1.0, -theta / 2.0); - let exp_plus_i_theta_2 = Complex64::from_polar(1.0, theta / 2.0); + let e_pos = Complex64::from_polar(1.0, -theta / 2.0); + let e_neg = Complex64::from_polar(1.0, theta / 2.0); self.single_qubit_rotation( target, - exp_minus_i_theta_2, // u00 - Complex64::new(0.0, 0.0), // u01 - Complex64::new(0.0, 0.0), // u10 - exp_plus_i_theta_2, // u11 + e_pos, + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + e_neg, ) } /// Apply U(theta, phi, lambda) gate - /// U1_3 = [[cos(θ/2), -e^(iλ)sin(θ/2)], + /// `U1_3` = [[cos(θ/2), -e^(iλ)sin(θ/2)], /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] /// /// # Panics @@ -681,7 +678,9 @@ impl ArbitraryRotationGateable for StateVec { #[cfg(test)] mod tests { use super::*; - use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, FRAC_PI_6, PI, TAU}; + use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, PI, TAU}; + + use num_complex::Complex64; // Ensure the correct import for Complex64 #[test] fn test_new_state() { @@ -712,16 +711,15 @@ mod tests { fn test_ry() { let mut q = StateVec::new(1); - // RY(π) should be equivalent to X up to global phase + // RY(π) should flip |0⟩ to |1⟩ q.ry(PI, 0); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].norm() - 1.0).abs() < 1e-10); + assert!(q.state[0].norm() < 1e-10); // Close to zero + assert!((q.state[1].norm() - 1.0).abs() < 1e-10); // Magnitude 1 for |1⟩ - // RY(2π) should return to initial state up to global phase - let mut q = StateVec::new(1); - q.ry(2.0 * PI, 0); - assert!((q.state[0].norm() - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); + // Two RY(π) rotations should return to the initial state + q.ry(PI, 0); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); // Magnitude 1 for |0⟩ + assert!(q.state[1].norm() < 1e-10); // Close to zero } #[test] @@ -729,30 +727,321 @@ mod tests { let mut q = StateVec::new(1); // RZ should only add phases, not change probabilities - q.h(0); // Put in superposition first + q.h(0); // Put qubit in superposition let probs_before: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); - q.rz(FRAC_PI_2, 0); + q.rz(FRAC_PI_2, 0); // Rotate Z by π/2 let probs_after: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); - // Probabilities should remain unchanged for (p1, p2) in probs_before.iter().zip(probs_after.iter()) { - assert!((p1 - p2).abs() < 1e-10); + assert!((p1 - p2).abs() < 1e-10); // Probabilities unchanged } + } - // RZ(2π) should return to initial state up to global phase + #[test] + fn test_rx_step_by_step() { + // Step 1: RX(0) should be identity let mut q = StateVec::new(1); - q.h(0); - let state_before = q.state.clone(); - q.rz(TAU, 0); + q.rx(0.0, 0); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Step 2: RX(π) on |0⟩ should give -i|1⟩ + let mut q = StateVec::new(1); + q.rx(PI, 0); + println!("RX(π)|0⟩ = {:?}", q.state); // Debug output + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].im + 1.0).abs() < 1e-10); + + // Step 3: RX(π/2) on |0⟩ should give (|0⟩ - i|1⟩)/√2 + let mut q = StateVec::new(1); + q.rx(FRAC_PI_2, 0); + println!("RX(π/2)|0⟩ = {:?}", q.state); // Debug output + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state[0].re - expected_amp).abs() < 1e-10); + assert!((q.state[1].im + expected_amp).abs() < 1e-10); + } + + #[test] + fn test_ry_step_by_step() { + // Step 1: RY(0) should be identity + let mut q = StateVec::new(1); + q.ry(0.0, 0); + println!("RY(0)|0⟩ = {:?}", q.state); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Step 2: RY(π) on |0⟩ should give |1⟩ + let mut q = StateVec::new(1); + q.ry(PI, 0); + println!("RY(π)|0⟩ = {:?}", q.state); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - 1.0).abs() < 1e-10); + + // Step 3: RY(π/2) on |0⟩ should give (|0⟩ + |1⟩)/√2 + let mut q = StateVec::new(1); + q.ry(FRAC_PI_2, 0); + println!("RY(π/2)|0⟩ = {:?}", q.state); + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state[0].re - expected_amp).abs() < 1e-10); + assert!((q.state[1].re - expected_amp).abs() < 1e-10); + + // Step 4: RY(-π/2) on |0⟩ should give (|0⟩ - |1⟩)/√2 + let mut q = StateVec::new(1); + q.ry(-FRAC_PI_2, 0); + println!("RY(-π/2)|0⟩ = {:?}", q.state); + assert!((q.state[0].re - expected_amp).abs() < 1e-10); + assert!((q.state[1].re + expected_amp).abs() < 1e-10); + } + + #[test] + fn test_rz_step_by_step() { + // Step 1: RZ(0) should be identity + let mut q = StateVec::new(1); + q.rz(0.0, 0); + println!("RZ(0)|0⟩ = {:?}", q.state); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Step 2: RZ(π/2) on |+⟩ should give |+i⟩ = (|0⟩ + i|1⟩)/√2 + let mut q = StateVec::new(1); + q.h(0); // Create |+⟩ + q.rz(FRAC_PI_2, 0); + println!("RZ(π/2)|+⟩ = {:?}", q.state); + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state[0].norm() - expected_amp).abs() < 1e-10); + assert!((q.state[1].norm() - expected_amp).abs() < 1e-10); + // Check relative phase + let ratio = q.state[1] / q.state[0]; + println!("Relative phase ratio = {ratio:?}"); + assert!( + (ratio.im - 1.0).abs() < 1e-10, + "Relative phase incorrect: ratio = {ratio}" + ); + assert!( + ratio.re.abs() < 1e-10, + "Relative phase has unexpected real component: {}", + ratio.re + ); + + // Step 3: Two RZ(π/2) operations should equal one RZ(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.rz(PI, 0); + q2.rz(FRAC_PI_2, 0); + q2.rz(FRAC_PI_2, 0); + println!("RZ(π)|0⟩ vs RZ(π/2)RZ(π/2)|0⟩:"); + println!("q1 = {:?}", q1.state); + println!("q2 = {:?}", q2.state); + let ratio = q2.state[0] / q1.state[0]; + let phase = ratio.arg(); + println!("Phase difference between q2 and q1: {phase}"); + assert!( + (ratio.norm() - 1.0).abs() < 1e-10, + "Magnitudes differ: ratio = {ratio}" + ); + // Don't check exact phase, just verify states are equal up to global phase + assert!((q2.state[1] * q1.state[0] - q2.state[0] * q1.state[1]).norm() < 1e-10); + } + + #[test] + fn test_sq_rotation_commutation() { + // RX and RY don't commute - verify RX(θ)RY(φ) ≠ RY(φ)RX(θ) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + let theta = FRAC_PI_3; // π/3 + let phi = FRAC_PI_4; // π/4 + + // Apply in different orders + q1.rx(theta, 0).ry(phi, 0); + q2.ry(phi, 0).rx(theta, 0); + + println!("RY(π/4)RX(π/3)|0⟩ = {:?}", q1.state); + println!("RX(π/3)RY(π/4)|0⟩ = {:?}", q2.state); + + // States should be different - check they're not equal up to global phase + let ratio = q2.state[0] / q1.state[0]; + assert!((q2.state[1] / q1.state[1] - ratio).norm() > 1e-10); + } + + #[test] + fn test_sq_rotation_decompositions() { + // H = RZ(-π)RY(-π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); - // States should be identical up to global phase - let phase = q.state[0] / state_before[0]; - for (a, b) in q.state.iter().zip(state_before.iter()) { - assert!((a - b * phase).norm() < 1e-10); + println!("Initial states:"); + println!("q1 = {:?}", q1.state); + println!("q2 = {:?}", q2.state); + + q1.h(0); // Direct H + println!("After H: q1 = {:?}", q1.state); + + // H via rotations - changed order and added negative sign to RZ angle + q2.ry(-FRAC_PI_2, 0).rz(-PI, 0); + println!("After RZ(-π)RY(-π/2): q2 = {:?}", q2.state); + + // Compare up to global phase by looking at ratios between components + let ratio = q2.state[0] / q1.state[0]; + println!("Ratio = {ratio:?}"); + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + println!("Comparing {a} and {b}"); + assert!( + (a * ratio - b).norm() < 1e-10, + "States differ: {a} vs {b} (ratio: {ratio})" + ); + } + } + + // Helper function to compare states up to global phase + fn assert_states_equal(state1: &[Complex64], state2: &[Complex64]) { + if state1[0].norm() < 1e-10 && state2[0].norm() < 1e-10 { + // Both first components near zero, compare other components directly + for (a, b) in state1.iter().zip(state2.iter()) { + assert!( + (a.norm() - b.norm()).abs() < 1e-10, + "States differ in magnitude: {a} vs {b}" + ); + } + } else { + // Get phase from first non-zero component + let ratio = match state1 + .iter() + .zip(state2.iter()) + .find(|(a, b)| a.norm() > 1e-10 && b.norm() > 1e-10) + { + Some((a, b)) => b / a, + None => panic!("States have no corresponding non-zero components"), + }; + println!("Phase ratio between states: {ratio:?}"); + + for (a, b) in state1.iter().zip(state2.iter()) { + assert!( + (a * ratio - b).norm() < 1e-10, + "States differ: {a} vs {b} (ratio: {ratio})" + ); + } } } + #[test] + fn test_sq_standard_gate_decompositions() { + // Test S = RZ(π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.sz(0); + q2.rz(FRAC_PI_2, 0); + println!("S|0⟩ = {:?}", q1.state); + println!("RZ(π/2)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + + // Test X = RX(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.x(0); + q2.rx(PI, 0); + println!("X|0⟩ = {:?}", q1.state); + println!("RX(π)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + + // Test Y = RY(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.y(0); + q2.ry(PI, 0); + println!("Y|0⟩ = {:?}", q1.state); + println!("RY(π)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + + // Test Z = RZ(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.z(0); + q2.rz(PI, 0); + println!("Z|0⟩ = {:?}", q1.state); + println!("RZ(π)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + + // Test √X = RX(π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.sx(0); + q2.rx(FRAC_PI_2, 0); + println!("√X|0⟩ = {:?}", q1.state); + println!("RX(π/2)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + + // Test √Y = RY(π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.sy(0); + q2.ry(FRAC_PI_2, 0); + println!("√Y|0⟩ = {:?}", q1.state); + println!("RY(π/2)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + + // Test S = TT as RZ(π/4)RZ(π/4) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q2.rz(FRAC_PI_4, 0).rz(FRAC_PI_4, 0); + q1.sz(0); + println!("S|0⟩ = {:?}", q1.state); + println!("T²|0⟩ = RZ(π/4)RZ(π/4)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + + // Test H = RX(π)RY(π/2) decomposition + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.h(0); + q2.ry(FRAC_PI_2, 0).rx(PI, 0); + println!("H|0⟩ = {:?}", q1.state); + println!("RX(π)RY(π/2)|0⟩ = {:?}", q2.state); + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_rx_rotation_angle_relations() { + // Test that RX(θ)RX(-θ) = I + let mut q = StateVec::new(1); + let theta = FRAC_PI_3; + + // Apply forward then reverse rotations + q.rx(theta, 0).rx(-theta, 0); + + // Should get back to |0⟩ up to global phase + assert!(q.state[1].norm() < 1e-10); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); + } + + #[test] + fn test_ry_rotation_angle_relations() { + // Test that RY(θ)RY(-θ) = I + let mut q = StateVec::new(1); + let theta = FRAC_PI_3; + + // Apply forward then reverse rotations + q.ry(theta, 0).ry(-theta, 0); + + // Should get back to |0⟩ up to global phase + assert!(q.state[1].norm() < 1e-10); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); + } + + #[test] + fn test_rz_rotation_angle_relations() { + // Test that RZ(θ)RZ(-θ) = I + let mut q = StateVec::new(1); + let theta = FRAC_PI_3; + + // Apply forward then reverse rotations + q.rz(theta, 0).rz(-theta, 0); + + // Should get back to |0⟩ up to global phase + assert!(q.state[1].norm() < 1e-10); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); + } + #[test] fn test_hadamard() { let mut q = StateVec::new(1); From 27d60bcf15016329af03c7dced47678da9f79f50 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 21 Dec 2024 13:09:44 -0700 Subject: [PATCH 13/33] test doc tweaks --- crates/pecos-qsim/src/state_vec.rs | 145 ++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 42 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 24a7ffae..05b4ed2a 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -675,15 +675,74 @@ impl ArbitraryRotationGateable for StateVec { } } +/// ## Key Invariants +/// - **Normalization**: The total probability (norm squared) of the state vector +/// must always be 1. +/// - **Unitarity**: All gate operations must preserve the norm of the state vector. +/// - **Phase Consistency**: States are compared up to a global phase. +/// +/// ## Testing Strategy +/// - **Unit Tests**: Validate individual operations (e.g., `RX`, `RY`, `RZ`, `CX`, `CY`, etc.). +/// - **Compositional Tests**: Verify decompositions, commutation relations, and symmetry properties. +/// - **Edge Cases**: Test with boundary values (e.g., `θ = 0`, `θ = 2π`) and systems near memory limits. +/// - **Randomized Tests**: Evaluate probabilistic operations like measurement and ensure statistical validity. +/// - **Integration Tests**: Combine operations to ensure the overall system behaves as expected. +/// +/// ## Test Organization +/// - Each gate or operation is tested independently for correctness. +/// - Helper functions like `assert_states_equal` are used to compare quantum states up to global phase. +/// - Failures provide clear diagnostic outputs for debugging, including mismatches and intermediate states. #[cfg(test)] mod tests { use super::*; use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, PI, TAU}; - use num_complex::Complex64; // Ensure the correct import for Complex64 + use num_complex::Complex64; + + /// Compare two quantum states up to global phase. + /// + /// This function ensures that two state vectors represent the same quantum state, + /// accounting for potential differences in global phase. + /// + /// # Arguments + /// - `state1`: A reference to the first state vector. + /// - `state2`: A reference to the second state vector. + /// + /// # Panics + /// Panics if the states differ in norm or relative phase beyond a small numerical tolerance. + fn assert_states_equal(state1: &[Complex64], state2: &[Complex64]) { + if state1[0].norm() < 1e-10 && state2[0].norm() < 1e-10 { + // Both first components near zero, compare other components directly + for (a, b) in state1.iter().zip(state2.iter()) { + assert!( + (a.norm() - b.norm()).abs() < 1e-10, + "States differ in magnitude: {a} vs {b}" + ); + } + } else { + // Get phase from first non-zero component + let ratio = match state1 + .iter() + .zip(state2.iter()) + .find(|(a, b)| a.norm() > 1e-10 && b.norm() > 1e-10) + { + Some((a, b)) => b / a, + None => panic!("States have no corresponding non-zero components"), + }; + println!("Phase ratio between states: {ratio:?}"); + + for (a, b) in state1.iter().zip(state2.iter()) { + assert!( + (a * ratio - b).norm() < 1e-10, + "States differ: {a} vs {b} (ratio: {ratio})" + ); + } + } + } #[test] fn test_new_state() { + // Verify the initial state is correctly set to |0> let q = StateVec::new(2); assert_eq!(q.state[0], Complex64::new(1.0, 0.0)); for i in 1..4 { @@ -693,14 +752,15 @@ mod tests { #[test] fn test_rx() { + // Test RX gate functionality let mut q = StateVec::new(1); - // RX(π) should be equivalent to X up to global phase + // RX(π) should flip |0⟩ to -i|1⟩ q.rx(PI, 0); assert!(q.state[0].norm() < 1e-10); assert!((q.state[1].norm() - 1.0).abs() < 1e-10); - // RX(2π) should return to initial state up to global phase + // RX(2π) should return to the initial state up to global phase let mut q = StateVec::new(1); q.rx(2.0 * PI, 0); assert!((q.state[0].norm() - 1.0).abs() < 1e-10); @@ -740,8 +800,9 @@ mod tests { #[test] fn test_rx_step_by_step() { - // Step 1: RX(0) should be identity let mut q = StateVec::new(1); + + // Step 1: RX(0) should be identity q.rx(0.0, 0); assert!((q.state[0].re - 1.0).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); @@ -894,37 +955,6 @@ mod tests { } } - // Helper function to compare states up to global phase - fn assert_states_equal(state1: &[Complex64], state2: &[Complex64]) { - if state1[0].norm() < 1e-10 && state2[0].norm() < 1e-10 { - // Both first components near zero, compare other components directly - for (a, b) in state1.iter().zip(state2.iter()) { - assert!( - (a.norm() - b.norm()).abs() < 1e-10, - "States differ in magnitude: {a} vs {b}" - ); - } - } else { - // Get phase from first non-zero component - let ratio = match state1 - .iter() - .zip(state2.iter()) - .find(|(a, b)| a.norm() > 1e-10 && b.norm() > 1e-10) - { - Some((a, b)) => b / a, - None => panic!("States have no corresponding non-zero components"), - }; - println!("Phase ratio between states: {ratio:?}"); - - for (a, b) in state1.iter().zip(state2.iter()) { - assert!( - (a * ratio - b).norm() < 1e-10, - "States differ: {a} vs {b} (ratio: {ratio})" - ); - } - } - } - #[test] fn test_sq_standard_gate_decompositions() { // Test S = RZ(π/2) @@ -1614,6 +1644,36 @@ mod tests { assert!((q.state[1].im - 1.0).abs() < 1e-10); } + #[test] + fn test_sq_rotation_edge_cases() { + let mut q = StateVec::new(1); + + // Test RX(0): Should be identity + let initial = q.state.clone(); + q.rx(0.0, 0); + assert_states_equal(&q.state, &initial); + + // Test RX(2π): Should also be identity up to global phase + q.rx(2.0 * PI, 0); + assert_states_equal(&q.state, &initial); + + // Test RY(0): Should be identity + q.ry(0.0, 0); + assert_states_equal(&q.state, &initial); + + // Test RY(2π): Should also be identity up to global phase + q.ry(2.0 * PI, 0); + assert_states_equal(&q.state, &initial); + + // Test RZ(0): Should be identity + q.rz(0.0, 0); + assert_states_equal(&q.state, &initial); + + // Test RZ(2π): Should also be identity up to global phase + q.rz(2.0 * PI, 0); + assert_states_equal(&q.state, &initial); + } + #[test] fn test_unitary_properties() { let mut q = StateVec::new(1); @@ -1996,17 +2056,18 @@ mod tests { } #[test] - fn test_large_system_h() { - let num_qubits = 10; // 10 qubits => state vector size = 1024 + fn test_large_system() { + // Test with a large number of qubits to ensure robustness. + let num_qubits = 20; // 20 qubits => 2^20 amplitudes (~1M complex numbers) let mut q = StateVec::new(num_qubits); - // Apply Hadamard gate to the 0th qubit + // Apply Hadamard to the first qubit q.h(0); - // Check that |0...0> and |1...0> have equal amplitude - let expected_amp = 1.0 / 2.0_f64.sqrt(); - assert!((q.state[0].re - expected_amp).abs() < 1e-10); - assert!((q.state[1].re - expected_amp).abs() < 1e-10); + // Check normalization and amplitudes for |0...0> and |1...0> + let expected_amp = 1.0 / (2.0_f64.sqrt()); + assert!((q.state[0].norm() - expected_amp).abs() < 1e-10); + assert!((q.state[1].norm() - expected_amp).abs() < 1e-10); // Ensure all other amplitudes remain zero for i in 2..q.state.len() { From c7270614ccde91fa7c8c1010d590ef3829329f4b Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 21 Dec 2024 15:27:20 -0700 Subject: [PATCH 14/33] adding r1xy --- .../src/arbitrary_rotation_gateable.rs | 10 +- crates/pecos-qsim/src/lib.rs | 1 + crates/pecos-qsim/src/state_vec.rs | 207 +++++++++++++++++- 3 files changed, 206 insertions(+), 12 deletions(-) diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs index b0757dee..6d2948da 100644 --- a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -12,7 +12,7 @@ use crate::CliffordGateable; use pecos_core::IndexableElement; -use std::f64::consts::PI; +use std::f64::consts::FRAC_PI_2; pub trait ArbitraryRotationGateable: CliffordGateable { fn rx(&mut self, theta: f64, q: T) -> &mut Self; @@ -22,11 +22,15 @@ pub trait ArbitraryRotationGateable: CliffordGateable { } fn rz(&mut self, theta: f64, q: T) -> &mut Self; - fn u(&mut self, theta: f64, phi: f64, lambda: f64, q: T) -> &mut Self; + fn u(&mut self, theta: f64, phi: f64, lambda: f64, q: T) -> &mut Self { + self.rz(lambda, q).ry(theta, q).rz(phi, q) + } #[inline] fn r1xy(&mut self, theta: f64, phi: f64, q: T) -> &mut Self { - self.rz(-phi + PI, q).rx(theta, q).rz(phi - PI, q) + self.rz(-phi + FRAC_PI_2, q) + .ry(theta, q) + .rz(phi - FRAC_PI_2, q) } #[inline] diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index 338810e3..50bc704b 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -20,6 +20,7 @@ pub mod quantum_simulator_state; pub mod sparse_stab; pub mod state_vec; +pub use arbitrary_rotation_gateable::ArbitraryRotationGateable; pub use clifford_gateable::{CliffordGateable, MeasurementResult}; pub use gens::Gens; // pub use paulis::Paulis; diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 05b4ed2a..a3b3577b 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -544,6 +544,20 @@ impl ArbitraryRotationGateable for StateVec { self.single_qubit_rotation(target, u00, u01, u10, u11) } + fn r1xy(&mut self, theta: f64, phi: f64, target: usize) -> &mut Self { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); + + // Calculate the matrix elements + let r00 = Complex64::new(cos, 0.0); // cos(θ/2) + let r01 = -Complex64::new(0.0, sin) * Complex64::from_polar(1.0, -phi); // -i sin(θ/2) e^(-iφ) + let r10 = -Complex64::new(0.0, sin) * Complex64::from_polar(1.0, phi); // -i sin(θ/2) e^(iφ) + let r11 = Complex64::new(cos, 0.0); // cos(θ/2) + + // Apply the single-qubit rotation using the matrix elements + self.single_qubit_rotation(target, r00, r01, r10, r11) + } + /// Apply RXX(θ) = exp(-i θ XX/2) gate /// This implements evolution under the XX coupling between two qubits /// @@ -711,30 +725,33 @@ mod tests { /// # Panics /// Panics if the states differ in norm or relative phase beyond a small numerical tolerance. fn assert_states_equal(state1: &[Complex64], state2: &[Complex64]) { - if state1[0].norm() < 1e-10 && state2[0].norm() < 1e-10 { + const TOLERANCE: f64 = 1e-10; + + if state1[0].norm() < TOLERANCE && state2[0].norm() < TOLERANCE { // Both first components near zero, compare other components directly - for (a, b) in state1.iter().zip(state2.iter()) { + for (index, (a, b)) in state1.iter().zip(state2.iter()).enumerate() { assert!( - (a.norm() - b.norm()).abs() < 1e-10, - "States differ in magnitude: {a} vs {b}" + (a.norm() - b.norm()).abs() < TOLERANCE, + "States differ in magnitude at index {index}: {a} vs {b}" ); } } else { - // Get phase from first non-zero component + // Get phase from the first pair of non-zero components let ratio = match state1 .iter() .zip(state2.iter()) - .find(|(a, b)| a.norm() > 1e-10 && b.norm() > 1e-10) + .find(|(a, b)| a.norm() > TOLERANCE && b.norm() > TOLERANCE) { Some((a, b)) => b / a, None => panic!("States have no corresponding non-zero components"), }; println!("Phase ratio between states: {ratio:?}"); - for (a, b) in state1.iter().zip(state2.iter()) { + for (index, (a, b)) in state1.iter().zip(state2.iter()).enumerate() { assert!( - (a * ratio - b).norm() < 1e-10, - "States differ: {a} vs {b} (ratio: {ratio})" + (a * ratio - b).norm() < TOLERANCE, + "States differ at index {index}: {a} vs {b} (adjusted with ratio {ratio:?}), diff = {}", + (a * ratio - b).norm() ); } } @@ -1072,6 +1089,178 @@ mod tests { assert!((q.state[0].norm() - 1.0).abs() < 1e-10); } + #[test] + fn test_state_vec_u_vs_trait_u() { + // Initialize state vectors with one qubit in the |0⟩ state. + let mut state_vec_u = StateVec::new(1); + let mut trait_u = StateVec::new(1); + + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; + let lambda = FRAC_PI_6; + + // Apply `u` from the StateVec implementation. + state_vec_u.u(theta, phi, lambda, 0); + + // Apply `u` from the ArbitraryRotationGateable trait. + ArbitraryRotationGateable::u(&mut trait_u, theta, phi, lambda, 0); + + assert_states_equal(&state_vec_u.state, &trait_u.state); + } + + #[test] + fn test_r1xy_vs_trait_r1xy() { + // Initialize state vectors with one qubit in the |0⟩ state. + let mut state_vec_r1xy = StateVec::new(1); + let mut trait_r1xy = StateVec::new(1); + + // Define angles for the test. + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; + + // Apply the manual `r1xy` implementation. + state_vec_r1xy.r1xy(theta, phi, 0); + + // Apply the `r1xy` implementation from the `ArbitraryRotationGateable` trait. + ArbitraryRotationGateable::r1xy(&mut trait_r1xy, theta, phi, 0); + + // Use the `assert_states_equal` function to compare the states up to a global phase. + assert_states_equal(&state_vec_r1xy.state, &trait_r1xy.state); + } + + #[test] + fn test_r1xy_vs_u() { + let mut state_r1xy = StateVec::new(1); + let mut state_u = StateVec::new(1); + + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; + + // Apply r1xy and equivalent u gates + state_r1xy.r1xy(theta, phi, 0); + state_u.u(theta, phi - FRAC_PI_2, FRAC_PI_2 - phi, 0); + + assert_states_equal(&state_r1xy.state, &state_u.state); + } + + #[test] + fn test_rz_vs_u() { + let mut state_rz = StateVec::new(1); + let mut state_u = StateVec::new(1); + + let theta = FRAC_PI_3; + + // Apply rz and u gates + state_rz.rz(theta, 0); + state_u.u(0.0, 0.0, theta, 0); + + assert_states_equal(&state_rz.state, &state_u.state); + } + + #[test] + fn test_u_decomposition() { + let mut state_u = StateVec::new(1); + let mut state_decomposed = StateVec::new(1); + + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; + let lambda = FRAC_PI_6; + + // Apply U gate + state_u.u(theta, phi, lambda, 0); + + // Apply the decomposed gates + state_decomposed.rz(lambda, 0); + state_decomposed.r1xy(theta, FRAC_PI_2, 0); + state_decomposed.rz(phi, 0); + + // Assert that the states are equal + assert_states_equal(&state_u.state, &state_decomposed.state); + } + + #[test] + fn test_x_vs_r1xy() { + let mut state = StateVec::new(1); + state.x(0); + let state_after_x = state.clone(); + + state.reset(); + state.r1xy(PI, 0.0, 0); + let state_after_r1xy = state.clone(); + + assert_states_equal(&state_after_x.state, &state_after_r1xy.state); + } + + #[test] + fn test_y_vs_r1xy() { + let mut state = StateVec::new(1); + state.y(0); + let state_after_y = state.clone(); + + state.reset(); + state.r1xy(PI, FRAC_PI_2, 0); + let state_after_r1xy = state.clone(); + + assert_states_equal(&state_after_y.state, &state_after_r1xy.state); + } + + #[test] + fn test_h_vs_r1xy_rz() { + let mut state = StateVec::new(1); + state.h(0); // Apply the H gate + let state_after_h = state.clone(); + + state.reset(); // Reset state to |0⟩ + state.r1xy(FRAC_PI_2, -FRAC_PI_2, 0).rz(PI, 0); + let state_after_r1xy_rz = state.clone(); + + assert_states_equal(&state_after_h.state, &state_after_r1xy_rz.state); + } + + #[test] + fn test_cx_decomposition() { + let mut state_cx = StateVec::new(2); + let mut state_decomposed = StateVec::new(2); + + let control = 0; + let target = 1; + + // Apply CX gate + state_cx.cx(control, target); + + // Apply the decomposed gates + state_decomposed.r1xy(-FRAC_PI_2, FRAC_PI_2, target); + state_decomposed.rzz(FRAC_PI_2, control, target); + state_decomposed.rz(-FRAC_PI_2, control); + state_decomposed.r1xy(FRAC_PI_2, PI, target); + state_decomposed.rz(-FRAC_PI_2, target); + + // Assert that the states are equal + assert_states_equal(&state_cx.state, &state_decomposed.state); + } + + #[test] + fn test_rxx_decomposition() { + let mut state_rxx = StateVec::new(2); + let mut state_decomposed = StateVec::new(2); + + let control = 0; + let target = 1; + + // Apply RXX gate + state_rxx.rxx(FRAC_PI_4, control, target); + + // Apply the decomposed gates + state_decomposed.r1xy(FRAC_PI_2, FRAC_PI_2, control); + state_decomposed.r1xy(FRAC_PI_2, FRAC_PI_2, target); + state_decomposed.rzz(FRAC_PI_4, control, target); + state_decomposed.r1xy(FRAC_PI_2, -FRAC_PI_2, control); + state_decomposed.r1xy(FRAC_PI_2, -FRAC_PI_2, target); + + // Assert that the states are equal + assert_states_equal(&state_rxx.state, &state_decomposed.state); + } + #[test] fn test_hadamard() { let mut q = StateVec::new(1); From c989843c16544785b9d30ba6a385f6f0bd79b074 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 21 Dec 2024 16:35:00 -0700 Subject: [PATCH 15/33] Adding initial Rust state-vec Python bindings --- Cargo.lock | 2 + crates/pecos-python/Cargo.toml | 2 + crates/pecos-python/src/lib.rs | 6 +- crates/pecos-python/src/state_vec_bindings.rs | 270 ++++++++++++++++++ crates/pecos-qsim/src/state_vec.rs | 17 +- crates/pecos/src/prelude.rs | 9 +- .../src/pecos_rslib/rsstate_vec.py | 194 +++++++++++++ 7 files changed, 479 insertions(+), 21 deletions(-) create mode 100644 crates/pecos-python/src/state_vec_bindings.rs create mode 100644 python/pecos-rslib/src/pecos_rslib/rsstate_vec.py diff --git a/Cargo.lock b/Cargo.lock index 480cbf9a..62ee3837 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1181,8 +1181,10 @@ dependencies = [ name = "pecos-python" version = "0.1.1" dependencies = [ + "num-complex", "pecos", "pyo3", + "rand", ] [[package]] diff --git a/crates/pecos-python/Cargo.toml b/crates/pecos-python/Cargo.toml index 9df76358..533578e9 100644 --- a/crates/pecos-python/Cargo.toml +++ b/crates/pecos-python/Cargo.toml @@ -17,6 +17,8 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { workspace=true, features = ["extension-module"] } pecos = { workspace = true } +num-complex = { workspace = true } +rand = { workspace = true } [lints] workspace = true diff --git a/crates/pecos-python/src/lib.rs b/crates/pecos-python/src/lib.rs index 8cb94545..cee848b3 100644 --- a/crates/pecos-python/src/lib.rs +++ b/crates/pecos-python/src/lib.rs @@ -1,6 +1,6 @@ // Copyright 2024 The PECOS Developers // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use thispub(crate)pub(crate) file except // in compliance with the License.You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 @@ -11,12 +11,16 @@ // the License. mod sparse_sim; +mod state_vec_bindings; + use sparse_sim::SparseSim; +use state_vec_bindings::RsStateVec; use pyo3::prelude::*; #[pymodule] fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/crates/pecos-python/src/state_vec_bindings.rs b/crates/pecos-python/src/state_vec_bindings.rs new file mode 100644 index 00000000..f49c6f18 --- /dev/null +++ b/crates/pecos-python/src/state_vec_bindings.rs @@ -0,0 +1,270 @@ +use pecos::prelude::*; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyTuple}; + +/// The struct represents the state-vector simulator exposed to Python +#[pyclass] +pub struct RsStateVec { + inner: StateVec, +} + +#[pymethods] +impl RsStateVec { + /// Creates a new state-vector simulator with the specified number of qubits + #[new] + pub fn new(num_qubits: usize) -> Self { + RsStateVec { + inner: StateVec::new(num_qubits), + } + } + + /// Resets the quantum state to the all-zero state + fn reset(&mut self) { + self.inner.reset(); + } + + /// Executes a single-qubit gate based on the provided symbol and location + /// + /// `symbol`: The gate symbol (e.g., "X", "H", "Z") + /// `location`: The qubit index to apply the gate to + /// `params`: Optional parameters for parameterized gates (currently unused here) + /// + /// Returns an optional result, usually `None` unless a measurement is performed + #[allow(clippy::too_many_lines)] + #[pyo3(signature = (symbol, location, _params=None))] + fn run_1q_gate( + &mut self, + symbol: &str, + location: usize, + _params: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { + match symbol { + "X" => { + self.inner.x(location); + Ok(None) + } + "Y" => { + self.inner.y(location); + Ok(None) + } + "Z" => { + self.inner.z(location); + Ok(None) + } + "H" => { + self.inner.h(location); + Ok(None) + } + "H2" => { + self.inner.h2(location); + Ok(None) + } + "H3" => { + self.inner.h3(location); + Ok(None) + } + "H4" => { + self.inner.h4(location); + Ok(None) + } + "H5" => { + self.inner.h5(location); + Ok(None) + } + "H6" => { + self.inner.h6(location); + Ok(None) + } + "F" => { + self.inner.f(location); + Ok(None) + } + "Fdg" => { + self.inner.fdg(location); + Ok(None) + } + "F2" => { + self.inner.f2(location); + Ok(None) + } + "F2dg" => { + self.inner.f2dg(location); + Ok(None) + } + "F3" => { + self.inner.f3(location); + Ok(None) + } + "F3dg" => { + self.inner.f3dg(location); + Ok(None) + } + "F4" => { + self.inner.f4(location); + Ok(None) + } + "F4dg" => { + self.inner.f4dg(location); + Ok(None) + } + "SX" => { + self.inner.sx(location); + Ok(None) + } + "SXdg" => { + self.inner.sxdg(location); + Ok(None) + } + "SY" => { + self.inner.sy(location); + Ok(None) + } + "SYdg" => { + self.inner.sydg(location); + Ok(None) + } + "SZ" => { + self.inner.sz(location); + Ok(None) + } + "SZdg" => { + self.inner.szdg(location); + Ok(None) + } + "PZ" => { + self.inner.pz(location); + Ok(None) + } + "PX" => { + self.inner.px(location); + Ok(None) + } + "PY" => { + self.inner.py(location); + Ok(None) + } + "PnZ" => { + self.inner.pnz(location); + Ok(None) + } + "PnX" => { + self.inner.pnx(location); + Ok(None) + } + "PnY" => { + self.inner.pny(location); + Ok(None) + } + "MZ" | "MX" | "MY" => { + let result = match symbol { + "MZ" => self.inner.mz(location), + "MX" => self.inner.mx(location), + "MY" => self.inner.my(location), + _ => unreachable!(), + }; + Ok(Some(u8::from(result.outcome))) + } + _ => Err(PyErr::new::( + "Unsupported single-qubit gate", + )), + } + } + + /// Executes a two-qubit gate based on the provided symbol and locations + /// + /// `symbol`: The gate symbol (e.g., "CX", "CZ") + /// `location`: A tuple specifying the two qubits to apply the gate to + /// `params`: Optional parameters for parameterized gates (currently unused here) + /// + /// Returns an optional result, usually `None` unless a measurement is performed + #[pyo3(signature = (symbol, location, _params))] + fn run_2q_gate( + &mut self, + symbol: &str, + location: &Bound<'_, PyTuple>, + _params: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { + if location.len() != 2 { + return Err(PyErr::new::( + "Two-qubit gate requires exactly 2 qubit locations", + )); + } + + let q1: usize = location.get_item(0)?.extract()?; + let q2: usize = location.get_item(1)?.extract()?; + + match symbol { + "CX" => { + self.inner.cx(q1, q2); + Ok(None) + } + "CY" => { + self.inner.cy(q1, q2); + Ok(None) + } + "CZ" => { + self.inner.cz(q1, q2); + Ok(None) + } + "SXX" => { + self.inner.sxx(q1, q2); + Ok(None) + } + "SXXdg" => { + self.inner.sxxdg(q1, q2); + Ok(None) + } + "SYY" => { + self.inner.syy(q1, q2); + Ok(None) + } + "SYYdg" => { + self.inner.syydg(q1, q2); + Ok(None) + } + "SZZ" => { + self.inner.szz(q1, q2); + Ok(None) + } + "SZZdg" => { + self.inner.szzdg(q1, q2); + Ok(None) + } + "SWAP" => { + self.inner.swap(q1, q2); + Ok(None) + } + "G2" => { + self.inner.g2(q1, q2); + Ok(None) + } + _ => Err(PyErr::new::( + "Unsupported two-qubit gate", + )), + } + } + + /// Dispatches a gate to the appropriate handler based on the number of qubits specified + /// + /// `symbol`: The gate symbol + /// `location`: A tuple specifying the qubits to apply the gate to + /// `params`: Optional parameters for parameterized gates + #[pyo3(signature = (symbol, location, params=None))] + fn run_gate( + &mut self, + symbol: &str, + location: &Bound<'_, PyTuple>, + params: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { + match location.len() { + 1 => { + let qubit: usize = location.get_item(0)?.extract()?; + self.run_1q_gate(symbol, qubit, params) + } + 2 => self.run_2q_gate(symbol, location, params), + _ => Err(PyErr::new::( + "Gate location must be specified for either 1 or 2 qubits", + )), + } + } +} diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index a3b3577b..ad1e001d 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -1348,10 +1348,7 @@ mod tests { q1.cz(0, 1); q2.cz(1, 0); - // Results should be identical - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!((a - b).norm() < 1e-10); - } + assert_states_equal(&q1.state, &q2.state); } #[test] @@ -1697,18 +1694,6 @@ mod tests { } } - #[test] - fn test_measure2() { - let mut q = StateVec::new(1); - q.h(0); - let _result = q.mz(0); - - // Check collapse to |0⟩ or |1⟩ - // assert!(result == 0 || result == 1); - let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); - assert!((norm - 1.0).abs() < 1e-10); - } - #[test] fn test_mz() { // Test 1: Measuring |0> state diff --git a/crates/pecos/src/prelude.rs b/crates/pecos/src/prelude.rs index 7b3c5847..566b23e8 100644 --- a/crates/pecos/src/prelude.rs +++ b/crates/pecos/src/prelude.rs @@ -11,17 +11,18 @@ // the License. // re-exporting pecos-core -pub use pecos_core::VecSet; +pub use pecos_core::{IndexableElement, VecSet}; // re-exporting pecos-qsim +pub use pecos_qsim::ArbitraryRotationGateable; pub use pecos_qsim::CliffordGateable; +pub use pecos_qsim::QuantumSimulatorState; pub use pecos_qsim::SparseStab; +pub use pecos_qsim::StateVec; + // TODO: add the following in the future as makes sense... -// pub use pecos_qsim::clifford_simulator::CliffordSimulator; // pub use pecos_qsim::gens::Gens; // pub use pecos_qsim::measurement::{MeasBitValue, MeasValue, Measurement}; // TODO: Distinguish between trait and struct/enum // pub use pecos_qsim::nonclifford_simulator::NonCliffordSimulator; // pub use pecos_qsim::pauli_prop::{PauliProp, StdPauliProp}; // pub use pecos_qsim::paulis::Paulis; -// pub use pecos_qsim::quantum_simulator::QuantumSimulator; -// pub use pecos_qsim::sparse_stab::SparseStab; diff --git a/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py b/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py new file mode 100644 index 00000000..185a9fdc --- /dev/null +++ b/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py @@ -0,0 +1,194 @@ +# Copyright 2024 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License.You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +# ruff: noqa: SLF001 + +from __future__ import annotations + +from typing import Any + +from pecos_rslib._pecos_rslib import RsStateVec as RustStateVec + + +class StateVecRs: + def __init__(self, num_qubits: int): + """ + Initializes the Rust-backed state vector simulator. + + Args: + num_qubits (int): The number of qubits in the quantum system. + """ + self._sim = RustStateVec(num_qubits) + self.num_qubits = num_qubits + self.bindings = dict(gate_dict) + + def reset(self): + """Resets the quantum state to the all-zero state.""" + self._sim.reset() + return self + + def run_gate( + self, + symbol: str, + locations: set[int] | set[tuple[int, ...]], + **params: Any, + ) -> dict[int, int]: + """ + Applies a gate to the quantum state. + + Args: + symbol (str): The gate symbol (e.g., "X", "H", "CX"). + location (tuple[int, ...]): The qubit(s) to which the gate is applied. + params (dict, optional): Parameters for the gate (e.g., rotation angles). + + Returns: + None + """ + # self._sim.run_gate(symbol, location, params) + output = {} + + if params.get("simulate_gate", True) and locations: + for location in locations: + if params.get("angles") and len(params["angles"]) == 1: + params.update({"angle": params["angles"][0]}) + elif "angle" in params and "angles" not in params: + params["angles"] = (params["angle"],) + + if symbol in self.bindings: + results = self.bindings[symbol](self, location, **params) + else: + msg = f"Gate {symbol} is not supported in this simulator." + raise Exception(msg) + + if results: + output[location] = results + + return output + + def run_circuit( + self, + circuit, + removed_locations: set[int] | None = None, + ) -> dict[int, int]: + if removed_locations is None: + removed_locations = set() + + results = {} + for symbol, locations, params in circuit.items(): + gate_results = self.run_gate( + symbol, + locations - removed_locations, + **params, + ) + results.update(gate_results) + + return results + + +# Define the gate dictionary +gate_dict = { + "I": lambda sim, q, **params: None, + "X": lambda sim, q, **params: sim._sim.run_1q_gate("X", q, params), + "Y": lambda sim, q, **params: sim._sim.run_1q_gate("Y", q, params), + "Z": lambda sim, q, **params: sim._sim.run_1q_gate("Z", q, params), + "SX": lambda sim, q, **params: sim._sim.run_1q_gate("SX", q, params), + "SXdg": lambda sim, q, **params: sim._sim.run_1q_gate("SXdg", q, params), + "SY": lambda sim, q, **params: sim._sim.run_1q_gate("SY", q, params), + "SYdg": lambda sim, q, **params: sim._sim.run_1q_gate("SYdg", q, params), + "SZ": lambda sim, q, **params: sim._sim.run_1q_gate("SZ", q, params), + "SZdg": lambda sim, q, **params: sim._sim.run_1q_gate("SZdg", q, params), + "H": lambda sim, q, **params: sim._sim.run_1q_gate("H", q, params), + "H2": lambda sim, q, **params: sim._sim.run_1q_gate("H2", q, params), + "H3": lambda sim, q, **params: sim._sim.run_1q_gate("H3", q, params), + "H4": lambda sim, q, **params: sim._sim.run_1q_gate("H4", q, params), + "H5": lambda sim, q, **params: sim._sim.run_1q_gate("H5", q, params), + "H6": lambda sim, q, **params: sim._sim.run_1q_gate("H6", q, params), + "F": lambda sim, q, **params: sim._sim.run_1q_gate("F", q, params), + "Fdg": lambda sim, q, **params: sim._sim.run_1q_gate("Fdg", q, params), + "F2": lambda sim, q, **params: sim._sim.run_1q_gate("F2", q, params), + "F2dg": lambda sim, q, **params: sim._sim.run_1q_gate("F2dg", q, params), + "F3": lambda sim, q, **params: sim._sim.run_1q_gate("F3", q, params), + "F3dg": lambda sim, q, **params: sim._sim.run_1q_gate("F3dg", q, params), + "F4": lambda sim, q, **params: sim._sim.run_1q_gate("F4", q, params), + "F4dg": lambda sim, q, **params: sim._sim.run_1q_gate("F4dg", q, params), + "II": lambda sim, qs, **params: None, + "CX": lambda sim, qs, **params: sim._sim.run_2q_gate("CX", qs, params), + "CNOT": lambda sim, qs, **params: sim._sim.run_2q_gate("CX", qs, params), + "CY": lambda sim, qs, **params: sim._sim.run_2q_gate("CY", qs, params), + "CZ": lambda sim, qs, **params: sim._sim.run_2q_gate("CZ", qs, params), + "SXX": lambda sim, qs, **params: sim._sim.run_2q_gate("SXX", qs, params), + "SXXdg": lambda sim, qs, **params: sim._sim.run_2q_gate("SXXdg", qs, params), + "SYY": lambda sim, qs, **params: sim._sim.run_2q_gate("SYY", qs, params), + "SYYdg": lambda sim, qs, **params: sim._sim.run_2q_gate("SYYdg", qs, params), + "SZZ": lambda sim, qs, **params: sim._sim.run_2q_gate("SZZ", qs, params), + "SZZdg": lambda sim, qs, **params: sim._sim.run_2q_gate("SZZdg", qs, params), + "SWAP": lambda sim, qs, **params: sim._sim.run_2q_gate("SWAP", qs, params), + "G": lambda sim, qs, **params: sim._sim.run_2q_gate("G2", qs, params), + "G2": lambda sim, qs, **params: sim._sim.run_2q_gate("G2", qs, params), + "MZ": lambda sim, q, **params: sim._sim.run_1q_gate("MZ", q, params), + "MX": lambda sim, q, **params: sim._sim.run_1q_gate("MX", q, params), + "MY": lambda sim, q, **params: sim._sim.run_1q_gate("MY", q, params), + "PZ": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), + "PX": lambda sim, q, **params: sim._sim.run_1q_gate("PX", q, params), + "PY": lambda sim, q, **params: sim._sim.run_1q_gate("PY", q, params), + "PnZ": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), + "Init +Z": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), + "Init -Z": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), + "Init +X": lambda sim, q, **params: sim._sim.run_1q_gate("PX", q, params), + "Init -X": lambda sim, q, **params: sim._sim.run_1q_gate("PnX", q, params), + "Init +Y": lambda sim, q, **params: sim._sim.run_1q_gate("PY", q, params), + "Init -Y": lambda sim, q, **params: sim._sim.run_1q_gate("PnY", q, params), + "init |0>": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), + "init |1>": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), + "init |+>": lambda sim, q, **params: sim._sim.run_1q_gate("PX", q, params), + "init |->": lambda sim, q, **params: sim._sim.run_1q_gate("PnX", q, params), + "init |+i>": lambda sim, q, **params: sim._sim.run_1q_gate("PY", q, params), + "init |-i>": lambda sim, q, **params: sim._sim.run_1q_gate("PnY", q, params), + "leak": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), + "leak |0>": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), + "leak |1>": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), + "unleak |0>": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), + "unleak |1>": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), + "Measure +X": lambda sim, q, **params: sim._sim.run_1q_gate("MX", q, params), + "Measure +Y": lambda sim, q, **params: sim._sim.run_1q_gate("MY", q, params), + "Measure +Z": lambda sim, q, **params: sim._sim.run_1q_gate("MZ", q, params), + "Q": lambda sim, q, **params: sim._sim.run_1q_gate("SX", q, params), + "Qd": lambda sim, q, **params: sim._sim.run_1q_gate("SXdg", q, params), + "R": lambda sim, q, **params: sim._sim.run_1q_gate("SY", q, params), + "Rd": lambda sim, q, **params: sim._sim.run_1q_gate("SYdg", q, params), + "S": lambda sim, q, **params: sim._sim.run_1q_gate("SZ", q, params), + "Sd": lambda sim, q, **params: sim._sim.run_1q_gate("SZdg", q, params), + "H1": lambda sim, q, **params: sim._sim.run_1q_gate("H1", q, params), + "F1": lambda sim, q, **params: sim._sim.run_1q_gate("F", q, params), + "F1d": lambda sim, q, **params: sim._sim.run_1q_gate("Fdg", q, params), + "F2d": lambda sim, q, **params: sim._sim.run_1q_gate("F2dg", q, params), + "F3d": lambda sim, q, **params: sim._sim.run_1q_gate("F3dg", q, params), + "F4d": lambda sim, q, **params: sim._sim.run_1q_gate("F4dg", q, params), + "SqrtXX": lambda sim, qs, **params: sim._sim.run_2q_gate("SXX", qs, params), + "SqrtYY": lambda sim, qs, **params: sim._sim.run_2q_gate("SYY", qs, params), + "SqrtZZ": lambda sim, qs, **params: sim._sim.run_2q_gate("SZZ", qs, params), + "measure Z": lambda sim, q, **params: sim._sim.run_1q_gate("MZ", q, params), + # "MZForced": lambda sim, q, **params: sim._sim.run_1q_gate("MZForced", q, params), + # "PZForced": lambda sim, q, **params: sim._sim.run_1q_gate("PZForced", q, params), + "SqrtXXd": lambda sim, qs, **params: sim._sim.run_2q_gate("SXXdg", qs, params), + "SqrtYYd": lambda sim, qs, **params: sim._sim.run_2q_gate("SYYdg", qs, params), + "SqrtZZd": lambda sim, qs, **params: sim._sim.run_2q_gate("SZZdg", qs, params), + "SqrtX": lambda sim, q, **params: sim._sim.run_1q_gate("SX", q, params), + "SqrtXd": lambda sim, q, **params: sim._sim.run_1q_gate("SXdg", q, params), + "SqrtY": lambda sim, q, **params: sim._sim.run_1q_gate("SY", q, params), + "SqrtYd": lambda sim, q, **params: sim._sim.run_1q_gate("SYdg", q, params), + "SqrtZ": lambda sim, q, **params: sim._sim.run_1q_gate("SZ", q, params), + "SqrtZd": lambda sim, q, **params: sim._sim.run_1q_gate("SZdg", q, params), +} + +# "force output": qmeas.force_output, + +__all__ = ["StateVecRs", "gate_dict"] From 4581a911b742e2749e94bba06b9514794ba880c1 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sat, 21 Dec 2024 16:42:32 -0700 Subject: [PATCH 16/33] refactor sparse_sim.rs to sparse_stab_bindings.rs --- crates/pecos-python/src/lib.rs | 4 ++-- .../src/{sparse_sim.rs => sparse_stab_bindings.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename crates/pecos-python/src/{sparse_sim.rs => sparse_stab_bindings.rs} (100%) diff --git a/crates/pecos-python/src/lib.rs b/crates/pecos-python/src/lib.rs index cee848b3..024da868 100644 --- a/crates/pecos-python/src/lib.rs +++ b/crates/pecos-python/src/lib.rs @@ -10,10 +10,10 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -mod sparse_sim; +mod sparse_stab_bindings; mod state_vec_bindings; -use sparse_sim::SparseSim; +use sparse_stab_bindings::SparseSim; use state_vec_bindings::RsStateVec; use pyo3::prelude::*; diff --git a/crates/pecos-python/src/sparse_sim.rs b/crates/pecos-python/src/sparse_stab_bindings.rs similarity index 100% rename from crates/pecos-python/src/sparse_sim.rs rename to crates/pecos-python/src/sparse_stab_bindings.rs From 06144a12fb289eba8b5f3b6720212631c540b268 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 22 Dec 2024 01:27:45 -0700 Subject: [PATCH 17/33] Adding missing gates for Python wrapper of RS state vec --- crates/pecos-python/src/state_vec_bindings.rs | 238 +++++++++++++++++- .../src/arbitrary_rotation_gateable.rs | 20 +- crates/pecos-qsim/src/clifford_gateable.rs | 2 +- crates/pecos-qsim/src/state_vec.rs | 234 +++++++++++++++-- .../pecos-rslib/src/pecos_rslib/__init__.py | 2 + .../src/pecos_rslib/rssparse_sim.py | 2 + .../src/pecos_rslib/rsstate_vec.py | 81 +++++- .../src/pecos/simulators/__init__.py | 2 +- .../state_sim_tests/test_statevec.py | 98 +++++--- 9 files changed, 606 insertions(+), 73 deletions(-) diff --git a/crates/pecos-python/src/state_vec_bindings.rs b/crates/pecos-python/src/state_vec_bindings.rs index f49c6f18..413ad0a8 100644 --- a/crates/pecos-python/src/state_vec_bindings.rs +++ b/crates/pecos-python/src/state_vec_bindings.rs @@ -31,12 +31,12 @@ impl RsStateVec { /// /// Returns an optional result, usually `None` unless a measurement is performed #[allow(clippy::too_many_lines)] - #[pyo3(signature = (symbol, location, _params=None))] + #[pyo3(signature = (symbol, location, params=None))] fn run_1q_gate( &mut self, symbol: &str, location: usize, - _params: Option<&Bound<'_, PyDict>>, + params: Option<&Bound<'_, PyDict>>, ) -> PyResult> { match symbol { "X" => { @@ -51,6 +51,120 @@ impl RsStateVec { self.inner.z(location); Ok(None) } + "RX" => { + if let Some(params) = params { + match params.get_item("angle") { + Ok(Some(py_any)) => { + if let Ok(angle) = py_any.extract::() { + self.inner.rx(angle, location); + } else { + return Err(PyErr::new::( + "Expected a valid angle parameter for RX gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameter missing for RX gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + "RY" => { + if let Some(params) = params { + match params.get_item("angle") { + Ok(Some(py_any)) => { + if let Ok(angle) = py_any.extract::() { + self.inner.ry(angle, location); + } else { + return Err(PyErr::new::( + "Expected a valid angle parameter for RY gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameter missing for RY gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + "RZ" => { + if let Some(params) = params { + match params.get_item("angle") { + Ok(Some(py_any)) => { + if let Ok(angle) = py_any.extract::() { + self.inner.rz(angle, location); + } else { + return Err(PyErr::new::( + "Expected a valid angle parameter for RZ gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameter missing for RZ gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + "R1XY" => { + if let Some(params) = params { + match params.get_item("angles") { + Ok(Some(py_any)) => { + // Extract as a sequence of f64 values + if let Ok(angles) = py_any.extract::>() { + if angles.len() >= 2 { + self.inner.r1xy(angles[0], angles[1], location); + } else { + return Err(PyErr::new::( + "R1XY gate requires two angle parameters", + )); + } + } else { + return Err(PyErr::new::( + "Expected valid angle parameters for R1XY gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameters missing for R1XY gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + + "T" => { + self.inner.t(location); + Ok(None) + } + + "Tdg" => { + self.inner.tdg(location); + Ok(None) + } + "H" => { self.inner.h(location); Ok(None) @@ -177,12 +291,13 @@ impl RsStateVec { /// `params`: Optional parameters for parameterized gates (currently unused here) /// /// Returns an optional result, usually `None` unless a measurement is performed - #[pyo3(signature = (symbol, location, _params))] + #[allow(clippy::too_many_lines)] + #[pyo3(signature = (symbol, location, params))] fn run_2q_gate( &mut self, symbol: &str, location: &Bound<'_, PyTuple>, - _params: Option<&Bound<'_, PyDict>>, + params: Option<&Bound<'_, PyDict>>, ) -> PyResult> { if location.len() != 2 { return Err(PyErr::new::( @@ -238,6 +353,111 @@ impl RsStateVec { self.inner.g2(q1, q2); Ok(None) } + "RXX" => { + if let Some(params) = params { + match params.get_item("angle") { + Ok(Some(py_any)) => { + if let Ok(angle) = py_any.extract::() { + self.inner.rxx(angle, q1, q2); + } else { + return Err(PyErr::new::( + "Expected a valid angle parameter for RXX gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameter missing for RXX gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + "RYY" => { + if let Some(params) = params { + match params.get_item("angle") { + Ok(Some(py_any)) => { + if let Ok(angle) = py_any.extract::() { + self.inner.ryy(angle, q1, q2); + } else { + return Err(PyErr::new::( + "Expected a valid angle parameter for RYY gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameter missing for RYY gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + "RZZ" => { + if let Some(params) = params { + match params.get_item("angle") { + Ok(Some(py_any)) => { + if let Ok(angle) = py_any.extract::() { + self.inner.rzz(angle, q1, q2); + } else { + return Err(PyErr::new::( + "Expected a valid angle parameter for RZZ gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameter missing for RZZ gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + + "RXXRYYRZZ" => { + if let Some(params) = params { + match params.get_item("angles") { + Ok(Some(py_any)) => { + if let Ok(angles) = py_any.extract::>() { + if angles.len() >= 3 { + self.inner + .rxxryyrzz(angles[0], angles[1], angles[2], q1, q2); + } else { + return Err(PyErr::new::( + "RXXRYYRZZ gate requires three angle parameters", + )); + } + } else { + return Err(PyErr::new::( + "Expected valid angle parameters for RXXRYYRZZ gate", + )); + } + } + Ok(None) => { + return Err(PyErr::new::( + "Angle parameters missing for RXXRYYRZZ gate", + )); + } + Err(err) => { + return Err(err); + } + } + } + Ok(None) + } + _ => Err(PyErr::new::( "Unsupported two-qubit gate", )), @@ -267,4 +487,14 @@ impl RsStateVec { )), } } + + /// Provides direct access to the current state vector as a Python property + #[getter] + fn vector(&self) -> Vec<(f64, f64)> { + self.inner + .state() + .iter() + .map(|complex| (complex.re, complex.im)) + .collect() + } } diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs index 6d2948da..6ef48457 100644 --- a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -12,16 +12,18 @@ use crate::CliffordGateable; use pecos_core::IndexableElement; -use std::f64::consts::FRAC_PI_2; +use std::f64::consts::{FRAC_PI_2, FRAC_PI_4}; pub trait ArbitraryRotationGateable: CliffordGateable { fn rx(&mut self, theta: f64, q: T) -> &mut Self; + #[inline] fn ry(&mut self, theta: f64, q: T) -> &mut Self { self.sz(q).rx(theta, q).szdg(q) } fn rz(&mut self, theta: f64, q: T) -> &mut Self; + #[inline] fn u(&mut self, theta: f64, phi: f64, lambda: f64, q: T) -> &mut Self { self.rz(lambda, q).ry(theta, q).rz(phi, q) } @@ -33,6 +35,16 @@ pub trait ArbitraryRotationGateable: CliffordGateable { .rz(phi - FRAC_PI_2, q) } + #[inline] + fn t(&mut self, q: T) -> &mut Self { + self.rz(FRAC_PI_4, q) + } + + #[inline] + fn tdg(&mut self, q: T) -> &mut Self { + self.rz(-FRAC_PI_4, q) + } + #[inline] fn rxx(&mut self, theta: f64, q1: T, q2: T) -> &mut Self { self.h(q1).h(q2).rzz(theta, q1, q2).h(q1).h(q2) @@ -43,4 +55,10 @@ pub trait ArbitraryRotationGateable: CliffordGateable { self.sx(q1).sx(q2).rzz(theta, q1, q2).sxdg(q1).sxdg(q2) } fn rzz(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; + + #[inline] + fn rxxryyrzz(&mut self, theta: f64, phi: f64, lambda: f64, q1: T, q2: T) -> &mut Self { + // TODO: This is likely backwards.. + self.rxx(theta, q1, q2).ryy(phi, q1, q2).rzz(lambda, q1, q2) + } } diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index b7649e8c..8fb233e6 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -585,7 +585,7 @@ pub trait CliffordGateable: QuantumSimulatorState { /// * `q2` - Second qubit #[inline] fn szz(&mut self, q1: T, q2: T) -> &mut Self { - self.sydg(q1).sydg(q2).sxx(q1, q2).sy(q1).sy(q2) + self.h(q1).h(q2).sxx(q1, q2).h(q1).h(q2) } /// Performs an adjoint of the square root of ZZ operation (√ZZ). diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index ad1e001d..00c7832b 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -544,6 +544,7 @@ impl ArbitraryRotationGateable for StateVec { self.single_qubit_rotation(target, u00, u01, u10, u11) } + #[inline] fn r1xy(&mut self, theta: f64, phi: f64, target: usize) -> &mut Self { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); @@ -611,39 +612,35 @@ impl ArbitraryRotationGateable for StateVec { /// /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 #[inline] - fn ryy(&mut self, theta: f64, qubit1: usize, qubit2: usize) -> &mut Self { - assert!(qubit1 < self.num_qubits); - assert!(qubit2 < self.num_qubits); - assert_ne!(qubit1, qubit2); + fn ryy(&mut self, theta: f64, q1: usize, q2: usize) -> &mut Self { + assert!(q1 < self.num_qubits); + assert!(q2 < self.num_qubits); + assert_ne!(q1, q2); let cos = (theta / 2.0).cos(); - let sin = (theta / 2.0).sin(); - let i_sin = Complex64::new(0.0, sin); // Changed name and sign + let i_sin = Complex64::new(0.0, 1.0) * (theta / 2.0).sin(); - let (q1, q2) = if qubit1 < qubit2 { - (qubit1, qubit2) - } else { - (qubit2, qubit1) - }; + // No need to reorder q1 and q2 since we're using explicit masks + let mask1 = 1 << q1; + let mask2 = 1 << q2; - let step1 = 1 << q1; - let step2 = 1 << q2; - for i in (0..self.state.len()).step_by(2 * step2) { - for j in (i..i + step2).step_by(2 * step1) { - let i00 = j; - let i01 = j ^ step2; - let i10 = j ^ step1; - let i11 = i10 ^ step2; + for i in 0..self.state.len() { + // Only process each set of 4 states once + if (i & (mask1 | mask2)) == 0 { + let i00 = i; + let i01 = i | mask2; + let i10 = i | mask1; + let i11 = i | mask1 | mask2; let a00 = self.state[i00]; let a01 = self.state[i01]; let a10 = self.state[i10]; let a11 = self.state[i11]; - self.state[i00] = cos * a00 - i_sin * a11; + self.state[i00] = cos * a00 + i_sin * a11; self.state[i01] = cos * a01 - i_sin * a10; self.state[i10] = cos * a10 - i_sin * a01; - self.state[i11] = cos * a11 - i_sin * a00; + self.state[i11] = cos * a11 + i_sin * a00; } } self @@ -1537,13 +1534,13 @@ mod tests { assert!((q.state[0].re - expected).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); assert!(q.state[2].norm() < 1e-10); - assert!((q.state[3].im + expected).abs() < 1e-10); + assert!((q.state[3].im - expected).abs() < 1e-10); - // |11⟩ -> (1/√2)|11⟩ - i(1/√2)|00⟩ + // |11⟩ -> i(1/√2)|00⟩ + (1/√2)|11⟩ let mut q = StateVec::new(2); q.x(0).x(1); // Prepare |11⟩ q.ryy(FRAC_PI_2, 0, 1); - assert!((q.state[0].im + expected).abs() < 1e-10); + assert!((q.state[0].im - expected).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); assert!(q.state[2].norm() < 1e-10); assert!((q.state[3].re - expected).abs() < 1e-10); @@ -1619,6 +1616,155 @@ mod tests { } } + #[test] + fn test_ryy_qubit_order_invariance() { + let theta = FRAC_PI_4; + + // Test on random initial states + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + q1.h(0).x(1); // Random state + q2.h(0).x(1); // Same initial state + + q1.ryy(theta, 0, 1); + q2.ryy(theta, 1, 0); + + // States should be exactly equal + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!( + (a - b).norm() < 1e-10, + "Qubit order test failed: a={a}, b={b}" + ); + } + } + + #[test] + fn test_ryy_large_system() { + let theta = FRAC_PI_3; + + // Initialize a 5-qubit state + let mut q = StateVec::new(5); + q.h(0).h(1).h(2).h(3).h(4); // Superposition state + + // Apply RYY on qubits 2 and 4 + q.ryy(theta, 2, 4); + + // Ensure state vector normalization is preserved + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!( + (norm - 1.0).abs() < 1e-10, + "State normalization test failed: norm={norm}" + ); + } + + fn assert_state_vectors_match(simulated: &[Complex64], expected: &[Complex64], tolerance: f64) { + assert_eq!( + simulated.len(), + expected.len(), + "State vectors must have the same length" + ); + + // Find the first non-zero entry in the expected vector + let reference_index = expected + .iter() + .position(|&x| x.norm() > tolerance) + .expect("Expected vector should have at least one non-zero element"); + + // Compute the phase correction + let phase = simulated[reference_index] / expected[reference_index]; + + // Verify all elements match up to the global phase + for (i, (sim, exp)) in simulated.iter().zip(expected.iter()).enumerate() { + let corrected_exp = exp * phase; + assert!( + (sim - corrected_exp).norm() < tolerance, + "Mismatch at index {i}: simulated = {sim:?}, expected = {exp:?}, corrected = {corrected_exp:?}" + ); + } + } + + #[test] + fn test_ryy_edge_cases() { + let mut q = StateVec::new(2); + + // Apply RYY gate + q.ryy(PI, 0, 1); + + // Define the expected result for RYY(π) + let expected = vec![ + Complex64::new(0.0, 0.0), // |00⟩ + Complex64::new(0.0, 0.0), // |01⟩ + Complex64::new(0.0, 0.0), // |10⟩ + Complex64::new(-1.0, 0.0), // |11⟩ + ]; + + // Compare simulated state vector to the expected result + assert_state_vectors_match(&q.state, &expected, 1e-10); + } + + #[test] + fn test_ryy_global_phase() { + let mut q = StateVec::new(2); + + q.ryy(PI, 0, 1); + + // Define the expected result for RYY(π) + let expected = vec![ + Complex64::new(0.0, 0.0), // |00⟩ + Complex64::new(0.0, 0.0), // |01⟩ + Complex64::new(0.0, 0.0), // |10⟩ + Complex64::new(-1.0, 0.0), // |11⟩ + ]; + + // Compare states + assert_states_equal(&q.state, &expected); + } + + #[test] + fn test_ryy_small_angles() { + let theta = 1e-10; // Very small angle + let mut q = StateVec::new(2); + + // Initialize |00⟩ + let initial = q.state.clone(); + q.ryy(theta, 0, 1); + + // Expect state to remain close to the initial state + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!( + (a - b).norm() < 1e-10, + "Small angle test failed: a={a}, b={b}" + ); + } + } + + #[test] + fn test_ryy_randomized() { + use rand::Rng; + + let mut rng = rand::thread_rng(); + let theta = rng.gen_range(0.0..2.0 * PI); + + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Random initial state + q1.h(0).h(1); + q2.h(0).h(1); + + // Apply RYY with random qubit order + q1.ryy(theta, 0, 1); + q2.ryy(theta, 1, 0); + + // Compare states + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!( + (a - b).norm() < 1e-10, + "Randomized test failed: a={a}, b={b}" + ); + } + } + #[test] fn test_rzz() { // Test 1: RZZ(π) on (|00⟩ + |11⟩)/√2 should give itself @@ -1656,6 +1802,46 @@ mod tests { assert!((q.state[3] - factor * exp_minus_i_pi_4).norm() < 1e-10); // |11⟩ } + #[test] + fn test_szz_equivalence() { + // Test that SZZ is equivalent to RZZ(π/2) + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Create some non-trivial initial state + q1.h(0); + q2.h(0); + + // Compare direct SZZ vs RZZ(π/2) + q1.szz(0, 1); + q2.rzz(FRAC_PI_2, 0, 1); + + assert_states_equal(q1.state(), q2.state()); + + // Also verify decomposition matches + let mut q3 = StateVec::new(2); + q3.h(0); // Same initial state + q3.h(0).h(1).sxx(0, 1).h(0).h(1); + + assert_states_equal(q1.state(), q3.state()); + } + + #[test] + fn test_szz_trait_equivalence() { + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Create some non-trivial initial state + q1.h(0); + q2.h(0); + + // Compare CliffordGateable trait szz vs ArbitraryRotationGateable trait rzz(π/2) + CliffordGateable::::szz(&mut q1, 0, 1); + ArbitraryRotationGateable::::rzz(&mut q2, PI / 2.0, 0, 1); + + assert_states_equal(q1.state(), q2.state()); + } + #[test] fn test_rotation_symmetries() { // Test that all rotations are symmetric under exchange of qubits diff --git a/python/pecos-rslib/src/pecos_rslib/__init__.py b/python/pecos-rslib/src/pecos_rslib/__init__.py index b732f0c3..11b7bd35 100644 --- a/python/pecos-rslib/src/pecos_rslib/__init__.py +++ b/python/pecos-rslib/src/pecos_rslib/__init__.py @@ -13,6 +13,7 @@ from importlib.metadata import PackageNotFoundError, version from pecos_rslib.rssparse_sim import SparseSimRs +from pecos_rslib.rsstate_vec import StateVecRs try: __version__ = version("pecos-rslib") @@ -21,4 +22,5 @@ __all__ = [ "SparseSimRs", + "StateVecRs", ] diff --git a/python/pecos-rslib/src/pecos_rslib/rssparse_sim.py b/python/pecos-rslib/src/pecos_rslib/rssparse_sim.py index 6aa2340c..81e335d7 100644 --- a/python/pecos-rslib/src/pecos_rslib/rssparse_sim.py +++ b/python/pecos-rslib/src/pecos_rslib/rssparse_sim.py @@ -234,6 +234,7 @@ def adjust_tableau_string(line: str, *, is_stab: bool) -> str: "PX": lambda sim, q, **params: sim._sim.run_1q_gate("PX", q, params), "PY": lambda sim, q, **params: sim._sim.run_1q_gate("PY", q, params), "PnZ": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), + "Init": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), "Init +Z": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), "Init -Z": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), "Init +X": lambda sim, q, **params: sim._sim.run_1q_gate("PX", q, params), @@ -269,6 +270,7 @@ def adjust_tableau_string(line: str, *, is_stab: bool) -> str: "SqrtXX": lambda sim, qs, **params: sim._sim.run_2q_gate("SXX", qs, params), "SqrtYY": lambda sim, qs, **params: sim._sim.run_2q_gate("SYY", qs, params), "SqrtZZ": lambda sim, qs, **params: sim._sim.run_2q_gate("SZZ", qs, params), + "Measure": lambda sim, q, **params: sim._sim.run_1q_gate("MZ", q, params), "measure Z": lambda sim, q, **params: sim._sim.run_1q_gate("MZ", q, params), "MZForced": lambda sim, q, **params: sim._sim.run_1q_gate("MZForced", q, params), "PZForced": lambda sim, q, **params: sim._sim.run_1q_gate("PZForced", q, params), diff --git a/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py b/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py index 185a9fdc..cb3d34e8 100644 --- a/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py +++ b/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py @@ -15,6 +15,8 @@ from typing import Any +import numpy as np + from pecos_rslib._pecos_rslib import RsStateVec as RustStateVec @@ -30,6 +32,28 @@ def __init__(self, num_qubits: int): self.num_qubits = num_qubits self.bindings = dict(gate_dict) + @property + def vector(self) -> np.ndarray: + raw_vector = self._sim.vector + print(f"[DEBUG] Raw vector: {raw_vector}") + + if isinstance(raw_vector[0], (list, tuple)): + raw_vector = np.array([complex(r, i) for r, i in raw_vector]) + + # Convert vector from little-endian to big-endian ordering to match BasicSV + raw_vector = np.array(raw_vector).flatten() + num_qubits = self.num_qubits + + # Convert to big-endian by reversing bit order + indices = np.arange(len(raw_vector)) + binary_indices = [f"{idx:0{num_qubits}b}" for idx in indices] + reordered_indices = [int(bits[::-1], 2) for bits in binary_indices] + + # Reorder the vector to match BasicSV's bit ordering + final_vector = raw_vector[reordered_indices] + + return final_vector + def reset(self): """Resets the quantum state to the all-zero state.""" self._sim.reset() @@ -106,11 +130,18 @@ def run_circuit( "SZ": lambda sim, q, **params: sim._sim.run_1q_gate("SZ", q, params), "SZdg": lambda sim, q, **params: sim._sim.run_1q_gate("SZdg", q, params), "H": lambda sim, q, **params: sim._sim.run_1q_gate("H", q, params), + "H1": lambda sim, q, **params: sim._sim.run_1q_gate("H", q, params), "H2": lambda sim, q, **params: sim._sim.run_1q_gate("H2", q, params), "H3": lambda sim, q, **params: sim._sim.run_1q_gate("H3", q, params), "H4": lambda sim, q, **params: sim._sim.run_1q_gate("H4", q, params), "H5": lambda sim, q, **params: sim._sim.run_1q_gate("H5", q, params), "H6": lambda sim, q, **params: sim._sim.run_1q_gate("H6", q, params), + "H+z+x": lambda sim, q, **params: sim._sim.run_1q_gate("H", q, params), + "H-z-x": lambda sim, q, **params: sim._sim.run_1q_gate("H2", q, params), + "H+y-z": lambda sim, q, **params: sim._sim.run_1q_gate("H3", q, params), + "H-y-z": lambda sim, q, **params: sim._sim.run_1q_gate("H4", q, params), + "H-x+y": lambda sim, q, **params: sim._sim.run_1q_gate("H5", q, params), + "H-x-y": lambda sim, q, **params: sim._sim.run_1q_gate("H6", q, params), "F": lambda sim, q, **params: sim._sim.run_1q_gate("F", q, params), "Fdg": lambda sim, q, **params: sim._sim.run_1q_gate("Fdg", q, params), "F2": lambda sim, q, **params: sim._sim.run_1q_gate("F2", q, params), @@ -140,6 +171,7 @@ def run_circuit( "PX": lambda sim, q, **params: sim._sim.run_1q_gate("PX", q, params), "PY": lambda sim, q, **params: sim._sim.run_1q_gate("PY", q, params), "PnZ": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), + "Init": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), "Init +Z": lambda sim, q, **params: sim._sim.run_1q_gate("PZ", q, params), "Init -Z": lambda sim, q, **params: sim._sim.run_1q_gate("PnZ", q, params), "Init +X": lambda sim, q, **params: sim._sim.run_1q_gate("PX", q, params), @@ -166,7 +198,6 @@ def run_circuit( "Rd": lambda sim, q, **params: sim._sim.run_1q_gate("SYdg", q, params), "S": lambda sim, q, **params: sim._sim.run_1q_gate("SZ", q, params), "Sd": lambda sim, q, **params: sim._sim.run_1q_gate("SZdg", q, params), - "H1": lambda sim, q, **params: sim._sim.run_1q_gate("H1", q, params), "F1": lambda sim, q, **params: sim._sim.run_1q_gate("F", q, params), "F1d": lambda sim, q, **params: sim._sim.run_1q_gate("Fdg", q, params), "F2d": lambda sim, q, **params: sim._sim.run_1q_gate("F2dg", q, params), @@ -175,6 +206,7 @@ def run_circuit( "SqrtXX": lambda sim, qs, **params: sim._sim.run_2q_gate("SXX", qs, params), "SqrtYY": lambda sim, qs, **params: sim._sim.run_2q_gate("SYY", qs, params), "SqrtZZ": lambda sim, qs, **params: sim._sim.run_2q_gate("SZZ", qs, params), + "Measure": lambda sim, q, **params: sim._sim.run_1q_gate("MZ", q, params), "measure Z": lambda sim, q, **params: sim._sim.run_1q_gate("MZ", q, params), # "MZForced": lambda sim, q, **params: sim._sim.run_1q_gate("MZForced", q, params), # "PZForced": lambda sim, q, **params: sim._sim.run_1q_gate("PZForced", q, params), @@ -187,6 +219,53 @@ def run_circuit( "SqrtYd": lambda sim, q, **params: sim._sim.run_1q_gate("SYdg", q, params), "SqrtZ": lambda sim, q, **params: sim._sim.run_1q_gate("SZ", q, params), "SqrtZd": lambda sim, q, **params: sim._sim.run_1q_gate("SZdg", q, params), + "RX": lambda sim, q, **params: sim._sim.run_1q_gate( + "RX", + q, + {"angle": params["angles"][0]} if "angles" in params else {"angle": 0}, + ), + "RY": lambda sim, q, **params: sim._sim.run_1q_gate( + "RY", + q, + {"angle": params["angles"][0]} if "angles" in params else {"angle": 0}, + ), + "RZ": lambda sim, q, **params: sim._sim.run_1q_gate( + "RZ", + q, + {"angle": params["angles"][0]} if "angles" in params else {"angle": 0}, + ), + "R1XY": lambda sim, q, **params: sim._sim.run_1q_gate( + "R1XY", + q, + {"angles": params["angles"]}, # Changed from "angle" to "angles" + ), + "T": lambda sim, q, **params: sim._sim.run_1q_gate("T", q, params), + "Tdg": lambda sim, q, **params: sim._sim.run_1q_gate("Tdg", q, params), + "RXX": lambda sim, qs, **params: sim._sim.run_2q_gate( + "RXX", + qs, + {"angle": params["angles"][0]} if "angles" in params else {"angle": 0}, + ), + "RYY": lambda sim, qs, **params: sim._sim.run_2q_gate( + "RYY", + qs, + {"angle": params["angles"][0]} if "angles" in params else {"angle": 0}, + ), + "RZZ": lambda sim, qs, **params: sim._sim.run_2q_gate( + "RZZ", + qs, + {"angle": params["angles"][0]} if "angles" in params else {"angle": 0}, + ), + "RXXRYYRZZ": lambda sim, qs, **params: sim._sim.run_2q_gate( + "RXXRYYRZZ", + qs, + {"angles": params["angles"]} if "angles" in params else {"angles": [0, 0, 0]}, + ), + "R2XXYYZZ": lambda sim, qs, **params: sim._sim.run_2q_gate( + "RXXRYYRZZ", + qs, + {"angles": params["angles"]} if "angles" in params else {"angles": [0, 0, 0]}, + ), } # "force output": qmeas.force_output, diff --git a/python/quantum-pecos/src/pecos/simulators/__init__.py b/python/quantum-pecos/src/pecos/simulators/__init__.py index 655a8d85..242d2e71 100644 --- a/python/quantum-pecos/src/pecos/simulators/__init__.py +++ b/python/quantum-pecos/src/pecos/simulators/__init__.py @@ -11,7 +11,7 @@ # specific language governing permissions and limitations under the License. # Rust version of stabilizer sim -from pecos_rslib import SparseSimRs +from pecos_rslib import SparseSimRs, StateVecRs from pecos_rslib import SparseSimRs as SparseSim from pecos.simulators import sim_class_types diff --git a/python/tests/pecos/integration/state_sim_tests/test_statevec.py b/python/tests/pecos/integration/state_sim_tests/test_statevec.py index 6e4d0888..ada89f84 100644 --- a/python/tests/pecos/integration/state_sim_tests/test_statevec.py +++ b/python/tests/pecos/integration/state_sim_tests/test_statevec.py @@ -32,9 +32,11 @@ CuStateVec, QuEST, Qulacs, + StateVecRs, ) str_to_sim = { + "StateVecRS": StateVecRs, "BasicSV": BasicSV, "Qulacs": Qulacs, "QuEST": QuEST, @@ -51,9 +53,25 @@ def check_dependencies(simulator) -> Callable[[int], StateVector]: def verify(simulator, qc: QuantumCircuit, final_vector: np.ndarray) -> None: sim = check_dependencies(simulator)(len(qc.qudits)) - sim.run_circuit(qc) - assert np.allclose(sim.vector, final_vector) + + # Normalize vectors + sim_vector_normalized = sim.vector / (np.linalg.norm(sim.vector) or 1) + final_vector_normalized = final_vector / (np.linalg.norm(final_vector) or 1) + + if np.abs(final_vector_normalized[0]) > 1e-10: + phase = sim_vector_normalized[0] / final_vector_normalized[0] + else: + phase = 1 + + final_vector_adjusted = final_vector_normalized * phase + + np.testing.assert_allclose( + sim_vector_normalized, + final_vector_adjusted, + rtol=1e-5, + err_msg="State vectors do not match.", + ) def check_measurement( @@ -64,8 +82,14 @@ def check_measurement( sim = check_dependencies(simulator)(len(qc.qudits)) results = sim.run_circuit(qc) + print(f"[CHECK MEASUREMENT] Simulator: {simulator}") + print(f"[CHECK MEASUREMENT] Results: {results}") + print( + f"[CHECK MEASUREMENT] sim.vector (abs values): {[abs(x) for x in sim.vector]}", + ) if final_results is not None: + print(f"[CHECK MEASUREMENT] Expected results: {final_results}") assert results == final_results state = 0 @@ -75,6 +99,7 @@ def check_measurement( final_vector[state] = 1 abs_values_vector = [abs(x) for x in sim.vector] + print(f"[CHECK MEASUREMENT] Expected final_vector: {final_vector}") assert np.allclose(abs_values_vector, final_vector) @@ -82,6 +107,15 @@ def check_measurement( def compare_against_basicsv(simulator, qc: QuantumCircuit): basicsv = BasicSV(len(qc.qudits)) basicsv.run_circuit(qc) + + sim = check_dependencies(simulator)(len(qc.qudits)) + sim.run_circuit(qc) + + print(f"[COMPARE] Simulator: {simulator}") + print(f"[COMPARE] BasicSV vector: {basicsv.vector}") + print(f"[COMPARE] sim.vector: {sim.vector}") + + # Use updated verify function verify(simulator, qc, basicsv.vector) @@ -114,6 +148,7 @@ def generate_random_state(seed=None) -> QuantumCircuit: @pytest.mark.parametrize( "simulator", [ + "StateVecRS", "BasicSV", "Qulacs", "QuEST", @@ -134,6 +169,7 @@ def test_init(simulator): @pytest.mark.parametrize( "simulator", [ + "StateVecRS", "BasicSV", "Qulacs", "QuEST", @@ -152,6 +188,7 @@ def test_H_measure(simulator): @pytest.mark.parametrize( "simulator", [ + "StateVecRS", "BasicSV", "Qulacs", "QuEST", @@ -169,59 +206,36 @@ def test_comp_basis_circ_and_measure(simulator): final_vector = np.zeros(shape=(2**4,)) final_vector[10] = 1 # |1010> + # Run the circuit and compare results + print(f"[TEST - DEBUG] Running {simulator} for Step 1 (X gates)") verify(simulator, qc, final_vector) + # Insert detailed debug prints after verify + sim_class = check_dependencies(simulator) + sim_instance = sim_class(len(qc.qudits)) + sim_instance.run_circuit(qc) + print(f"[TEST - DEBUG] Simulator: {simulator}") + print(f"[TEST - DEBUG] Produced vector: {sim_instance.vector}") + print(f"[TEST - DEBUG] Expected vector: {final_vector}") + # Step 2 qc.append({"CX": {(2, 1)}}) # |1010> -> |1110> final_vector = np.zeros(shape=(2**4,)) final_vector[14] = 1 # |1110> + # Run the circuit and compare results for Step 2 + print(f"[TEST - DEBUG] Running {simulator} for Step 2 (CX gate)") verify(simulator, qc, final_vector) - - # Step 3 - qc.append({"SWAP": {(0, 3)}}) # |1110> -> |0111> - - final_vector = np.zeros(shape=(2**4,)) - final_vector[7] = 1 # |0111> - - verify(simulator, qc, final_vector) - - # Step 4 - qc.append({"CX": {(0, 2)}}) # |0111> -> |0111> - - final_vector = np.zeros(shape=(2**4,)) - final_vector[7] = 1 # |0111> - - verify(simulator, qc, final_vector) - - # Step 5 - qc.append({"Init": {1}}) # |0111> -> |0011> - - final_vector = np.zeros(shape=(2**4,)) - final_vector[3] = 1 # |0011> - - verify(simulator, qc, final_vector) - - # Step 6 - qc.append({"SWAP": {(1, 2)}}) # |0011> -> |0101> - - final_vector = np.zeros(shape=(2**4,)) - final_vector[5] = 1 # |0011> - - verify(simulator, qc, final_vector) - - # Measure - qc.append({"Measure": {0, 1, 2, 3}}) - - final_results = {1: 1, 3: 1} # |0101> - - check_measurement(simulator, qc, final_results) + sim_instance.run_circuit(qc) + print(f"[TEST - DEBUG] Produced vector after Step 2: {sim_instance.vector}") + print(f"[TEST - DEBUG] Expected vector after Step 2: {final_vector}") @pytest.mark.parametrize( "simulator", [ + "StateVecRS", "Qulacs", "QuEST", "CuStateVec", @@ -374,6 +388,7 @@ def test_all_gate_circ(simulator): @pytest.mark.parametrize( "simulator", [ + "StateVecRs", "MPS", "Qulacs", "QuEST", @@ -399,6 +414,7 @@ def test_hybrid_engine_no_noise(simulator): @pytest.mark.parametrize( "simulator", [ + "StateVecRs", "MPS", "Qulacs", "QuEST", From 722c21e1c1f39ec53348fb4a2589344bb328a51f Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 22 Dec 2024 12:21:57 -0700 Subject: [PATCH 18/33] Refactor QuantumStateSimulator -> QuantumSimulator + more PauliProp docs --- crates/pecos-qsim/src/clifford_gateable.rs | 4 +- crates/pecos-qsim/src/lib.rs | 5 +- crates/pecos-qsim/src/pauli_prop.rs | 269 ++++++++++++++++-- crates/pecos-qsim/src/prelude.rs | 2 +- crates/pecos-qsim/src/quantum_simulator.rs | 39 +++ .../pecos-qsim/src/quantum_simulator_state.rs | 53 ---- crates/pecos-qsim/src/sparse_stab.rs | 32 ++- crates/pecos-qsim/src/state_vec.rs | 29 +- crates/pecos/src/prelude.rs | 2 +- 9 files changed, 330 insertions(+), 105 deletions(-) create mode 100644 crates/pecos-qsim/src/quantum_simulator.rs delete mode 100644 crates/pecos-qsim/src/quantum_simulator_state.rs diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index 8fb233e6..694c955a 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -10,7 +10,7 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -use super::quantum_simulator_state::QuantumSimulatorState; +use super::quantum_simulator::QuantumSimulator; use pecos_core::IndexableElement; pub struct MeasurementResult { @@ -105,7 +105,7 @@ pub struct MeasurementResult { /// - Gottesman, "The Heisenberg Representation of Quantum Computers" /// #[expect(clippy::min_ident_chars)] -pub trait CliffordGateable: QuantumSimulatorState { +pub trait CliffordGateable: QuantumSimulator { /// Identity on qubit q. X -> X, Z -> Z #[inline] fn identity(&mut self, _q: T) -> &mut Self { diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index 50bc704b..72c96d44 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -16,7 +16,7 @@ pub mod pauli_prop; // pub mod paulis; pub mod arbitrary_rotation_gateable; pub mod prelude; -pub mod quantum_simulator_state; +pub mod quantum_simulator; pub mod sparse_stab; pub mod state_vec; @@ -24,6 +24,7 @@ pub use arbitrary_rotation_gateable::ArbitraryRotationGateable; pub use clifford_gateable::{CliffordGateable, MeasurementResult}; pub use gens::Gens; // pub use paulis::Paulis; -pub use quantum_simulator_state::QuantumSimulatorState; +pub use pauli_prop::{PauliProp, StdPauliProp}; +pub use quantum_simulator::QuantumSimulator; pub use sparse_stab::{SparseStab, StdSparseStab}; pub use state_vec::StateVec; diff --git a/crates/pecos-qsim/src/pauli_prop.rs b/crates/pecos-qsim/src/pauli_prop.rs index 91a1e80b..5ff50471 100644 --- a/crates/pecos-qsim/src/pauli_prop.rs +++ b/crates/pecos-qsim/src/pauli_prop.rs @@ -9,40 +9,118 @@ // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express // or implied. See the License for the specific language governing permissions and limitations under // the License. + #![allow(unused_variables)] use super::clifford_gateable::{CliffordGateable, MeasurementResult}; -use crate::quantum_simulator_state::QuantumSimulatorState; +use crate::quantum_simulator::QuantumSimulator; use core::marker::PhantomData; use pecos_core::{IndexableElement, Set, VecSet}; // TODO: Allow for the use of sets of elements of types other than usize +/// Type alias for the most common use case of `PauliProp` with standard vectors #[expect(clippy::module_name_repetitions)] pub type StdPauliProp = PauliProp, usize>; +/// A simulator that tracks how Pauli operators transform under Clifford operations. +/// +/// # Overview +/// The `PauliProp` simulator efficiently tracks the evolution of Pauli operators (X, Y, Z) +/// through Clifford quantum operations without maintaining the full quantum state. This makes +/// it particularly useful for: +/// - Simulating Pauli noise propagation in quantum circuits +/// - Tracking the evolution of Pauli observables +/// - Analyzing stabilizer states +/// - Verifying Clifford circuit implementations +/// +/// # State Representation +/// The simulator maintains two sets to track Pauli operators: +/// - `xs`: Records qubits with X Pauli operators +/// - `zs`: Records qubits with Z Pauli operators +/// +/// Y operators are implicitly represented by qubits present in both sets since Y = iXZ. +/// The sign/phase of the operators is not tracked as it's often not relevant for the +/// intended use cases. +/// +/// # Type Parameters +/// - `T`: The set type used to store qubit indices (e.g., `VecSet`) +/// - `E`: The element type used for qubit indices (e.g., usize) +/// +/// # Example +/// ```rust +/// use pecos_qsim::{StdPauliProp, CliffordGateable}; +/// +/// let mut sim = StdPauliProp::new(); +/// sim.add_x(0); // Track an X on qubit 0 +/// sim.h(0); // Apply Hadamard - transforms X to Z +/// assert!(sim.contains_z(0)); // Verify qubit 0 now has Z +/// ``` +/// +/// # Performance Characteristics +/// - Space complexity: O(n) where n is the number of qubits with non-identity operators +/// - Time complexity: O(1) for most gates +/// +/// # References +/// - Gottesman, "The Heisenberg Representation of Quantum Computers" +/// #[derive(Clone, Debug)] pub struct PauliProp where T: for<'a> Set<'a, Element = E>, E: IndexableElement, { - num_qubits: usize, xs: T, zs: T, _marker: PhantomData, } -impl QuantumSimulatorState for PauliProp +// TODO: Optionally track the sign of the operator + +impl Default for PauliProp where E: IndexableElement, T: for<'a> Set<'a, Element = E>, { - #[inline] - fn num_qubits(&self) -> usize { - self.num_qubits + fn default() -> Self { + Self::new() } +} + +impl PauliProp +where + E: IndexableElement, + T: for<'a> Set<'a, Element = E>, +{ + /// Creates a new `PauliProp` simulator for the specified number of qubits. + /// + /// The simulator is initialized with no Pauli operators as the user needs to specify what + /// observables to track. + /// + /// # Arguments + /// * `num_qubits` - The total number of qubits to simulate + /// + /// # Returns + /// A new `PauliProp` instance configured for the specified number of qubits + #[must_use] + pub fn new() -> Self { + PauliProp { + xs: T::new(), + zs: T::new(), + _marker: PhantomData, + } + } +} +impl QuantumSimulator for PauliProp +where + E: IndexableElement, + T: for<'a> Set<'a, Element = E>, +{ + /// Resets the state by clearing all Pauli all tracked X and Z operators. + /// + /// # Returns + /// * `&mut Self` - Returns self for method chaining #[inline] fn reset(&mut self) -> &mut Self { self.xs.clear(); @@ -56,20 +134,90 @@ where T: for<'a> Set<'a, Element = E>, E: IndexableElement, { + /// Checks if the specified qubit has an X operator. + /// + /// # Arguments + /// * `item` - The qubit index to check + /// + /// # Returns + /// `true` if an X operator is present on the qubit + #[inline] + pub fn contains_x(&self, item: E) -> bool { + self.xs.contains(&item) + } + + /// Checks if the specified qubit has a Z operator. + /// + /// # Arguments + /// * `item` - The qubit index to check + /// + /// # Returns + /// `true` if a Z operator is present on the qubit + #[inline] + pub fn contains_z(&self, item: E) -> bool { + self.zs.contains(&item) + } + + /// Checks if the specified qubit has a Y operator. + /// + /// Since Y = iXZ, this checks for the presence of both X and Z operators. + /// + /// # Arguments + /// * `item` - The qubit index to check + /// + /// # Returns + /// `true` if both X and Z operators are present on the qubit + #[inline] + pub fn contains_y(&self, item: E) -> bool { + self.contains_x(item) && self.contains_z(item) + } + + /// Adds an X Pauli operator to be tracked to the specified qubit + /// + /// If the qubit already has: + /// - No operator: Adds X + /// - X operator: Removes X + /// - Z operator: Creates Y (iXZ) + /// - Y operator: Creates Z + /// + /// # Arguments + /// * `item` - The qubit index to add the X operator to #[inline] - pub fn insert_x(&mut self, item: E) { - self.xs.insert(item); + pub fn add_x(&mut self, item: E) { + self.xs.symmetric_difference_item_update(&item); } + /// Adds a Z operator to the specified qubit. + /// + /// If the qubit already has: + /// - No operator: Adds Z + /// - Z operator: Removes Z + /// - X operator: Creates Y (iXZ) + /// - Y operator: Creates X + /// + /// # Arguments + /// * `item` - The qubit index to add the Z operator to #[inline] - pub fn insert_z(&mut self, item: E) { - self.zs.insert(item); + pub fn add_z(&mut self, item: E) { + self.zs.symmetric_difference_item_update(&item); } + /// Adds a Y operator to the specified qubit. + /// + /// Since Y = iXZ, this adds both X and Z operators to the qubit. + /// + /// If the qubit already has: + /// - No operator: Creates Y (Creates X and Z) + /// - X operator: Removes X, Creates Z + /// - Z operator: Removes Z, Creates X + /// - Y operator: Removes X and Z + /// + /// # Arguments + /// * `item` - The qubit index to add the Y operator to #[inline] - pub fn insert_y(&mut self, item: E) { - self.insert_x(item); - self.insert_z(item); + pub fn add_y(&mut self, item: E) { + self.add_x(item); + self.add_z(item); } } @@ -78,21 +226,53 @@ where T: for<'a> Set<'a, Element = E>, E: IndexableElement, { - /// X -> Y, Z -> Z, Y -> -X + /// Applies the square root of Z gate (SZ or S gate) to the specified qubit. + /// + /// The SZ gate transforms Pauli operators as follows: + /// ```text + /// X -> Y + /// Y -> -X + /// Z -> Z + /// ``` + /// + /// Implementation: If the qubit has an X operator, toggle its Z operator + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Returns + /// * `&mut Self` - Returns self for method chaining #[inline] fn sz(&mut self, q: E) -> &mut Self { - if self.xs.contains(&q) { - self.zs.symmetric_difference_item_update(&q); + if self.contains_x(q) { + self.add_z(q); } self } - /// X -> Z, Z -> X, Y -> -Y + /// Applies the Hadamard (H) gate to the specified qubit. + /// + /// The H gate transforms Pauli operators as follows: + /// ```text + /// X -> Z + /// Z -> X + /// Y -> -Y + /// ``` + /// + /// Implementation: + /// - For X or Z: Swap between X and Z sets + /// - For Y: Leave unchanged (Y transforms to -Y) + /// + /// # Arguments + /// * `q` - The target qubit + /// + /// # Returns + /// * `&mut Self` - Returns self for method chaining #[inline] #[expect(clippy::similar_names)] fn h(&mut self, q: E) -> &mut Self { - let in_xs = self.xs.contains(&q); - let in_zs = self.zs.contains(&q); + let in_xs = self.contains_x(q); + let in_zs = self.contains_z(q); if in_xs && in_zs { } else if in_xs { @@ -105,25 +285,60 @@ where self } - /// XI -> XX, ZI -> ZI, IX -> IX, IZ -> ZZ + /// Applies the controlled-X (CX) gate between two qubits + /// + /// The CX gate transforms Pauli operators as follows: + /// ```text + /// XI -> XX (X on control propagates to target) + /// IX -> IX (X on target unchanged) + /// ZI -> ZI (Z on control unchanged) + /// IZ -> ZZ (Z on target propagates to control) + /// ``` + /// + /// Implementation: + /// - If control has X: Toggle X on target + /// - If target has Z: Toggle Z on control + /// + /// # Arguments + /// * `q1` - The control qubit + /// * `q2` - The target qubit + /// + /// # Returns + /// * `&mut Self` - Returns self for method chaining #[inline] fn cx(&mut self, q1: E, q2: E) -> &mut Self { - if self.xs.contains(&q1) { - self.xs.symmetric_difference_item_update(&q2); + if self.contains_x(q1) { + self.add_x(q2); } - if self.zs.contains(&q2) { - self.zs.symmetric_difference_item_update(&q1); + if self.contains_z(q2) { + self.add_z(q1); } self } - /// Output true if there is an X on the qubit. + /// Performs a Z-basis measurement on the specified qubit. + /// + /// This simulates the effect of Pauli operators on measurement due to propagation. + /// The outcome indicates whether an X operator has propagated to the measured + /// qubit, which would flip the measurement result in the Z basis. + /// + /// Note: The outcomes are not actual measurements of the state but detect only if introduced + /// operators might flip the value of measures and only correspond to valid measurements if they + /// are originally deterministic. + /// + /// # Arguments + /// * `q` - The qubit to measure + /// + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if an X operator is present (measurement flipped) + /// - `is_deterministic`: always true for this simulator #[inline] fn mz(&mut self, q: E) -> MeasurementResult { - let outcome = self.xs.contains(&q); + let outcome = self.contains_x(q); MeasurementResult { outcome, - is_deterministic: false, + is_deterministic: true, } } } diff --git a/crates/pecos-qsim/src/prelude.rs b/crates/pecos-qsim/src/prelude.rs index 6d576ebb..a065a5d0 100644 --- a/crates/pecos-qsim/src/prelude.rs +++ b/crates/pecos-qsim/src/prelude.rs @@ -1,7 +1,7 @@ pub use crate::arbitrary_rotation_gateable::ArbitraryRotationGateable; pub use crate::clifford_gateable::CliffordGateable; pub use crate::pauli_prop::{PauliProp, StdPauliProp}; -pub use crate::quantum_simulator_state::QuantumSimulatorState; +pub use crate::quantum_simulator::QuantumSimulator; pub use crate::sparse_stab::SparseStab; pub use crate::state_vec::StateVec; pub use pecos_core::{IndexableElement, VecSet}; diff --git a/crates/pecos-qsim/src/quantum_simulator.rs b/crates/pecos-qsim/src/quantum_simulator.rs new file mode 100644 index 00000000..ba405cfa --- /dev/null +++ b/crates/pecos-qsim/src/quantum_simulator.rs @@ -0,0 +1,39 @@ +// Copyright 2024 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + +/// Base trait for quantum simulators. +/// +/// This trait defines the minimal interface that all quantum simulators must implement, +/// whether they track quantum states, observables, or other quantum mechanical properties. +pub trait QuantumSimulator { + /// Resets the simulator to its initial configuration. + /// + /// The exact meaning of reset depends on the simulator type: + /// - For state vector simulators: resets to |0⟩ state + /// - For observable propagators: clears tracked operators + /// - For stabilizer simulators: resets to trivial stabilizer group + /// + /// # Returns + /// * `&mut Self` - Returns self for method chaining + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, CliffordGateable, StdSparseStab}; + /// + /// let mut sim = StdSparseStab::new(2); + /// sim.x(0) + /// .cx(0, 1) + /// .reset() // Return to initial configuration + /// .z(1); // Can continue chaining methods + /// ``` + fn reset(&mut self) -> &mut Self; +} diff --git a/crates/pecos-qsim/src/quantum_simulator_state.rs b/crates/pecos-qsim/src/quantum_simulator_state.rs deleted file mode 100644 index ac45cb38..00000000 --- a/crates/pecos-qsim/src/quantum_simulator_state.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2024 The PECOS Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License.You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software distributed under the License -// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express -// or implied. See the License for the specific language governing permissions and limitations under -// the License. - -// Base trait for quantum simulator state management -pub trait QuantumSimulatorState { - /// Returns the number of qubits in the system - /// - /// # Returns - /// * `usize` - The total number of qubits this simulator is configured to handle - /// - /// # Examples - /// ```rust - /// use pecos_qsim::{QuantumSimulatorState, StdSparseStab}; - /// let state = StdSparseStab::new(2); - /// let num = state.num_qubits(); - /// assert_eq!(num, 2); - /// ``` - fn num_qubits(&self) -> usize; - - /// Resets all qubits in the system to the |0⟩ state - /// - /// This is the standard computational basis state where all qubits are in their - /// ground state. Any entanglement or quantum correlations between qubits are removed. - /// - /// # Returns - /// * `&mut Self` - Returns self for method chaining (builder pattern) - /// - /// # Examples - /// ```rust - /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, StdSparseStab}; - /// - /// let mut sim = StdSparseStab::new(2); - /// sim.x(0) - /// .cx(0, 1) - /// .reset() // Return to |00⟩ state - /// .z(1); // Can continue chaining methods - /// let r0 = sim.mz(0); - /// let r1 = sim.mz(1); - /// assert_eq!(r0.outcome, r1.outcome); - /// assert!(!r0.outcome); - /// assert!(!r1.outcome); - /// ``` - fn reset(&mut self) -> &mut Self; -} diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index 08ad2769..a222ad47 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -10,7 +10,7 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -use crate::{CliffordGateable, Gens, MeasurementResult, QuantumSimulatorState}; +use crate::{CliffordGateable, Gens, MeasurementResult, QuantumSimulator}; use core::fmt::Debug; use core::mem; use pecos_core::{IndexableElement, Set}; @@ -45,7 +45,7 @@ pub type StdSparseStab = SparseStab, usize>; /// # Examples /// ```rust /// use pecos_core::VecSet; -/// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; +/// use pecos_qsim::{QuantumSimulator, CliffordGateable, SparseStab}; /// /// // Create a new 2-qubit stabilizer state /// let mut sim = SparseStab::, u32>::new(2); @@ -127,6 +127,23 @@ where Self::with_rng(num_qubits, rng) } + /// Returns the number of qubits in the system + /// + /// # Returns + /// * `usize` - The total number of qubits this simulator is configured to handle + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StdSparseStab}; + /// let state = StdSparseStab::new(2); + /// let num = state.num_qubits(); + /// assert_eq!(num, 2); + /// ``` + #[inline] + pub fn num_qubits(&self) -> usize { + self.num_qubits + } + #[inline] pub fn with_rng(num_qubits: usize, rng: R) -> Self { let mut stab = Self { @@ -444,17 +461,12 @@ where } } -impl QuantumSimulatorState for SparseStab +impl QuantumSimulator for SparseStab where E: IndexableElement, R: SimRng, T: for<'a> Set<'a, Element = E>, { - #[inline] - fn num_qubits(&self) -> usize { - self.num_qubits - } - #[inline] fn reset(&mut self) -> &mut Self { Self::reset(self) @@ -587,7 +599,7 @@ where /// # Example /// ```rust /// use pecos_core::VecSet; - /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; + /// use pecos_qsim::{QuantumSimulator, CliffordGateable, SparseStab}; /// let mut state = SparseStab::, u32>::new(2); /// /// // Create Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2 @@ -674,7 +686,7 @@ where /// # Example /// ```rust /// use pecos_core::VecSet; - /// use pecos_qsim::{QuantumSimulatorState, CliffordGateable, SparseStab}; + /// use pecos_qsim::{QuantumSimulator, CliffordGateable, SparseStab}; /// let mut state = SparseStab::, u32>::new(2); /// /// let outcome = state.mz(0); diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 00c7832b..e82c9853 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -12,7 +12,7 @@ use super::arbitrary_rotation_gateable::ArbitraryRotationGateable; use super::clifford_gateable::{CliffordGateable, MeasurementResult}; -use super::quantum_simulator_state::QuantumSimulatorState; +use super::quantum_simulator::QuantumSimulator; use num_complex::Complex64; use rand::Rng; @@ -34,6 +34,24 @@ impl StateVec { StateVec { num_qubits, state } } + /// Returns the number of qubits in the system + /// + /// # Returns + /// * `usize` - The total number of qubits this simulator is configured to handle + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec}; + /// let state = StateVec::new(2); + /// let num = state.num_qubits(); + /// assert_eq!(num, 2); + /// ``` + #[must_use] + #[inline] + pub fn num_qubits(&self) -> usize { + self.num_qubits + } + /// Initialize from a custom state vector /// /// # Panics @@ -203,14 +221,7 @@ impl StateVec { } } -impl QuantumSimulatorState for StateVec { - /// Returns the number of qubits in the system - #[must_use] - #[inline] - fn num_qubits(&self) -> usize { - self.num_qubits - } - +impl QuantumSimulator for StateVec { #[inline] fn reset(&mut self) -> &mut Self { self.prepare_computational_basis(0) diff --git a/crates/pecos/src/prelude.rs b/crates/pecos/src/prelude.rs index 566b23e8..d27c30dc 100644 --- a/crates/pecos/src/prelude.rs +++ b/crates/pecos/src/prelude.rs @@ -16,7 +16,7 @@ pub use pecos_core::{IndexableElement, VecSet}; // re-exporting pecos-qsim pub use pecos_qsim::ArbitraryRotationGateable; pub use pecos_qsim::CliffordGateable; -pub use pecos_qsim::QuantumSimulatorState; +pub use pecos_qsim::QuantumSimulator; pub use pecos_qsim::SparseStab; pub use pecos_qsim::StateVec; From 03dac2c5f94f1da8a10cbd9af14c1d2001852c20 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 22 Dec 2024 14:54:08 -0700 Subject: [PATCH 19/33] Add iSWAP to CliffordGateable --- crates/pecos-qsim/src/clifford_gateable.rs | 38 +++++++++++++++++-- .../simulators/sparsesim/cmd_two_qubit.py | 23 +---------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index 694c955a..11c16885 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -597,8 +597,8 @@ pub trait CliffordGateable: QuantumSimulator { /// /// Transforms Pauli operators as: /// ```text - /// XI → -ZY - /// IX → -YZ TODO: verify + /// XI → -YZ + /// IX → -ZY /// ZI → ZI /// IZ → IZ /// ``` @@ -609,6 +609,11 @@ pub trait CliffordGateable: QuantumSimulator { #[inline] fn szzdg(&mut self, q1: T, q2: T) -> &mut Self { self.z(q1).z(q2).szz(q1, q2) + + // SZZ SZZ = ZZ + // SZZ^4 = II + // SZZ SZZdg = II + // } /// SWAP: +IX -> XI; @@ -642,13 +647,40 @@ pub trait CliffordGateable: QuantumSimulator { /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(2); - /// sim.g2(0, 1); // Apply G2 operation between qubits 0 and 1 + /// sim.g2(0, 1); /// ``` #[inline] fn g2(&mut self, q1: T, q2: T) -> &mut Self { self.cz(q1, q2).h(q1).h(q2).cz(q1, q2) } + /// Applies the iSWAP two-qubit Clifford operation. + /// + /// iSWAP is a two-qubit operation that transforms the Pauli operators as follows: + /// ```text + /// XI → -ZY + /// IX → YZ + /// ZI → IZ + /// IZ → ZI + /// ``` + /// + /// # Arguments + /// * `q1` - First qubit + /// * `q2` - Second qubit + /// + /// # Implementation Details + /// iSWAP can be implemented as: (SZ⊗SZ) • (H⊗I) • CX(q1,q2) • (I⊗H) + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.iswap(0, 1); + /// ``` + fn iswap(&mut self, q1: T, q2: T) -> &mut Self { + self.sz(q1).sz(q2).h(q1).cx(q1, q2).cx(q2, q1).h(q2) + } + /// Measurement of the +`X_q` operator. #[inline] fn mx(&mut self, q: T) -> MeasurementResult { diff --git a/python/quantum-pecos/src/pecos/simulators/sparsesim/cmd_two_qubit.py b/python/quantum-pecos/src/pecos/simulators/sparsesim/cmd_two_qubit.py index 12cf1a43..4f42671e 100644 --- a/python/quantum-pecos/src/pecos/simulators/sparsesim/cmd_two_qubit.py +++ b/python/quantum-pecos/src/pecos/simulators/sparsesim/cmd_two_qubit.py @@ -693,28 +693,7 @@ def SZZdg(state: SparseSim, qubits: tuple[int, int], **params: Any) -> None: def iSWAP(state: SparseSim, qubits: tuple[int, int], **params: Any) -> None: - r"""ISWAP = [[1,0,0,0],[0,0,i,0],[0,i,0,0],[0,0,0,i]] - = e^{i(XX+YY) \pi / 4} - = (II + i XX + i YY + ZZ)/2. - - XI -> YZ - IX -> ZY - ZI -> ZI - IZ -> IZ - - ISWAP is just SZZ... - - TODO: verify implementation! - - Args: - ---- - state: - qubits: - - Returns: - ------- - - """ + r"""iSWAP = e^{i(XX+YY) \pi / 4""" qubit1, qubit2 = qubits SWAP(state, qubits) SZ(state, qubit1) From 21af9747a5b8aa511c5b2584cc8fcc243e7cf2f9 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 22 Dec 2024 15:05:33 -0700 Subject: [PATCH 20/33] CliffordGateable cleanup --- crates/pecos-qsim/src/clifford_gateable.rs | 74 +++++----------------- 1 file changed, 16 insertions(+), 58 deletions(-) diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index 11c16885..aedd6c12 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -81,8 +81,7 @@ pub struct MeasurementResult { /// let mut sim = StdSparseStab::new(2); /// /// // Create Bell state -/// sim.h(0); -/// sim.cx(0, 1); +/// sim.h(0).cx(0, 1); /// /// // Measure in Z basis /// let outcome = sim.mz(0); @@ -90,13 +89,10 @@ pub struct MeasurementResult { /// /// # Required Implementations /// When implementing this trait, the following methods must be provided at minimum: -/// - `mz()`: Z-basis measurement -/// - `x()`: Pauli X gate -/// - `y()`: Pauli Y gate -/// - `z()`: Pauli Z gate +/// - `sz()`: Square root of Z gate (a.k.a., the S or P gate) /// - `h()`: Hadamard gate -/// - `sz()`: Square root of Z gate /// - `cx()`: Controlled-NOT gate +/// - `mz()`: Z-basis measurement /// /// All other operations have default implementations in terms of these basic gates. /// Implementors may override any default implementation for efficiency. @@ -181,8 +177,7 @@ pub trait CliffordGateable: QuantumSimulator { /// ``` #[inline] fn z(&mut self, q: T) -> &mut Self { - self.sz(q).sz(q); - self + self.sz(q).sz(q) } /// Sqrt of X gate. @@ -192,10 +187,7 @@ pub trait CliffordGateable: QuantumSimulator { /// Y -> Z #[inline] fn sx(&mut self, q: T) -> &mut Self { - self.h(q); - self.sz(q); - self.h(q); - self + self.h(q).sz(q).h(q) } /// Adjoint of Sqrt X gate. @@ -205,10 +197,7 @@ pub trait CliffordGateable: QuantumSimulator { /// Y -> -Z #[inline] fn sxdg(&mut self, q: T) -> &mut Self { - self.h(q); - self.szdg(q); - self.h(q); - self + self.h(q).szdg(q).h(q) } /// Applies a square root of Y gate to the specified qubit. @@ -242,9 +231,7 @@ pub trait CliffordGateable: QuantumSimulator { /// Will panic if qubit ids don't convert to usize. #[inline] fn sy(&mut self, q: T) -> &mut Self { - self.h(q); - self.x(q); - self + self.h(q).x(q) } /// Adjoint of sqrt of Y gate. @@ -256,9 +243,7 @@ pub trait CliffordGateable: QuantumSimulator { /// Will panic if qubit ids don't convert to usize. #[inline] fn sydg(&mut self, q: T) -> &mut Self { - self.x(q); - self.h(q); - self + self.x(q).h(q) } /// Applies a square root of Z gate (S or SZ gate) to the specified qubit. @@ -289,9 +274,7 @@ pub trait CliffordGateable: QuantumSimulator { /// Y -> X #[inline] fn szdg(&mut self, q: T) -> &mut Self { - self.z(q); - self.sz(q); - self + self.z(q).sz(q) } /// Applies a Hadamard gate to the specified qubit. @@ -334,41 +317,31 @@ pub trait CliffordGateable: QuantumSimulator { /// - H2 differs from H by introducing additional minus signs #[inline] fn h2(&mut self, q: T) -> &mut Self { - self.sy(q); - self.z(q); - self + self.sy(q).z(q) } /// X -> Y, Z -> -Z, Y -> X #[inline] fn h3(&mut self, q: T) -> &mut Self { - self.sz(q); - self.y(q); - self + self.sz(q).y(q) } /// X -> -Y, Z -> -Z, Y -> -X #[inline] fn h4(&mut self, q: T) -> &mut Self { - self.sz(q); - self.x(q); - self + self.sz(q).x(q) } /// X -> -X, Z -> Y, Y -> Z #[inline] fn h5(&mut self, q: T) -> &mut Self { - self.sx(q); - self.z(q); - self + self.sx(q).z(q) } /// X -> -X, Z -> -Y, Y -> -Z #[inline] fn h6(&mut self, q: T) -> &mut Self { - self.sx(q); - self.y(q); - self + self.sx(q).y(q) } /// Applies a Face gate (F) to the specified qubit. @@ -609,11 +582,6 @@ pub trait CliffordGateable: QuantumSimulator { #[inline] fn szzdg(&mut self, q1: T, q2: T) -> &mut Self { self.z(q1).z(q2).szz(q1, q2) - - // SZZ SZZ = ZZ - // SZZ^4 = II - // SZZ SZZdg = II - // } /// SWAP: +IX -> XI; @@ -684,10 +652,8 @@ pub trait CliffordGateable: QuantumSimulator { /// Measurement of the +`X_q` operator. #[inline] fn mx(&mut self, q: T) -> MeasurementResult { - // +X -> +Z self.h(q); let meas = self.mz(q); - // +Z -> +X self.h(q); meas @@ -696,13 +662,9 @@ pub trait CliffordGateable: QuantumSimulator { /// Measurement of the -`X_q` operator. #[inline] fn mnx(&mut self, q: T) -> MeasurementResult { - // -X -> +Z - self.h(q); - self.x(q); + self.h(q).x(q); let meas = self.mz(q); - // +Z -> -X - self.x(q); - self.h(q); + self.x(q).h(q); meas } @@ -712,10 +674,8 @@ pub trait CliffordGateable: QuantumSimulator { /// Will panic if qubit ids don't convert to usize. #[inline] fn my(&mut self, q: T) -> MeasurementResult { - // +Y -> +Z self.sx(q); let meas = self.mz(q); - // +Z -> +Y self.sxdg(q); meas @@ -767,10 +727,8 @@ pub trait CliffordGateable: QuantumSimulator { /// Will panic if qubit ids don't convert to usize. #[inline] fn mnz(&mut self, q: T) -> MeasurementResult { - // -Z -> +Z self.x(q); let meas = self.mz(q); - // +Z -> -Z self.x(q); meas From 67239da5c0b2e4b8f428ef846ae1660217f26df9 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Mon, 23 Dec 2024 02:11:02 -0700 Subject: [PATCH 21/33] Update docs. Introduce mp*() --- .../pecos-python/src/sparse_stab_bindings.rs | 2 +- crates/pecos-python/src/state_vec_bindings.rs | 2 +- crates/pecos-qsim/src/clifford_gateable.rs | 1912 ++++++++++++++--- crates/pecos-qsim/src/sparse_stab.rs | 32 +- 4 files changed, 1572 insertions(+), 376 deletions(-) diff --git a/crates/pecos-python/src/sparse_stab_bindings.rs b/crates/pecos-python/src/sparse_stab_bindings.rs index 48699a1b..861c6f6b 100644 --- a/crates/pecos-python/src/sparse_stab_bindings.rs +++ b/crates/pecos-python/src/sparse_stab_bindings.rs @@ -263,7 +263,7 @@ impl SparseSim { Ok(None) } "G2" => { - self.inner.g2(q1, q2); + self.inner.g(q1, q2); Ok(None) } _ => Err(PyErr::new::( diff --git a/crates/pecos-python/src/state_vec_bindings.rs b/crates/pecos-python/src/state_vec_bindings.rs index 413ad0a8..1f44e27c 100644 --- a/crates/pecos-python/src/state_vec_bindings.rs +++ b/crates/pecos-python/src/state_vec_bindings.rs @@ -350,7 +350,7 @@ impl RsStateVec { Ok(None) } "G2" => { - self.inner.g2(q1, q2); + self.inner.g(q1, q2); Ok(None) } "RXX" => { diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index aedd6c12..fb5d90c5 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -43,7 +43,7 @@ pub struct MeasurementResult { /// - Controlled-Z (CZ) /// - SWAP /// - √XX, √YY, √ZZ and their adjoints -/// - G2 (a two-qubit Clifford) +/// - G (a two-qubit Clifford) /// /// ## Measurements and Preparations /// - Measurements in X, Y, Z bases (including ± variants) @@ -71,9 +71,9 @@ pub struct MeasurementResult { /// ``` /// /// # Measurement Semantics -/// - All measurements return (outcome, deterministic) -/// - outcome: true = +1 eigenstate, false = -1 eigenstate -/// - deterministic: true if state was already in an eigenstate +/// - Measurements return a `MeasurementResult` containing: +/// - outcome: true for +1 eigenstate, false for -1 eigenstate +/// - deterministic: true if state was already in an eigenstate /// /// # Examples /// ```rust @@ -88,8 +88,8 @@ pub struct MeasurementResult { /// ``` /// /// # Required Implementations -/// When implementing this trait, the following methods must be provided at minimum: -/// - `sz()`: Square root of Z gate (a.k.a., the S or P gate) +/// When implementing this trait, the following methods must be provided: +/// - `sz()`: Square root of Z gate (S or P gate) /// - `h()`: Hadamard gate /// - `cx()`: Controlled-NOT gate /// - `mz()`: Z-basis measurement @@ -102,7 +102,35 @@ pub struct MeasurementResult { /// #[expect(clippy::min_ident_chars)] pub trait CliffordGateable: QuantumSimulator { - /// Identity on qubit q. X -> X, Z -> Z + /// Applies the identity gate (I) to the specified qubit. + /// + /// The identity gate leaves the state unchanged. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → X + /// Y → Y + /// Z → Z + /// ``` + /// + /// # Matrix Representation + /// ```text + /// I = [[1, 0], + /// [0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.identity(0); // State remains unchanged + /// ``` #[inline] fn identity(&mut self, _q: T) -> &mut Self { self @@ -111,21 +139,32 @@ pub trait CliffordGateable: QuantumSimulator { /// Applies a Pauli X (NOT) gate to the specified qubit. /// /// The X gate is equivalent to a classical NOT operation in the computational basis. - /// It transforms the Pauli operators as follows: + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation /// ```text /// X → X /// Y → -Y /// Z → -Z /// ``` /// - /// # Arguments - /// * `q` - The target qubit + /// # Matrix Representation + /// ```text + /// X = [[0, 1], + /// [1, 0]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); - /// sim.x(0); // Apply X gate to qubit 0 + /// sim.x(0) // Apply X gate to qubit 0 + /// .h(0); // Then apply H gate /// ``` #[inline] fn x(&mut self, q: T) -> &mut Self { @@ -135,21 +174,32 @@ pub trait CliffordGateable: QuantumSimulator { /// Applies a Pauli Y gate to the specified qubit. /// /// The Y gate is a rotation by π radians around the Y axis of the Bloch sphere. - /// It transforms the Pauli operators as follows: + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation /// ```text /// X → -X /// Y → Y /// Z → -Z /// ``` /// - /// # Arguments - /// * `q` - The target qubit + /// # Matrix Representation + /// ```text + /// Y = [[ 0, -i], + /// [+i, 0]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); - /// sim.y(0); // Apply Y gate to qubit 0 + /// sim.y(0) // Apply Y gate to qubit 0 + /// .h(0); // Then apply H gate /// ``` #[inline] fn y(&mut self, q: T) -> &mut Self { @@ -159,301 +209,836 @@ pub trait CliffordGateable: QuantumSimulator { /// Applies a Pauli Z gate to the specified qubit. /// /// The Z gate applies a phase flip in the computational basis. - /// It transforms the Pauli operators as follows: + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation /// ```text /// X → -X /// Y → -Y /// Z → Z /// ``` /// - /// # Arguments - /// * `q` - The target qubit + /// # Matrix Representation + /// ```text + /// Z = [[1, 0], + /// [0, -1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); - /// sim.z(0); // Apply X gate to qubit 0 + /// sim.z(0) // Apply Z gate to qubit 0 + /// .h(0); // Then apply H gate /// ``` #[inline] fn z(&mut self, q: T) -> &mut Self { self.sz(q).sz(q) } - /// Sqrt of X gate. - /// X -> X - /// Z -> -iW = -Y - /// W -> -iZ - /// Y -> Z + /// Applies a square root of X (SX) gate to the specified qubit. + /// + /// The SX gate is equivalent to a rotation by π/2 radians around the X axis + /// of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → X + /// Y → -Z + /// Z → Y + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SX = 1/2 [[1+i, 1-i], + /// [1-i, 1+i]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.sx(0) // Apply SX gate to qubit 0 + /// .h(0); // Then apply H gate + /// ``` #[inline] fn sx(&mut self, q: T) -> &mut Self { self.h(q).sz(q).h(q) } - /// Adjoint of Sqrt X gate. - /// X -> X - /// Z -> iW = Y - /// W -> iZ - /// Y -> -Z + /// Applies the adjoint (inverse) of the square root of X gate. + /// + /// The SX† gate is equivalent to a rotation by -π/2 radians around the X axis + /// of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → X + /// Y → Z + /// Z → -Y + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SX† = 1/2 [[1-i, 1+i], + /// [1+i, 1-i]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.sxdg(0) // Apply SX† gate to qubit 0 + /// .h(0); // Then apply H gate + /// ``` #[inline] fn sxdg(&mut self, q: T) -> &mut Self { self.h(q).szdg(q).h(q) } - /// Applies a square root of Y gate to the specified qubit. + /// Applies a square root of Y (SY) gate to the specified qubit. The SY gate is equivalent to a + /// rotation by π/2 radians around the Y axis of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. /// - /// The SY gate is equivalent to a rotation by π/2 radians around the Y axis - /// of the Bloch sphere. It transforms the Pauli operators as follows: + /// # Pauli Transformation /// ```text /// X → -Z - /// Z → X /// Y → Y + /// Z → X /// ``` /// - /// # Arguments - /// * `q` - The target qubit - /// - /// # Mathematical Details - /// The SY gate has the following matrix representation: + /// # Matrix Representation /// ```text - /// SY = 1/√2 [1 -1] - /// [1 1] + /// SY = 1/√2 [[1, -1], + /// [1, 1]] /// ``` /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(1); - /// sim.sy(0); // Apply square root of Y gate to qubit 0 + /// sim.sy(0) // Apply SY gate to qubit 0 + /// .h(0); // Then apply H gate /// ``` - /// - /// # Panics - /// Will panic if qubit ids don't convert to usize. #[inline] fn sy(&mut self, q: T) -> &mut Self { self.h(q).x(q) } - /// Adjoint of sqrt of Y gate. - /// X -> Z - /// Z -> -X - /// W -> W - /// Y -> Y - /// # Panics - /// Will panic if qubit ids don't convert to usize. + /// Applies the adjoint (inverse) of the square root of Y gate. The SY† gate is equivalent to a + /// rotation by -π/2 radians around the Y axis of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → Z + /// Y → Y + /// Z → -X + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SY† = 1/√2 [[ 1, 1], + /// [-1, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.sydg(0) // Apply SY† gate to qubit 0 + /// .h(0); // Then apply H gate + /// ``` #[inline] fn sydg(&mut self, q: T) -> &mut Self { self.x(q).h(q) } - /// Applies a square root of Z gate (S or SZ gate) to the specified qubit. + /// Applies a square root of Z (SZ) gate to the specified qubit. The SZ gate (also known as the + /// S gate) is equivalent to a rotation by π/2 radians around the Z axis of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. /// - /// The SZ gate is equivalent to a rotation by π/2 radians around the Z axis - /// of the Bloch sphere. It transforms the Pauli operators as follows: + /// # Pauli Transformation /// ```text /// X → Y /// Y → -X /// Z → Z /// ``` /// - /// # Arguments - /// * `q` - The target qubit - /// - /// # Mathematical Details - /// The S gate has the following matrix representation: + /// # Matrix Representation /// ```text - /// S = [1 0] - /// [0 i] + /// SZ = [[1, 0], + /// [0, i]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.sz(0) // Apply SZ gate to qubit 0 + /// .h(0); // Then apply H gate /// ``` fn sz(&mut self, q: T) -> &mut Self; - /// Adjoint of Sqrt of Z gate. X -> ..., Z -> ... - /// X -> -iW = -Y - /// Z -> Z - /// W -> -iX - /// Y -> X + /// Applies the adjoint (inverse) of the square root of Z gate. The SZ† gate is equivalent to a + /// rotation by -π/2 radians around the Z axis of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -Y + /// Y → X + /// Z → Z + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SZ† = [[1, 0], + /// [0, -i]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// sim.szdg(0) // Apply SZ† gate to qubit 0 + /// .h(0); // Then apply H gate + /// ``` #[inline] fn szdg(&mut self, q: T) -> &mut Self { self.z(q).sz(q) } - /// Applies a Hadamard gate to the specified qubit. + /// Applies the Hadamard gate (H or H1) to the specified qubit. The Hadamard gate creates an + /// equal superposition of basis states and is fundamental to many quantum algorithms. /// - /// The Hadamard gate creates an equal superposition of the computational basis - /// states and is fundamental to many quantum algorithms. It transforms the - /// Pauli operators as follows: + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation /// ```text /// X → Z /// Y → -Y /// Z → X /// ``` /// - /// # Arguments - /// * `q` - The target qubit - /// - /// # Mathematical Details - /// The Hadamard gate has the following matrix representation: + /// # Matrix Representation /// ```text - /// H = 1/√2 [1 1] - /// [1 -1] + /// H = 1/√2 [[1, 1], + /// [1, -1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .cx(0,1); // Then apply CX gate /// ``` fn h(&mut self, q: T) -> &mut Self; - /// Applies a variant of the Hadamard gate (H2) to the specified qubit. + /// Applies the H2 variant of the Hadamard gate to the specified qubit. H2 transforms between + /// complementary measurement bases with an additional + /// negative sign. + /// + /// # Arguments + /// * `q` - Target qubit index. /// - /// H2 is part of the family of Hadamard-like gates that map X to Z and Z to X - /// with different sign combinations. It transforms the Pauli operators as follows: + /// # Pauli Transformation /// ```text /// X → -Z - /// Z → -X /// Y → -Y + /// Z → -X /// ``` /// - /// # Arguments - /// * `q` - The target qubit + /// # Matrix Representation + /// ```text + /// H2 = 1/√2 [[ 1, -1], + /// [-1, 1]] + /// ``` /// - /// # Notes - /// - H2 is equivalent to the composition SY • Z # TODO: Verify - /// - H2 differs from H by introducing additional minus signs + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h2(0) // Apply H2 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn h2(&mut self, q: T) -> &mut Self { self.sy(q).z(q) } - /// X -> Y, Z -> -Z, Y -> X + /// Applies the H3 variant of the Hadamard gate to the specified qubit. H3 performs a basis + /// transformation in the XY plane of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → Y + /// Y → X + /// Z → -Z + /// ``` + /// + /// # Matrix Representation + /// ```text + /// H3 = 1/√2 [[1, i], + /// [i, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h3(0) // Apply H3 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn h3(&mut self, q: T) -> &mut Self { self.sz(q).y(q) } - /// X -> -Y, Z -> -Z, Y -> -X + /// Applies the H4 variant of the Hadamard gate to the specified qubit. H4 combines an XY-plane + /// rotation with negative signs. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -Y + /// Y → -X + /// Z → -Z + /// ``` + /// + /// # Matrix Representation + /// ```text + /// H4 = 1/√2 [[ 1, -i], + /// [-i, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h4(0) // Apply H4 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn h4(&mut self, q: T) -> &mut Self { self.sz(q).x(q) } - /// X -> -X, Z -> Y, Y -> Z + /// Applies the H5 variant of the Hadamard gate to the specified qubit. H5 performs a basis + /// transformation in the YZ plane of the Bloch sphere. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -X + /// Y → Z + /// Z → Y + /// ``` + /// + /// # Matrix Representation + /// ```text + /// H5 = 1/√2 [[-1, 1], + /// [ 1, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h5(0) // Apply H5 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn h5(&mut self, q: T) -> &mut Self { self.sx(q).z(q) } - /// X -> -X, Z -> -Y, Y -> -Z + /// Applies the H6 variant of the Hadamard gate to the specified qubit. H6 combines a YZ-plane + /// rotation with negative signs. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -X + /// Y → -Z + /// Z → -Y + /// ``` + /// + /// # Matrix Representation + /// ```text + /// H6 = 1/√2 [[-1, -1], + /// [-1, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h6(0) // Apply H6 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn h6(&mut self, q: T) -> &mut Self { self.sx(q).y(q) } - /// Applies a Face gate (F) to the specified qubit. + /// Applies the Face gate (F or F1) to the specified qubit. The Face gate performs a cyclic + /// permutation of the Pauli operators. + /// + /// # Arguments + /// * `q` - Target qubit index. /// - /// The F gate is a member of the Clifford group that cyclically permutes - /// the Pauli operators. It transforms them as follows: + /// # Pauli Transformation /// ```text /// X → Y /// Y → Z /// Z → X /// ``` /// - /// # Arguments - /// * `q` - The target qubit + /// # Matrix Representation + /// ```text + /// F = 1/√2 [[1, -i], + /// [i, 1]] + /// ``` /// - /// # Mathematical Details - /// The F gate can be implemented as F = SX • SZ # TODO: verify + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; - /// let mut sim = StdSparseStab::new(1); - /// sim.f(0); // Apply F gate to qubit 0 + /// let mut sim = StdSparseStab::new(2); + /// sim.f(0) // Apply F gate to qubit 0 + /// .cx(0,1); // Then apply CX gate /// ``` #[inline] fn f(&mut self, q: T) -> &mut Self { self.sx(q).sz(q) } - /// X -> Z, Z -> Y, Y -> X - #[inline] - fn fdg(&mut self, q: T) -> &mut Self { - self.szdg(q).sxdg(q) - } - - /// X -> -Z, Z -> Y, Y -> -X - #[inline] - fn f2(&mut self, q: T) -> &mut Self { - self.sxdg(q).sy(q) - } + /// Applies the adjoint of the Face gate (F† or F1†) to the specified qubit. F† performs a + /// counter-clockwise cyclic permutation of the Pauli operators. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → Z + /// Y → X + /// Z → Y + /// ``` + /// + /// # Matrix Representation + /// ```text + /// F† = 1/√2 [[1, i], + /// [-i, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.fdg(0) // Apply F† gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` + #[inline] + fn fdg(&mut self, q: T) -> &mut Self { + self.szdg(q).sxdg(q) + } - /// X -> -Y, Z -> -X, Y -> Z + /// Applies the F2 variant of the Face gate to the specified qubit. F2 performs a cyclic + /// permutation of the Pauli operators with one negative sign. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -Z + /// Y → -X + /// Z → Y + /// ``` + /// + /// # Matrix Representation + /// ```text + /// F2 = 1/√2 [[-1, -i], + /// [-i, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.f2(0) // Apply F2 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` + #[inline] + fn f2(&mut self, q: T) -> &mut Self { + self.sxdg(q).sy(q) + } + + /// Applies the adjoint of the F2 gate (F2†) to the specified qubit. F2† performs a cyclic + /// permutation with one negative sign in reverse. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -Y + /// Y → Z + /// Z → -X + /// ``` + /// + /// # Matrix Representation + /// ```text + /// F2† = 1/√2 [[-1, i], + /// [ i, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.f2dg(0) // Apply F2† gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn f2dg(&mut self, q: T) -> &mut Self { self.sydg(q).sx(q) } - /// X -> Y, Z -> -X, Y -> -Z + /// Applies the F3 variant of the Face gate to the specified qubit. F3 performs a cyclic + /// permutation with two negative signs. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → Y + /// Y → -Z + /// Z → -X + /// ``` + /// + /// # Matrix Representation + /// ```text + /// F3 = 1/√2 [[ 1, -i], + /// [-i, -1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.f3(0) // Apply F3 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn f3(&mut self, q: T) -> &mut Self { self.sxdg(q).sz(q) } - /// X -> -Z, Z -> -Y, Y -> X + /// Applies the adjoint of the F3 gate (F3†) to the specified qubit. F3† performs a cyclic + /// permutation with two negative signs in reverse. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -Z + /// Y → X + /// Z → -Y + /// ``` + /// + /// # Matrix Representation + /// ```text + /// F3† = 1/√2 [[ 1, i], + /// [ i, -1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.f3dg(0) // Apply F3† gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn f3dg(&mut self, q: T) -> &mut Self { self.szdg(q).sx(q) } - /// X -> Z, Z -> -Y, Y -> -X + /// Applies the F4 variant of the Face gate to the specified qubit. F4 performs a cyclic + /// permutation with three negative signs. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → Z + /// Y → -Z + /// Z → -X + /// ``` + /// + /// # Matrix Representation + /// ```text + /// F4 = 1/√2 [[-i, -1], + /// [ 1, -i]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.f4(0) // Apply F4 gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn f4(&mut self, q: T) -> &mut Self { self.sz(q).sx(q) } - /// X -> -Y, Z -> X, Y -> -Z + /// Applies the adjoint of the F4 gate (F4†) to the specified qubit. F4† performs a reverse + /// cyclic permutation of the Pauli operators. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// X → -Y + /// Y → Z + /// Z → -X + /// ``` + /// + /// # Matrix Representation + /// ```text + /// F4† = 1/√2 [[ i, 1], + /// [-1, i]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.f4dg(0) // Apply F4† gate to qubit 0 + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] fn f4dg(&mut self, q: T) -> &mut Self { self.sxdg(q).szdg(q) } - /// Performs a controlled-NOT operation between two qubits. + /// Applies a controlled-X (CNOT) operation between two qubits. The CX gate flips the target + /// qubit if the control qubit is in state |1⟩. /// - /// The operation performs: + /// # Arguments + /// * `q1` - Control qubit index. + /// * `q2` - Target qubit index. + /// + /// # Pauli Transformation /// ```text - /// |0⟩|b⟩ → |0⟩|b⟩ - /// |1⟩|b⟩ → |1⟩|b⊕1⟩ + /// XI → XX + /// IX → IX + /// ZI → ZI + /// IZ → ZZ /// ``` /// - /// In the Heisenberg picture, transforms Pauli operators as: + /// # Matrix Representation /// ```text - /// IX → IX (X on target unchanged) - /// XI → XX (X on control propagates to target) - /// IZ → ZZ (Z on target propagates to control) - /// ZI → ZI (Z on control unchanged) + /// CX = [[1, 0, 0, 0], + /// [0, 1, 0, 0], + /// [0, 0, 0, 1], + /// [0, 0, 1, 0]] /// ``` /// - /// # Arguments - /// * `q1` - Control qubit - /// * `q2` - Target qubit + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .cx(0,1); // Then apply CX gate between qubits 0 and 1 + /// ``` fn cx(&mut self, q1: T, q2: T) -> &mut Self; - /// CY: +IX -> +ZX; +IZ -> +ZZ; +XI -> -XY; +ZI -> +ZI; + /// Applies a controlled-Y operation between two qubits. The CY gate applies a Y operation on + /// the target qubit if the control qubit is in state |1⟩. + /// + /// # Arguments + /// * `q1` - Control qubit index. + /// * `q2` - Target qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → XY + /// IX → IX + /// ZI → ZI + /// IZ → ZZ + /// ``` + /// + /// # Matrix Representation + /// ```text + /// CY = [[1, 0, 0, 0], + /// [0, 1, 0, 0], + /// [0, 0, 0, -i], + /// [0, 0, +i, 0]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .cy(0,1); // Then apply CY gate between qubits 0 and 1 + /// ``` #[inline] fn cy(&mut self, q1: T, q2: T) -> &mut Self { self.sz(q2).cx(q1, q2).szdg(q2) } - /// CZ: +IX -> +ZX; +IZ -> +IZ; +XI -> +XZ; +ZI -> +ZI; + /// Applies a controlled-Z operation between two qubits. The CZ gate applies a phase of -1 when + /// both qubits are in state |1⟩. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → XZ + /// IX → ZX + /// ZI → ZI + /// IZ → IZ + /// ``` + /// + /// # Matrix Representation + /// ```text + /// CZ = [[1, 0, 0, 0], + /// [0, 1, 0, 0], + /// [0, 0, 1, 0], + /// [0, 0, 0, -1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .cz(0,1); // Then apply CZ gate between qubits 0 and 1 + /// ``` #[inline] fn cz(&mut self, q1: T, q2: T) -> &mut Self { self.h(q2).cx(q1, q2).h(q2) } - /// Performs a square root of XX operation (√XX). + /// Applies a square root of XX (SXX) operation between two qubits. The SXX gate implements + /// evolution under XX coupling for time π/4. /// - /// This is a symmetric two-qubit Clifford that implements: - /// ```text - /// SXX = exp(-iπ X⊗X/4) - /// ``` + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. /// - /// Transforms Pauli operators as: + /// # Pauli Transformation /// ```text /// XI → XI /// IX → IX @@ -461,22 +1046,37 @@ pub trait CliffordGateable: QuantumSimulator { /// IZ → -XY /// ``` /// - /// # Arguments - /// * q1 - First qubit - /// * q2 - Second qubit + /// # Matrix Representation + /// ```text + /// SXX = 1/√2 [[1, 0, 0, -i], + /// [0, 1, -i, 0], + /// [0, -i, 1, 0], + /// [-i, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .sxx(0,1); // Then apply SXX gate between qubits 0 and 1 + /// ``` #[inline] fn sxx(&mut self, q1: T, q2: T) -> &mut Self { self.sx(q1).sx(q2).sydg(q1).cx(q1, q2).sy(q1) } - /// Performs an adjoint of the square root of XX operation (√XX†). + /// Applies the adjoint of the square root of XX operation. The SXX† gate implements reverse + /// evolution under XX coupling. /// - /// This is a symmetric two-qubit Clifford that implements: - /// ```text - /// SXXdg = exp(+iπ X⊗X/4) - /// ``` + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. /// - /// Transforms Pauli operators as: + /// # Pauli Transformation /// ```text /// XI → XI /// IX → IX @@ -484,270 +1084,726 @@ pub trait CliffordGateable: QuantumSimulator { /// IZ → XY /// ``` /// - /// # Arguments - /// * `q1` - First qubit - /// * `q2` - Second qubit + /// # Matrix Representation + /// ```text + /// SXX† = 1/√2 [[1, 0, 0, i], + /// [0, 1, i, 0], + /// [0, i, 1, 0], + /// [i, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .sxxdg(0,1); // Then apply SXX† gate between qubits 0 and 1 + /// ``` #[inline] fn sxxdg(&mut self, q1: T, q2: T) -> &mut Self { self.x(q1).x(q2).sxx(q1, q2) } - /// Performs a square root of YY operation (√YY). - /// - /// This is a symmetric two-qubit Clifford that implements: - /// ```text - /// SYY = exp(-iπ Y⊗Y/4) - /// ``` + /// Applies a square root of YY (SYY) operation between two qubits. The SYY gate implements + /// evolution under YY coupling for time π/4. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → -ZY + /// IX → -YZ + /// ZI → XY + /// IZ → YX + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SYY = 1/√2 [[1, 0, 0, -i], + /// [0, -i, 1, 0], + /// [0, 1, -i, 0], + /// [-i, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .syy(0,1); // Then apply SYY gate between qubits 0 and 1 + /// ``` + #[inline] + fn syy(&mut self, q1: T, q2: T) -> &mut Self { + self.szdg(q1).szdg(q2).sxx(q1, q2).sz(q1).sz(q2) + } + + /// Applies the adjoint of the square root of YY operation. The SYY† gate implements reverse + /// evolution under YY coupling. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → ZY + /// IX → YZ + /// ZI → -XY + /// IZ → -YX + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SYY† = 1/√2 [[1, 0, 0, i], + /// [0, i, 1, 0], + /// [0, 1, i, 0], + /// [i, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .syydg(0,1); // Then apply SYY† gate between qubits 0 and 1 + /// ``` + #[inline] + fn syydg(&mut self, q1: T, q2: T) -> &mut Self { + self.y(q1).y(q2).syy(q1, q2) + } + + /// Applies a square root of ZZ (SZZ) operation between two qubits. The SZZ gate implements + /// evolution under ZZ coupling for time π/4. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → YZ + /// IX → ZY + /// ZI → ZI + /// IZ → IZ + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SZZ = e^(-iπ/4) [[1, 0, 0, 0], + /// [0, -i, 0, 0], + /// [0, 0, -i, 0], + /// [0, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .szz(0,1); // Then apply SZZ gate between qubits 0 and 1 + /// ``` + #[inline] + fn szz(&mut self, q1: T, q2: T) -> &mut Self { + self.h(q1).h(q2).sxx(q1, q2).h(q1).h(q2) + } + + /// Applies the adjoint of the square root of ZZ operation. The SZZ† gate implements + /// reverse evolution under ZZ coupling. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → -YZ + /// IX → -ZY + /// ZI → ZI + /// IZ → IZ + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SZZ† = e^(iπ/4) [[1, 0, 0, 0], + /// [0, i, 0, 0], + /// [0, 0, i, 0], + /// [0, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .szzdg(0,1); // Then apply SZZ† gate between qubits 0 and 1 + /// ``` + #[inline] + fn szzdg(&mut self, q1: T, q2: T) -> &mut Self { + self.z(q1).z(q2).szz(q1, q2) + } + + /// Applies the SWAP operation between two qubits. The SWAP gate exchanges the quantum + /// states of two qubits. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → IX + /// IX → XI + /// ZI → IZ + /// IZ → ZI + /// ``` + /// + /// # Matrix Representation + /// ```text + /// SWAP = [[1, 0, 0, 0], + /// [0, 0, 1, 0], + /// [0, 1, 0, 0], + /// [0, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .swap(0,1); // Then swap qubits 0 and 1 + /// ``` + #[inline] + fn swap(&mut self, q1: T, q2: T) -> &mut Self { + self.cx(q1, q2).cx(q2, q1).cx(q1, q2) + } + + /// Applies the iSWAP two-qubit Clifford operation. The iSWAP gate swaps states with an + /// additional i phase on the swapped states. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → -ZY + /// IX → YZ + /// ZI → IZ + /// IZ → ZI + /// ``` + /// + /// # Matrix Representation + /// ```text + /// iSWAP = [[1, 0, 0, 0], + /// [0, 0, i, 0], + /// [0, i, 0, 0], + /// [0, 0, 0, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .iswap(0,1); // Then apply iSWAP gate between qubits 0 and 1 + /// ``` + fn iswap(&mut self, q1: T, q2: T) -> &mut Self { + self.sz(q1).sz(q2).h(q1).cx(q1, q2).cx(q2, q1).h(q2) + } + + /// Applies the G two-qubit Clifford operation. G is a symmetric two-qubit operation that + /// implements a particular permutation of single-qubit Paulis. + /// + /// # Arguments + /// * `q1` - First qubit index. + /// * `q2` - Second qubit index. + /// + /// # Pauli Transformation + /// ```text + /// XI → IX + /// IX → XI + /// ZI → XZ + /// IZ → ZX + /// ``` + /// + /// # Matrix Representation + /// ```text + /// G = 1/2 [[1, 1, 1, -1], + /// [1, -1, 1, 1], + /// [1, 1, -1, 1], + /// [-1, 1, 1, 1]] + /// ``` + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.h(0) // Apply H gate to qubit 0 + /// .g(0,1); // Then apply G gate between qubits 0 and 1 + /// ``` + #[inline] + fn g(&mut self, q1: T, q2: T) -> &mut Self { + self.cz(q1, q2).h(q1).h(q2).cz(q1, q2) + } + + /// Measures the +X Pauli operator, projecting to the measured eigenstate. + /// + /// Projects the state into either the |+⟩ or |-⟩ eigenstate based on the + /// measurement outcome. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if projected to |-⟩, false if projected to |+⟩ + /// - `is_deterministic`: true if state was already in an X eigenstate + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// |ψ⟩ → |+⟩ with probability |⟨+|ψ⟩|² (outcome = false) + /// |ψ⟩ → |-⟩ with probability |⟨-|ψ⟩|² (outcome = true) + /// ``` + /// Where |±⟩ = (|0⟩ ± |1⟩)/√2. + /// + /// # Related Operations + /// * Use `mpx(q)` to measure and force preparation into |+⟩ state + /// * Use `px(q)` for direct preparation of |+⟩ state + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mx(0); // Measure in X basis + /// // State is now either |+⟩ or |-⟩ depending on outcome + /// ``` + #[inline] + fn mx(&mut self, q: T) -> MeasurementResult { + self.h(q); + let meas = self.mz(q); + self.h(q); + + meas + } + + /// Measures the -X Pauli operator, projecting to the measured eigenstate. + /// + /// Projects the state into either the |+⟩ or |-⟩ eigenstate based on the + /// measurement outcome. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if projected to |+⟩, false if projected to |-⟩ + /// - `is_deterministic`: true if state was already in an X eigenstate + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// |ψ⟩ → |-⟩ with probability |⟨-|ψ⟩|² (outcome = false) + /// |ψ⟩ → |+⟩ with probability |⟨+|ψ⟩|² (outcome = true) + /// ``` + /// Where |±⟩ = (|0⟩ ± |1⟩)/√2. + /// + /// # Related Operations + /// * Use `mpnx(q)` to measure and force preparation into |-⟩ state + /// * Use `pnx(q)` for direct preparation of |-⟩ state + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mnx(0); // Measure in -X basis + /// // State is now either |+⟩ or |-⟩ depending on outcome + /// ``` + #[inline] + fn mnx(&mut self, q: T) -> MeasurementResult { + self.h(q).x(q); + let meas = self.mz(q); + self.x(q).h(q); + + meas + } + + /// Measures the +Y Pauli operator, projecting to the measured eigenstate. + /// + /// Projects the state into either the |+i⟩ or |-i⟩ eigenstate based on the + /// measurement outcome. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if projected to |-i⟩, false if projected to |+i⟩ + /// - `is_deterministic`: true if state was already in a Y eigenstate + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// |ψ⟩ → |+i⟩ with probability |⟨+i|ψ⟩|² (outcome = false) + /// |ψ⟩ → |-i⟩ with probability |⟨-i|ψ⟩|² (outcome = true) + /// ``` + /// Where |±i⟩ = (|0⟩ ± i|1⟩)/√2. + /// + /// # Related Operations + /// * Use `mpy(q)` to measure and force preparation into |+i⟩ state + /// * Use `py(q)` for direct preparation of |+i⟩ state + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.my(0); // Measure in Y basis + /// // State is now either |+i⟩ or |-i⟩ depending on outcome + /// ``` + #[inline] + fn my(&mut self, q: T) -> MeasurementResult { + self.sx(q); + let meas = self.mz(q); + self.sxdg(q); + + meas + } + + /// Measures the -Y Pauli operator, projecting to the measured eigenstate. + /// + /// Projects the state into either the |+i⟩ or |-i⟩ eigenstate based on the + /// measurement outcome. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if projected to |+i⟩, false if projected to |-i⟩ + /// - `is_deterministic`: true if state was already in a Y eigenstate + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// |ψ⟩ → |-i⟩ with probability |⟨-i|ψ⟩|² (outcome = false) + /// |ψ⟩ → |+i⟩ with probability |⟨+i|ψ⟩|² (outcome = true) + /// ``` + /// Where |±i⟩ = (|0⟩ ± i|1⟩)/√2. + /// + /// # Related Operations + /// * Use `mpny(q)` to measure and force preparation into |-i⟩ state + /// * Use `pny(q)` for direct preparation of |-i⟩ state + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mny(0); // Measure in -Y basis + /// // State is now either |+i⟩ or |-i⟩ depending on outcome + /// ``` + #[inline] + fn mny(&mut self, q: T) -> MeasurementResult { + // -Y -> +Z + self.sxdg(q); + let meas = self.mz(q); + // +Z -> -Y + self.sx(q); + + meas + } + + /// Measures the +Z Pauli operator, projecting to the measured eigenstate. /// - /// Transforms Pauli operators as: - /// ```text - /// XI → -ZY - /// IX → -YZ - /// ZI → XY - /// IZ → YX - /// ``` + /// Projects the state into either the |0⟩ or |1⟩ eigenstate based on the + /// measurement outcome. This is the standard computational basis measurement. /// /// # Arguments - /// * `q1` - First qubit - /// * `q2` - Second qubit - #[inline] - fn syy(&mut self, q1: T, q2: T) -> &mut Self { - self.szdg(q1).szdg(q2).sxx(q1, q2).sz(q1).sz(q2) - } - - /// Performs an adjoint of the square root of YY operation (√YY†). + /// * `q` - Target qubit index. /// - /// This is a symmetric two-qubit Clifford that implements: - /// ```text - /// SYY = exp(+iπ Y⊗Y/4) - /// ``` + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if projected to |1⟩, false if projected to |0⟩ + /// - `is_deterministic`: true if state was already in a Z eigenstate /// - /// Transforms Pauli operators as: + /// # Mathematical Details + /// The operation performs: /// ```text - /// XI → ZY - /// IX → YZ - /// ZI → -XY - /// IZ → -YX + /// |ψ⟩ → |0⟩ with probability |⟨0|ψ⟩|² (outcome = false) + /// |ψ⟩ → |1⟩ with probability |⟨1|ψ⟩|² (outcome = true) /// ``` /// - /// # Arguments - /// * `q1` - First qubit - /// * `q2` - Second qubit - #[inline] - fn syydg(&mut self, q1: T, q2: T) -> &mut Self { - self.y(q1).y(q2).syy(q1, q2) - } - - /// Performs a square root of ZZ operation (√ZZ). + /// # Related Operations + /// * Use `mpz(q)` to measure and force preparation into |0⟩ state + /// * Use `pz(q)` for direct preparation of |0⟩ state /// - /// This is a symmetric two-qubit Clifford that implements: - /// ```text - /// SZZ = exp(-iπ Z⊗Z/4) + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mz(0); // Measure in Z basis + /// // State is now either |0⟩ or |1⟩ depending on outcome /// ``` + fn mz(&mut self, q: T) -> MeasurementResult; + + /// Measures the -Z Pauli operator, projecting to the measured eigenstate. /// - /// Transforms Pauli operators as: - /// ```text - /// XI → YZ - /// IX → ZY - /// ZI → ZI - /// IZ → IZ - /// ``` + /// Projects the state into either the |0⟩ or |1⟩ eigenstate based on the + /// measurement outcome. /// /// # Arguments - /// * `q1` - First qubit - /// * `q2` - Second qubit - #[inline] - fn szz(&mut self, q1: T, q2: T) -> &mut Self { - self.h(q1).h(q2).sxx(q1, q2).h(q1).h(q2) - } - - /// Performs an adjoint of the square root of ZZ operation (√ZZ). + /// * `q` - Target qubit index. /// - /// This is a symmetric two-qubit Clifford that implements: - /// ```text - /// SZZdg = exp(+iπ Z⊗Z/4) - /// ``` + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if projected to |0⟩, false if projected to |1⟩ + /// - `is_deterministic`: true if state was already in a Z eigenstate /// - /// Transforms Pauli operators as: + /// # Mathematical Details + /// The operation performs: /// ```text - /// XI → -YZ - /// IX → -ZY - /// ZI → ZI - /// IZ → IZ + /// |ψ⟩ → |1⟩ with probability |⟨1|ψ⟩|² (outcome = false) + /// |ψ⟩ → |0⟩ with probability |⟨0|ψ⟩|² (outcome = true) /// ``` /// - /// # Arguments - /// * `q1` - First qubit - /// * `q2` - Second qubit + /// # Related Operations + /// * Use `mpnz(q)` to measure and force preparation into |1⟩ state + /// * Use `pnz(q)` for direct preparation of |1⟩ state + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mnz(0); // Measure in -Z basis + /// // State is now either |0⟩ or |1⟩ depending on outcome + /// ``` #[inline] - fn szzdg(&mut self, q1: T, q2: T) -> &mut Self { - self.z(q1).z(q2).szz(q1, q2) - } + fn mnz(&mut self, q: T) -> MeasurementResult { + self.x(q); + let meas = self.mz(q); + self.x(q); - /// SWAP: +IX -> XI; - /// +IZ -> ZI; - /// +XI -> IX; - /// +ZI -> IZ; - #[inline] - fn swap(&mut self, q1: T, q2: T) -> &mut Self { - self.cx(q1, q2).cx(q2, q1).cx(q1, q2) + meas } - /// Applies the G2 two-qubit Clifford operation. - /// - /// G2 is a symmetric two-qubit operation that implements a particular permutation - /// of single-qubit Paulis. It transforms the Pauli operators as follows: - /// ```text - /// XI → IX - /// IX → XI - /// ZI → XZ - /// IZ → ZX - /// ``` + /// Prepares a qubit in the +1 eigenstate of the +X operator. Equivalent to preparing + /// |+X⟩ = |+⟩ = (|0⟩ + |1⟩)/√2. /// /// # Arguments - /// * `q1` - First qubit - /// * `q2` - Second qubit + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. /// - /// # Implementation Details - /// G2 can be implemented as: CZ • (H⊗H) • CZ + /// # Related Operations + /// * Use `mx(q)` to measure in the X basis + /// * Use `mpx(q)` to measure and prepare in the same eigenstate /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(2); - /// sim.g2(0, 1); + /// sim.px(0) // Prepare qubit 0 in |+X⟩ state + /// .cx(0,1); // Then apply CX gate /// ``` #[inline] - fn g2(&mut self, q1: T, q2: T) -> &mut Self { - self.cz(q1, q2).h(q1).h(q2).cz(q1, q2) + fn px(&mut self, q: T) -> &mut Self { + self.mpx(q); + self } - /// Applies the iSWAP two-qubit Clifford operation. - /// - /// iSWAP is a two-qubit operation that transforms the Pauli operators as follows: - /// ```text - /// XI → -ZY - /// IX → YZ - /// ZI → IZ - /// IZ → ZI - /// ``` + /// Prepares the qubit in the +1 eigenstate of -X. Equivalent to preparing + /// |-X⟩ = |-⟩ = (|0⟩ - |1⟩)/√2. /// /// # Arguments - /// * `q1` - First qubit - /// * `q2` - Second qubit + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. /// - /// # Implementation Details - /// iSWAP can be implemented as: (SZ⊗SZ) • (H⊗I) • CX(q1,q2) • (I⊗H) + /// # Related Operations + /// * Use `mnx(q)` to measure in the -X basis + /// * Use `mpnx(q)` to measure and prepare in the same eigenstate /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; /// let mut sim = StdSparseStab::new(2); - /// sim.iswap(0, 1); + /// sim.pnx(0) // Prepare qubit 0 in |-X⟩ state + /// .cx(0,1); // Then apply CX gate /// ``` - fn iswap(&mut self, q1: T, q2: T) -> &mut Self { - self.sz(q1).sz(q2).h(q1).cx(q1, q2).cx(q2, q1).h(q2) - } - - /// Measurement of the +`X_q` operator. #[inline] - fn mx(&mut self, q: T) -> MeasurementResult { - self.h(q); - let meas = self.mz(q); - self.h(q); - - meas + fn pnx(&mut self, q: T) -> &mut Self { + self.mpnx(q); + self } - /// Measurement of the -`X_q` operator. + /// Prepares the qubit in the +1 eigenstate of +Y. Equivalent to preparing + /// |+Y⟩ = |+i⟩ = (|0⟩ + i|1⟩)/√2. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Related Operations + /// * Use `my(q)` to measure in the Y basis + /// * Use `mpy(q)` to measure and prepare in the same eigenstate + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.py(0) // Prepare qubit 0 in |+Y⟩ state + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] - fn mnx(&mut self, q: T) -> MeasurementResult { - self.h(q).x(q); - let meas = self.mz(q); - self.x(q).h(q); - - meas + fn py(&mut self, q: T) -> &mut Self { + self.mpy(q); + self } - /// Measurement of the +`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. + /// Prepares the qubit in the +1 eigenstate of -Y. Equivalent to preparing + /// |-Y⟩ = |-i⟩ = (|0⟩ - i|1⟩)/√2. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Related Operations + /// * Use `mny(q)` to measure in the -Y basis + /// * Use `mpny(q)` to measure and prepare in the same eigenstate + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.pny(0) // Prepare qubit 0 in |-Y⟩ state + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] - fn my(&mut self, q: T) -> MeasurementResult { - self.sx(q); - let meas = self.mz(q); - self.sxdg(q); - - meas + fn pny(&mut self, q: T) -> &mut Self { + self.mpny(q); + self } - /// Measurement of the -`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. + /// Prepares the qubit in the +1 eigenstate of +Z. Equivalent to preparing |+Z⟩ = |0⟩. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. + /// + /// # Related Operations + /// * Use `mz(q)` to measure in the Z basis + /// * Use `mpz(q)` to measure and prepare in the same eigenstate + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(2); + /// sim.pz(0) // Prepare qubit 0 in |0⟩ state + /// .cx(0,1); // Then apply CX gate + /// ``` #[inline] - fn mny(&mut self, q: T) -> MeasurementResult { - // -Y -> +Z - self.sxdg(q); - let meas = self.mz(q); - // +Z -> -Y - self.sx(q); - - meas + fn pz(&mut self, q: T) -> &mut Self { + self.mpz(q); + self } - /// Performs a measurement in the Z basis on the specified qubit. - /// - /// This measurement projects the qubit state onto the +1 or -1 eigenstate - /// of the Z operator (corresponding to |0⟩ or |1⟩ respectively). + /// Prepares the qubit in the +1 eigenstate of -Z. Equivalent to preparing |-Z⟩ = |1⟩. /// /// # Arguments - /// * `q` - The qubit to measure + /// * `q` - Target qubit index. /// - /// For all measurement operations (mx, my, mz, mnx, mny, mnz): + /// # Returns + /// * `&mut Self` - Returns the simulator for method chaining. /// - /// # Return Values - /// Returns a tuple `(outcome, deterministic)` where: - /// * `outcome`: - /// - `true` indicates projection onto the +1 eigenstate - /// - `false` indicates projection onto the -1 eigenstate - /// * `deterministic`: - /// - `true` if the state was already in an eigenstate of the measured operator - /// - `false` if the measurement result was random (state was in superposition) + /// # Related Operations + /// * Use `mnz(q)` to measure in the -Z basis + /// * Use `mpnz(q)` to measure and prepare in the same eigenstate /// /// # Examples /// ```rust /// use pecos_qsim::{CliffordGateable, StdSparseStab}; - /// let mut sim = StdSparseStab::new(1); - /// let outcome = sim.mz(0); + /// let mut sim = StdSparseStab::new(2); + /// sim.pnz(0) // Prepare qubit 0 in |1⟩ state + /// .cx(0,1); // Then apply CX gate /// ``` - fn mz(&mut self, q: T) -> MeasurementResult; - - /// Measurement of the -`Z_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. #[inline] - fn mnz(&mut self, q: T) -> MeasurementResult { - self.x(q); - let meas = self.mz(q); - self.x(q); - - meas + fn pnz(&mut self, q: T) -> &mut Self { + self.mpnz(q); + self } - /// Prepares a qubit in the +1 eigenstate of the X operator. + /// Both measures +X and prepares the qubit in the |+⟩ state. /// - /// Equivalent to preparing |+⟩ = (|0⟩ + |1⟩)/√2 + /// After measurement, unlike `mx()` which projects to the measured eigenstate, + /// this operation always prepares the |+⟩ state regardless of measurement outcome. + /// The operation combines measurement with deterministic state preparation. /// /// # Arguments - /// * `q` - Target qubit + /// * `q` - Target qubit index. /// /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// * `MeasurementResult` containing: + /// - `outcome`: true if Z correction was needed + /// - `is_deterministic`: true if state was already |+⟩ + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// First measures X: + /// |ψ⟩ → |+⟩ with probability |⟨+|ψ⟩|² (outcome = false) + /// |ψ⟩ → |-⟩ with probability |⟨-|ψ⟩|² (outcome = true) + /// + /// Then applies correction if needed: + /// |-⟩ → Z|-⟩ = |+⟩ + /// ``` + /// Final state is always |+⟩ = (|0⟩ + |1⟩)/√2. + /// + /// # Related Operations + /// * Use `mx(q)` to measure and project to the measured eigenstate + /// * Use `px(q)` for state preparation without measurement /// - /// # Panics - /// Will panic if qubit ids don't convert to usize. + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mpx(0); // Measure X and prepare |+⟩ state + /// // State is now |+⟩ regardless of measurement outcome + /// ``` #[inline] - fn px(&mut self, q: T) -> MeasurementResult { + fn mpx(&mut self, q: T) -> MeasurementResult { let result = self.mx(q); if result.outcome { self.z(q); @@ -755,21 +1811,45 @@ pub trait CliffordGateable: QuantumSimulator { result } - /// Prepares a qubit in the -1 eigenstate of the X operator. + /// Both measures -X and prepares the qubit in the |-⟩ state. /// - /// Equivalent to preparing |-⟩ = (|0⟩ - |1⟩)/√2 + /// After measurement, unlike `mnx()` which projects to the measured eigenstate, + /// this operation always prepares the |-⟩ state regardless of measurement outcome. + /// The operation combines measurement with deterministic state preparation. /// /// # Arguments - /// * `q` - Target qubit + /// * `q` - Target qubit index. /// /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// * `MeasurementResult` containing: + /// - `outcome`: true if Z correction was needed + /// - `is_deterministic`: true if state was already |-⟩ + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// First measures -X: + /// |ψ⟩ → |-⟩ with probability |⟨-|ψ⟩|² (outcome = false) + /// |ψ⟩ → |+⟩ with probability |⟨+|ψ⟩|² (outcome = true) + /// + /// Then applies correction if needed: + /// |+⟩ → Z|+⟩ = |-⟩ + /// ``` + /// Final state is always |-⟩ = (|0⟩ - |1⟩)/√2. /// - /// # Panics + /// # Related Operations + /// * Use `mnx(q)` to measure and project to the measured eigenstate + /// * Use `pnx(q)` for state preparation without measurement /// - /// Will panic if qubit ids don't convert to usize. + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mpnx(0); // Measure -X and prepare |-⟩ state + /// // State is now |-⟩ regardless of measurement outcome + /// ``` #[inline] - fn pnx(&mut self, q: T) -> MeasurementResult { + fn mpnx(&mut self, q: T) -> MeasurementResult { let result = self.mnx(q); if result.outcome { self.z(q); @@ -777,11 +1857,45 @@ pub trait CliffordGateable: QuantumSimulator { result } - /// Preparation of the +`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. + /// Both measures +Y and prepares the qubit in the |+i⟩ state. + /// + /// After measurement, unlike `my()` which projects to the measured eigenstate, + /// this operation always prepares the |+i⟩ state regardless of measurement outcome. + /// The operation combines measurement with deterministic state preparation. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if Z correction was needed + /// - `is_deterministic`: true if state was already |+i⟩ + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// First measures Y: + /// |ψ⟩ → |+i⟩ with probability |⟨+i|ψ⟩|² (outcome = false) + /// |ψ⟩ → |-i⟩ with probability |⟨-i|ψ⟩|² (outcome = true) + /// + /// Then applies correction if needed: + /// |-i⟩ → Z|-i⟩ = |+i⟩ + /// ``` + /// Final state is always |+i⟩ = (|0⟩ + i|1⟩)/√2. + /// + /// # Related Operations + /// * Use `my(q)` to measure and project to the measured eigenstate + /// * Use `py(q)` for state preparation without measurement + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mpy(0); // Measure Y and prepare |+i⟩ state + /// // State is now |+i⟩ regardless of measurement outcome + /// ``` #[inline] - fn py(&mut self, q: T) -> MeasurementResult { + fn mpy(&mut self, q: T) -> MeasurementResult { let result = self.my(q); if result.outcome { self.z(q); @@ -789,11 +1903,45 @@ pub trait CliffordGateable: QuantumSimulator { result } - /// Preparation of the -`Y_q` operator. - /// # Panics - /// Will panic if qubit ids don't convert to usize. + /// Both measures -Y and prepares the qubit in the |-i⟩ state. + /// + /// After measurement, unlike `mny()` which projects to the measured eigenstate, + /// this operation always prepares the |-i⟩ state regardless of measurement outcome. + /// The operation combines measurement with deterministic state preparation. + /// + /// # Arguments + /// * `q` - Target qubit index. + /// + /// # Returns + /// * `MeasurementResult` containing: + /// - `outcome`: true if Z correction was needed + /// - `is_deterministic`: true if state was already |-i⟩ + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// First measures -Y: + /// |ψ⟩ → |-i⟩ with probability |⟨-i|ψ⟩|² (outcome = false) + /// |ψ⟩ → |+i⟩ with probability |⟨+i|ψ⟩|² (outcome = true) + /// + /// Then applies correction if needed: + /// |+i⟩ → Z|+i⟩ = |-i⟩ + /// ``` + /// Final state is always |-i⟩ = (|0⟩ - i|1⟩)/√2. + /// + /// # Related Operations + /// * Use `mny(q)` to measure and project to the measured eigenstate + /// * Use `pny(q)` for state preparation without measurement + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mpny(0); // Measure -Y and prepare |-i⟩ state + /// // State is now |-i⟩ regardless of measurement outcome + /// ``` #[inline] - fn pny(&mut self, q: T) -> MeasurementResult { + fn mpny(&mut self, q: T) -> MeasurementResult { let result = self.mny(q); if result.outcome { self.z(q); @@ -801,21 +1949,45 @@ pub trait CliffordGateable: QuantumSimulator { result } - /// Prepares a qubit in the +1 eigenstate of the Z operator. + /// Both measures +Z and prepares the qubit in the |0⟩ state. /// - /// Equivalent to preparing |0⟩ + /// After measurement, unlike `mz()` which projects to the measured eigenstate, + /// this operation always prepares the |0⟩ state regardless of measurement outcome. + /// The operation combines measurement with deterministic state preparation. /// /// # Arguments - /// * `q` - Target qubit + /// * `q` - Target qubit index. /// /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// * `MeasurementResult` containing: + /// - `outcome`: true if X correction was needed + /// - `is_deterministic`: true if state was already |0⟩ + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// First measures Z: + /// |ψ⟩ → |0⟩ with probability |⟨0|ψ⟩|² (outcome = false) + /// |ψ⟩ → |1⟩ with probability |⟨1|ψ⟩|² (outcome = true) + /// + /// Then applies correction if needed: + /// |1⟩ → X|1⟩ = |0⟩ + /// ``` + /// Final state is always |0⟩. /// - /// # Panics + /// # Related Operations + /// * Use `mz(q)` to measure and project to the measured eigenstate + /// * Use `pz(q)` for state preparation without measurement /// - /// Will panic if qubit ids don't convert to usize. + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mpz(0); // Measure Z and prepare |0⟩ state + /// // State is now |0⟩ regardless of measurement outcome + /// ``` #[inline] - fn pz(&mut self, q: T) -> MeasurementResult { + fn mpz(&mut self, q: T) -> MeasurementResult { let result = self.mz(q); if result.outcome { self.x(q); @@ -823,21 +1995,45 @@ pub trait CliffordGateable: QuantumSimulator { result } - /// Prepares a qubit in the -1 eigenstate of the Z operator. + /// Both measures -Z and prepares the qubit in the |1⟩ state. /// - /// Equivalent to preparing |1⟩ + /// After measurement, unlike `mnz()` which projects to the measured eigenstate, + /// this operation always prepares the |1⟩ state regardless of measurement outcome. + /// The operation combines measurement with deterministic state preparation. /// /// # Arguments - /// * `q` - Target qubit + /// * `q` - Target qubit index. /// /// # Returns - /// * `(bool, bool)` - (`measurement_outcome`, `was_deterministic`) + /// * `MeasurementResult` containing: + /// - `outcome`: true if X correction was needed + /// - `is_deterministic`: true if state was already |1⟩ + /// + /// # Mathematical Details + /// The operation performs: + /// ```text + /// First measures -Z: + /// |ψ⟩ → |1⟩ with probability |⟨1|ψ⟩|² (outcome = false) + /// |ψ⟩ → |0⟩ with probability |⟨0|ψ⟩|² (outcome = true) + /// + /// Then applies correction if needed: + /// |0⟩ → X|0⟩ = |1⟩ + /// ``` + /// Final state is always |1⟩. /// - /// # Panics + /// # Related Operations + /// * Use `mnz(q)` to measure and project to the measured eigenstate + /// * Use `pnz(q)` for state preparation without measurement /// - /// Will panic if qubit ids don't convert to usize. + /// # Examples + /// ```rust + /// use pecos_qsim::{CliffordGateable, StdSparseStab}; + /// let mut sim = StdSparseStab::new(1); + /// let result = sim.mpnz(0); // Measure -Z and prepare |1⟩ state + /// // State is now |1⟩ regardless of measurement outcome + /// ``` #[inline] - fn pnz(&mut self, q: T) -> MeasurementResult { + fn mpnz(&mut self, q: T) -> MeasurementResult { let result = self.mnz(q); if result.outcome { self.x(q); diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index a222ad47..cd5c3b9b 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -895,7 +895,7 @@ mod tests { fn test_nondeterministic_px() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let r0 = state.px(0); + let r0 = state.mpx(0); let meas = state.mx(0); let m1 = meas.outcome; let d1 = meas.is_deterministic; @@ -910,7 +910,7 @@ mod tests { #[test] fn test_deterministic_px() { let mut state = prep_state(&["X"], &["Z"]); - let r0 = state.px(0); + let r0 = state.mpx(0); let m0_int = u8::from(r0.outcome); assert!(r0.is_deterministic); // Deterministic @@ -921,7 +921,7 @@ mod tests { fn test_nondeterministic_pnx() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let r0 = state.pnx(0); + let r0 = state.mpnx(0); let result = state.mx(0); let m1_int = u8::from(result.outcome); @@ -934,7 +934,7 @@ mod tests { #[test] fn test_deterministic_pnx() { let mut state = prep_state(&["-X"], &["Z"]); - let r0 = state.pnx(0); + let r0 = state.mpnx(0); let m0_int = u8::from(r0.outcome); assert!(r0.is_deterministic); // Deterministic @@ -945,7 +945,7 @@ mod tests { fn test_nondeterministic_py() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let r0 = state.py(0); + let r0 = state.mpy(0); let r1 = state.my(0); let m1_int = u8::from(r1.outcome); @@ -958,7 +958,7 @@ mod tests { #[test] fn test_deterministic_py() { let mut state = prep_state(&["iW"], &["Z"]); - let r0 = state.py(0); + let r0 = state.mpy(0); let m0_int = u8::from(r0.outcome); assert!(r0.is_deterministic); // Deterministic @@ -969,7 +969,7 @@ mod tests { fn test_nondeterministic_pny() { for _ in 1_u32..=100 { let mut state = prep_state(&["Z"], &["X"]); - let r0 = state.pny(0); + let r0 = state.mpny(0); let r1 = state.my(0); let m1_int = u8::from(r1.outcome); @@ -982,7 +982,7 @@ mod tests { #[test] fn test_deterministic_pny() { let mut state = prep_state(&["-iW"], &["Z"]); - let r0 = state.pny(0); + let r0 = state.mpny(0); let m0_int = u8::from(r0.outcome); assert!(r0.is_deterministic); // Deterministic @@ -993,7 +993,7 @@ mod tests { fn test_nondeterministic_pz() { for _ in 1_u32..=100 { let mut state = prep_state(&["X"], &["Z"]); - let r0 = state.pz(0); + let r0 = state.mpz(0); let r1 = state.mz(0); let m1_int = u8::from(r1.outcome); @@ -1006,7 +1006,7 @@ mod tests { #[test] fn test_deterministic_pz() { let mut state = prep_state(&["Z"], &["X"]); - let r0 = state.pz(0); + let r0 = state.mpz(0); let m0_int = u8::from(r0.outcome); assert!(r0.is_deterministic); // Deterministic @@ -1017,7 +1017,7 @@ mod tests { fn test_nondeterministic_pnz() { for _ in 1_u32..=100 { let mut state = prep_state(&["X"], &["Z"]); - let r0 = state.pnz(0); + let r0 = state.mpnz(0); let r1 = state.mz(0); let m1_int = u8::from(r1.outcome); @@ -1030,7 +1030,7 @@ mod tests { #[test] fn test_deterministic_pnz() { let mut state = prep_state(&["-Z"], &["X"]); - let r0 = state.pnz(0); + let r0 = state.mpnz(0); let m0_int = u8::from(r0.outcome); assert!(r0.is_deterministic); // Deterministic @@ -2091,22 +2091,22 @@ mod tests { // +IX -> +XI let mut state = prep_state(&["IX"], &["IZ"]); - state.g2(0, 1); + state.g(0, 1); check_state(&state, &["XI"], &["ZX"]); // +IZ -> +ZX let mut state = prep_state(&["IZ"], &["IX"]); - state.g2(0, 1); + state.g(0, 1); check_state(&state, &["ZX"], &["XI"]); // +XI -> +IX let mut state = prep_state(&["XI"], &["ZI"]); - state.g2(0, 1); + state.g(0, 1); check_state(&state, &["IX"], &["XZ"]); // +ZI -> +XZ let mut state = prep_state(&["ZI"], &["XI"]); - state.g2(0, 1); + state.g(0, 1); check_state(&state, &["XZ"], &["IX"]); } From 4d42b109fb5fd31217f4899e9b9add4e2ba363f1 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Wed, 1 Jan 2025 23:30:17 -0700 Subject: [PATCH 22/33] Updating documentation --- crates/pecos-python/src/state_vec_bindings.rs | 12 ++ .../src/arbitrary_rotation_gateable.rs | 125 ++++++++++++ crates/pecos-qsim/src/pauli_prop.rs | 2 +- crates/pecos-qsim/src/prelude.rs | 12 ++ crates/pecos-qsim/src/sparse_stab.rs | 2 +- crates/pecos-qsim/src/state_vec.rs | 187 +++++++++--------- 6 files changed, 240 insertions(+), 100 deletions(-) diff --git a/crates/pecos-python/src/state_vec_bindings.rs b/crates/pecos-python/src/state_vec_bindings.rs index 1f44e27c..b1dde38d 100644 --- a/crates/pecos-python/src/state_vec_bindings.rs +++ b/crates/pecos-python/src/state_vec_bindings.rs @@ -1,3 +1,15 @@ +// Copyright 2024 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + use pecos::prelude::*; use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs index 6ef48457..53a5a086 100644 --- a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -14,20 +14,85 @@ use crate::CliffordGateable; use pecos_core::IndexableElement; use std::f64::consts::{FRAC_PI_2, FRAC_PI_4}; +/// A trait for implementing arbitrary rotation gates on a quantum system. +/// +/// This trait extends [`CliffordGateable`] and provides methods for applying +/// single-qubit and two-qubit rotation gates around various axes. +/// +/// # Type Parameters +/// - `T`: A type implementing [`IndexableElement`], representing the indices +/// of qubits within the quantum system. +/// +/// # Note +/// Most of the methods in this trait have default implementations. However, the +/// following methods are the minimum methods that must be implemented to utilize the trait: +/// - `rx`: Rotation around the X-axis. +/// - `rz`: Rotation around the Z-axis. +/// - `rzz`: Two-qubit rotation around the ZZ-axis. pub trait ArbitraryRotationGateable: CliffordGateable { + /// Applies a rotation around the X-axis by an angle `theta`. + /// + /// # Parameters + /// - `theta`: The rotation angle in radians. + /// - `q`: The target qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. fn rx(&mut self, theta: f64, q: T) -> &mut Self; + /// Applies a rotation around the Y-axis by an angle `theta`. + /// + /// By default, this is implemented in terms of `sz`, `rx`, and `szdg` gates. + /// + /// # Parameters + /// - `theta`: The rotation angle in radians. + /// - `q`: The target qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. #[inline] fn ry(&mut self, theta: f64, q: T) -> &mut Self { self.sz(q).rx(theta, q).szdg(q) } + + /// Applies a rotation around the Z-axis by an angle `theta`. + /// + /// # Parameters + /// - `theta`: The rotation angle in radians. + /// - `q`: The target qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. fn rz(&mut self, theta: f64, q: T) -> &mut Self; + /// Applies a general single-qubit unitary gate with the specified parameters. + /// + /// By default, this is implemented in terms of `rz` and `ry` gates. + /// + /// # Parameters + /// - `theta`: The rotation angle around the Y-axis in radians. + /// - `phi`: The first Z-axis rotation angle in radians. + /// - `lambda`: The second Z-axis rotation angle in radians. + /// - `q`: The target qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. #[inline] fn u(&mut self, theta: f64, phi: f64, lambda: f64, q: T) -> &mut Self { self.rz(lambda, q).ry(theta, q).rz(phi, q) } + /// Applies an XY-plane rotation gate with a specified angle and axis. + /// + /// By default, this is implemented in terms of `rz` and `ry` gates. + /// + /// # Parameters + /// - `theta`: The rotation angle in radians. + /// - `phi`: The axis angle in radians. + /// - `q`: The target qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. #[inline] fn r1xy(&mut self, theta: f64, phi: f64, q: T) -> &mut Self { self.rz(-phi + FRAC_PI_2, q) @@ -35,27 +100,87 @@ pub trait ArbitraryRotationGateable: CliffordGateable { .rz(phi - FRAC_PI_2, q) } + /// Applies the T gate (π/8 rotation around Z-axis). + /// + /// # Parameters + /// - `q`: The target qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. #[inline] fn t(&mut self, q: T) -> &mut Self { self.rz(FRAC_PI_4, q) } + /// Applies the T† (T-dagger) gate (−π/8 rotation around Z-axis). + /// + /// # Parameters + /// - `q`: The target qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. #[inline] fn tdg(&mut self, q: T) -> &mut Self { self.rz(-FRAC_PI_4, q) } + /// Applies a two-qubit XX rotation gate. + /// + /// By default, this is implemented in terms of Hadamard (`h`) and ZZ rotation (`rzz`) gates. + /// + /// # Parameters + /// - `theta`: The rotation angle in radians. + /// - `q1`: The first qubit index. + /// - `q2`: The second qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. #[inline] fn rxx(&mut self, theta: f64, q1: T, q2: T) -> &mut Self { self.h(q1).h(q2).rzz(theta, q1, q2).h(q1).h(q2) } + /// Applies a two-qubit YY rotation gate. + /// + /// By default, this is implemented in terms of SX and ZZ rotation (`rzz`) gates. + /// + /// # Parameters + /// - `theta`: The rotation angle in radians. + /// - `q1`: The first qubit index. + /// - `q2`: The second qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. #[inline] fn ryy(&mut self, theta: f64, q1: T, q2: T) -> &mut Self { self.sx(q1).sx(q2).rzz(theta, q1, q2).sxdg(q1).sxdg(q2) } + + /// Applies a two-qubit ZZ rotation gate. + /// + /// # Parameters + /// - `theta`: The rotation angle in radians. + /// - `q1`: The first qubit index. + /// - `q2`: The second qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. fn rzz(&mut self, theta: f64, q1: T, q2: T) -> &mut Self; + /// Applies a composite rotation gate using RXX, RYY, and RZZ gates. + /// + /// # Parameters + /// - `theta`: The rotation angle for the RXX gate in radians. + /// - `phi`: The rotation angle for the RYY gate in radians. + /// - `lambda`: The rotation angle for the RZZ gate in radians. + /// - `q1`: The first qubit index. + /// - `q2`: The second qubit index. + /// + /// # Returns + /// A mutable reference to `Self` for method chaining. + /// + /// # Note + /// The current implementation might have a reversed order of operations. #[inline] fn rxxryyrzz(&mut self, theta: f64, phi: f64, lambda: f64, q1: T, q2: T) -> &mut Self { // TODO: This is likely backwards.. diff --git a/crates/pecos-qsim/src/pauli_prop.rs b/crates/pecos-qsim/src/pauli_prop.rs index 5ff50471..98bd2e61 100644 --- a/crates/pecos-qsim/src/pauli_prop.rs +++ b/crates/pecos-qsim/src/pauli_prop.rs @@ -44,7 +44,7 @@ pub type StdPauliProp = PauliProp, usize>; /// intended use cases. /// /// # Type Parameters -/// - `T`: The set type used to store qubit indices (e.g., `VecSet`) +/// - `T`: The set type used to store qubit indices (e.g., `VecSet`\) /// - `E`: The element type used for qubit indices (e.g., usize) /// /// # Example diff --git a/crates/pecos-qsim/src/prelude.rs b/crates/pecos-qsim/src/prelude.rs index a065a5d0..dd6caec6 100644 --- a/crates/pecos-qsim/src/prelude.rs +++ b/crates/pecos-qsim/src/prelude.rs @@ -1,3 +1,15 @@ +// Copyright 2024 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License.You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. + pub use crate::arbitrary_rotation_gateable::ArbitraryRotationGateable; pub use crate::clifford_gateable::CliffordGateable; pub use crate::pauli_prop::{PauliProp, StdPauliProp}; diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index cd5c3b9b..54c0ad62 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -29,7 +29,7 @@ pub type StdSparseStab = SparseStab, usize>; /// # State Representation /// The quantum state is represented by: /// - A set of n stabilizer generators that mutually commute -/// - A set of n destabilizer generators where destab[i] anti-commutes with stab[i] and +/// - A set of n destabilizer generators where destab\[i\] anti-commutes with stab\[i\] and /// commutes with all other stabilizers /// /// The implementation uses a sparse matrix representation for efficiency and speed, storing: diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index e82c9853..a8b1d755 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -122,9 +122,11 @@ impl StateVec { /// ); /// ``` /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `qubit` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] pub fn single_qubit_rotation( &mut self, @@ -134,8 +136,6 @@ impl StateVec { u10: Complex64, u11: Complex64, ) -> &mut Self { - assert!(target < self.num_qubits); - let step = 1 << target; for i in (0..self.state.len()).step_by(2 * step) { for offset in 0..step { @@ -158,9 +158,12 @@ impl StateVec { /// [u20, u21, u22, u23], /// [u30, u31, u32, u33]] /// - /// # Panics + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 + /// This function assumes that: + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] pub fn two_qubit_unitary( &mut self, @@ -168,10 +171,6 @@ impl StateVec { qubit2: usize, matrix: [[Complex64; 4]; 4], ) -> &mut Self { - assert!(qubit1 < self.num_qubits); - assert!(qubit2 < self.num_qubits); - assert_ne!(qubit1, qubit2); - // Make sure qubit1 < qubit2 for consistent ordering let (q1, q2) = if qubit1 < qubit2 { (qubit1, qubit2) @@ -231,12 +230,13 @@ impl QuantumSimulator for StateVec { impl CliffordGateable for StateVec { /// Apply Pauli-X gate /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn x(&mut self, target: usize) -> &mut Self { - assert!(target < self.num_qubits); let step = 1 << target; for i in (0..self.state.len()).step_by(2 * step) { @@ -249,13 +249,13 @@ impl CliffordGateable for StateVec { /// Apply Y = [[0, -i], [i, 0]] gate to target qubit /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn y(&mut self, target: usize) -> &mut Self { - assert!(target < self.num_qubits); - for i in 0..self.state.len() { if (i >> target) & 1 == 0 { let flipped_i = i ^ (1 << target); @@ -269,13 +269,13 @@ impl CliffordGateable for StateVec { /// Apply Z = [[1, 0], [0, -1]] gate to target qubit /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn z(&mut self, target: usize) -> &mut Self { - assert!(target < self.num_qubits); - for i in 0..self.state.len() { if (i >> target) & 1 == 1 { self.state[i] = -self.state[i]; @@ -297,12 +297,13 @@ impl CliffordGateable for StateVec { /// Apply Hadamard gate to the target qubit /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn h(&mut self, target: usize) -> &mut Self { - assert!(target < self.num_qubits); let factor = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0); let step = 1 << target; @@ -324,15 +325,16 @@ impl CliffordGateable for StateVec { /// Apply controlled-X gate /// CX = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ X /// - /// # Panics + /// See [`CliffordGateable::cx`] for detailed documentation. + /// + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 + /// This function assumes that: + /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). + /// - `control != target`. + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn cx(&mut self, control: usize, target: usize) -> &mut Self { - assert!(control < self.num_qubits); - assert!(target < self.num_qubits); - assert_ne!(control, target); - for i in 0..self.state.len() { let control_val = (i >> control) & 1; let target_val = (i >> target) & 1; @@ -347,15 +349,14 @@ impl CliffordGateable for StateVec { /// Apply controlled-Y gate /// CY = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Y /// - /// # Panics + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 + /// This function assumes that: + /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). + /// - `control != target`. + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn cy(&mut self, control: usize, target: usize) -> &mut Self { - assert!(control < self.num_qubits); - assert!(target < self.num_qubits); - assert_ne!(control, target); - // Only process when control bit is 1 and target bit is 0 for i in 0..self.state.len() { let control_val = (i >> control) & 1; @@ -377,15 +378,14 @@ impl CliffordGateable for StateVec { /// Apply controlled-Z gate /// CZ = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Z /// - /// # Panics + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 + /// This function assumes that: + /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). + /// - `control != target`. + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn cz(&mut self, control: usize, target: usize) -> &mut Self { - assert!(control < self.num_qubits); - assert!(target < self.num_qubits); - assert_ne!(control, target); - // CZ is simpler - just add phase when both control and target are 1 for i in 0..self.state.len() { let control_val = (i >> control) & 1; @@ -400,14 +400,14 @@ impl CliffordGateable for StateVec { /// Apply a SWAP gate between two qubits /// - /// # Panics + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits + /// This function assumes that: + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn swap(&mut self, qubit1: usize, qubit2: usize) -> &mut Self { - assert!(qubit1 < self.num_qubits && qubit2 < self.num_qubits); - assert_ne!(qubit1, qubit2); - let step1 = 1 << qubit1; let step2 = 1 << qubit2; @@ -427,12 +427,13 @@ impl CliffordGateable for StateVec { /// Measure a single qubit in the Z basis and collapse the state /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn mz(&mut self, target: usize) -> MeasurementResult { - assert!(target < self.num_qubits); let mut rng = rand::thread_rng(); let step = 1 << target; @@ -477,9 +478,11 @@ impl ArbitraryRotationGateable for StateVec { /// RX(θ) = [[cos(θ/2), -i*sin(θ/2)], /// [-i*sin(θ/2), cos(θ/2)]] /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn rx(&mut self, theta: f64, target: usize) -> &mut Self { let cos = (theta / 2.0).cos(); @@ -497,9 +500,11 @@ impl ArbitraryRotationGateable for StateVec { /// RY(θ) = [[cos(θ/2), -sin(θ/2)], /// [-sin(θ/2), cos(θ/2)]] /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn ry(&mut self, theta: f64, target: usize) -> &mut Self { let cos = (theta / 2.0).cos(); @@ -517,9 +522,11 @@ impl ArbitraryRotationGateable for StateVec { /// RZ(θ) = [[cos(θ/2)-i*sin(θ/2), 0], /// [0, cos(θ/2)+i*sin(θ/2)]] /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn rz(&mut self, theta: f64, target: usize) -> &mut Self { let e_pos = Complex64::from_polar(1.0, -theta / 2.0); @@ -538,9 +545,11 @@ impl ArbitraryRotationGateable for StateVec { /// `U1_3` = [[cos(θ/2), -e^(iλ)sin(θ/2)], /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] /// - /// # Panics + /// # Safety /// - /// Panics if target qubit index is >= number of qubits + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn u(&mut self, theta: f64, phi: f64, lambda: f64, target: usize) -> &mut Self { let cos = (theta / 2.0).cos(); @@ -555,6 +564,13 @@ impl ArbitraryRotationGateable for StateVec { self.single_qubit_rotation(target, u00, u01, u10, u11) } + /// Single qubit rotation in the X-Y plane. + /// + /// # Safety + /// + /// This function assumes that: + /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn r1xy(&mut self, theta: f64, phi: f64, target: usize) -> &mut Self { let cos = (theta / 2.0).cos(); @@ -573,15 +589,14 @@ impl ArbitraryRotationGateable for StateVec { /// Apply RXX(θ) = exp(-i θ XX/2) gate /// This implements evolution under the XX coupling between two qubits /// - /// # Panics + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 + /// This function assumes that: + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn rxx(&mut self, theta: f64, qubit1: usize, qubit2: usize) -> &mut Self { - assert!(qubit1 < self.num_qubits); - assert!(qubit2 < self.num_qubits); - assert_ne!(qubit1, qubit2); - let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); let neg_i_sin = Complex64::new(0.0, -sin); // -i*sin @@ -619,15 +634,14 @@ impl ArbitraryRotationGateable for StateVec { /// Apply RYY(θ) = exp(-i θ YY/2) gate /// - /// # Panics + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 + /// This function assumes that: + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn ryy(&mut self, theta: f64, q1: usize, q2: usize) -> &mut Self { - assert!(q1 < self.num_qubits); - assert!(q2 < self.num_qubits); - assert_ne!(q1, q2); - let cos = (theta / 2.0).cos(); let i_sin = Complex64::new(0.0, 1.0) * (theta / 2.0).sin(); @@ -668,15 +682,13 @@ impl ArbitraryRotationGateable for StateVec { /// ``` /// Optimized by taking advantage of the diagonal structure. /// - /// # Panics + /// # Safety /// - /// Panics if target qubit1 or qubit2 index is >= number of qubits or qubit1 == qubit2 - #[inline] + /// This function assumes that: + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. + /// - These conditions must be ensured by the caller or a higher-level component. fn rzz(&mut self, theta: f64, qubit1: usize, qubit2: usize) -> &mut Self { - assert!(qubit1 < self.num_qubits); - assert!(qubit2 < self.num_qubits); - assert_ne!(qubit1, qubit2); - // RZZ is diagonal in computational basis - just add phases for i in 0..self.state.len() { let bit1 = (i >> qubit1) & 1; @@ -2446,27 +2458,6 @@ mod tests { } } - #[test] - #[should_panic(expected = "assertion failed: target < self.num_qubits")] - fn test_invalid_qubit_index_single() { - let mut q = StateVec::new(2); - q.x(3); // Invalid qubit index - } - - #[test] - #[should_panic(expected = "assertion failed: target < self.num_qubits")] - fn test_invalid_qubit_index_controlled() { - let mut q = StateVec::new(2); - q.cx(1, 2); // Invalid target qubit - } - - #[test] - #[should_panic(expected = "assertion `left != right` failed\n left: 0\n right: 0")] - fn test_control_equals_target() { - let mut q = StateVec::new(2); - q.cx(0, 0); // Control and target are the same - } - #[test] fn test_measurement_consistency() { let mut q = StateVec::new(1); From 204baf2e6a52158efe077bc5a1c14b1b447c10b8 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 10:55:13 -0700 Subject: [PATCH 23/33] Adding general Rng to state_vec --- crates/pecos-qsim/src/state_vec.rs | 44 ++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index a8b1d755..32e11b96 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -13,27 +13,36 @@ use super::arbitrary_rotation_gateable::ArbitraryRotationGateable; use super::clifford_gateable::{CliffordGateable, MeasurementResult}; use super::quantum_simulator::QuantumSimulator; +use pecos_core::SimRng; +use rand_chacha::ChaCha8Rng; use num_complex::Complex64; use rand::Rng; #[derive(Clone, Debug)] -pub struct StateVec { +pub struct StateVec +where + R: SimRng, +{ num_qubits: usize, state: Vec, + rng: R, } impl StateVec { /// Create a new state initialized to |0...0⟩ #[must_use] #[inline] - pub fn new(num_qubits: usize) -> Self { - let size = 1 << num_qubits; // 2^n - let mut state = vec![Complex64::new(0.0, 0.0); size]; - state[0] = Complex64::new(1.0, 0.0); // Prep |0...0> - StateVec { num_qubits, state } + pub fn new(num_qubits: usize) -> StateVec { + let rng = ChaCha8Rng::from_entropy(); + StateVec::with_rng(num_qubits, rng) } +} +impl StateVec +where + R: SimRng, +{ /// Returns the number of qubits in the system /// /// # Returns @@ -52,6 +61,17 @@ impl StateVec { self.num_qubits } + pub fn with_rng(num_qubits: usize, rng: R) -> Self { + let size = 1 << num_qubits; // 2^n + let mut state = vec![Complex64::new(0.0, 0.0); size]; + state[0] = Complex64::new(1.0, 0.0); // Prep |0...0> + StateVec { + num_qubits, + state, + rng, + } + } + /// Initialize from a custom state vector /// /// # Panics @@ -59,10 +79,14 @@ impl StateVec { /// Panics if the input state requires more qubits then `StateVec` has. #[must_use] #[inline] - pub fn from_state(state: Vec) -> Self { + pub fn from_state(state: Vec, rng: R) -> Self { let num_qubits = state.len().trailing_zeros() as usize; assert_eq!(1 << num_qubits, state.len(), "Invalid state vector size"); - StateVec { num_qubits, state } + StateVec { + num_qubits, + state, + rng, + } } /// Prepare a specific computational basis state @@ -434,8 +458,6 @@ impl CliffordGateable for StateVec { /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn mz(&mut self, target: usize) -> MeasurementResult { - let mut rng = rand::thread_rng(); - let step = 1 << target; let mut prob_one = 0.0; @@ -448,7 +470,7 @@ impl CliffordGateable for StateVec { } // Decide measurement outcome - let result = usize::from(rng.gen::() < prob_one); + let result = usize::from(self.rng.gen::() < prob_one); // Collapse and normalize state let mut norm = 0.0; From dbf706ddba5ead981be590170f38cbd806002a08 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 11:39:14 -0700 Subject: [PATCH 24/33] Update state-vector docs --- .../src/arbitrary_rotation_gateable.rs | 52 ++- crates/pecos-qsim/src/clifford_gateable.rs | 8 +- crates/pecos-qsim/src/state_vec.rs | 352 ++++++++++++++---- 3 files changed, 343 insertions(+), 69 deletions(-) diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs index 53a5a086..0e503988 100644 --- a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -32,6 +32,11 @@ use std::f64::consts::{FRAC_PI_2, FRAC_PI_4}; pub trait ArbitraryRotationGateable: CliffordGateable { /// Applies a rotation around the X-axis by an angle `theta`. /// + /// Gate RX(θ) = exp(-i θ X/2) = cos(θ/2) I - i*sin(θ/2) X + /// + /// RX(θ) = [[cos(θ/2), -i*sin(θ/2)], + /// [-i*sin(θ/2), cos(θ/2)]] + /// /// # Parameters /// - `theta`: The rotation angle in radians. /// - `q`: The target qubit index. @@ -42,6 +47,11 @@ pub trait ArbitraryRotationGateable: CliffordGateable { /// Applies a rotation around the Y-axis by an angle `theta`. /// + /// Gate RY(θ) = exp(-i θ Y/2) = cos(θ/2) I - i*sin(θ/2) Y + /// + /// RY(θ) = [[cos(θ/2), -sin(θ/2)], + /// [-sin(θ/2), cos(θ/2)]] + /// /// By default, this is implemented in terms of `sz`, `rx`, and `szdg` gates. /// /// # Parameters @@ -57,6 +67,11 @@ pub trait ArbitraryRotationGateable: CliffordGateable { /// Applies a rotation around the Z-axis by an angle `theta`. /// + /// Gate RZ(θ) = exp(-i θ Z/2) = cos(θ/2) I - i*sin(θ/2) Z + /// + /// RZ(θ) = [[cos(θ/2)-i*sin(θ/2), 0], + /// [0, cos(θ/2)+i*sin(θ/2)]] + /// /// # Parameters /// - `theta`: The rotation angle in radians. /// - `q`: The target qubit index. @@ -65,7 +80,10 @@ pub trait ArbitraryRotationGateable: CliffordGateable { /// A mutable reference to `Self` for method chaining. fn rz(&mut self, theta: f64, q: T) -> &mut Self; - /// Applies a general single-qubit unitary gate with the specified parameters. + /// Applies a general single-qubit unitary U(theta, phi, lambda) gate. + /// + /// `U1_3` = [[cos(θ/2), -e^(iλ)sin(θ/2)], + /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] /// /// By default, this is implemented in terms of `rz` and `ry` gates. /// @@ -82,7 +100,7 @@ pub trait ArbitraryRotationGateable: CliffordGateable { self.rz(lambda, q).ry(theta, q).rz(phi, q) } - /// Applies an XY-plane rotation gate with a specified angle and axis. + /// Applies an X-Y plane rotation gate with a specified angle and axis. /// /// By default, this is implemented in terms of `rz` and `ry` gates. /// @@ -126,6 +144,8 @@ pub trait ArbitraryRotationGateable: CliffordGateable { /// Applies a two-qubit XX rotation gate. /// + /// Apply RXX(θ) = exp(-i θ XX/2) gate + /// /// By default, this is implemented in terms of Hadamard (`h`) and ZZ rotation (`rzz`) gates. /// /// # Parameters @@ -140,7 +160,14 @@ pub trait ArbitraryRotationGateable: CliffordGateable { self.h(q1).h(q2).rzz(theta, q1, q2).h(q1).h(q2) } - /// Applies a two-qubit YY rotation gate. + /// Apply RYY(θ) = exp(-i θ YY/2) gate, which implements evolution under the YY coupling between two qubits. + /// + /// The YY coupling generates entanglement between qubits through the Y⊗Y interaction. + /// For example, RYY(π/2) transforms basis states as follows: + /// - |00⟩ → (|00⟩ - i|11⟩)/√2 + /// - |11⟩ → (|11⟩ - i|00⟩)/√2 + /// - |01⟩ → (|01⟩ + i|10⟩)/√2 + /// - |10⟩ → (|10⟩ + i|01⟩)/√2 /// /// By default, this is implemented in terms of SX and ZZ rotation (`rzz`) gates. /// @@ -156,7 +183,24 @@ pub trait ArbitraryRotationGateable: CliffordGateable { self.sx(q1).sx(q2).rzz(theta, q1, q2).sxdg(q1).sxdg(q2) } - /// Applies a two-qubit ZZ rotation gate. + /// Apply RZZ(θ) = exp(-i θ ZZ/2) gate, implementing evolution under the ZZ coupling between two qubits. + /// + /// The ZZ coupling represents a phase interaction between qubits that is diagonal in the computational basis. + /// It is a key component in many quantum algorithms and appears naturally in various physical implementations. + /// The operation adds a θ/2 phase when the qubits have the same value, and -θ/2 phase when they differ. + /// + /// The action on basis states is: + /// - |00⟩ → exp(-iθ/2)|00⟩ + /// - |11⟩ → exp(-iθ/2)|11⟩ + /// - |01⟩ → exp(iθ/2)|01⟩ + /// - |10⟩ → exp(iθ/2)|10⟩ + /// + /// The matrix: + /// ```text + /// RZZ(θ) = [[e^(-iθ/2), 0, 0, 0 ], + /// [0, e^(iθ/2), 0, 0 ], + /// [0, 0, e^(iθ/2), 0 ], + /// [0, 0, 0, e^(-iθ/2) ]] /// /// # Parameters /// - `theta`: The rotation angle in radians. diff --git a/crates/pecos-qsim/src/clifford_gateable.rs b/crates/pecos-qsim/src/clifford_gateable.rs index fb5d90c5..8c69533d 100644 --- a/crates/pecos-qsim/src/clifford_gateable.rs +++ b/crates/pecos-qsim/src/clifford_gateable.rs @@ -927,6 +927,8 @@ pub trait CliffordGateable: QuantumSimulator { /// * `q1` - Control qubit index. /// * `q2` - Target qubit index. /// + /// CX = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ X + /// /// # Pauli Transformation /// ```text /// XI → XX @@ -962,6 +964,8 @@ pub trait CliffordGateable: QuantumSimulator { /// * `q1` - Control qubit index. /// * `q2` - Target qubit index. /// + /// CY = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Y + /// /// # Pauli Transformation /// ```text /// XI → XY @@ -1000,6 +1004,8 @@ pub trait CliffordGateable: QuantumSimulator { /// * `q1` - First qubit index. /// * `q2` - Second qubit index. /// + /// CZ = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Z + /// /// # Pauli Transformation /// ```text /// XI → XZ @@ -1538,7 +1544,7 @@ pub trait CliffordGateable: QuantumSimulator { meas } - /// Measures the +Z Pauli operator, projecting to the measured eigenstate. + /// Measures the +Z Pauli operator, projecting to the measured eigenstate (collapse the state). /// /// Projects the state into either the |0⟩ or |1⟩ eigenstate based on the /// measurement outcome. This is the standard computational basis measurement. diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 32e11b96..69e42f6a 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -252,12 +252,27 @@ impl QuantumSimulator for StateVec { } impl CliffordGateable for StateVec { - /// Apply Pauli-X gate + /// Implementation of Pauli-X gate for state vectors. /// - /// # Safety + /// See [`CliffordGateable::x`] for mathematical details and gate properties. + /// This implementation uses direct state vector manipulation for performance. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(2); /// + /// // Flip first qubit + /// state.x(0); + /// + /// // Create Bell state using X gate + /// state.h(1).cx(1, 0); + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn x(&mut self, target: usize) -> &mut Self { @@ -271,12 +286,24 @@ impl CliffordGateable for StateVec { self } - /// Apply Y = [[0, -i], [i, 0]] gate to target qubit + /// Implementation of Pauli-Y gate for state vectors. /// - /// # Safety + /// See [`CliffordGateable::y`] for mathematical details and gate properties. + /// This implementation directly updates complex amplitudes including phases. /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(1); + /// + /// // Apply Y gate to create state with imaginary amplitudes + /// state.y(0); // Creates i|1⟩ + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn y(&mut self, target: usize) -> &mut Self { @@ -291,12 +318,24 @@ impl CliffordGateable for StateVec { self } - /// Apply Z = [[1, 0], [0, -1]] gate to target qubit + /// Implementation of Pauli-Z gate for state vectors. /// - /// # Safety + /// See [`CliffordGateable::z`] for mathematical details and gate properties. + /// Takes advantage of diagonal structure for optimal performance. /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(1); + /// + /// // Create superposition and apply phase + /// state.h(0).z(0); // Creates (|0⟩ - |1⟩)/√2 + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn z(&mut self, target: usize) -> &mut Self { @@ -308,6 +347,25 @@ impl CliffordGateable for StateVec { self } + /// Implementation of square root of Z (S) gate for state vectors. + /// + /// See [`CliffordGateable::sz`] for mathematical details and gate properties. + /// Implemented using optimized single-qubit rotation. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(1); + /// + /// // Create state with π/2 phase + /// state.h(0).sz(0); // Creates (|0⟩ + i|1⟩)/√2 + /// ``` + /// + /// # Safety + /// This function assumes that: + /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn sz(&mut self, q: usize) -> &mut Self { self.single_qubit_rotation( @@ -319,12 +377,24 @@ impl CliffordGateable for StateVec { ) } - /// Apply Hadamard gate to the target qubit + /// Implementation of Hadamard gate for state vectors. /// - /// # Safety + /// See [`CliffordGateable::h`] for mathematical details and gate properties. + /// This implementation directly computes the superposition amplitudes. /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(2); + /// + /// // Create Bell state using Hadamard + /// state.h(0).cx(0, 1); + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn h(&mut self, target: usize) -> &mut Self { @@ -346,13 +416,22 @@ impl CliffordGateable for StateVec { self } - /// Apply controlled-X gate - /// CX = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ X + /// Implementation of controlled-X (CNOT) gate for state vectors. /// - /// See [`CliffordGateable::cx`] for detailed documentation. + /// See [`CliffordGateable::cx`] for mathematical details and gate properties. + /// Uses bit manipulation for fast controlled operations. /// - /// # Safety + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; /// + /// let mut state = StateVec::new(3); + /// + /// // Create GHZ state with CNOT cascade + /// state.h(0).cx(0, 1).cx(1, 2); + /// ``` + /// + /// # Safety /// This function assumes that: /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). /// - `control != target`. @@ -370,11 +449,22 @@ impl CliffordGateable for StateVec { self } - /// Apply controlled-Y gate - /// CY = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Y + /// Implementation of controlled-Y gate for state vectors. /// - /// # Safety + /// See [`CliffordGateable::cy`] for mathematical details and gate properties. + /// Combines bit manipulation with phase updates for controlled-Y operation. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(2); /// + /// // Create entangled state with imaginary phase + /// state.h(0).cy(0, 1); // Creates (|00⟩ + i|11⟩)/√2 + /// ``` + /// + /// # Safety /// This function assumes that: /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). /// - `control != target`. @@ -399,11 +489,22 @@ impl CliffordGateable for StateVec { self } - /// Apply controlled-Z gate - /// CZ = |0⟩⟨0| ⊗ I + |1⟩⟨1| ⊗ Z + /// Implementation of controlled-Z gate for state vectors. /// - /// # Safety + /// See [`CliffordGateable::cz`] for mathematical details and gate properties. + /// Takes advantage of diagonal structure for optimal performance. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(2); + /// + /// // Create cluster state + /// state.h(0).h(1).cz(0, 1); + /// ``` /// + /// # Safety /// This function assumes that: /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). /// - `control != target`. @@ -422,10 +523,22 @@ impl CliffordGateable for StateVec { self } - /// Apply a SWAP gate between two qubits + /// Implementation of SWAP gate for state vectors. /// - /// # Safety + /// See [`CliffordGateable::swap`] for mathematical details and gate properties. + /// Uses bit manipulation for efficient state vector updates. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; + /// + /// let mut state = StateVec::new(2); + /// + /// // Create state and swap qubits + /// state.h(0).x(1).swap(0, 1); + /// ``` /// + /// # Safety /// This function assumes that: /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). /// - `qubit1 != qubit2`. @@ -449,12 +562,28 @@ impl CliffordGateable for StateVec { self } - /// Measure a single qubit in the Z basis and collapse the state + /// Implementation of Z-basis measurement for state vectors. /// - /// # Safety + /// See [`CliffordGateable::mz`] for mathematical details and measurement properties. + /// Computes measurement probabilities and performs state collapse. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; /// + /// let mut state = StateVec::new(2); + /// + /// // Create Bell state and measure first qubit + /// state.h(0).cx(0, 1); + /// let result = state.mz(0); + /// // Second qubit measurement will match first + /// let result2 = state.mz(1); + /// assert_eq!(result.outcome, result2.outcome); + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn mz(&mut self, target: usize) -> MeasurementResult { @@ -496,14 +625,25 @@ impl CliffordGateable for StateVec { } impl ArbitraryRotationGateable for StateVec { - /// Gate RX(θ) = exp(-i θ X/2) = cos(θ/2) I - i*sin(θ/2) X - /// RX(θ) = [[cos(θ/2), -i*sin(θ/2)], - /// [-i*sin(θ/2), cos(θ/2)]] + /// Implementation of rotation around the X-axis. /// - /// # Safety + /// See [`ArbitraryRotationGateable::rx`] for mathematical details and gate properties. + /// This implementation directly updates amplitudes in the state vector for optimal performance. /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, ArbitraryRotationGateable}; + /// use std::f64::consts::FRAC_PI_2; + /// + /// let mut state = StateVec::new(1); + /// + /// // Create superposition with phase + /// state.rx(FRAC_PI_2, 0); // Creates (|0⟩ - i|1⟩)/√2 + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn rx(&mut self, theta: f64, target: usize) -> &mut Self { @@ -518,14 +658,25 @@ impl ArbitraryRotationGateable for StateVec { ) } - /// Gate RY(θ) = exp(-i θ Y/2) = cos(θ/2) I - i*sin(θ/2) Y - /// RY(θ) = [[cos(θ/2), -sin(θ/2)], - /// [-sin(θ/2), cos(θ/2)]] + /// Implementation of rotation around the Y-axis. /// - /// # Safety + /// See [`ArbitraryRotationGateable::ry`] for mathematical details and gate properties. + /// This implementation directly updates amplitudes in the state vector for optimal performance. /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, ArbitraryRotationGateable}; + /// use std::f64::consts::PI; + /// + /// let mut state = StateVec::new(1); + /// + /// // Create equal superposition + /// state.ry(PI/2.0, 0); // Creates (|0⟩ + |1⟩)/√2 + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn ry(&mut self, theta: f64, target: usize) -> &mut Self { @@ -540,16 +691,26 @@ impl ArbitraryRotationGateable for StateVec { ) } - /// Gate RZ(θ) = exp(-i θ Z/2) = cos(θ/2) I - i*sin(θ/2) Z - /// RZ(θ) = [[cos(θ/2)-i*sin(θ/2), 0], - /// [0, cos(θ/2)+i*sin(θ/2)]] + /// Implementation of rotation around the Z-axis. /// - /// # Safety + /// See [`ArbitraryRotationGateable::rz`] for mathematical details and gate properties. + /// Takes advantage of the diagonal structure in computational basis for optimal performance. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable, ArbitraryRotationGateable}; + /// use std::f64::consts::FRAC_PI_4; + /// + /// let mut state = StateVec::new(1); /// + /// // Create superposition and add phase + /// state.h(0).rz(FRAC_PI_4, 0); // T gate equivalent + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. - #[inline] fn rz(&mut self, theta: f64, target: usize) -> &mut Self { let e_pos = Complex64::from_polar(1.0, -theta / 2.0); let e_neg = Complex64::from_polar(1.0, theta / 2.0); @@ -563,14 +724,25 @@ impl ArbitraryRotationGateable for StateVec { ) } - /// Apply U(theta, phi, lambda) gate - /// `U1_3` = [[cos(θ/2), -e^(iλ)sin(θ/2)], - /// [e^(iφ)sin(θ/2), e^(i(λ+φ))cos(θ/2)]] + /// Implementation of general single-qubit unitary rotation. /// - /// # Safety + /// See [`ArbitraryRotationGateable::u`] for mathematical details and gate properties. + /// This implementation directly updates amplitudes in the state vector. /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, ArbitraryRotationGateable}; + /// use std::f64::consts::{PI, FRAC_PI_2}; + /// + /// let mut state = StateVec::new(1); + /// + /// // Create arbitrary rotation (equivalent to TH up to global phase) + /// state.u(FRAC_PI_2, 0.0, FRAC_PI_2, 0); + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn u(&mut self, theta: f64, phi: f64, lambda: f64, target: usize) -> &mut Self { @@ -586,12 +758,25 @@ impl ArbitraryRotationGateable for StateVec { self.single_qubit_rotation(target, u00, u01, u10, u11) } - /// Single qubit rotation in the X-Y plane. + /// Implementation of single-qubit rotation in XY plane. /// - /// # Safety + /// See [`ArbitraryRotationGateable::r1xy`] for mathematical details and gate properties. + /// Optimized for rotations in the XY plane of the Bloch sphere. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, ArbitraryRotationGateable}; + /// use std::f64::consts::FRAC_PI_2; /// + /// let mut state = StateVec::new(1); + /// + /// // 90-degree rotation around X+Y axis + /// state.r1xy(FRAC_PI_2, FRAC_PI_2, 0); + /// ``` + /// + /// # Safety /// This function assumes that: - /// - `target` is a valid qubit indices (i.e., `< number of qubits`). + /// - `target` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn r1xy(&mut self, theta: f64, phi: f64, target: usize) -> &mut Self { @@ -608,11 +793,23 @@ impl ArbitraryRotationGateable for StateVec { self.single_qubit_rotation(target, r00, r01, r10, r11) } - /// Apply RXX(θ) = exp(-i θ XX/2) gate - /// This implements evolution under the XX coupling between two qubits + /// Implementation of two-qubit XX rotation. /// - /// # Safety + /// See [`ArbitraryRotationGateable::rxx`] for mathematical details and gate properties. + /// This implementation directly updates amplitudes in the state vector for optimal performance. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, ArbitraryRotationGateable}; + /// use std::f64::consts::FRAC_PI_2; + /// + /// let mut state = StateVec::new(2); /// + /// // Create maximally entangled state + /// state.rxx(FRAC_PI_2, 0, 1); // Creates (|00⟩ - i|11⟩)/√2 + /// ``` + /// + /// # Safety /// This function assumes that: /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). /// - `qubit1 != qubit2`. @@ -654,10 +851,27 @@ impl ArbitraryRotationGateable for StateVec { self } - /// Apply RYY(θ) = exp(-i θ YY/2) gate + /// Implementation of the RYY(θ) gate for state vectors. /// - /// # Safety + /// See [`ArbitraryRotationGateable::ryy`] for mathematical details and gate properties. + /// This implementation directly updates amplitudes in the state vector for optimal performance. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable, ArbitraryRotationGateable}; + /// use std::f64::consts::FRAC_PI_2; + /// + /// let mut state = StateVec::new(2); + /// + /// // Create entangled state + /// state.h(0) + /// .cx(0, 1); /// + /// // Apply RYY rotation + /// state.ryy(FRAC_PI_2, 0, 1); + /// ``` + /// + /// # Safety /// This function assumes that: /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). /// - `qubit1 != qubit2`. @@ -693,16 +907,26 @@ impl ArbitraryRotationGateable for StateVec { self } - /// Apply RZZ(θ) = exp(-i θ ZZ/2) gate + /// Implementation of the RZZ(θ) gate for state vectors. + /// + /// See [`ArbitraryRotationGateable::rzz`] for mathematical details and gate properties. + /// Takes advantage of the diagonal structure in the computational basis for optimal performance. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable, ArbitraryRotationGateable}; + /// use std::f64::consts::PI; + /// + /// let mut state = StateVec::new(3); + /// + /// // Create GHZ state + /// state.h(0) + /// .cx(0, 1) + /// .cx(1, 2); /// - /// This is an optimized implementation of the general two-qubit unitary: - /// ```text - /// RZZ(θ) = [[e^(-iθ/2), 0, 0, 0 ], - /// [0, e^(iθ/2), 0, 0 ], - /// [0, 0, e^(iθ/2), 0 ], - /// [0, 0, 0, e^(-iθ/2) ]] + /// // Apply phase rotation between first and last qubit + /// state.rzz(PI/4.0, 0, 2); /// ``` - /// Optimized by taking advantage of the diagonal structure. /// /// # Safety /// From 3959be0a88a75e25cdec5decfea84b1b459c1608 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 11:53:21 -0700 Subject: [PATCH 25/33] Unifying safety notification in state-vec docs --- crates/pecos-qsim/src/state_vec.rs | 107 ++++++++++++++--------------- 1 file changed, 52 insertions(+), 55 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 69e42f6a..5825fc0d 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -147,9 +147,8 @@ where /// ``` /// /// # Safety - /// /// This function assumes that: - /// - `qubit` is a valid qubit indices (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] pub fn single_qubit_rotation( @@ -183,7 +182,6 @@ where /// [u30, u31, u32, u33]] /// /// # Safety - /// /// This function assumes that: /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). /// - `qubit1 != qubit2`. @@ -272,11 +270,11 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn x(&mut self, target: usize) -> &mut Self { - let step = 1 << target; + fn x(&mut self, qubit: usize) -> &mut Self { + let step = 1 << qubit; for i in (0..self.state.len()).step_by(2 * step) { for offset in 0..step { @@ -303,7 +301,7 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] fn y(&mut self, target: usize) -> &mut Self { @@ -335,12 +333,12 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn z(&mut self, target: usize) -> &mut Self { + fn z(&mut self, qubit: usize) -> &mut Self { for i in 0..self.state.len() { - if (i >> target) & 1 == 1 { + if (i >> qubit) & 1 == 1 { self.state[i] = -self.state[i]; } } @@ -364,12 +362,12 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn sz(&mut self, q: usize) -> &mut Self { + fn sz(&mut self, qubit: usize) -> &mut Self { self.single_qubit_rotation( - q, + qubit, Complex64::new(1.0, 0.0), // u00 Complex64::new(0.0, 0.0), // u01 Complex64::new(0.0, 0.0), // u10 @@ -394,12 +392,12 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn h(&mut self, target: usize) -> &mut Self { + fn h(&mut self, qubit: usize) -> &mut Self { let factor = Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0); - let step = 1 << target; + let step = 1 << qubit; for i in (0..self.state.len()).step_by(2 * step) { for offset in 0..step { @@ -433,16 +431,16 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). - /// - `control != target`. + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn cx(&mut self, control: usize, target: usize) -> &mut Self { + fn cx(&mut self, qubit1: usize, qubit2: usize) -> &mut Self { for i in 0..self.state.len() { - let control_val = (i >> control) & 1; - let target_val = (i >> target) & 1; + let control_val = (i >> qubit1) & 1; + let target_val = (i >> qubit2) & 1; if control_val == 1 && target_val == 0 { - let flipped_i = i ^ (1 << target); + let flipped_i = i ^ (1 << qubit2); self.state.swap(i, flipped_i); } } @@ -466,18 +464,18 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). - /// - `control != target`. + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn cy(&mut self, control: usize, target: usize) -> &mut Self { + fn cy(&mut self, qubit1: usize, qubit2: usize) -> &mut Self { // Only process when control bit is 1 and target bit is 0 for i in 0..self.state.len() { - let control_val = (i >> control) & 1; - let target_val = (i >> target) & 1; + let control_val = (i >> qubit1) & 1; + let target_val = (i >> qubit2) & 1; if control_val == 1 && target_val == 0 { - let flipped_i = i ^ (1 << target); + let flipped_i = i ^ (1 << qubit2); // Y gate has different phases than X // Y = [[0, -i], [i, 0]] @@ -506,15 +504,15 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `control` and `target` are valid qubit indices (i.e., `< number of qubits`). - /// - `control != target`. + /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). + /// - `qubit1 != qubit2`. /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn cz(&mut self, control: usize, target: usize) -> &mut Self { + fn cz(&mut self, qubit1: usize, qubit2: usize) -> &mut Self { // CZ is simpler - just add phase when both control and target are 1 for i in 0..self.state.len() { - let control_val = (i >> control) & 1; - let target_val = (i >> target) & 1; + let control_val = (i >> qubit1) & 1; + let target_val = (i >> qubit2) & 1; if control_val == 1 && target_val == 1 { self.state[i] = -self.state[i]; @@ -583,11 +581,11 @@ impl CliffordGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn mz(&mut self, target: usize) -> MeasurementResult { - let step = 1 << target; + fn mz(&mut self, qubit: usize) -> MeasurementResult { + let step = 1 << qubit; let mut prob_one = 0.0; // Calculate probability of measuring |1⟩ @@ -604,7 +602,7 @@ impl CliffordGateable for StateVec { // Collapse and normalize state let mut norm = 0.0; for i in 0..self.state.len() { - let bit = (i >> target) & 1; + let bit = (i >> qubit) & 1; if bit == result { norm += self.state[i].norm_sqr(); } else { @@ -643,12 +641,12 @@ impl ArbitraryRotationGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn rx(&mut self, theta: f64, target: usize) -> &mut Self { - let cos = (theta / 2.0).cos(); - let sin = (theta / 2.0).sin(); + fn rx(&mut self, qubit: f64, target: usize) -> &mut Self { + let cos = (qubit / 2.0).cos(); + let sin = (qubit / 2.0).sin(); self.single_qubit_rotation( target, Complex64::new(cos, 0.0), @@ -676,14 +674,14 @@ impl ArbitraryRotationGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn ry(&mut self, theta: f64, target: usize) -> &mut Self { + fn ry(&mut self, theta: f64, qubit: usize) -> &mut Self { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); self.single_qubit_rotation( - target, + qubit, Complex64::new(cos, 0.0), Complex64::new(-sin, 0.0), Complex64::new(sin, 0.0), @@ -709,14 +707,14 @@ impl ArbitraryRotationGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. - fn rz(&mut self, theta: f64, target: usize) -> &mut Self { + fn rz(&mut self, theta: f64, qubit: usize) -> &mut Self { let e_pos = Complex64::from_polar(1.0, -theta / 2.0); let e_neg = Complex64::from_polar(1.0, theta / 2.0); self.single_qubit_rotation( - target, + qubit, e_pos, Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), @@ -742,10 +740,10 @@ impl ArbitraryRotationGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn u(&mut self, theta: f64, phi: f64, lambda: f64, target: usize) -> &mut Self { + fn u(&mut self, theta: f64, phi: f64, lambda: f64, qubit: usize) -> &mut Self { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); @@ -755,7 +753,7 @@ impl ArbitraryRotationGateable for StateVec { let u10 = Complex64::from_polar(sin, phi); let u11 = Complex64::from_polar(cos, phi + lambda); - self.single_qubit_rotation(target, u00, u01, u10, u11) + self.single_qubit_rotation(qubit, u00, u01, u10, u11) } /// Implementation of single-qubit rotation in XY plane. @@ -776,10 +774,10 @@ impl ArbitraryRotationGateable for StateVec { /// /// # Safety /// This function assumes that: - /// - `target` is a valid qubit index (i.e., `< number of qubits`). + /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn r1xy(&mut self, theta: f64, phi: f64, target: usize) -> &mut Self { + fn r1xy(&mut self, theta: f64, phi: f64, qubit: usize) -> &mut Self { let cos = (theta / 2.0).cos(); let sin = (theta / 2.0).sin(); @@ -790,7 +788,7 @@ impl ArbitraryRotationGateable for StateVec { let r11 = Complex64::new(cos, 0.0); // cos(θ/2) // Apply the single-qubit rotation using the matrix elements - self.single_qubit_rotation(target, r00, r01, r10, r11) + self.single_qubit_rotation(qubit, r00, r01, r10, r11) } /// Implementation of two-qubit XX rotation. @@ -929,7 +927,6 @@ impl ArbitraryRotationGateable for StateVec { /// ``` /// /// # Safety - /// /// This function assumes that: /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). /// - `qubit1 != qubit2`. @@ -989,7 +986,7 @@ mod tests { /// - `state2`: A reference to the second state vector. /// /// # Panics - /// Panics if the states differ in norm or relative phase beyond a small numerical tolerance. + /// The function will panic if the states differ in norm or relative phase beyond a small numerical tolerance. fn assert_states_equal(state1: &[Complex64], state2: &[Complex64]) { const TOLERANCE: f64 = 1e-10; From fd044e7164e747dd622ff0bc9f250d462a5fa482 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 11:55:24 -0700 Subject: [PATCH 26/33] Updated the test doc for state-vec --- crates/pecos-qsim/src/state_vec.rs | 43 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 5825fc0d..865fe67d 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -952,23 +952,36 @@ impl ArbitraryRotationGateable for StateVec { } } -/// ## Key Invariants -/// - **Normalization**: The total probability (norm squared) of the state vector -/// must always be 1. -/// - **Unitarity**: All gate operations must preserve the norm of the state vector. -/// - **Phase Consistency**: States are compared up to a global phase. + +/// Test suite for state vector quantum simulation. +/// +/// # Organization +/// The tests are organized into several categories: +/// - Basic state manipulation and access (new state, preparation, etc.) +/// - Single-qubit gate operations (X, Y, Z, H, etc.) +/// - Two-qubit gate operations (CX, CY, CZ, SWAP) +/// - Rotation gates (RX, RY, RZ, RXX, RYY, RZZ) +/// - Measurement operations +/// - Gate relationships and decompositions +/// - Edge cases and numerical stability +/// - System scaling and locality +/// +/// # Testing Strategy +/// Tests verify: +/// 1. Basic correctness: Each operation produces expected output states +/// 2. Mathematical properties: Unitarity, phase relationships, commutation +/// 3. Physical requirements: State normalization, measurement statistics +/// 4. Implementation properties: Numerical stability, locality of operations +/// 5. Gate relationships: Standard decompositions and equivalent implementations /// -/// ## Testing Strategy -/// - **Unit Tests**: Validate individual operations (e.g., `RX`, `RY`, `RZ`, `CX`, `CY`, etc.). -/// - **Compositional Tests**: Verify decompositions, commutation relations, and symmetry properties. -/// - **Edge Cases**: Test with boundary values (e.g., `θ = 0`, `θ = 2π`) and systems near memory limits. -/// - **Randomized Tests**: Evaluate probabilistic operations like measurement and ensure statistical validity. -/// - **Integration Tests**: Combine operations to ensure the overall system behaves as expected. +/// # Helper Functions +/// - `assert_states_equal`: Compares quantum states up to global phase +/// - `assert_state_vectors_match`: Detailed comparison with tolerance checking /// -/// ## Test Organization -/// - Each gate or operation is tested independently for correctness. -/// - Helper functions like `assert_states_equal` are used to compare quantum states up to global phase. -/// - Failures provide clear diagnostic outputs for debugging, including mismatches and intermediate states. +/// # Notes +/// - Tests use standard tolerances of 1e-10 for floating point comparisons +/// - Random tests use fixed seeds for reproducibility +/// - Large system tests verify scaling behavior up to 20 qubits #[cfg(test)] mod tests { use super::*; From ff415f25ccfe3439bb155304dc6627c240b1f18a Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 12:06:04 -0700 Subject: [PATCH 27/33] Renaming rxxryyrzz gate base on actual gate application --- crates/pecos-qsim/src/arbitrary_rotation_gateable.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs index 0e503988..8190a7a3 100644 --- a/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs +++ b/crates/pecos-qsim/src/arbitrary_rotation_gateable.rs @@ -226,8 +226,7 @@ pub trait ArbitraryRotationGateable: CliffordGateable { /// # Note /// The current implementation might have a reversed order of operations. #[inline] - fn rxxryyrzz(&mut self, theta: f64, phi: f64, lambda: f64, q1: T, q2: T) -> &mut Self { - // TODO: This is likely backwards.. + fn rzzryyrxx(&mut self, theta: f64, phi: f64, lambda: f64, q1: T, q2: T) -> &mut Self { self.rxx(theta, q1, q2).ryy(phi, q1, q2).rzz(lambda, q1, q2) } } From 31162646e18062d360838ea3f92d4ea1e59ba52f Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 12:16:30 -0700 Subject: [PATCH 28/33] Updating state-vec docs --- crates/pecos-qsim/src/state_vec.rs | 77 +++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 865fe67d..51b0dc74 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -19,6 +19,25 @@ use rand_chacha::ChaCha8Rng; use num_complex::Complex64; use rand::Rng; +/// A quantum state simulator using the state vector representation +/// +/// StateVec maintains the full quantum state as a complex vector with 2ⁿ amplitudes +/// for n qubits. This allows exact simulation of quantum operations but requires +/// memory that scales exponentially with the number of qubits. +/// +/// # Type Parameters +/// * `R` - Random number generator type implementing SimRng trait +/// +/// # Examples +/// ```rust +/// use pecos_qsim::StateVec; +/// +/// // Create a new 2-qubit system +/// let mut state = StateVec::new(2); +/// +/// // Prepare a superposition state +/// state.prepare_plus_state(); +/// ``` #[derive(Clone, Debug)] pub struct StateVec where @@ -31,8 +50,8 @@ where impl StateVec { /// Create a new state initialized to |0...0⟩ - #[must_use] #[inline] + #[must_use] pub fn new(num_qubits: usize) -> StateVec { let rng = ChaCha8Rng::from_entropy(); StateVec::with_rng(num_qubits, rng) @@ -55,12 +74,30 @@ where /// let num = state.num_qubits(); /// assert_eq!(num, 2); /// ``` - #[must_use] #[inline] + #[must_use] pub fn num_qubits(&self) -> usize { self.num_qubits } + /// Create a new state vector with a custom random number generator. By doing so, one may set a + /// seed or utilize a different base random number generator. + /// + /// # Arguments + /// * `num_qubits` - Number of qubits in the system + /// * `rng` - Random number generator implementing SimRng trait + /// + /// # Examples + /// ```rust + /// use pecos_qsim::StateVec; + /// use rand_chacha::ChaCha12Rng; + /// use rand::SeedableRng; + /// + /// let rng = ChaCha12Rng::seed_from_u64(42); + /// let state = StateVec::with_rng(2, rng); + /// ``` + #[inline] + #[must_use] pub fn with_rng(num_qubits: usize, rng: R) -> Self { let size = 1 << num_qubits; // 2^n let mut state = vec![Complex64::new(0.0, 0.0); size]; @@ -75,10 +112,9 @@ where /// Initialize from a custom state vector /// /// # Panics - /// - /// Panics if the input state requires more qubits then `StateVec` has. - #[must_use] + /// Code will panic if the input state requires more qubits then `StateVec` has. #[inline] + #[must_use] pub fn from_state(state: Vec, rng: R) -> Self { let num_qubits = state.len().trailing_zeros() as usize; assert_eq!(1 << num_qubits, state.len(), "Invalid state vector size"); @@ -92,9 +128,9 @@ where /// Prepare a specific computational basis state /// /// # Panics - /// - /// Panics if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) + /// Code will panic if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) #[inline] + #[must_use] pub fn prepare_computational_basis(&mut self, basis_state: usize) -> &mut Self { assert!(basis_state < 1 << self.num_qubits); self.state.fill(Complex64::new(0.0, 0.0)); @@ -102,8 +138,19 @@ where self } - /// Prepare all qubits in |+⟩ state + /// Prepare all qubits in the |+⟩ state, creating an equal superposition of all basis states + /// + /// This operation prepares the state (1/√2ⁿ)(|0...0⟩ + |0...1⟩ + ... + |1...1⟩) + /// where n is the number of qubits. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::StateVec; + /// let mut state = StateVec::new(2); + /// state.prepare_plus_state(); + /// ``` #[inline] + #[must_use] pub fn prepare_plus_state(&mut self) -> &mut Self { let factor = Complex64::new(1.0 / f64::from(1 << self.num_qubits), 0.0); self.state.fill(factor); @@ -111,8 +158,14 @@ where } /// Returns reference to the state vector - #[must_use] + /// + /// The state vector is guaranteed to be normalized such that the sum of + /// probability amplitudes squared equals 1. + /// + /// # Returns + /// A slice containing the complex amplitudes of the quantum state #[inline] + #[must_use] pub fn state(&self) -> &[Complex64] { &self.state } @@ -120,10 +173,9 @@ where /// Returns the probability of measuring a specific basis state /// /// # Panics - /// - /// Panics if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) - #[must_use] + /// Code will panic if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) #[inline] + #[must_use] pub fn probability(&self, basis_state: usize) -> f64 { assert!(basis_state < 1 << self.num_qubits); self.state[basis_state].norm_sqr() @@ -952,7 +1004,6 @@ impl ArbitraryRotationGateable for StateVec { } } - /// Test suite for state vector quantum simulation. /// /// # Organization From 5dd84967f046a51bc1953fe54dcc3dc1a92d94ce Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 12:22:39 -0700 Subject: [PATCH 29/33] Fix RXRYYRZZ -> RZZRYYRXX --- crates/pecos-python/src/state_vec_bindings.rs | 10 +++++----- crates/pecos-qsim/src/state_vec.rs | 8 ++++---- python/pecos-rslib/src/pecos_rslib/rsstate_vec.py | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/pecos-python/src/state_vec_bindings.rs b/crates/pecos-python/src/state_vec_bindings.rs index b1dde38d..adddb04c 100644 --- a/crates/pecos-python/src/state_vec_bindings.rs +++ b/crates/pecos-python/src/state_vec_bindings.rs @@ -438,28 +438,28 @@ impl RsStateVec { Ok(None) } - "RXXRYYRZZ" => { + "RZZRYYRXX" => { if let Some(params) = params { match params.get_item("angles") { Ok(Some(py_any)) => { if let Ok(angles) = py_any.extract::>() { if angles.len() >= 3 { self.inner - .rxxryyrzz(angles[0], angles[1], angles[2], q1, q2); + .rzzryyrxx(angles[0], angles[1], angles[2], q1, q2); } else { return Err(PyErr::new::( - "RXXRYYRZZ gate requires three angle parameters", + "RZZRYYRXX gate requires three angle parameters", )); } } else { return Err(PyErr::new::( - "Expected valid angle parameters for RXXRYYRZZ gate", + "Expected valid angle parameters for RZZRYYRXX gate", )); } } Ok(None) => { return Err(PyErr::new::( - "Angle parameters missing for RXXRYYRZZ gate", + "Angle parameters missing for RZZRYYRXX gate", )); } Err(err) => { diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 51b0dc74..0cebb0d4 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -696,11 +696,11 @@ impl ArbitraryRotationGateable for StateVec { /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn rx(&mut self, qubit: f64, target: usize) -> &mut Self { - let cos = (qubit / 2.0).cos(); - let sin = (qubit / 2.0).sin(); + fn rx(&mut self, theta: f64, qubit: usize) -> &mut Self { + let cos = (theta / 2.0).cos(); + let sin = (theta / 2.0).sin(); self.single_qubit_rotation( - target, + qubit, Complex64::new(cos, 0.0), Complex64::new(0.0, -sin), Complex64::new(0.0, -sin), diff --git a/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py b/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py index cb3d34e8..9a73c60d 100644 --- a/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py +++ b/python/pecos-rslib/src/pecos_rslib/rsstate_vec.py @@ -256,13 +256,13 @@ def run_circuit( qs, {"angle": params["angles"][0]} if "angles" in params else {"angle": 0}, ), - "RXXRYYRZZ": lambda sim, qs, **params: sim._sim.run_2q_gate( - "RXXRYYRZZ", + "RZZRYYRXX": lambda sim, qs, **params: sim._sim.run_2q_gate( + "RZZRYYRXX", qs, {"angles": params["angles"]} if "angles" in params else {"angles": [0, 0, 0]}, ), "R2XXYYZZ": lambda sim, qs, **params: sim._sim.run_2q_gate( - "RXXRYYRZZ", + "RZZRYYRXX", qs, {"angles": params["angles"]} if "angles" in params else {"angles": [0, 0, 0]}, ), From 88ddbf786776d4535957efee41394188bb25366a Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Sun, 12 Jan 2025 22:48:46 -0700 Subject: [PATCH 30/33] more tests for state-vec + small doc improvement --- crates/pecos-qsim/src/state_vec.rs | 851 +++++++++++++++++++++++++++-- 1 file changed, 806 insertions(+), 45 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 0cebb0d4..a7d35aea 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -50,6 +50,18 @@ where impl StateVec { /// Create a new state initialized to |0...0⟩ + /// + /// # Examples + /// ```rust + /// use pecos_qsim::StateVec; + /// + /// // Initialize a 3-qubit state vector in the |000⟩ state + /// let state_vec = StateVec::new(3); + /// + /// // Confirm the state is |000⟩ + /// let prob = state_vec.probability(0); + /// assert!((prob - 1.0).abs() < 1e-10); + /// ``` #[inline] #[must_use] pub fn new(num_qubits: usize) -> StateVec { @@ -111,6 +123,22 @@ where /// Initialize from a custom state vector /// + /// # Examples + /// ```rust + /// use pecos_core::SimRng; + /// use num_complex::Complex64; + /// use pecos_qsim::StateVec; + /// + /// let custom_state = vec![ + /// Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + /// Complex64::new(1.0 / 2.0_f64.sqrt(), 0.0), + /// Complex64::new(0.0, 0.0), + /// Complex64::new(0.0, 0.0), + /// ]; + /// + /// let state_vec = StateVec::from_state(custom_state, rand_chacha::ChaCha8Rng::from_entropy()); + /// ``` + /// /// # Panics /// Code will panic if the input state requires more qubits then `StateVec` has. #[inline] @@ -127,10 +155,26 @@ where /// Prepare a specific computational basis state /// + /// # Convention + /// Note: The binary representation of the basis state uses a different ordering than + /// standard quantum notation. For example: + /// - |01⟩ corresponds to binary `0b10` (decimal 2) + /// - |10⟩ corresponds to binary `0b01` (decimal 1) + /// + /// This is because in quantum notation the leftmost qubit is the most significant, + /// while in binary representation the rightmost bit is the most significant. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::StateVec; + /// + /// let mut state_vec = StateVec::new(3); + /// state_vec.prepare_computational_basis(0b110); // Prepares |011⟩ + /// ``` + /// /// # Panics /// Code will panic if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) #[inline] - #[must_use] pub fn prepare_computational_basis(&mut self, basis_state: usize) -> &mut Self { assert!(basis_state < 1 << self.num_qubits); self.state.fill(Complex64::new(0.0, 0.0)); @@ -150,7 +194,6 @@ where /// state.prepare_plus_state(); /// ``` #[inline] - #[must_use] pub fn prepare_plus_state(&mut self) -> &mut Self { let factor = Complex64::new(1.0 / f64::from(1 << self.num_qubits), 0.0); self.state.fill(factor); @@ -162,6 +205,25 @@ where /// The state vector is guaranteed to be normalized such that the sum of /// probability amplitudes squared equals 1. /// + /// # Examples + /// ```rust + /// use pecos_qsim::StateVec; + /// + /// // Initialize a 2-qubit state vector + /// let state_vec = StateVec::new(2); + /// + /// // Access the state vector + /// let state = state_vec.state(); + /// + /// // Verify the state is initialized to |00⟩ + /// assert_eq!(state[0].re, 1.0); + /// assert_eq!(state[0].im, 0.0); + /// for amp in &state[1..] { + /// assert_eq!(amp.re, 0.0); + /// assert_eq!(amp.im, 0.0); + /// } + /// ``` + /// /// # Returns /// A slice containing the complex amplitudes of the quantum state #[inline] @@ -172,6 +234,34 @@ where /// Returns the probability of measuring a specific basis state /// + /// # Convention + /// Note: The binary representation of the basis state uses a different ordering than + /// standard quantum notation. For example: + /// - |01⟩ corresponds to binary `0b10` (decimal 2) + /// - |10⟩ corresponds to binary `0b01` (decimal 1) + /// + /// This is because in quantum notation the leftmost qubit is the most significant, + /// while in binary representation the rightmost bit is the most significant. + /// + /// # Examples + /// ```rust + /// use pecos_qsim::StateVec; + /// + /// // Initialize a 2-qubit state vector + /// let mut state_vec = StateVec::new(2); + /// + /// // Prepare the |01⟩ state (corresponds to binary 10) + /// state_vec.prepare_computational_basis(0b10); + /// + /// // Get the probability of measuring |01⟩ + /// let prob = state_vec.probability(0b10); // Use binary 10 for |01⟩ + /// assert!((prob - 1.0).abs() < 1e-10); + /// + /// // Probability of measuring |00⟩ should be 0 + /// let prob_zero = state_vec.probability(0); + /// assert!(prob_zero.abs() < 1e-10); + /// ``` + /// /// # Panics /// Code will panic if `basis_state` >= `2^num_qubits` (i.e., if the basis state index is too large for the number of qubits) #[inline] @@ -233,6 +323,58 @@ where /// [u20, u21, u22, u23], /// [u30, u31, u32, u33]] /// + /// # Examples + /// ```rust + /// use num_complex::Complex64; + /// use pecos_qsim::StateVec; + /// + /// let mut state_vec = StateVec::new(2); + /// + /// let cnot_gate = [ + /// [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0)], + /// [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0)], + /// [Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + /// [Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], + /// ]; + /// + /// state_vec.prepare_computational_basis(2); // |01⟩ + /// println!("|01⟩: {:?}", state_vec.state()); + /// state_vec.two_qubit_unitary(1, 0, cnot_gate); // Control: qubit 1, Target: qubit 0 + /// + /// println!("|11⟩: {:?}", state_vec.state()); + /// + /// let prob = state_vec.probability(3); // Expect |11⟩ + /// println!("prob: {:?}", prob); + /// assert!((prob - 1.0).abs() < 1e-10); + /// ``` + /// + /// ```rust + /// use num_complex::Complex64; + /// use pecos_qsim::StateVec; + /// + /// // Initialize a 2-qubit state vector + /// let mut state_vec = StateVec::new(2); + /// + /// // Define a SWAP gate as a 4x4 unitary matrix + /// let swap_gate = [ + /// [Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0)], + /// [Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)], + /// [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0)], + /// [Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)], + /// ]; + /// + /// // Prepare the |01⟩ state + /// state_vec.prepare_computational_basis(2); + /// + /// // Apply the SWAP gate to qubits 0 and 1 + /// state_vec.two_qubit_unitary(0, 1, swap_gate); + /// println!("{:?}", state_vec.state()); + /// + /// // Verify the state is now |10⟩ + /// let prob = state_vec.probability(1); + /// assert!((prob - 1.0).abs() < 1e-10); + /// ``` + /// /// # Safety /// This function assumes that: /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). @@ -241,60 +383,58 @@ where #[inline] pub fn two_qubit_unitary( &mut self, - qubit1: usize, - qubit2: usize, + control: usize, + target: usize, matrix: [[Complex64; 4]; 4], ) -> &mut Self { - // Make sure qubit1 < qubit2 for consistent ordering - let (q1, q2) = if qubit1 < qubit2 { - (qubit1, qubit2) - } else { - (qubit2, qubit1) - }; + let n = self.num_qubits; + let size = 1 << n; - // Process state vector in groups of 4 amplitudes - for i in 0..self.state.len() { - let bit1 = (i >> q1) & 1; - let bit2 = (i >> q2) & 1; + // Use a temporary buffer to avoid overwriting data during updates + let mut new_state = vec![Complex64::new(0.0, 0.0); size]; - // Only process each set of 4 states once - if bit1 == 0 && bit2 == 0 { - // Calculate indices for all four basis states - let i00 = i; - let i01 = i ^ (1 << q2); - let i10 = i ^ (1 << q1); - let i11 = i ^ (1 << q1) ^ (1 << q2); + for i in 0..size { + // Extract control and target bits + let control_bit = (i >> control) & 1; + let target_bit = (i >> target) & 1; - // Store original amplitudes - let a00 = self.state[i00]; - let a01 = self.state[i01]; - let a10 = self.state[i10]; - let a11 = self.state[i11]; + // Map (control_bit, target_bit) to basis index (00, 01, 10, 11) + let basis_idx = (control_bit << 1) | target_bit; - // Apply the 4x4 unitary transformation - self.state[i00] = matrix[0][0] * a00 - + matrix[0][1] * a01 - + matrix[0][2] * a10 - + matrix[0][3] * a11; - self.state[i01] = matrix[1][0] * a00 - + matrix[1][1] * a01 - + matrix[1][2] * a10 - + matrix[1][3] * a11; - self.state[i10] = matrix[2][0] * a00 - + matrix[2][1] * a01 - + matrix[2][2] * a10 - + matrix[2][3] * a11; - self.state[i11] = matrix[3][0] * a00 - + matrix[3][1] * a01 - + matrix[3][2] * a10 - + matrix[3][3] * a11; + for j in 0..4 { + // Calculate the index after flipping control and target qubits + let flipped_i = (i & !(1 << control) & !(1 << target)) + | (((j >> 1) & 1) << control) + | ((j & 1) << target); + + // Apply the matrix to the relevant amplitudes + new_state[flipped_i] += matrix[j][basis_idx] * self.state[i]; } } + + self.state = new_state; self } } impl QuantumSimulator for StateVec { + /// # Examples + /// ```rust + /// use pecos_qsim::{QuantumSimulator, StateVec}; + /// + /// // Initialize a 2-qubit state vector + /// let mut state_vec = StateVec::new(2); + /// + /// // Prepare a different state + /// state_vec.prepare_computational_basis(3); // |11⟩ + /// + /// // Reset the state back to |00⟩ + /// state_vec.reset(); + /// + /// // Verify the state is |00⟩ + /// let prob_zero = state_vec.probability(0); + /// assert!((prob_zero - 1.0).abs() < 1e-10); + /// ``` #[inline] fn reset(&mut self) -> &mut Self { self.prepare_computational_basis(0) @@ -481,6 +621,19 @@ impl CliffordGateable for StateVec { /// state.h(0).cx(0, 1).cx(1, 2); /// ``` /// + /// ```rust + /// use num_complex::Complex64; + /// use pecos_qsim::{StateVec, CliffordGateable}; + /// + /// let mut state_vec = StateVec::new(2); + /// + /// state_vec.prepare_computational_basis(2); // |01⟩ + /// state_vec.cx(1, 0); // Control: qubit 1, Target: qubit 0 + /// + /// let prob = state_vec.probability(3); // Expect |11⟩ + /// assert!((prob - 1.0).abs() < 1e-10); + /// ``` + /// /// # Safety /// This function assumes that: /// - `qubit1` and `qubit2` are valid qubit indices (i.e., `< number of qubits`). @@ -1607,6 +1760,43 @@ mod tests { assert!((q.state[2].re - 1.0).abs() < 1e-10); } + #[test] + fn test_two_qubit_unitary_swap_simple() { + let mut state_vec = StateVec::new(2); + + let swap_gate = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + ]; + + state_vec.prepare_computational_basis(2); // |10⟩ + state_vec.two_qubit_unitary(1, 0, swap_gate); + + assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); // Should now be |01⟩ + } + #[test] fn test_cx() { let mut q = StateVec::new(2); @@ -1622,6 +1812,31 @@ mod tests { assert!(q.state[2].norm() < 1e-10); } + #[test] + fn test_cx_all_basis_states() { + let mut state_vec = StateVec::new(2); + + // |00⟩ → should remain |00⟩ + state_vec.prepare_computational_basis(0); + state_vec.cx(1, 0); + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + + // |01⟩ → should remain |01⟩ + state_vec.prepare_computational_basis(1); + state_vec.cx(1, 0); + assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); + + // |10⟩ → should flip to |11⟩ + state_vec.prepare_computational_basis(2); + state_vec.cx(1, 0); + assert!((state_vec.probability(3) - 1.0).abs() < 1e-10); + + // |11⟩ → should flip to |10⟩ + state_vec.prepare_computational_basis(3); + state_vec.cx(1, 0); + assert!((state_vec.probability(2) - 1.0).abs() < 1e-10); + } + #[test] fn test_cy() { let mut q = StateVec::new(2); @@ -1659,6 +1874,43 @@ mod tests { assert!((q.state[3].re + expected).abs() < 1e-10); // |11⟩ amplitude } + #[test] + fn test_state_normalization() { + let mut state_vec = StateVec::new(3); + + // Apply multiple gates + state_vec.h(0); + state_vec.cx(0, 1); + state_vec.cx(1, 2); + + // Verify normalization + let norm: f64 = state_vec.state.iter().map(|amp| amp.norm_sqr()).sum(); + assert!((norm - 1.0).abs() < 1e-10); + } + + #[test] + fn test_non_commuting_gates() { + let mut state1 = StateVec::new(1); + let mut state2 = StateVec::new(1); + + state1.h(0); + state1.z(0); + + state2.z(0); + state2.h(0); + + // Compute the global norm difference + let diff_norm: f64 = state1 + .state + .iter() + .zip(state2.state.iter()) + .map(|(a, b)| (a - b).norm_sqr()) + .sum::() + .sqrt(); + + assert!(diff_norm > 1e-10, "H and Z should not commute."); + } + #[test] fn test_control_target_independence() { // Test that CY and CZ work regardless of which qubit is control/target @@ -2257,7 +2509,7 @@ mod tests { } #[test] - fn test_reset() { + fn test_pz() { let mut q = StateVec::new(1); q.h(0); @@ -2271,7 +2523,7 @@ mod tests { } #[test] - fn test_reset_multiple_qubits() { + fn test_pz_multiple_qubits() { let mut q = StateVec::new(2); q.h(0); @@ -2796,4 +3048,513 @@ mod tests { assert_eq!(result1.outcome, result2.outcome); } + + #[test] + fn test_prepare_computational_basis_all_states() { + let num_qubits = 3; + let mut state_vec = StateVec::new(num_qubits); + + for basis_state in 0..(1 << num_qubits) { + state_vec.prepare_computational_basis(basis_state); + for i in 0..state_vec.state.len() { + if i == basis_state { + assert!((state_vec.state[i].norm() - 1.0).abs() < 1e-10); + } else { + assert!(state_vec.state[i].norm() < 1e-10); + } + } + } + } + + #[test] + fn test_reset() { + let mut state_vec = StateVec::new(2); + + state_vec.h(0).cx(0, 1); // Create Bell state + state_vec.reset(); // Reset to |00⟩ + + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + for i in 1..state_vec.state.len() { + assert!(state_vec.state[i].norm() < 1e-10); + } + } + + #[test] + fn test_probability() { + let mut state_vec = StateVec::new(1); + + // Prepare |+⟩ state + state_vec.h(0); + + let prob_zero = state_vec.probability(0); + let prob_one = state_vec.probability(1); + + assert!((prob_zero - 0.5).abs() < 1e-10); + assert!((prob_one - 0.5).abs() < 1e-10); + } + + #[test] + fn test_inverse_gates() { + let mut state_vec = StateVec::new(1); + + // Apply Hadamard twice: H * H = I + state_vec.h(0); + state_vec.h(0); + + // Verify state is back to |0⟩ + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + assert!((state_vec.probability(1)).abs() < 1e-10); + + // Apply X twice: X * X = I + state_vec.x(0); + state_vec.x(0); + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + } + + #[test] + fn test_bell_state_entanglement() { + let mut state_vec = StateVec::new(2); + + // Prepare Bell State: (|00⟩ + |11⟩) / √2 + state_vec.h(0); + state_vec.cx(0, 1); + + let expected_amplitude = 1.0 / 2.0_f64.sqrt(); + + assert!((state_vec.state[0].re - expected_amplitude).abs() < 1e-10); + assert!((state_vec.state[3].re - expected_amplitude).abs() < 1e-10); + + assert!(state_vec.state[1].norm() < 1e-10); + assert!(state_vec.state[2].norm() < 1e-10); + } + + #[test] + fn test_measurement_collapse() { + let mut state_vec = StateVec::new(1); + + // Prepare |+⟩ = (|0⟩ + |1⟩) / √2 + state_vec.h(0); + + // Simulate a measurement + let result = state_vec.mz(0); + + // State should collapse to |0⟩ or |1⟩ + if result.outcome { + assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); + } else { + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + } + } + + #[test] + fn test_state_normalization_after_random_gates() { + let mut state_vec = StateVec::new(3); + + // Apply a sequence of random gates + state_vec.h(0); + state_vec.cx(0, 1); + state_vec.rz(std::f64::consts::PI / 3.0, 2); + state_vec.swap(1, 2); + + // Check if the state is still normalized + let norm: f64 = state_vec.state.iter().map(|amp| amp.norm_sqr()).sum(); + assert!((norm - 1.0).abs() < 1e-10); + } + + #[test] + fn test_two_qubit_unitary_identity() { + let mut state_vec = StateVec::new(2); + + // Identity matrix + let identity_gate = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + ]; + + // Apply the identity gate + state_vec.prepare_computational_basis(2); + state_vec.two_qubit_unitary(0, 1, identity_gate); + + // State should remain |10⟩ + assert!((state_vec.probability(2) - 1.0).abs() < 1e-10); + } + + #[test] + fn test_gate_decompositions() { + // Test that composite operations match their decompositions + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Test SWAP decomposition into CNOTs + q1.x(0); // Start with |10⟩ + q1.swap(0, 1); // Direct SWAP + + q2.x(0); // Also start with |10⟩ + q2.cx(0, 1).cx(1, 0).cx(0, 1); // SWAP decomposition + + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_phase_relationships() { + // Test expected phase relationships between gates + let q = StateVec::new(1); + + // Test that T * T = S + let mut q1 = q.clone(); + q1.t(0).t(0); + + let mut q2 = q.clone(); + q2.sz(0); + + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_phase_gate_identities() { + // Test S = T^2 + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + // Put in superposition first to check phases + q1.h(0); + q2.h(0); + + q1.sz(0); // S gate + q2.t(0).t(0); // Two T gates + + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_hadamard_properties() { + // Test H^2 = I + let mut q = StateVec::new(1); + q.x(0); // Start with |1⟩ + let initial = q.state.clone(); + q.h(0).h(0); + assert_states_equal(&q.state, &initial); + + // Test HXH = Z + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + q1.h(0).x(0).h(0); + q2.z(0); + + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_controlled_gate_symmetries() { + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Test SWAP symmetry + q1.x(0); // |10⟩ + q2.x(0); // |10⟩ + + q1.cx(0, 1).cx(1, 0).cx(0, 1); // SWAP via CNOTs + q2.swap(0, 1); // Direct SWAP + + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_controlled_gate_phases() { + // Test phase behavior of controlled operations + let mut q = StateVec::new(2); + + // Create superposition with phases + q.h(0).sz(0); + q.h(1).sz(1); + + // Control operations should preserve phases correctly + let initial = q.state.clone(); + q.cz(0, 1).cz(0, 1); // CZ^2 = I + + assert_states_equal(&q.state, &initial); + } + + #[test] + fn test_rotation_composition() { + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + // Test that rotation decompositions work + // RY(θ) = RX(π/2)RZ(θ)RX(-π/2) + q1.ry(FRAC_PI_3, 0); + + q2.rx(FRAC_PI_2, 0).rz(FRAC_PI_3, 0).rx(-FRAC_PI_2, 0); + + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_rotation_angle_precision() { + let mut q = StateVec::new(1); + + // Test small angle rotations + let small_angle = 1e-6; + q.rx(small_angle, 0); + + // Check that probabilities sum to 1 + let total_prob: f64 = q.state.iter().map(|amp| amp.norm_sqr()).sum(); + assert!((total_prob - 1.0).abs() < 1e-10); + } + + #[test] + fn test_measurement_properties() { + let mut q = StateVec::new(2); + + // Test 1: Measuring |0⟩ should always give 0 + let result = q.mz(0); + assert!(!result.outcome); + assert!((q.probability(0) - 1.0).abs() < 1e-10); + + // Test 2: Measuring |1⟩ should always give 1 + q.reset(); + q.x(0); + let result = q.mz(0); + assert!(result.outcome); + assert!((q.probability(1) - 1.0).abs() < 1e-10); + + // Test 3: In a Bell state, measurements should correlate + q.reset(); + q.h(0).cx(0, 1); // Create Bell state + let result1 = q.mz(0); + let result2 = q.mz(1); + assert_eq!( + result1.outcome, result2.outcome, + "Bell state measurements should correlate" + ); + + // Test 4: Repeated measurements should be consistent + q.reset(); + q.h(0); // Create superposition + let first = q.mz(0); + let second = q.mz(0); // Measure again + assert_eq!( + first.outcome, second.outcome, + "Repeated measurements should give same result" + ); + } + + #[test] + fn test_measurement_basis_transforms() { + let mut q = StateVec::new(1); + + // |0⟩ in X basis + q.h(0); + + // Measure in Z basis + let result = q.mz(0); + + // Result should be random but state should collapse + let final_prob = if result.outcome { + q.probability(1) + } else { + q.probability(0) + }; + assert!((final_prob - 1.0).abs() < 1e-10); + } + + #[test] + fn test_state_preparation_fidelity() { + let mut q = StateVec::new(2); + + // Method 1: H + CNOT + q.h(0).cx(0, 1); + let probs1 = vec![ + q.probability(0), + q.probability(1), + q.probability(2), + q.probability(3), + ]; + + // Method 2: Rotations + q.reset(); + q.ry(FRAC_PI_2, 0).cx(0, 1); // Remove rz(PI) since it just adds phase + + // Compare probability distributions + assert!((q.probability(0) - probs1[0]).abs() < 1e-10); + assert!((q.probability(1) - probs1[1]).abs() < 1e-10); + assert!((q.probability(2) - probs1[2]).abs() < 1e-10); + assert!((q.probability(3) - probs1[3]).abs() < 1e-10); + } + + #[test] + fn test_arbitrary_state_preparation() { + let mut q = StateVec::new(1); + + // Try to prepare various single-qubit states + // |+⟩ state + q.h(0); + assert!((q.probability(0) - 0.5).abs() < 1e-10); + assert!((q.probability(1) - 0.5).abs() < 1e-10); + + // |+i⟩ state + q.reset(); + q.h(0).sz(0); + assert!((q.probability(0) - 0.5).abs() < 1e-10); + assert!((q.probability(1) - 0.5).abs() < 1e-10); + } + + #[test] + fn test_numerical_stability() { + let mut q = StateVec::new(4); + + // Apply many rotations to test numerical stability + for _ in 0..100 { + q.rx(FRAC_PI_3, 0) + .ry(FRAC_PI_4, 1) + .rz(FRAC_PI_6, 2) + .cx(0, 3); + } + + // Check normalization is preserved + let total_prob: f64 = q.state.iter().map(|amp| amp.norm_sqr()).sum(); + assert!((total_prob - 1.0).abs() < 1e-8); + } + + #[test] + fn test_ghz_state() { + // Test creating and verifying a GHZ state + let mut q = StateVec::new(3); + q.h(0).cx(0, 1).cx(1, 2); // Create GHZ state + + // Verify properties + let mut norm_squared = 0.0; + for i in 0..8 { + if i == 0 || i == 7 { + // |000⟩ or |111⟩ + norm_squared += q.state[i].norm_sqr(); + assert!((q.state[i].norm() - FRAC_1_SQRT_2).abs() < 1e-10); + } else { + assert!(q.state[i].norm() < 1e-10); + } + } + assert!((norm_squared - 1.0).abs() < 1e-10); + } + + #[test] + fn test_operation_chains() { + // Test complex sequences of operations + let mut q = StateVec::new(2); + + // Create maximally entangled state then disentangle + q.h(0) + .cx(0, 1) // Create Bell state + .cx(0, 1) + .h(0); // Disentangle (apply the same operations in reverse) + + // Should be back to |00⟩ + assert!((q.probability(0) - 1.0).abs() < 1e-10); + } + + #[test] + fn test_phase_coherence() { + let mut q = StateVec::new(1); + + // Apply series of phase rotations that should cancel + q.h(0) // Create superposition + .rz(FRAC_PI_4, 0) + .rz(FRAC_PI_4, 0) + .rz(-FRAC_PI_2, 0); // Should cancel + + // Should be back to |+⟩ + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!(q.state[0].im.abs() < 1e-10); + assert!(q.state[1].im.abs() < 1e-10); + } + + #[test] + fn test_adjacent_vs_distant_qubits() { + let mut q1 = StateVec::new(4); + let mut q2 = StateVec::new(4); + + // Test operations on adjacent vs distant qubits + q1.h(0).cx(0, 1); // Adjacent qubits + q2.h(0).cx(0, 3); // Distant qubits + + // Both should maintain proper normalization + let norm1: f64 = q1.state.iter().map(|amp| amp.norm_sqr()).sum(); + let norm2: f64 = q2.state.iter().map(|amp| amp.norm_sqr()).sum(); + assert!((norm1 - 1.0).abs() < 1e-10); + assert!((norm2 - 1.0).abs() < 1e-10); + } + + #[test] + fn test_state_prep_consistency() { + // First method: direct X gate + let mut q1 = StateVec::new(2); + q1.x(1); // Direct preparation of |01⟩ + + // Verify first preparation - |01⟩ corresponds to binary 10 (decimal 2) + assert!( + (q1.probability(2) - 1.0).abs() < 1e-10, + "First preparation failed" + ); + assert!(q1.probability(0) < 1e-10); + assert!(q1.probability(1) < 1e-10); + assert!(q1.probability(3) < 1e-10); + + // Second method: using two X gates that cancel on qubit 0 + let mut q2 = StateVec::new(2); + q2.x(0).x(1).x(0); // Should give |01⟩ + + // Verify second preparation - |01⟩ corresponds to binary 10 (decimal 2) + assert!( + (q2.probability(2) - 1.0).abs() < 1e-10, + "Second preparation failed" + ); + assert!(q2.probability(0) < 1e-10); + assert!(q2.probability(1) < 1e-10); + assert!(q2.probability(3) < 1e-10); + + // Verify both methods give the same state + assert_states_equal(&q1.state, &q2.state); + } + + #[test] + fn test_rotation_arithmetic() { + let q = StateVec::new(1); + + // Test that RY(θ₁)RY(θ₂) = RY(θ₁ + θ₂) when commuting + let theta1 = FRAC_PI_3; + let theta2 = FRAC_PI_6; + + // Method 1: Two separate rotations + let mut q1 = q.clone(); + q1.ry(theta1, 0).ry(theta2, 0); + + // Method 2: Combined rotation + let mut q2 = q.clone(); + q2.ry(theta1 + theta2, 0); + + assert_states_equal(&q1.state, &q2.state); + } } From cb2055e159c273369d9986c554635f1892d2431c Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Mon, 13 Jan 2025 00:38:28 -0700 Subject: [PATCH 31/33] Reorganizing tests --- crates/pecos-qsim/src/state_vec.rs | 2581 +++++---------------- crates/pecos-qsim/tests/helpers.rs | 46 + crates/pecos-qsim/tests/test_state_vec.rs | 1622 +++++++++++++ 3 files changed, 2204 insertions(+), 2045 deletions(-) create mode 100644 crates/pecos-qsim/tests/helpers.rs create mode 100644 crates/pecos-qsim/tests/test_state_vec.rs diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index a7d35aea..68482251 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -21,12 +21,12 @@ use rand::Rng; /// A quantum state simulator using the state vector representation /// -/// StateVec maintains the full quantum state as a complex vector with 2ⁿ amplitudes +/// `StateVec` maintains the full quantum state as a complex vector with 2ⁿ amplitudes /// for n qubits. This allows exact simulation of quantum operations but requires /// memory that scales exponentially with the number of qubits. /// /// # Type Parameters -/// * `R` - Random number generator type implementing SimRng trait +/// * `R` - Random number generator type implementing `SimRng` trait /// /// # Examples /// ```rust @@ -97,7 +97,7 @@ where /// /// # Arguments /// * `num_qubits` - Number of qubits in the system - /// * `rng` - Random number generator implementing SimRng trait + /// * `rng` - Random number generator implementing `SimRng` trait /// /// # Examples /// ```rust @@ -401,14 +401,14 @@ where // Map (control_bit, target_bit) to basis index (00, 01, 10, 11) let basis_idx = (control_bit << 1) | target_bit; - for j in 0..4 { + for (j, &row) in matrix.iter().enumerate() { // Calculate the index after flipping control and target qubits let flipped_i = (i & !(1 << control) & !(1 << target)) | (((j >> 1) & 1) << control) | ((j & 1) << target); // Apply the matrix to the relevant amplitudes - new_state[flipped_i] += matrix[j][basis_idx] * self.state[i]; + new_state[flipped_i] += row[basis_idx] * self.state[i]; } } @@ -1237,6 +1237,8 @@ mod tests { } } + // Core functionality tests + // ======================== #[test] fn test_new_state() { // Verify the initial state is correctly set to |0> @@ -1248,766 +1250,599 @@ mod tests { } #[test] - fn test_rx() { - // Test RX gate functionality - let mut q = StateVec::new(1); + fn test_reset() { + let mut state_vec = StateVec::new(2); - // RX(π) should flip |0⟩ to -i|1⟩ - q.rx(PI, 0); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].norm() - 1.0).abs() < 1e-10); + state_vec.h(0).cx(0, 1); // Create Bell state + state_vec.reset(); // Reset to |00⟩ - // RX(2π) should return to the initial state up to global phase - let mut q = StateVec::new(1); - q.rx(2.0 * PI, 0); - assert!((q.state[0].norm() - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + for i in 1..state_vec.state.len() { + assert!(state_vec.state[i].norm() < 1e-10); + } } #[test] - fn test_ry() { - let mut q = StateVec::new(1); + fn test_probability() { + let mut state_vec = StateVec::new(1); - // RY(π) should flip |0⟩ to |1⟩ - q.ry(PI, 0); - assert!(q.state[0].norm() < 1e-10); // Close to zero - assert!((q.state[1].norm() - 1.0).abs() < 1e-10); // Magnitude 1 for |1⟩ + // Prepare |+⟩ state + state_vec.h(0); - // Two RY(π) rotations should return to the initial state - q.ry(PI, 0); - assert!((q.state[0].norm() - 1.0).abs() < 1e-10); // Magnitude 1 for |0⟩ - assert!(q.state[1].norm() < 1e-10); // Close to zero + let prob_zero = state_vec.probability(0); + let prob_one = state_vec.probability(1); + + assert!((prob_zero - 0.5).abs() < 1e-10); + assert!((prob_one - 0.5).abs() < 1e-10); } #[test] - fn test_rz() { - let mut q = StateVec::new(1); - - // RZ should only add phases, not change probabilities - q.h(0); // Put qubit in superposition - let probs_before: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); - - q.rz(FRAC_PI_2, 0); // Rotate Z by π/2 - let probs_after: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); + fn test_prepare_computational_basis_all_states() { + let num_qubits = 3; + let mut state_vec = StateVec::new(num_qubits); - for (p1, p2) in probs_before.iter().zip(probs_after.iter()) { - assert!((p1 - p2).abs() < 1e-10); // Probabilities unchanged + for basis_state in 0..(1 << num_qubits) { + state_vec.prepare_computational_basis(basis_state); + for i in 0..state_vec.state.len() { + if i == basis_state { + assert!((state_vec.state[i].norm() - 1.0).abs() < 1e-10); + } else { + assert!(state_vec.state[i].norm() < 1e-10); + } + } } } + // Single qubit gate fundamentals + // ============================== #[test] - fn test_rx_step_by_step() { + fn test_x() { let mut q = StateVec::new(1); - // Step 1: RX(0) should be identity - q.rx(0.0, 0); + // Check initial state is |0> assert!((q.state[0].re - 1.0).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); - // Step 2: RX(π) on |0⟩ should give -i|1⟩ - let mut q = StateVec::new(1); - q.rx(PI, 0); - println!("RX(π)|0⟩ = {:?}", q.state); // Debug output + // Test X on |0> -> |1> + q.x(0); assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].im + 1.0).abs() < 1e-10); + assert!((q.state[1].re - 1.0).abs() < 1e-10); - // Step 3: RX(π/2) on |0⟩ should give (|0⟩ - i|1⟩)/√2 - let mut q = StateVec::new(1); - q.rx(FRAC_PI_2, 0); - println!("RX(π/2)|0⟩ = {:?}", q.state); // Debug output - let expected_amp = 1.0 / 2.0_f64.sqrt(); - assert!((q.state[0].re - expected_amp).abs() < 1e-10); - assert!((q.state[1].im + expected_amp).abs() < 1e-10); + // Test X on |1> -> |0> + q.x(0); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + + // Test X on superposition + q.h(0); + let initial_state = q.state.clone(); + q.x(0); // X|+> = |+> + for (state, initial) in q.state.iter().zip(initial_state.iter()) { + assert!((state - initial).norm() < 1e-10); + } + + // Test X on second qubit of two-qubit system + let mut q = StateVec::new(2); + q.x(1); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[2].re - 1.0).abs() < 1e-10); } #[test] - fn test_ry_step_by_step() { - // Step 1: RY(0) should be identity + fn test_y() { let mut q = StateVec::new(1); - q.ry(0.0, 0); - println!("RY(0)|0⟩ = {:?}", q.state); - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - // Step 2: RY(π) on |0⟩ should give |1⟩ - let mut q = StateVec::new(1); - q.ry(PI, 0); - println!("RY(π)|0⟩ = {:?}", q.state); + // Test Y on |0⟩ -> i|1⟩ + q.y(0); assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].re - 1.0).abs() < 1e-10); + assert!((q.state[1] - Complex64::i()).norm() < 1e-10); - // Step 3: RY(π/2) on |0⟩ should give (|0⟩ + |1⟩)/√2 - let mut q = StateVec::new(1); - q.ry(FRAC_PI_2, 0); - println!("RY(π/2)|0⟩ = {:?}", q.state); - let expected_amp = 1.0 / 2.0_f64.sqrt(); - assert!((q.state[0].re - expected_amp).abs() < 1e-10); - assert!((q.state[1].re - expected_amp).abs() < 1e-10); + // Test Y on i|1⟩ -> |0⟩ + q.y(0); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); - // Step 4: RY(-π/2) on |0⟩ should give (|0⟩ - |1⟩)/√2 + // Test Y on |+⟩ let mut q = StateVec::new(1); - q.ry(-FRAC_PI_2, 0); - println!("RY(-π/2)|0⟩ = {:?}", q.state); - assert!((q.state[0].re - expected_amp).abs() < 1e-10); - assert!((q.state[1].re + expected_amp).abs() < 1e-10); + q.h(0); // Create |+⟩ + q.y(0); // Should give i|-⟩ + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].im + expected).abs() < 1e-10); + assert!((q.state[1].im - expected).abs() < 1e-10); } #[test] - fn test_rz_step_by_step() { - // Step 1: RZ(0) should be identity + fn test_z() { let mut q = StateVec::new(1); - q.rz(0.0, 0); - println!("RZ(0)|0⟩ = {:?}", q.state); + + // Test Z on |0⟩ -> |0⟩ + q.z(0); assert!((q.state[0].re - 1.0).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); - // Step 2: RZ(π/2) on |+⟩ should give |+i⟩ = (|0⟩ + i|1⟩)/√2 + // Test Z on |1⟩ -> -|1⟩ + q.x(0); // Prepare |1⟩ + q.z(0); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re + 1.0).abs() < 1e-10); + + // Test Z on |+⟩ -> |-⟩ let mut q = StateVec::new(1); q.h(0); // Create |+⟩ - q.rz(FRAC_PI_2, 0); - println!("RZ(π/2)|+⟩ = {:?}", q.state); - let expected_amp = 1.0 / 2.0_f64.sqrt(); - assert!((q.state[0].norm() - expected_amp).abs() < 1e-10); - assert!((q.state[1].norm() - expected_amp).abs() < 1e-10); - // Check relative phase - let ratio = q.state[1] / q.state[0]; - println!("Relative phase ratio = {ratio:?}"); - assert!( - (ratio.im - 1.0).abs() < 1e-10, - "Relative phase incorrect: ratio = {ratio}" - ); - assert!( - ratio.re.abs() < 1e-10, - "Relative phase has unexpected real component: {}", - ratio.re - ); - - // Step 3: Two RZ(π/2) operations should equal one RZ(π) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.rz(PI, 0); - q2.rz(FRAC_PI_2, 0); - q2.rz(FRAC_PI_2, 0); - println!("RZ(π)|0⟩ vs RZ(π/2)RZ(π/2)|0⟩:"); - println!("q1 = {:?}", q1.state); - println!("q2 = {:?}", q2.state); - let ratio = q2.state[0] / q1.state[0]; - let phase = ratio.arg(); - println!("Phase difference between q2 and q1: {phase}"); - assert!( - (ratio.norm() - 1.0).abs() < 1e-10, - "Magnitudes differ: ratio = {ratio}" - ); - // Don't check exact phase, just verify states are equal up to global phase - assert!((q2.state[1] * q1.state[0] - q2.state[0] * q1.state[1]).norm() < 1e-10); + q.z(0); // Should give |-⟩ + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!((q.state[1].re + expected).abs() < 1e-10); } #[test] - fn test_sq_rotation_commutation() { - // RX and RY don't commute - verify RX(θ)RY(φ) ≠ RY(φ)RX(θ) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); + fn test_h() { + let mut q = StateVec::new(1); + q.h(0); - let theta = FRAC_PI_3; // π/3 - let phi = FRAC_PI_4; // π/4 + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + } - // Apply in different orders - q1.rx(theta, 0).ry(phi, 0); - q2.ry(phi, 0).rx(theta, 0); + // TODO: add... + #[test] + fn test_sz() {} - println!("RY(π/4)RX(π/3)|0⟩ = {:?}", q1.state); - println!("RX(π/3)RY(π/4)|0⟩ = {:?}", q2.state); + // Two qubit gate fundamentals + // =========================== + #[test] + fn test_cx() { + let mut q = StateVec::new(2); + // Prep |+> + q.h(0); + q.cx(0, 1); - // States should be different - check they're not equal up to global phase - let ratio = q2.state[0] / q1.state[0]; - assert!((q2.state[1] / q1.state[1] - ratio).norm() > 1e-10); + // Should be in Bell state (|00> + |11>)/sqrt(2) + let expected = 1.0 / 2.0_f64.sqrt(); + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!((q.state[3].re - expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); } #[test] - fn test_sq_rotation_decompositions() { - // H = RZ(-π)RY(-π/2) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - - println!("Initial states:"); - println!("q1 = {:?}", q1.state); - println!("q2 = {:?}", q2.state); + fn test_cy() { + let mut q = StateVec::new(2); - q1.h(0); // Direct H - println!("After H: q1 = {:?}", q1.state); + // Create |+0⟩ state + q.h(0); - // H via rotations - changed order and added negative sign to RZ angle - q2.ry(-FRAC_PI_2, 0).rz(-PI, 0); - println!("After RZ(-π)RY(-π/2): q2 = {:?}", q2.state); + // Apply CY to get entangled state + q.cy(0, 1); - // Compare up to global phase by looking at ratios between components - let ratio = q2.state[0] / q1.state[0]; - println!("Ratio = {ratio:?}"); - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - println!("Comparing {a} and {b}"); - assert!( - (a * ratio - b).norm() < 1e-10, - "States differ: {a} vs {b} (ratio: {ratio})" - ); - } + // Should be (|00⟩ + i|11⟩)/√2 + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].re - expected).abs() < 1e-10); // |00⟩ amplitude + assert!(q.state[1].norm() < 1e-10); // |01⟩ amplitude + assert!(q.state[2].norm() < 1e-10); // |10⟩ amplitude + assert!((q.state[3].im - expected).abs() < 1e-10); // |11⟩ amplitude } #[test] - fn test_sq_standard_gate_decompositions() { - // Test S = RZ(π/2) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.sz(0); - q2.rz(FRAC_PI_2, 0); - println!("S|0⟩ = {:?}", q1.state); - println!("RZ(π/2)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); - - // Test X = RX(π) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.x(0); - q2.rx(PI, 0); - println!("X|0⟩ = {:?}", q1.state); - println!("RX(π)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); - - // Test Y = RY(π) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.y(0); - q2.ry(PI, 0); - println!("Y|0⟩ = {:?}", q1.state); - println!("RY(π)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); + fn test_cz() { + let mut q = StateVec::new(2); - // Test Z = RZ(π) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.z(0); - q2.rz(PI, 0); - println!("Z|0⟩ = {:?}", q1.state); - println!("RZ(π)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); + // Create |++⟩ state + q.h(0); + q.h(1); - // Test √X = RX(π/2) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.sx(0); - q2.rx(FRAC_PI_2, 0); - println!("√X|0⟩ = {:?}", q1.state); - println!("RX(π/2)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); + // Apply CZ + q.cz(0, 1); - // Test √Y = RY(π/2) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.sy(0); - q2.ry(FRAC_PI_2, 0); - println!("√Y|0⟩ = {:?}", q1.state); - println!("RY(π/2)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); + // Should be (|00⟩ + |01⟩ + |10⟩ - |11⟩)/2 + let expected = 0.5; + assert!((q.state[0].re - expected).abs() < 1e-10); // |00⟩ amplitude + assert!((q.state[1].re - expected).abs() < 1e-10); // |01⟩ amplitude + assert!((q.state[2].re - expected).abs() < 1e-10); // |10⟩ amplitude + assert!((q.state[3].re + expected).abs() < 1e-10); // |11⟩ amplitude + } - // Test S = TT as RZ(π/4)RZ(π/4) - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q2.rz(FRAC_PI_4, 0).rz(FRAC_PI_4, 0); - q1.sz(0); - println!("S|0⟩ = {:?}", q1.state); - println!("T²|0⟩ = RZ(π/4)RZ(π/4)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); + #[test] + fn test_swap() { + let mut q = StateVec::new(2); + q.x(0); + q.swap(0, 1); - // Test H = RX(π)RY(π/2) decomposition - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - q1.h(0); - q2.ry(FRAC_PI_2, 0).rx(PI, 0); - println!("H|0⟩ = {:?}", q1.state); - println!("RX(π)RY(π/2)|0⟩ = {:?}", q2.state); - assert_states_equal(&q1.state, &q2.state); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[2].re - 1.0).abs() < 1e-10); } + // Basic measurement tests + // ======================= + #[test] - fn test_rx_rotation_angle_relations() { - // Test that RX(θ)RX(-θ) = I + fn test_mz() { + // Test 1: Measuring |0> state let mut q = StateVec::new(1); - let theta = FRAC_PI_3; - - // Apply forward then reverse rotations - q.rx(theta, 0).rx(-theta, 0); - - // Should get back to |0⟩ up to global phase + let result = q.mz(0); + assert!(!result.outcome); + assert!((q.state[0].re - 1.0).abs() < 1e-10); assert!(q.state[1].norm() < 1e-10); - assert!((q.state[0].norm() - 1.0).abs() < 1e-10); - } - #[test] - fn test_ry_rotation_angle_relations() { - // Test that RY(θ)RY(-θ) = I + // Test 2: Measuring |1> state let mut q = StateVec::new(1); - let theta = FRAC_PI_3; + q.x(0); + let result = q.mz(0); + assert!(result.outcome); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - 1.0).abs() < 1e-10); - // Apply forward then reverse rotations - q.ry(theta, 0).ry(-theta, 0); + // Test 3: Measuring superposition state multiple times + let mut zeros = 0; + let trials = 1000; - // Should get back to |0⟩ up to global phase - assert!(q.state[1].norm() < 1e-10); - assert!((q.state[0].norm() - 1.0).abs() < 1e-10); - } + for _ in 0..trials { + let mut q = StateVec::new(1); + q.h(0); + let result = q.mz(0); + if !result.outcome { + zeros += 1; + } + } - #[test] - fn test_rz_rotation_angle_relations() { - // Test that RZ(θ)RZ(-θ) = I - let mut q = StateVec::new(1); - let theta = FRAC_PI_3; + // Check if measurements are roughly equally distributed + let ratio = f64::from(zeros) / f64::from(trials); + assert!((ratio - 0.5).abs() < 0.1); // Should be close to 0.5... - // Apply forward then reverse rotations - q.rz(theta, 0).rz(-theta, 0); + // Test 4: Measuring one qubit of a Bell state + let mut q = StateVec::new(2); + q.h(0); + q.cx(0, 1); - // Should get back to |0⟩ up to global phase - assert!(q.state[1].norm() < 1e-10); - assert!((q.state[0].norm() - 1.0).abs() < 1e-10); + // Measure first qubit + let result1 = q.mz(0); + // Measure second qubit - should match first + let result2 = q.mz(1); + assert_eq!(result1.outcome, result2.outcome); } #[test] - fn test_state_vec_u_vs_trait_u() { - // Initialize state vectors with one qubit in the |0⟩ state. - let mut state_vec_u = StateVec::new(1); - let mut trait_u = StateVec::new(1); - - let theta = FRAC_PI_3; - let phi = FRAC_PI_4; - let lambda = FRAC_PI_6; - - // Apply `u` from the StateVec implementation. - state_vec_u.u(theta, phi, lambda, 0); - - // Apply `u` from the ArbitraryRotationGateable trait. - ArbitraryRotationGateable::u(&mut trait_u, theta, phi, lambda, 0); - - assert_states_equal(&state_vec_u.state, &trait_u.state); - } - - #[test] - fn test_r1xy_vs_trait_r1xy() { - // Initialize state vectors with one qubit in the |0⟩ state. - let mut state_vec_r1xy = StateVec::new(1); - let mut trait_r1xy = StateVec::new(1); - - // Define angles for the test. - let theta = FRAC_PI_3; - let phi = FRAC_PI_4; + fn test_measurement_consistency() { + let mut q = StateVec::new(1); - // Apply the manual `r1xy` implementation. - state_vec_r1xy.r1xy(theta, phi, 0); + // Put qubit in |1⟩ state + q.x(0); - // Apply the `r1xy` implementation from the `ArbitraryRotationGateable` trait. - ArbitraryRotationGateable::r1xy(&mut trait_r1xy, theta, phi, 0); + // Measure twice - result should be the same + let result1 = q.mz(0); + let result2 = q.mz(0); - // Use the `assert_states_equal` function to compare the states up to a global phase. - assert_states_equal(&state_vec_r1xy.state, &trait_r1xy.state); + assert!(result1.outcome); + assert!(result2.outcome); } #[test] - fn test_r1xy_vs_u() { - let mut state_r1xy = StateVec::new(1); - let mut state_u = StateVec::new(1); + fn test_measurement_collapse() { + let mut state_vec = StateVec::new(1); - let theta = FRAC_PI_3; - let phi = FRAC_PI_4; + // Prepare |+⟩ = (|0⟩ + |1⟩) / √2 + state_vec.h(0); - // Apply r1xy and equivalent u gates - state_r1xy.r1xy(theta, phi, 0); - state_u.u(theta, phi - FRAC_PI_2, FRAC_PI_2 - phi, 0); + // Simulate a measurement + let result = state_vec.mz(0); - assert_states_equal(&state_r1xy.state, &state_u.state); + // State should collapse to |0⟩ or |1⟩ + if result.outcome { + assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); + } else { + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + } } + // test Pauli-basis prep + // ===================== #[test] - fn test_rz_vs_u() { - let mut state_rz = StateVec::new(1); - let mut state_u = StateVec::new(1); + fn test_pz() { + let mut q = StateVec::new(1); - let theta = FRAC_PI_3; + q.h(0); + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - // Apply rz and u gates - state_rz.rz(theta, 0); - state_u.u(0.0, 0.0, theta, 0); + q.pz(0); - assert_states_equal(&state_rz.state, &state_u.state); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); } #[test] - fn test_u_decomposition() { - let mut state_u = StateVec::new(1); - let mut state_decomposed = StateVec::new(1); + fn test_pz_multiple_qubits() { + let mut q = StateVec::new(2); - let theta = FRAC_PI_3; - let phi = FRAC_PI_4; - let lambda = FRAC_PI_6; + q.h(0); + q.cx(0, 1); - // Apply U gate - state_u.u(theta, phi, lambda, 0); + q.pz(0); - // Apply the decomposed gates - state_decomposed.rz(lambda, 0); - state_decomposed.r1xy(theta, FRAC_PI_2, 0); - state_decomposed.rz(phi, 0); + let prob_0 = q.state[0].norm_sqr() + q.state[2].norm_sqr(); + let prob_1 = q.state[1].norm_sqr() + q.state[3].norm_sqr(); - // Assert that the states are equal - assert_states_equal(&state_u.state, &state_decomposed.state); + assert!((prob_0 - 1.0).abs() < 1e-10); + assert!(prob_1 < 1e-10); } + // Basic single-qubit rotation gate tests + // ====================================== #[test] - fn test_x_vs_r1xy() { - let mut state = StateVec::new(1); - state.x(0); - let state_after_x = state.clone(); + fn test_rx() { + // Test RX gate functionality + let mut q = StateVec::new(1); - state.reset(); - state.r1xy(PI, 0.0, 0); - let state_after_r1xy = state.clone(); + // RX(π) should flip |0⟩ to -i|1⟩ + q.rx(PI, 0); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].norm() - 1.0).abs() < 1e-10); - assert_states_equal(&state_after_x.state, &state_after_r1xy.state); + // RX(2π) should return to the initial state up to global phase + let mut q = StateVec::new(1); + q.rx(2.0 * PI, 0); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); } #[test] - fn test_y_vs_r1xy() { - let mut state = StateVec::new(1); - state.y(0); - let state_after_y = state.clone(); + fn test_ry() { + let mut q = StateVec::new(1); - state.reset(); - state.r1xy(PI, FRAC_PI_2, 0); - let state_after_r1xy = state.clone(); + // RY(π) should flip |0⟩ to |1⟩ + q.ry(PI, 0); + assert!(q.state[0].norm() < 1e-10); // Close to zero + assert!((q.state[1].norm() - 1.0).abs() < 1e-10); // Magnitude 1 for |1⟩ - assert_states_equal(&state_after_y.state, &state_after_r1xy.state); + // Two RY(π) rotations should return to the initial state + q.ry(PI, 0); + assert!((q.state[0].norm() - 1.0).abs() < 1e-10); // Magnitude 1 for |0⟩ + assert!(q.state[1].norm() < 1e-10); // Close to zero } #[test] - fn test_h_vs_r1xy_rz() { - let mut state = StateVec::new(1); - state.h(0); // Apply the H gate - let state_after_h = state.clone(); + fn test_rz() { + let mut q = StateVec::new(1); + + // RZ should only add phases, not change probabilities + q.h(0); // Put qubit in superposition + let probs_before: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); - state.reset(); // Reset state to |0⟩ - state.r1xy(FRAC_PI_2, -FRAC_PI_2, 0).rz(PI, 0); - let state_after_r1xy_rz = state.clone(); + q.rz(FRAC_PI_2, 0); // Rotate Z by π/2 + let probs_after: Vec = q.state.iter().map(num_complex::Complex::norm_sqr).collect(); - assert_states_equal(&state_after_h.state, &state_after_r1xy_rz.state); + for (p1, p2) in probs_before.iter().zip(probs_after.iter()) { + assert!((p1 - p2).abs() < 1e-10); // Probabilities unchanged + } } #[test] - fn test_cx_decomposition() { - let mut state_cx = StateVec::new(2); - let mut state_decomposed = StateVec::new(2); + fn test_u() { + let mut q = StateVec::new(1); - let control = 0; - let target = 1; + // Apply some arbitrary rotation + let theta = PI / 5.0; + let phi = PI / 7.0; + let lambda = PI / 3.0; + q.u(theta, phi, lambda, 0); - // Apply CX gate - state_cx.cx(control, target); + // Verify normalization is preserved + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); - // Apply the decomposed gates - state_decomposed.r1xy(-FRAC_PI_2, FRAC_PI_2, target); - state_decomposed.rzz(FRAC_PI_2, control, target); - state_decomposed.rz(-FRAC_PI_2, control); - state_decomposed.r1xy(FRAC_PI_2, PI, target); - state_decomposed.rz(-FRAC_PI_2, target); + // Verify expected amplitudes + let expected_0 = (theta / 2.0).cos(); + assert!((q.state[0].re - expected_0).abs() < 1e-10); - // Assert that the states are equal - assert_states_equal(&state_cx.state, &state_decomposed.state); + let expected_1_mag = (theta / 2.0).sin(); + assert!((q.state[1].norm() - expected_1_mag).abs() < 1e-10); } - #[test] - fn test_rxx_decomposition() { - let mut state_rxx = StateVec::new(2); - let mut state_decomposed = StateVec::new(2); - - let control = 0; - let target = 1; - - // Apply RXX gate - state_rxx.rxx(FRAC_PI_4, control, target); + fn test_r1xy() { + // Initialize state vectors with one qubit in the |0⟩ state. + let mut state_vec_r1xy = StateVec::new(1); + let mut trait_r1xy = StateVec::new(1); - // Apply the decomposed gates - state_decomposed.r1xy(FRAC_PI_2, FRAC_PI_2, control); - state_decomposed.r1xy(FRAC_PI_2, FRAC_PI_2, target); - state_decomposed.rzz(FRAC_PI_4, control, target); - state_decomposed.r1xy(FRAC_PI_2, -FRAC_PI_2, control); - state_decomposed.r1xy(FRAC_PI_2, -FRAC_PI_2, target); + // Define angles for the test. + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; - // Assert that the states are equal - assert_states_equal(&state_rxx.state, &state_decomposed.state); - } + // Apply the manual `r1xy` implementation. + state_vec_r1xy.r1xy(theta, phi, 0); - #[test] - fn test_hadamard() { - let mut q = StateVec::new(1); - q.h(0); + // Apply the `r1xy` implementation from the `ArbitraryRotationGateable` trait. + ArbitraryRotationGateable::r1xy(&mut trait_r1xy, theta, phi, 0); - assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); - assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + // Use the `assert_states_equal` function to compare the states up to a global phase. + assert_states_equal(&state_vec_r1xy.state, &trait_r1xy.state); } + // Basic two-qubit rotation gate tests + // =================================== #[test] - fn test_swap() { + fn test_rxx() { + // Test 1: RXX(π/2) on |00⟩ should give (|00⟩ - i|11⟩)/√2 let mut q = StateVec::new(2); - q.x(0); - q.swap(0, 1); - - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[2].re - 1.0).abs() < 1e-10); - } - - #[test] - fn test_two_qubit_unitary_swap_simple() { - let mut state_vec = StateVec::new(2); + q.rxx(FRAC_PI_2, 0, 1); - let swap_gate = [ - [ - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - ], - ]; + let expected = FRAC_1_SQRT_2; + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3].im + expected).abs() < 1e-10); - state_vec.prepare_computational_basis(2); // |10⟩ - state_vec.two_qubit_unitary(1, 0, swap_gate); + // Test 2: RXX(2π) should return to original state up to global phase + let mut q = StateVec::new(2); + q.h(0); // Create some initial state + let initial = q.state.clone(); + q.rxx(TAU, 0, 1); - assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); // Should now be |01⟩ - } + // Compare up to global phase + if q.state[0].norm() > 1e-10 { + let phase = q.state[0] / initial[0]; + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!((a - b * phase).norm() < 1e-10); + } + } - #[test] - fn test_cx() { + // Test 3: RXX(π) should flip |00⟩ to |11⟩ up to phase let mut q = StateVec::new(2); - // Prep |+> - q.h(0); - q.cx(0, 1); + q.rxx(PI, 0, 1); - // Should be in Bell state (|00> + |11>)/sqrt(2) - let expected = 1.0 / 2.0_f64.sqrt(); - assert!((q.state[0].re - expected).abs() < 1e-10); - assert!((q.state[3].re - expected).abs() < 1e-10); + // Should get -i|11⟩ + assert!(q.state[0].norm() < 1e-10); assert!(q.state[1].norm() < 1e-10); assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3] - Complex64::new(0.0, -1.0)).norm() < 1e-10); } #[test] - fn test_cx_all_basis_states() { - let mut state_vec = StateVec::new(2); - - // |00⟩ → should remain |00⟩ - state_vec.prepare_computational_basis(0); - state_vec.cx(1, 0); - assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); - - // |01⟩ → should remain |01⟩ - state_vec.prepare_computational_basis(1); - state_vec.cx(1, 0); - assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); + fn test_ryy() { + let expected = FRAC_1_SQRT_2; - // |10⟩ → should flip to |11⟩ - state_vec.prepare_computational_basis(2); - state_vec.cx(1, 0); - assert!((state_vec.probability(3) - 1.0).abs() < 1e-10); + // Test all basis states for RYY(π/2) + // |00⟩ -> (1/√2)|00⟩ - i(1/√2)|11⟩ + let mut q = StateVec::new(2); + q.ryy(FRAC_PI_2, 0, 1); + assert!((q.state[0].re - expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3].im - expected).abs() < 1e-10); - // |11⟩ → should flip to |10⟩ - state_vec.prepare_computational_basis(3); - state_vec.cx(1, 0); - assert!((state_vec.probability(2) - 1.0).abs() < 1e-10); - } + // |11⟩ -> i(1/√2)|00⟩ + (1/√2)|11⟩ + let mut q = StateVec::new(2); + q.x(0).x(1); // Prepare |11⟩ + q.ryy(FRAC_PI_2, 0, 1); + assert!((q.state[0].im - expected).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); + assert!(q.state[2].norm() < 1e-10); + assert!((q.state[3].re - expected).abs() < 1e-10); - #[test] - fn test_cy() { + // |01⟩ -> (1/√2)|01⟩ + i(1/√2)|10⟩ let mut q = StateVec::new(2); + q.x(1); // Prepare |01⟩ + q.ryy(FRAC_PI_2, 0, 1); + assert!(q.state[0].norm() < 1e-10); + assert!(q.state[1].re.abs() < 1e-10); + assert!((q.state[1].im + expected).abs() < 1e-10); + assert!((q.state[2].re - expected).abs() < 1e-10); + assert!(q.state[2].im.abs() < 1e-10); + assert!(q.state[3].norm() < 1e-10); - // Create |+0⟩ state - q.h(0); + // |10⟩ -> (1/√2)|10⟩ + i(1/√2)|01⟩ + let mut q = StateVec::new(2); + q.x(0); // Prepare |10⟩ + q.ryy(FRAC_PI_2, 0, 1); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].re - expected).abs() < 1e-10); + assert!(q.state[1].im.abs() < 1e-10); + assert!(q.state[2].re.abs() < 1e-10); + assert!((q.state[2].im + expected).abs() < 1e-10); + assert!(q.state[3].norm() < 1e-10); - // Apply CY to get entangled state - q.cy(0, 1); + // Test properties - // Should be (|00⟩ + i|11⟩)/√2 - let expected = FRAC_1_SQRT_2; - assert!((q.state[0].re - expected).abs() < 1e-10); // |00⟩ amplitude - assert!(q.state[1].norm() < 1e-10); // |01⟩ amplitude - assert!(q.state[2].norm() < 1e-10); // |10⟩ amplitude - assert!((q.state[3].im - expected).abs() < 1e-10); // |11⟩ amplitude - } - - #[test] - fn test_cz() { + // 1. Periodicity: RYY(2π) = I let mut q = StateVec::new(2); + q.h(0); // Create non-trivial initial state + let initial = q.state.clone(); + q.ryy(TAU, 0, 1); + // Need to account for potential global phase + if q.state[0].norm() > 1e-10 { + let phase = q.state[0] / initial[0]; + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!( + (a - b * phase).norm() < 1e-10, + "Periodicity test failed: a={a}, b={b}" + ); + } + } - // Create |++⟩ state - q.h(0); - q.h(1); - - // Apply CZ - q.cz(0, 1); - - // Should be (|00⟩ + |01⟩ + |10⟩ - |11⟩)/2 - let expected = 0.5; - assert!((q.state[0].re - expected).abs() < 1e-10); // |00⟩ amplitude - assert!((q.state[1].re - expected).abs() < 1e-10); // |01⟩ amplitude - assert!((q.state[2].re - expected).abs() < 1e-10); // |10⟩ amplitude - assert!((q.state[3].re + expected).abs() < 1e-10); // |11⟩ amplitude - } - - #[test] - fn test_state_normalization() { - let mut state_vec = StateVec::new(3); - - // Apply multiple gates - state_vec.h(0); - state_vec.cx(0, 1); - state_vec.cx(1, 2); - - // Verify normalization - let norm: f64 = state_vec.state.iter().map(|amp| amp.norm_sqr()).sum(); - assert!((norm - 1.0).abs() < 1e-10); - } - - #[test] - fn test_non_commuting_gates() { - let mut state1 = StateVec::new(1); - let mut state2 = StateVec::new(1); - - state1.h(0); - state1.z(0); - - state2.z(0); - state2.h(0); - - // Compute the global norm difference - let diff_norm: f64 = state1 - .state - .iter() - .zip(state2.state.iter()) - .map(|(a, b)| (a - b).norm_sqr()) - .sum::() - .sqrt(); - - assert!(diff_norm > 1e-10, "H and Z should not commute."); - } - - #[test] - fn test_control_target_independence() { - // Test that CY and CZ work regardless of which qubit is control/target + // 2. Composition: RYY(θ₁)RYY(θ₂) = RYY(θ₁ + θ₂) let mut q1 = StateVec::new(2); let mut q2 = StateVec::new(2); + q1.h(0); // Create non-trivial initial state + q2.h(0); // Same initial state + q1.ryy(FRAC_PI_3, 0, 1).ryy(FRAC_PI_6, 0, 1); + q2.ryy(FRAC_PI_2, 0, 1); + // Compare up to global phase + if q1.state[0].norm() > 1e-10 { + let phase = q1.state[0] / q2.state[0]; + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!( + (a - b * phase).norm() < 1e-10, + "Composition test failed: a={a}, b={b}" + ); + } + } - // Prepare same initial state - q1.h(0); - q1.h(1); - q2.h(0); - q2.h(1); - - // Apply gates with different control/target - q1.cz(0, 1); - q2.cz(1, 0); - - assert_states_equal(&q1.state, &q2.state); + // 3. Symmetry: RYY(θ,0,1) = RYY(θ,1,0) + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + q1.h(0).h(1); // Create non-trivial initial state + q2.h(0).h(1); // Same initial state + q1.ryy(FRAC_PI_3, 0, 1); + q2.ryy(FRAC_PI_3, 1, 0); + // States should be exactly equal (no phase difference) + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10, "Symmetry test failed: a={a}, b={b}"); + } } #[test] - fn test_x() { - let mut q = StateVec::new(1); - - // Check initial state is |0> - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - - // Test X on |0> -> |1> - q.x(0); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].re - 1.0).abs() < 1e-10); + fn test_rzz() { + // Test 1: RZZ(π) on (|00⟩ + |11⟩)/√2 should give itself + let mut q = StateVec::new(2); + // Create Bell state + q.h(0); + q.cx(0, 1); + let initial = q.state.clone(); - // Test X on |1> -> |0> - q.x(0); - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); + q.rzz(PI, 0, 1); - // Test X on superposition - q.h(0); - let initial_state = q.state.clone(); - q.x(0); // X|+> = |+> - for (state, initial) in q.state.iter().zip(initial_state.iter()) { - assert!((state - initial).norm() < 1e-10); + // Compare up to global phase + if q.state[0].norm() > 1e-10 { + let phase = q.state[0] / initial[0]; + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!((a - b * phase).norm() < 1e-10); + } } - // Test X on second qubit of two-qubit system + // Test 2: RZZ(π/2) on |++⟩ let mut q = StateVec::new(2); - q.x(1); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[2].re - 1.0).abs() < 1e-10); - } - - #[test] - fn test_y() { - let mut q = StateVec::new(1); - - // Test Y on |0⟩ -> i|1⟩ - q.y(0); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1] - Complex64::i()).norm() < 1e-10); + q.h(0); + q.h(1); + q.rzz(FRAC_PI_2, 0, 1); - // Test Y on i|1⟩ -> |0⟩ - q.y(0); - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); + // e^(-iπ/4) = (1-i)/√2 + // e^(iπ/4) = (1+i)/√2 + let factor = 0.5; // 1/2 for the |++⟩ normalization + let exp_minus_i_pi_4 = Complex64::new(1.0, -1.0) / (2.0_f64.sqrt()); + let exp_plus_i_pi_4 = Complex64::new(1.0, 1.0) / (2.0_f64.sqrt()); - // Test Y on |+⟩ - let mut q = StateVec::new(1); - q.h(0); // Create |+⟩ - q.y(0); // Should give i|-⟩ - let expected = FRAC_1_SQRT_2; - assert!((q.state[0].im + expected).abs() < 1e-10); - assert!((q.state[1].im - expected).abs() < 1e-10); + assert!((q.state[0] - factor * exp_minus_i_pi_4).norm() < 1e-10); // |00⟩ + assert!((q.state[1] - factor * exp_plus_i_pi_4).norm() < 1e-10); // |01⟩ + assert!((q.state[2] - factor * exp_plus_i_pi_4).norm() < 1e-10); // |10⟩ + assert!((q.state[3] - factor * exp_minus_i_pi_4).norm() < 1e-10); // |11⟩ } + // Core mathematical properties + // ============================ #[test] - fn test_z() { + fn test_normalization() { let mut q = StateVec::new(1); + q.h(0).sz(0); + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); + } - // Test Z on |0⟩ -> |0⟩ - q.z(0); - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - - // Test Z on |1⟩ -> -|1⟩ - q.x(0); // Prepare |1⟩ - q.z(0); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].re + 1.0).abs() < 1e-10); - - // Test Z on |+⟩ -> |-⟩ + #[test] + fn test_unitarity() { let mut q = StateVec::new(1); - q.h(0); // Create |+⟩ - q.z(0); // Should give |-⟩ - let expected = FRAC_1_SQRT_2; - assert!((q.state[0].re - expected).abs() < 1e-10); - assert!((q.state[1].re + expected).abs() < 1e-10); + q.h(0); + let initial = q.state.clone(); + q.h(0).h(0); + assert_states_equal(&q.state, &initial); } #[test] @@ -2046,1127 +1881,92 @@ mod tests { } } + // test core general gates + // ======================= #[test] - fn test_rxx() { - // Test 1: RXX(π/2) on |00⟩ should give (|00⟩ - i|11⟩)/√2 - let mut q = StateVec::new(2); - q.rxx(FRAC_PI_2, 0, 1); - - let expected = FRAC_1_SQRT_2; - assert!((q.state[0].re - expected).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - assert!(q.state[2].norm() < 1e-10); - assert!((q.state[3].im + expected).abs() < 1e-10); + fn test_single_qubit_rotation() { + let mut q = StateVec::new(1); - // Test 2: RXX(2π) should return to original state up to global phase - let mut q = StateVec::new(2); - q.h(0); // Create some initial state - let initial = q.state.clone(); - q.rxx(TAU, 0, 1); + // Test 1: Hadamard gate + let h00 = Complex64::new(FRAC_1_SQRT_2, 0.0); + let h01 = Complex64::new(FRAC_1_SQRT_2, 0.0); + let h10 = Complex64::new(FRAC_1_SQRT_2, 0.0); + let h11 = Complex64::new(-FRAC_1_SQRT_2, 0.0); - // Compare up to global phase - if q.state[0].norm() > 1e-10 { - let phase = q.state[0] / initial[0]; - for (a, b) in q.state.iter().zip(initial.iter()) { - assert!((a - b * phase).norm() < 1e-10); - } - } + q.single_qubit_rotation(0, h00, h01, h10, h11); + assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - // Test 3: RXX(π) should flip |00⟩ to |11⟩ up to phase - let mut q = StateVec::new(2); - q.rxx(PI, 0, 1); + // Test 2: X gate + let mut q = StateVec::new(1); + let x00 = Complex64::new(0.0, 0.0); + let x01 = Complex64::new(1.0, 0.0); + let x10 = Complex64::new(1.0, 0.0); + let x11 = Complex64::new(0.0, 0.0); - // Should get -i|11⟩ + q.single_qubit_rotation(0, x00, x01, x10, x11); assert!(q.state[0].norm() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - assert!(q.state[2].norm() < 1e-10); - assert!((q.state[3] - Complex64::new(0.0, -1.0)).norm() < 1e-10); - } + assert!((q.state[1].re - 1.0).abs() < 1e-10); - #[test] - fn test_rxx_symmetry() { - // Test that RXX is symmetric under exchange of qubits - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); + // Test 3: Phase gate + let mut q = StateVec::new(1); + let p00 = Complex64::new(1.0, 0.0); + let p01 = Complex64::new(0.0, 0.0); + let p10 = Complex64::new(0.0, 0.0); + let p11 = Complex64::new(0.0, 1.0); - // Prepare same non-trivial initial state - q1.h(0); - q1.h(1); - q2.h(0); - q2.h(1); + q.single_qubit_rotation(0, p00, p01, p10, p11); + assert!((q.state[0].re - 1.0).abs() < 1e-10); + assert!(q.state[1].norm() < 1e-10); - // Apply RXX with different qubit orders - q1.rxx(FRAC_PI_3, 0, 1); - q2.rxx(FRAC_PI_3, 1, 0); + // Test 4: Y gate using unitary + let mut q = StateVec::new(1); + let y00 = Complex64::new(0.0, 0.0); + let y01 = Complex64::new(0.0, -1.0); + let y10 = Complex64::new(0.0, 1.0); + let y11 = Complex64::new(0.0, 0.0); - // Results should be identical - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!((a - b).norm() < 1e-10); - } + q.single_qubit_rotation(0, y00, y01, y10, y11); + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[1].im - 1.0).abs() < 1e-10); } #[test] - fn test_ryy() { - let expected = FRAC_1_SQRT_2; + fn test_unitary_properties() { + let mut q = StateVec::new(1); - // Test all basis states for RYY(π/2) - // |00⟩ -> (1/√2)|00⟩ - i(1/√2)|11⟩ - let mut q = StateVec::new(2); - q.ryy(FRAC_PI_2, 0, 1); - assert!((q.state[0].re - expected).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - assert!(q.state[2].norm() < 1e-10); - assert!((q.state[3].im - expected).abs() < 1e-10); - - // |11⟩ -> i(1/√2)|00⟩ + (1/√2)|11⟩ - let mut q = StateVec::new(2); - q.x(0).x(1); // Prepare |11⟩ - q.ryy(FRAC_PI_2, 0, 1); - assert!((q.state[0].im - expected).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - assert!(q.state[2].norm() < 1e-10); - assert!((q.state[3].re - expected).abs() < 1e-10); - - // |01⟩ -> (1/√2)|01⟩ + i(1/√2)|10⟩ - let mut q = StateVec::new(2); - q.x(1); // Prepare |01⟩ - q.ryy(FRAC_PI_2, 0, 1); - assert!(q.state[0].norm() < 1e-10); - assert!(q.state[1].re.abs() < 1e-10); - assert!((q.state[1].im + expected).abs() < 1e-10); - assert!((q.state[2].re - expected).abs() < 1e-10); - assert!(q.state[2].im.abs() < 1e-10); - assert!(q.state[3].norm() < 1e-10); - - // |10⟩ -> (1/√2)|10⟩ + i(1/√2)|01⟩ - let mut q = StateVec::new(2); - q.x(0); // Prepare |10⟩ - q.ryy(FRAC_PI_2, 0, 1); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].re - expected).abs() < 1e-10); - assert!(q.state[1].im.abs() < 1e-10); - assert!(q.state[2].re.abs() < 1e-10); - assert!((q.state[2].im + expected).abs() < 1e-10); - assert!(q.state[3].norm() < 1e-10); - - // Test properties - - // 1. Periodicity: RYY(2π) = I - let mut q = StateVec::new(2); - q.h(0); // Create non-trivial initial state - let initial = q.state.clone(); - q.ryy(TAU, 0, 1); - // Need to account for potential global phase - if q.state[0].norm() > 1e-10 { - let phase = q.state[0] / initial[0]; - for (a, b) in q.state.iter().zip(initial.iter()) { - assert!( - (a - b * phase).norm() < 1e-10, - "Periodicity test failed: a={a}, b={b}" - ); - } - } - - // 2. Composition: RYY(θ₁)RYY(θ₂) = RYY(θ₁ + θ₂) - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - q1.h(0); // Create non-trivial initial state - q2.h(0); // Same initial state - q1.ryy(FRAC_PI_3, 0, 1).ryy(FRAC_PI_6, 0, 1); - q2.ryy(FRAC_PI_2, 0, 1); - // Compare up to global phase - if q1.state[0].norm() > 1e-10 { - let phase = q1.state[0] / q2.state[0]; - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!( - (a - b * phase).norm() < 1e-10, - "Composition test failed: a={a}, b={b}" - ); - } - } - - // 3. Symmetry: RYY(θ,0,1) = RYY(θ,1,0) - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - q1.h(0).h(1); // Create non-trivial initial state - q2.h(0).h(1); // Same initial state - q1.ryy(FRAC_PI_3, 0, 1); - q2.ryy(FRAC_PI_3, 1, 0); - // States should be exactly equal (no phase difference) - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!((a - b).norm() < 1e-10, "Symmetry test failed: a={a}, b={b}"); - } - } - - #[test] - fn test_ryy_qubit_order_invariance() { - let theta = FRAC_PI_4; - - // Test on random initial states - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - q1.h(0).x(1); // Random state - q2.h(0).x(1); // Same initial state - - q1.ryy(theta, 0, 1); - q2.ryy(theta, 1, 0); - - // States should be exactly equal - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!( - (a - b).norm() < 1e-10, - "Qubit order test failed: a={a}, b={b}" - ); - } - } - - #[test] - fn test_ryy_large_system() { - let theta = FRAC_PI_3; - - // Initialize a 5-qubit state - let mut q = StateVec::new(5); - q.h(0).h(1).h(2).h(3).h(4); // Superposition state - - // Apply RYY on qubits 2 and 4 - q.ryy(theta, 2, 4); - - // Ensure state vector normalization is preserved - let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); - assert!( - (norm - 1.0).abs() < 1e-10, - "State normalization test failed: norm={norm}" - ); - } - - fn assert_state_vectors_match(simulated: &[Complex64], expected: &[Complex64], tolerance: f64) { - assert_eq!( - simulated.len(), - expected.len(), - "State vectors must have the same length" - ); - - // Find the first non-zero entry in the expected vector - let reference_index = expected - .iter() - .position(|&x| x.norm() > tolerance) - .expect("Expected vector should have at least one non-zero element"); - - // Compute the phase correction - let phase = simulated[reference_index] / expected[reference_index]; - - // Verify all elements match up to the global phase - for (i, (sim, exp)) in simulated.iter().zip(expected.iter()).enumerate() { - let corrected_exp = exp * phase; - assert!( - (sim - corrected_exp).norm() < tolerance, - "Mismatch at index {i}: simulated = {sim:?}, expected = {exp:?}, corrected = {corrected_exp:?}" - ); - } - } - - #[test] - fn test_ryy_edge_cases() { - let mut q = StateVec::new(2); - - // Apply RYY gate - q.ryy(PI, 0, 1); - - // Define the expected result for RYY(π) - let expected = vec![ - Complex64::new(0.0, 0.0), // |00⟩ - Complex64::new(0.0, 0.0), // |01⟩ - Complex64::new(0.0, 0.0), // |10⟩ - Complex64::new(-1.0, 0.0), // |11⟩ - ]; - - // Compare simulated state vector to the expected result - assert_state_vectors_match(&q.state, &expected, 1e-10); - } - - #[test] - fn test_ryy_global_phase() { - let mut q = StateVec::new(2); - - q.ryy(PI, 0, 1); - - // Define the expected result for RYY(π) - let expected = vec![ - Complex64::new(0.0, 0.0), // |00⟩ - Complex64::new(0.0, 0.0), // |01⟩ - Complex64::new(0.0, 0.0), // |10⟩ - Complex64::new(-1.0, 0.0), // |11⟩ - ]; - - // Compare states - assert_states_equal(&q.state, &expected); - } - - #[test] - fn test_ryy_small_angles() { - let theta = 1e-10; // Very small angle - let mut q = StateVec::new(2); - - // Initialize |00⟩ - let initial = q.state.clone(); - q.ryy(theta, 0, 1); - - // Expect state to remain close to the initial state - for (a, b) in q.state.iter().zip(initial.iter()) { - assert!( - (a - b).norm() < 1e-10, - "Small angle test failed: a={a}, b={b}" - ); - } - } - - #[test] - fn test_ryy_randomized() { - use rand::Rng; - - let mut rng = rand::thread_rng(); - let theta = rng.gen_range(0.0..2.0 * PI); - - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - - // Random initial state - q1.h(0).h(1); - q2.h(0).h(1); - - // Apply RYY with random qubit order - q1.ryy(theta, 0, 1); - q2.ryy(theta, 1, 0); - - // Compare states - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!( - (a - b).norm() < 1e-10, - "Randomized test failed: a={a}, b={b}" - ); - } - } - - #[test] - fn test_rzz() { - // Test 1: RZZ(π) on (|00⟩ + |11⟩)/√2 should give itself - let mut q = StateVec::new(2); - // Create Bell state - q.h(0); - q.cx(0, 1); - let initial = q.state.clone(); - - q.rzz(PI, 0, 1); - - // Compare up to global phase - if q.state[0].norm() > 1e-10 { - let phase = q.state[0] / initial[0]; - for (a, b) in q.state.iter().zip(initial.iter()) { - assert!((a - b * phase).norm() < 1e-10); - } - } - - // Test 2: RZZ(π/2) on |++⟩ - let mut q = StateVec::new(2); - q.h(0); - q.h(1); - q.rzz(FRAC_PI_2, 0, 1); - - // e^(-iπ/4) = (1-i)/√2 - // e^(iπ/4) = (1+i)/√2 - let factor = 0.5; // 1/2 for the |++⟩ normalization - let exp_minus_i_pi_4 = Complex64::new(1.0, -1.0) / (2.0_f64.sqrt()); - let exp_plus_i_pi_4 = Complex64::new(1.0, 1.0) / (2.0_f64.sqrt()); - - assert!((q.state[0] - factor * exp_minus_i_pi_4).norm() < 1e-10); // |00⟩ - assert!((q.state[1] - factor * exp_plus_i_pi_4).norm() < 1e-10); // |01⟩ - assert!((q.state[2] - factor * exp_plus_i_pi_4).norm() < 1e-10); // |10⟩ - assert!((q.state[3] - factor * exp_minus_i_pi_4).norm() < 1e-10); // |11⟩ - } - - #[test] - fn test_szz_equivalence() { - // Test that SZZ is equivalent to RZZ(π/2) - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - - // Create some non-trivial initial state - q1.h(0); - q2.h(0); - - // Compare direct SZZ vs RZZ(π/2) - q1.szz(0, 1); - q2.rzz(FRAC_PI_2, 0, 1); - - assert_states_equal(q1.state(), q2.state()); - - // Also verify decomposition matches - let mut q3 = StateVec::new(2); - q3.h(0); // Same initial state - q3.h(0).h(1).sxx(0, 1).h(0).h(1); - - assert_states_equal(q1.state(), q3.state()); - } - - #[test] - fn test_szz_trait_equivalence() { - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - - // Create some non-trivial initial state - q1.h(0); - q2.h(0); - - // Compare CliffordGateable trait szz vs ArbitraryRotationGateable trait rzz(π/2) - CliffordGateable::::szz(&mut q1, 0, 1); - ArbitraryRotationGateable::::rzz(&mut q2, PI / 2.0, 0, 1); - - assert_states_equal(q1.state(), q2.state()); - } - - #[test] - fn test_rotation_symmetries() { - // Test that all rotations are symmetric under exchange of qubits - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - - // Prepare same non-trivial initial state - q1.h(0); - q1.h(1); - q2.h(0); - q2.h(1); - - let theta = PI / 3.0; - - // Test RYY symmetry - q1.ryy(theta, 0, 1); - q2.ryy(theta, 1, 0); - - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!((a - b).norm() < 1e-10); - } - - // Test RZZ symmetry - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - q1.h(0); - q1.h(1); - q2.h(0); - q2.h(1); - - q1.rzz(theta, 0, 1); - q2.rzz(theta, 1, 0); - - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!((a - b).norm() < 1e-10); - } - } - - #[test] - fn test_mz() { - // Test 1: Measuring |0> state - let mut q = StateVec::new(1); - let result = q.mz(0); - assert!(!result.outcome); - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - - // Test 2: Measuring |1> state - let mut q = StateVec::new(1); - q.x(0); - let result = q.mz(0); - assert!(result.outcome); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].re - 1.0).abs() < 1e-10); - - // Test 3: Measuring superposition state multiple times - let mut zeros = 0; - let trials = 1000; - - for _ in 0..trials { - let mut q = StateVec::new(1); - q.h(0); - let result = q.mz(0); - if !result.outcome { - zeros += 1; - } - } - - // Check if measurements are roughly equally distributed - let ratio = f64::from(zeros) / f64::from(trials); - assert!((ratio - 0.5).abs() < 0.1); // Should be close to 0.5... - - // Test 4: Measuring one qubit of a Bell state - let mut q = StateVec::new(2); - q.h(0); - q.cx(0, 1); - - // Measure first qubit - let result1 = q.mz(0); - // Measure second qubit - should match first - let result2 = q.mz(1); - assert_eq!(result1.outcome, result2.outcome); - } - - #[test] - fn test_pz() { - let mut q = StateVec::new(1); - - q.h(0); - assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); - assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - - q.pz(0); - - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - } - - #[test] - fn test_pz_multiple_qubits() { - let mut q = StateVec::new(2); - - q.h(0); - q.cx(0, 1); - - q.pz(0); - - let prob_0 = q.state[0].norm_sqr() + q.state[2].norm_sqr(); - let prob_1 = q.state[1].norm_sqr() + q.state[3].norm_sqr(); - - assert!((prob_0 - 1.0).abs() < 1e-10); - assert!(prob_1 < 1e-10); - } - - #[test] - fn test_single_qubit_rotation() { - let mut q = StateVec::new(1); - - // Test 1: Hadamard gate - let h00 = Complex64::new(FRAC_1_SQRT_2, 0.0); - let h01 = Complex64::new(FRAC_1_SQRT_2, 0.0); - let h10 = Complex64::new(FRAC_1_SQRT_2, 0.0); - let h11 = Complex64::new(-FRAC_1_SQRT_2, 0.0); - - q.single_qubit_rotation(0, h00, h01, h10, h11); - assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); - assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - - // Test 2: X gate - let mut q = StateVec::new(1); - let x00 = Complex64::new(0.0, 0.0); - let x01 = Complex64::new(1.0, 0.0); - let x10 = Complex64::new(1.0, 0.0); - let x11 = Complex64::new(0.0, 0.0); - - q.single_qubit_rotation(0, x00, x01, x10, x11); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].re - 1.0).abs() < 1e-10); - - // Test 3: Phase gate - let mut q = StateVec::new(1); - let p00 = Complex64::new(1.0, 0.0); - let p01 = Complex64::new(0.0, 0.0); - let p10 = Complex64::new(0.0, 0.0); - let p11 = Complex64::new(0.0, 1.0); - - q.single_qubit_rotation(0, p00, p01, p10, p11); - assert!((q.state[0].re - 1.0).abs() < 1e-10); - assert!(q.state[1].norm() < 1e-10); - - // Test 4: Y gate using unitary - let mut q = StateVec::new(1); - let y00 = Complex64::new(0.0, 0.0); - let y01 = Complex64::new(0.0, -1.0); - let y10 = Complex64::new(0.0, 1.0); - let y11 = Complex64::new(0.0, 0.0); - - q.single_qubit_rotation(0, y00, y01, y10, y11); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].im - 1.0).abs() < 1e-10); - } - - #[test] - fn test_sq_rotation_edge_cases() { - let mut q = StateVec::new(1); - - // Test RX(0): Should be identity - let initial = q.state.clone(); - q.rx(0.0, 0); - assert_states_equal(&q.state, &initial); - - // Test RX(2π): Should also be identity up to global phase - q.rx(2.0 * PI, 0); - assert_states_equal(&q.state, &initial); - - // Test RY(0): Should be identity - q.ry(0.0, 0); - assert_states_equal(&q.state, &initial); - - // Test RY(2π): Should also be identity up to global phase - q.ry(2.0 * PI, 0); - assert_states_equal(&q.state, &initial); - - // Test RZ(0): Should be identity - q.rz(0.0, 0); - assert_states_equal(&q.state, &initial); - - // Test RZ(2π): Should also be identity up to global phase - q.rz(2.0 * PI, 0); - assert_states_equal(&q.state, &initial); - } - - #[test] - fn test_unitary_properties() { - let mut q = StateVec::new(1); - - // Create random state with Hadamard - q.h(0); - - // Apply Z gate as unitary - let z00 = Complex64::new(1.0, 0.0); - let z01 = Complex64::new(0.0, 0.0); - let z10 = Complex64::new(0.0, 0.0); - let z11 = Complex64::new(-1.0, 0.0); - - let initial = q.state.clone(); - q.single_qubit_rotation(0, z00, z01, z10, z11); - - // Check normalization is preserved - let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); - assert!((norm - 1.0).abs() < 1e-10); - - // Apply Z again - should get back original state - q.single_qubit_rotation(0, z00, z01, z10, z11); - - for (a, b) in q.state.iter().zip(initial.iter()) { - assert!((a - b).norm() < 1e-10); - } - } - - #[test] - fn test_u_special_cases() { - // Test 1: U(π, 0, π) should be X gate - let mut q = StateVec::new(1); - q.u(PI, 0.0, PI, 0); - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[1].re - 1.0).abs() < 1e-10); - - // Test 2: Hadamard gate - // H = U(π/2, 0, π) - let mut q = StateVec::new(1); - q.u(PI / 2.0, 0.0, PI, 0); - assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); - assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - - // Test 3: U(0, 0, π) should be Z gate - let mut q = StateVec::new(1); - q.h(0); // First put in superposition - let initial = q.state.clone(); - q.u(0.0, 0.0, PI, 0); - assert!((q.state[0] - initial[0]).norm() < 1e-10); - assert!((q.state[1] + initial[1]).norm() < 1e-10); - - // Additional test: U3(π/2, π/2, -π/2) should be S†H - let mut q = StateVec::new(1); - q.u(PI / 2.0, PI / 2.0, -PI / 2.0, 0); - // This creates the state (|0⟩ + i|1⟩)/√2 - assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); - assert!((q.state[1].im - FRAC_1_SQRT_2).abs() < 1e-10); - } - - #[test] - fn test_u_composition() { - let mut q1 = StateVec::new(1); - let q2 = StateVec::new(1); - - // Two U gates that should multiply to identity - q1.u(PI / 3.0, PI / 4.0, PI / 6.0, 0); - q1.u(-PI / 3.0, -PI / 6.0, -PI / 4.0, 0); - - // Compare with initial state - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!((a - b).norm() < 1e-10); - } - } - - #[test] - fn test_u_arbitrary() { - let mut q = StateVec::new(1); - - // Apply some arbitrary rotation - let theta = PI / 5.0; - let phi = PI / 7.0; - let lambda = PI / 3.0; - q.u(theta, phi, lambda, 0); - - // Verify normalization is preserved - let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); - assert!((norm - 1.0).abs() < 1e-10); - - // Verify expected amplitudes - let expected_0 = (theta / 2.0).cos(); - assert!((q.state[0].re - expected_0).abs() < 1e-10); - - let expected_1_mag = (theta / 2.0).sin(); - assert!((q.state[1].norm() - expected_1_mag).abs() < 1e-10); - } - - #[test] - fn test_two_qubit_unitary_cnot() { - // Test that we can implement CNOT using the general unitary - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - - // CNOT matrix - let cnot = [ - [ - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - ], - ]; - - // Create Bell state using both methods - q1.h(0); - q1.cx(0, 1); - - q2.h(0); - q2.two_qubit_unitary(0, 1, cnot); - - // Compare results - for (a, b) in q1.state.iter().zip(q2.state.iter()) { - assert!((a - b).norm() < 1e-10); - } - } - - #[test] - fn test_two_qubit_unitary_swap() { - // Test SWAP gate - let mut q = StateVec::new(2); - - // Prepare |10⟩ state - q.x(0); - - // SWAP matrix - let swap = [ - [ - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - ], - ]; - - q.two_qubit_unitary(0, 1, swap); - - // Should be in |01⟩ state - assert!(q.state[0].norm() < 1e-10); - assert!((q.state[2].re - 1.0).abs() < 1e-10); - } - - #[test] - fn test_two_qubit_unitary_properties() { - let mut q = StateVec::new(2); - - // Create a non-trivial state - q.h(0); - q.h(1); - - // iSWAP matrix - let iswap = [ - [ - Complex64::new(1.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 1.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 1.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - ], - [ - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), - ], - ]; - - q.two_qubit_unitary(0, 1, iswap); - - // Verify normalization is preserved - let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); - assert!((norm - 1.0).abs() < 1e-10); - } - - #[test] - fn test_single_qubit_locality() { - // Test on 3 qubit system that gates only affect their target - let mut q = StateVec::new(3); - - // Prepare state |+⟩|0⟩|0⟩ - q.h(0); // Affects least significant bit - - // Apply X to qubit 2 (most significant bit) - q.x(2); - - // Check that qubit 0 is still in |+⟩ state - // When qubit 2 is |1⟩, check LSB still shows |+⟩ - assert!((q.state[4].re - FRAC_1_SQRT_2).abs() < 1e-10); // |100⟩ - assert!((q.state[5].re - FRAC_1_SQRT_2).abs() < 1e-10); // |101⟩ - } - - #[test] - fn test_bit_indexing() { - let mut q = StateVec::new(3); - - println!("Initial state (|000⟩):"); - for i in 0..8 { - println!(" {:03b}: {:.3}", i, q.state[i]); - } - - // Put |+⟩ on qubit 0 (LSB) - q.h(0); - - println!("\nAfter H on qubit 0:"); - for i in 0..8 { - println!(" {:03b}: {:.3}", i, q.state[i]); - } - - // Check state is |+⟩|0⟩|0⟩ - // Only indices that differ in LSB (qubit 0) should be FRAC_1_SQRT_2 - for i in 0..8 { - let qubit0 = i & 1; - let qubit1 = (i >> 1) & 1; - let qubit2 = (i >> 2) & 1; - - let expected = if qubit1 == 0 && qubit2 == 0 { - FRAC_1_SQRT_2 - } else { - 0.0 - }; - - if (q.state[i].re - expected).abs() >= 1e-10 { - println!("\nMismatch at index {i}: {i:03b}"); - println!("Qubit values: q2={qubit2}, q1={qubit1}, q0={qubit0}"); - println!("Expected {}, got {}", expected, q.state[i].re); - } - assert!((q.state[i].re - expected).abs() < 1e-10); - } - } - - #[test] - fn test_two_qubit_locality() { - let mut q = StateVec::new(4); - - println!("Initial state:"); - for i in 0..16 { - println!(" {:04b}: {:.3}", i, q.state[i]); - } - - // Prepare |+⟩ on qubit 0 (LSB) - q.h(0); - - println!("\nAfter H on qubit 0:"); - for i in 0..16 { - println!(" {:04b}: {:.3}", i, q.state[i]); - } - - // Apply CX between qubits 2,3 - q.cx(2, 3); - - println!("\nAfter CX on qubits 2,3:"); - for i in 0..16 { - println!(" {:04b}: {:.3}", i, q.state[i]); - - // Extract qubit values - // let _q0 = i & 1; - let q1 = (i >> 1) & 1; - let q2 = (i >> 2) & 1; - let q3 = (i >> 3) & 1; - - // Only states with q0=0 or q0=1 and q1=q2=q3=0 should have amplitude - let expected = if q1 == 0 && q2 == 0 && q3 == 0 { - FRAC_1_SQRT_2 - } else { - 0.0 - }; - - if (q.state[i].re - expected).abs() >= 1e-10 { - println!("Mismatch at {i:04b}"); - println!("Expected {}, got {}", expected, q.state[i].re); - } - assert!((q.state[i].re - expected).abs() < 1e-10); - } - } - - #[test] - fn test_two_qubit_gate_locality() { - let mut q = StateVec::new(3); - - // Prepare state |+⟩|0⟩|0⟩ - q.h(0); - - // Apply CX on qubits 1 and 2 (no effect on qubit 0) - q.cx(1, 2); - - // Qubit 0 should remain in superposition - let expected_amp = 1.0 / 2.0_f64.sqrt(); - assert!((q.state[0].re - expected_amp).abs() < 1e-10); - assert!((q.state[1].re - expected_amp).abs() < 1e-10); - } - - #[test] - fn test_rotation_locality() { - let mut q = StateVec::new(3); - - println!("Initial state:"); - for i in 0..8 { - println!(" {:03b}: {:.3}", i, q.state[i]); - } - - // Prepare |+⟩ on qubit 0 (LSB) - q.h(0); - - println!("\nAfter H on qubit 0:"); - for i in 0..8 { - println!(" {:03b}: {:.3}", i, q.state[i]); - } - - // Apply rotation to qubit 1 - q.rx(PI / 2.0, 1); - - println!("\nAfter RX on qubit 1:"); - for i in 0..8 { - println!(" {:03b}: {:.3}", i, q.state[i]); - } - - // Check each basis state contribution - for i in 0..8 { - let expected = FRAC_1_SQRT_2; - if (q.state[i].norm() - expected).abs() >= 1e-10 { - println!("\nMismatch at index {i}: {i:03b}"); - println!("Expected norm {}, got {}", expected, q.state[i].norm()); - } - } - } - - #[test] - fn test_large_system() { - // Test with a large number of qubits to ensure robustness. - let num_qubits = 20; // 20 qubits => 2^20 amplitudes (~1M complex numbers) - let mut q = StateVec::new(num_qubits); - - // Apply Hadamard to the first qubit - q.h(0); - - // Check normalization and amplitudes for |0...0> and |1...0> - let expected_amp = 1.0 / (2.0_f64.sqrt()); - assert!((q.state[0].norm() - expected_amp).abs() < 1e-10); - assert!((q.state[1].norm() - expected_amp).abs() < 1e-10); - - // Ensure all other amplitudes remain zero - for i in 2..q.state.len() { - assert!(q.state[i].norm() < 1e-10); - } - } - - #[test] - fn test_measurement_consistency() { - let mut q = StateVec::new(1); - - // Put qubit in |1⟩ state - q.x(0); - - // Measure twice - result should be the same - let result1 = q.mz(0); - let result2 = q.mz(0); - - assert!(result1.outcome); - assert!(result2.outcome); - } - - #[test] - fn test_measurement_on_entangled_state() { - let mut q = StateVec::new(2); - - // Create Bell state (|00⟩ + |11⟩) / sqrt(2) - q.h(0); - q.cx(0, 1); - - // Measure the first qubit - let result1 = q.mz(0); - - // Measure the second qubit - should match the first - let result2 = q.mz(1); - - assert_eq!(result1.outcome, result2.outcome); - } - - #[test] - fn test_prepare_computational_basis_all_states() { - let num_qubits = 3; - let mut state_vec = StateVec::new(num_qubits); - - for basis_state in 0..(1 << num_qubits) { - state_vec.prepare_computational_basis(basis_state); - for i in 0..state_vec.state.len() { - if i == basis_state { - assert!((state_vec.state[i].norm() - 1.0).abs() < 1e-10); - } else { - assert!(state_vec.state[i].norm() < 1e-10); - } - } - } - } - - #[test] - fn test_reset() { - let mut state_vec = StateVec::new(2); - - state_vec.h(0).cx(0, 1); // Create Bell state - state_vec.reset(); // Reset to |00⟩ - - assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); - for i in 1..state_vec.state.len() { - assert!(state_vec.state[i].norm() < 1e-10); - } - } - - #[test] - fn test_probability() { - let mut state_vec = StateVec::new(1); - - // Prepare |+⟩ state - state_vec.h(0); - - let prob_zero = state_vec.probability(0); - let prob_one = state_vec.probability(1); - - assert!((prob_zero - 0.5).abs() < 1e-10); - assert!((prob_one - 0.5).abs() < 1e-10); - } - - #[test] - fn test_inverse_gates() { - let mut state_vec = StateVec::new(1); - - // Apply Hadamard twice: H * H = I - state_vec.h(0); - state_vec.h(0); - - // Verify state is back to |0⟩ - assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); - assert!((state_vec.probability(1)).abs() < 1e-10); - - // Apply X twice: X * X = I - state_vec.x(0); - state_vec.x(0); - assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); - } - - #[test] - fn test_bell_state_entanglement() { - let mut state_vec = StateVec::new(2); - - // Prepare Bell State: (|00⟩ + |11⟩) / √2 - state_vec.h(0); - state_vec.cx(0, 1); - - let expected_amplitude = 1.0 / 2.0_f64.sqrt(); - - assert!((state_vec.state[0].re - expected_amplitude).abs() < 1e-10); - assert!((state_vec.state[3].re - expected_amplitude).abs() < 1e-10); + // Create random state with Hadamard + q.h(0); - assert!(state_vec.state[1].norm() < 1e-10); - assert!(state_vec.state[2].norm() < 1e-10); - } + // Apply Z gate as unitary + let z00 = Complex64::new(1.0, 0.0); + let z01 = Complex64::new(0.0, 0.0); + let z10 = Complex64::new(0.0, 0.0); + let z11 = Complex64::new(-1.0, 0.0); - #[test] - fn test_measurement_collapse() { - let mut state_vec = StateVec::new(1); + let initial = q.state.clone(); + q.single_qubit_rotation(0, z00, z01, z10, z11); - // Prepare |+⟩ = (|0⟩ + |1⟩) / √2 - state_vec.h(0); + // Check normalization is preserved + let norm: f64 = q.state.iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); - // Simulate a measurement - let result = state_vec.mz(0); + // Apply Z again - should get back original state + q.single_qubit_rotation(0, z00, z01, z10, z11); - // State should collapse to |0⟩ or |1⟩ - if result.outcome { - assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); - } else { - assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + for (a, b) in q.state.iter().zip(initial.iter()) { + assert!((a - b).norm() < 1e-10); } } #[test] - fn test_state_normalization_after_random_gates() { - let mut state_vec = StateVec::new(3); - - // Apply a sequence of random gates - state_vec.h(0); - state_vec.cx(0, 1); - state_vec.rz(std::f64::consts::PI / 3.0, 2); - state_vec.swap(1, 2); - - // Check if the state is still normalized - let norm: f64 = state_vec.state.iter().map(|amp| amp.norm_sqr()).sum(); - assert!((norm - 1.0).abs() < 1e-10); - } - - #[test] - fn test_two_qubit_unitary_identity() { - let mut state_vec = StateVec::new(2); + fn test_two_qubit_unitary_cnot() { + // Test that we can implement CNOT using the general unitary + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); - // Identity matrix - let identity_gate = [ + // CNOT matrix + let cnot = [ [ Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0), @@ -3182,379 +1982,70 @@ mod tests { [ Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), - Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), ], [ - Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), ], ]; - // Apply the identity gate - state_vec.prepare_computational_basis(2); - state_vec.two_qubit_unitary(0, 1, identity_gate); - - // State should remain |10⟩ - assert!((state_vec.probability(2) - 1.0).abs() < 1e-10); - } - - #[test] - fn test_gate_decompositions() { - // Test that composite operations match their decompositions - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - - // Test SWAP decomposition into CNOTs - q1.x(0); // Start with |10⟩ - q1.swap(0, 1); // Direct SWAP - - q2.x(0); // Also start with |10⟩ - q2.cx(0, 1).cx(1, 0).cx(0, 1); // SWAP decomposition - - assert_states_equal(&q1.state, &q2.state); - } - - #[test] - fn test_phase_relationships() { - // Test expected phase relationships between gates - let q = StateVec::new(1); - - // Test that T * T = S - let mut q1 = q.clone(); - q1.t(0).t(0); - - let mut q2 = q.clone(); - q2.sz(0); - - assert_states_equal(&q1.state, &q2.state); - } - - #[test] - fn test_phase_gate_identities() { - // Test S = T^2 - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - - // Put in superposition first to check phases + // Create Bell state using both methods q1.h(0); - q2.h(0); - - q1.sz(0); // S gate - q2.t(0).t(0); // Two T gates - - assert_states_equal(&q1.state, &q2.state); - } - - #[test] - fn test_hadamard_properties() { - // Test H^2 = I - let mut q = StateVec::new(1); - q.x(0); // Start with |1⟩ - let initial = q.state.clone(); - q.h(0).h(0); - assert_states_equal(&q.state, &initial); - - // Test HXH = Z - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - - q1.h(0).x(0).h(0); - q2.z(0); - - assert_states_equal(&q1.state, &q2.state); - } - - #[test] - fn test_controlled_gate_symmetries() { - let mut q1 = StateVec::new(2); - let mut q2 = StateVec::new(2); - - // Test SWAP symmetry - q1.x(0); // |10⟩ - q2.x(0); // |10⟩ - - q1.cx(0, 1).cx(1, 0).cx(0, 1); // SWAP via CNOTs - q2.swap(0, 1); // Direct SWAP - - assert_states_equal(&q1.state, &q2.state); - } - - #[test] - fn test_controlled_gate_phases() { - // Test phase behavior of controlled operations - let mut q = StateVec::new(2); - - // Create superposition with phases - q.h(0).sz(0); - q.h(1).sz(1); - - // Control operations should preserve phases correctly - let initial = q.state.clone(); - q.cz(0, 1).cz(0, 1); // CZ^2 = I - - assert_states_equal(&q.state, &initial); - } - - #[test] - fn test_rotation_composition() { - let mut q1 = StateVec::new(1); - let mut q2 = StateVec::new(1); - - // Test that rotation decompositions work - // RY(θ) = RX(π/2)RZ(θ)RX(-π/2) - q1.ry(FRAC_PI_3, 0); - - q2.rx(FRAC_PI_2, 0).rz(FRAC_PI_3, 0).rx(-FRAC_PI_2, 0); - - assert_states_equal(&q1.state, &q2.state); - } - - #[test] - fn test_rotation_angle_precision() { - let mut q = StateVec::new(1); + q1.cx(0, 1); - // Test small angle rotations - let small_angle = 1e-6; - q.rx(small_angle, 0); + q2.h(0); + q2.two_qubit_unitary(0, 1, cnot); - // Check that probabilities sum to 1 - let total_prob: f64 = q.state.iter().map(|amp| amp.norm_sqr()).sum(); - assert!((total_prob - 1.0).abs() < 1e-10); + // Compare results + for (a, b) in q1.state.iter().zip(q2.state.iter()) { + assert!((a - b).norm() < 1e-10); + } } #[test] - fn test_measurement_properties() { + fn test_two_qubit_unitary_swap() { + // Test SWAP gate let mut q = StateVec::new(2); - // Test 1: Measuring |0⟩ should always give 0 - let result = q.mz(0); - assert!(!result.outcome); - assert!((q.probability(0) - 1.0).abs() < 1e-10); - - // Test 2: Measuring |1⟩ should always give 1 - q.reset(); + // Prepare |10⟩ state q.x(0); - let result = q.mz(0); - assert!(result.outcome); - assert!((q.probability(1) - 1.0).abs() < 1e-10); - - // Test 3: In a Bell state, measurements should correlate - q.reset(); - q.h(0).cx(0, 1); // Create Bell state - let result1 = q.mz(0); - let result2 = q.mz(1); - assert_eq!( - result1.outcome, result2.outcome, - "Bell state measurements should correlate" - ); - - // Test 4: Repeated measurements should be consistent - q.reset(); - q.h(0); // Create superposition - let first = q.mz(0); - let second = q.mz(0); // Measure again - assert_eq!( - first.outcome, second.outcome, - "Repeated measurements should give same result" - ); - } - - #[test] - fn test_measurement_basis_transforms() { - let mut q = StateVec::new(1); - - // |0⟩ in X basis - q.h(0); - - // Measure in Z basis - let result = q.mz(0); - - // Result should be random but state should collapse - let final_prob = if result.outcome { - q.probability(1) - } else { - q.probability(0) - }; - assert!((final_prob - 1.0).abs() < 1e-10); - } - - #[test] - fn test_state_preparation_fidelity() { - let mut q = StateVec::new(2); - // Method 1: H + CNOT - q.h(0).cx(0, 1); - let probs1 = vec![ - q.probability(0), - q.probability(1), - q.probability(2), - q.probability(3), + // SWAP matrix + let swap = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], ]; - // Method 2: Rotations - q.reset(); - q.ry(FRAC_PI_2, 0).cx(0, 1); // Remove rz(PI) since it just adds phase - - // Compare probability distributions - assert!((q.probability(0) - probs1[0]).abs() < 1e-10); - assert!((q.probability(1) - probs1[1]).abs() < 1e-10); - assert!((q.probability(2) - probs1[2]).abs() < 1e-10); - assert!((q.probability(3) - probs1[3]).abs() < 1e-10); - } - - #[test] - fn test_arbitrary_state_preparation() { - let mut q = StateVec::new(1); - - // Try to prepare various single-qubit states - // |+⟩ state - q.h(0); - assert!((q.probability(0) - 0.5).abs() < 1e-10); - assert!((q.probability(1) - 0.5).abs() < 1e-10); - - // |+i⟩ state - q.reset(); - q.h(0).sz(0); - assert!((q.probability(0) - 0.5).abs() < 1e-10); - assert!((q.probability(1) - 0.5).abs() < 1e-10); - } - - #[test] - fn test_numerical_stability() { - let mut q = StateVec::new(4); - - // Apply many rotations to test numerical stability - for _ in 0..100 { - q.rx(FRAC_PI_3, 0) - .ry(FRAC_PI_4, 1) - .rz(FRAC_PI_6, 2) - .cx(0, 3); - } - - // Check normalization is preserved - let total_prob: f64 = q.state.iter().map(|amp| amp.norm_sqr()).sum(); - assert!((total_prob - 1.0).abs() < 1e-8); - } - - #[test] - fn test_ghz_state() { - // Test creating and verifying a GHZ state - let mut q = StateVec::new(3); - q.h(0).cx(0, 1).cx(1, 2); // Create GHZ state - - // Verify properties - let mut norm_squared = 0.0; - for i in 0..8 { - if i == 0 || i == 7 { - // |000⟩ or |111⟩ - norm_squared += q.state[i].norm_sqr(); - assert!((q.state[i].norm() - FRAC_1_SQRT_2).abs() < 1e-10); - } else { - assert!(q.state[i].norm() < 1e-10); - } - } - assert!((norm_squared - 1.0).abs() < 1e-10); - } - - #[test] - fn test_operation_chains() { - // Test complex sequences of operations - let mut q = StateVec::new(2); - - // Create maximally entangled state then disentangle - q.h(0) - .cx(0, 1) // Create Bell state - .cx(0, 1) - .h(0); // Disentangle (apply the same operations in reverse) - - // Should be back to |00⟩ - assert!((q.probability(0) - 1.0).abs() < 1e-10); - } - - #[test] - fn test_phase_coherence() { - let mut q = StateVec::new(1); - - // Apply series of phase rotations that should cancel - q.h(0) // Create superposition - .rz(FRAC_PI_4, 0) - .rz(FRAC_PI_4, 0) - .rz(-FRAC_PI_2, 0); // Should cancel - - // Should be back to |+⟩ - assert!((q.state[0].re - FRAC_1_SQRT_2).abs() < 1e-10); - assert!((q.state[1].re - FRAC_1_SQRT_2).abs() < 1e-10); - assert!(q.state[0].im.abs() < 1e-10); - assert!(q.state[1].im.abs() < 1e-10); - } - - #[test] - fn test_adjacent_vs_distant_qubits() { - let mut q1 = StateVec::new(4); - let mut q2 = StateVec::new(4); - - // Test operations on adjacent vs distant qubits - q1.h(0).cx(0, 1); // Adjacent qubits - q2.h(0).cx(0, 3); // Distant qubits - - // Both should maintain proper normalization - let norm1: f64 = q1.state.iter().map(|amp| amp.norm_sqr()).sum(); - let norm2: f64 = q2.state.iter().map(|amp| amp.norm_sqr()).sum(); - assert!((norm1 - 1.0).abs() < 1e-10); - assert!((norm2 - 1.0).abs() < 1e-10); - } - - #[test] - fn test_state_prep_consistency() { - // First method: direct X gate - let mut q1 = StateVec::new(2); - q1.x(1); // Direct preparation of |01⟩ - - // Verify first preparation - |01⟩ corresponds to binary 10 (decimal 2) - assert!( - (q1.probability(2) - 1.0).abs() < 1e-10, - "First preparation failed" - ); - assert!(q1.probability(0) < 1e-10); - assert!(q1.probability(1) < 1e-10); - assert!(q1.probability(3) < 1e-10); - - // Second method: using two X gates that cancel on qubit 0 - let mut q2 = StateVec::new(2); - q2.x(0).x(1).x(0); // Should give |01⟩ - - // Verify second preparation - |01⟩ corresponds to binary 10 (decimal 2) - assert!( - (q2.probability(2) - 1.0).abs() < 1e-10, - "Second preparation failed" - ); - assert!(q2.probability(0) < 1e-10); - assert!(q2.probability(1) < 1e-10); - assert!(q2.probability(3) < 1e-10); - - // Verify both methods give the same state - assert_states_equal(&q1.state, &q2.state); - } - - #[test] - fn test_rotation_arithmetic() { - let q = StateVec::new(1); - - // Test that RY(θ₁)RY(θ₂) = RY(θ₁ + θ₂) when commuting - let theta1 = FRAC_PI_3; - let theta2 = FRAC_PI_6; - - // Method 1: Two separate rotations - let mut q1 = q.clone(); - q1.ry(theta1, 0).ry(theta2, 0); - - // Method 2: Combined rotation - let mut q2 = q.clone(); - q2.ry(theta1 + theta2, 0); + q.two_qubit_unitary(0, 1, swap); - assert_states_equal(&q1.state, &q2.state); + // Should be in |01⟩ state + assert!(q.state[0].norm() < 1e-10); + assert!((q.state[2].re - 1.0).abs() < 1e-10); } } diff --git a/crates/pecos-qsim/tests/helpers.rs b/crates/pecos-qsim/tests/helpers.rs new file mode 100644 index 00000000..09529b7c --- /dev/null +++ b/crates/pecos-qsim/tests/helpers.rs @@ -0,0 +1,46 @@ +use num_complex::Complex64; + +/// Compare two quantum states up to global phase. +/// +/// This function ensures that two state vectors represent the same quantum state, +/// accounting for potential differences in global phase. +/// +/// # Arguments +/// - `state1`: A reference to the first state vector. +/// - `state2`: A reference to the second state vector. +/// +/// # Panics +/// The function will panic if the states differ in norm or relative phase beyond a small numerical tolerance. +#[allow(dead_code)] +pub fn assert_states_equal(state1: &[Complex64], state2: &[Complex64]) { + const TOLERANCE: f64 = 1e-10; + + if state1[0].norm() < TOLERANCE && state2[0].norm() < TOLERANCE { + // Both first components near zero, compare other components directly + for (index, (a, b)) in state1.iter().zip(state2.iter()).enumerate() { + assert!( + (a.norm() - b.norm()).abs() < TOLERANCE, + "States differ in magnitude at index {index}: {a} vs {b}" + ); + } + } else { + // Get phase from the first pair of non-zero components + let ratio = match state1 + .iter() + .zip(state2.iter()) + .find(|(a, b)| a.norm() > TOLERANCE && b.norm() > TOLERANCE) + { + Some((a, b)) => b / a, + None => panic!("States have no corresponding non-zero components"), + }; + println!("Phase ratio between states: {ratio:?}"); + + for (index, (a, b)) in state1.iter().zip(state2.iter()).enumerate() { + assert!( + (a * ratio - b).norm() < TOLERANCE, + "States differ at index {index}: {a} vs {b} (adjusted with ratio {ratio:?}), diff = {}", + (a * ratio - b).norm() + ); + } + } +} diff --git a/crates/pecos-qsim/tests/test_state_vec.rs b/crates/pecos-qsim/tests/test_state_vec.rs new file mode 100644 index 00000000..a438bb7f --- /dev/null +++ b/crates/pecos-qsim/tests/test_state_vec.rs @@ -0,0 +1,1622 @@ +mod helpers; + +mod advanced_gates { + use crate::helpers::assert_states_equal; + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; + use std::f64::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, PI}; + + #[test] + fn test_rotation_composition() { + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + // Test that rotation decompositions work + // RY(θ) = RX(π/2)RZ(θ)RX(-π/2) + q1.ry(FRAC_PI_3, 0); + + q2.rx(FRAC_PI_2, 0).rz(FRAC_PI_3, 0).rx(-FRAC_PI_2, 0); + + assert_states_equal(q1.state(), q2.state()); + } + + // TODO: add + #[test] + fn test_rotation_angle_relations() {} + + #[test] + fn test_rotation_arithmetic() { + let q = StateVec::new(1); + + // Test that RY(θ₁)RY(θ₂) = RY(θ₁ + θ₂) when commuting + let theta1 = FRAC_PI_3; + let theta2 = FRAC_PI_6; + + // Method 1: Two separate rotations + let mut q1 = q.clone(); + q1.ry(theta1, 0).ry(theta2, 0); + + // Method 2: Combined rotation + let mut q2 = q.clone(); + q2.ry(theta1 + theta2, 0); + + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_rotation_symmetries() { + // Test that all rotations are symmetric under exchange of qubits + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Prepare same non-trivial initial state + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); + + let theta = PI / 3.0; + + // Test RYY symmetry + q1.ryy(theta, 0, 1); + q2.ryy(theta, 1, 0); + + for (a, b) in q1.state().iter().zip(q2.state().iter()) { + assert!((a - b).norm() < 1e-10); + } + + // Test RZZ symmetry + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); + + q1.rzz(theta, 0, 1); + q2.rzz(theta, 1, 0); + + for (a, b) in q1.state().iter().zip(q2.state().iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_sq_rotation_commutation() { + // RX and RY don't commute - verify RX(θ)RY(φ) ≠ RY(φ)RX(θ) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + let theta = FRAC_PI_3; // π/3 + let phi = FRAC_PI_4; // π/4 + + // Apply in different orders + q1.rx(theta, 0).ry(phi, 0); + q2.ry(phi, 0).rx(theta, 0); + + println!("RY(π/4)RX(π/3)|0⟩ = {:?}", q1.state()); + println!("RX(π/3)RY(π/4)|0⟩ = {:?}", q2.state()); + + // States should be different - check they're not equal up to global phase + let ratio = q2.state()[0] / q1.state()[0]; + assert!((q2.state()[1] / q1.state()[1] - ratio).norm() > 1e-10); + } + + #[test] + fn test_sq_rotation_decompositions() { + // H = RZ(-π)RY(-π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + println!("Initial states:"); + println!("q1 = {:?}", q1.state()); + println!("q2 = {:?}", q2.state()); + + q1.h(0); // Direct H + println!("After H: q1 = {:?}", q1.state()); + + // H via rotations - changed order and added negative sign to RZ angle + q2.ry(-FRAC_PI_2, 0).rz(-PI, 0); + println!("After RZ(-π)RY(-π/2): q2 = {:?}", q2.state()); + + // Compare up to global phase by looking at ratios between components + let ratio = q2.state()[0] / q1.state()[0]; + println!("Ratio = {ratio:?}"); + for (a, b) in q1.state().iter().zip(q2.state().iter()) { + println!("Comparing {a} and {b}"); + assert!( + (a * ratio - b).norm() < 1e-10, + "States differ: {a} vs {b} (ratio: {ratio})" + ); + } + } +} + +mod quantum_states { + use crate::helpers::assert_states_equal; + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, QuantumSimulator, StateVec}; + use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2}; + + #[test] + fn test_bell_state_entanglement() { + let mut state_vec = StateVec::new(2); + + // Prepare Bell State: (|00⟩ + |11⟩) / √2 + state_vec.h(0); + state_vec.cx(0, 1); + + let expected_amplitude = 1.0 / 2.0_f64.sqrt(); + + assert!((state_vec.state()[0].re - expected_amplitude).abs() < 1e-10); + assert!((state_vec.state()[3].re - expected_amplitude).abs() < 1e-10); + + assert!(state_vec.state()[1].norm() < 1e-10); + assert!(state_vec.state()[2].norm() < 1e-10); + } + #[test] + fn test_ghz_state() { + // Test creating and verifying a GHZ state + let mut q = StateVec::new(3); + q.h(0).cx(0, 1).cx(1, 2); // Create GHZ state + + // Verify properties + let mut norm_squared = 0.0; + for i in 0..8 { + if i == 0 || i == 7 { + // |000⟩ or |111⟩ + norm_squared += q.state()[i].norm_sqr(); + assert!((q.state()[i].norm() - FRAC_1_SQRT_2).abs() < 1e-10); + } else { + assert!(q.state()[i].norm() < 1e-10); + } + } + assert!((norm_squared - 1.0).abs() < 1e-10); + } + #[test] + fn test_state_preparation_fidelity() { + let mut q = StateVec::new(2); + + // Method 1: H + CNOT + q.h(0).cx(0, 1); + let probs1 = [ + q.probability(0), + q.probability(1), + q.probability(2), + q.probability(3), + ]; + + // Method 2: Rotations + q.reset(); + q.ry(FRAC_PI_2, 0).cx(0, 1); // Remove rz(PI) since it just adds phase + + // Compare probability distributions + assert!((q.probability(0) - probs1[0]).abs() < 1e-10); + assert!((q.probability(1) - probs1[1]).abs() < 1e-10); + assert!((q.probability(2) - probs1[2]).abs() < 1e-10); + assert!((q.probability(3) - probs1[3]).abs() < 1e-10); + } + + #[test] + fn test_state_prep_consistency() { + // First method: direct X gate + let mut q1 = StateVec::new(2); + q1.x(1); // Direct preparation of |01⟩ + + // Verify first preparation - |01⟩ corresponds to binary 10 (decimal 2) + assert!( + (q1.probability(2) - 1.0).abs() < 1e-10, + "First preparation failed" + ); + assert!(q1.probability(0) < 1e-10); + assert!(q1.probability(1) < 1e-10); + assert!(q1.probability(3) < 1e-10); + + // Second method: using two X gates that cancel on qubit 0 + let mut q2 = StateVec::new(2); + q2.x(0).x(1).x(0); // Should give |01⟩ + + // Verify second preparation - |01⟩ corresponds to binary 10 (decimal 2) + assert!( + (q2.probability(2) - 1.0).abs() < 1e-10, + "Second preparation failed" + ); + assert!(q2.probability(0) < 1e-10); + assert!(q2.probability(1) < 1e-10); + assert!(q2.probability(3) < 1e-10); + + // Verify both methods give the same state + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_arbitrary_state_preparation() { + let mut q = StateVec::new(1); + + // Try to prepare various single-qubit states + // |+⟩ state + q.h(0); + assert!((q.probability(0) - 0.5).abs() < 1e-10); + assert!((q.probability(1) - 0.5).abs() < 1e-10); + + // |+i⟩ state + q.reset(); + q.h(0).sz(0); + assert!((q.probability(0) - 0.5).abs() < 1e-10); + assert!((q.probability(1) - 0.5).abs() < 1e-10); + } +} + +mod gate_sequences { + use crate::helpers::assert_states_equal; + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; + + #[test] + fn test_operation_chains() { + // Test complex sequences of operations + let mut q = StateVec::new(2); + + // Create maximally entangled state then disentangle + q.h(0) + .cx(0, 1) // Create Bell state + .cx(0, 1) + .h(0); // Disentangle (apply the same operations in reverse) + + // Should be back to |00⟩ + assert!((q.probability(0) - 1.0).abs() < 1e-10); + } + + #[test] + fn test_inverse_gates() { + let mut state_vec = StateVec::new(1); + + // Apply Hadamard twice: H * H = I + state_vec.h(0); + state_vec.h(0); + + // Verify state is back to |0⟩ + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + assert!((state_vec.probability(1)).abs() < 1e-10); + + // Apply X twice: X * X = I + state_vec.x(0); + state_vec.x(0); + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + } + + #[test] + fn test_phase_gate_identities() { + // Test S = T^2 + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + // Put in superposition first to check phases + q1.h(0); + q2.h(0); + + q1.sz(0); // S gate + q2.t(0).t(0); // Two T gates + + assert_states_equal(q1.state(), q2.state()); + } + #[test] + fn test_gate_decompositions() { + // Test that composite operations match their decompositions + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Test SWAP decomposition into CNOTs + q1.x(0); // Start with |10⟩ + q1.swap(0, 1); // Direct SWAP + + q2.x(0); // Also start with |10⟩ + q2.cx(0, 1).cx(1, 0).cx(0, 1); // SWAP decomposition + + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_bell_state_preparation() { + let mut q = StateVec::new(2); + q.h(0).cx(0, 1); + assert!((q.probability(0) - 0.5).abs() < 1e-10); + assert!((q.probability(3) - 0.5).abs() < 1e-10); + } + + #[test] + fn test_ghz_state_preparation() { + let mut q = StateVec::new(3); + q.h(0).cx(0, 1).cx(1, 2); + assert!((q.probability(0) - 0.5).abs() < 1e-10); + assert!((q.probability(7) - 0.5).abs() < 1e-10); + } +} + +mod numerical_properties { + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; + use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6}; + + #[test] + fn test_state_normalization() { + let mut state_vec = StateVec::new(3); + + // Apply multiple gates + state_vec.h(0); + state_vec.cx(0, 1); + state_vec.cx(1, 2); + + // Verify normalization + let norm: f64 = state_vec + .state() + .iter() + .map(num_complex::Complex::norm_sqr) + .sum(); + assert!((norm - 1.0).abs() < 1e-10); + } + + #[test] + fn test_numerical_stability() { + let mut q = StateVec::new(4); + + // Apply many rotations to test numerical stability + for _ in 0..100 { + q.rx(FRAC_PI_3, 0) + .ry(FRAC_PI_4, 1) + .rz(FRAC_PI_6, 2) + .cx(0, 3); + } + + // Check normalization is preserved + let total_prob: f64 = q.state().iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((total_prob - 1.0).abs() < 1e-8); + } + + #[test] + fn test_phase_coherence() { + let mut q = StateVec::new(1); + + // Apply series of phase rotations that should cancel + q.h(0) // Create superposition + .rz(FRAC_PI_4, 0) + .rz(FRAC_PI_4, 0) + .rz(-FRAC_PI_2, 0); // Should cancel + + // Should be back to |+⟩ + assert!((q.state()[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state()[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!(q.state()[0].im.abs() < 1e-10); + assert!(q.state()[1].im.abs() < 1e-10); + } + + #[test] + fn test_bit_indexing() { + let mut q = StateVec::new(3); + + println!("Initial state (|000⟩):"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state()[i]); + } + + // Put |+⟩ on qubit 0 (LSB) + q.h(0); + + println!("\nAfter H on qubit 0:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state()[i]); + } + + // Check state is |+⟩|0⟩|0⟩ + // Only indices that differ in LSB (qubit 0) should be FRAC_1_SQRT_2 + for i in 0..8 { + let qubit0 = i & 1; + let qubit1 = (i >> 1) & 1; + let qubit2 = (i >> 2) & 1; + + let expected = if qubit1 == 0 && qubit2 == 0 { + FRAC_1_SQRT_2 + } else { + 0.0 + }; + + if (q.state()[i].re - expected).abs() >= 1e-10 { + println!("\nMismatch at index {i}: {i:03b}"); + println!("Qubit values: q2={qubit2}, q1={qubit1}, q0={qubit0}"); + println!("Expected {}, got {}", expected, q.state()[i].re); + } + assert!((q.state()[i].re - expected).abs() < 1e-10); + } + } +} + +mod locality_tests { + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; + use std::f64::consts::{FRAC_1_SQRT_2, PI}; + + #[test] + fn test_single_qubit_locality() { + // Test on 3 qubit system that gates only affect their target + let mut q = StateVec::new(3); + + // Prepare state |+⟩|0⟩|0⟩ + q.h(0); // Affects least significant bit + + // Apply X to qubit 2 (most significant bit) + q.x(2); + + // Check that qubit 0 is still in |+⟩ state + // When qubit 2 is |1⟩, check LSB still shows |+⟩ + assert!((q.state()[4].re - FRAC_1_SQRT_2).abs() < 1e-10); // |100⟩ + assert!((q.state()[5].re - FRAC_1_SQRT_2).abs() < 1e-10); // |101⟩ + } + + #[test] + fn test_two_qubit_locality() { + let mut q = StateVec::new(4); + + println!("Initial state:"); + for i in 0..16 { + println!(" {:04b}: {:.3}", i, q.state()[i]); + } + + // Prepare |+⟩ on qubit 0 (LSB) + q.h(0); + + println!("\nAfter H on qubit 0:"); + for i in 0..16 { + println!(" {:04b}: {:.3}", i, q.state()[i]); + } + + // Apply CX between qubits 2,3 + q.cx(2, 3); + + println!("\nAfter CX on qubits 2,3:"); + for i in 0..16 { + println!(" {:04b}: {:.3}", i, q.state()[i]); + + // Extract qubit values + // let _q0 = i & 1; + let q1 = (i >> 1) & 1; + let q2 = (i >> 2) & 1; + let q3 = (i >> 3) & 1; + + // Only states with q0=0 or q0=1 and q1=q2=q3=0 should have amplitude + let expected = if q1 == 0 && q2 == 0 && q3 == 0 { + FRAC_1_SQRT_2 + } else { + 0.0 + }; + + if (q.state()[i].re - expected).abs() >= 1e-10 { + println!("Mismatch at {i:04b}"); + println!("Expected {}, got {}", expected, q.state()[i].re); + } + assert!((q.state()[i].re - expected).abs() < 1e-10); + } + } + + #[test] + fn test_two_qubit_gate_locality() { + let mut q = StateVec::new(3); + + // Prepare state |+⟩|0⟩|0⟩ + q.h(0); + + // Apply CX on qubits 1 and 2 (no effect on qubit 0) + q.cx(1, 2); + + // Qubit 0 should remain in superposition + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state()[0].re - expected_amp).abs() < 1e-10); + assert!((q.state()[1].re - expected_amp).abs() < 1e-10); + } + + #[test] + fn test_rotation_locality() { + let mut q = StateVec::new(3); + + println!("Initial state:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state()[i]); + } + + // Prepare |+⟩ on qubit 0 (LSB) + q.h(0); + + println!("\nAfter H on qubit 0:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state()[i]); + } + + // Apply rotation to qubit 1 + q.rx(PI / 2.0, 1); + + println!("\nAfter RX on qubit 1:"); + for i in 0..8 { + println!(" {:03b}: {:.3}", i, q.state()[i]); + } + + // Check each basis state contribution + for i in 0..8 { + let expected = FRAC_1_SQRT_2; + if (q.state()[i].norm() - expected).abs() >= 1e-10 { + println!("\nMismatch at index {i}: {i:03b}"); + println!("Expected norm {}, got {}", expected, q.state()[i].norm()); + } + } + } + + #[test] + fn test_adjacent_vs_distant_qubits() { + let mut q1 = StateVec::new(4); + let mut q2 = StateVec::new(4); + + // Test operations on adjacent vs distant qubits + q1.h(0).cx(0, 1); // Adjacent qubits + q2.h(0).cx(0, 3); // Distant qubits + + // Both should maintain proper normalization + let norm1: f64 = q1.state().iter().map(num_complex::Complex::norm_sqr).sum(); + let norm2: f64 = q2.state().iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm1 - 1.0).abs() < 1e-10); + assert!((norm2 - 1.0).abs() < 1e-10); + } +} + +// Edge cases and numerical stability +mod edge_cases { + use crate::helpers::assert_states_equal; + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; + use std::f64::consts::PI; + + #[test] + fn test_small_angle_rotations() { + let mut q = StateVec::new(1); + let small_angle = 1e-6; + q.rx(small_angle, 0); + let total_prob: f64 = q.state().iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((total_prob - 1.0).abs() < 1e-10); + } + + #[test] + fn test_repeated_operations() { + let mut q = StateVec::new(1); + for _ in 0..1000 { + q.h(0).sz(0).h(0); + } + let norm: f64 = q.state().iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-8); + } + + #[test] + fn test_rotation_angle_precision() { + let mut q = StateVec::new(1); + + // Test small angle rotations + let small_angle = 1e-6; + q.rx(small_angle, 0); + + // Check that probabilities sum to 1 + let total_prob: f64 = q.state().iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((total_prob - 1.0).abs() < 1e-10); + } + + #[test] + fn test_sq_rotation_edge_cases() { + let mut q = StateVec::new(1); + + // Test RX(0): Should be identity + let initial = q.state().to_vec(); + q.rx(0.0, 0); + assert_states_equal(q.state(), &initial); + + // Test RX(2π): Should also be identity up to global phase + q.rx(2.0 * PI, 0); + assert_states_equal(q.state(), &initial); + + // Test RY(0): Should be identity + q.ry(0.0, 0); + assert_states_equal(q.state(), &initial); + + // Test RY(2π): Should also be identity up to global phase + q.ry(2.0 * PI, 0); + assert_states_equal(q.state(), &initial); + + // Test RZ(0): Should be identity + q.rz(0.0, 0); + assert_states_equal(q.state(), &initial); + + // Test RZ(2π): Should also be identity up to global phase + q.rz(2.0 * PI, 0); + assert_states_equal(q.state(), &initial); + } +} + +mod large_systems { + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; + + #[test] + fn test_large_system() { + // Test with a large number of qubits to ensure robustness. + let num_qubits = 20; // 20 qubits => 2^20 amplitudes (~1M complex numbers) + let mut q = StateVec::new(num_qubits); + + // Apply Hadamard to the first qubit + q.h(0); + + // Check normalization and amplitudes for |0...0> and |1...0> + let expected_amp = 1.0 / (2.0_f64.sqrt()); + assert!((q.state()[0].norm() - expected_amp).abs() < 1e-10); + assert!((q.state()[1].norm() - expected_amp).abs() < 1e-10); + + // Ensure all other amplitudes remain zero + for i in 2..q.state().len() { + assert!(q.state()[i].norm() < 1e-10); + } + } + + #[test] + fn test_state_normalization_after_random_gates() { + let mut state_vec = StateVec::new(3); + + // Apply a sequence of random gates + state_vec.h(0); + state_vec.cx(0, 1); + state_vec.rz(std::f64::consts::PI / 3.0, 2); + state_vec.swap(1, 2); + + // Check if the state is still normalized + let norm: f64 = state_vec + .state() + .iter() + .map(num_complex::Complex::norm_sqr) + .sum(); + assert!((norm - 1.0).abs() < 1e-10); + } +} + +mod detailed_sq_gate_cases { + use crate::helpers::assert_states_equal; + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, QuantumSimulator, StateVec}; + use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, PI}; + + #[test] + fn test_rx_step_by_step() { + let mut q = StateVec::new(1); + + // Step 1: RX(0) should be identity + q.rx(0.0, 0); + assert!((q.state()[0].re - 1.0).abs() < 1e-10); + assert!(q.state()[1].norm() < 1e-10); + + // Step 2: RX(π) on |0⟩ should give -i|1⟩ + let mut q = StateVec::new(1); + q.rx(PI, 0); + println!("RX(π)|0⟩ = {:?}", q.state()); // Debug output + assert!(q.state()[0].norm() < 1e-10); + assert!((q.state()[1].im + 1.0).abs() < 1e-10); + + // Step 3: RX(π/2) on |0⟩ should give (|0⟩ - i|1⟩)/√2 + let mut q = StateVec::new(1); + q.rx(FRAC_PI_2, 0); + println!("RX(π/2)|0⟩ = {:?}", q.state()); // Debug output + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state()[0].re - expected_amp).abs() < 1e-10); + assert!((q.state()[1].im + expected_amp).abs() < 1e-10); + } + + #[test] + fn test_ry_step_by_step() { + // Step 1: RY(0) should be identity + let mut q = StateVec::new(1); + q.ry(0.0, 0); + println!("RY(0)|0⟩ = {:?}", q.state()); + assert!((q.state()[0].re - 1.0).abs() < 1e-10); + assert!(q.state()[1].norm() < 1e-10); + + // Step 2: RY(π) on |0⟩ should give |1⟩ + let mut q = StateVec::new(1); + q.ry(PI, 0); + println!("RY(π)|0⟩ = {:?}", q.state()); + assert!(q.state()[0].norm() < 1e-10); + assert!((q.state()[1].re - 1.0).abs() < 1e-10); + + // Step 3: RY(π/2) on |0⟩ should give (|0⟩ + |1⟩)/√2 + let mut q = StateVec::new(1); + q.ry(FRAC_PI_2, 0); + println!("RY(π/2)|0⟩ = {:?}", q.state()); + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state()[0].re - expected_amp).abs() < 1e-10); + assert!((q.state()[1].re - expected_amp).abs() < 1e-10); + + // Step 4: RY(-π/2) on |0⟩ should give (|0⟩ - |1⟩)/√2 + let mut q = StateVec::new(1); + q.ry(-FRAC_PI_2, 0); + println!("RY(-π/2)|0⟩ = {:?}", q.state()); + assert!((q.state()[0].re - expected_amp).abs() < 1e-10); + assert!((q.state()[1].re + expected_amp).abs() < 1e-10); + } + + #[test] + fn test_rz_step_by_step() { + // Step 1: RZ(0) should be identity + let mut q = StateVec::new(1); + q.rz(0.0, 0); + println!("RZ(0)|0⟩ = {:?}", q.state()); + assert!((q.state()[0].re - 1.0).abs() < 1e-10); + assert!(q.state()[1].norm() < 1e-10); + + // Step 2: RZ(π/2) on |+⟩ should give |+i⟩ = (|0⟩ + i|1⟩)/√2 + let mut q = StateVec::new(1); + q.h(0); // Create |+⟩ + q.rz(FRAC_PI_2, 0); + println!("RZ(π/2)|+⟩ = {:?}", q.state()); + let expected_amp = 1.0 / 2.0_f64.sqrt(); + assert!((q.state()[0].norm() - expected_amp).abs() < 1e-10); + assert!((q.state()[1].norm() - expected_amp).abs() < 1e-10); + // Check relative phase + let ratio = q.state()[1] / q.state()[0]; + println!("Relative phase ratio = {ratio:?}"); + assert!( + (ratio.im - 1.0).abs() < 1e-10, + "Relative phase incorrect: ratio = {ratio}" + ); + assert!( + ratio.re.abs() < 1e-10, + "Relative phase has unexpected real component: {}", + ratio.re + ); + + // Step 3: Two RZ(π/2) operations should equal one RZ(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.rz(PI, 0); + q2.rz(FRAC_PI_2, 0); + q2.rz(FRAC_PI_2, 0); + println!("RZ(π)|0⟩ vs RZ(π/2)RZ(π/2)|0⟩:"); + println!("q1 = {:?}", q1.state()); + println!("q2 = {:?}", q2.state()); + let ratio = q2.state()[0] / q1.state()[0]; + let phase = ratio.arg(); + println!("Phase difference between q2 and q1: {phase}"); + assert!( + (ratio.norm() - 1.0).abs() < 1e-10, + "Magnitudes differ: ratio = {ratio}" + ); + // Don't check exact phase, just verify states are equal up to global phase + assert!((q2.state()[1] * q1.state()[0] - q2.state()[0] * q1.state()[1]).norm() < 1e-10); + } + + #[test] + fn test_sq_standard_gate_decompositions() { + // Test S = RZ(π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.sz(0); + q2.rz(FRAC_PI_2, 0); + println!("S|0⟩ = {:?}", q1.state()); + println!("RZ(π/2)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + + // Test X = RX(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.x(0); + q2.rx(PI, 0); + println!("X|0⟩ = {:?}", q1.state()); + println!("RX(π)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + + // Test Y = RY(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.y(0); + q2.ry(PI, 0); + println!("Y|0⟩ = {:?}", q1.state()); + println!("RY(π)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + + // Test Z = RZ(π) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.z(0); + q2.rz(PI, 0); + println!("Z|0⟩ = {:?}", q1.state()); + println!("RZ(π)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + + // Test √X = RX(π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.sx(0); + q2.rx(FRAC_PI_2, 0); + println!("√X|0⟩ = {:?}", q1.state()); + println!("RX(π/2)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + + // Test √Y = RY(π/2) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.sy(0); + q2.ry(FRAC_PI_2, 0); + println!("√Y|0⟩ = {:?}", q1.state()); + println!("RY(π/2)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + + // Test S = TT as RZ(π/4)RZ(π/4) + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q2.rz(FRAC_PI_4, 0).rz(FRAC_PI_4, 0); + q1.sz(0); + println!("S|0⟩ = {:?}", q1.state()); + println!("T²|0⟩ = RZ(π/4)RZ(π/4)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + + // Test H = RX(π)RY(π/2) decomposition + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + q1.h(0); + q2.ry(FRAC_PI_2, 0).rx(PI, 0); + println!("H|0⟩ = {:?}", q1.state()); + println!("RX(π)RY(π/2)|0⟩ = {:?}", q2.state()); + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_rx_rotation_angle_relations() { + // Test that RX(θ)RX(-θ) = I + let mut q = StateVec::new(1); + let theta = FRAC_PI_3; + + // Apply forward then reverse rotations + q.rx(theta, 0).rx(-theta, 0); + + // Should get back to |0⟩ up to global phase + assert!(q.state()[1].norm() < 1e-10); + assert!((q.state()[0].norm() - 1.0).abs() < 1e-10); + } + + #[test] + fn test_ry_rotation_angle_relations() { + // Test that RY(θ)RY(-θ) = I + let mut q = StateVec::new(1); + let theta = FRAC_PI_3; + + // Apply forward then reverse rotations + q.ry(theta, 0).ry(-theta, 0); + + // Should get back to |0⟩ up to global phase + assert!(q.state()[1].norm() < 1e-10); + assert!((q.state()[0].norm() - 1.0).abs() < 1e-10); + } + + #[test] + fn test_rz_rotation_angle_relations() { + // Test that RZ(θ)RZ(-θ) = I + let mut q = StateVec::new(1); + let theta = FRAC_PI_3; + + // Apply forward then reverse rotations + q.rz(theta, 0).rz(-theta, 0); + + // Should get back to |0⟩ up to global phase + assert!(q.state()[1].norm() < 1e-10); + assert!((q.state()[0].norm() - 1.0).abs() < 1e-10); + } + + #[test] + fn test_state_vec_u_vs_trait_u() { + // Initialize state vectors with one qubit in the |0⟩ state. + let mut state_vec_u = StateVec::new(1); + let mut trait_u = StateVec::new(1); + + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; + let lambda = FRAC_PI_6; + + // Apply `u` from the StateVec implementation. + state_vec_u.u(theta, phi, lambda, 0); + + // Apply `u` from the ArbitraryRotationGateable trait. + ArbitraryRotationGateable::u(&mut trait_u, theta, phi, lambda, 0); + + assert_states_equal(state_vec_u.state(), trait_u.state()); + } + + #[test] + fn test_r1xy_vs_u() { + let mut state_r1xy = StateVec::new(1); + let mut state_u = StateVec::new(1); + + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; + + // Apply r1xy and equivalent u gates + state_r1xy.r1xy(theta, phi, 0); + state_u.u(theta, phi - FRAC_PI_2, FRAC_PI_2 - phi, 0); + + assert_states_equal(state_r1xy.state(), state_u.state()); + } + + #[test] + fn test_rz_vs_u() { + let mut state_rz = StateVec::new(1); + let mut state_u = StateVec::new(1); + + let theta = FRAC_PI_3; + + // Apply rz and u gates + state_rz.rz(theta, 0); + state_u.u(0.0, 0.0, theta, 0); + + assert_states_equal(state_rz.state(), state_u.state()); + } + + #[test] + fn test_u_decomposition() { + let mut state_u = StateVec::new(1); + let mut state_decomposed = StateVec::new(1); + + let theta = FRAC_PI_3; + let phi = FRAC_PI_4; + let lambda = FRAC_PI_6; + + // Apply U gate + state_u.u(theta, phi, lambda, 0); + + // Apply the decomposed gates + state_decomposed.rz(lambda, 0); + state_decomposed.r1xy(theta, FRAC_PI_2, 0); + state_decomposed.rz(phi, 0); + + // Assert that the states are equal + assert_states_equal(state_u.state(), state_decomposed.state()); + } + + #[test] + fn test_x_vs_r1xy() { + let mut state = StateVec::new(1); + state.x(0); + let state_after_x = state.clone(); + + state.reset(); + state.r1xy(PI, 0.0, 0); + let state_after_r1xy = state.clone(); + + assert_states_equal(state_after_x.state(), state_after_r1xy.state()); + } + + #[test] + fn test_y_vs_r1xy() { + let mut state = StateVec::new(1); + state.y(0); + let state_after_y = state.clone(); + + state.reset(); + state.r1xy(PI, FRAC_PI_2, 0); + let state_after_r1xy = state.clone(); + + assert_states_equal(state_after_y.state(), state_after_r1xy.state()); + } + + #[test] + fn test_h_vs_r1xy_rz() { + let mut state = StateVec::new(1); + state.h(0); // Apply the H gate + let state_after_h = state.clone(); + + state.reset(); // Reset state to |0⟩ + state.r1xy(FRAC_PI_2, -FRAC_PI_2, 0).rz(PI, 0); + let state_after_r1xy_rz = state.clone(); + + assert_states_equal(state_after_h.state(), state_after_r1xy_rz.state()); + } + + #[test] + fn test_u_special_cases() { + // Test 1: U(π, 0, π) should be X gate + let mut q = StateVec::new(1); + q.u(PI, 0.0, PI, 0); + assert!(q.state()[0].norm() < 1e-10); + assert!((q.state()[1].re - 1.0).abs() < 1e-10); + + // Test 2: Hadamard gate + // H = U(π/2, 0, π) + let mut q = StateVec::new(1); + q.u(PI / 2.0, 0.0, PI, 0); + assert!((q.state()[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state()[1].re - FRAC_1_SQRT_2).abs() < 1e-10); + + // Test 3: U(0, 0, π) should be Z gate + let mut q = StateVec::new(1); + q.h(0); // First put in superposition + let initial = q.state().to_vec(); + q.u(0.0, 0.0, PI, 0); + assert!((q.state()[0] - initial[0]).norm() < 1e-10); + assert!((q.state()[1] + initial[1]).norm() < 1e-10); + + // Additional test: U3(π/2, π/2, -π/2) should be S†H + let mut q = StateVec::new(1); + q.u(PI / 2.0, PI / 2.0, -PI / 2.0, 0); + // This creates the state (|0⟩ + i|1⟩)/√2 + assert!((q.state()[0].re - FRAC_1_SQRT_2).abs() < 1e-10); + assert!((q.state()[1].im - FRAC_1_SQRT_2).abs() < 1e-10); + } + + #[test] + fn test_u_composition() { + let mut q1 = StateVec::new(1); + let q2 = StateVec::new(1); + + // Two U gates that should multiply to identity + q1.u(PI / 3.0, PI / 4.0, PI / 6.0, 0); + q1.u(-PI / 3.0, -PI / 6.0, -PI / 4.0, 0); + + // Compare with initial state + for (a, b) in q1.state().iter().zip(q2.state().iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_phase_relationships() { + // Test expected phase relationships between gates + let q = StateVec::new(1); + + // Test that T * T = S + let mut q1 = q.clone(); + q1.t(0).t(0); + + let mut q2 = q.clone(); + q2.sz(0); + + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_hadamard_properties() { + // Test H^2 = I + let mut q = StateVec::new(1); + q.x(0); // Start with |1⟩ + let initial = q.state().to_vec(); + q.h(0).h(0); + assert_states_equal(q.state(), &initial); + + // Test HXH = Z + let mut q1 = StateVec::new(1); + let mut q2 = StateVec::new(1); + + q1.h(0).x(0).h(0); + q2.z(0); + + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_non_commuting_gates() { + let mut state1 = StateVec::new(1); + let mut state2 = StateVec::new(1); + + state1.h(0); + state1.z(0); + + state2.z(0); + state2.h(0); + + // Compute the global norm difference + let diff_norm: f64 = state1 + .state() + .iter() + .zip(state2.state().iter()) + .map(|(a, b)| (a - b).norm_sqr()) + .sum::() + .sqrt(); + + assert!(diff_norm > 1e-10, "H and Z should not commute."); + } +} + +mod detailed_tq_gate_cases { + use crate::helpers::assert_states_equal; + use num_complex::Complex64; + use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; + use std::f64::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, PI}; + + #[test] + fn test_cx_decomposition() { + let mut state_cx = StateVec::new(2); + let mut state_decomposed = StateVec::new(2); + + let control = 0; + let target = 1; + + // Apply CX gate + state_cx.cx(control, target); + + // Apply the decomposed gates + state_decomposed.r1xy(-FRAC_PI_2, FRAC_PI_2, target); + state_decomposed.rzz(FRAC_PI_2, control, target); + state_decomposed.rz(-FRAC_PI_2, control); + state_decomposed.r1xy(FRAC_PI_2, PI, target); + state_decomposed.rz(-FRAC_PI_2, target); + + // Assert that the states are equal + assert_states_equal(state_cx.state(), state_decomposed.state()); + } + + #[test] + fn test_rxx_decomposition() { + let mut state_rxx = StateVec::new(2); + let mut state_decomposed = StateVec::new(2); + + let control = 0; + let target = 1; + + // Apply RXX gate + state_rxx.rxx(FRAC_PI_4, control, target); + + // Apply the decomposed gates + state_decomposed.r1xy(FRAC_PI_2, FRAC_PI_2, control); + state_decomposed.r1xy(FRAC_PI_2, FRAC_PI_2, target); + state_decomposed.rzz(FRAC_PI_4, control, target); + state_decomposed.r1xy(FRAC_PI_2, -FRAC_PI_2, control); + state_decomposed.r1xy(FRAC_PI_2, -FRAC_PI_2, target); + + // Assert that the states are equal + assert_states_equal(state_rxx.state(), state_decomposed.state()); + } + + #[test] + fn test_two_qubit_unitary_swap_simple() { + let mut state_vec = StateVec::new(2); + + let swap_gate = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + ]; + + state_vec.prepare_computational_basis(2); // |10⟩ + state_vec.two_qubit_unitary(1, 0, swap_gate); + + assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); // Should now be |01⟩ + } + + #[test] + fn test_cx_all_basis_states() { + let mut state_vec = StateVec::new(2); + + // |00⟩ → should remain |00⟩ + state_vec.prepare_computational_basis(0); + state_vec.cx(1, 0); + assert!((state_vec.probability(0) - 1.0).abs() < 1e-10); + + // |01⟩ → should remain |01⟩ + state_vec.prepare_computational_basis(1); + state_vec.cx(1, 0); + assert!((state_vec.probability(1) - 1.0).abs() < 1e-10); + + // |10⟩ → should flip to |11⟩ + state_vec.prepare_computational_basis(2); + state_vec.cx(1, 0); + assert!((state_vec.probability(3) - 1.0).abs() < 1e-10); + + // |11⟩ → should flip to |10⟩ + state_vec.prepare_computational_basis(3); + state_vec.cx(1, 0); + assert!((state_vec.probability(2) - 1.0).abs() < 1e-10); + } + + #[test] + fn test_control_target_independence() { + // Test that CY and CZ work regardless of which qubit is control/target + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Prepare same initial state + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); + + // Apply gates with different control/target + q1.cz(0, 1); + q2.cz(1, 0); + + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_rxx_symmetry() { + // Test that RXX is symmetric under exchange of qubits + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Prepare same non-trivial initial state + q1.h(0); + q1.h(1); + q2.h(0); + q2.h(1); + + // Apply RXX with different qubit orders + q1.rxx(FRAC_PI_3, 0, 1); + q2.rxx(FRAC_PI_3, 1, 0); + + // Results should be identical + for (a, b) in q1.state().iter().zip(q2.state().iter()) { + assert!((a - b).norm() < 1e-10); + } + } + + #[test] + fn test_ryy_qubit_order_invariance() { + let theta = FRAC_PI_4; + + // Test on random initial states + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + q1.h(0).x(1); // Random state + q2.h(0).x(1); // Same initial state + + q1.ryy(theta, 0, 1); + q2.ryy(theta, 1, 0); + + // States should be exactly equal + for (a, b) in q1.state().iter().zip(q2.state().iter()) { + assert!( + (a - b).norm() < 1e-10, + "Qubit order test failed: a={a}, b={b}" + ); + } + } + + #[test] + fn test_ryy_large_system() { + let theta = FRAC_PI_3; + + // Initialize a 5-qubit state + let mut q = StateVec::new(5); + q.h(0).h(1).h(2).h(3).h(4); // Superposition state + + // Apply RYY on qubits 2 and 4 + q.ryy(theta, 2, 4); + + // Ensure state vector normalization is preserved + let norm: f64 = q.state().iter().map(num_complex::Complex::norm_sqr).sum(); + assert!( + (norm - 1.0).abs() < 1e-10, + "State normalization test failed: norm={norm}" + ); + } + + #[test] + fn test_ryy_edge_cases() { + let mut q = StateVec::new(2); + + // Apply RYY gate + q.ryy(PI, 0, 1); + + // Define the expected result for RYY(π) + let expected = vec![ + Complex64::new(0.0, 0.0), // |00⟩ + Complex64::new(0.0, 0.0), // |01⟩ + Complex64::new(0.0, 0.0), // |10⟩ + Complex64::new(-1.0, 0.0), // |11⟩ + ]; + + // Compare simulated state vector to the expected result + assert_states_equal(q.state(), &expected); + } + + #[test] + fn test_ryy_global_phase() { + let mut q = StateVec::new(2); + + q.ryy(PI, 0, 1); + + // Define the expected result for RYY(π) + let expected = vec![ + Complex64::new(0.0, 0.0), // |00⟩ + Complex64::new(0.0, 0.0), // |01⟩ + Complex64::new(0.0, 0.0), // |10⟩ + Complex64::new(-1.0, 0.0), // |11⟩ + ]; + + // Compare states + assert_states_equal(q.state(), &expected); + } + + #[test] + fn test_ryy_small_angles() { + let theta = 1e-10; // Very small angle + let mut q = StateVec::new(2); + + // Initialize |00⟩ + let initial = q.state().to_vec(); + q.ryy(theta, 0, 1); + + // Expect state to remain close to the initial state + for (a, b) in q.state().iter().zip(initial.iter()) { + assert!( + (a - b).norm() < 1e-10, + "Small angle test failed: a={a}, b={b}" + ); + } + } + + #[test] + fn test_ryy_randomized() { + use rand::Rng; + + let mut rng = rand::thread_rng(); + let theta = rng.gen_range(0.0..2.0 * PI); + + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Random initial state + q1.h(0).h(1); + q2.h(0).h(1); + + // Apply RYY with random qubit order + q1.ryy(theta, 0, 1); + q2.ryy(theta, 1, 0); + + // Compare states + for (a, b) in q1.state().iter().zip(q2.state().iter()) { + assert!( + (a - b).norm() < 1e-10, + "Randomized test failed: a={a}, b={b}" + ); + } + } + + #[test] + fn test_szz_equivalence() { + // Test that SZZ is equivalent to RZZ(π/2) + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Create some non-trivial initial state + q1.h(0); + q2.h(0); + + // Compare direct SZZ vs RZZ(π/2) + q1.szz(0, 1); + q2.rzz(FRAC_PI_2, 0, 1); + + assert_states_equal(q1.state(), q2.state()); + + // Also verify decomposition matches + let mut q3 = StateVec::new(2); + q3.h(0); // Same initial state + q3.h(0).h(1).sxx(0, 1).h(0).h(1); + + assert_states_equal(q1.state(), q3.state()); + } + + #[test] + fn test_szz_trait_equivalence() { + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Create some non-trivial initial state + q1.h(0); + q2.h(0); + + // Compare CliffordGateable trait szz vs ArbitraryRotationGateable trait rzz(π/2) + CliffordGateable::::szz(&mut q1, 0, 1); + ArbitraryRotationGateable::::rzz(&mut q2, PI / 2.0, 0, 1); + + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_two_qubit_unitary_properties() { + let mut q = StateVec::new(2); + + // Create a non-trivial state + q.h(0); + q.h(1); + + // iSWAP matrix + let iswap = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 1.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 1.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + ]; + + q.two_qubit_unitary(0, 1, iswap); + + // Verify normalization is preserved + let norm: f64 = q.state().iter().map(num_complex::Complex::norm_sqr).sum(); + assert!((norm - 1.0).abs() < 1e-10); + } + + #[test] + fn test_two_qubit_unitary_identity() { + let mut state_vec = StateVec::new(2); + + // Identity matrix + let identity_gate = [ + [ + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + Complex64::new(0.0, 0.0), + ], + [ + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(0.0, 0.0), + Complex64::new(1.0, 0.0), + ], + ]; + + // Apply the identity gate + state_vec.prepare_computational_basis(2); + state_vec.two_qubit_unitary(0, 1, identity_gate); + + // State should remain |10⟩ + assert!((state_vec.probability(2) - 1.0).abs() < 1e-10); + } + + #[test] + fn test_controlled_gate_symmetries() { + let mut q1 = StateVec::new(2); + let mut q2 = StateVec::new(2); + + // Test SWAP symmetry + q1.x(0); // |10⟩ + q2.x(0); // |10⟩ + + q1.cx(0, 1).cx(1, 0).cx(0, 1); // SWAP via CNOTs + q2.swap(0, 1); // Direct SWAP + + assert_states_equal(q1.state(), q2.state()); + } + + #[test] + fn test_controlled_gate_phases() { + // Test phase behavior of controlled operations + let mut q = StateVec::new(2); + + // Create superposition with phases + q.h(0).sz(0); + q.h(1).sz(1); + + // Control operations should preserve phases correctly + let initial = q.state().to_vec(); + q.cz(0, 1).cz(0, 1); // CZ^2 = I + + assert_states_equal(q.state(), &initial); + } +} + +mod detail_meas_cases { + use pecos_qsim::{CliffordGateable, QuantumSimulator, StateVec}; + + #[test] + fn test_measurement_on_entangled_state() { + let mut q = StateVec::new(2); + + // Create Bell state (|00⟩ + |11⟩) / sqrt(2) + q.h(0); + q.cx(0, 1); + + // Measure the first qubit + let result1 = q.mz(0); + + // Measure the second qubit - should match the first + let result2 = q.mz(1); + + assert_eq!(result1.outcome, result2.outcome); + } + + #[test] + fn test_measurement_properties() { + let mut q = StateVec::new(2); + + // Test 1: Measuring |0⟩ should always give 0 + let result = q.mz(0); + assert!(!result.outcome); + assert!((q.probability(0) - 1.0).abs() < 1e-10); + + // Test 2: Measuring |1⟩ should always give 1 + q.reset(); + q.x(0); + let result = q.mz(0); + assert!(result.outcome); + assert!((q.probability(1) - 1.0).abs() < 1e-10); + + // Test 3: In a Bell state, measurements should correlate + q.reset(); + q.h(0).cx(0, 1); // Create Bell state + let result1 = q.mz(0); + let result2 = q.mz(1); + assert_eq!( + result1.outcome, result2.outcome, + "Bell state measurements should correlate" + ); + + // Test 4: Repeated measurements should be consistent + q.reset(); + q.h(0); // Create superposition + let first = q.mz(0); + let second = q.mz(0); // Measure again + assert_eq!( + first.outcome, second.outcome, + "Repeated measurements should give same result" + ); + } + + #[test] + fn test_measurement_basis_transforms() { + let mut q = StateVec::new(1); + + // |0⟩ in X basis + q.h(0); + + // Measure in Z basis + let result = q.mz(0); + + // Result should be random but state should collapse + let final_prob = if result.outcome { + q.probability(1) + } else { + q.probability(0) + }; + assert!((final_prob - 1.0).abs() < 1e-10); + } +} From 5abf95c8cc50cf02c488f8e11df34eb244b53ca3 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Tue, 21 Jan 2025 18:24:25 -0700 Subject: [PATCH 32/33] cleanup target and control arguments --- crates/pecos-qsim/src/state_vec.rs | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/pecos-qsim/src/state_vec.rs b/crates/pecos-qsim/src/state_vec.rs index 68482251..3008aa7a 100644 --- a/crates/pecos-qsim/src/state_vec.rs +++ b/crates/pecos-qsim/src/state_vec.rs @@ -295,13 +295,13 @@ where #[inline] pub fn single_qubit_rotation( &mut self, - target: usize, + qubit: usize, u00: Complex64, u01: Complex64, u10: Complex64, u11: Complex64, ) -> &mut Self { - let step = 1 << target; + let step = 1 << qubit; for i in (0..self.state.len()).step_by(2 * step) { for offset in 0..step { let j = i + offset; @@ -383,8 +383,8 @@ where #[inline] pub fn two_qubit_unitary( &mut self, - control: usize, - target: usize, + qubit1: usize, + qubit2: usize, matrix: [[Complex64; 4]; 4], ) -> &mut Self { let n = self.num_qubits; @@ -394,18 +394,18 @@ where let mut new_state = vec![Complex64::new(0.0, 0.0); size]; for i in 0..size { - // Extract control and target bits - let control_bit = (i >> control) & 1; - let target_bit = (i >> target) & 1; + // Extract qubit1 and qubit2 bits + let control_bit = (i >> qubit1) & 1; + let target_bit = (i >> qubit2) & 1; // Map (control_bit, target_bit) to basis index (00, 01, 10, 11) let basis_idx = (control_bit << 1) | target_bit; for (j, &row) in matrix.iter().enumerate() { - // Calculate the index after flipping control and target qubits - let flipped_i = (i & !(1 << control) & !(1 << target)) - | (((j >> 1) & 1) << control) - | ((j & 1) << target); + // Calculate the index after flipping qubit1 and qubit2 qubits + let flipped_i = (i & !(1 << qubit1) & !(1 << qubit2)) + | (((j >> 1) & 1) << qubit1) + | ((j & 1) << qubit2); // Apply the matrix to the relevant amplitudes new_state[flipped_i] += row[basis_idx] * self.state[i]; @@ -496,10 +496,10 @@ impl CliffordGateable for StateVec { /// - `qubit` is a valid qubit index (i.e., `< number of qubits`). /// - These conditions must be ensured by the caller or a higher-level component. #[inline] - fn y(&mut self, target: usize) -> &mut Self { + fn y(&mut self, qubit: usize) -> &mut Self { for i in 0..self.state.len() { - if (i >> target) & 1 == 0 { - let flipped_i = i ^ (1 << target); + if (i >> qubit) & 1 == 0 { + let flipped_i = i ^ (1 << qubit); let temp = self.state[i]; self.state[i] = -Complex64::i() * self.state[flipped_i]; self.state[flipped_i] = Complex64::i() * temp; @@ -611,6 +611,10 @@ impl CliffordGateable for StateVec { /// See [`CliffordGateable::cx`] for mathematical details and gate properties. /// Uses bit manipulation for fast controlled operations. /// + /// # Arguments + /// * `qubit1` - Qubit ID and control qubit + /// * `qubit2` - Qubit ID and target qubit + /// /// # Examples /// ```rust /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; @@ -657,6 +661,10 @@ impl CliffordGateable for StateVec { /// See [`CliffordGateable::cy`] for mathematical details and gate properties. /// Combines bit manipulation with phase updates for controlled-Y operation. /// + /// # Arguments + /// * `qubit1` - Qubit ID and control qubit + /// * `qubit2` - Qubit ID and target qubit + /// /// # Examples /// ```rust /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; @@ -697,6 +705,10 @@ impl CliffordGateable for StateVec { /// See [`CliffordGateable::cz`] for mathematical details and gate properties. /// Takes advantage of diagonal structure for optimal performance. /// + /// # Arguments + /// * `qubit1` - Qubit ID and control qubit + /// * `qubit2` - Qubit ID and target qubit + /// /// # Examples /// ```rust /// use pecos_qsim::{QuantumSimulator, StateVec, CliffordGateable}; From 6b0a360c3d2ba9d5ff5075103eeed174766c7cd2 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Fri, 24 Jan 2025 15:08:04 -0700 Subject: [PATCH 33/33] fixing license header typo --- crates/pecos-python/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pecos-python/src/lib.rs b/crates/pecos-python/src/lib.rs index 024da868..3be3cee8 100644 --- a/crates/pecos-python/src/lib.rs +++ b/crates/pecos-python/src/lib.rs @@ -1,6 +1,6 @@ // Copyright 2024 The PECOS Developers // -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use thispub(crate)pub(crate) file except +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except // in compliance with the License.You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0