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")