Dynamic circuits: if/else, loops and conditions
A dynamic circuit measures some qubits in the middle and lets the classical result decide what happens next: run a correction, repeat a block until it succeeds, pick one of several branches. OpenQASM 3 writes this with if, else, while, for and switch; Qiskit writes it with `with qc.if_test(...)` blocks. QubitLab draws every one of them in the composer, so you can build them from the palette, read the condition on the frame, and jump to the matching line of QASM or Python.
Why measure in the middle?
Teleportation, error correction, repeat-until-success state preparation and many hardware-efficient algorithms all need feed-forward: a measurement outcome that is used while the circuit is still running. Without control flow you would have to defer every measurement to the end and replace the classical decision with extra quantum gates, which costs qubits and depth.
if / else in OpenQASM 3
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[0];
h q[1];
c[0] = measure q[0];
c[1] = measure q[1];
if ((c[0] ^ c[1]) == true) {
x q[2];
} else {
z q[2];
}
c[2] = measure q[2];| Condition | Meaning |
|---|---|
| if (c[0]) | bit 0 is 1 |
| if (c[0] == false) · if (!c[0]) | bit 0 is 0 |
| if (c == 3) | the whole register reads 3 (c[0] is the least-significant bit, so c[0] = c[1] = 1) |
| if (c[0] && c[1]) · if (c[0] || c[1]) | both bits set · at least one set |
| if ((c[0] ^ c[1]) == true) | exactly one bit set (parity) |
| if (c[0] != c[1]) | the bits differ |
| } else if (c[1]) { | chained branches |
Qiskit itself only imports `bit`, `!bit`, `bit == true/false` and `register == int`. QubitLab rewrites every other condition into nested single-bit tests before Qiskit sees it, so you can write the natural form and still simulate, compile and export to Python.
The same circuit in Qiskit (Python)
from qiskit import QuantumCircuit
from qiskit.circuit.classical import expr
qc = QuantumCircuit(3, 3)
qc.h(0)
qc.h(1)
qc.measure(0, 0)
qc.measure(1, 1)
with qc.if_test(expr.bit_xor(c[0], c[1])) as else_:
qc.x(2)
with else_:
qc.z(2)
qc.measure(2, 2)
# single bit: with qc.if_test((c[0], 1)):
# whole register: with qc.if_test((c, 3)):
# combined bits: expr.logic_and(c[0], expr.logic_not(c[1]))Loops: while, for, break
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
c[0] = measure q[0];
while (c[0] == true) { // repeat until q0 measures 0
x q[0];
h q[0];
c[0] = measure q[0];
}
for uint i in [0:1] { // constant loops are unrolled: h q[0]; h q[1];
h q[i];
}
for uint k in [0:3] { // loops with break stay loops
c[1] = measure q[1];
if (c[1]) { break; }
x q[1];
}- while repeats its body as long as the condition holds; measure inside the body so it can end.
- for with literal bounds is unrolled (the body is copied once per value, indices like q[(i+1)%3] are folded), so statevector tools still work on it. A for loop that contains break or continue is kept as a real loop and runs only on the shot-based simulator.
- switch (c) { case 0 { … } case 1, 2 { … } default { … } } picks one arm by the register value; Qiskit: with qc.switch(c) as case: / with case(0): / with case(case.DEFAULT):.
Building them in the composer
- Palette → Control flow: tap if, if / else, while, switch, break or continue, then tap the column where the block should start (or drag it there). The frame appears across all wires.
- Drag gates into the frame (or tap a gate, then a cell inside it). The frame shrinks to the wires it uses.
- Click the tag on the frame to edit the condition below the circuit; Unwrap keeps the gates and removes the frame; Delete removes both.
- Double-click any gate or frame tag to select its statement in the QASM or Python panel. Code typed in the panel is drawn back the same way.
- Pasted programs with several registers (mid_measurement, final_measurement, psi …) show a Rewrite for the composer button that merges them into one q and one c register with bit order kept.
Where dynamic circuits run
| Tool | Dynamic circuits |
|---|---|
| Simulate | Yes. The ideal reference is sampled with 20 000 shots instead of a statevector. |
| Compile / Compare levels | Yes; control flow passes through the transpiler. |
| Hardware | if/else runs on IBM Heron machines; check the backend's dynamic-circuit support for loops. |
| State · Trace · Debug | No: a branch has no single statevector. They say so and point you to Simulate. |
Parity branch
Open this circuit, run Simulate and read the probabilities. Then change the condition to c[0] && c[1] and predict how often q2 is flipped before running again.
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[0];
h q[1];
c[0] = measure q[0];
c[1] = measure q[1];
if ((c[0] ^ c[1]) == true) {
x q[2];
} else {
z q[2];
}
c[2] = measure q[2];
Hint
The two H gates make all four values of c[1]c[0] equally likely.
Check your answer
With XOR, c[2] = 1 for 01 and 10: half the shots. With AND only 11 flips q2: a quarter of the shots.
Repeat until success
Build the while loop from the Loops section (or open the example “Repeat until success”). What does c read at the end, and why is it always the same?
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] c;
h q[0];
c[0] = measure q[0];
while (c[0] == true) {
x q[0];
h q[0];
c[0] = measure q[0];
}
x q[1];
c[1] = measure q[1];
Hint
The loop only exits when c[0] is 0.
Check your answer
c = 10 in every shot: c[0] must be 0 to leave the loop and q1 is always flipped afterwards. The number of loop rounds is random, the result is not.