From 6039718d2c547312df46c9681a5cad7bd14474a9 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Thu, 22 Jan 2026 14:23:55 -0800 Subject: [PATCH 1/9] add simulators test infrastructure --- Cargo.lock | 1 + source/pip/Cargo.toml | 3 + .../pip/src/qir_simulation/cpu_simulators.rs | 678 ++++++++++++++++++ 3 files changed, 682 insertions(+) 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/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..43adca1940 100644 --- a/source/pip/src/qir_simulation/cpu_simulators.rs +++ b/source/pip/src/qir_simulation/cpu_simulators.rs @@ -263,3 +263,681 @@ 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 cnot(q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::CNOT, q1, q2) + } + pub fn cx(q1: u32, q2: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::CX, q1, q2) + } + 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 m(q: u32, r: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::M, q, r) + } + 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 + 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)]` + macro_rules! qir { + ( $($inst:expr);* $(;)? ) => {{ + vec![$($inst),*] + }}; + } + + #[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) + /// - `unique`: Shows only unique results, sorted + /// - `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, + /// expect: 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, )? + expect: $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; + + // ==================== 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)] + 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)] + 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") + } + } + + /// Unique format: shows only unique results, sorted, one per line. + /// Useful for verifying that specific outcomes are possible. + /// Example: "001\n010\n110" + pub fn unique(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") + } + + /// Count format: shows the total number of shots. + /// Useful for quick sanity checks on shot count. + /// Example: "100" + pub fn count(output: &[String]) -> String { + output.len().to_string() + } + + /// 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 + ) + } + + /// Loss count format: counts how many results contain loss ('-'). + /// Useful for testing noisy simulations with qubit loss. + /// + /// Example output: + /// ```text + /// total: 100 + /// loss: 5 + /// no_loss: 95 + /// ``` + pub fn loss_count(output: &[String]) -> String { + let output = normalize_output(output); + let loss_count = output.iter().filter(|s| s.contains('-')).count(); + let no_loss_count = output.len() - loss_count; + format!( + "total: {}\nloss: {}\nno_loss: {}", + output.len(), + loss_count, + no_loss_count + ) + } + } + + mod full_state_noiseless { + use super::{super::*, test_utils::*}; + use expect_test::expect; + + #[test] + fn simple_x_gate_noiseless() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn simple_h_gate_with_seed() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 10, + seed: 42, + expect: expect![[r#" + 0 + 1 + 0 + 1 + 1 + 1 + 1 + 0 + 0 + 0"#]], + } + } + + #[test] + fn swap_gate_test() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"01"#]], + } + } + } + + mod full_state_noisy { + use super::{super::*, test_utils::*}; + use expect_test::expect; + + #[test] + fn noisy_simulator_with_noise_config() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 10, + noise: noise_config! { + x: { + x: 1e-10, + z: 1e-10, + }, + }, + expect: expect![[r#" + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1"#]], + } + } + } + + mod clifford { + use super::{super::*, test_utils::*}; + use expect_test::expect; + + #[test] + fn stabilizer_simulator_test() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"11"#]], + } + } + } +} From 3ca55d758c89e99221438bc6048fee7028e65682 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Mon, 26 Jan 2026 15:13:45 -0800 Subject: [PATCH 2/9] add unit tests --- .../pip/src/qir_simulation/cpu_simulators.rs | 1067 ++++++++++++++++- 1 file changed, 1007 insertions(+), 60 deletions(-) diff --git a/source/pip/src/qir_simulation/cpu_simulators.rs b/source/pip/src/qir_simulation/cpu_simulators.rs index 43adca1940..b56066fc2a 100644 --- a/source/pip/src/qir_simulation/cpu_simulators.rs +++ b/source/pip/src/qir_simulation/cpu_simulators.rs @@ -566,7 +566,6 @@ mod tests { /// - `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) - /// - `unique`: Shows only unique results, sorted /// - `count`: Shows the total number of shots /// - `summary`: Shows shots, unique count, and loss count /// - `loss_count`: Counts results with qubit loss @@ -768,16 +767,6 @@ mod tests { } } - /// Unique format: shows only unique results, sorted, one per line. - /// Useful for verifying that specific outcomes are possible. - /// Example: "001\n010\n110" - pub fn unique(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") - } - /// Count format: shows the total number of shots. /// Useful for quick sanity checks on shot count. /// Example: "100" @@ -826,13 +815,76 @@ mod tests { mod full_state_noiseless { use super::{super::*, test_utils::*}; use expect_test::expect; + use std::f64::consts::PI; + + // ==================== Single-Qubit Gate Tests ==================== + + #[test] + fn x_gate_flips_qubit() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } #[test] - fn simple_x_gate_noiseless() { + fn double_x_gate_returns_to_zero() { check_sim! { simulator: NoiselessSimulator, program: qir! { x(0); + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn y_gate_flips_qubit() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + y(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn z_gate_preserves_zero_state() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + z(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn z_gate_applies_phase() { + // H·Z·H = X, which flips |0⟩ to |1⟩ + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + z(0); + h(0); mresetz(0, 0); }, num_qubits: 1, @@ -842,7 +894,7 @@ mod tests { } #[test] - fn simple_h_gate_with_seed() { + fn h_gate_creates_superposition() { check_sim! { simulator: NoiselessSimulator, program: qir! { @@ -851,83 +903,221 @@ mod tests { }, num_qubits: 1, num_results: 1, - shots: 10, + shots: 100, seed: 42, + format: histogram, expect: expect![[r#" - 0 - 1 - 0 - 1 - 1 - 1 - 1 - 0 - 0 - 0"#]], + 0: 46 + 1: 54"#]], } } #[test] - fn swap_gate_test() { + fn double_h_gate_returns_to_zero() { check_sim! { simulator: NoiselessSimulator, program: qir! { - x(0); - swap(0, 1); + h(0); + h(0); mresetz(0, 0); - mresetz(1, 1); }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"01"#]], + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], } } - } - mod full_state_noisy { - use super::{super::*, test_utils::*}; - use expect_test::expect; + #[test] + fn s_gate_preserves_computational_basis() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + s(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } #[test] - fn noisy_simulator_with_noise_config() { + fn s_gate_applies_phase() { + // S = sqrt(Z), so S·S = Z + // H·Z·H = X, which flips |0⟩ to |1⟩ + // Therefore H·S·S·H|0⟩ = |1⟩ check_sim! { - simulator: NoisySimulator, + simulator: NoiselessSimulator, program: qir! { - x(0); + h(0); + s(0); + s(0); + h(0); mresetz(0, 0); }, num_qubits: 1, num_results: 1, - shots: 10, - noise: noise_config! { - x: { - x: 1e-10, - z: 1e-10, - }, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn s_adj_cancels_s() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + s(0); + s_adj(0); + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn t_gate_preserves_computational_basis() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + t(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn t_gate_applies_phase() { + // T = sqrt(S) = fourth root of Z, so T^4 = Z + // H·Z·H = X, which flips |0⟩ to |1⟩ + // Therefore H·T·T·T·T·H|0⟩ = |1⟩ + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + t(0); + t(0); + t(0); + t(0); + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn t_adj_cancels_t() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + t(0); + t_adj(0); + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + // ==================== Rotation Gate Tests ==================== + + #[test] + fn rx_pi_flips_qubit() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + rx(PI, 0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn ry_pi_flips_qubit() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + ry(PI, 0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn rz_preserves_zero_state() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + rz(PI, 0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn rz_pi_equivalent_to_z() { + // RZ(π) applies a π phase, equivalent to Z (up to global phase) + // H·RZ(π)·H should flip |0⟩ to |1⟩ + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + rz(PI, 0); + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn rx_half_pi_creates_superposition() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + rx(PI / 2.0, 0); + mresetz(0, 0); }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: 42, + format: histogram, expect: expect![[r#" - 1 - 1 - 1 - 1 - 1 - 1 - 1 - 1 - 1 - 1"#]], + 0: 46 + 1: 54"#]], } } - } - mod clifford { - use super::{super::*, test_utils::*}; - use expect_test::expect; + // ==================== Two-Qubit Gate Tests ==================== #[test] - fn stabilizer_simulator_test() { + fn cx_gate_entangles_qubits() { check_sim! { - simulator: StabilizerSimulator, + simulator: NoiselessSimulator, program: qir! { x(0); cx(0, 1); @@ -939,5 +1129,762 @@ mod tests { expect: expect![[r#"11"#]], } } + + #[test] + fn cx_gate_no_flip_when_control_is_zero() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"00"#]], + } + } + + #[test] + fn cz_gate_preserves_computational_basis() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + x(1); + cz(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"11"#]], + } + } + + #[test] + fn cz_gate_applies_phase() { + // CZ applies a phase flip when both qubits are |1⟩ + // Start with Bell state |00⟩ + |11⟩, apply CZ to get |00⟩ - |11⟩ + // Then reverse Bell circuit: CX followed by H on control + // |00⟩ - |11⟩ → CX → |00⟩ - |10⟩ → H⊗I → |10⟩ + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + cx(0, 1); + cz(0, 1); + cx(0, 1); + h(0); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"10"#]], + } + } + + #[test] + fn swap_gate_exchanges_qubit_states() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"01"#]], + } + } + + #[test] + fn double_swap_returns_to_original() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + swap(0, 1); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"10"#]], + } + } + + // ==================== Two-Qubit Rotation Gate Tests ==================== + + #[test] + fn rxx_pi_creates_bell_state() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + rxx(PI / 2.0, 0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 100, + seed: 42, + format: histogram, + expect: expect![[r#" + 00: 46 + 11: 54"#]], + } + } + + #[test] + fn ryy_pi_creates_bell_state() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + ryy(PI / 2.0, 0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 100, + seed: 42, + format: histogram, + expect: expect![[r#" + 00: 46 + 11: 54"#]], + } + } + + #[test] + fn rzz_preserves_computational_basis() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + rzz(PI, 0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"00"#]], + } + } + + #[test] + fn rzz_applies_phase() { + // Start with |01⟩, apply H⊗H to get |+⟩|-⟩ + // RZZ(π) transforms |+⟩|-⟩ to |-⟩|+⟩ (with global phase) + // H⊗H transforms |-⟩|+⟩ to |10⟩ + // Without RZZ, H⊗H would return |+−⟩ back to |01⟩ + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(1); + h(0); + h(1); + rzz(PI, 0, 1); + h(0); + h(1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"10"#]], + } + } + + // ==================== Bell State Tests ==================== + + #[test] + fn bell_state_produces_correlated_measurements() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 100, + seed: 42, + format: histogram, + expect: expect![[r#" + 00: 49 + 11: 51"#]], + } + } + + // ==================== Reset Tests ==================== + + #[test] + fn reset_returns_qubit_to_zero() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + reset(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn mresetz_resets_after_measurement() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + mresetz(0, 0); + mresetz(0, 1); + }, + num_qubits: 1, + num_results: 2, + expect: expect![[r#"10"#]], + } + } + + // ==================== Multi-Qubit Tests ==================== + + #[test] + fn ghz_state_three_qubits() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + cx(0, 1); + cx(1, 2); + mresetz(0, 0); + mresetz(1, 1); + mresetz(2, 2); + }, + num_qubits: 3, + num_results: 3, + shots: 100, + seed: 42, + format: histogram, + expect: expect![[r#" + 000: 51 + 111: 49"#]], + } + } + } + + mod full_state_noisy { + use super::{super::*, test_utils::*}; + use expect_test::expect; + + // ==================== Basic Noisy Tests ==================== + + #[test] + fn noiseless_config_produces_clean_results() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + noise: noise_config! {}, + format: histogram, + expect: expect![[r#"1: 100"#]], + } + } + + #[test] + fn x_noise_causes_bit_flips() { + // High X noise on X gate should cause some results to flip back to 0 + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: 42, + noise: noise_config! { + x: { + x: 0.1, + }, + }, + format: histogram, + expect: expect![[r#" + 0: 97 + 1: 903"#]], + } + } + + #[test] + fn z_noise_does_not_affect_computational_basis() { + // Z noise should not change measurement outcomes in computational basis + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: 42, + noise: noise_config! { + x: { + z: 0.5, + }, + }, + format: histogram, + expect: expect![[r#"1: 100"#]], + } + } + + #[test] + fn loss_noise_produces_loss_marker() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: 42, + noise: noise_config! { + x: { + loss: 0.1, + }, + }, + format: summary, + expect: expect![[r#" + shots: 1000 + unique: 2 + loss: 119"#]], + } + } + + // ==================== Two-Qubit Noise Tests ==================== + + #[test] + fn cx_noise_affects_entangled_qubits() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: 42, + noise: noise_config! { + cx: { + xi: 0.05, + ix: 0.05, + }, + }, + format: top_n(4), + expect: expect![[r#" + 11: 908 + 10: 56 + 01: 36"#]], + } + } + + // ==================== Hadamard with Noise ==================== + + #[test] + fn hadamard_with_noise_still_produces_superposition() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: 42, + noise: noise_config! { + h: { + x: 0.01, + z: 0.01, + }, + }, + format: histogram, + expect: expect![[r#" + 0: 46 + 1: 54"#]], + } + } + + // ==================== Multiple Gates with Noise ==================== + + #[test] + fn bell_state_with_noise_produces_errors() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + h(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: 42, + noise: noise_config! { + h: { + x: 0.02, + }, + cx: { + xi: 0.02, + ix: 0.02, + }, + }, + format: top_n(4), + expect: expect![[r#" + 00: 491 + 11: 481 + 01: 18 + 10: 10"#]], + } + } + } + + mod clifford { + use super::{super::*, test_utils::*}; + use expect_test::expect; + + // ==================== Single-Qubit Clifford Gate Tests ==================== + + #[test] + fn x_gate_flips_qubit() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn y_gate_flips_qubit() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + y(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn z_gate_preserves_zero() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + z(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn z_gate_applies_phase() { + // H·Z·H = X, which flips |0⟩ to |1⟩ + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + z(0); + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn h_gate_creates_superposition() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: 42, + format: histogram, + expect: expect![[r#" + 0: 50 + 1: 50"#]], + } + } + + #[test] + fn s_gate_preserves_computational_basis() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + s(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn s_gate_applies_phase() { + // S = sqrt(Z), so S·S = Z + // H·Z·H = X, which flips |0⟩ to |1⟩ + // Therefore H·S·S·H|0⟩ = |1⟩ + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + s(0); + s(0); + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"1"#]], + } + } + + #[test] + fn s_adj_cancels_s() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + s(0); + s_adj(0); + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + // ==================== Two-Qubit Clifford Gate Tests ==================== + + #[test] + fn cx_gate_entangles_qubits() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"11"#]], + } + } + + #[test] + fn cz_gate_preserves_computational_basis() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + x(1); + cz(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"11"#]], + } + } + + #[test] + fn cz_gate_applies_phase() { + // CZ applies a phase flip when both qubits are |1⟩ + // Start with Bell state |00⟩ + |11⟩, apply CZ to get |00⟩ - |11⟩ + // Then reverse Bell circuit: CX followed by H on control + // |00⟩ - |11⟩ → CX → |00⟩ - |10⟩ → H⊗I → |10⟩ + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + cx(0, 1); + cz(0, 1); + cx(0, 1); + h(0); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"10"#]], + } + } + + #[test] + fn swap_gate_exchanges_qubit_states() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"01"#]], + } + } + + // ==================== Bell State Tests ==================== + + #[test] + fn bell_state_produces_correlated_measurements() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 100, + seed: 42, + format: histogram, + expect: expect![[r#" + 00: 58 + 11: 42"#]], + } + } + + // ==================== GHZ State Test ==================== + + #[test] + fn ghz_state_three_qubits() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + cx(0, 1); + cx(1, 2); + mresetz(0, 0); + mresetz(1, 1); + mresetz(2, 2); + }, + num_qubits: 3, + num_results: 3, + shots: 100, + seed: 42, + format: histogram, + expect: expect![[r#" + 000: 56 + 111: 44"#]], + } + } + + // ==================== Reset Tests ==================== + + #[test] + fn reset_returns_qubit_to_zero() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + reset(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + expect: expect![[r#"0"#]], + } + } + + #[test] + fn mresetz_resets_after_measurement() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + mresetz(0, 1); + }, + num_qubits: 1, + num_results: 2, + expect: expect![[r#"10"#]], + } + } + + // ==================== Noisy Stabilizer Tests ==================== + + #[test] + fn stabilizer_with_noise() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: 42, + noise: noise_config! { + cx: { + xi: 0.05, + ix: 0.05, + }, + }, + format: top_n(4), + expect: expect![[r#" + 11: 908 + 10: 56 + 01: 36"#]], + } + } } } From b994e4f9154aa69426761f457a9f3f7f6f5c4b31 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Wed, 28 Jan 2026 11:19:06 -0800 Subject: [PATCH 3/9] add within-apply --- .../pip/src/qir_simulation/cpu_simulators.rs | 162 ++++++++++++------ 1 file changed, 107 insertions(+), 55 deletions(-) diff --git a/source/pip/src/qir_simulation/cpu_simulators.rs b/source/pip/src/qir_simulation/cpu_simulators.rs index b56066fc2a..3e7566c0ed 100644 --- a/source/pip/src/qir_simulation/cpu_simulators.rs +++ b/source/pip/src/qir_simulation/cpu_simulators.rs @@ -536,10 +536,63 @@ mod tests { /// } /// ``` /// 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 { - ( $($inst:expr);* $(;)? ) => {{ - vec![$($inst),*] + // 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)] @@ -882,9 +935,7 @@ mod tests { check_sim! { simulator: NoiselessSimulator, program: qir! { - h(0); - z(0); - h(0); + within { h(0) } apply { z(0) } mresetz(0, 0); }, num_qubits: 1, @@ -949,10 +1000,10 @@ mod tests { check_sim! { simulator: NoiselessSimulator, program: qir! { - h(0); - s(0); - s(0); - h(0); + within { h(0) } apply { + s(0); + s(0); + } mresetz(0, 0); }, num_qubits: 1, @@ -966,10 +1017,10 @@ mod tests { check_sim! { simulator: NoiselessSimulator, program: qir! { - h(0); - s(0); - s_adj(0); - h(0); + within { h(0) } apply { + s(0); + s_adj(0); + } mresetz(0, 0); }, num_qubits: 1, @@ -1000,12 +1051,12 @@ mod tests { check_sim! { simulator: NoiselessSimulator, program: qir! { - h(0); - t(0); - t(0); - t(0); - t(0); - h(0); + within { h(0) } apply { + t(0); + t(0); + t(0); + t(0); + } mresetz(0, 0); }, num_qubits: 1, @@ -1019,10 +1070,10 @@ mod tests { check_sim! { simulator: NoiselessSimulator, program: qir! { - h(0); - t(0); - t_adj(0); - h(0); + within { h(0) } apply { + t(0); + t_adj(0); + } mresetz(0, 0); }, num_qubits: 1, @@ -1082,9 +1133,7 @@ mod tests { check_sim! { simulator: NoiselessSimulator, program: qir! { - h(0); - rz(PI, 0); - h(0); + within { h(0) } apply { rz(PI, 0) } mresetz(0, 0); }, num_qubits: 1, @@ -1171,11 +1220,11 @@ mod tests { check_sim! { simulator: NoiselessSimulator, program: qir! { - h(0); - cx(0, 1); - cz(0, 1); - cx(0, 1); - h(0); + within { h(0) } apply { + cx(0, 1); + cz(0, 1); + cx(0, 1); + } mresetz(0, 0); mresetz(1, 1); }, @@ -1285,11 +1334,7 @@ mod tests { simulator: NoiselessSimulator, program: qir! { x(1); - h(0); - h(1); - rzz(PI, 0, 1); - h(0); - h(1); + within { h(0); h(1); } apply { rzz(PI, 0, 1) } mresetz(0, 0); mresetz(1, 1); }, @@ -1622,9 +1667,7 @@ mod tests { check_sim! { simulator: StabilizerSimulator, program: qir! { - h(0); - z(0); - h(0); + within { h(0) } apply { z(0) } mresetz(0, 0); }, num_qubits: 1, @@ -1722,42 +1765,51 @@ mod tests { } #[test] - fn cz_gate_preserves_computational_basis() { + fn cz_gate_with_zero_as_ctrl() { check_sim! { simulator: StabilizerSimulator, - program: qir! { - x(0); - x(1); + program: qir!{ cz(0, 1); - mresetz(0, 0); + mresetz(1, 0); + within { h(1) } apply { cz(0, 1) } mresetz(1, 1); }, num_qubits: 2, num_results: 2, - expect: expect![[r#"11"#]], + expect: expect![["00"]], } } #[test] - fn cz_gate_applies_phase() { - // CZ applies a phase flip when both qubits are |1⟩ - // Start with Bell state |00⟩ + |11⟩, apply CZ to get |00⟩ - |11⟩ - // Then reverse Bell circuit: CX followed by H on control - // |00⟩ - |11⟩ → CX → |00⟩ - |10⟩ → H⊗I → |10⟩ + fn cz_gate_with_one_as_ctrl() { check_sim! { simulator: StabilizerSimulator, program: qir! { - h(0); - cx(0, 1); + x(0); // flip control so that cz works cz(0, 1); - cx(0, 1); - h(0); + mresetz(1, 0); // measure qubit 1 + within { h(1) } apply { cz(0, 1) } + mresetz(1, 1); // measure qubit 1 again + }, + num_qubits: 2, + num_results: 2, + expect: expect![[r#"01"#]], // these two results are on the same qubit + } + } + + #[test] + fn cz_gate_applies_phase() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(1); + within { h(0) } apply { cz(0, 1) } mresetz(0, 0); mresetz(1, 1); }, num_qubits: 2, num_results: 2, - expect: expect![[r#"10"#]], + expect: expect![[r#"11"#]], } } From 3241744a8e721b741424a86bc13b6b7c0e8a5a43 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Thu, 29 Jan 2026 13:19:46 -0800 Subject: [PATCH 4/9] better tests --- source/noisy_simulator/src/lib.rs | 4 + .../src/state_vector_simulator.rs | 55 +- source/noisy_simulator/src/tests.rs | 4 +- .../pip/src/qir_simulation/cpu_simulators.rs | 1688 +---------------- .../qir_simulation/cpu_simulators/tests.rs | 11 + .../tests/clifford_noiseless.rs | 523 +++++ .../cpu_simulators/tests/clifford_noisy.rs | 267 +++ .../tests/full_state_noiseless.rs | 705 +++++++ .../cpu_simulators/tests/full_state_noisy.rs | 479 +++++ .../cpu_simulators/tests/test_utils.rs | 716 +++++++ .../src/cpu_full_state_simulator.rs | 10 + source/simulators/src/lib.rs | 5 + source/simulators/src/stabilizer_simulator.rs | 5 + 13 files changed, 2789 insertions(+), 1683 deletions(-) create mode 100644 source/pip/src/qir_simulation/cpu_simulators/tests.rs create mode 100644 source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs create mode 100644 source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs create mode 100644 source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs create mode 100644 source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs create mode 100644 source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs 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/src/qir_simulation/cpu_simulators.rs b/source/pip/src/qir_simulation/cpu_simulators.rs index 3e7566c0ed..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,1682 +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 cnot(q1: u32, q2: u32) -> QirInstruction { - QirInstruction::TwoQubitGate(QirInstructionId::CNOT, q1, q2) - } - pub fn cx(q1: u32, q2: u32) -> QirInstruction { - QirInstruction::TwoQubitGate(QirInstructionId::CX, q1, q2) - } - 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 m(q: u32, r: u32) -> QirInstruction { - QirInstruction::TwoQubitGate(QirInstructionId::M, q, r) - } - 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 - 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, - /// expect: 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, )? - expect: $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; - - // ==================== 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)] - 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)] - 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") - } - } - - /// Count format: shows the total number of shots. - /// Useful for quick sanity checks on shot count. - /// Example: "100" - pub fn count(output: &[String]) -> String { - output.len().to_string() - } - - /// 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 - ) - } - - /// Loss count format: counts how many results contain loss ('-'). - /// Useful for testing noisy simulations with qubit loss. - /// - /// Example output: - /// ```text - /// total: 100 - /// loss: 5 - /// no_loss: 95 - /// ``` - pub fn loss_count(output: &[String]) -> String { - let output = normalize_output(output); - let loss_count = output.iter().filter(|s| s.contains('-')).count(); - let no_loss_count = output.len() - loss_count; - format!( - "total: {}\nloss: {}\nno_loss: {}", - output.len(), - loss_count, - no_loss_count - ) - } - } - - mod full_state_noiseless { - use super::{super::*, test_utils::*}; - use expect_test::expect; - use std::f64::consts::PI; - - // ==================== Single-Qubit Gate Tests ==================== - - #[test] - fn x_gate_flips_qubit() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn double_x_gate_returns_to_zero() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - x(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn y_gate_flips_qubit() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - y(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn z_gate_preserves_zero_state() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - z(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn z_gate_applies_phase() { - // H·Z·H = X, which flips |0⟩ to |1⟩ - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - within { h(0) } apply { z(0) } - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn h_gate_creates_superposition() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - h(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 0: 46 - 1: 54"#]], - } - } - - #[test] - fn double_h_gate_returns_to_zero() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - h(0); - h(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn s_gate_preserves_computational_basis() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - s(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn s_gate_applies_phase() { - // S = sqrt(Z), so S·S = Z - // H·Z·H = X, which flips |0⟩ to |1⟩ - // Therefore H·S·S·H|0⟩ = |1⟩ - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - within { h(0) } apply { - s(0); - s(0); - } - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn s_adj_cancels_s() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - within { h(0) } apply { - s(0); - s_adj(0); - } - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn t_gate_preserves_computational_basis() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - t(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn t_gate_applies_phase() { - // T = sqrt(S) = fourth root of Z, so T^4 = Z - // H·Z·H = X, which flips |0⟩ to |1⟩ - // Therefore H·T·T·T·T·H|0⟩ = |1⟩ - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - within { h(0) } apply { - t(0); - t(0); - t(0); - t(0); - } - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn t_adj_cancels_t() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - within { h(0) } apply { - t(0); - t_adj(0); - } - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - // ==================== Rotation Gate Tests ==================== - - #[test] - fn rx_pi_flips_qubit() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - rx(PI, 0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn ry_pi_flips_qubit() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - ry(PI, 0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn rz_preserves_zero_state() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - rz(PI, 0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn rz_pi_equivalent_to_z() { - // RZ(π) applies a π phase, equivalent to Z (up to global phase) - // H·RZ(π)·H should flip |0⟩ to |1⟩ - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - within { h(0) } apply { rz(PI, 0) } - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn rx_half_pi_creates_superposition() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - rx(PI / 2.0, 0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 0: 46 - 1: 54"#]], - } - } - - // ==================== Two-Qubit Gate Tests ==================== - - #[test] - fn cx_gate_entangles_qubits() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"11"#]], - } - } - - #[test] - fn cx_gate_no_flip_when_control_is_zero() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"00"#]], - } - } - - #[test] - fn cz_gate_preserves_computational_basis() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - x(1); - cz(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"11"#]], - } - } - - #[test] - fn cz_gate_applies_phase() { - // CZ applies a phase flip when both qubits are |1⟩ - // Start with Bell state |00⟩ + |11⟩, apply CZ to get |00⟩ - |11⟩ - // Then reverse Bell circuit: CX followed by H on control - // |00⟩ - |11⟩ → CX → |00⟩ - |10⟩ → H⊗I → |10⟩ - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - within { h(0) } apply { - cx(0, 1); - cz(0, 1); - cx(0, 1); - } - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"10"#]], - } - } - - #[test] - fn swap_gate_exchanges_qubit_states() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - swap(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"01"#]], - } - } - - #[test] - fn double_swap_returns_to_original() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - swap(0, 1); - swap(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"10"#]], - } - } - - // ==================== Two-Qubit Rotation Gate Tests ==================== - - #[test] - fn rxx_pi_creates_bell_state() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - rxx(PI / 2.0, 0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 00: 46 - 11: 54"#]], - } - } - - #[test] - fn ryy_pi_creates_bell_state() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - ryy(PI / 2.0, 0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 00: 46 - 11: 54"#]], - } - } - - #[test] - fn rzz_preserves_computational_basis() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - rzz(PI, 0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"00"#]], - } - } - - #[test] - fn rzz_applies_phase() { - // Start with |01⟩, apply H⊗H to get |+⟩|-⟩ - // RZZ(π) transforms |+⟩|-⟩ to |-⟩|+⟩ (with global phase) - // H⊗H transforms |-⟩|+⟩ to |10⟩ - // Without RZZ, H⊗H would return |+−⟩ back to |01⟩ - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(1); - within { h(0); h(1); } apply { rzz(PI, 0, 1) } - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"10"#]], - } - } - - // ==================== Bell State Tests ==================== - - #[test] - fn bell_state_produces_correlated_measurements() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - h(0); - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 00: 49 - 11: 51"#]], - } - } - - // ==================== Reset Tests ==================== - - #[test] - fn reset_returns_qubit_to_zero() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - reset(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn mresetz_resets_after_measurement() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - x(0); - mresetz(0, 0); - mresetz(0, 1); - }, - num_qubits: 1, - num_results: 2, - expect: expect![[r#"10"#]], - } - } - - // ==================== Multi-Qubit Tests ==================== - - #[test] - fn ghz_state_three_qubits() { - check_sim! { - simulator: NoiselessSimulator, - program: qir! { - h(0); - cx(0, 1); - cx(1, 2); - mresetz(0, 0); - mresetz(1, 1); - mresetz(2, 2); - }, - num_qubits: 3, - num_results: 3, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 000: 51 - 111: 49"#]], - } - } - } - - mod full_state_noisy { - use super::{super::*, test_utils::*}; - use expect_test::expect; - - // ==================== Basic Noisy Tests ==================== - - #[test] - fn noiseless_config_produces_clean_results() { - check_sim! { - simulator: NoisySimulator, - program: qir! { - x(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 100, - noise: noise_config! {}, - format: histogram, - expect: expect![[r#"1: 100"#]], - } - } - - #[test] - fn x_noise_causes_bit_flips() { - // High X noise on X gate should cause some results to flip back to 0 - check_sim! { - simulator: NoisySimulator, - program: qir! { - x(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 1000, - seed: 42, - noise: noise_config! { - x: { - x: 0.1, - }, - }, - format: histogram, - expect: expect![[r#" - 0: 97 - 1: 903"#]], - } - } - - #[test] - fn z_noise_does_not_affect_computational_basis() { - // Z noise should not change measurement outcomes in computational basis - check_sim! { - simulator: NoisySimulator, - program: qir! { - x(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 100, - seed: 42, - noise: noise_config! { - x: { - z: 0.5, - }, - }, - format: histogram, - expect: expect![[r#"1: 100"#]], - } - } - - #[test] - fn loss_noise_produces_loss_marker() { - check_sim! { - simulator: NoisySimulator, - program: qir! { - x(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 1000, - seed: 42, - noise: noise_config! { - x: { - loss: 0.1, - }, - }, - format: summary, - expect: expect![[r#" - shots: 1000 - unique: 2 - loss: 119"#]], - } - } - - // ==================== Two-Qubit Noise Tests ==================== - - #[test] - fn cx_noise_affects_entangled_qubits() { - check_sim! { - simulator: NoisySimulator, - program: qir! { - x(0); - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - shots: 1000, - seed: 42, - noise: noise_config! { - cx: { - xi: 0.05, - ix: 0.05, - }, - }, - format: top_n(4), - expect: expect![[r#" - 11: 908 - 10: 56 - 01: 36"#]], - } - } - - // ==================== Hadamard with Noise ==================== - - #[test] - fn hadamard_with_noise_still_produces_superposition() { - check_sim! { - simulator: NoisySimulator, - program: qir! { - h(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 100, - seed: 42, - noise: noise_config! { - h: { - x: 0.01, - z: 0.01, - }, - }, - format: histogram, - expect: expect![[r#" - 0: 46 - 1: 54"#]], - } - } - - // ==================== Multiple Gates with Noise ==================== - - #[test] - fn bell_state_with_noise_produces_errors() { - check_sim! { - simulator: NoisySimulator, - program: qir! { - h(0); - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - shots: 1000, - seed: 42, - noise: noise_config! { - h: { - x: 0.02, - }, - cx: { - xi: 0.02, - ix: 0.02, - }, - }, - format: top_n(4), - expect: expect![[r#" - 00: 491 - 11: 481 - 01: 18 - 10: 10"#]], - } - } - } - - mod clifford { - use super::{super::*, test_utils::*}; - use expect_test::expect; - - // ==================== Single-Qubit Clifford Gate Tests ==================== - - #[test] - fn x_gate_flips_qubit() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn y_gate_flips_qubit() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - y(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn z_gate_preserves_zero() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - z(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn z_gate_applies_phase() { - // H·Z·H = X, which flips |0⟩ to |1⟩ - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - within { h(0) } apply { z(0) } - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn h_gate_creates_superposition() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - h(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 0: 50 - 1: 50"#]], - } - } - - #[test] - fn s_gate_preserves_computational_basis() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - s(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn s_gate_applies_phase() { - // S = sqrt(Z), so S·S = Z - // H·Z·H = X, which flips |0⟩ to |1⟩ - // Therefore H·S·S·H|0⟩ = |1⟩ - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - h(0); - s(0); - s(0); - h(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"1"#]], - } - } - - #[test] - fn s_adj_cancels_s() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - h(0); - s(0); - s_adj(0); - h(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - // ==================== Two-Qubit Clifford Gate Tests ==================== - - #[test] - fn cx_gate_entangles_qubits() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"11"#]], - } - } - - #[test] - fn cz_gate_with_zero_as_ctrl() { - check_sim! { - simulator: StabilizerSimulator, - program: qir!{ - cz(0, 1); - mresetz(1, 0); - within { h(1) } apply { cz(0, 1) } - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![["00"]], - } - } - - #[test] - fn cz_gate_with_one_as_ctrl() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); // flip control so that cz works - cz(0, 1); - mresetz(1, 0); // measure qubit 1 - within { h(1) } apply { cz(0, 1) } - mresetz(1, 1); // measure qubit 1 again - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"01"#]], // these two results are on the same qubit - } - } - - #[test] - fn cz_gate_applies_phase() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(1); - within { h(0) } apply { cz(0, 1) } - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"11"#]], - } - } - - #[test] - fn swap_gate_exchanges_qubit_states() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); - swap(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - expect: expect![[r#"01"#]], - } - } - - // ==================== Bell State Tests ==================== - - #[test] - fn bell_state_produces_correlated_measurements() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - h(0); - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 00: 58 - 11: 42"#]], - } - } - - // ==================== GHZ State Test ==================== - - #[test] - fn ghz_state_three_qubits() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - h(0); - cx(0, 1); - cx(1, 2); - mresetz(0, 0); - mresetz(1, 1); - mresetz(2, 2); - }, - num_qubits: 3, - num_results: 3, - shots: 100, - seed: 42, - format: histogram, - expect: expect![[r#" - 000: 56 - 111: 44"#]], - } - } - - // ==================== Reset Tests ==================== - - #[test] - fn reset_returns_qubit_to_zero() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); - reset(0); - mresetz(0, 0); - }, - num_qubits: 1, - num_results: 1, - expect: expect![[r#"0"#]], - } - } - - #[test] - fn mresetz_resets_after_measurement() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); - mresetz(0, 0); - mresetz(0, 1); - }, - num_qubits: 1, - num_results: 2, - expect: expect![[r#"10"#]], - } - } - - // ==================== Noisy Stabilizer Tests ==================== - - #[test] - fn stabilizer_with_noise() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); - cx(0, 1); - mresetz(0, 0); - mresetz(1, 1); - }, - num_qubits: 2, - num_results: 2, - shots: 1000, - seed: 42, - noise: noise_config! { - cx: { - xi: 0.05, - ix: 0.05, - }, - }, - format: top_n(4), - expect: expect![[r#" - 11: 908 - 10: 56 - 01: 36"#]], - } - } - } + sim } diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests.rs b/source/pip/src/qir_simulation/cpu_simulators/tests.rs new file mode 100644 index 0000000000..58decd6f95 --- /dev/null +++ b/source/pip/src/qir_simulation/cpu_simulators/tests.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod clifford_noiseless; +mod clifford_noisy; +mod full_state_noiseless; +mod full_state_noisy; +mod test_utils; + +/// Seed used for reproducible randomness in tests. +const SEED: u32 = 42; diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs new file mode 100644 index 0000000000..a4337b6bbc --- /dev/null +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs @@ -0,0 +1,523 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for the noiseless Clifford/stabilizer simulator. +//! +//! The stabilizer simulator efficiently simulates quantum circuits composed +//! of Clifford gates using the stabilizer formalism. +//! +//! # Supported Gates +//! +//! ```text +//! | Category | Gates | +//! |-----------------|--------------------------------------------| +//! | Single-qubit | I, X, Y, Z, H, S, S_ADJ, SX, SX_ADJ | +//! | Two-qubit | CX, CZ, SWAP | +//! | Measurement | MZ, MRESETZ, RESET | +//! | Other | MOV | +//! ``` +//! +//! # Not Supported (Panics) +//! +//! `T`, `T_ADJ`, `Rx`, `Ry`, `Rz`, `Rxx`, `Ryy`, `Rzz` (non-Clifford gates) +//! +//! # Gate Properties +//! +//! The `~` symbol denotes equivalence up to global phase. +//! +//! ```text +//! | Gate | Properties | +//! |---------|---------------------------------------------------| +//! | I | I ~ {} (identity does nothing) | +//! | X | X flips qubit, X X ~ I | +//! | Y | Y flips qubit, Y Y ~ I | +//! | Z | Z|0⟩ = |0⟩, H Z H ~ X | +//! | H | H^2 ~ I, creates superposition | +//! | S | S^2 ~ Z, S S_ADJ ~ I | +//! | S_ADJ | S_ADJ^2 ~ Z | +//! | SX | SX^2 ~ X, SX SX_ADJ ~ I | +//! | SX_ADJ | SX_ADJ^2 ~ X | +//! | CX | CX|00⟩ = |00⟩, CX|10⟩ = |11⟩ | +//! | CZ | CZ|x0⟩ = |x0⟩, CZ(a,b) = CZ(b,a) | +//! | SWAP | Exchanges qubit states, SWAP SWAP ~ I | +//! | RESET | Returns qubit to |0⟩ | +//! | MRESETZ | Measures and resets to |0⟩ | +//! | MOV | MOV ~ I (no-op in noiseless simulation) | +//! ``` +//! +//! # Multi-Qubit States +//! +//! ```text +//! | State | Preparation | Expected Outcomes | +//! |-------|----------------------------|---------------------| +//! | Bell | H(0); CX(0,1) | 00 or 11 (50/50) | +//! | GHZ | H(0); CX(0,1); CX(1,2) | 000 or 111 (50/50) | +//! ``` + +use super::{super::*, SEED, test_utils::*}; +use expect_test::expect; + +// ==================== Single-Qubit Gate Tests ==================== + +// I gate tests +#[test] +fn i_gate_does_nothing() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! {}, + qir! { i(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// X gate tests +#[test] +fn x_gate_flips_qubit() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"1"#]], + } +} + +#[test] +fn double_x_gate_eq_identity() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { i(0) }, + qir! { x(0); x(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// Y gate tests +#[test] +fn y_gate_flips_qubit() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + y(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"1"#]], + } +} + +#[test] +fn double_y_gate_eq_identity() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { i(0) }, + qir! { y(0); y(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// Z gate tests +#[test] +fn z_gate_preserves_zero() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + z(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"0"#]], + } +} + +#[test] +fn z_gate_preserves_one() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + z(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"1"#]], + } +} + +#[test] +fn h_z_h_eq_x() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { x(0) }, + qir! { within { h(0) } apply { z(0) } } + ], + num_qubits: 1, + num_results: 0, + } +} + +// H gate tests +#[test] +fn h_gate_creates_superposition() { + // H creates equal superposition - should see both 0 and 1 + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: SEED, + format: outcomes, + output: expect![[r#" + 0 + 1"#]], + } +} + +#[test] +fn h_squared_eq_identity() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { i(0) }, + qir! { h(0); h(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// S gate tests +#[test] +fn s_gate_preserves_computational_basis() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + s(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"0"#]], + } +} + +#[test] +fn s_squared_eq_z() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { z(0) }, + qir! { s(0); s(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn s_and_s_adj_cancel() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { i(0) }, + qir! { s(0); s_adj(0) }, + qir! { s_adj(0); s(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn s_adj_squared_eq_z() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { z(0) }, + qir! { s_adj(0); s_adj(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// SX gate tests +#[test] +fn sx_squared_eq_x() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { x(0) }, + qir! { sx(0); sx(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn sx_and_sx_adj_cancel() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { i(0) }, + qir! { sx(0); sx_adj(0) }, + qir! { sx_adj(0); sx(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn sx_adj_squared_eq_x() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { x(0) }, + qir! { sx_adj(0); sx_adj(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// ==================== Two-Qubit Gate Tests ==================== + +// CX gate tests +#[test] +fn cx_on_zero_control_eq_identity() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + output: expect![[r#"00"#]], + } +} + +#[test] +fn cx_on_one_control_flips_target() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + output: expect![[r#"11"#]], + } +} + +// CZ gate tests +#[test] +fn cz_on_zero_control_eq_identity() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + cz(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + output: expect![[r#"00"#]], + } +} + +#[test] +fn cz_applies_phase_when_control_is_one() { + // CZ applies Z to target when control is |1⟩ + // H·Z·H = X, so if we conjugate target by H, we see the flip + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); // Set control to |1⟩ + within { h(1) } apply { cz(0, 1) } + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + output: expect![[r#"11"#]], + } +} + +#[test] +fn cz_symmetric() { + // CZ is symmetric: CZ(a,b) = CZ(b,a) + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { cz(0, 1) }, + qir! { cz(1, 0) } + ], + num_qubits: 2, + num_results: 0, + } +} + +// SWAP gate tests +#[test] +fn swap_exchanges_qubit_states() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + output: expect![[r#"01"#]], + } +} + +#[test] +fn swap_twice_eq_identity() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { x(0) }, + qir! { x(0); swap(0, 1); swap(0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +// ==================== Reset and Measurement Tests ==================== + +#[test] +fn reset_returns_qubit_to_zero() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { i(0) }, + qir! { x(0); reset(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn mresetz_resets_after_measurement() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); // Measures 1, resets to 0 + mresetz(0, 1); // Measures 0 + }, + num_qubits: 1, + num_results: 2, + output: expect![[r#"10"#]], + } +} + +#[test] +fn mz_does_not_reset() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mz(0, 0); // Measures 1, does not reset + mz(0, 1); // Measures 1 again + }, + num_qubits: 1, + num_results: 2, + output: expect![[r#"11"#]], + } +} + +// ==================== Multi-Qubit State Tests ==================== + +#[test] +fn bell_state_produces_correlated_measurements() { + // Bell state produces only correlated outcomes: 00 or 11 + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 100, + seed: SEED, + format: outcomes, + output: expect![[r#" + 00 + 11"#]], + } +} + +#[test] +fn ghz_state_three_qubits() { + // GHZ state produces only 000 or 111 + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + cx(0, 1); + cx(1, 2); + mresetz(0, 0); + mresetz(1, 1); + mresetz(2, 2); + }, + num_qubits: 3, + num_results: 3, + shots: 100, + seed: SEED, + format: outcomes, + output: expect![[r#" + 000 + 111"#]], + } +} + +// ==================== MOV Gate Tests ==================== + +#[test] +fn mov_is_noop_without_noise() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! {}, + qir! { mov(0) } + ], + num_qubits: 1, + num_results: 0, + } +} diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs new file mode 100644 index 0000000000..3ef67345ee --- /dev/null +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for the noisy Clifford/stabilizer simulator. +//! +//! The stabilizer simulator supports noisy simulation with Pauli noise +//! and qubit loss, efficiently tracking errors in the stabilizer formalism. +//! +//! # Supported Gates +//! +//! Same as noiseless stabilizer simulator (see `clifford_noiseless`). +//! +//! # Noise Model +//! +//! Same as noisy full-state simulator (see `full_state_noisy`): +//! +//! - **Pauli noise**: X (bit-flip), Y (bit+phase flip), Z (phase-flip) +//! - **Loss noise**: Qubit loss producing '-' measurement result +//! - **Two-qubit noise**: Pauli strings like XI, IX, XX, etc. +//! +//! # Notes +//! +//! - The I gate is a no-op, so noise on I gate is not applied +//! - MRESETZ noise is applied before measurement, not after +//! +//! # Test Categories +//! +//! ```text +//! | Category | Description | +//! |-----------------------|--------------------------------------------| +//! | Noiseless config | Empty noise config produces clean results | +//! | X noise (bit-flip) | Flips measurement outcomes | +//! | Z noise (phase-flip) | No effect on computational basis | +//! | Loss noise | Produces '-' marker in measurements | +//! | Two-qubit gate noise | XI, IX, XX, etc. affect respective qubits | +//! | Combined noise | Multiple noise sources on entangled states | +//! ``` + +use super::{super::*, SEED, test_utils::*}; +use expect_test::expect; + +// ==================== Noiseless Config Tests ==================== + +#[test] +fn noiseless_config_produces_clean_results() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + noise: noise_config! {}, + format: histogram, + output: expect![[r#"1: 100"#]], + } +} + +// ==================== X Noise (Bit-Flip) Tests ==================== + +#[test] +fn x_noise_on_x_gate_causes_bit_flips() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { x: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 0: 97 + 1: 903"#]], + } +} + +// ==================== Z Noise (Phase-Flip) Tests ==================== + +#[test] +fn z_noise_does_not_affect_computational_basis() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: SEED, + noise: noise_config! { + x: { z: 0.5 }, + }, + format: histogram, + output: expect![[r#"1: 100"#]], + } +} + +// ==================== Loss Noise Tests ==================== + +#[test] +fn loss_noise_produces_loss_marker() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { loss: 0.1 }, + }, + format: summary, + output: expect![[r#" + shots: 1000 + unique: 2 + loss: 119"#]], + } +} + +// ==================== Two-Qubit Gate Noise Tests ==================== + +#[test] +fn cx_noise_affects_entangled_qubits() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + cx: { + xi: 0.05, + ix: 0.05, + }, + }, + format: top_n(4), + output: expect![[r#" + 11: 908 + 10: 56 + 01: 36"#]], + } +} + +#[test] +fn cz_noise_affects_state() { + // H creates superposition, CZ with noise introduces errors + // Should only see 00 and 10 (control always 0, target in superposition) + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + cz(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + cz: { xi: 0.1 }, + }, + format: outcomes, + output: expect![[r#" + 00 + 10"#]], + } +} + +#[test] +fn swap_noise_affects_swapped_qubits() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + swap: { xi: 0.1, ix: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 00: 103 + 01: 805 + 11: 92"#]], + } +} + +// ==================== Combined Noise Tests ==================== + +#[test] +fn bell_state_with_combined_noise() { + // Bell state with noise - should see all 4 computational basis states + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + h(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + h: { x: 0.02 }, + cx: { xi: 0.02, ix: 0.02 }, + }, + format: outcomes, + output: expect![[r#" + 00 + 01 + 10 + 11"#]], + } +} + +// ==================== MOV Gate Noise Tests ==================== + +#[test] +fn mov_with_loss_noise() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mov(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + mov: { loss: 0.1 }, + }, + format: summary, + output: expect![[r#" + shots: 1000 + unique: 2 + loss: 97"#]], + } +} diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs new file mode 100644 index 0000000000..3f24099a8e --- /dev/null +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs @@ -0,0 +1,705 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for the noiseless full-state simulator. +//! +//! The full-state simulator uses a dense state vector representation to +//! simulate quantum circuits exactly. This module verifies that gates +//! satisfy their expected algebraic identities. +//! +//! # Supported Gates +//! +//! ```text +//! | Category | Gates | +//! |-------------------|--------------------------------------------| +//! | Single-qubit | I, X, Y, Z, H, S, S_ADJ, SX, SX_ADJ, T, T_ADJ | +//! | Two-qubit | CX, CY, CZ, SWAP | +//! | Three-qubit | CCX | +//! | Rotation | Rx, Ry, Rz, Rxx, Ryy, Rzz | +//! | Measurement | M, MZ, MRESETZ, RESET | +//! | Other | MOV | +//! ``` +//! +//! # Gate Properties +//! +//! The `~` symbol denotes equivalence up to global phase. +//! +//! ```text +//! | Gate | Properties | +//! |---------|---------------------------------------------------| +//! | I | I ~ {} (identity does nothing) | +//! | X | X flips qubit, X X ~ I | +//! | Y | Y ~ X Z ~ Z X, Y Y ~ I | +//! | Z | H Z H ~ X | +//! | H | H^2 ~ I (self-inverse), H X H ~ Z | +//! | S | S^2 ~ Z | +//! | S_ADJ | S S_ADJ ~ I, S_ADJ^2 ~ Z | +//! | SX | SX^2 ~ X | +//! | SX_ADJ | SX SX_ADJ ~ I, SX_ADJ^2 ~ X | +//! | T | T^4 ~ Z | +//! | T_ADJ | T T_ADJ ~ I, T_ADJ^4 ~ Z | +//! | CX | CX on |0⟩ control ~ I, CX on |1⟩ control ~ X | +//! | CZ | CZ on |0⟩ control ~ I, CZ on |1⟩ control ~ Z | +//! | SWAP | (X ⊗ Z) SWAP ~ Z ⊗ X | +//! | Rx | Rx(0) ~ I, Rx(π) ~ X, Rx(π/2) ~ SX | +//! | Ry | Ry(0) ~ I, Ry(π) ~ Y | +//! | Rz | Rz(0) ~ I, Rz(π) ~ Z, Rz(π/2) ~ S, Rz(π/4) ~ T | +//! | Rxx | Rxx(0) ~ I, Rxx(π) ~ X ⊗ X | +//! | Ryy | Ryy(0) ~ I, Ryy(π) ~ Y ⊗ Y | +//! | Rzz | Rzz(0) ~ I, Rzz(π) ~ Z ⊗ Z | +//! | M | M ~ M M (idempotent) | +//! | RESET | OP RESET ~ I (resets to |0⟩) | +//! | MRESETZ | OP MRESETZ ~ I (measures and resets) | +//! | MOV | MOV ~ I (no-op in noiseless simulation) | +//! ``` + +use super::{super::*, test_utils::*}; +use expect_test::expect; +use std::f64::consts::PI; + +// ==================== Single-Qubit Gate Tests ==================== + +// I gate tests +#[test] +fn i_gate_does_nothing() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! {}, + qir! { i(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// H gate tests +#[test] +fn h_squared_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { h(0); h(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn h_x_h_eq_z() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0) }, + qir! { h(0); x(0); h(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// X gate tests +#[test] +fn x_gate_flips_qubit() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"1"#]], + } +} + +#[test] +fn double_x_gate_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { x(0); x(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// Z gate tests +#[test] +fn x_gate_eq_h_z_h() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0) }, + qir! { h(0); z(0); h(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// Y gate tests +#[test] +fn y_gate_eq_x_z_and_z_x() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { y(0) }, + qir! { x(0); z(0); }, + qir! { z(0); x(0); }, + ], + num_qubits: 1, + num_results: 0, + } +} + +// S gate tests +#[test] +fn s_squared_eq_z() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0) }, + qir! { s(0); s(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// S_ADJ gate tests +#[test] +fn s_and_s_adj_cancel() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { s(0); s_adj(0); }, + qir! { s_adj(0); s(0); }, + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn s_adj_squared_eq_z() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0) }, + qir! { s_adj(0); s_adj(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// SX gate tests +#[test] +fn sx_squared_eq_x() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0) }, + qir! { sx(0); sx(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// SX_ADJ gate tests +#[test] +fn sx_and_sx_adj_cancel() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { sx(0); sx_adj(0); }, + qir! { sx_adj(0); sx(0); }, + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn sx_adj_squared_eq_x() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0) }, + qir! { sx_adj(0); sx_adj(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// T gate tests +#[test] +fn t_fourth_eq_z() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0) }, + qir! { t(0); t(0); t(0); t(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// T_ADJ gate tests +#[test] +fn t_and_t_adj_cancel() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { t(0); t_adj(0); }, + qir! { t_adj(0); t(0); }, + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn t_adj_fourth_eq_z() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0) }, + qir! { t_adj(0); t_adj(0); t_adj(0); t_adj(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// M gate tests +#[test] +fn m_eq_m_m() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { mz(0, 0) }, + qir! { mz(0, 0); mz(0, 0); } + ], + num_qubits: 1, + num_results: 1, + } +} + +// RESET gate tests +#[test] +fn op_reset_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { x(0); reset(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + +// MRESETZ gate tests +#[test] +fn op_mresetz_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { x(0); mresetz(0, 0); } + ], + num_qubits: 1, + num_results: 1, + } +} + +// MOV gate tests +#[test] +fn mov_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { mov(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// ==================== Two-Qubit Gate Tests ==================== + +// CX gate tests +#[test] +fn cx_on_zero_control_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(1) }, + qir! { cx(0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +#[test] +fn cx_on_one_control_eq_x() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0); x(1) }, + qir! { x(0); cx(0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +// CZ gate tests +#[test] +fn cz_on_zero_control_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(1) }, + qir! { cz(0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +#[test] +fn cz_on_one_control_eq_z() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0); within { h(1) } apply { z(1) } }, + qir! { x(0); within { h(1) } apply { cz(0, 1) } } + ], + num_qubits: 2, + num_results: 0, + } +} + +// SWAP gate tests +#[test] +fn xz_swap_eq_zx() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0); x(1) }, + qir! { x(0); z(1); swap(0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +// ==================== Rotation Gate Tests ==================== + +// Rx gate tests +#[test] +fn rx_zero_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { rx(0.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rx_two_pi_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { rx(2.0 * PI, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rx_pi_eq_x() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0) }, + qir! { rx(PI, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rx_half_pi_eq_sx() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { sx(0) }, + qir! { rx(PI / 2.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rx_neg_half_pi_eq_sx_adj() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { sx_adj(0) }, + qir! { rx(-PI / 2.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// Ry gate tests +#[test] +fn ry_zero_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { ry(0.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn ry_two_pi_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { ry(2.0 * PI, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn ry_pi_eq_y() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { y(0) }, + qir! { ry(PI, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// Rz gate tests +#[test] +fn rz_zero_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { rz(0.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rz_two_pi_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { rz(2.0 * PI, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rz_pi_eq_z() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0) }, + qir! { rz(PI, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rz_half_pi_eq_s() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { s(0) }, + qir! { rz(PI / 2.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rz_neg_half_pi_eq_s_adj() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { s_adj(0) }, + qir! { rz(-PI / 2.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rz_quarter_pi_eq_t() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { t(0) }, + qir! { rz(PI / 4.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn rz_neg_quarter_pi_eq_t_adj() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { t_adj(0) }, + qir! { rz(-PI / 4.0, 0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +// ==================== Two-Qubit Rotation Gate Tests ==================== + +// Rxx gate tests +#[test] +fn rxx_zero_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0); i(1) }, + qir! { rxx(0.0, 0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +#[test] +fn rxx_pi_eq_x_tensor_x() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0); x(1) }, + qir! { rxx(PI, 0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +// Ryy gate tests +#[test] +fn ryy_zero_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0); i(1) }, + qir! { ryy(0.0, 0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +#[test] +fn ryy_pi_eq_y_tensor_y() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { y(0); y(1) }, + qir! { ryy(PI, 0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +// Rzz gate tests +#[test] +fn rzz_zero_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0); i(1) }, + qir! { rzz(0.0, 0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + +#[test] +fn rzz_pi_eq_z_tensor_z() { + // Z⊗Z on |00⟩ gives |00⟩ (both have eigenvalue +1) + // This is equivalent to identity on computational basis states + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { z(0); z(1) }, + qir! { rzz(PI, 0, 1) } + ], + num_qubits: 2, + num_results: 0, + } + + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { within { h(0); h(1) } apply { z(0); z(1) } }, + qir! { within { h(0); h(1) } apply { rzz(PI, 0, 1) } } + ], + num_qubits: 2, + num_results: 0, + } +} diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs new file mode 100644 index 0000000000..ba4c4aba89 --- /dev/null +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs @@ -0,0 +1,479 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Tests for the noisy full-state simulator. +//! +//! The noisy full-state simulator extends the noiseless simulator with +//! configurable Pauli noise and qubit loss. This module verifies that +//! noise is correctly applied to quantum operations. +//! +//! # Supported Gates +//! +//! Same as noiseless full-state simulator (see `full_state_noiseless`). +//! +//! # Noise Model +//! +//! Each gate can have an associated noise configuration: +//! +//! - **Pauli noise**: X (bit-flip), Y (bit+phase flip), Z (phase-flip) +//! - **Loss noise**: Qubit loss producing '-' measurement result +//! - **Two-qubit noise**: Pauli strings like XI, IX, XX, YZ, etc. +//! +//! # Notes +//! +//! - The I gate is a no-op, so noise on I gate is not applied +//! - MRESETZ noise is applied before measurement, not after +//! +//! # Test Categories +//! +//! ```text +//! | Category | Description | +//! |-----------------------|--------------------------------------------| +//! | Noiseless config | Empty noise config produces clean results | +//! | X noise (bit-flip) | Flips measurement outcomes | +//! | Z noise (phase-flip) | No effect on computational basis | +//! | Loss noise | Produces '-' marker in measurements | +//! | Two-qubit gate noise | XI, IX, XX, etc. affect respective qubits | +//! | Multiple gates | Noise accumulates across gate sequence | +//! | Gate-specific noise | Different gates can have different noise | +//! | Rotation gate noise | Noise on Rx, Ry, Rz, Rxx, Ryy, Rzz gates | +//! ``` + +use super::{super::*, SEED, test_utils::*}; +use expect_test::expect; + +// ==================== Noiseless Config Tests ==================== + +#[test] +fn noiseless_config_produces_clean_results() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + noise: noise_config! {}, + format: histogram, + output: expect![[r#"1: 100"#]], + } +} + +// ==================== X Noise (Bit-Flip) Tests ==================== + +#[test] +fn x_noise_on_x_gate_causes_bit_flips() { + // X noise on X gate: X·X = I, so some results flip back to 0 + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { x: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 0: 97 + 1: 903"#]], + } +} + +#[test] +fn x_noise_on_h_gate_affects_superposition() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + h: { x: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 0: 475 + 1: 525"#]], + } +} + +// ==================== Z Noise (Phase-Flip) Tests ==================== + +#[test] +fn z_noise_does_not_affect_computational_basis() { + // Z noise should not change measurement outcomes in computational basis + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: SEED, + noise: noise_config! { + x: { z: 0.5 }, + }, + format: histogram, + output: expect![[r#"1: 100"#]], + } +} + +#[test] +fn z_noise_on_superposition_affects_interference() { + // Z noise on H gate affects phase, changing interference pattern + // H·Z·H = X, so Z errors in superposition can flip outcomes + check_sim! { + simulator: NoisySimulator, + program: qir! { + h(0); + h(0); // H·H = I, should give |0⟩ without noise + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + h: { z: 0.2 }, + }, + format: histogram, + output: expect![[r#" + 0: 819 + 1: 181"#]], + } +} + +// ==================== Loss Noise Tests ==================== + +#[test] +fn loss_noise_produces_loss_marker() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { loss: 0.1 }, + }, + format: summary, + output: expect![[r#" + shots: 1000 + unique: 2 + loss: 119"#]], + } +} + +#[test] +fn loss_appears_in_histogram() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { loss: 0.1 }, + }, + format: histogram, + output: expect![[r#" + -: 119 + 1: 881"#]], + } +} + +// ==================== Two-Qubit Gate Noise Tests ==================== + +#[test] +fn cx_xi_noise_flips_control_qubit() { + // XI noise on CX flips the control qubit + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + cx: { xi: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 01: 92 + 11: 908"#]], + } +} + +#[test] +fn cx_ix_noise_flips_target_qubit() { + // IX noise on CX flips the target qubit + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + cx: { ix: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 10: 92 + 11: 908"#]], + } +} + +#[test] +fn cx_xx_noise_flips_both_qubits() { + // XX noise on CX flips both qubits + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + cx: { xx: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 00: 92 + 11: 908"#]], + } +} + +#[test] +fn cz_noise_affects_state() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + h(0); + cz(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + cz: { xi: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 00: 506 + 10: 494"#]], + } +} + +#[test] +fn swap_noise_affects_swapped_qubits() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + swap: { xi: 0.1, ix: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 00: 103 + 01: 805 + 11: 92"#]], + } +} + +// ==================== Gate-Specific Noise Tests ==================== + +#[test] +fn different_gates_have_different_noise() { + // X gate has noise, H gate doesn't - H produces 50/50, X noise flips some + check_sim! { + simulator: NoisySimulator, + program: qir! { + h(0); + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { x: 0.2 }, + }, + format: histogram, + output: expect![[r#" + 0: 506 + 1: 494"#]], + } +} + +// ==================== Multiple Gates / Accumulated Noise Tests ==================== + +#[test] +fn noise_accumulates_across_multiple_gates() { + // Two X gates, each with noise - errors compound + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + x(0); // X·X = I, so result should be 0 without noise + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { x: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 0: 818 + 1: 182"#]], + } +} + +#[test] +fn bell_state_with_combined_noise() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + h(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + h: { x: 0.02 }, + cx: { xi: 0.02, ix: 0.02 }, + }, + format: top_n(4), + output: expect![[r#" + 00: 491 + 11: 481 + 01: 18 + 10: 10"#]], + } +} + +// ==================== Rotation Gate Noise Tests ==================== + +#[test] +fn rx_gate_with_noise() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + rx(std::f64::consts::PI, 0); // Rx(π) ~ X + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + rx: { x: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 0: 97 + 1: 903"#]], + } +} + +#[test] +fn rz_gate_with_z_noise_no_effect_on_basis() { + // Rz followed by Z noise - no effect on computational basis + check_sim! { + simulator: NoisySimulator, + program: qir! { + rz(std::f64::consts::PI, 0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: SEED, + noise: noise_config! { + rz: { z: 0.5 }, + }, + format: histogram, + output: expect![[r#"0: 100"#]], + } +} + +// ==================== Multi-Qubit Rotation Gate Noise Tests ==================== + +#[test] +fn rxx_gate_with_noise() { + check_sim! { + simulator: NoisySimulator, + program: qir! { + rxx(std::f64::consts::PI, 0, 1); // Rxx(π) ~ X⊗X + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + rxx: { xi: 0.1 }, + }, + format: histogram, + output: expect![[r#" + 01: 89 + 11: 911"#]], + } +} diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs new file mode 100644 index 0000000000..7bffd53d4b --- /dev/null +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs @@ -0,0 +1,716 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// #![allow(dead_code)] + +use crate::qir_simulation::{QirInstruction, QirInstructionId, cpu_simulators::run_shot}; + +// ==================== Instruction Builder Functions ==================== +// These functions create QirInstruction values for use in check_sim! tests. + +// Single-qubit gates +pub fn i(q: u32) -> 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) +} +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 m(q: u32, r: u32) -> QirInstruction { + QirInstruction::TwoQubitGate(QirInstructionId::M, q, r) +} +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 +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 +/// - `num_results`: The number of measurement results +/// +/// # Example +/// ```ignore +/// check_programs_are_eq! { +/// simulator: NoiselessSimulator, +/// programs: [ +/// qir! { i(0) }, +/// qir! { x(0); x(0); } +/// ], +/// num_qubits: 1, +/// num_results: 0, +/// } +/// ``` +macro_rules! check_programs_are_eq { + ( + 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)] +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)] +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") + } +} + +/// Count format: shows the total number of shots. +/// Useful for quick sanity checks on shot count. +/// Example: "100" +pub fn count(output: &[String]) -> String { + output.len().to_string() +} + +/// 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 + ) +} + +/// Loss count format: counts how many results contain loss ('-'). +/// Useful for testing noisy simulations with qubit loss. +/// +/// Example output: +/// ```text +/// total: 100 +/// loss: 5 +/// no_loss: 95 +/// ``` +pub fn loss_count(output: &[String]) -> String { + let output = normalize_output(output); + let loss_count = output.iter().filter(|s| s.contains('-')).count(); + let no_loss_count = output.len() - loss_count; + format!( + "total: {}\nloss: {}\nno_loss: {}", + output.len(), + loss_count, + no_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() + } } From c00d2268e0ed87a456eed43c6bf98d8df4637b63 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Thu, 29 Jan 2026 13:52:23 -0800 Subject: [PATCH 5/9] add tests showing simulator completes all shots --- .../tests/clifford_noiseless.rs | 21 ++++++++++ .../cpu_simulators/tests/clifford_noisy.rs | 42 +++++++++++++------ .../tests/full_state_noiseless.rs | 21 ++++++++++ .../cpu_simulators/tests/test_utils.rs | 34 ++------------- 4 files changed, 76 insertions(+), 42 deletions(-) diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs index a4337b6bbc..666e318853 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs @@ -57,6 +57,27 @@ use super::{super::*, SEED, test_utils::*}; use expect_test::expect; +// ==================== Generic Simulator Tests ==================== + +#[test] +fn simulator_completes_all_shots() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 50, + format: summary, + output: expect![[r#" + shots: 50 + unique: 1 + loss: 0"#]], + } +} + // ==================== Single-Qubit Gate Tests ==================== // I gate tests diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs index 3ef67345ee..ce21f60a32 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs @@ -116,16 +116,34 @@ fn loss_noise_produces_loss_marker() { }, num_qubits: 1, num_results: 1, - shots: 1000, + shots: 100, seed: SEED, noise: noise_config! { x: { loss: 0.1 }, }, - format: summary, + format: histogram, output: expect![[r#" - shots: 1000 - unique: 2 - loss: 119"#]], + -: 5 + 1: 95"#]], + } +} + +#[test] +fn max_loss_probability_always_results_in_loss() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + noise: noise_config! { + x: { loss: 1.0 }, + }, + format: histogram, + output: expect!["-: 100"], } } @@ -151,22 +169,22 @@ fn cx_noise_affects_entangled_qubits() { ix: 0.05, }, }, - format: top_n(4), + format: histogram, output: expect![[r#" - 11: 908 - 10: 56 - 01: 36"#]], + 01: 36 + 10: 56 + 11: 908"#]], } } #[test] fn cz_noise_affects_state() { - // H creates superposition, CZ with noise introduces errors - // Should only see 00 and 10 (control always 0, target in superposition) + // CZ with noise introduces errors + // Should only see 00 in a noiseless simulation, + // but because of noisy we should also see 10 now. check_sim! { simulator: StabilizerSimulator, program: qir! { - h(0); cz(0, 1); mresetz(0, 0); mresetz(1, 1); diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs index 3f24099a8e..f53d02392d 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs @@ -57,6 +57,27 @@ use super::{super::*, test_utils::*}; use expect_test::expect; use std::f64::consts::PI; +// ==================== Generic Simulator Tests ==================== + +#[test] +fn simulator_completes_all_shots() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 50, + format: summary, + output: expect![[r#" + shots: 50 + unique: 1 + loss: 0"#]], + } +} + // ==================== Single-Qubit Gate Tests ==================== // I gate tests diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs index 7bffd53d4b..fb96627fb9 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs @@ -53,6 +53,7 @@ pub fn reset(q: u32) -> QirInstruction { 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) } @@ -73,6 +74,7 @@ pub fn mresetz(q: u32, r: u32) -> QirInstruction { } // 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) } @@ -599,7 +601,7 @@ pub fn histogram(output: &[String]) -> String { /// 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)] +#[allow(clippy::cast_precision_loss, dead_code)] pub fn histogram_percent(output: &[String]) -> String { use std::collections::BTreeMap; let output = normalize_output(output); @@ -640,7 +642,7 @@ pub fn top_n(n: usize) -> impl Fn(&[String]) -> String { /// 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)] +#[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; @@ -661,13 +663,6 @@ pub fn top_n_percent(n: usize) -> impl Fn(&[String]) -> String { } } -/// Count format: shows the total number of shots. -/// Useful for quick sanity checks on shot count. -/// Example: "100" -pub fn count(output: &[String]) -> String { - output.len().to_string() -} - /// 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" @@ -684,27 +679,6 @@ pub fn summary(output: &[String]) -> String { ) } -/// Loss count format: counts how many results contain loss ('-'). -/// Useful for testing noisy simulations with qubit loss. -/// -/// Example output: -/// ```text -/// total: 100 -/// loss: 5 -/// no_loss: 95 -/// ``` -pub fn loss_count(output: &[String]) -> String { - let output = normalize_output(output); - let loss_count = output.iter().filter(|s| s.contains('-')).count(); - let no_loss_count = output.len() - loss_count; - format!( - "total: {}\nloss: {}\nno_loss: {}", - output.len(), - loss_count, - no_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 From 83d1904311dc5358b93459345933b963219d52b4 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Thu, 29 Jan 2026 13:52:36 -0800 Subject: [PATCH 6/9] add noise intrinsics tests --- .../cpu_simulators/tests/clifford_noisy.rs | 187 ++++++++++++++++ .../cpu_simulators/tests/full_state_noisy.rs | 208 ++++++++++++++++++ 2 files changed, 395 insertions(+) diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs index ce21f60a32..a052d43e3f 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs @@ -283,3 +283,190 @@ fn mov_with_loss_noise() { loss: 97"#]], } } + +// ==================== Correlated Noise Intrinsic Tests ==================== + +#[test] +fn noise_intrinsic_single_qubit_x_noise() { + // Single-qubit X noise via intrinsic + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + noise_intrinsic(0, &[0]); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { x: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 0: 886 + 1: 114"#]], + } +} + +#[test] +fn noise_intrinsic_single_qubit_z_noise_no_effect() { + // Z noise on |0⟩ has no observable effect + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + noise_intrinsic(0, &[0]); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { z: 0.5 }, + }, + }, + format: histogram, + output: expect![[r#"0: 100"#]], + } +} + +#[test] +fn noise_intrinsic_two_qubit_correlated_xx_noise() { + // Two-qubit XX noise causes correlated bit flips + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + noise_intrinsic(0, &[0, 1]); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { xx: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 00: 886 + 11: 114"#]], + } +} + +#[test] +fn noise_intrinsic_two_qubit_independent_noise() { + // XI and IX noise cause independent flips on each qubit + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + noise_intrinsic(0, &[0, 1]); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { xi: 0.1, ix: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 00: 783 + 01: 103 + 10: 114"#]], + } +} + +#[test] +fn noise_intrinsic_multiple_ids() { + // Multiple intrinsic IDs with different noise configurations + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + noise_intrinsic(0, &[0]); + noise_intrinsic(1, &[1]); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { x: 0.2 }, + 1: { x: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 00: 702 + 01: 81 + 10: 191 + 11: 26"#]], + } +} + +#[test] +fn noise_intrinsic_three_qubit_correlated() { + // Three-qubit correlated noise (XXX flips all three) + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + noise_intrinsic(0, &[0, 1, 2]); + mresetz(0, 0); + mresetz(1, 1); + mresetz(2, 2); + }, + num_qubits: 3, + num_results: 3, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { xxx: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 000: 886 + 111: 114"#]], + } +} + +#[test] +fn noise_intrinsic_combined_with_gate_noise() { + // Intrinsic noise combined with regular gate noise + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + noise_intrinsic(0, &[0]); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { x: 0.1 }, + intrinsics: { + 0: { x: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 0: 178 + 1: 822"#]], + } +} diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs index ba4c4aba89..a1f297ed7c 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs @@ -42,6 +42,27 @@ use super::{super::*, SEED, test_utils::*}; use expect_test::expect; +// ==================== Generic Simulator Tests ==================== + +#[test] +fn simulator_completes_all_shots() { + check_sim! { + simulator: StabilizerSimulator, + program: qir! { + x(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 50, + format: summary, + output: expect![[r#" + shots: 50 + unique: 1 + loss: 0"#]], + } +} + // ==================== Noiseless Config Tests ==================== #[test] @@ -477,3 +498,190 @@ fn rxx_gate_with_noise() { 11: 911"#]], } } + +// ==================== Correlated Noise Intrinsic Tests ==================== + +#[test] +fn noise_intrinsic_single_qubit_x_noise() { + // Single-qubit X noise via intrinsic + check_sim! { + simulator: NoisySimulator, + program: qir! { + noise_intrinsic(0, &[0]); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { x: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 0: 886 + 1: 114"#]], + } +} + +#[test] +fn noise_intrinsic_single_qubit_z_noise_no_effect() { + // Z noise on |0⟩ has no observable effect + check_sim! { + simulator: NoisySimulator, + program: qir! { + noise_intrinsic(0, &[0]); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { z: 0.5 }, + }, + }, + format: histogram, + output: expect![[r#"0: 100"#]], + } +} + +#[test] +fn noise_intrinsic_two_qubit_correlated_xx_noise() { + // Two-qubit XX noise causes correlated bit flips + check_sim! { + simulator: NoisySimulator, + program: qir! { + noise_intrinsic(0, &[0, 1]); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { xx: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 00: 886 + 11: 114"#]], + } +} + +#[test] +fn noise_intrinsic_two_qubit_independent_noise() { + // XI and IX noise cause independent flips on each qubit + check_sim! { + simulator: NoisySimulator, + program: qir! { + noise_intrinsic(0, &[0, 1]); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { xi: 0.1, ix: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 00: 783 + 01: 103 + 10: 114"#]], + } +} + +#[test] +fn noise_intrinsic_multiple_ids() { + // Multiple intrinsic IDs with different noise configurations + check_sim! { + simulator: NoisySimulator, + program: qir! { + noise_intrinsic(0, &[0]); + noise_intrinsic(1, &[1]); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { x: 0.2 }, + 1: { x: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 00: 702 + 01: 81 + 10: 191 + 11: 26"#]], + } +} + +#[test] +fn noise_intrinsic_three_qubit_correlated() { + // Three-qubit correlated noise (XXX flips all three) + check_sim! { + simulator: NoisySimulator, + program: qir! { + noise_intrinsic(0, &[0, 1, 2]); + mresetz(0, 0); + mresetz(1, 1); + mresetz(2, 2); + }, + num_qubits: 3, + num_results: 3, + shots: 1000, + seed: SEED, + noise: noise_config! { + intrinsics: { + 0: { xxx: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 000: 886 + 111: 114"#]], + } +} + +#[test] +fn noise_intrinsic_combined_with_gate_noise() { + // Intrinsic noise combined with regular gate noise + check_sim! { + simulator: NoisySimulator, + program: qir! { + x(0); + noise_intrinsic(0, &[0]); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 1000, + seed: SEED, + noise: noise_config! { + x: { x: 0.1 }, + intrinsics: { + 0: { x: 0.1 }, + }, + }, + format: histogram, + output: expect![[r#" + 0: 178 + 1: 822"#]], + } +} From 0bb816030e4cdadaf8f6e10ba98bf31dff0f555f Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Thu, 29 Jan 2026 14:01:21 -0800 Subject: [PATCH 7/9] add mz tests --- .../cpu_simulators/tests/clifford_noiseless.rs | 14 ++++++++++++++ .../cpu_simulators/tests/full_state_noiseless.rs | 11 ++++++----- .../cpu_simulators/tests/test_utils.rs | 3 --- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs index 666e318853..65057b9322 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs @@ -480,6 +480,20 @@ fn mz_does_not_reset() { } } +#[test] +fn mz_is_idempotent() { + // M M ~ M (repeated measurement gives same result) + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { x(0); mz(0, 0) }, + qir! { x(0); mz(0, 0); mz(0, 1) } + ], + num_qubits: 1, + num_results: 2, + } +} + // ==================== Multi-Qubit State Tests ==================== #[test] diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs index f53d02392d..8caddcf3e3 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs @@ -304,17 +304,18 @@ fn t_adj_fourth_eq_z() { } } -// M gate tests +// MZ gate tests #[test] -fn m_eq_m_m() { +fn mz_is_idempotent() { + // M M ~ M (repeated measurement gives same result) check_programs_are_eq! { simulator: NoiselessSimulator, programs: [ - qir! { mz(0, 0) }, - qir! { mz(0, 0); mz(0, 0); } + qir! { x(0); mz(0, 0) }, + qir! { x(0); mz(0, 0); mz(0, 1) } ], num_qubits: 1, - num_results: 1, + num_results: 2, } } diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs index fb96627fb9..f9aa5e1267 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs @@ -63,9 +63,6 @@ pub fn cz(q1: u32, q2: u32) -> QirInstruction { pub fn swap(q1: u32, q2: u32) -> QirInstruction { QirInstruction::TwoQubitGate(QirInstructionId::SWAP, q1, q2) } -pub fn m(q: u32, r: u32) -> QirInstruction { - QirInstruction::TwoQubitGate(QirInstructionId::M, q, r) -} pub fn mz(q: u32, r: u32) -> QirInstruction { QirInstruction::TwoQubitGate(QirInstructionId::MZ, q, r) } From efe95156ed9306a166263bbf373786640f9468cb Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Thu, 29 Jan 2026 15:08:12 -0800 Subject: [PATCH 8/9] add some missing tests --- .../tests/clifford_noiseless.rs | 59 +++- .../tests/full_state_noiseless.rs | 270 +++++++++++++++++- 2 files changed, 310 insertions(+), 19 deletions(-) diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs index 65057b9322..156a96410c 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs @@ -29,19 +29,20 @@ //! | Gate | Properties | //! |---------|---------------------------------------------------| //! | I | I ~ {} (identity does nothing) | -//! | X | X flips qubit, X X ~ I | -//! | Y | Y flips qubit, Y Y ~ I | +//! | X | X flips qubit, X X ~ I, X ~ H Z H | +//! | Y | Y flips qubit, Y Y ~ I, Y ~ X Z ~ Z X | //! | Z | Z|0⟩ = |0⟩, H Z H ~ X | -//! | H | H^2 ~ I, creates superposition | +//! | H | H^2 ~ I, H X H ~ Z, creates superposition | //! | S | S^2 ~ Z, S S_ADJ ~ I | //! | S_ADJ | S_ADJ^2 ~ Z | //! | SX | SX^2 ~ X, SX SX_ADJ ~ I | //! | SX_ADJ | SX_ADJ^2 ~ X | //! | CX | CX|00⟩ = |00⟩, CX|10⟩ = |11⟩ | //! | CZ | CZ|x0⟩ = |x0⟩, CZ(a,b) = CZ(b,a) | -//! | SWAP | Exchanges qubit states, SWAP SWAP ~ I | -//! | RESET | Returns qubit to |0⟩ | -//! | MRESETZ | Measures and resets to |0⟩ | +//! | SWAP | Exchanges states, SWAP^2 ~ I | +//! | MZ | MZ ~ MZ MZ (idempotent, does not reset) | +//! | RESET | OP RESET ~ I (resets to |0⟩) | +//! | MRESETZ | OP MRESETZ ~ I (measures and resets) | //! | MOV | MOV ~ I (no-op in noiseless simulation) | //! ``` //! @@ -150,6 +151,20 @@ fn double_y_gate_eq_identity() { } } +#[test] +fn y_gate_eq_x_z_and_z_x() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { y(0) }, + qir! { x(0); z(0) }, + qir! { z(0); x(0) }, + ], + num_qubits: 1, + num_results: 0, + } +} + // Z gate tests #[test] fn z_gate_preserves_zero() { @@ -227,6 +242,32 @@ fn h_squared_eq_identity() { } } +#[test] +fn h_x_h_eq_z() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { z(0) }, + qir! { h(0); x(0); h(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + +#[test] +fn x_gate_eq_h_z_h() { + check_programs_are_eq! { + simulator: StabilizerSimulator, + programs: [ + qir! { x(0) }, + qir! { h(0); z(0); h(0) } + ], + num_qubits: 1, + num_results: 0, + } +} + // S gate tests #[test] fn s_gate_preserves_computational_basis() { @@ -395,10 +436,10 @@ fn cz_applies_phase_when_control_is_one() { fn cz_symmetric() { // CZ is symmetric: CZ(a,b) = CZ(b,a) check_programs_are_eq! { - simulator: StabilizerSimulator, + simulator: NoiselessSimulator, programs: [ - qir! { cz(0, 1) }, - qir! { cz(1, 0) } + qir! { within { x(0); h(1) } apply { cz(0, 1) } }, + qir! { within { x(0); h(1) } apply { cz(1, 0) } } ], num_qubits: 2, num_results: 0, diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs index 8caddcf3e3..d82646e0b3 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs @@ -29,31 +29,41 @@ //! |---------|---------------------------------------------------| //! | I | I ~ {} (identity does nothing) | //! | X | X flips qubit, X X ~ I | -//! | Y | Y ~ X Z ~ Z X, Y Y ~ I | -//! | Z | H Z H ~ X | -//! | H | H^2 ~ I (self-inverse), H X H ~ Z | -//! | S | S^2 ~ Z | +//! | Y | Y flips qubit, Y ~ X Z ~ Z X, Y Y ~ I | +//! | Z | Z|0⟩ = |0⟩, Z|1⟩ = |1⟩, H Z H ~ X | +//! | H | H^2 ~ I, H X H ~ Z, creates superposition | +//! | S | S^2 ~ Z, S preserves computational basis | //! | S_ADJ | S S_ADJ ~ I, S_ADJ^2 ~ Z | //! | SX | SX^2 ~ X | //! | SX_ADJ | SX SX_ADJ ~ I, SX_ADJ^2 ~ X | //! | T | T^4 ~ Z | //! | T_ADJ | T T_ADJ ~ I, T_ADJ^4 ~ Z | //! | CX | CX on |0⟩ control ~ I, CX on |1⟩ control ~ X | -//! | CZ | CZ on |0⟩ control ~ I, CZ on |1⟩ control ~ Z | -//! | SWAP | (X ⊗ Z) SWAP ~ Z ⊗ X | +//! | CZ | CZ on |0⟩ control ~ I, CZ(a,b) = CZ(b,a) | +//! | SWAP | Exchanges states, SWAP SWAP ~ I | //! | Rx | Rx(0) ~ I, Rx(π) ~ X, Rx(π/2) ~ SX | //! | Ry | Ry(0) ~ I, Ry(π) ~ Y | //! | Rz | Rz(0) ~ I, Rz(π) ~ Z, Rz(π/2) ~ S, Rz(π/4) ~ T | //! | Rxx | Rxx(0) ~ I, Rxx(π) ~ X ⊗ X | //! | Ryy | Ryy(0) ~ I, Ryy(π) ~ Y ⊗ Y | //! | Rzz | Rzz(0) ~ I, Rzz(π) ~ Z ⊗ Z | -//! | M | M ~ M M (idempotent) | +//! | M | M ~ M M (idempotent, does not reset) | +//! | MZ | MZ ~ MZ MZ (idempotent, does not reset) | //! | RESET | OP RESET ~ I (resets to |0⟩) | //! | MRESETZ | OP MRESETZ ~ I (measures and resets) | //! | MOV | MOV ~ I (no-op in noiseless simulation) | //! ``` +//! +//! # Multi-Qubit States +//! +//! ```text +//! | State | Preparation | Expected Outcomes | +//! |-------|----------------------------|---------------------| +//! | Bell | H(0); CX(0,1) | 00 or 11 (50/50) | +//! | GHZ | H(0); CX(0,1); CX(1,2) | 000 or 111 (50/50) | +//! ``` -use super::{super::*, test_utils::*}; +use super::{super::*, SEED, test_utils::*}; use expect_test::expect; use std::f64::consts::PI; @@ -121,6 +131,26 @@ fn h_x_h_eq_z() { } } +#[test] +fn h_gate_creates_superposition() { + // H creates equal superposition - should see both 0 and 1 + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + shots: 100, + seed: SEED, + format: histogram, + output: expect![[r#" + 0: 46 + 1: 54"#]], + } +} + // X gate tests #[test] fn x_gate_flips_qubit() { @@ -150,6 +180,48 @@ fn double_x_gate_eq_identity() { } // Z gate tests +#[test] +fn z_gate_preserves_zero() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + z(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"0"#]], + } +} + +#[test] +fn z_gate_preserves_one() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + z(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"1"#]], + } +} + +#[test] +fn h_z_h_eq_x() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0) }, + qir! { within { h(0) } apply { z(0) } } + ], + num_qubits: 1, + num_results: 0, + } +} + #[test] fn x_gate_eq_h_z_h() { check_programs_are_eq! { @@ -164,6 +236,33 @@ fn x_gate_eq_h_z_h() { } // Y gate tests +#[test] +fn y_gate_flips_qubit() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + y(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"1"#]], + } +} + +#[test] +fn double_y_gate_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { i(0) }, + qir! { y(0); y(0); } + ], + num_qubits: 1, + num_results: 0, + } +} + #[test] fn y_gate_eq_x_z_and_z_x() { check_programs_are_eq! { @@ -192,6 +291,20 @@ fn s_squared_eq_z() { } } +#[test] +fn s_gate_preserves_computational_basis() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + s(0); + mresetz(0, 0); + }, + num_qubits: 1, + num_results: 1, + output: expect![[r#"0"#]], + } +} + // S_ADJ gate tests #[test] fn s_and_s_adj_cancel() { @@ -319,9 +432,39 @@ fn mz_is_idempotent() { } } +#[test] +fn mz_does_not_reset() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + mz(0, 0); // Measures 1, does not reset + mz(0, 1); // Measures 1 again + }, + num_qubits: 1, + num_results: 2, + output: expect![[r#"11"#]], + } +} + +#[test] +fn mresetz_resets_after_measurement() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + mresetz(0, 0); // Measures 1, resets to 0 + mresetz(0, 1); // Measures 0 + }, + num_qubits: 1, + num_results: 2, + output: expect![[r#"10"#]], + } +} + // RESET gate tests #[test] -fn op_reset_eq_identity() { +fn reset_returns_qubit_to_zero() { check_programs_are_eq! { simulator: NoiselessSimulator, programs: [ @@ -335,7 +478,7 @@ fn op_reset_eq_identity() { // MRESETZ gate tests #[test] -fn op_mresetz_eq_identity() { +fn mresetz_returns_qubit_to_zero() { check_programs_are_eq! { simulator: NoiselessSimulator, programs: [ @@ -417,6 +560,38 @@ fn cz_on_one_control_eq_z() { } } +#[test] +fn cz_applies_phase_when_control_is_one() { + // CZ applies Z to target when control is |1⟩ + // H·Z·H = X, so if we conjugate target by H, we see the flip + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); // Set control to |1⟩ + within { h(1) } apply { cz(0, 1) } + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + output: expect![[r#"11"#]], + } +} + +#[test] +fn cz_symmetric() { + // CZ is symmetric: CZ(a,b) = CZ(b,a) + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { within { x(0); h(1) } apply { cz(0, 1) } }, + qir! { within { x(0); h(1) } apply { cz(1, 0) } } + ], + num_qubits: 2, + num_results: 0, + } +} + // SWAP gate tests #[test] fn xz_swap_eq_zx() { @@ -431,6 +606,35 @@ fn xz_swap_eq_zx() { } } +#[test] +fn swap_exchanges_qubit_states() { + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + x(0); + swap(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + output: expect![[r#"01"#]], + } +} + +#[test] +fn swap_twice_eq_identity() { + check_programs_are_eq! { + simulator: NoiselessSimulator, + programs: [ + qir! { x(0) }, + qir! { x(0); swap(0, 1); swap(0, 1) } + ], + num_qubits: 2, + num_results: 0, + } +} + // ==================== Rotation Gate Tests ==================== // Rx gate tests @@ -725,3 +929,49 @@ fn rzz_pi_eq_z_tensor_z() { num_results: 0, } } + +// ==================== Multi-Qubit State Tests ==================== + +#[test] +fn bell_state_produces_correlated_measurements() { + // Bell state produces only correlated outcomes: 00 or 11 + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + cx(0, 1); + mresetz(0, 0); + mresetz(1, 1); + }, + num_qubits: 2, + num_results: 2, + shots: 100, + format: outcomes, + output: expect![[r#" + 00 + 11"#]], + } +} + +#[test] +fn ghz_state_three_qubits() { + // GHZ state produces only 000 or 111 + check_sim! { + simulator: NoiselessSimulator, + program: qir! { + h(0); + cx(0, 1); + cx(1, 2); + mresetz(0, 0); + mresetz(1, 1); + mresetz(2, 2); + }, + num_qubits: 3, + num_results: 3, + shots: 100, + format: outcomes, + output: expect![[r#" + 000 + 111"#]], + } +} From dc7e2ac4935203bdb7128f5ad4bec1a8673a97c4 Mon Sep 17 00:00:00 2001 From: Oscar Puente Date: Thu, 29 Jan 2026 15:26:13 -0800 Subject: [PATCH 9/9] make `num_results` field optional --- .../tests/clifford_noiseless.rs | 18 ------- .../tests/full_state_noiseless.rs | 48 ------------------- .../cpu_simulators/tests/test_utils.rs | 20 +++++++- 3 files changed, 18 insertions(+), 68 deletions(-) diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs index 156a96410c..4547993199 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs @@ -91,7 +91,6 @@ fn i_gate_does_nothing() { qir! { i(0) } ], num_qubits: 1, - num_results: 0, } } @@ -119,7 +118,6 @@ fn double_x_gate_eq_identity() { qir! { x(0); x(0) } ], num_qubits: 1, - num_results: 0, } } @@ -147,7 +145,6 @@ fn double_y_gate_eq_identity() { qir! { y(0); y(0) } ], num_qubits: 1, - num_results: 0, } } @@ -161,7 +158,6 @@ fn y_gate_eq_x_z_and_z_x() { qir! { z(0); x(0) }, ], num_qubits: 1, - num_results: 0, } } @@ -204,7 +200,6 @@ fn h_z_h_eq_x() { qir! { within { h(0) } apply { z(0) } } ], num_qubits: 1, - num_results: 0, } } @@ -238,7 +233,6 @@ fn h_squared_eq_identity() { qir! { h(0); h(0) } ], num_qubits: 1, - num_results: 0, } } @@ -251,7 +245,6 @@ fn h_x_h_eq_z() { qir! { h(0); x(0); h(0) } ], num_qubits: 1, - num_results: 0, } } @@ -264,7 +257,6 @@ fn x_gate_eq_h_z_h() { qir! { h(0); z(0); h(0) } ], num_qubits: 1, - num_results: 0, } } @@ -292,7 +284,6 @@ fn s_squared_eq_z() { qir! { s(0); s(0) } ], num_qubits: 1, - num_results: 0, } } @@ -306,7 +297,6 @@ fn s_and_s_adj_cancel() { qir! { s_adj(0); s(0) } ], num_qubits: 1, - num_results: 0, } } @@ -319,7 +309,6 @@ fn s_adj_squared_eq_z() { qir! { s_adj(0); s_adj(0) } ], num_qubits: 1, - num_results: 0, } } @@ -333,7 +322,6 @@ fn sx_squared_eq_x() { qir! { sx(0); sx(0) } ], num_qubits: 1, - num_results: 0, } } @@ -347,7 +335,6 @@ fn sx_and_sx_adj_cancel() { qir! { sx_adj(0); sx(0) } ], num_qubits: 1, - num_results: 0, } } @@ -360,7 +347,6 @@ fn sx_adj_squared_eq_x() { qir! { sx_adj(0); sx_adj(0) } ], num_qubits: 1, - num_results: 0, } } @@ -442,7 +428,6 @@ fn cz_symmetric() { qir! { within { x(0); h(1) } apply { cz(1, 0) } } ], num_qubits: 2, - num_results: 0, } } @@ -472,7 +457,6 @@ fn swap_twice_eq_identity() { qir! { x(0); swap(0, 1); swap(0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -487,7 +471,6 @@ fn reset_returns_qubit_to_zero() { qir! { x(0); reset(0) } ], num_qubits: 1, - num_results: 0, } } @@ -594,6 +577,5 @@ fn mov_is_noop_without_noise() { qir! { mov(0) } ], num_qubits: 1, - num_results: 0, } } diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs index d82646e0b3..d42fe20998 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs @@ -100,7 +100,6 @@ fn i_gate_does_nothing() { qir! { i(0) } ], num_qubits: 1, - num_results: 0, } } @@ -114,7 +113,6 @@ fn h_squared_eq_identity() { qir! { h(0); h(0); } ], num_qubits: 1, - num_results: 0, } } @@ -127,7 +125,6 @@ fn h_x_h_eq_z() { qir! { h(0); x(0); h(0); } ], num_qubits: 1, - num_results: 0, } } @@ -175,7 +172,6 @@ fn double_x_gate_eq_identity() { qir! { x(0); x(0); } ], num_qubits: 1, - num_results: 0, } } @@ -218,7 +214,6 @@ fn h_z_h_eq_x() { qir! { within { h(0) } apply { z(0) } } ], num_qubits: 1, - num_results: 0, } } @@ -231,7 +226,6 @@ fn x_gate_eq_h_z_h() { qir! { h(0); z(0); h(0); } ], num_qubits: 1, - num_results: 0, } } @@ -259,7 +253,6 @@ fn double_y_gate_eq_identity() { qir! { y(0); y(0); } ], num_qubits: 1, - num_results: 0, } } @@ -273,7 +266,6 @@ fn y_gate_eq_x_z_and_z_x() { qir! { z(0); x(0); }, ], num_qubits: 1, - num_results: 0, } } @@ -287,7 +279,6 @@ fn s_squared_eq_z() { qir! { s(0); s(0); } ], num_qubits: 1, - num_results: 0, } } @@ -316,7 +307,6 @@ fn s_and_s_adj_cancel() { qir! { s_adj(0); s(0); }, ], num_qubits: 1, - num_results: 0, } } @@ -329,7 +319,6 @@ fn s_adj_squared_eq_z() { qir! { s_adj(0); s_adj(0); } ], num_qubits: 1, - num_results: 0, } } @@ -343,7 +332,6 @@ fn sx_squared_eq_x() { qir! { sx(0); sx(0); } ], num_qubits: 1, - num_results: 0, } } @@ -358,7 +346,6 @@ fn sx_and_sx_adj_cancel() { qir! { sx_adj(0); sx(0); }, ], num_qubits: 1, - num_results: 0, } } @@ -371,7 +358,6 @@ fn sx_adj_squared_eq_x() { qir! { sx_adj(0); sx_adj(0); } ], num_qubits: 1, - num_results: 0, } } @@ -385,7 +371,6 @@ fn t_fourth_eq_z() { qir! { t(0); t(0); t(0); t(0); } ], num_qubits: 1, - num_results: 0, } } @@ -400,7 +385,6 @@ fn t_and_t_adj_cancel() { qir! { t_adj(0); t(0); }, ], num_qubits: 1, - num_results: 0, } } @@ -413,7 +397,6 @@ fn t_adj_fourth_eq_z() { qir! { t_adj(0); t_adj(0); t_adj(0); t_adj(0); } ], num_qubits: 1, - num_results: 0, } } @@ -472,7 +455,6 @@ fn reset_returns_qubit_to_zero() { qir! { x(0); reset(0); } ], num_qubits: 1, - num_results: 0, } } @@ -500,7 +482,6 @@ fn mov_eq_identity() { qir! { mov(0) } ], num_qubits: 1, - num_results: 0, } } @@ -516,7 +497,6 @@ fn cx_on_zero_control_eq_identity() { qir! { cx(0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -529,7 +509,6 @@ fn cx_on_one_control_eq_x() { qir! { x(0); cx(0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -543,7 +522,6 @@ fn cz_on_zero_control_eq_identity() { qir! { cz(0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -556,7 +534,6 @@ fn cz_on_one_control_eq_z() { qir! { x(0); within { h(1) } apply { cz(0, 1) } } ], num_qubits: 2, - num_results: 0, } } @@ -588,7 +565,6 @@ fn cz_symmetric() { qir! { within { x(0); h(1) } apply { cz(1, 0) } } ], num_qubits: 2, - num_results: 0, } } @@ -602,7 +578,6 @@ fn xz_swap_eq_zx() { qir! { x(0); z(1); swap(0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -631,7 +606,6 @@ fn swap_twice_eq_identity() { qir! { x(0); swap(0, 1); swap(0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -647,7 +621,6 @@ fn rx_zero_eq_identity() { qir! { rx(0.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -660,7 +633,6 @@ fn rx_two_pi_eq_identity() { qir! { rx(2.0 * PI, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -673,7 +645,6 @@ fn rx_pi_eq_x() { qir! { rx(PI, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -686,7 +657,6 @@ fn rx_half_pi_eq_sx() { qir! { rx(PI / 2.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -699,7 +669,6 @@ fn rx_neg_half_pi_eq_sx_adj() { qir! { rx(-PI / 2.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -713,7 +682,6 @@ fn ry_zero_eq_identity() { qir! { ry(0.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -726,7 +694,6 @@ fn ry_two_pi_eq_identity() { qir! { ry(2.0 * PI, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -739,7 +706,6 @@ fn ry_pi_eq_y() { qir! { ry(PI, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -753,7 +719,6 @@ fn rz_zero_eq_identity() { qir! { rz(0.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -766,7 +731,6 @@ fn rz_two_pi_eq_identity() { qir! { rz(2.0 * PI, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -779,7 +743,6 @@ fn rz_pi_eq_z() { qir! { rz(PI, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -792,7 +755,6 @@ fn rz_half_pi_eq_s() { qir! { rz(PI / 2.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -805,7 +767,6 @@ fn rz_neg_half_pi_eq_s_adj() { qir! { rz(-PI / 2.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -818,7 +779,6 @@ fn rz_quarter_pi_eq_t() { qir! { rz(PI / 4.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -831,7 +791,6 @@ fn rz_neg_quarter_pi_eq_t_adj() { qir! { rz(-PI / 4.0, 0) } ], num_qubits: 1, - num_results: 0, } } @@ -847,7 +806,6 @@ fn rxx_zero_eq_identity() { qir! { rxx(0.0, 0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -860,7 +818,6 @@ fn rxx_pi_eq_x_tensor_x() { qir! { rxx(PI, 0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -874,7 +831,6 @@ fn ryy_zero_eq_identity() { qir! { ryy(0.0, 0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -887,7 +843,6 @@ fn ryy_pi_eq_y_tensor_y() { qir! { ryy(PI, 0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -901,7 +856,6 @@ fn rzz_zero_eq_identity() { qir! { rzz(0.0, 0, 1) } ], num_qubits: 2, - num_results: 0, } } @@ -916,7 +870,6 @@ fn rzz_pi_eq_z_tensor_z() { qir! { rzz(PI, 0, 1) } ], num_qubits: 2, - num_results: 0, } check_programs_are_eq! { @@ -926,7 +879,6 @@ fn rzz_pi_eq_z_tensor_z() { qir! { within { h(0); h(1) } apply { rzz(PI, 0, 1) } } ], num_qubits: 2, - num_results: 0, } } diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs index f9aa5e1267..b79a11c843 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs +++ b/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs @@ -459,7 +459,9 @@ pub(crate) use check_sim; /// - `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 -/// - `num_results`: The number of measurement results +/// +/// # Optional fields: +/// - `num_results`: The number of measurement results (defaults to 0) /// /// # Example /// ```ignore @@ -470,10 +472,24 @@ pub(crate) use check_sim; /// qir! { x(0); x(0); } /// ], /// num_qubits: 1, -/// num_results: 0, /// } /// ``` 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 ),+ $(,)? ],