diff --git a/source/pip/src/qir_simulation.rs b/source/pip/src/qir_simulation.rs index 6e41c2da97..999f791430 100644 --- a/source/pip/src/qir_simulation.rs +++ b/source/pip/src/qir_simulation.rs @@ -4,6 +4,8 @@ mod correlated_noise; pub(crate) mod cpu_simulators; pub(crate) mod gpu_full_state; +#[cfg(test)] +mod tests; use crate::qir_simulation::correlated_noise::parse_noise_table; diff --git a/source/pip/src/qir_simulation/cpu_simulators.rs b/source/pip/src/qir_simulation/cpu_simulators.rs index 45d594ae34..7ec7e06cc4 100644 --- a/source/pip/src/qir_simulation/cpu_simulators.rs +++ b/source/pip/src/qir_simulation/cpu_simulators.rs @@ -1,9 +1,6 @@ // 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}; @@ -145,7 +142,7 @@ where .into_py_any(py) } -fn run( +pub(crate) fn run( instructions: &[QirInstruction], num_qubits: u32, num_results: u32, @@ -210,7 +207,7 @@ where values } -fn run_shot(instructions: &[QirInstruction], sim: &mut S) { +pub(crate) fn run_shot(instructions: &[QirInstruction], sim: &mut S) { for qir_inst in instructions { match qir_inst { QirInstruction::OneQubitGate(id, qubit) => match id { 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 deleted file mode 100644 index 1890935b33..0000000000 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noiseless.rs +++ /dev/null @@ -1,585 +0,0 @@ -// 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. -//! -//! # Equivalence -//! -//! The `~` symbol means: for every computational basis state |b⟩, the two -//! programs produce the same output state up to a global phase (which may -//! differ per basis state). This is verified by `check_programs_are_eq!`. -//! -//! Gate truth tables on all basis states are tested separately using -//! `check_basis_table!`. -//! -//! # 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, 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, 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 states, SWAP^2 ~ I | -//! | MZ | MZ ~ MZ MZ (idempotent, does not reset) | -//! | RESET | OP RESET ~ |0⟩ (resets to |0⟩) | -//! | MRESETZ | OP MRESETZ ~ |0⟩ (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::*, 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"#]], - } -} - -// Note: Gate truth tables (check_basis_table!) are not used here because the -// stabilizer simulator's state_dump() returns a CliffordUnitary (the operator), -// not a state vector. Comparing operators is too strict for basis-state tests -// (e.g., Y|0⟩ ~ |1⟩ as states, but Y ≠ X as operators). -// -// Instead, gate behavior on specific basis states is verified via check_sim! -// tests below (cx_on_zero_control_eq_identity, cx_on_one_control_flips_target, -// etc.), and algebraic identities are verified via check_programs_are_eq! which -// correctly checks unitary equivalence on all basis states. - -#[test] -fn single_qubit_gate_truth_tables() { - check_basis_table! { - simulator: StabilizerSimulator, - num_qubits: 1, - table: [ - // I gate: identity - (qir! { i(0) }, 0 => 0), - (qir! { i(0) }, 1 => 1), - // X gate: bit flip - (qir! { x(0) }, 0 => 1), - (qir! { x(0) }, 1 => 0), - // Y gate: bit flip (phase differs but same basis state) - (qir! { y(0) }, 0 => 1), - (qir! { y(0) }, 1 => 0), - // Z gate: phase only, no bit change - (qir! { z(0) }, 0 => 0), - (qir! { z(0) }, 1 => 1), - // Z gate: bit flip within H - (qir! { within { h(0) } apply { z(0) } }, 0 => 1), - (qir! { within { h(0) } apply { z(0) } }, 1 => 0), - // S gate: phase only - (qir! { s(0) }, 0 => 0), - (qir! { s(0) }, 1 => 1), - // S_ADJ gate: phase only - (qir! { s_adj(0) }, 0 => 0), - (qir! { s_adj(0) }, 1 => 1), - ], - } -} - -#[test] -fn two_qubit_gate_truth_tables() { - check_basis_table! { - simulator: StabilizerSimulator, - num_qubits: 2, - table: [ - // CX(control=q0, target=q1): flips q1 when q0=|1⟩ - (qir! { cx(0, 1) }, 0b00 => 0b00), - (qir! { cx(0, 1) }, 0b01 => 0b11), // q0=1 → flip q1 - (qir! { cx(0, 1) }, 0b10 => 0b10), // q0=0 → identity - (qir! { cx(0, 1) }, 0b11 => 0b01), // q0=1 → flip q1 - // CY gate: phase only, flips q1 when q0=|1⟩ - (qir! { cy(0, 1) }, 0b00 => 0b00), - (qir! { cy(0, 1) }, 0b01 => 0b11), // q0=1 → flip q1 - (qir! { cy(0, 1) }, 0b10 => 0b10), // q0=0 → identity - (qir! { cy(0, 1) }, 0b11 => 0b01), // q0=1 → flip q1 - // CZ gate: phase only, no bit changes - (qir! { cz(0, 1) }, 0b00 => 0b00), - (qir! { cz(0, 1) }, 0b01 => 0b01), - (qir! { cz(0, 1) }, 0b10 => 0b10), - (qir! { cz(0, 1) }, 0b11 => 0b11), - // CZ gate: bitflip within H - (qir! { within { h(1) } apply { cz(0, 1) } }, 0b00 => 0b00), - (qir! { within { h(1) } apply { cz(0, 1) } }, 0b01 => 0b11), - (qir! { within { h(1) } apply { cz(0, 1) } }, 0b10 => 0b10), - (qir! { within { h(1) } apply { cz(0, 1) } }, 0b11 => 0b01), - // SWAP gate: exchanges qubit states - (qir! { swap(0, 1) }, 0b00 => 0b00), - (qir! { swap(0, 1) }, 0b01 => 0b10), - (qir! { swap(0, 1) }, 0b10 => 0b01), - (qir! { swap(0, 1) }, 0b11 => 0b11), - ], - } -} - -// ==================== Single-Qubit Gate Tests ==================== - -// X gate tests -#[test] -fn x_is_self_adjoint() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! { i(0) }, - qir! { x(0); x(0) } - ], - num_qubits: 1, - } -} - -#[test] -fn x_eq_h_z_h() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! { x(0) }, - qir! { within { h(0) } apply { z(0) } } - ], - num_qubits: 1, - } -} - -// Y gate tests -#[test] -fn y_is_self_adjoint() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! { i(0) }, - qir! { y(0); y(0) } - ], - num_qubits: 1, - } -} - -#[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, - } -} - -// Z gate tests -#[test] -fn z_is_self_adjoint() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! { i(0) }, - qir! { within { h(0) } apply { z(0); z(0) } } - ], - num_qubits: 1, - } -} - -#[test] -fn z_eq_h_x_h() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! { z(0) }, - qir! { within { h(0) } apply { x(0) } } - ], - num_qubits: 1, - } -} - -// 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_is_self_adjoint() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! { i(0) }, - qir! { h(0); h(0) } - ], - num_qubits: 1, - } -} - -// S gate tests -#[test] -fn s_squared_eq_z() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! { z(0) }, - qir! { s(0); s(0) } - ], - num_qubits: 1, - } -} - -#[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, - } -} - -#[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, - } -} - -// 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, - } -} - -#[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, - } -} - -#[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, - } -} - -// ==================== 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! { within { x(0); h(1) } apply { cz(0, 1) } }, - qir! { within { x(0); h(1) } apply { cz(1, 0) } } - ], - num_qubits: 2, - } -} - -// 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, - } -} - -// ==================== Reset and Measurement Tests ==================== - -#[test] -fn reset_takes_qubit_back_to_zero() { - check_sim! { - simulator: StabilizerSimulator, - program: qir! { - x(0); - reset(0); // Resets to 0 - mz(0, 0); // Measures 0 - }, - num_qubits: 1, - num_results: 1, - output: expect![[r#"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"#]], - } -} - -#[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, - } -} - -// ==================== MOV Gate Tests ==================== - -#[test] -fn mov_is_noop_without_noise() { - check_programs_are_eq! { - simulator: StabilizerSimulator, - programs: [ - qir! {}, - qir! { mov(0) } - ], - num_qubits: 1, - } -} - -// ==================== 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"#]], - } -} 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 deleted file mode 100644 index a052d43e3f..0000000000 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/clifford_noisy.rs +++ /dev/null @@ -1,472 +0,0 @@ -// 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: 100, - seed: SEED, - noise: noise_config! { - x: { loss: 0.1 }, - }, - format: histogram, - output: expect![[r#" - -: 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"], - } -} - -// ==================== 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: histogram, - output: expect![[r#" - 01: 36 - 10: 56 - 11: 908"#]], - } -} - -#[test] -fn cz_noise_affects_state() { - // 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! { - 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"#]], - } -} - -// ==================== 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/gpu_full_state.rs b/source/pip/src/qir_simulation/gpu_full_state.rs index d548c96a0b..da03239eb6 100644 --- a/source/pip/src/qir_simulation/gpu_full_state.rs +++ b/source/pip/src/qir_simulation/gpu_full_state.rs @@ -336,7 +336,7 @@ impl GpuContext { } } -fn map_instruction(qir_inst: &QirInstruction) -> Option { +pub(crate) fn map_instruction(qir_inst: &QirInstruction) -> Option { let op = match qir_inst { QirInstruction::OneQubitGate(id, qubit) => match id { QirInstructionId::I => Op::new_id_gate(*qubit), diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests.rs b/source/pip/src/qir_simulation/tests.rs similarity index 64% rename from source/pip/src/qir_simulation/cpu_simulators/tests.rs rename to source/pip/src/qir_simulation/tests.rs index 58decd6f95..9837417c7b 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests.rs +++ b/source/pip/src/qir_simulation/tests.rs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -mod clifford_noiseless; -mod clifford_noisy; -mod full_state_noiseless; -mod full_state_noisy; +mod noiseless_tests; +mod noisy_tests; mod test_utils; /// Seed used for reproducible randomness in tests. diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs b/source/pip/src/qir_simulation/tests/noiseless_tests.rs similarity index 72% rename from source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs rename to source/pip/src/qir_simulation/tests/noiseless_tests.rs index 27324195d6..0d01e98980 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noiseless.rs +++ b/source/pip/src/qir_simulation/tests/noiseless_tests.rs @@ -1,79 +1,20 @@ // 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. +//! Noiseless tests for the CPU full-state, GPU full-state, and Clifford simulators. //! //! # Equivalence //! -//! The `~` symbol means: for every computational basis state |b⟩, the two -//! programs produce the same output state up to a global phase (which may -//! differ per basis state). This is verified by `check_programs_are_eq!`. -//! -//! Gate truth tables on all basis states are tested separately using -//! `check_basis_table!`. -//! -//! # 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. +//! For every computational basis state |b⟩, we check that the programs produce the +//! same output state up to a global phase (which may differ per basis state). +//! This is verified by `check_programs_are_eq!`. //! -//! ```text -//! | Gate | Properties | -//! |---------|---------------------------------------------------| -//! | I | I ~ {} (identity does nothing) | -//! | X | X flips qubit, X X ~ I | -//! | 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(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, does not reset) | -//! | MZ | MZ ~ MZ MZ (idempotent, does not reset) | -//! | RESET | OP RESET ~ |0⟩ (resets to |0⟩) | -//! | MRESETZ | OP MRESETZ ~ |0⟩ (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::*, SEED, test_utils::*}; +//! (The GPU simulator doesn't expose its internal state, so we just check the outputs are the same.) + +use super::{SEED, test_utils::*}; use expect_test::expect; +use qdk_simulators::cpu_full_state_simulator::{NoiselessSimulator, NoisySimulator}; +use qdk_simulators::stabilizer_simulator::StabilizerSimulator; use std::f64::consts::PI; // ==================== Generic Simulator Tests ==================== @@ -81,7 +22,7 @@ use std::f64::consts::PI; #[test] fn simulator_completes_all_shots() { check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mresetz(0, 0); @@ -105,7 +46,7 @@ fn simulator_completes_all_shots() { #[test] fn single_qubit_gate_truth_tables() { check_basis_table! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], num_qubits: 1, table: [ // I gate: identity @@ -129,6 +70,13 @@ fn single_qubit_gate_truth_tables() { // S_ADJ gate: phase only (qir! { s_adj(0) }, 0 => 0), (qir! { s_adj(0) }, 1 => 1), + ], + } + + check_basis_table! { + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], + num_qubits: 1, + table: [ // T gate: phase only (qir! { t(0) }, 0 => 0), (qir! { t(0) }, 1 => 1), @@ -142,7 +90,7 @@ fn single_qubit_gate_truth_tables() { #[test] fn two_qubit_gate_truth_tables() { check_basis_table! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], num_qubits: 2, table: [ // CX(control=q0, target=q1): flips q1 when q0=|1⟩ @@ -180,7 +128,7 @@ fn two_qubit_gate_truth_tables() { #[test] fn x_is_self_adjoint() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { x(0); x(0) } @@ -192,7 +140,7 @@ fn x_is_self_adjoint() { #[test] fn x_eq_h_z_h() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { x(0) }, qir! { within { h(0) } apply { z(0) } } @@ -205,7 +153,7 @@ fn x_eq_h_z_h() { #[test] fn y_is_self_adjoint() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { y(0); y(0) } @@ -217,7 +165,7 @@ fn y_is_self_adjoint() { #[test] fn y_gate_eq_x_z_and_z_x() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { y(0) }, qir! { x(0); z(0) }, @@ -231,7 +179,7 @@ fn y_gate_eq_x_z_and_z_x() { #[test] fn z_is_self_adjoint() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { within { h(0) } apply { z(0); z(0) } } @@ -243,7 +191,7 @@ fn z_is_self_adjoint() { #[test] fn z_eq_h_x_h() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { z(0) }, qir! { within { h(0) } apply { x(0) } } @@ -257,7 +205,7 @@ fn z_eq_h_x_h() { fn h_gate_creates_superposition() { // H creates equal superposition - should see both 0 and 1 check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { h(0); mresetz(0, 0); @@ -276,7 +224,7 @@ fn h_gate_creates_superposition() { #[test] fn h_is_self_adjoint() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { h(0); h(0) } @@ -289,7 +237,7 @@ fn h_is_self_adjoint() { #[test] fn s_squared_eq_z() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { z(0) }, qir! { s(0); s(0) } @@ -301,7 +249,7 @@ fn s_squared_eq_z() { #[test] fn s_and_s_adj_cancel() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { s(0); s_adj(0) }, @@ -314,7 +262,7 @@ fn s_and_s_adj_cancel() { #[test] fn s_adj_squared_eq_z() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { z(0) }, qir! { s_adj(0); s_adj(0) } @@ -327,7 +275,7 @@ fn s_adj_squared_eq_z() { #[test] fn sx_squared_eq_x() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { x(0) }, qir! { sx(0); sx(0) } @@ -339,7 +287,7 @@ fn sx_squared_eq_x() { #[test] fn sx_and_sx_adj_cancel() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { sx(0); sx_adj(0) }, @@ -352,7 +300,7 @@ fn sx_and_sx_adj_cancel() { #[test] fn sx_adj_squared_eq_x() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { x(0) }, qir! { sx_adj(0); sx_adj(0) } @@ -365,7 +313,7 @@ fn sx_adj_squared_eq_x() { #[test] fn t_fourth_eq_z() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { z(0) }, qir! { t(0); t(0); t(0); t(0); } @@ -378,7 +326,7 @@ fn t_fourth_eq_z() { #[test] fn t_and_t_adj_cancel() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { t(0); t_adj(0); }, @@ -391,7 +339,7 @@ fn t_and_t_adj_cancel() { #[test] fn t_adj_fourth_eq_z() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { z(0) }, qir! { t_adj(0); t_adj(0); t_adj(0); t_adj(0); } @@ -402,11 +350,12 @@ fn t_adj_fourth_eq_z() { // ==================== Two-Qubit Gate Tests ==================== +// CZ gate tests #[test] fn cz_symmetric() { // CZ is symmetric: CZ(a,b) = CZ(b,a) check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { within { x(0); h(1) } apply { cz(0, 1) } }, qir! { within { x(0); h(1) } apply { cz(1, 0) } } @@ -421,7 +370,7 @@ fn swap_commutes_operands() { // SWAP · (A⊗B) = (B⊗A) · SWAP for any single-qubit gates A, B. // Test with A=X, B=H: SWAP·(X⊗H)·SWAP = H⊗X check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { h(0); x(1) }, qir! { within { swap(0, 1) } apply { x(0); h(1) } } @@ -433,7 +382,7 @@ fn swap_commutes_operands() { #[test] fn swap_exchanges_qubit_states() { check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); swap(0, 1); @@ -449,7 +398,7 @@ fn swap_exchanges_qubit_states() { #[test] fn swap_twice_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! { x(0) }, qir! { x(0); swap(0, 1); swap(0, 1) } @@ -464,7 +413,7 @@ fn swap_twice_eq_identity() { #[test] fn rx_zero_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { rx(0.0, 0) } @@ -476,7 +425,7 @@ fn rx_zero_eq_identity() { #[test] fn rx_two_pi_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { rx(2.0 * PI, 0) } @@ -488,7 +437,7 @@ fn rx_two_pi_eq_identity() { #[test] fn rx_pi_eq_x() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { x(0) }, qir! { rx(PI, 0) } @@ -500,7 +449,7 @@ fn rx_pi_eq_x() { #[test] fn rx_half_pi_eq_sx() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { sx(0) }, qir! { rx(PI / 2.0, 0) } @@ -512,7 +461,7 @@ fn rx_half_pi_eq_sx() { #[test] fn rx_neg_half_pi_eq_sx_adj() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { sx_adj(0) }, qir! { rx(-PI / 2.0, 0) } @@ -525,7 +474,7 @@ fn rx_neg_half_pi_eq_sx_adj() { #[test] fn ry_zero_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { ry(0.0, 0) } @@ -537,7 +486,7 @@ fn ry_zero_eq_identity() { #[test] fn ry_two_pi_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { ry(2.0 * PI, 0) } @@ -549,7 +498,7 @@ fn ry_two_pi_eq_identity() { #[test] fn ry_pi_eq_y() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { y(0) }, qir! { ry(PI, 0) } @@ -562,7 +511,7 @@ fn ry_pi_eq_y() { #[test] fn rz_zero_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { rz(0.0, 0) } @@ -574,7 +523,7 @@ fn rz_zero_eq_identity() { #[test] fn rz_two_pi_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0) }, qir! { rz(2.0 * PI, 0) } @@ -586,7 +535,7 @@ fn rz_two_pi_eq_identity() { #[test] fn rz_pi_eq_z() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { z(0) }, qir! { rz(PI, 0) } @@ -598,7 +547,7 @@ fn rz_pi_eq_z() { #[test] fn rz_half_pi_eq_s() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { s(0) }, qir! { rz(PI / 2.0, 0) } @@ -610,7 +559,7 @@ fn rz_half_pi_eq_s() { #[test] fn rz_neg_half_pi_eq_s_adj() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { s_adj(0) }, qir! { rz(-PI / 2.0, 0) } @@ -622,7 +571,7 @@ fn rz_neg_half_pi_eq_s_adj() { #[test] fn rz_quarter_pi_eq_t() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { t(0) }, qir! { rz(PI / 4.0, 0) } @@ -634,7 +583,7 @@ fn rz_quarter_pi_eq_t() { #[test] fn rz_neg_quarter_pi_eq_t_adj() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { t_adj(0) }, qir! { rz(-PI / 4.0, 0) } @@ -649,7 +598,7 @@ fn rz_neg_quarter_pi_eq_t_adj() { #[test] fn rxx_zero_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0); i(1) }, qir! { rxx(0.0, 0, 1) } @@ -661,7 +610,7 @@ fn rxx_zero_eq_identity() { #[test] fn rxx_pi_eq_x_tensor_x() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { x(0); x(1) }, qir! { rxx(PI, 0, 1) } @@ -674,7 +623,7 @@ fn rxx_pi_eq_x_tensor_x() { #[test] fn ryy_zero_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0); i(1) }, qir! { ryy(0.0, 0, 1) } @@ -686,7 +635,7 @@ fn ryy_zero_eq_identity() { #[test] fn ryy_pi_eq_y_tensor_y() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { y(0); y(1) }, qir! { ryy(PI, 0, 1) } @@ -699,7 +648,7 @@ fn ryy_pi_eq_y_tensor_y() { #[test] fn rzz_zero_eq_identity() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { i(0); i(1) }, qir! { rzz(0.0, 0, 1) } @@ -708,7 +657,7 @@ fn rzz_zero_eq_identity() { } check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { within { h(0); h(1) } apply { i(0); i(1) } }, qir! { within { h(0); h(1) } apply { rzz(0.0, 0, 1) } } @@ -722,7 +671,7 @@ 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, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { z(0); z(1) }, qir! { rzz(PI, 0, 1) } @@ -731,7 +680,7 @@ fn rzz_pi_eq_z_tensor_z() { } check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, GpuSimulator], programs: [ qir! { within { h(0); h(1) } apply { z(0); z(1) } }, qir! { within { h(0); h(1) } apply { rzz(PI, 0, 1) } } @@ -745,7 +694,7 @@ fn rzz_pi_eq_z_tensor_z() { #[test] fn reset_takes_qubit_back_to_zero() { check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); reset(0); // Resets to 0 @@ -760,7 +709,7 @@ fn reset_takes_qubit_back_to_zero() { #[test] fn mresetz_resets_after_measurement() { check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mresetz(0, 0); // Measures 1, resets to 0 @@ -775,7 +724,7 @@ fn mresetz_resets_after_measurement() { #[test] fn mz_does_not_reset() { check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mz(0, 0); // Measures 1, does not reset @@ -787,26 +736,12 @@ fn mz_does_not_reset() { } } -#[test] -fn mz_is_idempotent() { - // M M ~ M (repeated measurement gives same result) - check_programs_are_eq! { - simulator: NoiselessSimulator, - programs: [ - qir! { x(0); mz(0, 0) }, - qir! { x(0); mz(0, 0); mz(0, 1) } - ], - num_qubits: 1, - num_results: 2, - } -} - // ==================== MOV Gate Tests ==================== #[test] fn mov_is_noop_without_noise() { check_programs_are_eq! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], programs: [ qir! {}, qir! { mov(0) } @@ -821,7 +756,7 @@ fn mov_is_noop_without_noise() { fn bell_state_produces_correlated_measurements() { // Bell state produces only correlated outcomes: 00 or 11 check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { h(0); cx(0, 1); @@ -842,7 +777,7 @@ fn bell_state_produces_correlated_measurements() { fn ghz_state_three_qubits() { // GHZ state produces only 000 or 111 check_sim! { - simulator: NoiselessSimulator, + simulators: [NoiselessSimulator, NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { h(0); cx(0, 1); diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs b/source/pip/src/qir_simulation/tests/noisy_tests.rs similarity index 71% rename from source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs rename to source/pip/src/qir_simulation/tests/noisy_tests.rs index 2f0276cd8c..94e3b96a5e 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/full_state_noisy.rs +++ b/source/pip/src/qir_simulation/tests/noisy_tests.rs @@ -1,53 +1,19 @@ // 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::*}; +//! Noiseless tests for the CPU full-state, GPU full-state, and Clifford simulators. + +use super::{SEED, test_utils::*}; use expect_test::expect; +use qdk_simulators::cpu_full_state_simulator::NoisySimulator; +use qdk_simulators::stabilizer_simulator::StabilizerSimulator; // ==================== Generic Simulator Tests ==================== #[test] fn simulator_completes_all_shots() { check_sim! { - simulator: StabilizerSimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mresetz(0, 0); @@ -68,7 +34,7 @@ fn simulator_completes_all_shots() { #[test] fn noiseless_config_produces_clean_results() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mresetz(0, 0); @@ -88,7 +54,7 @@ fn noiseless_config_produces_clean_results() { 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, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mresetz(0, 0); @@ -100,17 +66,17 @@ fn x_noise_on_x_gate_causes_bit_flips() { noise: noise_config! { x: { x: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 0: 97 - 1: 903"#]], + 0: 0.1 + 1: 0.9"#]], } } #[test] fn x_noise_on_h_gate_does_not_affect_outcome() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { h(0); mresetz(0, 0); @@ -122,10 +88,10 @@ fn x_noise_on_h_gate_does_not_affect_outcome() { noise: noise_config! { h: { x: 0.3 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 0: 498 - 1: 502"#]], + 0: 0.5 + 1: 0.5"#]], } } @@ -135,7 +101,7 @@ fn x_noise_on_h_gate_does_not_affect_outcome() { fn z_noise_does_not_affect_computational_basis() { // Z noise should not change measurement outcomes in computational basis check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mresetz(0, 0); @@ -147,8 +113,8 @@ fn z_noise_does_not_affect_computational_basis() { noise: noise_config! { x: { z: 0.5 }, }, - format: histogram, - output: expect![[r#"1: 100"#]], + format: histogram_probability(2), + output: expect![[r#"1: 1.00"#]], } } @@ -157,7 +123,7 @@ 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, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { h(0); h(0); // H·H = I, should give |0⟩ without noise @@ -170,10 +136,10 @@ fn z_noise_on_superposition_affects_interference() { noise: noise_config! { h: { z: 0.2 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 0: 819 - 1: 181"#]], + 0: 0.8 + 1: 0.2"#]], } } @@ -182,7 +148,7 @@ fn z_noise_on_superposition_affects_interference() { #[test] fn loss_noise_produces_loss_marker() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); mresetz(0, 0); @@ -194,33 +160,36 @@ fn loss_noise_produces_loss_marker() { noise: noise_config! { x: { loss: 0.1 }, }, - format: summary, + format: histogram_probability(1), output: expect![[r#" - shots: 1000 - unique: 2 - loss: 119"#]], + -: 0.1 + 1: 0.9"#]], } } +#[ignore = "loss behavior in cpu and gpu simulators is different"] #[test] -fn loss_appears_in_histogram() { +fn two_qubit_loss() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { - x(0); + cz(0, 1); mresetz(0, 0); + mresetz(1, 1); }, - num_qubits: 1, - num_results: 1, - shots: 1000, + num_qubits: 2, + num_results: 2, + shots: 100_000, seed: SEED, noise: noise_config! { - x: { loss: 0.1 }, + cz: { loss: 0.1 }, }, - format: histogram, + format: histogram_probability(2), output: expect![[r#" - -: 119 - 1: 881"#]], + --: 0.01 + -0: 0.09 + 0-: 0.09 + 00: 0.81"#]], } } @@ -230,7 +199,7 @@ fn loss_appears_in_histogram() { fn cx_xi_noise_flips_control_qubit() { // XI noise on CX flips the control qubit check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); cx(0, 1); @@ -244,10 +213,10 @@ fn cx_xi_noise_flips_control_qubit() { noise: noise_config! { cx: { xi: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 01: 92 - 11: 908"#]], + 01: 0.1 + 11: 0.9"#]], } } @@ -255,7 +224,7 @@ fn cx_xi_noise_flips_control_qubit() { fn cx_ix_noise_flips_target_qubit() { // IX noise on CX flips the target qubit check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); cx(0, 1); @@ -269,10 +238,10 @@ fn cx_ix_noise_flips_target_qubit() { noise: noise_config! { cx: { ix: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 10: 92 - 11: 908"#]], + 10: 0.1 + 11: 0.9"#]], } } @@ -280,7 +249,7 @@ fn cx_ix_noise_flips_target_qubit() { fn cx_xx_noise_flips_both_qubits() { // XX noise on CX flips both qubits check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); cx(0, 1); @@ -294,19 +263,18 @@ fn cx_xx_noise_flips_both_qubits() { noise: noise_config! { cx: { xx: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 00: 92 - 11: 908"#]], + 00: 0.1 + 11: 0.9"#]], } } #[test] -fn cz_noise_does_not_affect_outcome() { +fn cz_noise_affects_outcome() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { - h(0); cz(0, 1); mresetz(0, 0); mresetz(1, 1); @@ -318,17 +286,17 @@ fn cz_noise_does_not_affect_outcome() { noise: noise_config! { cz: { xi: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 00: 506 - 10: 494"#]], + 00: 0.9 + 10: 0.1"#]], } } #[test] fn swap_noise_affects_swapped_qubits() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); swap(0, 1); @@ -340,13 +308,12 @@ fn swap_noise_affects_swapped_qubits() { shots: 1000, seed: SEED, noise: noise_config! { - swap: { xi: 0.1, ix: 0.1 }, + swap: { ix: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 00: 103 - 01: 805 - 11: 92"#]], + 00: 0.1 + 01: 0.9"#]], } } @@ -356,7 +323,7 @@ fn swap_noise_affects_swapped_qubits() { fn different_gates_have_different_noise() { // Z gate has noise, X gate doesn't, Z noise flips some check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { z(0); x(0); @@ -369,10 +336,10 @@ fn different_gates_have_different_noise() { noise: noise_config! { z: { x: 0.2 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 0: 181 - 1: 819"#]], + 0: 0.2 + 1: 0.8"#]], } } @@ -382,7 +349,7 @@ fn different_gates_have_different_noise() { fn noise_accumulates_across_multiple_gates() { // Two X gates, each with noise - errors compound check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); x(0); // X·X = I, so result should be 0 without noise @@ -390,22 +357,23 @@ fn noise_accumulates_across_multiple_gates() { }, num_qubits: 1, num_results: 1, - shots: 100_000, + shots: 10_000, seed: SEED, noise: noise_config! { x: { x: 0.1 }, }, - format: histogram_percent, + format: histogram_probability(2), output: expect![[r#" - 0: 82.15% - 1: 17.85%"#]], + 0: 0.82 + 1: 0.18"#]], } } +#[ignore = "loss behavior in cpu and gpu simulators is different"] #[test] fn bell_state_with_combined_noise() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { h(0); cx(0, 1); @@ -414,20 +382,20 @@ fn bell_state_with_combined_noise() { }, num_qubits: 2, num_results: 2, - shots: 100_000, + shots: 200_000, seed: SEED, noise: noise_config! { h: { loss: 0.1 }, cx: { xi: 0.02, ix: 0.02 }, }, - format: histogram_percent, + format: histogram_probability(2), output: expect![[r#" - -0: 9.80% - -1: 0.19% - 00: 43.03% - 01: 1.75% - 10: 1.83% - 11: 43.40%"#]], + -0: 0.10 + -1: 0.00 + 00: 0.43 + 01: 0.02 + 10: 0.02 + 11: 0.43"#]], } } @@ -436,7 +404,7 @@ fn bell_state_with_combined_noise() { #[test] fn rx_gate_with_noise() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, GpuSimulator], program: qir! { rx(std::f64::consts::PI, 0); // Rx(π) ~ X mresetz(0, 0); @@ -448,10 +416,10 @@ fn rx_gate_with_noise() { noise: noise_config! { rx: { x: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 0: 97 - 1: 903"#]], + 0: 0.1 + 1: 0.9"#]], } } @@ -459,7 +427,7 @@ fn rx_gate_with_noise() { fn rz_gate_with_z_noise_no_effect_on_basis() { // Rz followed by Z noise - no effect on computational basis check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, GpuSimulator], program: qir! { rz(std::f64::consts::PI, 0); mresetz(0, 0); @@ -481,7 +449,7 @@ fn rz_gate_with_z_noise_no_effect_on_basis() { #[test] fn rxx_gate_with_noise() { check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, GpuSimulator], program: qir! { rxx(std::f64::consts::PI, 0, 1); // Rxx(π) ~ X⊗X mresetz(0, 0); @@ -494,10 +462,10 @@ fn rxx_gate_with_noise() { noise: noise_config! { rxx: { xi: 0.1 }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 01: 89 - 11: 911"#]], + 01: 0.1 + 11: 0.9"#]], } } @@ -507,7 +475,7 @@ fn rxx_gate_with_noise() { fn noise_intrinsic_single_qubit_x_noise() { // Single-qubit X noise via intrinsic check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { noise_intrinsic(0, &[0]); mresetz(0, 0); @@ -521,10 +489,10 @@ fn noise_intrinsic_single_qubit_x_noise() { 0: { x: 0.1 }, }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 0: 886 - 1: 114"#]], + 0: 0.9 + 1: 0.1"#]], } } @@ -532,7 +500,7 @@ fn noise_intrinsic_single_qubit_x_noise() { fn noise_intrinsic_single_qubit_z_noise_no_effect() { // Z noise on |0⟩ has no observable effect check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { noise_intrinsic(0, &[0]); mresetz(0, 0); @@ -555,7 +523,7 @@ fn noise_intrinsic_single_qubit_z_noise_no_effect() { fn noise_intrinsic_two_qubit_correlated_xx_noise() { // Two-qubit XX noise causes correlated bit flips check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { noise_intrinsic(0, &[0, 1]); mresetz(0, 0); @@ -570,10 +538,10 @@ fn noise_intrinsic_two_qubit_correlated_xx_noise() { 0: { xx: 0.1 }, }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 00: 886 - 11: 114"#]], + 00: 0.9 + 11: 0.1"#]], } } @@ -581,7 +549,7 @@ fn noise_intrinsic_two_qubit_correlated_xx_noise() { fn noise_intrinsic_two_qubit_independent_noise() { // XI and IX noise cause independent flips on each qubit check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { noise_intrinsic(0, &[0, 1]); mresetz(0, 0); @@ -596,11 +564,11 @@ fn noise_intrinsic_two_qubit_independent_noise() { 0: { xi: 0.1, ix: 0.1 }, }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 00: 783 - 01: 103 - 10: 114"#]], + 00: 0.8 + 01: 0.1 + 10: 0.1"#]], } } @@ -608,7 +576,7 @@ fn noise_intrinsic_two_qubit_independent_noise() { fn noise_intrinsic_multiple_ids() { // Multiple intrinsic IDs with different noise configurations check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { noise_intrinsic(0, &[0]); noise_intrinsic(1, &[1]); @@ -617,7 +585,7 @@ fn noise_intrinsic_multiple_ids() { }, num_qubits: 2, num_results: 2, - shots: 1000, + shots: 10_000, seed: SEED, noise: noise_config! { intrinsics: { @@ -625,12 +593,12 @@ fn noise_intrinsic_multiple_ids() { 1: { x: 0.5 }, }, }, - format: histogram, + format: histogram_probability(2), output: expect![[r#" - 00: 459 - 01: 427 - 10: 58 - 11: 56"#]], + 00: 0.45 + 01: 0.45 + 10: 0.05 + 11: 0.05"#]], } } @@ -638,7 +606,7 @@ fn noise_intrinsic_multiple_ids() { fn noise_intrinsic_three_qubit_correlated() { // Three-qubit correlated noise (XXX flips all three) check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { noise_intrinsic(0, &[0, 1, 2]); mresetz(0, 0); @@ -654,10 +622,10 @@ fn noise_intrinsic_three_qubit_correlated() { 0: { xxx: 0.1 }, }, }, - format: histogram, + format: histogram_probability(1), output: expect![[r#" - 000: 886 - 111: 114"#]], + 000: 0.9 + 111: 0.1"#]], } } @@ -665,7 +633,7 @@ fn noise_intrinsic_three_qubit_correlated() { fn noise_intrinsic_combined_with_gate_noise() { // Intrinsic noise combined with regular gate noise check_sim! { - simulator: NoisySimulator, + simulators: [NoisySimulator, StabilizerSimulator, GpuSimulator], program: qir! { x(0); noise_intrinsic(0, &[0]); @@ -673,7 +641,7 @@ fn noise_intrinsic_combined_with_gate_noise() { }, num_qubits: 1, num_results: 1, - shots: 1000, + shots: 10_000, seed: SEED, noise: noise_config! { x: { x: 0.1 }, @@ -681,9 +649,9 @@ fn noise_intrinsic_combined_with_gate_noise() { 0: { x: 0.1 }, }, }, - format: histogram, + format: histogram_probability(2), output: expect![[r#" - 0: 178 - 1: 822"#]], + 0: 0.18 + 1: 0.82"#]], } } diff --git a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs b/source/pip/src/qir_simulation/tests/test_utils.rs similarity index 63% rename from source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs rename to source/pip/src/qir_simulation/tests/test_utils.rs index de71d42a9d..c4a6498480 100644 --- a/source/pip/src/qir_simulation/cpu_simulators/tests/test_utils.rs +++ b/source/pip/src/qir_simulation/tests/test_utils.rs @@ -4,6 +4,7 @@ // #![allow(dead_code)] use crate::qir_simulation::{QirInstruction, QirInstructionId, cpu_simulators::run_shot}; +use qdk_simulators::noise_config as noise_config_mod; // ==================== Instruction Builder Functions ==================== // These functions create QirInstruction values for use in check_sim! tests. @@ -237,7 +238,7 @@ macro_rules! noise_config { // Entry point ( $( $field:ident : { $($inner:tt)* } ),* $(,)? ) => {{ #[allow(unused_mut)] - let mut config = noise_config::NoiseConfig::::NOISELESS; + let mut config = qdk_simulators::noise_config::NoiseConfig::::NOISELESS; $( noise_config!(@field config, $field, { $($inner)* }); )* @@ -247,7 +248,7 @@ macro_rules! noise_config { // Handle intrinsics field specially (@field $config:ident, intrinsics, { $( $id:literal : { $($pauli:ident : $prob:expr),* $(,)? } ),* $(,)? }) => {{ $( - let mut table = noise_config::NoiseTable::::noiseless(0); + let mut table = qdk_simulators::noise_config::NoiseTable::::noiseless(0); $( noise_config!(@set_pauli table, $pauli, $prob); )* @@ -327,7 +328,7 @@ macro_rules! noise_config { // 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); + let mut table = qdk_simulators::noise_config::NoiseTable::::noiseless($qubits); $( noise_config!(@set_pauli table, $pauli, $prob); )* @@ -394,11 +395,11 @@ macro_rules! qir { // 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),*]; + let mut result: Vec<_> = vec![$($acc),*]; result.extend(qir!($($within_tt)*)); // forward within result.extend(qir!($($apply_tt)*)); // apply - let within_adj: Vec = { - let mut v: Vec = qir!($($within_tt)*) + let within_adj: Vec<_> = { + let mut v: Vec<_> = qir!($($within_tt)*) .into_iter() .map(adjoint) // compute adjoint of each gate .collect(); @@ -406,7 +407,7 @@ macro_rules! qir { v }; result.extend(within_adj); - let remaining: Vec = qir!(@accum [] $($rest)*); + let remaining: Vec<_> = qir!(@accum [] $($rest)*); result.extend(remaining); result }}; @@ -434,7 +435,7 @@ pub(crate) use qir; /// /// # Required fields: /// - `simulator`: One of `StabilizerSimulator`, `NoisySimulator`, or `NoiselessSimulator` -/// - `program`: An expression that evaluates to `Vec` (use `qir!` macro) +/// - `program`: An expression that evaluates to a `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) @@ -475,9 +476,51 @@ pub(crate) use qir; /// } /// ``` macro_rules! check_sim { + // Multi-simulator entry: runs the same test on each listed simulator + ( + simulators: [ $($sim:ident),+ $(,)? ], + $($rest:tt)* + ) => {{ + check_sim!(@multi_sim [ $($sim),+ ] $($rest)*) + }}; + + // Internal: process first simulator and recurse for the rest + (@multi_sim [ $first:ident, $($remaining:ident),+ ] $($body:tt)*) => {{ + check_sim! { + simulator: $first, + $($body)* + } + check_sim!(@multi_sim [ $($remaining),+ ] $($body)*) + }}; + + // Internal: base case — process the last simulator + (@multi_sim [ $last:ident ] $($body:tt)*) => {{ + check_sim! { + simulator: $last, + $($body)* + } + }}; + + // GPU simulator entry: wraps in require_gpu! for safe skipping + ( + simulator: GpuSimulator, + $($fields:tt)* + ) => {{ + require_gpu! { + check_sim!(@run_all GpuSimulator, $($fields)*) + } + }}; + // Main entry with all fields ( simulator: $sim:ident, + $($fields:tt)* + ) => {{ + check_sim!(@run_all $sim, $($fields)*) + }}; + + // Internal: shared logic for all simulators + (@run_all $sim:ident, program: $program:expr, num_qubits: $num_qubits:expr, num_results: $num_results:expr, @@ -488,12 +531,12 @@ macro_rules! check_sim { output: $expected:expr $(,)? ) => {{ // Get instructions from the expression - let instructions: Vec = $program; + 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 noise: qdk_simulators::noise_config::NoiseConfig = check_sim!(@default_noise $( $noise )?); let format_fn = check_sim!(@default_format $( $format )?); // Create simulator and run @@ -516,7 +559,7 @@ macro_rules! check_sim { // Default noise (@default_noise $noise:expr) => { $noise }; - (@default_noise) => { noise_config::NoiseConfig::::NOISELESS }; + (@default_noise) => { qdk_simulators::noise_config::NoiseConfig::::NOISELESS }; // Default format (@default_format $format:expr) => { $format }; @@ -524,6 +567,8 @@ macro_rules! check_sim { // Run with StabilizerSimulator (@run StabilizerSimulator, $instructions:expr, $num_qubits:expr, $num_results:expr, $shots:expr, $seed:expr, $noise:expr) => {{ + use $crate::qir_simulation::cpu_simulators::run; + use qdk_simulators::Simulator as _; let make_simulator = |num_qubits, num_results, seed, noise| { StabilizerSimulator::new(num_qubits as usize, num_results as usize, seed, noise) }; @@ -532,7 +577,8 @@ macro_rules! check_sim { // 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; + use $crate::qir_simulation::cpu_simulators::run; + use qdk_simulators::{cpu_full_state_simulator::noise::Fault, noise_config::CumulativeNoiseConfig, Simulator as _}; let make_simulator = |num_qubits, num_results, seed, noise| { NoisySimulator::new(num_qubits as usize, num_results as usize, seed, noise) }; @@ -541,12 +587,19 @@ macro_rules! check_sim { // 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; + use $crate::qir_simulation::cpu_simulators::run; + use qdk_simulators::{cpu_full_state_simulator::noise::Fault, noise_config::CumulativeNoiseConfig, Simulator as _}; + use std::sync::Arc; 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) }}; + + // Run with GPU simulator + (@run GpuSimulator, $instructions:expr, $num_qubits:expr, $num_results:expr, $shots:expr, $seed:expr, $noise:expr) => {{ + run_on_gpu($instructions, $num_qubits, $num_results, $shots, $seed, $noise) + }}; } #[cfg(test)] @@ -561,7 +614,7 @@ pub fn check_programs_are_eq_cpu( S::StateDumpData: PartialEq + std::fmt::Debug, { for basis_state in 0u32..(1u32 << num_qubits) { - let prep: Vec = (0..num_qubits) + let prep: Vec<_> = (0..num_qubits) .filter(|q| (basis_state >> q) & 1 == 1) .map(x) .collect(); @@ -596,6 +649,85 @@ pub fn check_programs_are_eq_cpu( } } +/// GPU variant of [`check_programs_are_eq_cpu`]. +/// +/// Since the GPU simulator does not expose internal state, this function verifies +/// program equivalence by running each program with `mresetz` measurements on every +/// computational basis state and comparing the resulting bitstrings. Two programs are +/// considered equivalent if they produce the same measurement outcome on all inputs. +/// +/// Once the GPU simulator exposes its internal state, we can change this function +/// to check for a stronger equivalence relation. +pub fn check_programs_are_eq_gpu( + programs: &[Vec], + num_qubits: u32, + num_results: u32, +) { + use crate::qir_simulation::gpu_full_state::map_instruction; + + // Use the larger of num_qubits and num_results for measurement slots + let result_count = std::cmp::max(num_qubits, num_results); + + for basis_state in 0u32..(1u32 << num_qubits) { + let prep: Vec<_> = (0..num_qubits) + .filter(|q| (basis_state >> q) & 1 == 1) + .map(x) + .collect(); + + let measurements: Vec<_> = (0..num_qubits).map(|i| mresetz(i, i)).collect(); + + let results: Vec = programs + .iter() + .map(|program| { + let ops: Vec = prep + .iter() + .chain(program.iter()) + .chain(measurements.iter()) + .filter_map(map_instruction) + .collect(); + + let gpu_noise: Option> = Some( + noise_table_f64_to_f32(noise_config_mod::NoiseConfig::::NOISELESS), + ); + + #[allow(clippy::cast_possible_wrap)] + let sim_results = qdk_simulators::run_shots_sync( + num_qubits as i32, + result_count as i32, + &ops, + &gpu_noise, + 1, + 0xfeed_face, + 0, + ) + .expect("GPU simulation failed"); + + sim_results.shot_results[0] + .iter() + .map(|r| match r { + 0 => '0', + 1 => '1', + _ => 'L', + }) + .collect::() + }) + .collect(); + + // Compare all results to the first one + for (i, result) in results.iter().enumerate().skip(1) { + assert!( + results[0] == *result, + "GPU: Program 0 and program {i} produce different measurements \ + on basis state |{basis_state:0width$b}⟩.\n\ + Program 0 result: {first}\n\ + Program {i} result: {result}", + first = results[0], + width = num_qubits as usize, + ); + } + } +} + /// Macro to check that multiple QIR programs produce the same state on every /// computational basis state, up to global phase. /// @@ -611,7 +743,7 @@ pub fn check_programs_are_eq_cpu( /// /// # Required fields: /// - `simulator`: One of `StabilizerSimulator`, `NoisySimulator`, or `NoiselessSimulator` -/// - `programs`: An array of expressions evaluating to `Vec` (use `qir!` macro) +/// - `programs`: An array of expressions evaluating to a `Vec` (use `qir!` macro) /// - `num_qubits`: The number of qubits in the simulation /// /// # Optional fields: @@ -629,7 +761,59 @@ pub fn check_programs_are_eq_cpu( /// } /// ``` macro_rules! check_programs_are_eq { - // Pattern without num_results - defaults to 0 + // Multi-simulator entry: runs the same test on each listed simulator + ( + simulators: [ $($sim:ident),+ $(,)? ], + $($rest:tt)* + ) => {{ + check_programs_are_eq!(@multi_sim [ $($sim),+ ] $($rest)*) + }}; + + // Internal: process first simulator and recurse for the rest + (@multi_sim [ $first:ident, $($remaining:ident),+ ] $($body:tt)*) => {{ + check_programs_are_eq! { + simulator: $first, + $($body)* + } + check_programs_are_eq!(@multi_sim [ $($remaining),+ ] $($body)*) + }}; + + // Internal: base case — process the last simulator + (@multi_sim [ $last:ident ] $($body:tt)*) => {{ + check_programs_are_eq! { + simulator: $last, + $($body)* + } + }}; + + // GPU simulator: pattern without num_results - defaults to 0 + ( + simulator: GpuSimulator, + programs: [ $( $program:expr ),+ $(,)? ], + num_qubits: $num_qubits:expr $(,)? + ) => {{ + check_programs_are_eq! { + simulator: GpuSimulator, + programs: [ $( $program ),+ ], + num_qubits: $num_qubits, + num_results: 0, + } + }}; + + // GPU simulator: pattern with explicit num_results + ( + simulator: GpuSimulator, + programs: [ $( $program:expr ),+ $(,)? ], + num_qubits: $num_qubits:expr, + num_results: $num_results:expr $(,)? + ) => {{ + require_gpu! { + let programs: Vec> = vec![ $( $program ),+ ]; + $crate::qir_simulation::tests::test_utils::check_programs_are_eq_gpu(&programs, $num_qubits, $num_results); + } + }}; + + // CPU simulators: pattern without num_results - defaults to 0 ( simulator: $sim:ident, programs: [ $( $program:expr ),+ $(,)? ], @@ -643,15 +827,15 @@ macro_rules! check_programs_are_eq { } }}; - // Pattern with explicit num_results + // CPU simulators: pattern with explicit num_results ( simulator: $sim:ident, programs: [ $( $program:expr ),+ $(,)? ], num_qubits: $num_qubits:expr, num_results: $num_results:expr $(,)? ) => {{ - let programs: Vec> = vec![ $( $program ),+ ]; - $crate::qir_simulation::cpu_simulators::tests::test_utils::check_programs_are_eq_cpu::<$sim>(&programs, $num_qubits, $num_results); + let programs: Vec> = vec![ $( $program ),+ ]; + $crate::qir_simulation::tests::test_utils::check_programs_are_eq_cpu::<$sim>(&programs, $num_qubits, $num_results); }}; } @@ -667,13 +851,12 @@ where measurements.push(mz(i, i)); } - #[allow(clippy::cast_possible_truncation)] for (program, input_bits, expected_bits) in table { - let actual_prep: Vec = (0..num_qubits) + let actual_prep: Vec<_> = (0..num_qubits) .filter(|q| (input_bits >> q) & 1 == 1) .map(x) .collect(); - let expected_prep: Vec = (0..num_qubits) + let expected_prep: Vec<_> = (0..num_qubits) .filter(|q| (expected_bits >> q) & 1 == 1) .map(x) .collect(); @@ -712,6 +895,84 @@ where } } +/// GPU variant of [`check_basis_table_cpu`]. +/// +/// Since the GPU simulator does not expose internal state, this function verifies +/// basis table entries by running each circuit with `mresetz` measurements and +/// comparing the resulting bitstring against the expected output. +/// +/// The instructions are converted to GPU `Op`s and executed via `run_shots_sync`. +/// The prep, program, and measurement instructions are mapped separately because +/// `QirInstruction` does not implement `Clone`. +pub fn check_basis_table_gpu(num_qubits: u32, table: &[(Vec, u32, u32)]) { + use crate::qir_simulation::gpu_full_state::map_instruction; + + for (program, input_bits, expected_bits) in table { + // Prepare the input basis state with X gates + let prep: Vec<_> = (0..num_qubits) + .filter(|q| (input_bits >> q) & 1 == 1) + .map(x) + .collect(); + + // Append mresetz measurements for all qubits + let measurements: Vec<_> = (0..num_qubits).map(|i| mresetz(i, i)).collect(); + + // Convert all three slices to GPU Ops + let ops: Vec = prep + .iter() + .chain(program.iter()) + .chain(measurements.iter()) + .filter_map(map_instruction) + .collect(); + + let rng_seed = 0xfeed_face; + let gpu_noise: Option> = Some( + noise_table_f64_to_f32(noise_config_mod::NoiseConfig::::NOISELESS), + ); + + #[allow(clippy::cast_possible_wrap)] + let sim_results = qdk_simulators::run_shots_sync( + num_qubits as i32, + num_qubits as i32, + &ops, + &gpu_noise, + 1, + rng_seed, + 0, + ) + .expect("GPU simulation failed"); + + // Build the actual result bitstring + let actual: String = sim_results.shot_results[0] + .iter() + .map(|r| match r { + 0 => '0', + 1 => '1', + _ => 'L', + }) + .collect(); + + // Build expected bitstring: qubit 0 is the leftmost character + let expected: String = (0..num_qubits) + .map(|q| { + if (expected_bits >> q) & 1 == 1 { + '1' + } else { + '0' + } + }) + .collect(); + + assert!( + actual == expected, + "GPU basis table test failed: gate={program:?} on input |{:0width$b}⟩ \ + expected \"{expected}\" but got \"{actual}\"", + input_bits.reverse_bits() >> (32 - num_qubits), + width = num_qubits as usize, + ); + } +} + /// Macro for table-driven basis state tests. /// /// Tests that applying a gate to a computational basis state produces the expected @@ -732,6 +993,49 @@ where /// } /// ``` macro_rules! check_basis_table { + // Multi-simulator entry: runs the same test on each listed simulator + ( + simulators: [ $($sim:ident),+ $(,)? ], + $($rest:tt)* + ) => {{ + check_basis_table!(@multi_sim [ $($sim),+ ] $($rest)*) + }}; + + // Internal: process first simulator and recurse for the rest + (@multi_sim [ $first:ident, $($remaining:ident),+ ] $($body:tt)*) => {{ + check_basis_table! { + simulator: $first, + $($body)* + } + check_basis_table!(@multi_sim [ $($remaining),+ ] $($body)*) + }}; + + // Internal: base case — process the last simulator + (@multi_sim [ $last:ident ] $($body:tt)*) => {{ + check_basis_table! { + simulator: $last, + $($body)* + } + }}; + + // GPU simulator: uses measurement-based comparison via run_on_gpu + ( + simulator: GpuSimulator, + num_qubits: $nq:expr, + table: [ + $( ( $gate:expr, $input:expr => $output:expr ) ),* + $(,)? + ] $(,)? + ) => {{ + require_gpu! { + let table: Vec<(Vec<_>, u32, u32)> = vec![ + $( ($gate, $input, $output) ),* + ]; + $crate::qir_simulation::tests::test_utils::check_basis_table_gpu($nq, &table); + } + }}; + + // CPU simulators: uses state-level comparison via Simulator trait ( simulator: $sim:ident, num_qubits: $nq:expr, @@ -740,10 +1044,10 @@ macro_rules! check_basis_table { $(,)? ] $(,)? ) => {{ - let table: Vec<(Vec, u32, u32)> = vec![ + let table: Vec<(Vec<_>, u32, u32)> = vec![ $( ($gate, $input, $output) ),* ]; - $crate::qir_simulation::cpu_simulators::tests::test_utils::check_basis_table_cpu::<$sim>($nq, &table); + $crate::qir_simulation::tests::test_utils::check_basis_table_cpu::<$sim>($nq, &table); }}; } @@ -788,20 +1092,25 @@ 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, dead_code)] -pub fn histogram_percent(output: &[String]) -> String { +#[allow(clippy::cast_precision_loss)] +pub fn histogram_probability(significant_digits: usize) -> impl Fn(&[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; + move |output: &[String]| -> String { + 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)| { + let probability = v as f64 / total; + format!("{k}: {probability:.significant_digits$}") + }) + .collect::>() + .join("\n") } - 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). @@ -876,3 +1185,139 @@ pub fn outcomes(output: &[String]) -> String { let unique_results: BTreeSet<&str> = output.iter().map(String::as_str).collect(); unique_results.into_iter().collect::>().join("\n") } + +// ==================== GPU Simulator Runner ==================== + +/// Run a QIR program on the GPU simulator. +/// +/// This converts `QirInstruction`s to GPU `Op`s via `map_instruction` and +/// calls `qdk_simulators::run_shots_sync` to execute them on the GPU. +/// The result format matches the CPU simulators: a `Vec` where +/// each string is a bitstring of '0', '1', or 'L' (for lost qubits). +pub fn run_on_gpu( + instructions: &[QirInstruction], + num_qubits: u32, + num_results: u32, + shots: u32, + seed: Option, + noise: noise_config_mod::NoiseConfig, +) -> Vec { + use crate::qir_simulation::gpu_full_state::map_instruction; + + // Convert QirInstructions to GPU Ops + let ops: Vec = + instructions.iter().filter_map(map_instruction).collect(); + + // Convert f64 noise config to f32/f64 for GPU + let gpu_noise: Option> = + Some(noise_table_f64_to_f32(noise)); + + let rng_seed = seed.unwrap_or(0xfeed_face); + + #[allow(clippy::cast_possible_wrap)] + let sim_results = qdk_simulators::run_shots_sync( + num_qubits as i32, + num_results as i32, + &ops, + &gpu_noise, + shots as i32, + rng_seed, + 0, + ) + .expect("GPU simulation failed"); + + let result_count = num_results as usize; + + sim_results + .shot_results + .iter() + .map(|shot_results| { + let mut bitstring = String::with_capacity(result_count); + for res in shot_results { + let ch = match res { + 0 => '0', + 1 => '1', + _ => 'L', + }; + bitstring.push(ch); + } + bitstring + }) + .collect() +} + +/// Convert a `NoiseTable` to `NoiseTable`. +#[allow(clippy::cast_possible_truncation)] +fn noise_table_f64_to_f32_table( + table: noise_config_mod::NoiseTable, +) -> noise_config_mod::NoiseTable { + noise_config_mod::NoiseTable { + qubits: table.qubits, + pauli_strings: table.pauli_strings, + probabilities: table.probabilities.into_iter().map(|p| p as f32).collect(), + loss: table.loss as f32, + } +} + +/// Convert a `NoiseConfig` to `NoiseConfig` for the GPU simulator. +fn noise_table_f64_to_f32( + noise: noise_config_mod::NoiseConfig, +) -> noise_config_mod::NoiseConfig { + noise_config_mod::NoiseConfig { + i: noise_table_f64_to_f32_table(noise.i), + x: noise_table_f64_to_f32_table(noise.x), + y: noise_table_f64_to_f32_table(noise.y), + z: noise_table_f64_to_f32_table(noise.z), + h: noise_table_f64_to_f32_table(noise.h), + s: noise_table_f64_to_f32_table(noise.s), + s_adj: noise_table_f64_to_f32_table(noise.s_adj), + t: noise_table_f64_to_f32_table(noise.t), + t_adj: noise_table_f64_to_f32_table(noise.t_adj), + sx: noise_table_f64_to_f32_table(noise.sx), + sx_adj: noise_table_f64_to_f32_table(noise.sx_adj), + rx: noise_table_f64_to_f32_table(noise.rx), + ry: noise_table_f64_to_f32_table(noise.ry), + rz: noise_table_f64_to_f32_table(noise.rz), + cx: noise_table_f64_to_f32_table(noise.cx), + cy: noise_table_f64_to_f32_table(noise.cy), + cz: noise_table_f64_to_f32_table(noise.cz), + rxx: noise_table_f64_to_f32_table(noise.rxx), + ryy: noise_table_f64_to_f32_table(noise.ryy), + rzz: noise_table_f64_to_f32_table(noise.rzz), + swap: noise_table_f64_to_f32_table(noise.swap), + ccx: noise_table_f64_to_f32_table(noise.ccx), + mov: noise_table_f64_to_f32_table(noise.mov), + mz: noise_table_f64_to_f32_table(noise.mz), + mresetz: noise_table_f64_to_f32_table(noise.mresetz), + idle: noise.idle, + intrinsics: noise.intrinsics, + } +} + +/// Check if a compatible GPU adapter is available on the system. +/// Returns `true` if GPU simulation is supported, `false` otherwise. +pub fn gpu_is_available() -> bool { + qdk_simulators::try_create_gpu_adapter().is_ok() +} + +/// Macro to skip a test if no compatible GPU adapter is available. +/// +/// Usage example: +/// ```ignore +/// require_gpu! { +/// conditional_gpu_code_here(); +/// conditional_gpu_code_here(); +/// } +/// ``` +macro_rules! require_gpu { + ($($body:tt)*) => { + if gpu_is_available() { + $($body)* + } else { + eprintln!("Skipping GPU test: no compatible GPU adapter found"); + } + }; +} + +#[cfg(test)] +pub(crate) use require_gpu;