Cookbook

Self-contained, copy-paste recipes for common quantum testing patterns. Each recipe is a complete, runnable test.


Recipe 1: Testing a single-qubit gate

"""test_gates.py — assert gate unitaries are correct."""
import numpy as np
import pytest
from pytest_quantum import assert_unitary, assert_state_fidelity_above


def test_hadamard():
    from qiskit import QuantumCircuit
    qc = QuantumCircuit(1)
    qc.h(0)
    H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
    assert_unitary(qc, H)


def test_t_gate():
    from qiskit import QuantumCircuit
    qc = QuantumCircuit(1)
    qc.t(0)
    T = np.diag([1, np.exp(1j * np.pi / 4)])
    assert_unitary(qc, T)


def test_rx_at_pi():
    from qiskit import QuantumCircuit
    from qiskit.quantum_info import Statevector
    qc = QuantumCircuit(1)
    qc.rx(np.pi, 0)                              # RX(π) ≈ -iX: |0⟩ → -i|1⟩
    sv = Statevector(qc).data
    assert_state_fidelity_above(sv, np.array([0, -1j]), threshold=0.9999)

Recipe 2: Shot-noise-safe Bell state test

"""test_bell.py — the classic Bell state, done correctly."""
from pytest_quantum import assert_measurement_distribution, assert_circuits_equivalent
import numpy as np


def test_bell_distribution(aer_simulator):
    from qiskit import QuantumCircuit, transpile

    qc = QuantumCircuit(2)
    qc.h(0)
    qc.cx(0, 1)
    qc.measure_all()
    counts = aer_simulator.run(transpile(qc, aer_simulator), shots=2000) \
                          .result().get_counts()

    # Chi-square test — never fails due to shot noise alone
    assert_measurement_distribution(
        counts,
        expected_probs={"00": 0.5, "11": 0.5},
        significance=0.01,
    )


def test_bell_cirq_vs_qiskit():
    """Same circuit in two frameworks should give the same unitary."""
    from qiskit import QuantumCircuit
    import cirq

    qk = QuantumCircuit(2); qk.h(0); qk.cx(0, 1)

    q = cirq.LineQubit.range(2)
    cq = cirq.Circuit(cirq.H(q[0]), cirq.CNOT(q[0], q[1]))

    assert_circuits_equivalent(qk, cq)

Recipe 3: Noisy simulation with density matrices

"""test_noisy.py — test circuits under realistic noise."""
from pytest_quantum import (
    assert_density_matrix_close,
    assert_purity_above,
    assert_trace_distance_below,
)


def test_noisy_rx(aer_noise_simulator):
    from qiskit import QuantumCircuit, transpile
    from qiskit.quantum_info import DensityMatrix

    sim = aer_noise_simulator(error_rate=0.01)
    qc = QuantumCircuit(1); qc.rx(np.pi/2, 0)
    transpiled = transpile(qc, sim)

    # Get density matrix from 10000 shots
    result = sim.run(transpiled, shots=10000).result()
    rho = DensityMatrix(result.get_statevector()).data

    # Even with 1% noise, purity should be > 0.9
    assert_purity_above(rho, min_purity=0.90)

    # Should be close to ideal |+y⟩ state
    ideal = DensityMatrix.from_label("+").data
    assert_trace_distance_below(rho, ideal, max_distance=0.05)

Recipe 4: Snapshot regression testing

"""test_regression.py — catch accidental circuit changes."""
from pytest_quantum import assert_unitary_snapshot, assert_distribution_snapshot


def test_my_circuit_no_regression():
    """First run: creates .snapshots/my_algo.npy. All future runs compare."""
    from qiskit import QuantumCircuit
    qc = QuantumCircuit(2)
    qc.h(0); qc.cx(0, 1); qc.s(0)
    assert_unitary_snapshot(qc, "my_algo")


