Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Classical Control Flow Patterns

Tags: tutorial

Quantum circuits often have structure that depends on classical control flow: iterating over qubits, applying gates based on a graph’s edges, or choosing between gate sequences. Qamomile supports these patterns through qmc.range, qmc.items, if branching, and while loops.

This chapter covers:

# Install the latest Qamomile through pip!
# !pip install "qamomile[qiskit,visualization]"
import os

import qamomile.circuit as qmc
from qamomile.qiskit import QiskitTranspiler

transpiler = QiskitTranspiler()

qmc.range Loops

qmc.range may take start, stop, and step arguments. Here we create a qkernel that applies H to every other qubit and then entangles adjacent pairs with CX.

@qmc.qkernel
def hadamard_chain(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, name="q")

    # Apply H to every other qubit
    q[0::2] = qmc.h(q[0::2])

    # Entangle adjacent pairs
    for i in qmc.range(n - 1):
        q[i], q[i + 1] = qmc.cx(q[i], q[i + 1])

    return qmc.measure(q)
hadamard_chain.draw(n=5, fold_loops=False)
<Figure size 975.5x416 with 1 Axes>

qmc.items for Sparse Interaction Data

Many variational algorithms apply gates only on specific pairs of qubits, determined by a graph or interaction map. Rather than looping over all pairs, you can pass a dictionary of interactions and iterate with qmc.items().

The dictionary type uses Qamomile’s symbolic types: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float] — keys are qubit index pairs, values are interaction weights.

@qmc.qkernel
def sparse_coupling(
    n: qmc.UInt,
    edges: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float],
    gamma: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, name="q")

    # Initial superposition
    for i in qmc.range(n):
        q[i] = qmc.h(q[i])

    # Apply RZZ interactions only on specified edges
    for (i, j), weight in qmc.items(edges):
        q[i], q[j] = qmc.rzz(q[i], q[j], gamma * weight)

    return qmc.measure(q)

Dict Subscript Lookup (d[key])

Besides iterating with qmc.items(), a qmc.Dict can be indexed directly with d[key]. The most useful pattern is indexing one dict with the iteration keys of another: iterate over sparse interaction terms in one dict while looking up per-edge scale factors in a second dict.

@qmc.qkernel
def per_edge_angles(
    n: qmc.UInt,
    edges: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float],
    gammas: qmc.Dict[qmc.Tuple[qmc.UInt, qmc.UInt], qmc.Float],
) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, name="q")

    for i in qmc.range(n):
        q[i] = qmc.h(q[i])

    # Each edge gets its own angle, looked up by the same (i, j) key
    for (i, j), weight in qmc.items(edges):
        q[i], q[j] = qmc.rzz(q[i], q[j], weight * gammas[(i, j)])

    return qmc.measure(q)
edge_data = {(0, 1): 1.0, (1, 2): -0.7}
gamma_data = {(0, 1): 0.3, (1, 2): 0.5}

circuit = transpiler.to_circuit(
    per_edge_angles,
    bindings={"n": 3, "edges": edge_data, "gammas": gamma_data},
)
_rzz_angles = sorted(
    float(_instr.operation.params[0])
    for _instr in circuit.data
    if _instr.operation.name == "rzz"
)
# Each RZZ angle is weight * gamma for its own edge.
assert _rzz_angles == sorted([1.0 * 0.3, -0.7 * 0.5])

Inspecting with transpiler.to_circuit()

draw() does not yet support all patterns (particularly items with complex types, if, and while). In such cases, use transpiler.to_circuit() to see the concrete transpiled circuit after all parameters are bound.

edge_data = {(0, 1): 1.0, (1, 2): -0.7, (0, 2): 0.3}

circuit = transpiler.to_circuit(
    sparse_coupling,
    bindings={"n": 3, "edges": edge_data, "gamma": 0.4},
)
print(circuit)
assert circuit.num_qubits == 3
# n=3 -> 3 initial H + 3 measurements; len(edge_data)=3 -> exactly 3 RZZ.
_ops = {}
for _instr in circuit.data:
    _ops[_instr.operation.name] = _ops.get(_instr.operation.name, 0) + 1
assert _ops == {"h": 3, "rzz": 3, "measure": 3}
     ┌───┐                                    ┌─┐   
q_0: ┤ H ├─■─────────────────────■────────────┤M├───
     ├───┤ │ZZ(0.4)              │         ┌─┐└╥┘   
q_1: ┤ H ├─■─────────■───────────┼─────────┤M├─╫────
     ├───┤           │ZZ(-0.28)  │ZZ(0.12) └╥┘ ║ ┌─┐
q_2: ┤ H ├───────────■───────────■──────────╫──╫─┤M├
     └───┘                                  ║  ║ └╥┘
c: 3/═══════════════════════════════════════╩══╩══╩═
                                            1  0  2 

Only the three edges in edge_data produce RZZ gates — no wasted operations.

if Branching and while Loops

Qamomile supports mid-circuit measurement followed by classical branching. The condition must be a measurement result (Bit), not an argument of qkernels.

This maps directly to hardware-level conditional execution: measure a qubit, then decide what to do next based on the outcome.

if on a measurement result

A common pattern: measure one qubit and conditionally apply a gate to another qubit based on the outcome.

