diff --git a/Cargo.lock b/Cargo.lock index 069dcabf19..7d0c941fb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2262,6 +2262,7 @@ name = "qsharp" version = "0.0.0" dependencies = [ "allocator", + "expect-test", "miette", "noisy_simulator", "num-bigint", diff --git a/source/noisy_simulator/src/lib.rs b/source/noisy_simulator/src/lib.rs index 11709cca75..725be959ec 100644 --- a/source/noisy_simulator/src/lib.rs +++ b/source/noisy_simulator/src/lib.rs @@ -198,3 +198,7 @@ macro_rules! handle_error { } pub(crate) use handle_error; + +pub(crate) fn eq_with_tolerance(left: f64, right: f64, tolerance: f64) -> bool { + (left - right).abs() <= tolerance +} diff --git a/source/noisy_simulator/src/state_vector_simulator.rs b/source/noisy_simulator/src/state_vector_simulator.rs index 73d189c8e6..67677ccbd5 100644 --- a/source/noisy_simulator/src/state_vector_simulator.rs +++ b/source/noisy_simulator/src/state_vector_simulator.rs @@ -10,11 +10,12 @@ mod tests; use rand::{Rng, SeedableRng, rngs::StdRng}; use crate::{ - ComplexVector, Error, NoisySimulator, SquareMatrix, TOLERANCE, handle_error, + ComplexVector, Error, NoisySimulator, SquareMatrix, TOLERANCE, eq_with_tolerance, handle_error, instrument::Instrument, kernel::apply_kernel, operation::Operation, }; /// A vector representing the state of a quantum system. +#[derive(Debug)] pub struct StateVector { /// Dimension of the vector. dimension: usize, @@ -26,6 +27,58 @@ pub struct StateVector { data: ComplexVector, } +impl PartialEq for StateVector { + /// Compares two state vectors for equality up to a global phase. + /// + /// Two state vectors are considered equal if one can be obtained from the other + /// by multiplying by a complex number of unit magnitude (a global phase factor). + fn eq(&self, other: &Self) -> bool { + use num_complex::Complex; + + if self.dimension != other.dimension || self.number_of_qubits != other.number_of_qubits { + return false; + } + + if !eq_with_tolerance(self.trace_change, other.trace_change, TOLERANCE) { + return false; + } + + // Find the first non-zero element in self to determine the global phase + let phase = self + .data + .iter() + .zip(other.data.iter()) + .find(|(a, _)| a.norm() > TOLERANCE) + .map(|(a, b)| { + if b.norm() > TOLERANCE { + // phase = b / a, so self * phase ≈ other + b / a + } else { + // a is non-zero but b is zero - not equal + Complex::new(f64::NAN, 0.0) + } + }); + + match phase { + Some(phase) if phase.re.is_nan() => false, + Some(phase) => { + // Check that the phase has unit magnitude + if !eq_with_tolerance(phase.norm(), 1.0, TOLERANCE) { + return false; + } + // Check that all elements match after applying the phase + self.data + .iter() + .zip(other.data.iter()) + .all(|(a, b)| eq_with_tolerance((a * phase - b).norm(), 0.0, TOLERANCE)) + } + // The first vector is the zero vector. + // We return `true` iff the second vector is also the zero vector. + None => other.data.iter().all(|b| b.norm() <= TOLERANCE), + } + } +} + impl StateVector { fn new(number_of_qubits: usize) -> Self { let dimension = 1 << number_of_qubits; diff --git a/source/noisy_simulator/src/tests.rs b/source/noisy_simulator/src/tests.rs index 24523a50fd..3479215635 100644 --- a/source/noisy_simulator/src/tests.rs +++ b/source/noisy_simulator/src/tests.rs @@ -4,7 +4,7 @@ pub mod noiseless_tests; pub mod noisy_tests; -use crate::TOLERANCE; +use crate::{TOLERANCE, eq_with_tolerance}; /// Assert that two f64 are equal up to a `TOLERANCE`. pub fn assert_approx_eq(left: f64, right: f64) { @@ -13,7 +13,7 @@ pub fn assert_approx_eq(left: f64, right: f64) { pub fn assert_approx_eq_with_tolerance(left: f64, right: f64, tolerance: f64) { assert!( - (left - right).abs() <= tolerance, + eq_with_tolerance(left, right, tolerance), "aprox_equal failed, left = {left}, right = {right}" ); } diff --git a/source/pip/Cargo.toml b/source/pip/Cargo.toml index a5bee271a3..e0ca38013b 100644 --- a/source/pip/Cargo.toml +++ b/source/pip/Cargo.toml @@ -24,6 +24,9 @@ serde_json = { workspace = true } rayon = { workspace = true } rand = { workspace = true } +[dev-dependencies] +expect-test = { workspace = true } + [lints] workspace = true diff --git a/source/pip/src/qir_simulation/cpu_simulators.rs b/source/pip/src/qir_simulation/cpu_simulators.rs index 5dbb0cbfe8..aff3759c91 100644 --- a/source/pip/src/qir_simulation/cpu_simulators.rs +++ b/source/pip/src/qir_simulation/cpu_simulators.rs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#[cfg(test)] +mod tests; + use crate::qir_simulation::{NoiseConfig, QirInstruction, QirInstructionId, unbind_noise_config}; use pyo3::{IntoPyObjectExt, exceptions::PyValueError, prelude::*, types::PyList}; use pyo3::{PyResult, pyfunction}; @@ -186,7 +189,8 @@ where .par_iter() .map(|shot_seed| { let simulator = make_simulator(num_qubits, num_results, *shot_seed, noise.clone()); - run_shot(instructions, simulator) + let mut simulator = run_shot(instructions, simulator); + simulator.take_measurements() }) .collect::>(); @@ -206,10 +210,11 @@ where values } -fn run_shot(instructions: &[QirInstruction], mut sim: impl Simulator) -> Vec { +fn run_shot(instructions: &[QirInstruction], mut sim: S) -> S { for qir_inst in instructions { match qir_inst { QirInstruction::OneQubitGate(id, qubit) => match id { + QirInstructionId::I => {} // Identity gate is a no-op QirInstructionId::H => sim.h(*qubit as usize), QirInstructionId::X => sim.x(*qubit as usize), QirInstructionId::Y => sim.y(*qubit as usize), @@ -261,5 +266,5 @@ fn run_shot(instructions: &[QirInstruction], mut sim: impl Simulator) -> Vec QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::I, q) +} +pub fn h(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::H, q) +} +pub fn x(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::X, q) +} +pub fn y(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::Y, q) +} +pub fn z(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::Z, q) +} +pub fn s(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::S, q) +} +pub fn s_adj(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::SAdj, q) +} +pub fn sx(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::SX, q) +} +pub fn sx_adj(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::SXAdj, q) +} +pub fn t(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::T, q) +} +pub fn t_adj(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::TAdj, q) +} +pub fn mov(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::Move, q) +} +pub fn reset(q: u32) -> QirInstruction { + QirInstruction::OneQubitGate(QirInstructionId::RESET, q) +} + +// Two-qubit gates +pub fn cx(q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::CX, q1, q2) +} +#[allow(dead_code, reason = "unimplemented")] +pub fn cy(q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::CY, q1, q2) +} +pub fn cz(q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::CZ, q1, q2) +} +pub fn swap(q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::SWAP, q1, q2) +} +pub fn mz(q: u32, r: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::MZ, q, r) +} +pub fn mresetz(q: u32, r: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::MResetZ, q, r) +} + +// Three-qubit gates +#[allow(dead_code, reason = "unimplemented")] +pub fn ccx(q1: u32, q2: u32, q3: u32) -> QirInstruction { + QirInstruction::ThreeQubitGate(QirInstructionId::CCX, q1, q2, q3) +} + +// Single-qubit rotation gates +pub fn rx(angle: f64, q: u32) -> QirInstruction { + QirInstruction::OneQubitRotationGate(QirInstructionId::RX, angle, q) +} +pub fn ry(angle: f64, q: u32) -> QirInstruction { + QirInstruction::OneQubitRotationGate(QirInstructionId::RY, angle, q) +} +pub fn rz(angle: f64, q: u32) -> QirInstruction { + QirInstruction::OneQubitRotationGate(QirInstructionId::RZ, angle, q) +} + +// Two-qubit rotation gates +pub fn rxx(angle: f64, q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitRotationGate(QirInstructionId::RXX, angle, q1, q2) +} +pub fn ryy(angle: f64, q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitRotationGate(QirInstructionId::RYY, angle, q1, q2) +} +pub fn rzz(angle: f64, q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitRotationGate(QirInstructionId::RZZ, angle, q1, q2) +} + +// Correlated noise intrinsic +pub fn noise_intrinsic(id: u32, qubits: &[u32]) -> QirInstruction { + QirInstruction::CorrelatedNoise(QirInstructionId::CorrelatedNoise, id, qubits.to_vec()) +} + +// ==================== Macros ==================== + +/// Macro to build a `NoiseConfig` for testing. +/// +/// # Example +/// ```ignore +/// noise_config! { +/// rx: { +/// x: 1e-5, +/// z: 1e-10, +/// loss: 1e-10, +/// }, +/// rxx: { +/// ix: 1e-10, +/// xi: 1e-10, +/// xx: 1e-5, +/// loss: 1e-10, +/// }, +/// intrinsics: { +/// 0: { +/// iizz: 1e-4, +/// ixix: 2e-4, +/// }, +/// 1: { +/// iziz: 1e-4, +/// iizz: 1e-5, +/// }, +/// }, +/// } +/// ``` +macro_rules! noise_config { + // Entry point + ( $( $field:ident : { $($inner:tt)* } ),* $(,)? ) => {{ + #[allow(unused_mut)] + let mut config = noise_config::NoiseConfig::::NOISELESS; + $( + noise_config!(@field config, $field, { $($inner)* }); + )* + config + }}; + + // Handle intrinsics field specially + (@field $config:ident, intrinsics, { $( $id:literal : { $($pauli:ident : $prob:expr),* $(,)? } ),* $(,)? }) => {{ + $( + let mut table = noise_config::NoiseTable::::noiseless(0); + $( + noise_config!(@set_pauli table, $pauli, $prob); + )* + $config.intrinsics.insert($id, table); + )* + }}; + + // Handle regular gate fields (single-qubit gates) + (@field $config:ident, i, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.i, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, x, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.x, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, y, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.y, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, z, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.z, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, h, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.h, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, s, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.s, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, s_adj, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.s_adj, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, t, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.t, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, t_adj, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.t_adj, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, sx, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.sx, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, sx_adj, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.sx_adj, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, rx, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.rx, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, ry, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.ry, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, rz, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.rz, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, mov, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.mov, 1, $($pauli : $prob),*); + }}; + (@field $config:ident, mresetz, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.mresetz, 1, $($pauli : $prob),*); + }}; + + // Handle two-qubit gate fields + (@field $config:ident, cx, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.cx, 2, $($pauli : $prob),*); + }}; + (@field $config:ident, cz, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.cz, 2, $($pauli : $prob),*); + }}; + (@field $config:ident, rxx, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.rxx, 2, $($pauli : $prob),*); + }}; + (@field $config:ident, ryy, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.ryy, 2, $($pauli : $prob),*); + }}; + (@field $config:ident, rzz, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.rzz, 2, $($pauli : $prob),*); + }}; + (@field $config:ident, swap, { $($pauli:ident : $prob:expr),* $(,)? }) => {{ + noise_config!(@set_table $config.swap, 2, $($pauli : $prob),*); + }}; + + // Helper to set a noise table with the given number of qubits + (@set_table $table:expr, $qubits:expr, $($pauli:ident : $prob:expr),* $(,)?) => {{ + let mut table = noise_config::NoiseTable::::noiseless($qubits); + $( + noise_config!(@set_pauli table, $pauli, $prob); + )* + $table = table; + }}; + + // Helper to set a single pauli entry + (@set_pauli $table:ident, loss, $prob:expr) => {{ + $table.loss = $prob; + }}; + (@set_pauli $table:ident, $pauli:ident, $prob:expr) => {{ + let pauli_str = stringify!($pauli).to_uppercase(); + // Update qubits if needed based on pauli string length + #[allow(clippy::cast_possible_truncation)] + if $table.qubits == 0 { + $table.qubits = pauli_str.len() as u32; + } + $table.pauli_strings.push(pauli_str); + $table.probabilities.push($prob); + }}; +} + +#[cfg(test)] +pub(crate) use noise_config; + +/// Macro to build a program (list of QIR instructions) for testing. +/// +/// # Example +/// ```ignore +/// qir! { +/// x(0); +/// cx(0, 1); +/// mresetz(0, 0); +/// mresetz(1, 1); +/// } +/// ``` +/// expands to `vec![x(0), cx(0, 1), mresetz(0, 0), mresetz(1, 1)]` +/// +/// The macro also supports the `within { } apply { }` construct for +/// the conjugation pattern (apply within, then apply, then reverse within): +/// ```ignore +/// qir! { +/// x(0); +/// within { +/// x(1); +/// h(1); +/// } apply { +/// cz(0, 1); +/// } +/// mresetz(0, 0); +/// } +/// ``` +/// expands to `vec![x(0), x(1), h(1), cz(0, 1), h(1), x(1), mresetz(0, 0)]` +macro_rules! qir { + // Internal rule: base case - empty input + (@accum [$($acc:expr),*] ) => { + vec![$($acc),*] + }; + + // Match within { } apply { } followed by semicolon and more instructions + (@accum [$($acc:expr),*] within { $($within_tt:tt)* } apply { $($apply_tt:tt)* } ; $($rest:tt)*) => {{ + compile_error!("semicolon after a within-apply block") + }}; + + // Match within { } apply { } at the end (no trailing semicolon or more instructions) + (@accum [$($acc:expr),*] within { $($within_tt:tt)* } apply { $($apply_tt:tt)* } $($rest:tt)*) => {{ + let mut result: Vec = vec![$($acc),*]; + result.extend(qir!($($within_tt)*)); // forward within + result.extend(qir!($($apply_tt)*)); // apply + let within_rev: Vec = { + let mut v = qir!($($within_tt)*); // expand tokens again for reverse + v.reverse(); + v + }; + result.extend(within_rev); + let remaining: Vec = qir!(@accum [] $($rest)*); + result.extend(remaining); + result + }}; + + // Match a single instruction followed by semicolon and more + (@accum [$($acc:expr),*] $inst:expr ; $($rest:tt)*) => { + qir!(@accum [$($acc,)* $inst] $($rest)*) + }; + + // Match final instruction without trailing semicolon + (@accum [$($acc:expr),*] $inst:expr) => { + qir!(@accum [$($acc,)* $inst]) + }; + + // Entry point + ( $($tokens:tt)* ) => { + qir!(@accum [] $($tokens)*) + }; +} + +#[cfg(test)] +pub(crate) use qir; + +/// Macro to build and run a simulation test. +/// +/// # Required fields: +/// - `simulator`: One of `StabilizerSimulator`, `NoisySimulator`, or `NoiselessSimulator` +/// - `program`: An expression that evaluates to `Vec` (use `qir!` macro) +/// - `num_qubits`: The number of qubits in the simulation +/// - `num_results`: The number of measurement results +/// - `expect`: The expected output (using `expect!` macro) +/// +/// # Optional fields: +/// - `shots`: Number of shots (defaults to 1) +/// - `seed`: Random seed (defaults to None) +/// - `noise`: A `NoiseConfig` built with `noise_config!` macro (defaults to NOISELESS) +/// - `format`: A function to format the output (defaults to `raw`) +/// +/// # Available format functions: +/// - `raw`: Joins all results with newlines (default) +/// - `histogram`: Counts occurrences of each result +/// - `histogram_percent`: Shows percentages for each result +/// - `top_n(n)`: Shows only top N results by count (descending) +/// - `top_n_percent(n)`: Shows only top N results with percentages (descending) +/// - `count`: Shows the total number of shots +/// - `summary`: Shows shots, unique count, and loss count +/// - `loss_count`: Counts results with qubit loss +/// +/// # Example +/// ```ignore +/// check_sim! { +/// simulator: NoisySimulator, +/// program: qir! { +/// x(2); +/// swap(2, 7); +/// mresetz(2, 0); +/// mresetz(7, 1); +/// }, +/// num_qubits: 8, +/// num_results: 2, +/// shots: 100, +/// seed: 42, +/// noise: noise_config! { ... }, +/// format: histogram, +/// output: expect![[r#"..."#]], +/// } +/// ``` +macro_rules! check_sim { + // Main entry with all fields + ( + simulator: $sim:ident, + program: $program:expr, + num_qubits: $num_qubits:expr, + num_results: $num_results:expr, + $( shots: $shots:expr, )? + $( seed: $seed:expr, )? + $( noise: $noise:expr, )? + $( format: $format:expr, )? + output: $expected:expr $(,)? + ) => {{ + // Get instructions from the expression + let instructions: Vec = $program; + + // Set defaults + let shots: u32 = check_sim!(@default_shots $( $shots )?); + let seed: Option = check_sim!(@default_seed $( $seed )?); + let noise: noise_config::NoiseConfig = check_sim!(@default_noise $( $noise )?); + let format_fn = check_sim!(@default_format $( $format )?); + + // Create simulator and run + let output = check_sim!(@run $sim, &instructions, $num_qubits, $num_results, shots, seed, noise); + + // Format output using the specified format function + let result_str = format_fn(&output); + + // Assert with expect + $expected.assert_eq(&result_str); + }}; + + // Default shots + (@default_shots $shots:expr) => { $shots }; + (@default_shots) => { 1 }; + + // Default seed + (@default_seed $seed:expr) => { Some($seed) }; + (@default_seed) => { None }; + + // Default noise + (@default_noise $noise:expr) => { $noise }; + (@default_noise) => { noise_config::NoiseConfig::::NOISELESS }; + + // Default format + (@default_format $format:expr) => { $format }; + (@default_format) => { raw }; + + // Run with StabilizerSimulator + (@run StabilizerSimulator, $instructions:expr, $num_qubits:expr, $num_results:expr, $shots:expr, $seed:expr, $noise:expr) => {{ + let make_simulator = |num_qubits, num_results, seed, noise| { + StabilizerSimulator::new(num_qubits as usize, num_results as usize, seed, noise) + }; + run($instructions, $num_qubits, $num_results, $shots, $seed, $noise, make_simulator) + }}; + + // Run with NoisySimulator + (@run NoisySimulator, $instructions:expr, $num_qubits:expr, $num_results:expr, $shots:expr, $seed:expr, $noise:expr) => {{ + use qdk_simulators::cpu_full_state_simulator::noise::Fault; + let make_simulator = |num_qubits, num_results, seed, noise| { + NoisySimulator::new(num_qubits as usize, num_results as usize, seed, noise) + }; + run::<_, CumulativeNoiseConfig, _>($instructions, $num_qubits, $num_results, $shots, $seed, $noise, make_simulator) + }}; + + // Run with NoiselessSimulator + (@run NoiselessSimulator, $instructions:expr, $num_qubits:expr, $num_results:expr, $shots:expr, $seed:expr, $noise:expr) => {{ + use qdk_simulators::cpu_full_state_simulator::noise::Fault; + let make_simulator = |num_qubits, num_results, seed, _noise: Arc>| { + NoiselessSimulator::new(num_qubits as usize, num_results as usize, seed, ()) + }; + run::<_, CumulativeNoiseConfig, _>($instructions, $num_qubits, $num_results, $shots, $seed, $noise, make_simulator) + }}; +} + +#[cfg(test)] +pub(crate) use check_sim; + +/// Macro to check that multiple QIR programs are equivalent. +/// +/// This macro runs each program in the list with a fresh simulator and compares their +/// final states. The programs are considered equivalent if they produce the same state +/// (up to global phase, as supported by the simulator's `state_dump` comparison). +/// +/// # Required fields: +/// - `simulator`: One of `StabilizerSimulator`, `NoisySimulator`, or `NoiselessSimulator` +/// - `programs`: An array of expressions evaluating to `Vec` (use `qir!` macro) +/// - `num_qubits`: The number of qubits in the simulation +/// +/// # Optional fields: +/// - `num_results`: The number of measurement results (defaults to 0) +/// +/// # Example +/// ```ignore +/// check_programs_are_eq! { +/// simulator: NoiselessSimulator, +/// programs: [ +/// qir! { i(0) }, +/// qir! { x(0); x(0); } +/// ], +/// num_qubits: 1, +/// } +/// ``` +macro_rules! check_programs_are_eq { + // Pattern without num_results - defaults to 0 + ( + simulator: $sim:ident, + programs: [ $( $program:expr ),+ $(,)? ], + num_qubits: $num_qubits:expr $(,)? + ) => {{ + check_programs_are_eq! { + simulator: $sim, + programs: [ $( $program ),+ ], + num_qubits: $num_qubits, + num_results: 0, + } + }}; + + // Pattern with explicit num_results + ( + simulator: $sim:ident, + programs: [ $( $program:expr ),+ $(,)? ], + num_qubits: $num_qubits:expr, + num_results: $num_results:expr $(,)? + ) => {{ + use qdk_simulators::Simulator; + let programs: Vec> = vec![ $( $program ),+ ]; + let simulators: Vec<_> = programs + .iter() + .map(|program| { + check_programs_are_eq!(@run_and_get_sim $sim, program, $num_qubits, $num_results) + }) + .collect(); + + // Compare all states to the first one + for (i, sim) in simulators.iter().enumerate().skip(1) { + assert!( + simulators[0].state_dump() == sim.state_dump(), + "Program 0 and program {} produce different states.\n\ + Program 0 state dump:\n{:#?}\n\n\ + Program {} state dump:\n{:#?}", + i, + simulators[0].state_dump(), + i, + simulators[1].state_dump(), + ); + } + }}; + + // Run with NoiselessSimulator and return the simulator + (@run_and_get_sim NoiselessSimulator, $program:expr, $num_qubits:expr, $num_results:expr) => {{ + run_and_get_simulator::( + $program, + $num_qubits as usize, + $num_results as usize, + 0, + (), + ) + }}; + + // Run with NoisySimulator and return the simulator + (@run_and_get_sim NoisySimulator, $program:expr, $num_qubits:expr, $num_results:expr) => {{ + use qdk_simulators::cpu_full_state_simulator::noise::Fault; + let noise: Arc> = Arc::new(noise_config::NoiseConfig::::NOISELESS.into()); + run_and_get_simulator::>>( + $program, + $num_qubits as usize, + $num_results as usize, + 0, + noise, + ) + }}; + + // Run with StabilizerSimulator and return the simulator + (@run_and_get_sim StabilizerSimulator, $program:expr, $num_qubits:expr, $num_results:expr) => {{ + use qdk_simulators::stabilizer_simulator::noise::Fault; + let noise: Arc> = Arc::new(noise_config::NoiseConfig::::NOISELESS.into()); + run_and_get_simulator::>>( + $program, + $num_qubits as usize, + $num_results as usize, + 0, + noise, + ) + }}; +} + +/// Helper function to run a QIR program and return the simulator with its final state. +pub fn run_and_get_simulator( + instructions: &[QirInstruction], + num_qubits: usize, + num_results: usize, + seed: u32, + noise: N, +) -> S +where + S: qdk_simulators::Simulator, +{ + let sim = S::new(num_qubits, num_results, seed, noise); + run_shot(instructions, sim) +} + +#[cfg(test)] +pub(crate) use check_programs_are_eq; + +// ==================== Format Functions ==================== +// These functions format the output of the simulator for testing. +// Use them with the `format:` field in `check_sim!`. + +/// Helper function to normalize simulator output by converting 'L' (loss) to '-'. +/// This ensures consistent loss representation across the test infrastructure. +fn normalize_output(output: &[String]) -> Vec { + output.iter().map(|s| s.replace('L', "-")).collect() +} + +/// Raw format: joins all shot results with newlines. +/// This is the default format. +/// Example: "010\n110\n001" +pub fn raw(output: &[String]) -> String { + let output = normalize_output(output); + output.join("\n") +} + +/// Histogram format: counts occurrences of each result and displays them sorted. +/// Useful for verifying probability distributions across many shots. +/// Example: "001: 25\n010: 50\n110: 25" +pub fn histogram(output: &[String]) -> String { + use std::collections::BTreeMap; + let output = normalize_output(output); + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + for result in &output { + *counts.entry(result.as_str()).or_insert(0) += 1; + } + counts + .into_iter() + .map(|(k, v)| format!("{k}: {v}")) + .collect::>() + .join("\n") +} + +/// Histogram with percentages: shows each result with its percentage. +/// Useful for verifying probability distributions with percentages. +/// Example: "001: 25.00%\n010: 50.00%\n110: 25.00%" +#[allow(clippy::cast_precision_loss, dead_code)] +pub fn histogram_percent(output: &[String]) -> String { + use std::collections::BTreeMap; + let output = normalize_output(output); + let total = output.len() as f64; + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + for result in &output { + *counts.entry(result.as_str()).or_insert(0) += 1; + } + counts + .into_iter() + .map(|(k, v)| format!("{k}: {:.2}%", (v as f64 / total) * 100.0)) + .collect::>() + .join("\n") +} + +/// Top N histogram: shows only the top N results by count, sorted by frequency (descending). +/// Useful for large quantum simulations where histograms are noisy. +/// Example with `top_n(3)`: "010: 50\n001: 30\n110: 15" +pub fn top_n(n: usize) -> impl Fn(&[String]) -> String { + move |output: &[String]| { + use std::collections::BTreeMap; + let output = normalize_output(output); + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + for result in &output { + *counts.entry(result.as_str()).or_insert(0) += 1; + } + let mut sorted: Vec<_> = counts.into_iter().collect(); + sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0))); + sorted + .into_iter() + .take(n) + .map(|(k, v)| format!("{k}: {v}")) + .collect::>() + .join("\n") + } +} + +/// Top N histogram with percentages: shows only the top N results by count with percentages. +/// Useful for large quantum simulations where histograms are noisy. +/// Example with `top_n_percent(3)`: "010: 50.00%\n001: 30.00%\n110: 15.00%" +#[allow(clippy::cast_precision_loss, dead_code)] +pub fn top_n_percent(n: usize) -> impl Fn(&[String]) -> String { + move |output: &[String]| { + use std::collections::BTreeMap; + let output = normalize_output(output); + let total = output.len() as f64; + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + for result in &output { + *counts.entry(result.as_str()).or_insert(0) += 1; + } + let mut sorted: Vec<_> = counts.into_iter().collect(); + sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0))); + sorted + .into_iter() + .take(n) + .map(|(k, v)| format!("{k}: {:.2}%", (v as f64 / total) * 100.0)) + .collect::>() + .join("\n") + } +} + +/// Summary format: shows shots, unique count, and loss count. +/// Useful for debugging and getting a quick overview of results. +/// Example: "shots: 100\nunique: 3\nloss: 5" +pub fn summary(output: &[String]) -> String { + use std::collections::BTreeSet; + let output = normalize_output(output); + let unique_results: BTreeSet<&str> = output.iter().map(String::as_str).collect(); + let loss_count = output.iter().filter(|s| s.contains('-')).count(); + format!( + "shots: {}\nunique: {}\nloss: {}", + output.len(), + unique_results.len(), + loss_count + ) +} + +/// Outcomes format: shows only the unique outcomes (sorted) without counts. +/// Useful for verifying that only valid outcomes appear in probabilistic tests. +/// Example: "00\n11" for a Bell state +pub fn outcomes(output: &[String]) -> String { + use std::collections::BTreeSet; + let output = normalize_output(output); + let unique_results: BTreeSet<&str> = output.iter().map(String::as_str).collect(); + unique_results.into_iter().collect::>().join("\n") +} diff --git a/source/simulators/src/cpu_full_state_simulator.rs b/source/simulators/src/cpu_full_state_simulator.rs index 6c7fd82571..f2962bf263 100644 --- a/source/simulators/src/cpu_full_state_simulator.rs +++ b/source/simulators/src/cpu_full_state_simulator.rs @@ -245,6 +245,7 @@ impl NoiselessSimulator { impl Simulator for NoiselessSimulator { type Noise = (); + type StateDumpData = noisy_simulator::StateVector; fn new(num_qubits: usize, num_results: usize, seed: u32, _noise: Self::Noise) -> Self { Self { @@ -394,6 +395,10 @@ impl Simulator for NoiselessSimulator { fn correlated_noise_intrinsic(&mut self, _intrinsic_id: IntrinsicID, _targets: &[usize]) { // Noise is a no-op for the noiseless simulator. } + + fn state_dump(&self) -> &Self::StateDumpData { + self.state.state().expect("state should be valid") + } } /// A noisy state-vector simulator. @@ -547,6 +552,7 @@ impl NoisySimulator { impl Simulator for NoisySimulator { type Noise = Arc>; + type StateDumpData = noisy_simulator::StateVector; fn new(num_qubits: usize, num_results: usize, seed: u32, noise_config: Self::Noise) -> Self { Self { @@ -863,4 +869,8 @@ impl Simulator for NoisySimulator { fn take_measurements(&mut self) -> Vec { std::mem::take(&mut self.measurements) } + + fn state_dump(&self) -> &Self::StateDumpData { + self.state.state().expect("state should be valid") + } } diff --git a/source/simulators/src/lib.rs b/source/simulators/src/lib.rs index 95ad26112c..353572d72f 100644 --- a/source/simulators/src/lib.rs +++ b/source/simulators/src/lib.rs @@ -21,6 +21,7 @@ pub enum MeasurementResult { pub trait Simulator { type Noise; + type StateDumpData; /// Creates a new simulator. fn new(num_qubits: usize, num_results: usize, seed: u32, noise: Self::Noise) -> Self; @@ -103,4 +104,8 @@ pub trait Simulator { /// Returns a list of the measurements recorded during the simulation. fn take_measurements(&mut self) -> Vec; + + /// Dumps the current state of the simulator in some representation that can be compared + /// for `PartialEq` up to a global phase. This is meant to be used for testing. + fn state_dump(&self) -> &Self::StateDumpData; } diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 2028df938a..74a691badf 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -178,6 +178,7 @@ impl StabilizerSimulator { impl Simulator for StabilizerSimulator { type Noise = Arc>; + type StateDumpData = paulimer::clifford::CliffordUnitary; fn new(num_qubits: usize, num_results: usize, seed: u32, noise_config: Self::Noise) -> Self { Self { @@ -396,4 +397,8 @@ impl Simulator for StabilizerSimulator { 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() + } }