Skip to main content
b01t provides three control flow combinators for structuring programs. repeat and for_each unroll at build time — they emit gates directly by calling your body function multiple times during the .build() call, so the resulting IR contains only flat gate sequences, not loops. if_then is different: it emits a genuine conditional branch in the IR and is only valid inside @adaptive programs. Import all three from b01t.
from b01t import repeat, for_each, if_then
repeat, for_each, and if_then are only valid inside a .build() call on a @parametric or @adaptive function. They are not available in exact programs (@coherent, @primitive).

repeat

repeat(count: int, body: Callable[[], None]) -> None
Calls body exactly count times at build time, unrolling the loop into the IR. Use it to apply the same gate pattern multiple times without writing it out manually.
count
int
required
Number of times to call body. Must be a non-negative integer known at build time.
body
Callable[[], None]
required
A zero-argument callable that emits one iteration of gates.
from b01t import parametric, QReg, h, rz, cx
from b01t import repeat
import math

@parametric
def power_of_two_layers(q: QReg) -> None:
    repeat(4, lambda: (h(q[0]), rz(math.pi / 8, q[0])))

prog = power_of_two_layers.build(("q", 1))
# prog.ops contains 8 GateOps (4 × h + 4 × rz)

for_each

for_each(data: Sequence[Any], body: Callable[[int, Any], None]) -> None
Iterates over data at build time, calling body(index, value) for each element. Use it to apply parametric gates driven by classical data without runtime overhead.
data
Sequence[Any]
required
A sequence of values to iterate over. The sequence is consumed entirely at build time.
body
Callable[[int, Any], None]
required
A callable that receives (index, value) and emits gates for that element.
from b01t import parametric, QReg, rz
from b01t import for_each
import math

angles = [math.pi / 4, math.pi / 3, math.pi / 6]

@parametric
def angle_sweep(q: QReg) -> None:
    for_each(angles, lambda i, theta: rz(theta, q[i % len(q)]))

prog = angle_sweep.build(("q", 3))
# Emits three rz gates with the three angles

if_then

if_then(
    cond: Any,
    then_body: Callable[[], None],
    else_body: Optional[Callable[[], None]] = None,
) -> None
Emits a conditional branch in the IR. Both then_body and else_body are captured as op sequences at build time and stored in an IfOp. At runtime (when lowered to hardware), the condition is evaluated and the appropriate branch is executed. if_then is only valid in @adaptive programs. Calling it inside a @coherent or @parametric function raises DSLValidationError.
cond
Any
required
The classical condition to branch on. Typically the result of a measure call (a string key like "m_q_0"). The condition is stored as-is in the IR and evaluated by the backend.
then_body
Callable[[], None]
required
A zero-argument callable that emits gates for the true branch.
else_body
Callable[[], None]
default:"None"
A zero-argument callable that emits gates for the false branch. If omitted, the false branch is empty.
from b01t import adaptive, QReg, h, x, measure
from b01t import if_then

@adaptive
def teleport_correction(ancilla: QReg, target: QReg) -> None:
    h(ancilla[0])
    m = measure(ancilla[0])
    if_then(
        m,
        lambda: x(target[0]),      # apply X if measurement = 1
        lambda: x(target[0]),      # else branch (here, same for illustration)
    )

prog = teleport_correction.build(("ancilla", 1), ("target", 1))
print(prog.effect)  # Effect.ADAPTIVE
With only a then branch:
from b01t import adaptive, QReg, h, z, measure
from b01t import if_then

@adaptive
def phase_correction(q: QReg, out: QReg) -> None:
    m = measure(q[0])
    if_then(m, lambda: z(out[0]))  # apply Z only when m is truthy

prog = phase_correction.build(("q", 1), ("out", 1))
The IfOp emitted by if_then is not supported by QiskitBackend in the current release. Use if_then to model the semantics in the IR; lowering to hardware requires a custom backend that handles classical feed-forward.