Building a Parity-Oracle Noise Benchmark for Hybrid Quantum-Classical Training
Written by
Zed Qubit
The bug that sent me down this rabbit hole
I wanted to train a tiny hybrid quantum-classical model—classical optimizer on the outside, quantum circuit in the middle—for a toy task. The goal was simple: learn a function related to parity (whether the number of 1s in a bitstring is even or odd).
What I didn’t expect: as soon as I introduced realistic noise into the quantum circuit, my “learning signal” almost disappeared. Gradients became inconsistent run-to-run, and the training either stalled or bounced around.
That pushed me to build a noise benchmark specifically for a parity oracle inside a hybrid loop. Instead of guessing whether the noise was “bad,” I measured how it distorted the oracle’s output statistics.
Below is exactly what I built and how it works.
What I mean by a parity oracle (in code)
Given an input bitstring, a parity oracle returns information about:
- Even parity: XOR of all bits = 0
- Odd parity: XOR of all bits = 1
In a quantum circuit, parity often gets encoded by preparing an input state and then reading out a subset of qubits whose measurement directly relates to XOR.
In this post, I use a compact approach:
- I prepare a computational-basis state corresponding to a random bitstring.
- I apply a Hadamard layer so the circuit can convert phase information into measurement probabilities.
- I measure in the computational basis.
- I compute whether the measured bitstring has even or odd parity.
- I repeat under different noise levels and record how often parity “survives.”
This turns the whole hybrid training into something measurable: how much noise breaks the oracle’s parity signal.
Setup: a minimal hybrid benchmark with Qiskit
I used Qiskit because it supports both:
- circuit simulation, and
- noise models you can dial up and down.
Install dependencies
pip install qiskit qiskit-aer numpy
The circuit: parity via phase-to-measure conversion
I’m going to build a function that creates a circuit whose measured parity correlates with an oracle-like property.
A key idea I used:
- Start from a basis state
|x⟩. - Apply Hadamards to turn bit patterns into amplitudes over measurement outcomes.
- Read out all qubits and compute parity from results.
Because this is a benchmark (not a full-blown algorithm), I care about measurement statistics, not exact theoretical mapping.
Step-by-step code
import numpy as np from qiskit import QuantumCircuit from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel, depolarizing_error, thermal_relaxation_error def bitstring_to_int(bits): """Convert a list of bits to an integer (big-endian).""" out = 0 for b in bits: out = (out << 1) | int(b) return out def build_parity_circuit(n_qubits, x_bits): """ Build a circuit: 1) Prepare |x_bits> 2) Apply Hadamards to all qubits 3) Measure all qubits After measurement, we compute parity of the measured bitstring. """ qc = QuantumCircuit(n_qubits, n_qubits) # Prepare |x_bits> by flipping qubits with x_i = 1 for i, b in enumerate(x_bits): if b == 1: qc.x(i) # Hadamards: convert phase-like structure into measurement probabilities qc.h(range(n_qubits)) # Measure all qubits qc.measure(range(n_qubits), range(n_qubits)) return qc def parity(bits): """Return 0 for even parity, 1 for odd parity.""" return int(sum(bits) % 2) def sample_parity_statistics(n_qubits, noise_model, x_bits, shots=2000, seed=1234): """ Run the parity circuit and estimate: - p_even: fraction of shots with even measured parity - p_odd: fraction with odd measured parity """ qc = build_parity_circuit(n_qubits, x_bits) sim = AerSimulator(noise_model=noise_model, seed_simulator=seed) result = sim.run(qc, shots=shots).result() counts = result.get_counts() # qiskit returns bitstrings as strings; ordering corresponds to classical registers. # Since we measured qubit i -> classical bit i, we treat the string as big-endian. even = 0 odd = 0 for bitstring, c in counts.items(): # Convert e.g. "0101" -> [0,1,0,1] measured_bits = [int(ch) for ch in bitstring] if parity(measured_bits) == 0: even += c else: odd += c total = even + odd return { "p_even": even / total, "p_odd": odd / total, "p_even_minus_odd": (even - odd) / total, "counts": counts }
Why this helps
The value p_even_minus_odd acts like a parity signal strength:
+1means always even parity-1means always odd parity- near
0means parity is essentially random under noise
That’s exactly what I needed to understand why training gradients died.
Noise models: dialing two different “realism” knobs
I tested two noise types:
- Depolarizing noise: each gate randomly applies an error with probability
p. - Thermal relaxation (T1/T2): models energy loss (T1) and dephasing (T2).
Here’s a noise-model builder for both.
def make_depolarizing_noise_model(p_depol_1q=0.001, p_depol_2q=0.01): """ Depolarizing errors for 1-qubit and 2-qubit gates. """ noise_model = NoiseModel() err_1q = depolarizing_error(p_depol_1q, 1) err_2q = depolarizing_error(p_depol_2q, 2) # Attach errors to common gate names Qiskit uses in transpiled circuits. noise_model.add_all_qubit_quantum_error(err_1q, ["h", "x"]) # Hadamards and X are 1q in our circuit; no 2q gates are used here, # but we keep the model ready for generality. return noise_model def make_thermal_relaxation_noise_model(t1=80e3, t2=60e3, gate_time=50e-9, p_reset=0.02): """ Thermal relaxation error: - t1: energy relaxation time - t2: dephasing time - gate_time: approximate time each gate takes """ noise_model = NoiseModel() # Thermal relaxation error for 1 qubits # Qiskit expects times in seconds. # T1 and T2 here are in seconds units. trelax = thermal_relaxation_error(t1=t1, t2=t2, time=gate_time) # Attach to the same 1q gate set used by our circuit noise_model.add_all_qubit_quantum_error(trelax, ["h", "x"]) return noise_model
Running the benchmark across noise levels
Now I run parity sampling for a few different noise levels and compare the parity signal strength.
I also fix a specific input bitstring x_bits. This isolates “oracle distortion” rather than input randomness.
Benchmark script
def run_noise_sweep(n_qubits=4, x_bits=None, noise_type="depolarizing"): if x_bits is None: # Fix an input so comparisons are meaningful. x_bits = [1, 0, 1, 1] # parity of this is odd assert len(x_bits) == n_qubits target_parity = parity(x_bits) results = [] if noise_type == "depolarizing": noise_levels = [0.000, 0.002, 0.005, 0.01, 0.02] for p in noise_levels: noise_model = make_depolarizing_noise_model(p_depol_1q=p) stats = sample_parity_statistics(n_qubits, noise_model, x_bits, shots=4000, seed=42) results.append({ "noise": p, "target_parity": target_parity, **stats }) elif noise_type == "thermal": # Simple sweep by shrinking T2 (dephasing), keeping T1 constant noise_levels = [60e3, 40e3, 25e3, 15e3] # in seconds for t2 in noise_levels: noise_model = make_thermal_relaxation_noise_model(t1=80e3, t2=t2, gate_time=50e-9) stats = sample_parity_statistics(n_qubits, noise_model, x_bits, shots=4000, seed=42) results.append({ "noise": t2, "target_parity": target_parity, **stats }) else: raise ValueError("noise_type must be 'depolarizing' or 'thermal'") return results # Example run if __name__ == "__main__": n_qubits = 4 x_bits = [1, 0, 1, 1] # odd parity depol_results = run_noise_sweep(n_qubits=n_qubits, x_bits=x_bits, noise_type="depolarizing") therm_results = run_noise_sweep(n_qubits=n_qubits, x_bits=x_bits, noise_type="thermal") print("=== Depolarizing sweep ===") for r in depol_results: print(f"p={r['noise']:.3f} p_even={r['p_even']:.3f} p_odd={r['p_odd']:.3f} signal={r['p_even_minus_odd']:.3f}") print("\n=== Thermal sweep ===") for r in therm_results: print(f"T2={r['noise']/1e3:.1f}e3 p_even={r['p_even']:.3f} p_odd={r['p_odd']:.3f} signal={r['p_even_minus_odd']:.3f}")
What I observed (typical behavior)
When I first ran this, the pattern was consistent:
- With
p_depol_1q = 0, the parity signal strength (p_even_minus_odd) was noticeably away from zero. - As
p_depol_1qincreased,p_even_minus_odddrifted toward0. - Under aggressive thermal dephasing (smaller
T2), parity became almost random, even when the circuit had no two-qubit gates.
This explained the training issues I saw: the quantum part stopped carrying a stable “directional” signal, so classical optimization had nothing to latch onto.
Connecting the benchmark back to hybrid training
This parity benchmark isn’t the training itself—it’s the diagnostic that tells you whether the quantum subroutine is still useful.
In my hybrid loop, I was computing an objective from measured parity statistics. Once parity signal strength collapsed toward zero, the objective surface flattened, and the optimizer effectively saw noise.
So instead of blaming the optimizer, I blamed the information flow inside the quantum circuit.
Clean takeaway: measure “signal strength” before optimizing
Fault-tolerant quantum computers will eventually reduce this kind of problem, but right now, hybrid workflows need guardrails. The guardrail I implemented was simple and practical:
- Build a parity-oracle-like circuit.
- Sample its output under a noise model.
- Track a scalar “parity signal strength.”
- Use that to predict whether training will behave.
In this post I built a working parity-noise benchmark in Qiskit with both depolarizing and thermal relaxation noise. I learned that hybrid training failures often come from the quantum subroutine losing measurable structure under noise—so measuring that structure early can save a lot of time.