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.

Parameterized Quantum Kernels

In Tutorial 01 we built qkernels with a fixed number of qubits. Qamomile allows you to treat values that determine circuit structure — such as the number of qubits and layers — as symbolic parameters. For instance, you can write a qkernel that contains n qubits and applies H gates to all of them, or one that applies a certain sequence of gates for p iterations. In Qamomile, parameters for circuit structure and those for rotation angles are required to be bound at different times: structure parameters must be bound at transpile time, while rotation angles must be bound at runtime.

This chapter teaches:

  • The typical roles of UInt and Float in qkernel inputs

  • qubit_array() and qmc.range() for parameterized circuits

  • The bind/sweep pattern: transpile once, execute many times

# Install the latest Qamomile through pip!
# !pip install qamomile

Typical Roles of UInt and Float

Qkernel parameters typically come in two flavors:

TypeTypical role
qmc.UIntCircuit structure (qubit count, number of iterations)
qmc.FloatGate parameters (rotation angles, weights)

In practice, UInt values that control qubit_array size or qmc.range bounds must be bound at transpile time, because the target quantum SDK needs a fixed circuit structure. Float values can stay as sweepable parameters.

The common pattern is: bind structure at transpile time, sweep gate parameters at execution time.

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

transpiler = QiskitTranspiler()

qubit_array and qmc.range

When the number of qubits depends on a parameter n, use qubit_array(n). To loop over the array, use qmc.range(n) instead of Python’s built-in range(). qmc.range takes start, stop, and step arguments just like Python’s range(). For instance, if you want to apply a gate to every other qubit, you can write for i in qmc.range(0, n, 2): ....

Why not Python range()?: At trace time, n is a symbol, not a Python integer — the qkernel body is traced to build an IR, and Python’s range() cannot iterate over a symbol. qmc.range() emits a loop node in the IR that the transpiler expands when n is bound to a concrete value.

@qmc.qkernel
def rotation_layer(n: qmc.UInt, theta: 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])
        q[i] = qmc.ry(q[i], theta)

    return qmc.measure(q)

draw() accepts keyword arguments that bind parameters to concrete values for visualization. Here n=4 fixes the circuit to 4 qubits and theta=0.3 provides a placeholder angle.

rotation_layer.draw(n=4, theta=0.3, fold_loops=False)
<Figure size 801.5x336 with 1 Axes>

Index-Based Updates

Notice the pattern: q[i] = qmc.h(q[i]). Qamomile treats quantum handles as affine — the accessed qubit is consumed, and the updated handle must be stored back in the same place.

Anti-pattern: iterating directly over the array. We intentionally raise an error for the for qi in q: pattern because it is incompatible with Qamomile’s affine enforcement.

try:

    @qmc.qkernel
    def bad_iteration(n: qmc.UInt) -> qmc.Vector[qmc.Qubit]:
        q = qmc.qubit_array(n, name="q")
        for qi in q:  # Direct iteration — bad!
            qi = qmc.h(qi)
        return q

    bad_iteration.draw(n=4)
except Exception as e:
    print(f"Error type: {type(e).__name__}")
    print(f"Error message: {e}")
Error type: SyntaxError
Error message: Direct iteration over sequences is not supported in @qkernel functions.
Line 4: 'for qi in q:'

Direct iteration cannot modify elements in-place, leading to silent bugs.
Use explicit index-based iteration instead:

  # Incorrect (current code):
  for qi in q:
      qi = qmc.operation(qi)

  # Correct:
  n = q.shape[0]
  for i in qmc.range(n):
      q[i] = qmc.operation(q[i])

Always use index-based access: for i in qmc.range(n): q[i] = qmc.h(q[i]).

The Bind/Sweep Pattern

This is the central workflow pattern for parameterized qkernels:

  1. Transpile once: bind structure parameters (n) and declare runtime parameters (theta) as sweepable.

  2. Execute many times: reuse the same transpiled executable with different runtime bindings.

This avoids re-transpiling the circuit for every parameter value.

# Transpile: fix n=4, keep theta as a sweepable parameter
exe = transpiler.transpile(
    rotation_layer,
    bindings={"n": 4},
    parameters=["theta"],
)

# Sweep: run the same executable with different theta values
for theta in [0.1, 0.5, 1.0]:
    result = exe.sample(
        transpiler.executor(),
        shots=128,
        bindings={"theta": theta},
    ).result()
    print(f"theta={theta:.1f} -> {result.results}")
theta=0.1 -> [((0, 1, 1, 1), 2), ((1, 0, 0, 1), 6), ((1, 1, 0, 1), 9), ((1, 1, 1, 1), 14), ((1, 0, 1, 1), 12), ((0, 0, 0, 0), 6), ((0, 1, 0, 0), 5), ((1, 1, 0, 0), 6), ((1, 0, 0, 0), 4), ((0, 1, 1, 0), 11), ((1, 0, 1, 0), 15), ((0, 1, 0, 1), 5), ((1, 1, 1, 0), 14), ((0, 0, 0, 1), 9), ((0, 0, 1, 0), 2), ((0, 0, 1, 1), 8)]
theta=0.5 -> [((1, 0, 1, 0), 1), ((0, 0, 0, 1), 2), ((1, 0, 0, 1), 2), ((0, 1, 0, 1), 2), ((1, 1, 1, 1), 41), ((1, 0, 1, 1), 13), ((0, 1, 0, 0), 2), ((1, 1, 0, 1), 13), ((0, 0, 1, 0), 1), ((0, 0, 1, 1), 3), ((0, 1, 1, 1), 17), ((1, 1, 1, 0), 16), ((1, 1, 0, 0), 8), ((0, 1, 1, 0), 7)]
theta=1.0 -> [((1, 0, 1, 1), 4), ((1, 1, 1, 1), 95), ((1, 1, 1, 0), 4), ((1, 1, 0, 0), 3), ((0, 0, 1, 1), 1), ((0, 1, 1, 1), 5), ((1, 1, 0, 1), 16)]

The transpiled executable is reused across all three runs — only the runtime binding {"theta": theta} changes.

To recap:

  • bindings={"n": 4} at transpile time: fixes the circuit structure.

  • parameters=["theta"] at transpile time: declares theta as sweepable.

  • bindings={"theta": ...} at execution time: provides the concrete value.

Summary

  • qmc.UInt values that control circuit structure (qubit count, loop bounds) typically need to be bound at transpile time. qmc.Float values (rotation angles) are natural candidates for runtime sweeping.

  • Use qmc.qubit_array(n) and qmc.range(n) for parameterized circuits. Always use index-based updates: q[i] = qmc.gate(q[i]).

  • The bind/sweep pattern — transpile(bindings=..., parameters=...) then loop — transpiles once and executes many times.

Next: Resource Estimation — symbolic cost analysis, gate breakdowns, and comparing design candidates.