Not all quantum programs fit the exact gate set that @coherent requires. When you need arbitrary rotation angles — like rx(0.785, q) — use @parametric. When your program needs to measure qubits and branch on the results, use @adaptive. Both decorators produce an IRProgram via .build(), the broad-IR compilation path.
@parametric: coherent programs with rotations
@parametric marks a function as a coherent (unitary) program that may use rotation gates with arbitrary float angles. The gate set includes everything in @coherent plus rx, ry, rz, cry, and crz.
from b01t import parametric, QReg, h, rx, rz, cx
@parametric
def variational_layer(sys: QReg, theta: float, phi: float):
h(sys[0])
rx(theta, sys[0])
rz(phi, sys[1])
cx(sys[0], sys[1])
Compile it to an IRProgram with .build(), passing register specs for the QReg parameters. Classical parameters like theta and phi are regular Python values — you pass them at build time as additional arguments after the register specs.
import math
ir = variational_layer.build(("sys", 2), math.pi / 4, math.pi / 2)
print(ir.name, ir.effect) # variational_layer coherent
@parametric and @coherent both produce unitary programs, but @coherent uses the exact gate set and provides a machine-checked safety certification. Use @parametric when you need rotation angles; use @coherent when you want the strongest correctness guarantees.
@adaptive: measurement and classical feedback
@adaptive marks a function as a program that may measure qubits and branch on classical results. Use measure(q) to measure a single qubit or measure_all(reg) to measure all qubits in a register. Use if_then() from b01t.kit to branch on a measurement result.
from b01t import adaptive, QReg, h, x, measure_all
from b01t.kit import if_then
@adaptive
def teleport_correct(sys: QReg, ancilla: QReg):
# ... Bell pair preparation and Bell measurement omitted ...
result = measure_all(sys)
if_then(
result,
lambda: x(ancilla[0]), # then: apply correction
)
Build an @adaptive function the same way — with .build() and register specs:
ir = my_adaptive_fn.build(("sys", 2), ("ancilla", 1))
@adaptive functions cannot be called from inside a @coherent or @parametric function. Adaptive programs break unitarity by branching on measurement outcomes. If you try it, DSLValidationError is raised: coherent functions cannot call adaptive functions.
Control flow
The b01t.kit module provides three control-flow helpers for use inside @parametric and @adaptive functions.
repeat(count, body)
Repeat a body lambda a fixed number of times at build time. Useful for Grover iterations, variational circuit layers, or any fixed-repetition pattern.
from b01t import parametric, QReg, h, cx
from b01t.kit import repeat
@parametric
def layer_stack(sys: QReg):
repeat(3, lambda: (h(sys[0]), cx(sys[0], sys[1])))
for_each(data, body)
Iterate over classical data at build time, emitting parameterized gates for each element. The body receives an index and the current data element.
from b01t import parametric, QReg, rz
from b01t.kit import for_each
@parametric
def angle_sweep(sys: QReg, angles: list):
for_each(angles, lambda i, theta: rz(theta, sys[i % sys.size]))
if_then(cond, then_body, else_body)
Branch on a classical condition — typically a measurement result from an earlier measure() or measure_all() call. Only valid in @adaptive functions.
from b01t import adaptive, QReg, h, x, z, measure_all
from b01t.kit import if_then
@adaptive
def measure_and_correct(sys: QReg):
h(sys[0])
result = measure_all(sys)
if_then(
result,
lambda: x(sys[0]), # then: flip
lambda: z(sys[0]), # else: phase flip
)
if_then() raises DSLValidationError if called inside a @coherent function. Classical branching is not allowed in unitary programs.
Complete example: Grover search
The Grover search demo combines @parametric and @adaptive. The oracle and diffusion are @parametric (they are unitary subroutines), and the outer search loop is @adaptive (it measures at the end).
from b01t import parametric, adaptive, QReg
from b01t import h, x, z, cx, ccx, cz, mcx, measure_all
from b01t import ancilla, compute, phase, uncompute
from b01t.kit import repeat
@parametric
def grover_step(sys: QReg):
"""One Grover iteration: oracle followed by diffusion."""
# Phase oracle for f(x) = x0 AND x1
with ancilla(1) as anc:
compute(lambda: ccx(sys[0], sys[1], anc[0]))
phase(lambda: z(anc[0]))
uncompute()
# Diffusion operator
for q in sys:
h(q)
x(q)
with ancilla(1) as anc:
compute(lambda: mcx(sys.wires()[:-1], anc[0]))
phase(lambda: cz(anc[0], sys[-1]))
uncompute()
for q in sys:
x(q)
h(q)
@adaptive
def grover_search(sys: QReg):
"""Full Grover search: initialize, iterate, measure."""
for q in sys:
h(q)
repeat(2, lambda: grover_step(sys))
return measure_all(sys)
# Build and inspect the IR
ir = grover_search.build(("sys", 2))
print(ir.effect) # adaptive
print(ir.is_safe) # False — adaptive programs are not certified safe
Calling @parametric from @adaptive
@parametric functions are unitary subroutines, so they can be called freely from inside @adaptive functions. The Grover example above shows this pattern: grover_step is @parametric and is called inside grover_search which is @adaptive.
The repeat() helper works in both @parametric and @adaptive contexts.
When to use which decorator
| @coherent | @parametric | @adaptive |
|---|
| Arbitrary rotation angles | No | Yes | Yes |
| Measurement | No | No | Yes |
Classical branching (if_then) | No | No | Yes |
| Safety certification | SAFE | No | No |
Can be called from @coherent | Yes | No | No |
| Build method | .build_exact() | .build() | .build() |
| Returns | ExactProgram | IRProgram | IRProgram |