def test_distribution_no_regression(aer_simulator):
    from qiskit import QuantumCircuit, transpile
    qc = QuantumCircuit(2); qc.h(0); qc.cx(0,1); qc.measure_all()
    counts = aer_simulator.run(transpile(qc, aer_simulator), shots=2000) \
                          .result().get_counts()
    assert_distribution_snapshot(counts, "bell_dist", max_tvd=0.05)

Regenerate all snapshots: pytest --quantum-update-snapshots


Recipe 5: VQE testing end-to-end

"""test_vqe.py — validate a variational quantum eigensolver."""
import numpy as np
from pytest_quantum import (
    assert_circuit_sweep_states,
    assert_vqe_converges,
    assert_ground_state_energy_close,
)


def test_ry_sweep(aer_simulator):
    from qiskit.circuit import Parameter, QuantumCircuit

    theta = Parameter("θ")
    qc = QuantumCircuit(1); qc.ry(theta, 0)

    # θ=0 → |0⟩, θ=π → |1⟩
    assert_circuit_sweep_states(
        qc,
        param_values=[{theta: 0}, {theta: np.pi}],
        expected_states=[np.array([1, 0]), np.array([0, 1])],
        backend=aer_simulator,
        atol=0.01,
    )


def build_h2_ansatz():
    from qiskit.circuit import Parameter, QuantumCircuit
    theta = Parameter("θ")
    qc = QuantumCircuit(2)
    qc.x(0); qc.ry(theta, 1); qc.cx(1, 0)
    return qc, theta


def test_h2_vqe_converges(qiskit_estimator):
    from qiskit.quantum_info import SparsePauliOp
    H = SparsePauliOp.from_list([("ZZ", 0.5), ("XX", -0.5)])

    def run_vqe():
        from scipy.optimize import minimize
        qc, theta = build_h2_ansatz()

        def cost(params):
            bound = qc.assign_parameters({theta: params[0]})
            result = qiskit_estimator.run([(bound, H)]).result()
            return float(result[0].data.evs)

        res = minimize(cost, x0=[0.0], method="COBYLA")
        return res.fun

    energy = run_vqe()
    assert_ground_state_energy_close(energy, expected_energy=-1.0, atol=0.1)

Recipe 6: Multi-framework parametrised test

"""test_multi_fw.py — run the same test on every installed framework."""
import pytest
from pytest_quantum import assert_unitary
import numpy as np


@pytest.mark.quantum_backends("qiskit", "cirq", "pennylane")
def test_hadamard_gate(quantum_backend_name):
    H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)

    if quantum_backend_name == "qiskit":
        from qiskit import QuantumCircuit
        qc = QuantumCircuit(1); qc.h(0)
        assert_unitary(qc, H)

    elif quantum_backend_name == "cirq":
        import cirq
        q = cirq.LineQubit.range(1)
        circuit = cirq.Circuit(cirq.H(q[0]))
        assert_unitary(circuit, H)

    elif quantum_backend_name == "pennylane":
        import pennylane as qml
        import numpy as np

        @qml.qnode(qml.device("default.qubit", wires=1))
        def circuit():
            qml.Hadamard(0)
            return qml.state()

        sv = np.array(circuit())
        expected = H[:, 0]  # H|0⟩ = |+⟩
        from pytest_quantum import assert_state_fidelity_above
        assert_state_fidelity_above(sv, expected, threshold=0.999)

Recipe 7: QEC with Stim

"""test_qec.py — validate a quantum error correcting code."""
from pytest_quantum import (
    assert_stim_logical_error_rate_below,
    assert_stabilizer_state,
)


def test_repetition_code():
    import stim

    # Distance-3 repetition code, 3 rounds
    code = stim.Circuit.generated(
        "repetition_code:memory",
        distance=3,
        rounds=3,
        after_clifford_depolarization=0.001,
    )
    # Physical error rate 0.1% → logical error rate should be < 0.01%
    assert_stim_logical_error_rate_below(code, max_error_rate=0.0001, shots=100_000)


