From c216f7eca018db90243a831f0fe457955058d20b Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 21 Apr 2026 16:12:16 -0700 Subject: [PATCH 1/4] Enable Clifford simulation in `qsharp.run` This change adds a new `type` parameter to `qsharp.run` that allows users to specify the simulation type they want to use, with the default the existing sparse simulation and a new option of "clifford" simulation. There is an additional parameter that allows specifying the number of qubits to use for Clifford simulation. This is accomplished by updated the `Backend` trait to have most functions be fallible, returning a `Result` type. This allows Clifford simulation to fail gracefully on non-Clifford operations and qubit allocation beyond the configured amount. --- source/compiler/qsc/src/interpret.rs | 90 ++- source/compiler/qsc/src/interpret/tests.rs | 10 +- source/compiler/qsc/src/lib.rs | 2 +- source/compiler/qsc_eval/src/backend.rs | 653 +++++++++++++----- .../qsc_eval/src/backend/noise_tests.rs | 188 ++--- source/compiler/qsc_eval/src/intrinsic.rs | 163 +++-- .../compiler/qsc_eval/src/intrinsic/tests.rs | 104 +-- source/compiler/qsc_eval/src/lib.rs | 13 +- source/compiler/qsc_partial_eval/src/lib.rs | 3 +- .../qsc_partial_eval/src/management.rs | 10 +- source/pip/qsharp/_native.pyi | 17 +- source/pip/qsharp/_qsharp.py | 14 + source/pip/qsharp/openqasm/_run.py | 20 +- source/pip/src/interop.rs | 75 +- source/pip/src/interpreter.rs | 16 +- source/pip/tests/test_clifford_simulator.py | 380 ++++++++++ source/resource_estimator/src/counts.rs | 118 ++-- source/simulators/src/stabilizer_simulator.rs | 198 +++++- 18 files changed, 1606 insertions(+), 468 deletions(-) diff --git a/source/compiler/qsc/src/interpret.rs b/source/compiler/qsc/src/interpret.rs index 8e4a2b982a..cfab2097a5 100644 --- a/source/compiler/qsc/src/interpret.rs +++ b/source/compiler/qsc/src/interpret.rs @@ -44,7 +44,7 @@ use qsc_data_structures::{ }; use qsc_eval::{ Env, ErrorBehavior, State, VariableInfo, - backend::{Backend, SparseSim, TracingBackend}, + backend::{Backend, CliffordSim, SparseSim, TracingBackend}, output::Receiver, }; pub use qsc_eval::{ @@ -122,6 +122,13 @@ pub enum Error { PartialEvaluation(#[from] WithSource), } +#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] +pub enum SimType { + #[default] + Sparse, + Clifford(usize), +} + /// A Q# interpreter. pub struct Interpreter { /// The incremental Q# compiler. @@ -844,30 +851,47 @@ impl Interpreter { qubit_loss: Option, noise_config: Option>, seed: Option, + sim_type: SimType, ) -> InterpretResult { let qubit_loss = if noise_config.is_none() { qubit_loss } else { None }; - let mut sim = match noise { - Some(noise) => SparseSim::new_with_noise(&noise), - None => match noise_config { - Some(config) => SparseSim::new_with_noise_config(config.into()), - None => SparseSim::new(), - }, - }; - if let Some(loss) = qubit_loss { - sim.set_loss(loss); - } - if seed.is_some() { - sim.set_seed(seed); + + match sim_type { + SimType::Sparse => { + let mut sim = match noise { + Some(noise) => SparseSim::new_with_noise(&noise), + None => match noise_config { + Some(config) => SparseSim::new_with_noise_config(config.into()), + None => SparseSim::new(), + }, + }; + if let Some(loss) = qubit_loss { + sim.set_loss(loss); + } + if seed.is_some() { + sim.set_seed(seed); + } + self.invoke_with_sim(&mut sim, receiver, callable, args, seed) + } + SimType::Clifford(num_qubits) => { + let mut sim = match noise_config { + Some(config) => CliffordSim::new_with_noise_config(num_qubits, config.into()), + None => CliffordSim::new(num_qubits), + }; + if seed.is_some() { + sim.set_seed(seed); + } + self.invoke_with_sim(&mut sim, receiver, callable, args, seed) + } } - self.invoke_with_sim(&mut sim, receiver, callable, args, seed) } /// Runs the given entry expression on a new instance of the environment and simulator, /// but using the current compilation. + #[allow(clippy::too_many_arguments)] pub fn run( &mut self, receiver: &mut impl Receiver, @@ -876,28 +900,42 @@ impl Interpreter { qubit_loss: Option, noise_config: Option>, seed: Option, + sim_type: SimType, ) -> InterpretResult { let qubit_loss = if noise_config.is_none() { qubit_loss } else { None }; - let mut sim = match noise { - Some(noise) => SparseSim::new_with_noise(&noise), - None => match noise_config { - Some(config) => SparseSim::new_with_noise_config(config.into()), - None => SparseSim::new(), - }, - }; - if let Some(loss) = qubit_loss { - sim.set_loss(loss); + match sim_type { + SimType::Sparse => { + let mut sim = match noise { + Some(noise) => SparseSim::new_with_noise(&noise), + None => match noise_config { + Some(config) => SparseSim::new_with_noise_config(config.into()), + None => SparseSim::new(), + }, + }; + if let Some(loss) = qubit_loss { + sim.set_loss(loss); + } + self.run_with_sim(&mut sim, receiver, expr, seed) + } + SimType::Clifford(num_qubits) => { + let mut sim = match noise_config { + Some(config) => CliffordSim::new_with_noise_config(num_qubits, config.into()), + None => CliffordSim::new(num_qubits), + }; + self.run_with_sim(&mut sim, receiver, expr, seed) + } } - self.run_with_sim(&mut sim, receiver, expr, seed) } /// Gets the current quantum state of the simulator. pub fn get_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { - self.sim.capture_quantum_state() + self.sim + .capture_quantum_state() + .expect("interpreter should use infallible sparse sim by default") } /// Get the current circuit representation of the program. @@ -1641,7 +1679,7 @@ impl Debugger { } pub fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { - self.interpreter.sim.capture_quantum_state() + self.interpreter.get_quantum_state() } pub fn circuit(&self) -> Circuit { diff --git a/source/compiler/qsc/src/interpret/tests.rs b/source/compiler/qsc/src/interpret/tests.rs index 8c8cbe8077..09bd204840 100644 --- a/source/compiler/qsc/src/interpret/tests.rs +++ b/source/compiler/qsc/src/interpret/tests.rs @@ -23,7 +23,15 @@ mod given_interpreter { fn run(interpreter: &mut Interpreter, expr: &str) -> (InterpretResult, String) { let mut cursor = Cursor::new(Vec::::new()); let mut receiver = CursorReceiver::new(&mut cursor); - let res = interpreter.run(&mut receiver, Some(expr), None, None, None, None); + let res = interpreter.run( + &mut receiver, + Some(expr), + None, + None, + None, + None, + Default::default(), + ); (res, receiver.dump()) } diff --git a/source/compiler/qsc/src/lib.rs b/source/compiler/qsc/src/lib.rs index 2c3194af02..7c29022b53 100644 --- a/source/compiler/qsc/src/lib.rs +++ b/source/compiler/qsc/src/lib.rs @@ -50,7 +50,7 @@ pub mod line_column { } pub use qsc_eval::{ - backend::{Backend, SparseSim}, + backend::{Backend, CliffordSim, SparseSim}, noise::PauliNoise, state::{ fmt_basis_state_label, fmt_complex, format_state_id, get_matrix_latex, get_phase, diff --git a/source/compiler/qsc_eval/src/backend.rs b/source/compiler/qsc_eval/src/backend.rs index 36f4a42536..ded6036ba4 100644 --- a/source/compiler/qsc_eval/src/backend.rs +++ b/source/compiler/qsc_eval/src/backend.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::f64::consts::{FRAC_PI_2, PI, TAU}; + use crate::debug::Frame; use crate::val::{self, Value}; use crate::{noise::PauliNoise, val::unwrap_tuple}; @@ -8,105 +10,109 @@ use ndarray::Array2; use num_bigint::BigUint; use num_complex::Complex; use num_traits::Zero; -use qdk_simulators::SparseStateSim; use qdk_simulators::cpu_full_state_simulator::noise::{Fault, PauliFault}; use qdk_simulators::noise_config::{CumulativeNoiseConfig, CumulativeNoiseTable}; +use qdk_simulators::stabilizer_simulator::{self, StabilizerSimulator}; +use qdk_simulators::{MeasurementResult, NearlyZero, Simulator as _, SparseStateSim}; +use qsc_data_structures::index_map::IndexMap; use rand::{Rng, RngCore}; use rand::{SeedableRng, rngs::StdRng}; #[cfg(test)] mod noise_tests; +type StateDump = (Vec<(BigUint, Complex)>, usize); + /// The trait that must be implemented by a quantum backend, whose functions will be invoked when /// quantum intrinsics are called. pub trait Backend { - fn ccx(&mut self, _ctl0: usize, _ctl1: usize, _q: usize) { - unimplemented!("ccx gate"); + fn ccx(&mut self, _ctl0: usize, _ctl1: usize, _q: usize) -> Result<(), String> { + Err("ccx gate not implemented".to_string()) } - fn cx(&mut self, _ctl: usize, _q: usize) { - unimplemented!("cx gate"); + fn cx(&mut self, _ctl: usize, _q: usize) -> Result<(), String> { + Err("cx gate not implemented".to_string()) } - fn cy(&mut self, _ctl: usize, _q: usize) { - unimplemented!("cy gate"); + fn cy(&mut self, _ctl: usize, _q: usize) -> Result<(), String> { + Err("cy gate not implemented".to_string()) } - fn cz(&mut self, _ctl: usize, _q: usize) { - unimplemented!("cz gate"); + fn cz(&mut self, _ctl: usize, _q: usize) -> Result<(), String> { + Err("cz gate not implemented".to_string()) } - fn h(&mut self, _q: usize) { - unimplemented!("h gate"); + fn h(&mut self, _q: usize) -> Result<(), String> { + Err("h gate not implemented".to_string()) } - fn m(&mut self, _q: usize) -> val::Result { - unimplemented!("m operation"); + fn m(&mut self, _q: usize) -> Result { + Err("m operation not implemented".to_string()) } - fn mresetz(&mut self, _q: usize) -> val::Result { - unimplemented!("mresetz operation"); + fn mresetz(&mut self, _q: usize) -> Result { + Err("mresetz operation not implemented".to_string()) } - fn reset(&mut self, _q: usize) { - unimplemented!("reset gate"); + fn reset(&mut self, _q: usize) -> Result<(), String> { + Err("reset gate not implemented".to_string()) } - fn rx(&mut self, _theta: f64, _q: usize) { - unimplemented!("rx gate"); + fn rx(&mut self, _theta: f64, _q: usize) -> Result<(), String> { + Err("rx gate not implemented".to_string()) } - fn rxx(&mut self, _theta: f64, _q0: usize, _q1: usize) { - unimplemented!("rxx gate"); + fn rxx(&mut self, _theta: f64, _q0: usize, _q1: usize) -> Result<(), String> { + Err("rxx gate not implemented".to_string()) } - fn ry(&mut self, _theta: f64, _q: usize) { - unimplemented!("ry gate"); + fn ry(&mut self, _theta: f64, _q: usize) -> Result<(), String> { + Err("ry gate not implemented".to_string()) } - fn ryy(&mut self, _theta: f64, _q0: usize, _q1: usize) { - unimplemented!("ryy gate"); + fn ryy(&mut self, _theta: f64, _q0: usize, _q1: usize) -> Result<(), String> { + Err("ryy gate not implemented".to_string()) } - fn rz(&mut self, _theta: f64, _q: usize) { - unimplemented!("rz gate"); + fn rz(&mut self, _theta: f64, _q: usize) -> Result<(), String> { + Err("rz gate not implemented".to_string()) } - fn rzz(&mut self, _theta: f64, _q0: usize, _q1: usize) { - unimplemented!("rzz gate"); + fn rzz(&mut self, _theta: f64, _q0: usize, _q1: usize) -> Result<(), String> { + Err("rzz gate not implemented".to_string()) } - fn sadj(&mut self, _q: usize) { - unimplemented!("sadj gate"); + fn sadj(&mut self, _q: usize) -> Result<(), String> { + Err("sadj gate not implemented".to_string()) } - fn s(&mut self, _q: usize) { - unimplemented!("s gate"); + fn s(&mut self, _q: usize) -> Result<(), String> { + Err("s gate not implemented".to_string()) } - fn sx(&mut self, _q: usize) { - unimplemented!("sx gate"); + fn sx(&mut self, _q: usize) -> Result<(), String> { + Err("sx gate not implemented".to_string()) } - fn swap(&mut self, _q0: usize, _q1: usize) { - unimplemented!("swap gate"); + fn swap(&mut self, _q0: usize, _q1: usize) -> Result<(), String> { + Err("swap gate not implemented".to_string()) } - fn tadj(&mut self, _q: usize) { - unimplemented!("tadj gate"); + fn tadj(&mut self, _q: usize) -> Result<(), String> { + Err("tadj gate not implemented".to_string()) } - fn t(&mut self, _q: usize) { - unimplemented!("t gate"); + fn t(&mut self, _q: usize) -> Result<(), String> { + Err("t gate not implemented".to_string()) } - fn x(&mut self, _q: usize) { - unimplemented!("x gate"); + fn x(&mut self, _q: usize) -> Result<(), String> { + Err("x gate not implemented".to_string()) } - fn y(&mut self, _q: usize) { - unimplemented!("y gate"); + fn y(&mut self, _q: usize) -> Result<(), String> { + Err("y gate not implemented".to_string()) } - fn z(&mut self, _q: usize) { - unimplemented!("z gate"); + fn z(&mut self, _q: usize) -> Result<(), String> { + Err("z gate not implemented".to_string()) } - fn qubit_allocate(&mut self) -> usize { - unimplemented!("qubit_allocate operation"); + fn qubit_allocate(&mut self) -> Result { + Err("qubit_allocate operation not implemented".to_string()) } /// `false` indicates that the qubit was in a non-zero state before the release, /// but should have been in the zero state. /// `true` otherwise. This includes the case when the qubit was in /// a non-zero state during a noisy simulation, which is allowed. - fn qubit_release(&mut self, _q: usize) -> bool { - unimplemented!("qubit_release operation"); + fn qubit_release(&mut self, _q: usize) -> Result { + Err("qubit_release operation not implemented".to_string()) } - fn qubit_swap_id(&mut self, _q0: usize, _q1: usize) { - unimplemented!("qubit_swap_id operation"); + fn qubit_swap_id(&mut self, _q0: usize, _q1: usize) -> Result<(), String> { + Err("qubit_swap_id operation not implemented".to_string()) } - fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { - unimplemented!("capture_quantum_state operation"); + fn capture_quantum_state(&mut self) -> Result { + Err("capture_quantum_state operation not implemented".to_string()) } - fn qubit_is_zero(&mut self, _q: usize) -> bool { - unimplemented!("qubit_is_zero operation"); + fn qubit_is_zero(&mut self, _q: usize) -> Result { + Err("qubit_is_zero operation not implemented".to_string()) } /// Executes custom intrinsic specified by `_name`. /// Returns None if this intrinsic is unknown. @@ -180,261 +186,287 @@ impl<'a, B: Backend> TracingBackend<'a, B> { } } - pub fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize, stack: &[Frame]) { + pub fn ccx( + &mut self, + ctl0: usize, + ctl1: usize, + q: usize, + stack: &[Frame], + ) -> Result<(), String> { if let OptionalBackend::Some(backend) = &mut self.backend { - backend.ccx(ctl0, ctl1, q); + backend.ccx(ctl0, ctl1, q)?; } if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "X", false, &[q], &[ctl0, ctl1], None); } + Ok(()) } - pub fn cx(&mut self, ctl: usize, q: usize, stack: &[Frame]) { + pub fn cx(&mut self, ctl: usize, q: usize, stack: &[Frame]) -> Result<(), String> { if let OptionalBackend::Some(backend) = &mut self.backend { - backend.cx(ctl, q); + backend.cx(ctl, q)?; } if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "X", false, &[q], &[ctl], None); } + Ok(()) } - pub fn cy(&mut self, ctl: usize, q: usize, stack: &[Frame]) { + pub fn cy(&mut self, ctl: usize, q: usize, stack: &[Frame]) -> Result<(), String> { if let OptionalBackend::Some(backend) = &mut self.backend { - backend.cy(ctl, q); + backend.cy(ctl, q)?; } if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Y", false, &[q], &[ctl], None); } + Ok(()) } - pub fn cz(&mut self, ctl: usize, q: usize, stack: &[Frame]) { + pub fn cz(&mut self, ctl: usize, q: usize, stack: &[Frame]) -> Result<(), String> { if let OptionalBackend::Some(backend) = &mut self.backend { - backend.cz(ctl, q); + backend.cz(ctl, q)?; } if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Z", false, &[q], &[ctl], None); } + Ok(()) } - pub fn h(&mut self, q: usize, stack: &[Frame]) { + pub fn h(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let OptionalBackend::Some(backend) = &mut self.backend { - backend.h(q); + backend.h(q)?; } if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "H", false, &[q], &[], None); } + Ok(()) } - pub fn m(&mut self, q: usize, stack: &[Frame]) -> val::Result { + pub fn m(&mut self, q: usize, stack: &[Frame]) -> Result { let r = match &mut self.backend { - OptionalBackend::Some(backend) => backend.m(q), + OptionalBackend::Some(backend) => backend.m(q)?, OptionalBackend::None(fallback) => fallback.result_allocate(), }; if let Some(tracer) = &mut self.tracer { tracer.measure(stack, "M", q, &r); } - r + Ok(r) } - pub fn mresetz(&mut self, q: usize, stack: &[Frame]) -> val::Result { + pub fn mresetz(&mut self, q: usize, stack: &[Frame]) -> Result { let r = match &mut self.backend { - OptionalBackend::Some(backend) => backend.mresetz(q), + OptionalBackend::Some(backend) => backend.mresetz(q)?, OptionalBackend::None(fallback) => fallback.result_allocate(), }; if let Some(tracer) = &mut self.tracer { tracer.measure(stack, "MResetZ", q, &r); } - r + Ok(r) } - pub fn reset(&mut self, q: usize, stack: &[Frame]) { + pub fn reset(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.reset(stack, q); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.reset(q); + backend.reset(q)?; } + Ok(()) } - pub fn rx(&mut self, theta: f64, q: usize, stack: &[Frame]) { + pub fn rx(&mut self, theta: f64, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rx", false, &[q], &[], Some(theta)); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.rx(theta, q); + backend.rx(theta, q)?; } + Ok(()) } - pub fn rxx(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) { + pub fn rxx(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rxx", false, &[q0, q1], &[], Some(theta)); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.rxx(theta, q0, q1); + backend.rxx(theta, q0, q1)?; } + Ok(()) } - pub fn ry(&mut self, theta: f64, q: usize, stack: &[Frame]) { + pub fn ry(&mut self, theta: f64, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Ry", false, &[q], &[], Some(theta)); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.ry(theta, q); + backend.ry(theta, q)?; } + Ok(()) } - pub fn ryy(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) { + pub fn ryy(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Ryy", false, &[q0, q1], &[], Some(theta)); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.ryy(theta, q0, q1); + backend.ryy(theta, q0, q1)?; } + Ok(()) } - pub fn rz(&mut self, theta: f64, q: usize, stack: &[Frame]) { + pub fn rz(&mut self, theta: f64, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rz", false, &[q], &[], Some(theta)); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.rz(theta, q); + backend.rz(theta, q)?; } + Ok(()) } - pub fn rzz(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) { + pub fn rzz(&mut self, theta: f64, q0: usize, q1: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Rzz", false, &[q0, q1], &[], Some(theta)); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.rzz(theta, q0, q1); + backend.rzz(theta, q0, q1)?; } + Ok(()) } - pub fn sadj(&mut self, q: usize, stack: &[Frame]) { + pub fn sadj(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "S", true, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.sadj(q); + backend.sadj(q)?; } + Ok(()) } - pub fn s(&mut self, q: usize, stack: &[Frame]) { + pub fn s(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "S", false, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.s(q); + backend.s(q)?; } + Ok(()) } - pub fn sx(&mut self, q: usize, stack: &[Frame]) { + pub fn sx(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "SX", false, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.sx(q); + backend.sx(q)?; } + Ok(()) } - pub fn swap(&mut self, q0: usize, q1: usize, stack: &[Frame]) { + pub fn swap(&mut self, q0: usize, q1: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "SWAP", false, &[q0, q1], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.swap(q0, q1); + backend.swap(q0, q1)?; } + Ok(()) } - pub fn tadj(&mut self, q: usize, stack: &[Frame]) { + pub fn tadj(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "T", true, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.tadj(q); + backend.tadj(q)?; } + Ok(()) } - pub fn t(&mut self, q: usize, stack: &[Frame]) { + pub fn t(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "T", false, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.t(q); + backend.t(q)?; } + Ok(()) } - pub fn x(&mut self, q: usize, stack: &[Frame]) { + pub fn x(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "X", false, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.x(q); + backend.x(q)?; } + Ok(()) } - pub fn y(&mut self, q: usize, stack: &[Frame]) { + pub fn y(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Y", false, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.y(q); + backend.y(q)?; } + Ok(()) } - pub fn z(&mut self, q: usize, stack: &[Frame]) { + pub fn z(&mut self, q: usize, stack: &[Frame]) -> Result<(), String> { if let Some(tracer) = &mut self.tracer { tracer.gate(stack, "Z", false, &[q], &[], None); } if let OptionalBackend::Some(backend) = &mut self.backend { - backend.z(q); + backend.z(q)?; } + Ok(()) } - pub fn qubit_allocate(&mut self, stack: &[Frame]) -> usize { + pub fn qubit_allocate(&mut self, stack: &[Frame]) -> Result { let q = match &mut self.backend { - OptionalBackend::Some(backend) => backend.qubit_allocate(), + OptionalBackend::Some(backend) => backend.qubit_allocate()?, OptionalBackend::None(fallback) => fallback.qubit_allocate(), }; if let Some(tracer) = &mut self.tracer { tracer.qubit_allocate(stack, q); } - q + Ok(q) } - pub fn qubit_release(&mut self, q: usize, stack: &[Frame]) -> bool { + pub fn qubit_release(&mut self, q: usize, stack: &[Frame]) -> Result { let b = match &mut self.backend { - OptionalBackend::Some(backend) => backend.qubit_release(q), + OptionalBackend::Some(backend) => backend.qubit_release(q)?, OptionalBackend::None(fallback) => fallback.qubit_release(q), }; if let Some(tracer) = &mut self.tracer { tracer.qubit_release(stack, q); } - b + Ok(b) } - pub fn qubit_swap_id(&mut self, q0: usize, q1: usize, stack: &[Frame]) { + pub fn qubit_swap_id(&mut self, q0: usize, q1: usize, stack: &[Frame]) -> Result<(), String> { if let OptionalBackend::Some(backend) = &mut self.backend { - backend.qubit_swap_id(q0, q1); + backend.qubit_swap_id(q0, q1)?; } if let Some(tracer) = &mut self.tracer { tracer.qubit_swap_id(stack, q0, q1); } + Ok(()) } - pub fn capture_quantum_state( - &mut self, - ) -> (Vec<(num_bigint::BigUint, num_complex::Complex)>, usize) { + pub fn capture_quantum_state(&mut self) -> Result { match &mut self.backend { OptionalBackend::Some(backend) => backend.capture_quantum_state(), - OptionalBackend::None(_) => (Vec::new(), 0), + OptionalBackend::None(_) => Ok((Vec::new(), 0)), } } - pub fn qubit_is_zero(&mut self, q: usize) -> bool { + pub fn qubit_is_zero(&mut self, q: usize) -> Result { match &mut self.backend { OptionalBackend::Some(backend) => backend.qubit_is_zero(q), - OptionalBackend::None(_) => true, + OptionalBackend::None(_) => Ok(true), } } @@ -698,7 +730,7 @@ impl SparseSim { } impl Backend for SparseSim { - fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) { + fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) -> Result<(), String> { match ( self.is_qubit_lost(ctl0), self.is_qubit_lost(ctl1), @@ -722,77 +754,84 @@ impl Backend for SparseSim { } } self.apply_faults(|noise| &noise.ccx, &[ctl0, ctl1, q]); + Ok(()) } - fn cx(&mut self, ctl: usize, q: usize) { + fn cx(&mut self, ctl: usize, q: usize) -> Result<(), String> { if !self.is_qubit_lost(ctl) && !self.is_qubit_lost(q) { self.sim.mcx(&[ctl], q); } self.apply_faults(|noise| &noise.cx, &[ctl, q]); + Ok(()) } - fn cy(&mut self, ctl: usize, q: usize) { + fn cy(&mut self, ctl: usize, q: usize) -> Result<(), String> { if !self.is_qubit_lost(ctl) && !self.is_qubit_lost(q) { self.sim.mcy(&[ctl], q); } self.apply_faults(|noise| &noise.cy, &[ctl, q]); + Ok(()) } - fn cz(&mut self, ctl: usize, q: usize) { + fn cz(&mut self, ctl: usize, q: usize) -> Result<(), String> { if !self.is_qubit_lost(ctl) && !self.is_qubit_lost(q) { self.sim.mcz(&[ctl], q); } self.apply_faults(|noise| &noise.cz, &[ctl, q]); + Ok(()) } - fn h(&mut self, q: usize) { + fn h(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.h(q); } self.apply_faults(|noise| &noise.h, &[q]); + Ok(()) } - fn m(&mut self, q: usize) -> val::Result { + fn m(&mut self, q: usize) -> Result { self.apply_faults(|noise| &noise.mz, &[q]); if self.is_qubit_lost(q) { // If the qubit is lost, we cannot measure it. // Mark it as no longer lost so it becomes usable again, since // measurement will "reload" the qubit. self.lost_qubits.set_bit(q as u64, false); - return val::Result::Loss; + return Ok(val::Result::Loss); } - val::Result::Val(self.sim.measure(q)) + Ok(val::Result::Val(self.sim.measure(q))) } - fn mresetz(&mut self, q: usize) -> val::Result { + fn mresetz(&mut self, q: usize) -> Result { self.apply_faults(|noise| &noise.mresetz, &[q]); if self.is_qubit_lost(q) { // If the qubit is lost, we cannot measure it. // Mark it as no longer lost so it becomes usable again, since // measurement will "reload" the qubit. self.lost_qubits.set_bit(q as u64, false); - return val::Result::Loss; + return Ok(val::Result::Loss); } let res = self.sim.measure(q); if res { self.sim.x(q); } - val::Result::Val(res) + Ok(val::Result::Val(res)) } - fn reset(&mut self, q: usize) { - self.mresetz(q); + fn reset(&mut self, q: usize) -> Result<(), String> { + self.mresetz(q)?; // Noise applied in mresetz. + Ok(()) } - fn rx(&mut self, theta: f64, q: usize) { + fn rx(&mut self, theta: f64, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.rx(theta, q); } self.apply_faults(|noise| &noise.rx, &[q]); + Ok(()) } - fn rxx(&mut self, theta: f64, q0: usize, q1: usize) { + fn rxx(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { // If only one qubit is lost, we can apply a single qubit rotation. // If both are lost, return without performing any operation. match (self.is_qubit_lost(q0), self.is_qubit_lost(q1)) { @@ -814,16 +853,18 @@ impl Backend for SparseSim { } } self.apply_faults(|noise| &noise.rxx, &[q0, q1]); + Ok(()) } - fn ry(&mut self, theta: f64, q: usize) { + fn ry(&mut self, theta: f64, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.ry(theta, q); } self.apply_faults(|noise| &noise.ry, &[q]); + Ok(()) } - fn ryy(&mut self, theta: f64, q0: usize, q1: usize) { + fn ryy(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { // If only one qubit is lost, we can apply a single qubit rotation. // If both are lost, return without performing any operation. match (self.is_qubit_lost(q0), self.is_qubit_lost(q1)) { @@ -853,16 +894,18 @@ impl Backend for SparseSim { } } self.apply_faults(|noise| &noise.ryy, &[q0, q1]); + Ok(()) } - fn rz(&mut self, theta: f64, q: usize) { + fn rz(&mut self, theta: f64, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.rz(theta, q); } self.apply_faults(|noise| &noise.rz, &[q]); + Ok(()) } - fn rzz(&mut self, theta: f64, q0: usize, q1: usize) { + fn rzz(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { // If only one qubit is lost, we can apply a single qubit rotation. // If both are lost, return without performing any operation. match (self.is_qubit_lost(q0), self.is_qubit_lost(q1)) { @@ -880,90 +923,100 @@ impl Backend for SparseSim { } } self.apply_faults(|noise| &noise.rzz, &[q0, q1]); + Ok(()) } - fn sadj(&mut self, q: usize) { + fn sadj(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.sadj(q); } self.apply_faults(|noise| &noise.s_adj, &[q]); + Ok(()) } - fn s(&mut self, q: usize) { + fn s(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.s(q); } self.apply_faults(|noise| &noise.s, &[q]); + Ok(()) } - fn sx(&mut self, q: usize) { + fn sx(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.h(q); self.sim.s(q); self.sim.h(q); } self.apply_faults(|noise| &noise.sx, &[q]); + Ok(()) } - fn swap(&mut self, q0: usize, q1: usize) { + fn swap(&mut self, q0: usize, q1: usize) -> Result<(), String> { if !self.is_qubit_lost(q0) && !self.is_qubit_lost(q1) { self.sim.swap_qubit_ids(q0, q1); } self.apply_faults(|noise| &noise.swap, &[q0, q1]); + Ok(()) } - fn tadj(&mut self, q: usize) { + fn tadj(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.tadj(q); } self.apply_faults(|noise| &noise.t_adj, &[q]); + Ok(()) } - fn t(&mut self, q: usize) { + fn t(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.t(q); } self.apply_faults(|noise| &noise.t, &[q]); + Ok(()) } - fn x(&mut self, q: usize) { + fn x(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.x(q); } self.apply_faults(|noise| &noise.x, &[q]); + Ok(()) } - fn y(&mut self, q: usize) { + fn y(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.y(q); } self.apply_faults(|noise| &noise.y, &[q]); + Ok(()) } - fn z(&mut self, q: usize) { + fn z(&mut self, q: usize) -> Result<(), String> { if !self.is_qubit_lost(q) { self.sim.z(q); } self.apply_faults(|noise| &noise.z, &[q]); + Ok(()) } - fn qubit_allocate(&mut self) -> usize { + fn qubit_allocate(&mut self) -> Result { // Fresh qubit start in ground state even with noise. - self.sim.allocate() + Ok(self.sim.allocate()) } - fn qubit_release(&mut self, q: usize) -> bool { + fn qubit_release(&mut self, q: usize) -> Result { if self.is_noiseless() { let was_zero = self.sim.qubit_is_zero(q); self.sim.release(q); - was_zero + Ok(was_zero) } else { self.sim.release(q); - true + Ok(true) } } - fn qubit_swap_id(&mut self, q0: usize, q1: usize) { + fn qubit_swap_id(&mut self, q0: usize, q1: usize) -> Result<(), String> { // This is a service function rather than a gate so it doesn't incur noise. self.sim.swap_qubit_ids(q0, q1); // We must also swap any loss bits for the qubits. @@ -976,9 +1029,10 @@ impl Backend for SparseSim { self.lost_qubits.set_bit(q0 as u64, q1_lost); self.lost_qubits.set_bit(q1 as u64, q0_lost); } + Ok(()) } - fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { + fn capture_quantum_state(&mut self) -> Result<(Vec<(BigUint, Complex)>, usize), String> { let (state, count) = self.sim.get_state(); // Because the simulator returns the state indices with opposite endianness from the // expected one, we need to reverse the bit order of the indices. @@ -995,12 +1049,12 @@ impl Backend for SparseSim { }) .collect::>(); new_state.sort_unstable_by(|a, b| a.0.cmp(&b.0)); - (new_state, count) + Ok((new_state, count)) } - fn qubit_is_zero(&mut self, q: usize) -> bool { + fn qubit_is_zero(&mut self, q: usize) -> Result { // This is a service function rather than a measurement so it doesn't incur noise. - self.sim.qubit_is_zero(q) + Ok(self.sim.qubit_is_zero(q)) } fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option> { @@ -1123,6 +1177,274 @@ impl Backend for SparseSim { } } +/// Default backend used when targeting Clifford simulation. +pub struct CliffordSim { + sim: StabilizerSimulator, + num_qubits: usize, + qubit_id_map: IndexMap, + is_noisy: bool, +} + +impl CliffordSim { + #[must_use] + pub fn new(num_qubits: usize) -> Self { + let seed = rand::thread_rng().next_u32(); + Self { + sim: StabilizerSimulator::new( + num_qubits, + 1, + seed, + CumulativeNoiseConfig::default().into(), + ), + num_qubits, + qubit_id_map: IndexMap::new(), + is_noisy: false, + } + } + + #[must_use] + pub fn new_with_noise_config( + num_qubits: usize, + noise_config: CumulativeNoiseConfig, + ) -> Self { + let seed = rand::thread_rng().next_u32(); + Self { + sim: StabilizerSimulator::new(num_qubits, 1, seed, noise_config.into()), + num_qubits, + qubit_id_map: IndexMap::new(), + is_noisy: true, + } + } +} + +impl Backend for CliffordSim { + fn cx(&mut self, ctl: usize, q: usize) -> Result<(), String> { + let (ctl_id, q_id) = (self.qubit_id_map[ctl], self.qubit_id_map[q]); + self.sim.cx(ctl_id, q_id); + Ok(()) + } + + fn cy(&mut self, ctl: usize, q: usize) -> Result<(), String> { + let (ctl_id, q_id) = (self.qubit_id_map[ctl], self.qubit_id_map[q]); + self.sim.cy(ctl_id, q_id); + Ok(()) + } + + fn cz(&mut self, ctl: usize, q: usize) -> Result<(), String> { + let (ctl_id, q_id) = (self.qubit_id_map[ctl], self.qubit_id_map[q]); + self.sim.cz(ctl_id, q_id); + Ok(()) + } + + fn h(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.h(q_id); + Ok(()) + } + + fn m(&mut self, q: usize) -> Result { + let q_id = self.qubit_id_map[q]; + self.sim.mz(q_id, 0); + let res = self + .sim + .measurements() + .last() + .expect("simulation should have one measurement"); + match res { + MeasurementResult::Zero => Ok(val::Result::Val(false)), + MeasurementResult::One => Ok(val::Result::Val(true)), + MeasurementResult::Loss => Ok(val::Result::Loss), + } + } + + fn mresetz(&mut self, q: usize) -> Result { + let q_id = self.qubit_id_map[q]; + self.sim.mresetz(q_id, 0); + let res = self + .sim + .measurements() + .last() + .expect("simulation should have one measurement"); + match res { + MeasurementResult::Zero => Ok(val::Result::Val(false)), + MeasurementResult::One => Ok(val::Result::Val(true)), + MeasurementResult::Loss => Ok(val::Result::Loss), + } + } + + fn reset(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.resetz(q_id); + Ok(()) + } + + fn rx(&mut self, theta: f64, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + check_normalized_angle(theta)?; + self.sim.rx(theta, q_id); + Ok(()) + } + + fn rxx(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + let (q0_id, q1_id) = (self.qubit_id_map[q0], self.qubit_id_map[q1]); + check_normalized_angle(theta)?; + self.sim.rxx(theta, q0_id, q1_id); + Ok(()) + } + + fn ry(&mut self, theta: f64, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + check_normalized_angle(theta)?; + self.sim.ry(theta, q_id); + Ok(()) + } + + fn ryy(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + let (q0_id, q1_id) = (self.qubit_id_map[q0], self.qubit_id_map[q1]); + check_normalized_angle(theta)?; + self.sim.ryy(theta, q0_id, q1_id); + Ok(()) + } + + fn rz(&mut self, theta: f64, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + check_normalized_angle(theta)?; + self.sim.rz(theta, q_id); + Ok(()) + } + + fn rzz(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + let (q0_id, q1_id) = (self.qubit_id_map[q0], self.qubit_id_map[q1]); + check_normalized_angle(theta)?; + self.sim.rzz(theta, q0_id, q1_id); + Ok(()) + } + + fn sadj(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.s_adj(q_id); + Ok(()) + } + + fn s(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.s(q_id); + Ok(()) + } + + fn sx(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.sx(q_id); + Ok(()) + } + + fn swap(&mut self, q0: usize, q1: usize) -> Result<(), String> { + let (q0_id, q1_id) = (self.qubit_id_map[q0], self.qubit_id_map[q1]); + self.sim.swap(q0_id, q1_id); + Ok(()) + } + + fn x(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.x(q_id); + Ok(()) + } + + fn y(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.y(q_id); + Ok(()) + } + + fn z(&mut self, q: usize) -> Result<(), String> { + let q_id = self.qubit_id_map[q]; + self.sim.z(q_id); + Ok(()) + } + + fn qubit_allocate(&mut self) -> Result { + let sorted_keys: Vec = self.qubit_id_map.iter().map(|(k, _)| k).collect(); + if sorted_keys.len() >= self.num_qubits { + return Err("qubit limit exceeded".to_string()); + } + let mut sorted_vals: Vec<&usize> = self.qubit_id_map.values().collect(); + sorted_vals.sort_unstable(); + let new_key = sorted_keys + .iter() + .enumerate() + .take_while(|(index, key)| index == *key) + .last() + .map_or(0_usize, |(_, &key)| key + 1); + let new_val = sorted_vals + .iter() + .enumerate() + .take_while(|(index, val)| index == **val) + .last() + .map_or(0_usize, |(_, &&val)| val + 1); + self.qubit_id_map.insert(new_key, new_val); + Ok(new_key) + } + + fn qubit_release(&mut self, q: usize) -> Result { + let is_zero = self.mresetz(q).expect("mresetz should not fail"); + self.qubit_id_map.remove(q); + // We return true for released qubits if simulation is noisy or if the qubit is known to be in the zero state. + Ok(self.is_noisy || !matches!(is_zero, val::Result::Val(true))) + } + + fn qubit_swap_id(&mut self, q0: usize, q1: usize) -> Result<(), String> { + let q0_id = self.qubit_id_map[q0]; + let q1_id = self.qubit_id_map[q1]; + self.qubit_id_map.insert(q0, q1_id); + self.qubit_id_map.insert(q1, q0_id); + Ok(()) + } + + fn t(&mut self, _q: usize) -> Result<(), String> { + Err("T gate is not supported in Clifford simulation".to_string()) + } + + fn tadj(&mut self, _q: usize) -> Result<(), String> { + Err("adjoint T gate is not supported in Clifford simulation".to_string()) + } + + fn custom_intrinsic(&mut self, name: &str, _arg: Value) -> Option> { + match name { + "BeginEstimateCaching" => Some(Ok(Value::Bool(true))), + "GlobalPhase" + | "EndEstimateCaching" + | "AccountForEstimatesInternal" + | "BeginRepeatEstimatesInternal" + | "EndRepeatEstimatesInternal" + | "EnableMemoryComputeArchitecture" => Some(Ok(Value::unit())), + "ConfigurePauliNoise" => Some(Err( + "dynamic noise configuration not supported in Clifford simulation".to_string(), + )), + "ConfigureQubitLoss" => Some(Err( + "dynamic qubit loss configuration not supported in Clifford simulation".to_string(), + )), + "ApplyIdleNoise" => Some(Err( + "idle noise application not supported in Clifford simulation".to_string(), + )), + "Apply" => Some(Err( + "arbitrary unitary application not supported in Clifford simulation".to_string(), + )), + "PostSelectZ" => Some(Err( + "post-selection not supported in Clifford simulation".to_string() + )), + _ => None, + } + } + + fn set_seed(&mut self, seed: Option) { + if let Some(seed) = seed { + self.sim.set_seed(seed); + } else { + self.sim.set_seed(rand::thread_rng().next_u64()); + } + } +} + fn unwrap_matrix_as_array2(matrix: Value, qubits: &[usize]) -> Array2> { let matrix: Vec>> = matrix .unwrap_array() @@ -1143,3 +1465,20 @@ fn unwrap_matrix_as_array2(matrix: Value, qubits: &[usize]) -> Array2 Result<(), String> { + let mut normalized_angle = theta % (TAU); + if normalized_angle < 0.0 { + normalized_angle += TAU; + } + if normalized_angle.is_nearly_zero() + || (normalized_angle - TAU).is_nearly_zero() + || (normalized_angle - FRAC_PI_2).is_nearly_zero() + || (normalized_angle - PI).is_nearly_zero() + || (normalized_angle - 3.0 * FRAC_PI_2).is_nearly_zero() + { + Ok(()) + } else { + Err("angle must be a multiple of PI/2 in Clifford simulation".to_string()) + } +} diff --git a/source/compiler/qsc_eval/src/backend/noise_tests.rs b/source/compiler/qsc_eval/src/backend/noise_tests.rs index c9c2cb6261..1c046f6411 100644 --- a/source/compiler/qsc_eval/src/backend/noise_tests.rs +++ b/source/compiler/qsc_eval/src/backend/noise_tests.rs @@ -80,17 +80,23 @@ fn noiseless_gate() { let noise = PauliNoise::from_probabilities(0.0, 0.0, 0.0) .expect("noiseless Pauli noise should be constructable."); let mut sim = SparseSim::new_with_noise(&noise); - let q = sim.qubit_allocate(); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); for _ in 0..100 { - sim.x(q); - let res1 = sim.m(q).unwrap_bool(); + let _ = sim.x(q); + let res1 = sim + .m(q) + .expect("sparse simulator is infinite") + .unwrap_bool(); assert!(res1, "Expected True without noise."); - sim.x(q); - let res2 = sim.m(q).unwrap_bool(); + let _ = sim.x(q); + let res2 = sim + .m(q) + .expect("sparse simulator is infinite") + .unwrap_bool(); assert!(!res2, "Expected False without noise."); } assert!( - sim.qubit_release(q), + sim.qubit_release(q).expect("sparse simulator is infinite"), "Expected correct qubit state on release." ); } @@ -101,15 +107,21 @@ fn bitflip_measurement() { .expect("bit flip noise with probability 100% should be constructable."); let mut sim = SparseSim::new_with_noise(&noise); assert!(!sim.is_noiseless(), "Expected noisy simulator."); - let q = sim.qubit_allocate(); // Allocation is noiseless even with noise. + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); // Allocation is noiseless even with noise. for _ in 0..100 { - let res1 = sim.m(q).unwrap_bool(); + let res1 = sim + .m(q) + .expect("sparse simulator is infinite") + .unwrap_bool(); assert!(res1, "Expected True for 100% bit flip noise."); - let res2 = sim.m(q).unwrap_bool(); + let res2 = sim + .m(q) + .expect("sparse simulator is infinite") + .unwrap_bool(); assert!(!res2, "Expected False for 100% bit flip noise."); } assert!( - sim.qubit_release(q), + sim.qubit_release(q).expect("sparse simulator is infinite"), "Expected correct qubit state on release." ); } @@ -123,12 +135,16 @@ fn noisy_measurement() { sim.set_seed(Some(0)); let mut true_count = 0; for _ in 0..1000 { - let q = sim.qubit_allocate(); // Allocation is noiseless even with noise. + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); // Allocation is noiseless even with noise. // sim.m sometimes applies X before measuring - if sim.m(q).unwrap_bool() { + if sim + .m(q) + .expect("sparse simulator is infinite") + .unwrap_bool() + { true_count += 1; } - sim.qubit_release(q); + sim.qubit_release(q).expect("sparse simulator is infinite"); } assert!( true_count > 200 && true_count < 400, @@ -153,7 +169,9 @@ pub fn state_to_string(input: &(Vec<(BigUint, Complex)>, usize)) -> String } fn check_state(sim: &mut SparseSim, expected: &Expect) { - let state = sim.capture_quantum_state(); + let state = sim + .capture_quantum_state() + .expect("sparse simulator is infinite"); expected.assert_eq(&state_to_string(&state)); } @@ -163,13 +181,13 @@ fn noisy_via_x() { .expect("bit flip noise with probability 100% should be constructable."); let mut sim = SparseSim::new_with_noise(&noise); assert!(!sim.is_noiseless(), "Expected noisy simulator."); - let q = sim.qubit_allocate(); // Allocation is noiseless even with noise. + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); // Allocation is noiseless even with noise. check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]); - sim.x(q); // Followed by X. So, no op. + let _ = sim.x(q); // Followed by X. So, no op. check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]); - sim.y(q); // Followed by X. + let _ = sim.y(q); // Followed by X. check_state(&mut sim, &expect!["|0⟩: 0.0000+1.0000𝑖 "]); - sim.z(q); // Followed by X. + let _ = sim.z(q); // Followed by X. check_state(&mut sim, &expect!["|1⟩: 0.0000+1.0000𝑖 "]); } @@ -179,13 +197,13 @@ fn noisy_via_y() { .expect("0.0, 1.0, 0.0 Pauli noise should be constructable."); let mut sim = SparseSim::new_with_noise(&noise); assert!(!sim.is_noiseless(), "Expected noisy simulator."); - let q = sim.qubit_allocate(); // Allocation is noiseless even with noise. + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); // Allocation is noiseless even with noise. check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]); - sim.x(q); // Followed by Y. + let _ = sim.x(q); // Followed by Y. check_state(&mut sim, &expect!["|0⟩: 0.0000βˆ’1.0000𝑖 "]); - sim.y(q); // Followed by Y. So, no op. + let _ = sim.y(q); // Followed by Y. So, no op. check_state(&mut sim, &expect!["|0⟩: 0.0000βˆ’1.0000𝑖 "]); - sim.z(q); // Followed by Y. + let _ = sim.z(q); // Followed by Y. check_state(&mut sim, &expect!["|1⟩: 1.0000+0.0000𝑖 "]); } @@ -195,21 +213,21 @@ fn noisy_via_z() { .expect("phase flip noise with probability 100% should be constructable."); let mut sim = SparseSim::new_with_noise(&noise); assert!(!sim.is_noiseless(), "Expected noisy simulator."); - let q = sim.qubit_allocate(); // Allocation is noiseless even with noise. + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); // Allocation is noiseless even with noise. check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]); - sim.x(q); // Followed by Z. + let _ = sim.x(q); // Followed by Z. check_state(&mut sim, &expect!["|1⟩: βˆ’1.0000+0.0000𝑖 "]); - sim.y(q); // Followed by Z. + let _ = sim.y(q); // Followed by Z. check_state(&mut sim, &expect!["|0⟩: 0.0000+1.0000𝑖 "]); - sim.z(q); // Followed by Z. So, no op. + let _ = sim.z(q); // Followed by Z. So, no op. check_state(&mut sim, &expect!["|0⟩: 0.0000+1.0000𝑖 "]); } #[test] fn measure_without_loss_returns_value() { let mut sim = SparseSim::new(); - let q = sim.qubit_allocate(); - let res = sim.m(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let res = sim.m(q).expect("sparse simulator is infinite"); assert!( matches!(res, val::Result::Val(_)), "Expected measurement to return a result" @@ -220,8 +238,8 @@ fn measure_without_loss_returns_value() { fn measure_with_loss_returns_loss() { let mut sim = SparseSim::new(); sim.set_loss(1.0); // Set loss probability to 100% - let q = sim.qubit_allocate(); - let res = sim.m(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let res = sim.m(q).expect("sparse simulator is infinite"); assert_eq!( res, val::Result::Loss, @@ -270,8 +288,8 @@ fn noise_config_x_gate_with_x_fault() { // X gate followed by 100% X fault = identity (X * X = I) let config = noise_config_with_single_qubit_fault(|c, t| c.x = t, "X"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.x(q); // X then X fault => |0⟩ + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.x(q); // X then X fault => |0⟩ check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]); } @@ -280,8 +298,8 @@ fn noise_config_x_gate_with_z_fault() { // X gate followed by 100% Z fault = ZX|0⟩ = Z|1⟩ = -|1⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.x = t, "Z"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.x(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.x(q); check_state(&mut sim, &expect!["|1⟩: βˆ’1.0000+0.0000𝑖 "]); } @@ -290,8 +308,8 @@ fn noise_config_x_gate_with_y_fault() { // X gate followed by 100% Y fault = YX|0⟩ = Y|1⟩ = -i|0⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.x = t, "Y"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.x(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.x(q); check_state(&mut sim, &expect!["|0⟩: 0.0000βˆ’1.0000𝑖 "]); } @@ -300,8 +318,8 @@ fn noise_config_h_gate_with_y_fault() { // H|0⟩ = |+⟩, then Y|+⟩ = i|βˆ’βŸ© let config = noise_config_with_single_qubit_fault(|c, t| c.h = t, "Y"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.h(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.h(q); check_state( &mut sim, &expect!["|0⟩: 0.0000+0.7071𝑖 |1⟩: 0.0000βˆ’0.7071𝑖 "], @@ -313,8 +331,8 @@ fn noise_config_h_gate_with_z_fault() { // H|0⟩ = |+⟩, then Z|+⟩ = |βˆ’βŸ© let config = noise_config_with_single_qubit_fault(|c, t| c.h = t, "Z"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.h(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.h(q); check_state( &mut sim, &expect!["|0⟩: 0.7071+0.0000𝑖 |1⟩: βˆ’0.7071+0.0000𝑖 "], @@ -326,8 +344,8 @@ fn noise_config_y_gate_with_y_fault() { // Y gate followed by 100% Y fault = Y*Y = I let config = noise_config_with_single_qubit_fault(|c, t| c.y = t, "Y"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.y(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.y(q); check_state(&mut sim, &expect!["|0⟩: 1.0000+0.0000𝑖 "]); } @@ -336,8 +354,8 @@ fn noise_config_z_gate_with_x_fault() { // Z|0⟩ = |0⟩, then X|0⟩ = |1⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.z = t, "X"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.z(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.z(q); check_state(&mut sim, &expect!["|1⟩: 1.0000+0.0000𝑖 "]); } @@ -346,8 +364,8 @@ fn noise_config_s_gate_with_x_fault() { // S|0⟩ = |0⟩ (S only adds phase to |1⟩), then X|0⟩ = |1⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.s = t, "X"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.s(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.s(q); check_state(&mut sim, &expect!["|1⟩: 1.0000+0.0000𝑖 "]); } @@ -356,8 +374,8 @@ fn noise_config_s_gate_with_y_fault() { // S|0⟩ = |0⟩, then Y|0⟩ = i|1⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.s = t, "Y"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.s(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.s(q); check_state(&mut sim, &expect!["|1⟩: 0.0000+1.0000𝑖 "]); } @@ -366,8 +384,8 @@ fn noise_config_t_gate_with_x_fault() { // T|0⟩ = |0⟩ (T only adds phase to |1⟩), then X|0⟩ = |1⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.t = t, "X"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.t(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.t(q); check_state(&mut sim, &expect!["|1⟩: 1.0000+0.0000𝑖 "]); } @@ -376,8 +394,8 @@ fn noise_config_sadj_gate_with_y_fault() { // Sadj|0⟩ = |0⟩, then Y|0⟩ = i|1⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.s_adj = t, "Y"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.sadj(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.sadj(q); check_state(&mut sim, &expect!["|1⟩: 0.0000+1.0000𝑖 "]); } @@ -386,8 +404,8 @@ fn noise_config_tadj_gate_with_y_fault() { // Tadj|0⟩ = |0⟩, then Y|0⟩ = i|1⟩ let config = noise_config_with_single_qubit_fault(|c, t| c.t_adj = t, "Y"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.tadj(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.tadj(q); check_state(&mut sim, &expect!["|1⟩: 0.0000+1.0000𝑖 "]); } @@ -397,8 +415,11 @@ fn noise_config_mz_with_x_fault() { // so it measures as True (|1⟩). let config = noise_config_with_single_qubit_fault(|c, t| c.mz = t, "X"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - let res = sim.m(q).unwrap_bool(); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let res = sim + .m(q) + .expect("sparse simulator is infinite") + .unwrap_bool(); assert!( res, "Expected True: X fault flips |0⟩ to |1⟩ before measurement." @@ -410,8 +431,11 @@ fn noise_config_mz_with_z_fault() { // Measurement with 100% Z fault: Z|0⟩ = |0⟩, so measurement is still False. let config = noise_config_with_single_qubit_fault(|c, t| c.mz = t, "Z"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - let res = sim.m(q).unwrap_bool(); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let res = sim + .m(q) + .expect("sparse simulator is infinite") + .unwrap_bool(); assert!( !res, "Expected False: Z fault on |0⟩ doesn't change measurement outcome." @@ -425,9 +449,9 @@ fn noise_config_cx_gate_with_xi_fault() { // CX(ctl, tgt) on |00⟩ = |00⟩, then XI fault: X on control, I on target => |10⟩ let config = noise_config_with_two_qubit_fault(|c, t| c.cx = t, "XI"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let ctl = sim.qubit_allocate(); - let tgt = sim.qubit_allocate(); - sim.cx(ctl, tgt); + let ctl = sim.qubit_allocate().expect("sparse simulator is infinite"); + let tgt = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.cx(ctl, tgt); check_state(&mut sim, &expect!["|10⟩: 1.0000+0.0000𝑖 "]); } @@ -436,9 +460,9 @@ fn noise_config_cx_gate_with_ix_fault() { // CX(ctl, tgt) on |00⟩ = |00⟩, then IX fault: I on control, X on target => |01⟩ let config = noise_config_with_two_qubit_fault(|c, t| c.cx = t, "IX"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let ctl = sim.qubit_allocate(); - let tgt = sim.qubit_allocate(); - sim.cx(ctl, tgt); + let ctl = sim.qubit_allocate().expect("sparse simulator is infinite"); + let tgt = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.cx(ctl, tgt); check_state(&mut sim, &expect!["|01⟩: 1.0000+0.0000𝑖 "]); } @@ -447,9 +471,9 @@ fn noise_config_cx_gate_with_xx_fault() { // CX(ctl, tgt) on |00⟩ = |00⟩, then XX fault: X on both => |11⟩ let config = noise_config_with_two_qubit_fault(|c, t| c.cx = t, "XX"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let ctl = sim.qubit_allocate(); - let tgt = sim.qubit_allocate(); - sim.cx(ctl, tgt); + let ctl = sim.qubit_allocate().expect("sparse simulator is infinite"); + let tgt = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.cx(ctl, tgt); check_state(&mut sim, &expect!["|11⟩: 1.0000+0.0000𝑖 "]); } @@ -459,9 +483,9 @@ fn noise_config_cz_gate_with_xy_fault() { // X|0⟩ = |1⟩, Y|0⟩ = i|1⟩ => |1i1⟩ let config = noise_config_with_two_qubit_fault(|c, t| c.cz = t, "XY"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q0 = sim.qubit_allocate(); - let q1 = sim.qubit_allocate(); - sim.cz(q0, q1); + let q0 = sim.qubit_allocate().expect("sparse simulator is infinite"); + let q1 = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.cz(q0, q1); check_state(&mut sim, &expect!["|11⟩: 0.0000+1.0000𝑖 "]); } @@ -470,10 +494,10 @@ fn noise_config_swap_gate_with_xx_fault() { // Prepare |10⟩, SWAP => |01⟩, then XX fault => |10⟩ again let config = noise_config_with_two_qubit_fault(|c, t| c.swap = t, "XX"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q0 = sim.qubit_allocate(); - let q1 = sim.qubit_allocate(); - sim.x(q0); // |10⟩ (x gate has no noise configured) - sim.swap(q0, q1); // SWAP => |01⟩, then XX => |10⟩ + let q0 = sim.qubit_allocate().expect("sparse simulator is infinite"); + let q1 = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.x(q0); // |10⟩ (x gate has no noise configured) + let _ = sim.swap(q0, q1); // SWAP => |01⟩, then XX => |10⟩ check_state(&mut sim, &expect!["|10⟩: 1.0000+0.0000𝑖 "]); } @@ -484,12 +508,12 @@ fn noise_config_only_affects_configured_gate() { // Configure X fault only on H gate; X gate should be noiseless. let config = noise_config_with_single_qubit_fault(|c, t| c.h = t, "X"); let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); // X gate has no noise configured, so X|0⟩ = |1⟩ without any fault - sim.x(q); + let _ = sim.x(q); check_state(&mut sim, &expect!["|1⟩: 1.0000+0.0000𝑖 "]); // Now apply H (which has 100% X fault): H|1⟩ = |βˆ’βŸ©, then X|βˆ’βŸ© = βˆ’|βˆ’βŸ© - sim.h(q); + let _ = sim.h(q); check_state( &mut sim, &expect!["|0⟩: βˆ’0.7071+0.0000𝑖 |1⟩: 0.7071+0.0000𝑖 "], @@ -508,8 +532,8 @@ fn noise_config_mz_with_loss() { loss: 1.0, }; let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - let res = sim.m(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let res = sim.m(q).expect("sparse simulator is infinite"); assert_eq!( res, val::Result::Loss, @@ -529,9 +553,9 @@ fn noise_config_gate_loss_causes_measurement_loss() { loss: 1.0, }; let mut sim = SparseSim::new_with_noise_config(config.into()); - let q = sim.qubit_allocate(); - sim.x(q); - let res = sim.m(q); + let q = sim.qubit_allocate().expect("sparse simulator is infinite"); + let _ = sim.x(q); + let res = sim.m(q).expect("sparse simulator is infinite"); assert_eq!( res, val::Result::Loss, diff --git a/source/compiler/qsc_eval/src/intrinsic.rs b/source/compiler/qsc_eval/src/intrinsic.rs index 5047bae140..4688754253 100644 --- a/source/compiler/qsc_eval/src/intrinsic.rs +++ b/source/compiler/qsc_eval/src/intrinsic.rs @@ -56,7 +56,9 @@ pub(crate) fn call( } } "DumpMachine" => { - let (state, qubit_count) = sim.capture_quantum_state(); + let (state, qubit_count) = sim + .capture_quantum_state() + .map_err(|e| Error::SimulationError(e, name_span))?; match out.state(state, qubit_count) { Ok(()) => Ok(Value::unit()), Err(_) => Err(Error::OutputFail(name_span)), @@ -75,7 +77,9 @@ pub(crate) fn call( if qubits.len() != qubits.iter().collect::>().len() { return Err(Error::QubitUniqueness(arg_span)); } - let (state, qubit_count) = sim.capture_quantum_state(); + let (state, qubit_count) = sim + .capture_quantum_state() + .map_err(|e| Error::SimulationError(e, name_span))?; let state = utils::split_state(&qubits, &state, qubit_count) .map_err(|()| Error::QubitsNotSeparable(arg_span))?; match out.state(state, qubits.len()) { @@ -96,7 +100,9 @@ pub(crate) fn call( if qubits.len() != qubits.iter().collect::>().len() { return Err(Error::QubitUniqueness(arg_span)); } - let (state, qubit_count) = sim.capture_quantum_state(); + let (state, qubit_count) = sim + .capture_quantum_state() + .map_err(|e| Error::SimulationError(e, name_span))?; let state = utils::split_state(&qubits, &state, qubit_count) .map_err(|()| Error::QubitsNotSeparable(arg_span))?; let matrix = utils::state_to_matrix(state, qubits.len() / 2); @@ -105,8 +111,8 @@ pub(crate) fn call( Err(_) => Err(Error::OutputFail(name_span)), } } - "PermuteLabels" => qubit_relabel(arg, arg_span, |q0, q1| { - sim.qubit_swap_id(q0, q1, call_stack); + "PermuteLabels" => qubit_relabel(arg, name_span, arg_span, |q0, q1| { + sim.qubit_swap_id(q0, q1, call_stack) }), "Message" => match out.message(&arg.unwrap_string()) { Ok(()) => Ok(Value::unit()), @@ -118,7 +124,8 @@ pub(crate) fn call( .try_deref() .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, - ), + ) + .map_err(|e| Error::SimulationError(e, name_span))?, )), "ArcCos" => Ok(Value::Double(arg.unwrap_double().acos())), "ArcSin" => Ok(Value::Double(arg.unwrap_double().asin())), @@ -164,55 +171,98 @@ pub(crate) fn call( "__quantum__qis__ccx__body" => three_qubit_gate( |ctl0, ctl1, q| sim.ccx(ctl0, ctl1, q, call_stack), arg, + name_span, + arg_span, + ), + "__quantum__qis__cx__body" => two_qubit_gate( + |ctl, q| sim.cx(ctl, q, call_stack), + arg, + name_span, + arg_span, + ), + "__quantum__qis__cy__body" => two_qubit_gate( + |ctl, q| sim.cy(ctl, q, call_stack), + arg, + name_span, + arg_span, + ), + "__quantum__qis__cz__body" => two_qubit_gate( + |ctl, q| sim.cz(ctl, q, call_stack), + arg, + name_span, + arg_span, + ), + "__quantum__qis__rx__body" => one_qubit_rotation( + |theta, q| sim.rx(theta, q, call_stack), + arg, + name_span, arg_span, ), - "__quantum__qis__cx__body" => { - two_qubit_gate(|ctl, q| sim.cx(ctl, q, call_stack), arg, arg_span) - } - "__quantum__qis__cy__body" => { - two_qubit_gate(|ctl, q| sim.cy(ctl, q, call_stack), arg, arg_span) - } - "__quantum__qis__cz__body" => { - two_qubit_gate(|ctl, q| sim.cz(ctl, q, call_stack), arg, arg_span) - } - "__quantum__qis__rx__body" => { - one_qubit_rotation(|theta, q| sim.rx(theta, q, call_stack), arg, arg_span) - } "__quantum__qis__rxx__body" => two_qubit_rotation( |theta, q0, q1| sim.rxx(theta, q0, q1, call_stack), arg, + name_span, + arg_span, + ), + "__quantum__qis__ry__body" => one_qubit_rotation( + |theta, q| sim.ry(theta, q, call_stack), + arg, + name_span, arg_span, ), - "__quantum__qis__ry__body" => { - one_qubit_rotation(|theta, q| sim.ry(theta, q, call_stack), arg, arg_span) - } "__quantum__qis__ryy__body" => two_qubit_rotation( |theta, q0, q1| sim.ryy(theta, q0, q1, call_stack), arg, + name_span, + arg_span, + ), + "__quantum__qis__rz__body" => one_qubit_rotation( + |theta, q| sim.rz(theta, q, call_stack), + arg, + name_span, arg_span, ), - "__quantum__qis__rz__body" => { - one_qubit_rotation(|theta, q| sim.rz(theta, q, call_stack), arg, arg_span) - } "__quantum__qis__rzz__body" => two_qubit_rotation( |theta, q0, q1| sim.rzz(theta, q0, q1, call_stack), arg, + name_span, arg_span, ), - "__quantum__qis__h__body" => one_qubit_gate(|q| sim.h(q, call_stack), arg, arg_span), - "__quantum__qis__s__body" => one_qubit_gate(|q| sim.s(q, call_stack), arg, arg_span), - "__quantum__qis__s__adj" => one_qubit_gate(|q| sim.sadj(q, call_stack), arg, arg_span), - "__quantum__qis__sx__body" => one_qubit_gate(|q| sim.sx(q, call_stack), arg, arg_span), - "__quantum__qis__t__body" => one_qubit_gate(|q| sim.t(q, call_stack), arg, arg_span), - "__quantum__qis__t__adj" => one_qubit_gate(|q| sim.tadj(q, call_stack), arg, arg_span), - "__quantum__qis__x__body" => one_qubit_gate(|q| sim.x(q, call_stack), arg, arg_span), - "__quantum__qis__y__body" => one_qubit_gate(|q| sim.y(q, call_stack), arg, arg_span), - "__quantum__qis__z__body" => one_qubit_gate(|q| sim.z(q, call_stack), arg, arg_span), - "__quantum__qis__swap__body" => { - two_qubit_gate(|q0, q1| sim.swap(q0, q1, call_stack), arg, arg_span) + "__quantum__qis__h__body" => { + one_qubit_gate(|q| sim.h(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__s__body" => { + one_qubit_gate(|q| sim.s(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__s__adj" => { + one_qubit_gate(|q| sim.sadj(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__sx__body" => { + one_qubit_gate(|q| sim.sx(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__t__body" => { + one_qubit_gate(|q| sim.t(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__t__adj" => { + one_qubit_gate(|q| sim.tadj(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__x__body" => { + one_qubit_gate(|q| sim.x(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__y__body" => { + one_qubit_gate(|q| sim.y(q, call_stack), arg, name_span, arg_span) } + "__quantum__qis__z__body" => { + one_qubit_gate(|q| sim.z(q, call_stack), arg, name_span, arg_span) + } + "__quantum__qis__swap__body" => two_qubit_gate( + |q0, q1| sim.swap(q0, q1, call_stack), + arg, + name_span, + arg_span, + ), "__quantum__qis__reset__body" => { - one_qubit_gate(|q| sim.reset(q, call_stack), arg, arg_span) + one_qubit_gate(|q| sim.reset(q, call_stack), arg, name_span, arg_span) } "__quantum__qis__m__body" => Ok(Value::Result( sim.m( @@ -221,7 +271,8 @@ pub(crate) fn call( .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, call_stack, - ), + ) + .map_err(|e| Error::SimulationError(e, name_span))?, )), "__quantum__qis__mresetz__body" => Ok(Value::Result( sim.mresetz( @@ -230,7 +281,8 @@ pub(crate) fn call( .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, call_stack, - ), + ) + .map_err(|e| Error::SimulationError(e, name_span))?, )), "__quantum__rt__read_loss" => Ok(Value::Bool(arg == Value::Result(val::Result::Loss))), _ => { @@ -256,8 +308,9 @@ pub(crate) fn call( } fn one_qubit_gate( - mut gate: impl FnMut(usize), + mut gate: impl FnMut(usize) -> Result<(), String>, arg: Value, + name_span: PackageSpan, arg_span: PackageSpan, ) -> Result { gate( @@ -265,13 +318,15 @@ fn one_qubit_gate( .try_deref() .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, - ); + ) + .map_err(|e| Error::SimulationError(e, name_span))?; Ok(Value::unit()) } fn two_qubit_gate( - mut gate: impl FnMut(usize, usize), + mut gate: impl FnMut(usize, usize) -> Result<(), String>, arg: Value, + name_span: PackageSpan, arg_span: PackageSpan, ) -> Result { let [x, y] = unwrap_tuple(arg); @@ -287,14 +342,16 @@ fn two_qubit_gate( .try_deref() .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, - ); + ) + .map_err(|e| Error::SimulationError(e, name_span))?; Ok(Value::unit()) } } fn one_qubit_rotation( - mut gate: impl FnMut(f64, usize), + mut gate: impl FnMut(f64, usize) -> Result<(), String>, arg: Value, + name_span: PackageSpan, arg_span: PackageSpan, ) -> Result { let [x, y] = unwrap_tuple(arg); @@ -308,14 +365,16 @@ fn one_qubit_rotation( .try_deref() .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, - ); + ) + .map_err(|e| Error::SimulationError(e, name_span))?; Ok(Value::unit()) } } fn three_qubit_gate( - mut gate: impl FnMut(usize, usize, usize), + mut gate: impl FnMut(usize, usize, usize) -> Result<(), String>, arg: Value, + name_span: PackageSpan, arg_span: PackageSpan, ) -> Result { let [x, y, z] = unwrap_tuple(arg); @@ -335,14 +394,16 @@ fn three_qubit_gate( .try_deref() .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, - ); + ) + .map_err(|e| Error::SimulationError(e, name_span))?; Ok(Value::unit()) } } fn two_qubit_rotation( - mut gate: impl FnMut(f64, usize, usize), + mut gate: impl FnMut(f64, usize, usize) -> Result<(), String>, arg: Value, + name_span: PackageSpan, arg_span: PackageSpan, ) -> Result { let [x, y, z] = unwrap_tuple(arg); @@ -362,7 +423,8 @@ fn two_qubit_rotation( .try_deref() .ok_or(Error::QubitUsedAfterRelease(arg_span))? .0, - ); + ) + .map_err(|e| Error::SimulationError(e, name_span))?; Ok(Value::unit()) } } @@ -372,8 +434,9 @@ fn two_qubit_rotation( /// if the qubits are not unique or if the relabeling is not a valid permutation. pub fn qubit_relabel( arg: Value, + name_span: PackageSpan, arg_span: PackageSpan, - mut swap: impl FnMut(usize, usize), + mut swap: impl FnMut(usize, usize) -> Result<(), String>, ) -> Result { let [left, right] = unwrap_tuple(arg); let left = left.unwrap_array(); @@ -424,7 +487,7 @@ pub fn qubit_relabel( .keys() .find(|k| mappings[*k] == r) .expect("mapped qubit should be present as both key and value"); - swap(l, label_r); + swap(l, label_r).map_err(|e| Error::SimulationError(e, name_span))?; mappings.insert(label_r, mapped_l); mappings.insert(l, mapped_r); } diff --git a/source/compiler/qsc_eval/src/intrinsic/tests.rs b/source/compiler/qsc_eval/src/intrinsic/tests.rs index 61ed73a6e8..3734a891bf 100644 --- a/source/compiler/qsc_eval/src/intrinsic/tests.rs +++ b/source/compiler/qsc_eval/src/intrinsic/tests.rs @@ -28,119 +28,119 @@ struct CustomSim { } impl Backend for CustomSim { - fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) { - self.sim.ccx(ctl0, ctl1, q); + fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) -> Result<(), String> { + self.sim.ccx(ctl0, ctl1, q) } - fn cx(&mut self, ctl: usize, q: usize) { - self.sim.cx(ctl, q); + fn cx(&mut self, ctl: usize, q: usize) -> Result<(), String> { + self.sim.cx(ctl, q) } - fn cy(&mut self, ctl: usize, q: usize) { - self.sim.cy(ctl, q); + fn cy(&mut self, ctl: usize, q: usize) -> Result<(), String> { + self.sim.cy(ctl, q) } - fn cz(&mut self, ctl: usize, q: usize) { - self.sim.cz(ctl, q); + fn cz(&mut self, ctl: usize, q: usize) -> Result<(), String> { + self.sim.cz(ctl, q) } - fn h(&mut self, q: usize) { - self.sim.h(q); + fn h(&mut self, q: usize) -> Result<(), String> { + self.sim.h(q) } - fn m(&mut self, q: usize) -> val::Result { + fn m(&mut self, q: usize) -> Result { self.sim.m(q) } - fn mresetz(&mut self, q: usize) -> val::Result { + fn mresetz(&mut self, q: usize) -> Result { self.sim.mresetz(q) } - fn reset(&mut self, q: usize) { - self.sim.reset(q); + fn reset(&mut self, q: usize) -> Result<(), String> { + self.sim.reset(q) } - fn rx(&mut self, theta: f64, q: usize) { - self.sim.rx(theta, q); + fn rx(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.sim.rx(theta, q) } - fn rxx(&mut self, theta: f64, q0: usize, q1: usize) { - self.sim.rxx(theta, q0, q1); + fn rxx(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.sim.rxx(theta, q0, q1) } - fn ry(&mut self, theta: f64, q: usize) { - self.sim.ry(theta, q); + fn ry(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.sim.ry(theta, q) } - fn ryy(&mut self, theta: f64, q0: usize, q1: usize) { - self.sim.ryy(theta, q0, q1); + fn ryy(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.sim.ryy(theta, q0, q1) } - fn rz(&mut self, theta: f64, q: usize) { - self.sim.rz(theta, q); + fn rz(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.sim.rz(theta, q) } - fn rzz(&mut self, theta: f64, q0: usize, q1: usize) { - self.sim.rzz(theta, q0, q1); + fn rzz(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.sim.rzz(theta, q0, q1) } - fn sadj(&mut self, q: usize) { - self.sim.sadj(q); + fn sadj(&mut self, q: usize) -> Result<(), String> { + self.sim.sadj(q) } - fn s(&mut self, q: usize) { - self.sim.s(q); + fn s(&mut self, q: usize) -> Result<(), String> { + self.sim.s(q) } - fn sx(&mut self, q: usize) { - self.sim.h(q); - self.sim.s(q); - self.sim.h(q); + fn sx(&mut self, q: usize) -> Result<(), String> { + self.sim.h(q)?; + self.sim.s(q)?; + self.sim.h(q) } - fn swap(&mut self, q0: usize, q1: usize) { - self.sim.swap(q0, q1); + fn swap(&mut self, q0: usize, q1: usize) -> Result<(), String> { + self.sim.swap(q0, q1) } - fn tadj(&mut self, q: usize) { - self.sim.tadj(q); + fn tadj(&mut self, q: usize) -> Result<(), String> { + self.sim.tadj(q) } - fn t(&mut self, q: usize) { - self.sim.t(q); + fn t(&mut self, q: usize) -> Result<(), String> { + self.sim.t(q) } - fn x(&mut self, q: usize) { - self.sim.x(q); + fn x(&mut self, q: usize) -> Result<(), String> { + self.sim.x(q) } - fn y(&mut self, q: usize) { - self.sim.y(q); + fn y(&mut self, q: usize) -> Result<(), String> { + self.sim.y(q) } - fn z(&mut self, q: usize) { - self.sim.z(q); + fn z(&mut self, q: usize) -> Result<(), String> { + self.sim.z(q) } - fn qubit_allocate(&mut self) -> usize { + fn qubit_allocate(&mut self) -> Result { self.sim.qubit_allocate() } - fn qubit_release(&mut self, q: usize) -> bool { + fn qubit_release(&mut self, q: usize) -> Result { self.sim.qubit_release(q) } - fn qubit_swap_id(&mut self, q0: usize, q1: usize) { - self.sim.qubit_swap_id(q0, q1); + fn qubit_swap_id(&mut self, q0: usize, q1: usize) -> Result<(), String> { + self.sim.qubit_swap_id(q0, q1) } fn capture_quantum_state( &mut self, - ) -> (Vec<(num_bigint::BigUint, num_complex::Complex)>, usize) { + ) -> Result<(Vec<(num_bigint::BigUint, num_complex::Complex)>, usize), String> { self.sim.capture_quantum_state() } - fn qubit_is_zero(&mut self, q: usize) -> bool { + fn qubit_is_zero(&mut self, q: usize) -> Result { self.sim.qubit_is_zero(q) } diff --git a/source/compiler/qsc_eval/src/lib.rs b/source/compiler/qsc_eval/src/lib.rs index 093bf625b4..26401135c6 100644 --- a/source/compiler/qsc_eval/src/lib.rs +++ b/source/compiler/qsc_eval/src/lib.rs @@ -178,6 +178,10 @@ pub enum Error { ))] ResultLossComparisonUnsupported(#[label("cannot compare result from qubit loss")] PackageSpan), + #[error("simulation error: {0}")] + #[diagnostic(code("Qsc.Eval.SimulationError"))] + SimulationError(String, #[label("simulation error")] PackageSpan), + #[error("name is not bound")] #[diagnostic(code("Qsc.Eval.UnboundName"))] UnboundName(#[label] PackageSpan), @@ -226,6 +230,7 @@ impl Error { | Error::ReleasedQubitNotZero(_, span) | Error::ResultComparisonUnsupported(span) | Error::ResultLossComparisonUnsupported(span) + | Error::SimulationError(_, span) | Error::UnboundName(span) | Error::UnknownIntrinsic(_, span) | Error::UnsupportedIntrinsicType(_, span) @@ -1326,7 +1331,9 @@ impl State { let name = &callee.name.name; let val = match name.as_ref() { "__quantum__rt__qubit_allocate" | "__quantum__rt__qubit_borrow" => { - let q = sim.qubit_allocate(&call_stack); + let q = sim + .qubit_allocate(&call_stack) + .map_err(|e| Error::SimulationError(e, callee_span))?; let q = Rc::new(Qubit(q)); env.track_qubit(Rc::clone(&q)); if let Some(counter) = &mut self.qubit_counter { @@ -1343,7 +1350,9 @@ impl State { .try_deref() .ok_or(Error::QubitDoubleRelease(arg_span))?; env.release_qubit(&qubit); - let is_zero = sim.qubit_release(qubit.0, &call_stack); + let is_zero = sim + .qubit_release(qubit.0, &call_stack) + .map_err(|e| Error::SimulationError(e, callee_span))?; let is_borrowed = self.dirty_qubits.remove(&qubit.0); if is_zero || is_borrowed { Value::unit() diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index 0994287cc9..8e532146c0 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -1781,8 +1781,9 @@ impl<'a> PartialEvaluator<'a> { callee_expr_span.span, ))); } - qubit_relabel(args_value, args_span, |q0, q1| { + qubit_relabel(args_value, callee_expr_span, args_span, |q0, q1| { self.resource_manager.swap_qubit_ids(q0, q1); + Ok(()) }) } .map_err(std::convert::Into::into), diff --git a/source/compiler/qsc_partial_eval/src/management.rs b/source/compiler/qsc_partial_eval/src/management.rs index 46f3355e65..b383d1fe7f 100644 --- a/source/compiler/qsc_partial_eval/src/management.rs +++ b/source/compiler/qsc_partial_eval/src/management.rs @@ -128,15 +128,17 @@ impl ResourceManager { pub struct QuantumIntrinsicsChecker {} impl Backend for QuantumIntrinsicsChecker { - fn qubit_is_zero(&mut self, _q: usize) -> bool { + fn qubit_is_zero(&mut self, _q: usize) -> std::result::Result { // Because `qubit_is_zero` is called on every qubit release, this must return // true to avoid a panic. - true + Ok(true) } // Needed for calls to `DumpMachine` and `DumpRegister`. - fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { - (Vec::new(), 0) + fn capture_quantum_state( + &mut self, + ) -> std::result::Result<(Vec<(BigUint, Complex)>, usize), String> { + Ok((Vec::new(), 0)) } // Only intrinsic functions are supported here since they're the only ones that will be classically evaluated. diff --git a/source/pip/qsharp/_native.pyi b/source/pip/qsharp/_native.pyi index 39359006ea..d1aab18ad8 100644 --- a/source/pip/qsharp/_native.pyi +++ b/source/pip/qsharp/_native.pyi @@ -2,7 +2,17 @@ # Licensed under the MIT License. from enum import Enum -from typing import Any, Callable, Optional, Dict, List, Tuple, TypedDict, overload +from typing import ( + Any, + Callable, + Optional, + Dict, + List, + Tuple, + TypedDict, + Literal, + overload, +) # pylint: disable=unused-argument # E302 is fighting with the formatter for number of blank lines @@ -187,6 +197,8 @@ class Interpreter: callable: Optional[GlobalCallable | Closure], args: Optional[Any], seed: Optional[int], + sim_type: Optional[Literal["sparse", "clifford"]], + num_qubits: Optional[int], ) -> Any: """ Runs the given Q# expression with an independent instance of the simulator. @@ -200,6 +212,9 @@ class Interpreter: :param callable: The callable to run, if no entry expression is provided. :param args: The arguments to pass to the callable, if any. :param seed: The seed to use for the random number generator in simulation, if any. + :param sim_type: The type of simulator to use. If not specified, the default sparse state vector simulation will be used. + :param num_qubits: The number of qubits to use for the simulation type "clifford". + If not specified, the Clifford simulator assumes a default of 1000 qubits. :returns values: A result or runtime errors. diff --git a/source/pip/qsharp/_qsharp.py b/source/pip/qsharp/_qsharp.py index 9837989b18..6e37c85ee3 100644 --- a/source/pip/qsharp/_qsharp.py +++ b/source/pip/qsharp/_qsharp.py @@ -32,6 +32,7 @@ List, Set, Iterable, + Literal, cast, ) from .estimator._estimator import ( @@ -779,6 +780,8 @@ def run( ] = None, qubit_loss: Optional[float] = None, seed: Optional[int] = None, + type: Optional[Literal["sparse", "clifford"]] = None, + num_qubits: Optional[int] = None, ) -> List[Any]: """ Runs the given Q# expression for the given number of shots. @@ -793,6 +796,9 @@ def run( :param noise: The noise to use in simulation. :param qubit_loss: The probability of qubit loss in simulation. :param seed: The seed to use for the random number generator in simulation, if any. + :param type: The type of simulator to use. If not specified, the default sparse state vector simulation will be used. + :param num_qubits: The number of qubits to use for the simulation type "clifford". + If not specified, the Clifford simulator assumes a default of 1000 qubits. :return: A list of results or runtime errors. If ``save_events`` is true, a list of ``ShotResult`` is returned. :rtype: List[Any] @@ -834,6 +840,12 @@ def on_save_events(output: Output) -> None: elif output.is_message(): results[-1]["messages"].append(str(output)) + if type is not None and type == "clifford": + if noise is not None and not isinstance(noise, NoiseConfig): + raise ValueError( + "only `NoiseConfig` is supported when using noise with the clifford simulator." + ) + callable = None run_entry_expr = None if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): @@ -871,6 +883,8 @@ def on_save_events(output: Output) -> None: callable, args, shot_seed, + type, + num_qubits, ) run_results = qsharp_value_to_python_value(run_results) results[-1]["result"] = run_results diff --git a/source/pip/qsharp/openqasm/_run.py b/source/pip/qsharp/openqasm/_run.py index 1b82cb41ff..63c08f4be6 100644 --- a/source/pip/qsharp/openqasm/_run.py +++ b/source/pip/qsharp/openqasm/_run.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. from time import monotonic -from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union, Literal from .._fs import read_file, list_directory, resolve from .._http import fetch_github from .._native import QasmError, Output, run_qasm_program # type: ignore @@ -41,6 +41,8 @@ def run( ] = None, qubit_loss: Optional[float] = None, as_bitstring: bool = False, + type: Optional[Literal["sparse", "clifford"]] = None, + num_qubits: Optional[int] = None, **kwargs: Any, ) -> List[Any]: """ @@ -66,6 +68,9 @@ def run( :type qubit_loss: float :param as_bitstring: If true, the result registers will be converted to bitstrings. :type as_bitstring: bool + :param type: The type of simulator to use. If not specified, the default sparse state vector simulation will be used. + :param num_qubits: The number of qubits to use for the simulation type "clifford". + If not specified, the Clifford simulator assumes a default of 1000 qubits. :param **kwargs: Additional keyword arguments for compiling the source program. Common options: - ``name`` (str): The name of the circuit. This is used as the entry point for the program. @@ -112,6 +117,12 @@ def on_save_events(output: Output) -> None: elif isinstance(source, str): source_str = source + if type is not None and type == "clifford": + if noise is not None and not isinstance(noise, NoiseConfig): + raise ValueError( + "only `NoiseConfig` is supported when using noise with the clifford simulator." + ) + noise_config = None if isinstance(noise, NoiseConfig): noise_config = noise @@ -136,6 +147,9 @@ def on_save_events(output: Output) -> None: qubit_loss=qubit_loss, callable=callable, args=args, + seed=kwargs.get("seed"), + sim_type=type, + num_qubits=num_qubits, ) results[-1]["result"] = run_results @@ -170,6 +184,10 @@ def on_save_events(output: Output) -> None: kwargs["search_path"] = "." kwargs["shots"] = shots + if type is not None: + kwargs["sim_type"] = type + if num_qubits is not None: + kwargs["num_qubits"] = num_qubits results = run_qasm_program( source_str, diff --git a/source/pip/src/interop.rs b/source/pip/src/interop.rs index 32b3d1b1c0..3d353210c3 100644 --- a/source/pip/src/interop.rs +++ b/source/pip/src/interop.rs @@ -18,13 +18,13 @@ use pyo3::types::{PyDict, PyList}; use qsc::circuit::TracerConfig; use qsc::hir::PackageId; use qsc::interpret::output::Receiver; -use qsc::interpret::{CircuitEntryPoint, Interpreter, into_errors}; +use qsc::interpret::{CircuitEntryPoint, Interpreter, SimType, into_errors}; use qsc::openqasm::compiler::compile_to_qsharp_ast_with_config; use qsc::openqasm::semantic::QasmSemanticParseResult; use qsc::openqasm::{OperationSignature, QubitSemantics}; use qsc::project::ProjectType; use qsc::target::Profile; -use qsc::{Backend, PackageType, PauliNoise, SparseSim}; +use qsc::{Backend, CliffordSim, PackageType, PauliNoise, SparseSim}; use qsc::{ LanguageFeatures, SourceMap, ast::Package, error::WithSource, interpret, project::FileSystem, }; @@ -97,6 +97,7 @@ pub(crate) fn run_qasm_program( let seed = get_seed(&kwargs); let shots = get_shots(&kwargs)?; let search_path = get_search_path(&kwargs)?; + let sim_type = get_sim_type(&kwargs)?; let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); let file_path = PathBuf::from_str(&search_path) @@ -147,6 +148,7 @@ pub(crate) fn run_qasm_program( noise_config.as_ref(), noise, loss, + sim_type, ); match result { Ok(result) => { @@ -160,6 +162,7 @@ pub(crate) fn run_qasm_program( } } +#[allow(clippy::too_many_arguments)] pub(crate) fn run_ast( interpreter: &mut Interpreter, receiver: &mut impl Receiver, @@ -168,24 +171,44 @@ pub(crate) fn run_ast( noise_config: Option<&qdk_simulators::noise_config::NoiseConfig>, noise: Option, loss: f64, + sim_type: SimType, ) -> Result, Vec> { let mut results = Vec::with_capacity(shots); for i in 0..shots { - let mut sim = if let Some(noise) = noise { - SparseSim::new_with_noise(&noise) - } else { - match noise_config { - Some(noise_config) => SparseSim::new_with_noise_config(noise_config.clone().into()), - None => SparseSim::new(), + let result = match sim_type { + SimType::Sparse => { + let mut sim = if let Some(noise) = noise { + SparseSim::new_with_noise(&noise) + } else { + match noise_config { + Some(noise_config) => { + SparseSim::new_with_noise_config(noise_config.clone().into()) + } + None => SparseSim::new(), + } + }; + if loss > 0.0 { + sim.set_loss(loss); + } + // If seed is provided, we want to use a different seed for each shot + // so that the results are different for each shot, but still deterministic + sim.set_seed(seed.map(|s| s + i as u64)); + interpreter.run_with_sim(&mut sim, receiver, None, None)? + } + SimType::Clifford(num_qubits) => { + let mut sim = match noise_config { + None => CliffordSim::new(num_qubits), + Some(noise_config) => { + CliffordSim::new_with_noise_config(num_qubits, noise_config.clone().into()) + } + }; + // If seed is provided, we want to use a different seed for each shot + // so that the results are different for each shot, but still deterministic + sim.set_seed(seed.map(|s| s + i as u64)); + interpreter.run_with_sim(&mut sim, receiver, None, None)? } }; - if loss > 0.0 { - sim.set_loss(loss); - } - // If seed is provided, we want to use a different seed for each shot - // so that the results are different for each shot, but still deterministic - sim.set_seed(seed.map(|s| s + i as u64)); - let result = interpreter.run_with_sim(&mut sim, receiver, None, None)?; + results.push(result); } @@ -801,6 +824,28 @@ where } } +/// Extracts the output semantics from the kwargs dictionary. +pub(crate) fn get_sim_type(kwargs: &Bound<'_, PyDict>) -> PyResult { + match kwargs.get_item("sim_type")? { + Some(obj) => Ok(match obj.extract::()?.as_str() { + "sparse" => SimType::Sparse, + "clifford" => { + // Clifford simulator needs a num_qubits, which defaults to 1000 if unspecified. + let num_qubits = kwargs + .get_item("num_qubits")? + .map_or_else(|| Ok(1000), |x| x.extract::())?; + SimType::Clifford(num_qubits) + } + other => { + return Err(PyException::new_err(format!( + "Invalid sim type specified: {other}" + ))); + } + }), + None => Ok(Default::default()), + } +} + /// Extracts the name from the kwargs dictionary. /// If the name is not present, returns "program". /// Otherwise, returns the name after sanitizing it. diff --git a/source/pip/src/interpreter.rs b/source/pip/src/interpreter.rs index 5605bb5d9c..456deb51c4 100644 --- a/source/pip/src/interpreter.rs +++ b/source/pip/src/interpreter.rs @@ -50,7 +50,7 @@ use qsc::{ fir::{self}, hir::ty::{Prim, Ty}, interpret::{ - self, CircuitEntryPoint, PauliNoise, TaggedItem, Value, + self, CircuitEntryPoint, PauliNoise, SimType, TaggedItem, Value, output::{Error, Receiver}, }, openqasm::{ @@ -711,7 +711,7 @@ impl Interpreter { } #[allow(clippy::too_many_arguments)] - #[pyo3(signature=(entry_expr=None, callback=None, noise_config=None, noise=None, qubit_loss=None, callable=None, args=None, seed=None))] + #[pyo3(signature=(entry_expr=None, callback=None, noise_config=None, noise=None, qubit_loss=None, callable=None, args=None, seed=None, sim_type=None, num_qubits=None))] fn run( &mut self, py: Python, @@ -723,6 +723,8 @@ impl Interpreter { callable: Option>, args: Option>, seed: Option, + sim_type: Option<&str>, + num_qubits: Option, ) -> PyResult> { let mut receiver = OptionalCallbackReceiver { callback, py }; @@ -744,6 +746,14 @@ impl Interpreter { let noise_config: Option> = noise_config.map(|noise_config| unbind_noise_config(py, noise_config)); + let sim_type = sim_type + .map(|s| match s { + "sparse" => SimType::Sparse, + "clifford" => SimType::Clifford(num_qubits.unwrap_or(1000)), + _ => panic!("invalid sim_type {s}"), + }) + .unwrap_or_default(); + let result = match callable_val { Some(callable) => { let (input_ty, output_ty) = self @@ -760,6 +770,7 @@ impl Interpreter { qubit_loss, noise_config, seed, + sim_type, ) } _ => self.interpreter.run( @@ -769,6 +780,7 @@ impl Interpreter { qubit_loss, noise_config, seed, + sim_type, ), }; diff --git a/source/pip/tests/test_clifford_simulator.py b/source/pip/tests/test_clifford_simulator.py index 40a1ddaffa..f2aa2c6425 100644 --- a/source/pip/tests/test_clifford_simulator.py +++ b/source/pip/tests/test_clifford_simulator.py @@ -2,9 +2,14 @@ # Licensed under the MIT License. from pathlib import Path +from collections import Counter +from typing import Sequence, cast import pyqir +import pytest +import math import qsharp +from qsharp import openqasm, QSharpError from qsharp._simulation import run_qir_clifford, NoiseConfig from qsharp._device._atom import NeutralAtomDevice from qsharp._device._atom._decomp import DecomposeRzAnglesToCliffordGates @@ -34,6 +39,18 @@ def read_file_relative(file_name: str) -> str: return Path(current_dir / file_name).read_text(encoding="utf-8") +def result_array_to_string(results: Sequence[Result]) -> str: + chars = [] + for value in results: + if value == Result.Zero: + chars.append("0") + elif value == Result.One: + chars.append("1") + else: + chars.append("-") + return "".join(chars) + + def test_smoke(): qsharp.init(target_profile=TargetProfile.Base) qsharp.eval(read_file_relative("CliffordIsing.qs")) @@ -207,3 +224,366 @@ def test_cy_direct_qir(): # This test should deterministically produce Zero. # If CZ or CX is executed instead of CY, then some measurements will produce One. assert all(shot[1] == Result.Zero for shot in output) + + +def test_clifford_run_no_noise(): + """Simple test that Clifford simulator works without noise.""" + qsharp.init(target_profile=TargetProfile.Base) + qsharp.eval(read_file_relative("CliffordIsing.qs")) + + output = qsharp.run( + "IsingModel2DEvolution(4, 4, PI() / 2.0, PI() / 2.0, 10.0, 10)", + 1, + type="clifford", + ) + print(output) + # Expecting deterministic output, no randomization seed needed. + assert output == [[Result.Zero] * 16], "Expected result of 0s with pi/2 angles." + + # Same execution should work with the operation itself. + output = qsharp.run( + qsharp.code.IsingModel2DEvolution, + 1, + 4, + 4, + math.pi / 2, + math.pi / 2, + 10.0, + 10, + type="clifford", + ) + print(output) + assert output == [[Result.Zero] * 16], "Expected result of 0s with pi/2 angles." + + +def test_clifford_run_bitflip_noise(): + """Bitflip noise for Clifford simulator.""" + qsharp.init(target_profile=TargetProfile.Base) + qsharp.eval(read_file_relative("CliffordIsing.qs")) + + p_noise = 0.005 + noise = NoiseConfig() + noise.rx.set_bitflip(p_noise) + noise.rzz.set_pauli_noise("XX", p_noise) + noise.mresetz.set_bitflip(p_noise) + + output = qsharp.run( + "IsingModel2DEvolution(4, 4, PI() / 2.0, PI() / 2.0, 10.0, 10)", + shots=1, + noise=noise, + seed=17, + type="clifford", + ) + result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] + print(result) + # Reasonable results obtained from manual run + assert result == ["0000001100000000"] + + # Same execution should work with the operation itself. + output = qsharp.run( + qsharp.code.IsingModel2DEvolution, + 1, + 4, + 4, + math.pi / 2, + math.pi / 2, + 10.0, + 10, + noise=noise, + seed=17, + type="clifford", + ) + result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] + print(result) + assert result == ["0000001100000000"] + + +def test_clifford_run_mixed_noise(): + qsharp.init(target_profile=TargetProfile.Base) + qsharp.eval(read_file_relative("CliffordIsing.qs")) + + noise = NoiseConfig() + noise.rz.set_bitflip(0.008) + noise.rz.loss = 0.005 + noise.rzz.set_depolarizing(0.008) + noise.rzz.loss = 0.005 + + output = qsharp.run( + "IsingModel2DEvolution(4, 4, PI() / 2.0, PI() / 2.0, 4.0, 4)", + shots=1, + noise=noise, + seed=234, + type="clifford", + ) + result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] + print(result) + # Reasonable results obtained from manual run + assert result == ["-000-01000100010"] + + +def test_clifford_run_isolated_loss(): + qsharp.init(target_profile=TargetProfile.Base) + program = """ +import Std.Math.PI; +operation Main() : Result[] { + use qs = Qubit[3]; + X(qs[0]); + X(qs[1]); + CNOT(qs[0], qs[1]); + // When loss is configured for X gate, qubit 2 should be unaffected. + Rx(PI() / 2.0, qs[2]); + Rx(PI() / 2.0, qs[2]); + MeasureEachZ(qs) +} + """ + qsharp.eval(program) + + noise = NoiseConfig() + noise.x.loss = 0.1 + + output = qsharp.run("Main()", shots=1000, noise=noise, type="clifford") + result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] + histogram = Counter(result) + total = sum(histogram.values()) + allowed_percent = { + "101": 0.81, + "1-1": 0.09, + "-11": 0.09, + "--1": 0.01, + } + tolerance = 0.2 * total + for bitstring, actual_count in histogram.items(): + assert ( + bitstring in allowed_percent + ), f"Unexpected measurement string: '{bitstring}'." + expected_count = allowed_percent[bitstring] * total + assert abs(actual_count - expected_count) <= tolerance, ( + f"Count for {bitstring} outside 20% tolerance. " + f"Actual={actual_count}, Expectedβ‰ˆ{expected_count:.0f}, Shots={total}." + ) + # We don't check for missing strings, as low-probability strings may not appear in finite shots. + + +def test_clifford_run_isolated_loss_and_noise(): + qsharp.init(target_profile=TargetProfile.Base) + program = """ +import Std.Math.PI; +operation Main() : Result[] { + use qs = Qubit[5]; + for _ in 1..100 { + X(qs[0]); + X(qs[1]); + CNOT(qs[0], qs[1]); + } + Rx(PI() / 2.0, qs[4]); + Rx(PI() / 2.0, qs[4]); + MeasureEachZ(qs) +} + """ + qsharp.eval(program) + + noise = NoiseConfig() + noise.x.set_bitflip(0.001) + noise.x.loss = 0.001 + + output = qsharp.run("Main()", shots=1000, noise=noise, type="clifford") + result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] + histogram = Counter(result) + total = sum(histogram.values()) + assert total > 0, "No measurement results recorded." + for bitstring in histogram: + assert bitstring.endswith("001"), f"Unexpected suffix in '{bitstring}'." + probability_00001 = histogram.get("00001", 0) / total + assert 0.5 < probability_00001 < 0.8, ( + f"Probability of 00001 outside expected range. " + f"Actual={probability_00001:.2%}, Shots={total}." + ) + + +def build_x_chain_qasm(n_instances: int, n_x: int) -> str: + # Construct multiple instances of x gate chains + prefix = f""" + OPENQASM 3.0; + include "stdgates.inc"; + bit[{n_instances}] c; + qubit[{n_instances}] q; + """ + + infix = """ + x q; + """ + + suffix = """ + c = measure q; + """ + + src_parallel = prefix + infix * n_x + suffix + + return src_parallel + + +def build_cy_noise_qasm(n_cy: int) -> str: + src = """ + OPENQASM 3.0; + include "stdgates.inc"; + bit[2] c; + qubit[2] q; + x q[0]; + h q[1]; + """ + src += "cy q[0], q[1];\n" * n_cy + src += """ + h q[1]; + c = measure q; + """ + + return src + + +@pytest.mark.parametrize( + "p_noise, n_x, n_instances, n_shots, max_percent", + [ + (0.001, 200, 6, 500, 5.0), + (0.01, 200, 6, 500, 5.0), + (0.001, 50, 12, 200, 5.0), + ], +) +def test_clifford_run_x_chain( + p_noise: float, n_x: int, n_instances: int, n_shots: int, max_percent: float +): + """ + Simulate multi-instance X-chain with bitflip noise many times + Compare result frequencies with analytically computed probabilities + """ + # Use the Clifford simulator with noise + qsharp.init() + noise = NoiseConfig() + noise.x.set_bitflip(p_noise) + + qasm = build_x_chain_qasm(n_instances, n_x) + output = openqasm.run(qasm, shots=n_shots, noise=noise, seed=42, type="clifford") + histogram = [0 for _ in range(n_instances + 1)] + for shot in output: + shot_results = cast(Sequence[Result], shot) + count_1 = shot_results.count(Result.One) + histogram[count_1] += 1 + + # Probability of obtaining 0 and 1 at the end of the X chain. + p_0 = ((2.0 * p_noise - 1.0) ** n_x + 1.0) / 2.0 + p_1 = 1.0 - p_0 + + # Number of results with k ones that should be there. + p_N = [ + p_0 ** ((n_instances - k)) * (p_1**k) * math.comb(n_instances, k) * n_shots + for k in range(n_instances + 1) + ] + + # Error % for deviation from analytical value + error_percent = [abs(a - b) * 100.0 / n_shots for (a, b) in zip(histogram, p_N)] + print(", ".join(f"{a} (Ξ”β‰ˆ{b:.1f}%)" for (a, b) in zip(histogram, error_percent))) + + # We tolerate configured percentage error. + assert all( + err < max_percent for err in error_percent + ), f"Error percent too high: {error_percent}" + + +def test_clifford_run_cy_noise_distribution(): + """ + Apply CY with per-gate Z noise and validate the expected odd-parity flip rate. + """ + n_cy = 10 + p_z = 0.01 + n_shots = 1000 + expected_p1 = (1.0 - (1.0 - 2.0 * p_z) ** n_cy) / 2.0 + + qsharp.init() + noise = NoiseConfig() + noise.cy.set_pauli_noise("IZ", p_z) + + qasm = build_cy_noise_qasm(n_cy) + output = openqasm.run(qasm, shots=n_shots, noise=noise, seed=77, type="clifford") + + count_target_one = 0 + for shot in output: + shot_results = cast(Sequence[Result], shot) + if shot_results[1] == Result.One: + count_target_one += 1 + + actual_p1 = count_target_one / n_shots + tolerance = 0.05 + print( + f"CY noise rate outside tolerance. Expectedβ‰ˆ{expected_p1:.3f}, " + f"actual={actual_p1:.3f}, tol={tolerance:.3f}" + ) + assert abs(actual_p1 - expected_p1) <= tolerance, "CY noise rate outside tolerance." + + +def test_clifford_run_with_t_fails(): + qsharp.init() + qsharp.eval( + """ + operation Main() : Result { + use q = Qubit(); + T(q); + return MResetZ(q); + } + """ + ) + try: + qsharp.run("Main()", shots=1, type="clifford") + assert False, "Expected QSharpError for non-Clifford gate" + except QSharpError as e: + assert "T gate is not supported in Clifford simulation" in str(e) + + +def test_clifford_run_with_adjoint_t_fails(): + qsharp.init() + qsharp.eval( + """ + operation Main() : Result { + use q = Qubit(); + Adjoint T(q); + return MResetZ(q); + } + """ + ) + try: + qsharp.run("Main()", shots=1, type="clifford") + assert False, "Expected QSharpError for non-Clifford gate" + except QSharpError as e: + assert "adjoint T gate is not supported in Clifford simulation" in str(e) + + +def test_clifford_run_with_non_clifford_rotation_fails(): + qsharp.init() + qsharp.eval( + """ + operation Main() : Result { + use q = Qubit(); + Rx(1.0, q); + return MResetZ(q); + } + """ + ) + try: + qsharp.run("Main()", shots=1, type="clifford") + assert False, "Expected QSharpError for non-Clifford gate" + except QSharpError as e: + assert "angle must be a multiple of PI/2 in Clifford simulation" in str(e) + + +def test_clifford_run_with_too_many_qubits_fails(): + qsharp.init() + qsharp.eval( + """ + operation Main() : Unit { + use qs = Qubit[10]; + } + """ + ) + try: + qsharp.run("Main()", shots=1, type="clifford", num_qubits=5) + assert False, "Expected QSharpError for too many qubits" + except QSharpError as e: + assert "qubit limit exceeded" in str(e) diff --git a/source/resource_estimator/src/counts.rs b/source/resource_estimator/src/counts.rs index 1cf8d8066b..f26a55f555 100644 --- a/source/resource_estimator/src/counts.rs +++ b/source/resource_estimator/src/counts.rs @@ -156,7 +156,8 @@ impl LogicalCounter { fn level_at(&mut self, q: usize) -> usize { while self.max_layer.len() <= q { - self.qubit_allocate(); + self.qubit_allocate() + .expect("qubit allocation should succeed"); } self.max_layer[q] @@ -375,7 +376,10 @@ impl LogicalCounter { // Allocate helper qubits let helper_qubits = (0..aux_qubit_count) - .map(|_| self.qubit_allocate()) + .map(|_| { + self.qubit_allocate() + .expect("qubit allocation should succeed") + }) .collect::>(); // Set barrier among all qubits @@ -441,7 +445,8 @@ impl LogicalCounter { // Release helper qubits for qubit in helper_qubits { - self.qubit_release(qubit); + self.qubit_release(qubit) + .expect("qubit release should succeed"); } Ok(()) @@ -480,144 +485,164 @@ impl LogicalCounter { } impl Backend for LogicalCounter { - fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) { + fn ccx(&mut self, ctl0: usize, ctl1: usize, q: usize) -> Result<(), String> { self.assert_compute_qubits([ctl0, ctl1, q]); self.ccz_count += 1; self.schedule_ccz(ctl0, ctl1, q); + Ok(()) } - fn cx(&mut self, ctl: usize, q: usize) { + fn cx(&mut self, ctl: usize, q: usize) -> Result<(), String> { self.assert_compute_qubits([ctl, q]); self.schedule_two_qubit_clifford(ctl, q); + Ok(()) } - fn cy(&mut self, ctl: usize, q: usize) { + fn cy(&mut self, ctl: usize, q: usize) -> Result<(), String> { self.assert_compute_qubits([ctl, q]); self.schedule_two_qubit_clifford(ctl, q); + Ok(()) } - fn cz(&mut self, ctl: usize, q: usize) { + fn cz(&mut self, ctl: usize, q: usize) -> Result<(), String> { self.assert_compute_qubits([ctl, q]); self.schedule_two_qubit_clifford(ctl, q); + Ok(()) } - fn h(&mut self, q: usize) { + fn h(&mut self, q: usize) -> Result<(), String> { self.assert_compute_qubits([q]); + Ok(()) } - fn m(&mut self, q: usize) -> BackendResult { + fn m(&mut self, q: usize) -> Result { self.assert_compute_qubits([q]); self.m_count += 1; if let Some(val) = self.post_select_measurements.remove(&q) { - val.into() + Ok(val.into()) } else { - self.rnd.borrow_mut().gen_bool(0.5).into() + Ok(self.rnd.borrow_mut().gen_bool(0.5).into()) } } - fn mresetz(&mut self, q: usize) -> BackendResult { + fn mresetz(&mut self, q: usize) -> Result { self.m(q) } - fn reset(&mut self, _q: usize) {} + fn reset(&mut self, _q: usize) -> Result<(), String> { + Ok(()) + } - fn rx(&mut self, theta: f64, q: usize) { - self.rz(theta, q); + fn rx(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.rz(theta, q) } - fn rxx(&mut self, theta: f64, q0: usize, q1: usize) { - self.rzz(theta, q0, q1); + fn rxx(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.rzz(theta, q0, q1) } - fn ry(&mut self, theta: f64, q: usize) { - self.rz(theta, q); + fn ry(&mut self, theta: f64, q: usize) -> Result<(), String> { + self.rz(theta, q) } - fn ryy(&mut self, theta: f64, q0: usize, q1: usize) { - self.rzz(theta, q0, q1); + fn ryy(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.rzz(theta, q0, q1) } - fn rz(&mut self, theta: f64, q: usize) { + fn rz(&mut self, theta: f64, q: usize) -> Result<(), String> { self.assert_compute_qubits([q]); let multiple = (theta / (PI / 4.0)).round(); if ((multiple * (PI / 4.0)) - theta).abs() <= f64::EPSILON { let multiple = (multiple as i64).rem_euclid(8) as u64; if multiple & 1 == 1 { - self.t(q); + self.t(q)?; } } else { self.r_count += 1; self.schedule_r(q); } + Ok(()) } - fn rzz(&mut self, theta: f64, q0: usize, q1: usize) { - self.cx(q1, q0); - self.rz(theta, q0); - self.cx(q1, q0); + fn rzz(&mut self, theta: f64, q0: usize, q1: usize) -> Result<(), String> { + self.cx(q1, q0)?; + self.rz(theta, q0)?; + self.cx(q1, q0) } - fn sadj(&mut self, q: usize) { + fn sadj(&mut self, q: usize) -> Result<(), String> { self.assert_compute_qubits([q]); + Ok(()) } - fn s(&mut self, q: usize) { + fn s(&mut self, q: usize) -> Result<(), String> { self.assert_compute_qubits([q]); + Ok(()) } - fn sx(&mut self, q: usize) { + fn sx(&mut self, q: usize) -> Result<(), String> { self.assert_compute_qubits([q]); + Ok(()) } - fn swap(&mut self, q0: usize, q1: usize) { + fn swap(&mut self, q0: usize, q1: usize) -> Result<(), String> { self.assert_compute_qubits([q0, q1]); self.schedule_two_qubit_clifford(q0, q1); + Ok(()) } - fn tadj(&mut self, q: usize) { + fn tadj(&mut self, q: usize) -> Result<(), String> { self.assert_compute_qubits([q]); self.t_count += 1; self.schedule_t(q); + Ok(()) } - fn t(&mut self, q: usize) { + fn t(&mut self, q: usize) -> Result<(), String> { self.assert_compute_qubits([q]); self.t_count += 1; self.schedule_t(q); + Ok(()) } - fn x(&mut self, _q: usize) {} + fn x(&mut self, _q: usize) -> Result<(), String> { + Ok(()) + } - fn y(&mut self, _q: usize) {} + fn y(&mut self, _q: usize) -> Result<(), String> { + Ok(()) + } - fn z(&mut self, _q: usize) {} + fn z(&mut self, _q: usize) -> Result<(), String> { + Ok(()) + } - fn qubit_allocate(&mut self) -> usize { + fn qubit_allocate(&mut self) -> Result { if let Some(index) = self.free_list.pop() { - index + Ok(index) } else { let index = self.next_free; self.next_free += 1; self.max_layer.push(self.allocation_barrier); - index + Ok(index) } } - fn qubit_release(&mut self, q: usize) -> bool { + fn qubit_release(&mut self, q: usize) -> Result { self.free_list.push(q); - true + Ok(true) } - fn qubit_swap_id(&mut self, q0: usize, q1: usize) { + fn qubit_swap_id(&mut self, q0: usize, q1: usize) -> Result<(), String> { // First swap the layer map for the qubits. self.max_layer.swap(q0, q1); @@ -630,14 +655,15 @@ impl Backend for LogicalCounter { if let Some(val) = q1_post_select { self.post_select_measurements.insert(q0, val); } + Ok(()) } - fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { - (Vec::new(), 0) + fn capture_quantum_state(&mut self) -> Result<(Vec<(BigUint, Complex)>, usize), String> { + Ok((Vec::new(), 0)) } - fn qubit_is_zero(&mut self, _q: usize) -> bool { - true + fn qubit_is_zero(&mut self, _q: usize) -> Result { + Ok(true) } fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option> { diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index b80275deb5..3af2eefd70 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -7,10 +7,10 @@ pub mod noise; pub mod operation; use crate::{ - MeasurementResult, QubitID, Simulator, + MeasurementResult, NearlyZero, QubitID, Simulator, noise_config::{CumulativeNoiseConfig, IntrinsicID}, }; -use noise::Fault; +pub use noise::Fault; use operation::Operation; use paulimer::{ Simulation, UnitaryOp, @@ -18,7 +18,10 @@ use paulimer::{ quantum_core, }; use rand::{SeedableRng as _, rngs::StdRng}; -use std::sync::Arc; +use std::{ + f64::consts::{FRAC_PI_2, PI, TAU}, + sync::Arc, +}; /// A stabilizer simulator with the ability to simulate atom loss. pub struct StabilizerSimulator { @@ -121,6 +124,11 @@ macro_rules! apply_noise { } impl StabilizerSimulator { + /// Sets the random seed of the simulator. + pub fn set_seed(&mut self, seed: u64) { + self.rng = StdRng::seed_from_u64(seed); + } + /// Increment the simulation time by one. /// This is used to compute the idle noise on qubits. pub fn step(&mut self) { @@ -386,6 +394,139 @@ impl Simulator for StabilizerSimulator { apply_noise!(self, cz, &[control, target]); } + fn rx(&mut self, angle: f64, target: QubitID) { + if !self.loss[target] { + self.apply_idle_noise(target); + + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::X, + UnitaryOp::SqrtX, + UnitaryOp::SqrtXInv, + ); + self.state.apply_unitary(unitary, &[target]); + + apply_loss!(self, rx, &[target]); + apply_noise!(self, rx, &[target]); + } + } + + fn ry(&mut self, angle: f64, target: QubitID) { + if !self.loss[target] { + self.apply_idle_noise(target); + + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::Y, + UnitaryOp::SqrtY, + UnitaryOp::SqrtYInv, + ); + self.state.apply_unitary(unitary, &[target]); + + apply_loss!(self, ry, &[target]); + apply_noise!(self, ry, &[target]); + } + } + + fn rz(&mut self, angle: f64, target: QubitID) { + if !self.loss[target] { + self.apply_idle_noise(target); + + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::Z, + UnitaryOp::SqrtZ, + UnitaryOp::SqrtZInv, + ); + self.state.apply_unitary(unitary, &[target]); + + apply_loss!(self, rz, &[target]); + apply_noise!(self, rz, &[target]); + } + } + + fn rxx(&mut self, angle: f64, q1: QubitID, q2: QubitID) { + if !self.loss[q1] && !self.loss[q2] { + self.apply_idle_noise(q1); + self.apply_idle_noise(q2); + + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::ControlledX, + UnitaryOp::SqrtX, + UnitaryOp::SqrtXInv, + ); + // NOTE: We perform the Rxx gate by changing basis to Y and performing the decomposition of Rzz. + self.state.apply_unitary(UnitaryOp::SqrtY, &[q1]); + self.state.apply_unitary(UnitaryOp::SqrtY, &[q2]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(unitary, &[q1]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(UnitaryOp::SqrtYInv, &[q1]); + self.state.apply_unitary(UnitaryOp::SqrtYInv, &[q2]); + + apply_loss!(self, rxx, &[q1, q2]); + apply_noise!(self, rxx, &[q1, q2]); + } + } + + fn ryy(&mut self, angle: f64, q1: QubitID, q2: QubitID) { + if !self.loss[q1] && !self.loss[q2] { + self.apply_idle_noise(q1); + self.apply_idle_noise(q2); + + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::ControlledZ, + UnitaryOp::SqrtZ, + UnitaryOp::SqrtZInv, + ); + // NOTE: We perform the Ryy gate by changing basis to Z and performing the decomposition of Rzz. + self.state.apply_unitary(UnitaryOp::SqrtX, &[q1]); + self.state.apply_unitary(UnitaryOp::SqrtX, &[q2]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(unitary, &[q1]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q1]); + self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q2]); + + apply_loss!(self, ryy, &[q1, q2]); + apply_noise!(self, ryy, &[q1, q2]); + } + } + + fn rzz(&mut self, angle: f64, q1: QubitID, q2: QubitID) { + if !self.loss[q1] && !self.loss[q2] { + self.apply_idle_noise(q1); + self.apply_idle_noise(q2); + + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::ControlledZ, + UnitaryOp::SqrtZ, + UnitaryOp::SqrtZInv, + ); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(unitary, &[q1]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + + apply_loss!(self, rzz, &[q1, q2]); + apply_noise!(self, rzz, &[q1, q2]); + } + } + fn swap(&mut self, q1: QubitID, q2: QubitID) { match (self.loss[q1], self.loss[q2]) { (true, true) => (), @@ -472,31 +613,34 @@ impl Simulator for StabilizerSimulator { unimplemented!("unssuported instruction in stabilizer simulator: T_ADJ") } - fn rx(&mut self, _angle: f64, _target: QubitID) { - unimplemented!("unssuported instruction in stabilizer simulator: Rx") - } - - fn ry(&mut self, _angle: f64, _target: QubitID) { - unimplemented!("unssuported instruction in stabilizer simulator: Ry") - } - - fn rz(&mut self, _angle: f64, _target: QubitID) { - unimplemented!("unssuported instruction in stabilizer simulator: Rz") - } - - fn rxx(&mut self, _angle: f64, _q1: QubitID, _q2: QubitID) { - unimplemented!("unssuported instruction in stabilizer simulator: Rxx") - } - - fn ryy(&mut self, _angle: f64, _q1: QubitID, _q2: QubitID) { - unimplemented!("unssuported instruction in stabilizer simulator: Ryy") - } - - fn rzz(&mut self, _angle: f64, _q1: QubitID, _q2: QubitID) { - unimplemented!("unssuported instruction in stabilizer simulator: Rzz") - } - fn state_dump(&self) -> &Self::StateDumpData { self.state.clifford() } } + +fn unitary_from_normalized_angle( + angle: f64, + pauli: UnitaryOp, + sqrt_pauli: UnitaryOp, + sqrt_pauli_inv: UnitaryOp, +) -> UnitaryOp { + let mut normalized_angle = angle % TAU; + if normalized_angle < 0.0 { + normalized_angle += TAU; + } + if normalized_angle.is_nearly_zero() || (normalized_angle - TAU / 2.0).is_nearly_zero() { + // The angle is a multiple of 2 * PI, so the operation is effectively an identity. + UnitaryOp::I + } else if (normalized_angle - PI).is_nearly_zero() { + // The angle is an odd multiple of PI, so the operation is effectively a Pauli gate. + pauli + } else if (normalized_angle - FRAC_PI_2).is_nearly_zero() { + // The angle is an odd multiple of PI / 2, so the operation is effectively a sqrt(Pauli) gate. + sqrt_pauli + } else if (normalized_angle - 3.0 * FRAC_PI_2).is_nearly_zero() { + // The angle is an odd multiple of 3 * PI / 2, so the operation is effectively a sqrt(Pauli) adjoint gate. + sqrt_pauli_inv + } else { + unimplemented!("unsupported rotation angle in stabilizer simulator: {angle}"); + } +} From 7034be8203af11fdb349ccf16029e06a6de420c4 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Fri, 1 May 2026 09:44:18 -0700 Subject: [PATCH 2/4] Fix up pinned test results --- source/pip/tests/test_clifford_simulator.py | 38 ++++++++------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/source/pip/tests/test_clifford_simulator.py b/source/pip/tests/test_clifford_simulator.py index f2aa2c6425..10ba284501 100644 --- a/source/pip/tests/test_clifford_simulator.py +++ b/source/pip/tests/test_clifford_simulator.py @@ -88,8 +88,7 @@ def test_million(): def test_program_with_branching_succeeds(): qsharp.init(target_profile=TargetProfile.Adaptive_RI) - qsharp.eval( - """ + qsharp.eval(""" operation Main() : Result { use q = Qubit(); H(q); @@ -98,8 +97,7 @@ def test_program_with_branching_succeeds(): } return MResetZ(q); } - """ - ) + """) ir = qsharp.compile("Main()") results = run_qir_clifford(str(ir), 1, NoiseConfig()) assert len(results) == 1 @@ -277,7 +275,7 @@ def test_clifford_run_bitflip_noise(): result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) # Reasonable results obtained from manual run - assert result == ["0000001100000000"] + assert result == ["0000000011000001"] # Same execution should work with the operation itself. output = qsharp.run( @@ -295,7 +293,7 @@ def test_clifford_run_bitflip_noise(): ) result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) - assert result == ["0000001100000000"] + assert result == ["0000000011000001"] def test_clifford_run_mixed_noise(): @@ -312,13 +310,13 @@ def test_clifford_run_mixed_noise(): "IsingModel2DEvolution(4, 4, PI() / 2.0, PI() / 2.0, 4.0, 4)", shots=1, noise=noise, - seed=234, + seed=67, type="clifford", ) result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) # Reasonable results obtained from manual run - assert result == ["-000-01000100010"] + assert result == ["0000111-00000000"] def test_clifford_run_isolated_loss(): @@ -521,15 +519,13 @@ def test_clifford_run_cy_noise_distribution(): def test_clifford_run_with_t_fails(): qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Main() : Result { use q = Qubit(); T(q); return MResetZ(q); } - """ - ) + """) try: qsharp.run("Main()", shots=1, type="clifford") assert False, "Expected QSharpError for non-Clifford gate" @@ -539,15 +535,13 @@ def test_clifford_run_with_t_fails(): def test_clifford_run_with_adjoint_t_fails(): qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Main() : Result { use q = Qubit(); Adjoint T(q); return MResetZ(q); } - """ - ) + """) try: qsharp.run("Main()", shots=1, type="clifford") assert False, "Expected QSharpError for non-Clifford gate" @@ -557,15 +551,13 @@ def test_clifford_run_with_adjoint_t_fails(): def test_clifford_run_with_non_clifford_rotation_fails(): qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Main() : Result { use q = Qubit(); Rx(1.0, q); return MResetZ(q); } - """ - ) + """) try: qsharp.run("Main()", shots=1, type="clifford") assert False, "Expected QSharpError for non-Clifford gate" @@ -575,13 +567,11 @@ def test_clifford_run_with_non_clifford_rotation_fails(): def test_clifford_run_with_too_many_qubits_fails(): qsharp.init() - qsharp.eval( - """ + qsharp.eval(""" operation Main() : Unit { use qs = Qubit[10]; } - """ - ) + """) try: qsharp.run("Main()", shots=1, type="clifford", num_qubits=5) assert False, "Expected QSharpError for too many qubits" From ba11fe75c858de38d31f278e622f985d8d60de2b Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 5 May 2026 13:43:15 -0700 Subject: [PATCH 3/4] Fix bugs, PR feedback --- source/pip/qsharp/_qsharp.py | 4 +- source/pip/tests/test_clifford_simulator.py | 31 +++- source/simulators/src/stabilizer_simulator.rs | 139 ++++++++++-------- 3 files changed, 108 insertions(+), 66 deletions(-) diff --git a/source/pip/qsharp/_qsharp.py b/source/pip/qsharp/_qsharp.py index 6e37c85ee3..fb9feaa198 100644 --- a/source/pip/qsharp/_qsharp.py +++ b/source/pip/qsharp/_qsharp.py @@ -353,7 +353,7 @@ def init( ) try: - (_, manifest_contents) = read_file(qsharp_json) + _, manifest_contents = read_file(qsharp_json) except Exception as e: raise QSharpError( f"Error reading {qsharp_json}. qsharp.json should exist at the project root and be a valid JSON file." @@ -840,7 +840,7 @@ def on_save_events(output: Output) -> None: elif output.is_message(): results[-1]["messages"].append(str(output)) - if type is not None and type == "clifford": + if type == "clifford": if noise is not None and not isinstance(noise, NoiseConfig): raise ValueError( "only `NoiseConfig` is supported when using noise with the clifford simulator." diff --git a/source/pip/tests/test_clifford_simulator.py b/source/pip/tests/test_clifford_simulator.py index 10ba284501..d2fe8be331 100644 --- a/source/pip/tests/test_clifford_simulator.py +++ b/source/pip/tests/test_clifford_simulator.py @@ -310,13 +310,13 @@ def test_clifford_run_mixed_noise(): "IsingModel2DEvolution(4, 4, PI() / 2.0, PI() / 2.0, 4.0, 4)", shots=1, noise=noise, - seed=67, + seed=228, type="clifford", ) result = [result_array_to_string(cast(Sequence[Result], x)) for x in output] print(result) # Reasonable results obtained from manual run - assert result == ["0000111-00000000"] + assert result == ["00000-0000000001"] def test_clifford_run_isolated_loss(): @@ -517,6 +517,33 @@ def test_clifford_run_cy_noise_distribution(): assert abs(actual_p1 - expected_p1) <= tolerance, "CY noise rate outside tolerance." +def test_clifford_run_with_rotation_by_clifford_angles_succeeds(): + qsharp.init() + qsharp.eval(""" + operation Main() : Result { + import Std.Math.PI; + use q = Qubit(); + Rx(PI(), q); // X equivalent + Rz(PI(), q); // Z equivalent + Ry(PI(), q); // Y equivalent, qubit back to |0> + Rx(PI() / 2.0, q); // Sqrt(X) equivalent, basis change to Y + Rz(PI() / 2.0, q); // Sqrt(Z) equivalent, basis change to X + Ry(PI() / 2.0, q); // Undo basis change, should end in |1> + Rz(2.0 * PI(), q); // Full rotation around Z, should have no effect + Rz(-2.0 * PI(), q); // Full rotation around Z, should have no effect + Ry(4.0 * PI(), q); // Full rotation around Y, should have no effect + Ry(-4.0 * PI(), q); // Full rotation around Y, should have no effect + Rx(-4.0 * PI(), q); // Full rotation around X, should have no effect + Rx(4.0 * PI(), q); // Full rotation around X, should have no effect + Rx(PI(), q); // Another X equivalent, should flip back to |0> + return MResetZ(q); + } + """) + output = qsharp.run("Main()", shots=1, type="clifford") + print(output) + assert output == [Result.Zero], "Expected result of 0 with Clifford rotations." + + def test_clifford_run_with_t_fails(): qsharp.init() qsharp.eval(""" diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 3af2eefd70..8c3a2b9320 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -452,78 +452,93 @@ impl Simulator for StabilizerSimulator { } fn rxx(&mut self, angle: f64, q1: QubitID, q2: QubitID) { - if !self.loss[q1] && !self.loss[q2] { - self.apply_idle_noise(q1); - self.apply_idle_noise(q2); + match (self.loss[q1], self.loss[q2]) { + (true, true) => (), + (true, false) => self.rx(angle, q2), + (false, true) => self.rx(angle, q1), + (false, false) => { + self.apply_idle_noise(q1); + self.apply_idle_noise(q2); - // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle - // and check to see if it is supported. - let unitary = unitary_from_normalized_angle( - angle, - UnitaryOp::ControlledX, - UnitaryOp::SqrtX, - UnitaryOp::SqrtXInv, - ); - // NOTE: We perform the Rxx gate by changing basis to Y and performing the decomposition of Rzz. - self.state.apply_unitary(UnitaryOp::SqrtY, &[q1]); - self.state.apply_unitary(UnitaryOp::SqrtY, &[q2]); - self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); - self.state.apply_unitary(unitary, &[q1]); - self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); - self.state.apply_unitary(UnitaryOp::SqrtYInv, &[q1]); - self.state.apply_unitary(UnitaryOp::SqrtYInv, &[q2]); - - apply_loss!(self, rxx, &[q1, q2]); - apply_noise!(self, rxx, &[q1, q2]); + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::Z, + UnitaryOp::SqrtZ, + UnitaryOp::SqrtZInv, + ); + // NOTE: We perform the Rxx gate by changing basis to Y and performing the decomposition of Rzz. + self.state.apply_unitary(UnitaryOp::Hadamard, &[q1]); + self.state.apply_unitary(UnitaryOp::Hadamard, &[q2]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(unitary, &[q1]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(UnitaryOp::Hadamard, &[q1]); + self.state.apply_unitary(UnitaryOp::Hadamard, &[q2]); + + apply_loss!(self, rxx, &[q1, q2]); + apply_noise!(self, rxx, &[q1, q2]); + } } } fn ryy(&mut self, angle: f64, q1: QubitID, q2: QubitID) { - if !self.loss[q1] && !self.loss[q2] { - self.apply_idle_noise(q1); - self.apply_idle_noise(q2); + match (self.loss[q1], self.loss[q2]) { + (true, true) => (), + (true, false) => self.ry(angle, q2), + (false, true) => self.ry(angle, q1), + (false, false) => { + self.apply_idle_noise(q1); + self.apply_idle_noise(q2); - // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle - // and check to see if it is supported. - let unitary = unitary_from_normalized_angle( - angle, - UnitaryOp::ControlledZ, - UnitaryOp::SqrtZ, - UnitaryOp::SqrtZInv, - ); - // NOTE: We perform the Ryy gate by changing basis to Z and performing the decomposition of Rzz. - self.state.apply_unitary(UnitaryOp::SqrtX, &[q1]); - self.state.apply_unitary(UnitaryOp::SqrtX, &[q2]); - self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); - self.state.apply_unitary(unitary, &[q1]); - self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); - self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q1]); - self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q2]); - - apply_loss!(self, ryy, &[q1, q2]); - apply_noise!(self, ryy, &[q1, q2]); + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::Z, + UnitaryOp::SqrtZ, + UnitaryOp::SqrtZInv, + ); + // NOTE: We perform the Ryy gate by changing basis to Z and performing the decomposition of Rzz. + self.state.apply_unitary(UnitaryOp::SqrtX, &[q1]); + self.state.apply_unitary(UnitaryOp::SqrtX, &[q2]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(unitary, &[q1]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q1]); + self.state.apply_unitary(UnitaryOp::SqrtXInv, &[q2]); + + apply_loss!(self, ryy, &[q1, q2]); + apply_noise!(self, ryy, &[q1, q2]); + } } } fn rzz(&mut self, angle: f64, q1: QubitID, q2: QubitID) { - if !self.loss[q1] && !self.loss[q2] { - self.apply_idle_noise(q1); - self.apply_idle_noise(q2); - - // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle - // and check to see if it is supported. - let unitary = unitary_from_normalized_angle( - angle, - UnitaryOp::ControlledZ, - UnitaryOp::SqrtZ, - UnitaryOp::SqrtZInv, - ); - self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); - self.state.apply_unitary(unitary, &[q1]); - self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); + match (self.loss[q1], self.loss[q2]) { + (true, true) => (), + (true, false) => self.rz(angle, q2), + (false, true) => self.rz(angle, q1), + (false, false) => { + self.apply_idle_noise(q1); + self.apply_idle_noise(q2); - apply_loss!(self, rzz, &[q1, q2]); - apply_noise!(self, rzz, &[q1, q2]); + // We can only perform rotations by multiples of PI / 2 in the stabilizer, so normalize the angle + // and check to see if it is supported. + let unitary = unitary_from_normalized_angle( + angle, + UnitaryOp::Z, + UnitaryOp::SqrtZ, + UnitaryOp::SqrtZInv, + ); + self.state.apply_unitary(UnitaryOp::ControlledZ, &[q2, q1]); + self.state.apply_unitary(unitary, &[q1]); + self.state.apply_unitary(UnitaryOp::ControlledZ, &[q2, q1]); + + apply_loss!(self, rzz, &[q1, q2]); + apply_noise!(self, rzz, &[q1, q2]); + } } } @@ -628,7 +643,7 @@ fn unitary_from_normalized_angle( if normalized_angle < 0.0 { normalized_angle += TAU; } - if normalized_angle.is_nearly_zero() || (normalized_angle - TAU / 2.0).is_nearly_zero() { + if normalized_angle.is_nearly_zero() || (normalized_angle - TAU).is_nearly_zero() { // The angle is a multiple of 2 * PI, so the operation is effectively an identity. UnitaryOp::I } else if (normalized_angle - PI).is_nearly_zero() { From 631202160540d931157e32d154a89216bc1edf12 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 5 May 2026 15:07:19 -0700 Subject: [PATCH 4/4] fix typo in Rzz, add joint rotations test --- source/pip/tests/test_clifford_simulator.py | 54 +++++++++++++++++++ source/simulators/src/stabilizer_simulator.rs | 4 +- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/source/pip/tests/test_clifford_simulator.py b/source/pip/tests/test_clifford_simulator.py index d2fe8be331..a083971b3a 100644 --- a/source/pip/tests/test_clifford_simulator.py +++ b/source/pip/tests/test_clifford_simulator.py @@ -544,6 +544,60 @@ def test_clifford_run_with_rotation_by_clifford_angles_succeeds(): assert output == [Result.Zero], "Expected result of 0 with Clifford rotations." +def test_clifford_run_joint_rotations_by_clifford_angles_succeeds(): + qsharp.init() + qsharp.eval(""" + operation Main() : Result[] { + import Std.Math.PI; + use qs = Qubit[2]; + within { + H(qs[0]); + H(qs[1]); + } apply { + Rzz(PI() / 2.0, qs[0], qs[1]); + { + CNOT(qs[1], qs[0]); + Rz(-PI() / 2.0, qs[0]); + CNOT(qs[1], qs[0]); + } + Rxx(PI() / 2.0, qs[0], qs[1]); + { + within { + H(qs[0]); + H(qs[1]); + } apply { + CNOT(qs[1], qs[0]); + Rz(-PI() / 2.0, qs[0]); + CNOT(qs[1], qs[0]); + } + } + Ryy(PI() / 2.0, qs[0], qs[1]); + { + within { + Adjoint S(qs[0]); + H(qs[0]); + Adjoint S(qs[1]); + H(qs[1]); + } apply { + CNOT(qs[1], qs[0]); + Rz(-PI() / 2.0, qs[0]); + CNOT(qs[1], qs[0]); + } + } + } + MResetEachZ(qs) + } + """) + output = qsharp.run("Main()", shots=1, type="clifford") + print(output) + assert output == [ + [ + Result.Zero, + Result.Zero, + ] + ], "Expected result of 00 with Clifford rotations." + + def test_clifford_run_with_t_fails(): qsharp.init() qsharp.eval(""" diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 8c3a2b9320..7e59317808 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -532,9 +532,9 @@ impl Simulator for StabilizerSimulator { UnitaryOp::SqrtZ, UnitaryOp::SqrtZInv, ); - self.state.apply_unitary(UnitaryOp::ControlledZ, &[q2, q1]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); self.state.apply_unitary(unitary, &[q1]); - self.state.apply_unitary(UnitaryOp::ControlledZ, &[q2, q1]); + self.state.apply_unitary(UnitaryOp::ControlledX, &[q2, q1]); apply_loss!(self, rzz, &[q1, q2]); apply_noise!(self, rzz, &[q1, q2]);