Qiskit Intermediate Free 7/61 in series 17 min read

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.

What you'll learn

  • dynamic circuits
  • mid-circuit measurement
  • classical feedforward
  • quantum teleportation
  • if_else

Prerequisites

  • Python proficiency
  • Beginner quantum computing concepts (superposition, entanglement)
  • Linear algebra basics

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?