def test_bell_stabilizers():
    import numpy as np
    # Bell state |Φ+⟩ = (|00⟩+|11⟩)/√2 is stabilised by XX and ZZ
    sv = np.array([1, 0, 0, 1]) / np.sqrt(2)
    assert_stabilizer_state(sv, stabilizers=["XX", "ZZ"])

Recipe 8: Randomised benchmarking

"""test_rb.py — characterise gate quality via RB."""
from pytest_quantum import assert_randomized_benchmarking, assert_quantum_volume


def test_1q_clifford_fidelity(aer_simulator):
    result = assert_randomized_benchmarking(
        aer_simulator,
        qubit=0,
        clifford_lengths=[1, 10, 20, 50, 100],
        num_sequences=20,
        shots=1024,
        min_fidelity_per_clifford=0.999,
    )
    print(f"Average gate fidelity: {result['fidelity']:.5f}")
    print(f"Decay parameter p: {result['decay_rate']:.5f}")


def test_qv4(aer_simulator):
    qv = assert_quantum_volume(
        aer_simulator,
        target_qv=4,
        num_trials=20,
        shots=1024,
    )
    assert qv >= 4

Recipe 9: Error mitigation with Mitiq

"""test_mitiq.py — verify ZNE improves expectation values."""
pytest.importorskip("mitiq", reason="mitiq not installed")

from pytest_quantum import assert_zne_reduces_error, assert_zne_expectation_close


def test_zne_on_bell(aer_noise_simulator):
    from qiskit import QuantumCircuit
    import numpy as np

    noisy_sim = aer_noise_simulator(error_rate=0.02)
    qc = QuantumCircuit(2); qc.h(0); qc.cx(0, 1)

    def executor(circuit):
        from qiskit import transpile
        transpiled = transpile(circuit, noisy_sim, optimization_level=0)
        result = noisy_sim.run(transpiled, shots=1024).result()
        counts = result.get_counts()
        total = sum(counts.values())
        return (counts.get("00", 0) + counts.get("11", 0)) / total - 0.5

    assert_zne_expectation_close(
        qc, executor, expected=0.5, atol=0.1,
    )

Recipe 10: Real hardware test (IBM Quantum)

"""test_hardware.py — run on real IBM Quantum hardware."""
import pytest
from pytest_quantum import (
    assert_circuit_fits_backend,
    assert_backend_calibration,
    assert_measurement_distribution,
    assert_real_counts_close,
)


@pytest.mark.quantum_real
def test_bell_on_ibm(ibm_backend):
    from qiskit import QuantumCircuit, transpile
    from qiskit_ibm_runtime import SamplerV2

    qc = QuantumCircuit(2)
    qc.h(0); qc.cx(0, 1)

    # Static check: does the circuit fit the backend?
    assert_circuit_fits_backend(qc, ibm_backend)
    assert_backend_calibration(ibm_backend, min_t1_us=20.0)

    # Run on real hardware
    transpiled = transpile(qc, ibm_backend, optimization_level=3)
    transpiled.measure_all()

    sampler = SamplerV2(ibm_backend)
    job = sampler.run([transpiled], shots=1024)

    # Hardware is noisy — use wider tolerance
    counts = dict(job.result()[0].data.meas.get_counts())
    assert_measurement_distribution(
        counts,
        expected_probs={"00": 0.5, "11": 0.5},
        significance=0.001,   # looser for hardware noise
    )

Recipe 11: Shot budget enforcement

"""test_budget.py — cap total shots in an integration test."""
from pytest_quantum import assert_measurement_distribution


def test_budget_enforced(aer_simulator, shot_budget):
    budget = shot_budget(max_shots=5000)

    for gate in ["h", "x", "y", "z"]:
        shots = budget.allocate(1000)
        counts = run_single_qubit_gate(aer_simulator, gate, shots=shots)
        assert_measurement_distribution(counts, expected_probs_for(gate))

    assert budget.remaining == 1000
    print(f"Used {budget.used} / {budget.max_shots} shots")