Tags: tutorial
As circuits grow, you want to avoid copy-pasting gate sequences. Qamomile offers two complementary reuse mechanisms:
Helper QKernel — call one
@qkernelfrom another, like normal function composition.@composite_gate— promote a qkernel to a named gate with customizable settings that appears as a single box in diagrams.
There is also a third pattern for top-down design:
Stub composite gate — a gate with no implementation body, used for resource estimation. For example, if you are designing a Grover search algorithm, you know the oracle will use ~40 T-gates, but you haven’t implemented it yet. A stub composite gate lets you estimate the total cost of the algorithm without the full oracle implementation.
# Install the latest Qamomile through pip!
# !pip install qamomileimport math
import qamomile.circuit as qmc
from qamomile.circuit.ir.operation.composite_gate import ResourceMetadata
from qamomile.qiskit import QiskitTranspiler
transpiler = QiskitTranspiler()Pattern 1: Helper QKernel¶
Any @qkernel function can be called from another @qkernel. The transpiler inlines the call — the result is a flat circuit.
@qmc.qkernel
def entangle_once(q0: qmc.Qubit, q1: qmc.Qubit) -> tuple[qmc.Qubit, qmc.Qubit]:
q0, q1 = qmc.cx(q0, q1)
return q0, q1
@qmc.qkernel
def ghz_with_helper(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(n, name="q")
q[0] = qmc.h(q[0])
for i in qmc.range(n - 1):
q[i], q[i + 1] = entangle_once(q[i], q[i + 1])
return qmc.measure(q)ghz_with_helper.draw(n=4, fold_loops=False)
result = (
transpiler.transpile(ghz_with_helper, bindings={"n": 4})
.sample(
transpiler.executor(),
shots=128,
)
.result()
)
print("GHZ result:", result.results)GHZ result: [((0, 0, 0, 0), 79), ((1, 1, 1, 1), 49)]
The helper entangle_once keeps the call site readable. In the transpiled circuit, it is inlined — you see individual CX gates, not a sub-block.
qc = transpiler.to_circuit(ghz_with_helper, bindings={"n": 4})
print(qc.draw()) ┌───┐ ┌─┐
q_0: ┤ H ├──■───────┤M├──────────────
└───┘┌─┴─┐ └╥┘ ┌─┐
q_1: ─────┤ X ├──■───╫──────┤M├──────
└───┘┌─┴─┐ ║ └╥┘┌─┐
q_2: ──────────┤ X ├─╫───■───╫─┤M├───
└───┘ ║ ┌─┴─┐ ║ └╥┘┌─┐
q_3: ────────────────╫─┤ X ├─╫──╫─┤M├
║ └───┘ ║ ║ └╥┘
c: 4/════════════════╩═══════╩══╩══╩═
0 1 2 3
Passing scalar literals to helpers¶
When a helper qkernel declares a scalar parameter (UInt, Float, or Bit), you can pass a raw Python literal at the call site — Qamomile auto-promotes int to UInt, float to Float, and bool to Bit. Writing helper(q, 0, 0.5) is equivalent to helper(q, qmc.uint(0), qmc.float_(0.5)). Use the explicit qmc.uint / qmc.float_ / qmc.bit constructors only when you want to name the value or share it across multiple call sites.
@qmc.qkernel
def rotate_first(
q: qmc.Vector[qmc.Qubit],
idx: qmc.UInt,
angle: qmc.Float,
) -> qmc.Vector[qmc.Qubit]:
q[idx] = qmc.ry(q[idx], angle)
return q
@qmc.qkernel
def helper_with_literals(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(n, name="q")
q = rotate_first(q, 0, 0.5) # int and float literals are accepted directly
return qmc.measure(q)
helper_with_literals.draw(n=3, fold_loops=False, inline=True)
Pattern 2: @composite_gate¶
When you want a reusable block to appear as a named box in circuit diagrams, promote it with @composite_gate. Also, as a more advanced use case, making it a composite gate allows you to give it custom settings such as giving it multiple ways of implementation.
Stack @composite_gate(name="...") on top of @qkernel:
@qmc.composite_gate(name="entangle")
@qmc.qkernel
def entangle_link(q0: qmc.Qubit, q1: qmc.Qubit) -> tuple[qmc.Qubit, qmc.Qubit]:
q0, q1 = qmc.cx(q0, q1)
return q0, q1
@qmc.qkernel
def ghz_with_composite(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(n, name="q")
q[0] = qmc.h(q[0])
for i in qmc.range(n - 1):
q[i], q[i + 1] = entangle_link(q[i], q[i + 1])
return qmc.measure(q)ghz_with_composite.draw(n=4, fold_loops=False)
When to use which?¶
| Pattern | Appears in draw() | Use when |
|---|---|---|
Helper @qkernel | Inlined (flat) | Code organization |
@composite_gate | Named box | Domain-level abstraction/advanced settings |
Pattern 3: Stub Composite Gate for Top-Down Design¶
Sometimes you want to design an algorithm’s structure before implementing every sub-component. A stub composite gate has no implementation body — just a name, qubit count, and optional resource metadata.
This lets you estimate the cost of the overall algorithm while the oracle or sub-routine is still under development.
To use a stub composite gate, specify stub=True in the @composite_gate decorator. At the same time, you can also give it resource information as ResourceMetadata.
@qmc.composite_gate(
stub=True,
name="oracle",
num_qubits=3,
resource_metadata=ResourceMetadata(
query_complexity=1,
t_gates=40,
),
)
def oracle_box():
pass
@qmc.qkernel
def algorithm_skeleton() -> qmc.Vector[qmc.Qubit]:
q = qmc.qubit_array(3, name="q")
for i in qmc.range(3):
q[i] = qmc.h(q[i])
q[0], q[1], q[2] = oracle_box(q[0], q[1], q[2])
return qalgorithm_skeleton.draw(fold_loops=False)
Resource Estimation for QKernels that Include Stub Gates¶
estimate_resources() can analyze a full qkernel even when oracle internals are unknown. Known scaffold gates are counted directly, and stub components are tracked through est.gates.oracle_calls / est.gates.oracle_queries.
est = algorithm_skeleton.estimate_resources().simplify()
print("qubits:", est.qubits)
print("total gates:", est.gates.total)qubits: 3
total gates: 3
Next, we build a qkernel that mixes ordinary gates with multiple stub oracles.
@qmc.composite_gate(
stub=True,
name="phase_oracle",
num_qubits=3,
resource_metadata=ResourceMetadata(query_complexity=2),
)
def phase_oracle():
pass
@qmc.composite_gate(
stub=True,
name="mixing_oracle",
num_qubits=3,
resource_metadata=ResourceMetadata(query_complexity=1),
)
def mixing_oracle():
pass
@qmc.qkernel
def iterative_oracle_skeleton(rounds: qmc.UInt) -> qmc.Vector[qmc.Qubit]:
q = qmc.qubit_array(3, name="q")
# Known scaffold (non-oracle) gates
q[0] = qmc.h(q[0])
q[1] = qmc.h(q[1])
q[0], q[1] = qmc.cx(q[0], q[1])
# One oracle call outside the loop
q[0], q[1], q[2] = phase_oracle(q[0], q[1], q[2])
# Each round mixes known gates and unknown oracle blocks
for i in qmc.range(rounds):
q[1] = qmc.ry(q[1], 0.3)
q[1], q[2] = qmc.cx(q[1], q[2])
q[0], q[1], q[2] = phase_oracle(q[0], q[1], q[2])
q[0], q[1], q[2] = mixing_oracle(q[0], q[1], q[2])
q[1], q[2] = qmc.cx(q[1], q[2])
return q
iterative_oracle_skeleton.draw(rounds=4, fold_loops=False)
oracle_est = iterative_oracle_skeleton.estimate_resources().simplify()
print("total gates:", oracle_est.gates.total)
print("two-qubit gates:", oracle_est.gates.two_qubit)
print("oracle_calls:", oracle_est.gates.oracle_calls)
print("oracle_queries:", oracle_est.gates.oracle_queries)total gates: 3*rounds + 3
two-qubit gates: 2*rounds + 1
oracle_calls: {'phase_oracle': rounds + 1, 'mixing_oracle': rounds}
oracle_queries: {'phase_oracle': 2*rounds + 2, 'mixing_oracle': rounds}
Substitute a concrete value for rounds to get numeric counts:
oracle_est_4 = oracle_est.substitute(rounds=4)
print("oracle_calls (rounds=4):", oracle_est_4.gates.oracle_calls)
print("oracle_queries (rounds=4):", oracle_est_4.gates.oracle_queries)oracle_calls (rounds=4): {'phase_oracle': 5, 'mixing_oracle': 4}
oracle_queries (rounds=4): {'phase_oracle': 10, 'mixing_oracle': 4}
In this example, resource analysis works without oracle internals: known gates contribute to total / two_qubit, while unknown oracle blocks are tracked as oracle_calls (for example, {'phase_oracle': rounds + 1, 'mixing_oracle': rounds}) and oracle_queries (weighted by each stub’s query_complexity).
This lets you reason about algorithm-level costs (such as qubit count, oracle queries) before committing to a full decomposition.
Pattern 4: Controlling a Built-in Gate¶
When you only need a controlled version of a single primitive gate
(qmc.rx, qmc.h, qmc.cp, ...), qmc.controlled accepts the
built-in gate function directly — no @qkernel wrapper required.
The keyword for any classical parameter follows the underlying gate’s
own parameter name (e.g. angle= for qmc.rx / qmc.ry, theta=
for qmc.p / qmc.cp).
Before — boilerplate wrapper:
@qmc.qkernel
def _rx_gate(q: qmc.Qubit, theta: qmc.Float) -> qmc.Qubit:
return qmc.rx(q, theta)
crx = qmc.controlled(_rx_gate)After — wrap directly:
crx = qmc.controlled(qmc.rx)@qmc.qkernel
def controlled_rx_demo() -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(2, name="q")
# Drive the control to |1> so the controlled rotation fires.
q[0] = qmc.x(q[0])
crx = qmc.controlled(qmc.rx)
q[0], q[1] = crx(q[0], q[1], angle=math.pi)
return qmc.measure(q)
controlled_rx_demo.draw()
Multi-control and the power parameter work the same as for any
controlled(...) factory — num_controls and power are properties
of the ControlledGate rather than of the wrapped function, so
nothing extra is needed:
@qmc.qkernel
def toffoli_via_builtin() -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(3, name="q")
q[0] = qmc.x(q[0])
q[1] = qmc.x(q[1])
ccx = qmc.controlled(qmc.x, num_controls=2)
q[0], q[1], q[2] = ccx(q[0], q[1], q[2])
return qmc.measure(q)
toffoli_via_builtin.draw()
Existing @qmc.qkernel-wrapped arguments still work — the built-in
form is purely additive. Reach for a hand-written @qkernel whenever
the gate body needs more than a single primitive call (e.g. an
H followed by an RX), since controlled of a multi-gate body is
exactly what @qkernel is for:
@qmc.qkernel
def h_then_rx(q: qmc.Qubit, theta: qmc.Float) -> qmc.Qubit:
q = qmc.h(q)
q = qmc.rx(q, theta)
return q
@qmc.qkernel
def controlled_pair_demo() -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(2, name="q")
q[0] = qmc.x(q[0])
cg = qmc.controlled(h_then_rx)
q[0], q[1] = cg(q[0], q[1], theta=math.pi / 4)
return qmc.measure(q)
controlled_pair_demo.draw()
Summary¶
Helper
@qkernel: call one qkernel from another for code reuse. The transpiler inlines the call into a flat circuit.@composite_gate: gives a qkernel a named identity visible in diagrams. Stack@composite_gateon top of@qkernel.Stub composite gate:
stub=TruewithResourceMetadatafor top-down design and resource estimation without a full implementation.est.gates.oracle_calls: even when oracle internals are unknown, this reports per-oracle call counts as a dict (including symbolic call counts).qmc.controlled(builtin_gate): skip the one-line@qkernelwrapper when controlling a single primitive — passqmc.rx,qmc.h,qmc.cp, etc. directly to the factory.