@qmc.qkernel
def conditional_flip() -> qmc.Bit:
    q0 = qmc.qubit("q0")
    q1 = qmc.qubit("q1")

    q0 = qmc.x(q0)  # Prepare |1⟩
    bit = qmc.measure(q0)

    # Conditionally flip q1 based on q0's measurement
    if bit:
        q1 = qmc.x(q1)
    else:
        pass

    return qmc.measure(q1)

This transpiles to a Qiskit if_else instruction and can be executed:

exe = transpiler.transpile(conditional_flip)
if os.environ.get("QAMOMILE_DOCS_TEST") == "1":
    print("Skipping dynamic-circuit execution in docs test mode.")
else:
    executor = transpiler.executor()
    job = exe.sample(executor, bindings={}, shots=100)
    result = job.result()
    for value, count in result.results:
        print(f"  bit={value}: {count} shots")
    # q0 prepared as |1>; the if-branch flips q1 to |1> on every shot.
    assert result.shots == 100
    assert result.results == [(1, 100)]
  bit=1: 100 shots

Since q0 is prepared as |1⟩, the measurement always yields 1, so q1 always gets flipped — every shot should return 1.

while on a measurement result

A while loop repeats until the measurement condition becomes false. This is useful for repeat-until-success protocols.

@qmc.qkernel
def repeat_until_zero() -> qmc.Bit:
    q = qmc.qubit("q")
    q = qmc.h(q)  # 50/50 chance of |0⟩ or |1⟩
    bit = qmc.measure(q)

    while bit:
        # Re-prepare and re-measure until we get 0. The register is a
        # body-local name: rebinding the outer `q` to a register
        # allocated in the body is rejected, because the runtime loop
        # re-executes its body on one persistent register without reset.
        q2 = qmc.qubit("q2")
        q2 = qmc.h(q2)
        bit = qmc.measure(q2)

    return bit

This transpiles to a Qiskit while_loop instruction. We can inspect the generated circuit structure:

exe_while = transpiler.transpile(repeat_until_zero)
qc_while = exe_while.compiled_quantum[0].circuit
print(qc_while)
assert qc_while.num_qubits == 2
# The `while bit:` lowers to a Qiskit `while_loop` instruction.
assert "while_loop" in {instr.operation.name for instr in qc_while.data}
     ┌───┐┌─┐                                  
q_0: ┤ H ├┤M├──────────────────────────────────
     └───┘└╥┘┌─────────      ┌───┐┌─┐ ───────┐ 
q_1: ──────╫─┤ While-0  ─|0>─┤ H ├┤M├  End-0 ├─
           ║ └────╥────      └───┘└╥┘ ───────┘ 
           ║ ┌────╨────┐           ║           
c: 1/══════╩═╡ c_0=0x1 ╞═══════════╩═══════════
           0 └─────────┘           0           

Combining if and while

You can combine both patterns. Here is a protocol that repeatedly measures and conditionally applies a correction gate:

@qmc.qkernel
def measure_and_correct() -> qmc.Bit:
    q0 = qmc.qubit("q0")
    q1 = qmc.qubit("q1")

    q0 = qmc.h(q0)
    bit = qmc.measure(q0)

    while bit:
        # If bit is 1, apply correction to q1
        if bit:
            q1 = qmc.x(q1)
        else:
            q1 = q1
        # Re-prepare and re-measure (body-local register name, as above)
        q0_retry = qmc.qubit("q0_retry")
        q0_retry = qmc.h(q0_retry)
        bit = qmc.measure(q0_retry)

    return qmc.measure(q1)
exe_combined = transpiler.transpile(measure_and_correct)
qc_combined = exe_combined.compiled_quantum[0].circuit
print(qc_combined)
assert qc_combined.num_qubits == 3
assert "while_loop" in {instr.operation.name for instr in qc_combined.data}
     ┌───┐┌─┐                                                            »
q_0: ┤ H ├┤M├────────────────────────────────────────────────────────────»
     └───┘└╥┘┌─────────        ┌──────  ┌───┐┌────────  ───────┐         »
q_1: ──────╫─┤          ───────┤ If-1  ─┤ X ├┤ Else-1    End-1 ├─────────»
           ║ │ While-0         └──╥───  └───┘└────────  ───────┘ ┌───┐┌─┐»
q_2: ──────╫─┤          ─|0>──────╫──────────────────────────────┤ H ├┤M├»
           ║ └────╥────           ║                              └───┘└╥┘»
           ║ ┌────╨────┐     ┌────╨────┐                               ║ »
c: 2/══════╩═╡ c_0=0x1 ╞═════╡ c_0=0x1 ╞═══════════════════════════════╩═»
           0 └─────────┘     └─────────┘                               0 »
«                  
«q_0: ─────────────
«      ───────┐ ┌─┐
«q_1:         ├─┤M├
«       End-0 │ └╥┘
«q_2:         ├──╫─
«      ───────┘  ║ 
«c: 2/═══════════╩═
«                1 

Summary

  • qmc.range(n) for looping over symbolic ranges.

  • qmc.items(dict) for iterating over sparse key-value data (edges, weights).

  • d[key] for looking up one dict by the iteration keys of another (per-edge coefficients or calibration scales).

  • if bit: and while bit: for branching on measurement results. Both branches must handle the same qubit handles (affine rule).

  • These control flow patterns transpile to native quantum SDK instructions (e.g., Qiskit if_else and while_loop).

Next: Reuse Patterns — helper qkernels, composite gates, and stub gates for top-down design.