Dynamic Quantum Circuits: Mid-Circuit Measurement and Classical Feedforward in Qiskit
Implement quantum teleportation using Qiskit's dynamic circuit primitives: mid-circuit measurement, classical feedforward with if_else, and comparison to the static circuit approach. Includes repeat-until-success gates.
Circuit diagrams
Traditional quantum circuits are static: all gates are determined before execution begins, and measurement only happens at the end. This model is convenient for simulation and programming, but it leaves a significant capability on the table. Real quantum hardware can measure individual qubits mid-circuit and immediately feed those classical results back into subsequent quantum operations, all within a single circuit execution and without exiting the coherence window.
This capability, called dynamic circuits or real-time classical feedforward, unlocks genuinely new protocols. Quantum teleportation requires it. Active quantum error correction requires it. Repeat-until-success gates use it to implement non-Clifford operations more resource-efficiently than magic state distillation at small scales.
What Makes a Circuit Dynamic
A static circuit applies gates in a fixed sequence. A dynamic circuit branches: the gate sequence applied to qubits later in the circuit depends on the outcomes of measurements taken earlier. The runtime must resolve the classical computation and apply the corrective gate before the remaining quantum state decoheres.
IBM Quantum hardware has supported real-time feedforward since 2022, with classical control latency low enough to act within coherence times. The control systems pipeline uses dedicated classical processors (FPGAs) co-located with the QPU to resolve conditions in tens of nanoseconds.
In Qiskit, dynamic circuits use the if_else and while_loop control flow instructions, along with measure inside circuits that contain subsequent quantum operations.
Quantum Teleportation: The Static Circuit Approach
Quantum teleportation transfers an unknown qubit state from Alice to Bob using a pre-shared Bell pair and classical communication. In the static (deferred measurement) formulation, all measurements happen at the end:
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
def static_teleportation():
"""
Static (deferred measurement) teleportation circuit.
Qubit layout: q[0] = message qubit, q[1] = Alice's Bell qubit,
q[2] = Bob's Bell qubit
"""
q = QuantumRegister(3, 'q')
c = ClassicalRegister(3, 'c')
qc = QuantumCircuit(q, c)
# --- Prepare the message qubit in an arbitrary state ---
# (Using |+> = H|0> as a concrete example)
qc.h(q[0])
qc.barrier(label="msg ready")
# --- Create Bell pair between Alice (q[1]) and Bob (q[2]) ---
qc.h(q[1])
qc.cx(q[1], q[2])
qc.barrier(label="bell pair")
# --- Alice's teleportation operations ---
qc.cx(q[0], q[1]) # entangle message with Alice's qubit
qc.h(q[0]) # rotate message qubit
# --- Static version: corrections applied before measurement ---
# Using CNOT and CZ controlled on message and Alice's qubits
qc.cx(q[1], q[2]) # conditional X correction
qc.cz(q[0], q[2]) # conditional Z correction
# --- Measure all qubits ---
qc.measure(q, c)
return qc
qc_static = static_teleportation()
print("Static teleportation circuit:")
print(qc_static.draw(output='text'))
This works correctly in simulation, but it requires long-range two-qubit gates between Alice’s and Bob’s qubits. In a real network scenario where Alice and Bob are physically separated, those long-range gates are impossible. Classical communication plus local corrections is the only option.
Quantum Teleportation: Dynamic Circuit Implementation
The dynamic version mirrors the physical protocol precisely. Alice measures her two qubits, sends 2 classical bits to Bob, and Bob applies corrections based on those bits:
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
def dynamic_teleportation():
"""
Dynamic teleportation circuit with mid-circuit measurement
and classical feedforward via if_else.
"""
q = QuantumRegister(3, 'q')
alice_bits = ClassicalRegister(2, 'alice') # Alice's measurement outcomes
bob_bit = ClassicalRegister(1, 'bob') # Bob's final measurement
qc = QuantumCircuit(q, alice_bits, bob_bit)
# --- Prepare message qubit in |+> ---
qc.h(q[0])
qc.barrier(label="msg")
# --- Create Bell pair between Alice and Bob ---
qc.h(q[1])
qc.cx(q[1], q[2])
qc.barrier(label="bell pair")
# --- Alice's local operations ---
qc.cx(q[0], q[1])
qc.h(q[0])
qc.barrier(label="alice ops")
# --- Mid-circuit measurement of Alice's qubits ---
qc.measure(q[0], alice_bits[0]) # measure message qubit -> bit 0
qc.measure(q[1], alice_bits[1]) # measure Alice's Bell qubit -> bit 1
qc.barrier(label="alice measures")
# --- Classical feedforward: Bob applies corrections ---
# If alice_bits[1] == 1: apply X to Bob's qubit
with qc.if_test((alice_bits[1], 1)):
qc.x(q[2])
# If alice_bits[0] == 1: apply Z to Bob's qubit
with qc.if_test((alice_bits[0], 1)):
qc.z(q[2])
qc.barrier(label="bob corrects")
# --- Bob measures to verify the state was teleported ---
qc.measure(q[2], bob_bit[0])
return qc
qc_dynamic = dynamic_teleportation()
print("Dynamic teleportation circuit:")
print(qc_dynamic.draw(output='text', fold=90))
The key difference: qc.measure() followed by qc.if_test() creates a real dependency. The simulator and IBM Quantum hardware execute the X and Z gates on q[2] only after the classical measurement outcomes are known. No long-range two-qubit gate between q[0]/q[1] and q[2] is required.
Simulating and Verifying the Protocol
from qiskit_aer import AerSimulator
import numpy as np
def verify_teleportation(n_angles=8, shots=2048):
"""
Verify teleportation fidelity by teleporting states at different
Bloch sphere angles and checking the output.
"""
sim = AerSimulator()
results = {}
for angle_idx in range(n_angles):
theta = angle_idx * np.pi / n_angles
q = QuantumRegister(3, 'q')
alice_bits = ClassicalRegister(2, 'alice')
bob_bit = ClassicalRegister(1, 'bob')
qc = QuantumCircuit(q, alice_bits, bob_bit)
# Prepare message qubit: Ry(theta)|0>
qc.ry(theta, q[0])
# Bell pair
qc.h(q[1])
qc.cx(q[1], q[2])
# Alice's operations + measurement
qc.cx(q[0], q[1])
qc.h(q[0])
qc.measure(q[0], alice_bits[0])
qc.measure(q[1], alice_bits[1])
# Bob's feedforward corrections
with qc.if_test((alice_bits[1], 1)):
qc.x(q[2])
with qc.if_test((alice_bits[0], 1)):
qc.z(q[2])
qc.measure(q[2], bob_bit[0])
result = sim.run(qc, shots=shots).result()
counts = result.get_counts()
# Expected probability of measuring |0> in Bob's qubit
p_zero_expected = np.cos(theta / 2) ** 2
# Count Bob's |0> outcomes
p_zero_measured = sum(
count for outcome, count in counts.items()
if outcome.split(' ')[0] == '0'
) / shots
results[f"theta={theta:.2f}"] = {
'expected_p0': round(p_zero_expected, 4),
'measured_p0': round(p_zero_measured, 4),
'error': round(abs(p_zero_expected - p_zero_measured), 4),
}
print(f"{'Angle':>15} {'Expected P(0)':>15} {'Measured P(0)':>15} {'Error':>10}")
print("-" * 58)
for label, data in results.items():
print(f"{label:>15} {data['expected_p0']:>15.4f} "
f"{data['measured_p0']:>15.4f} {data['error']:>10.4f}")
verify_teleportation()
Repeat-Until-Success Gates
Dynamic circuits make a new primitive possible: the repeat-until-success (RUS) gate. Some non-Clifford gates can be implemented probabilistically using only Clifford gates and ancilla measurements. If the ancilla outcome is “success”, the target qubit is in the desired state. If not, apply a known correction and try again. Classically, you loop until the ancilla signals success.
def repeat_until_success_T_approx(max_attempts=5):
"""
Illustrative repeat-until-success circuit structure.
Implements an approximate T gate using RUS logic.
This is a schematic; real RUS circuits require specific ancilla states.
"""
target = QuantumRegister(1, 'target')
anc = QuantumRegister(1, 'anc')
flag = ClassicalRegister(1, 'flag')
out = ClassicalRegister(1, 'out')
qc = QuantumCircuit(target, anc, flag, out)
# Prepare target in |+>
qc.h(target[0])
# First attempt
qc.h(anc[0])
qc.t(anc[0])
qc.cx(target[0], anc[0])
qc.measure(anc[0], flag[0])
# If ancilla measured 1 (failure), apply correction and retry
with qc.if_test((flag[0], 1)):
qc.x(target[0]) # Pauli correction
# A real RUS circuit would retry the gate here
# Qiskit while_loop handles the retry logic
qc.measure(target[0], out[0])
return qc
qc_rus = repeat_until_success_T_approx()
print("\nRepeat-Until-Success circuit structure:")
print(qc_rus.draw(output='text'))
The practical advantage of RUS over magic state distillation is that for small systems (1-10 qubits), RUS requires fewer resources and can be faster. The expected number of attempts before success is typically 2-5 for well-designed protocols. For large-scale fault-tolerant computation, magic state distillation remains dominant, but RUS is valuable for NISQ and early fault-tolerant demonstrations.
Running on IBM Quantum Hardware
Dynamic circuits on IBM Quantum require the hardware to support real-time classical control, available on Heron and Eagle processors via the IfElseOp primitive:
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Connect to IBM Quantum (requires token)
# service = QiskitRuntimeService()
# backend = service.least_busy(simulator=False, operational=True, dynamic_circuits=True)
# Transpile the dynamic teleportation circuit
# pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
# qc_transpiled = pm.run(qc_dynamic)
# Run via Sampler primitive
# sampler = Sampler(backend)
# job = sampler.run([qc_transpiled], shots=1024)
# result = job.result()
# print(result[0].data.bob.get_counts())
The dynamic_circuits=True filter in least_busy() is important: not all IBM backends support feedforward. As of 2025, Heron r2 processors on IBM Quantum have sub-microsecond feedforward latency, enabling genuine real-time correction within qubit coherence times of 100-500 microseconds.
Dynamic circuits are one of the clearest demonstrations that quantum hardware is evolving beyond the NISQ model toward programmable, interactive quantum systems where classical and quantum computation interleave in real time.
Was this tutorial helpful?