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

Tags: tutorial

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:

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.

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.

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.

<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.

Error type: SyntaxError
Error message: Direct iteration over sequences is not supported in @qkernel functions.
Line 6: '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]).

Broadcasting Single-Qubit Gates Over an Array

When the same single-qubit gate is applied to every qubit in a register, you can pass the array directly instead of writing the index loop. This is equivalent to the explicit for i in qmc.range(n): q[i] = qmc.gate(q[i]) pattern — under the hood it emits the same IR loop, so resource estimation, transpilation, and visualization see no difference. For rotation gates the same scalar angle is shared across every qubit; per-qubit angles still need an explicit loop indexing into a parameter vector.

<Figure size 801.5x336 with 1 Axes>

Both rotation_layer (above) and rotation_layer_broadcast produce the same circuit. The broadcast form is the idiomatic choice for whole-register layers; reach for the explicit loop when each qubit needs a different angle (for example, indexing into Vector[Float]) or when only a slice of the register is touched.

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.

theta=0.1 -> [((0, 0, 0, 0), 5), ((0, 0, 1, 0), 4), ((0, 0, 1, 1), 8), ((0, 1, 0, 1), 8), ((1, 0, 0, 1), 11), ((0, 1, 0, 0), 5), ((1, 1, 0, 1), 8), ((1, 0, 1, 1), 11), ((1, 1, 1, 1), 13), ((1, 0, 0, 0), 8), ((1, 1, 0, 0), 7), ((1, 1, 1, 0), 10), ((0, 1, 1, 1), 7), ((0, 0, 0, 1), 4), ((1, 0, 1, 0), 7), ((0, 1, 1, 0), 12)]
theta=0.5 -> [((0, 1, 0, 0), 2), ((1, 0, 1, 0), 4), ((1, 1, 0, 0), 7), ((1, 0, 0, 0), 2), ((1, 0, 0, 1), 2), ((0, 0, 0, 0), 2), ((0, 1, 1, 0), 1), ((0, 1, 0, 1), 6), ((0, 0, 1, 1), 6), ((0, 1, 1, 1), 15), ((0, 0, 0, 1), 3), ((1, 1, 1, 1), 47), ((1, 0, 1, 1), 12), ((1, 1, 0, 1), 10), ((1, 1, 1, 0), 9)]
theta=1.0 -> [((0, 1, 0, 1), 1), ((1, 0, 0, 1), 1), ((1, 0, 1, 1), 5), ((1, 1, 1, 1), 88), ((1, 1, 1, 0), 12), ((1, 1, 0, 1), 17), ((0, 1, 1, 1), 4)]

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: Vector Slicing — VectorView, slice assignment, nested slices, and passing views to helper kernels.