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.

Qamomile v0.13.0

Qamomile v0.13.0 adds SELECT and global-phase operations. In addition, qmc.control now accepts an integer control_value. Qiskit and circuit visualization are now optional, so users who want to continue using Qiskit should run pip install "qamomile[qiskit]".

pip install qamomile==0.13.0

Breaking Changes

New Features

Exact structural math expressions

qmc.log2 and qmc.ceil build target-neutral classical expressions inside a qkernel. They are useful for deriving register widths and other compile-time structure without evaluating traced values in host Python. Concrete inputs fold immediately, while unbound inputs remain exact in resource estimates and are resolved when their arguments are supplied through transpile-time bindings. Because these expressions determine circuit structure, their arguments are not runtime backend parameters.

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

@qmc.qkernel
def logarithmic_register(size: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    width = qmc.ceil(qmc.log2(size))
    return qmc.measure(qmc.qubit_array(width, name="register"))

transpiler = QiskitTranspiler()
executable = transpiler.transpile(
    logarithmic_register,
    bindings={"size": 9},
)

Semantic qmc.select and value-activated controls

qmc.select([U0, U1, ...]) creates a SELECT operation that applies case Ui to the shared target qubits when the LSB-first index register represents the integer i. The index width can be inferred from the number of cases or specified with a Python int or qmc.UInt. In addition, qmc.control(..., control_value=v) creates a controlled operation that applies the target operation when the flattened control qubits match the concrete LSB-first value v (#597).

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

@qmc.qkernel
def select_example() -> tuple[qmc.Bit, qmc.Bit]:
    index = qmc.qubit(name="index")
    target = qmc.qubit(name="target")
    index = qmc.x(index)   # select case 1
    target = qmc.x(target)
    index, target = qmc.select([qmc.x, qmc.z])(index, target)
    return qmc.measure(index), qmc.measure(target)

transpiler = QiskitTranspiler()
executable = transpiler.transpile(select_example)

Exact global phase for reusable operations

qmc.global_phase(target, phase) applies target followed by exp(i * phase) and preserves the phase explicitly through nested qkernel calls, control, inverse, powers, structured control flow, and Protobuf round trips. A standalone global phase is not observable, but under controlled or SELECT operations it becomes a relative phase and appears in interference results (#593, #596).

import math

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

@qmc.qkernel
def identity(qubit: qmc.Qubit) -> qmc.Qubit:
    return qubit

@qmc.qkernel
def phased_identity(qubit: qmc.Qubit) -> qmc.Qubit:
    return qmc.global_phase(identity, math.pi)(qubit)

@qmc.qkernel
def phase_kickback() -> tuple[qmc.Bit, qmc.Bit]:
    control = qmc.h(qmc.qubit(name="control"))
    target = qmc.h(qmc.qubit(name="target"))
    control, target = qmc.control(phased_identity)(control, target)
    control = qmc.h(control)
    target = qmc.h(target)
    return qmc.measure(control), qmc.measure(target)

transpiler = QiskitTranspiler()
executable = transpiler.transpile(phase_kickback)

Loop-carried scalars and classical comparisons

Reassigning a same-type UInt or Float inside qmc.range and qmc.items now carries the updated value into the next iteration instead of raising ValidationError. This supports accumulators and values computed for later gate parameters or compile-time branches. Runtime-trip-count while carries, measurement-backed Bit carries, mixed-type reassignments, and measurement-derived carries inside loops that also contain quantum work remain unsupported. In addition, Bit, UInt, and Float handles now implement the supported Python comparison operators without collapsing to host-language booleans during tracing. This includes numeric ordering, equality and inequality, mixed UInt/Float comparisons, and Bit equality with Bit, bool, zero-or-one integers, or UInt (#580, #587).

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

@qmc.qkernel
def accumulated_branch(n: qmc.UInt) -> qmc.Bit:
    qubit = qmc.qubit(name="qubit")
    total = 0
    for i in qmc.range(n):
        total = total + i
    if total >= 3:
        qubit = qmc.x(qubit)
    return qmc.measure(qubit)

transpiler = QiskitTranspiler()
executable = transpiler.transpile(accumulated_branch, bindings={"n": 3})

Retaining projection and reset operations

qmc.project_x, qmc.project_y, and qmc.project_z return both the projected qubit and its classical Bit, so a qkernel can continue using the post-measurement state. qmc.reset returns a fresh handle for the same qubit in |0>, and qmc.measure_reset combines Z-basis projection and reset. These operations keep Qamomile’s affine ownership checks intact because the input handle is consumed and the returned qubit handle carries the state forward (#599).

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

@qmc.qkernel
def measure_and_reset() -> tuple[qmc.Bit, qmc.Bit]:
    qubit = qmc.x(qmc.qubit(name="qubit"))
    qubit, before = qmc.measure_reset(qubit)
    after = qmc.measure(qubit)
    return before, after

transpiler = QiskitTranspiler()
executable = transpiler.transpile(measure_and_reset)

Generic and explicit Möttönen amplitude encoding

qmc.amplitude_encoding now describes the state to prepare without prescribing how the Engine synthesizes it. qmc.mottonen_amplitude_encoding explicitly uses the Möttönen algorithm (#602).

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

@qmc.qkernel
def prepare_bell() -> qmc.Vector[qmc.Bit]:
    qubits = qmc.qubit_array(2, name="qubits")
    qubits = qmc.mottonen_amplitude_encoding(
        qubits, [1.0, 0.0, 0.0, 1.0]
    )
    return qmc.measure(qubits)

transpiler = QiskitTranspiler()
executable = transpiler.transpile(prepare_bell)

See Möttönen Amplitude Encoding.

Unified callables, opaque oracles, and an expanded standard library

Named composite gates are now ordinary qkernels with one call model for direct calls, control, inverse, serialization, visualization, and body-derived resource estimation. qmc.opaque(name, ...) and qmc.Oracle add bodyless callables for top-down design; they can carry a fixed or context-dependent resource cost until a target implementation or substitution is supplied. ResourceEstimate now reports logical width, gate categories, depth, callable/query counts, assumptions, an explanation trace, and estimate quality, including symbolic expressions for unbound inputs. The public standard library now includes qmc.mcx, reversible ripple-carry and modular arithmetic, Grover search helpers, and Shor order finding. State-preparation helpers also use this callable model, so the same executable bodies drive transpilation and resource estimates (#571, #586).

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

@qmc.qkernel
def stdlib_mcx() -> tuple[qmc.Vector[qmc.Bit], qmc.Bit]:
    controls = qmc.x(qmc.qubit_array(3, name="controls"))
    target = qmc.qubit(name="target")
    controls, target = qmc.mcx(controls, target)
    return qmc.measure(controls), qmc.measure(target)

estimate = stdlib_mcx.estimate_resources()
transpiler = QiskitTranspiler()
executable = transpiler.transpile(stdlib_mcx)

Protobuf qkernel serialization

qamomile.circuit.serialization.serialize() writes a static, unbound qkernel as deterministic Protobuf bytes, including its public signature, hierarchical body, callable definitions, defaults, annotations, global-phase operations, and value relationships. deserialize() reconstructs a qkernel-like object that can receive fresh compile-time bindings and runtime parameters through the normal transpiler interface. Bound or lowered artifacts, runtime values, and Engine objects remain outside the format (#595, #596).

import qamomile.circuit as qmc
from qamomile.circuit.serialization import deserialize, serialize
from qamomile.qiskit import QiskitTranspiler

@qmc.qkernel
def serializable_flip() -> qmc.Bit:
    qubit = qmc.x(qmc.qubit(name="qubit"))
    return qmc.measure(qubit)

payload = serialize(serializable_flip)
restored = deserialize(payload)

transpiler = QiskitTranspiler()
executable = transpiler.transpile(restored)

Internal Changes

The features above are built on a target-neutral preparation stage, a unified callable graph, and an Engine-neutral circuit representation. These boundaries let circuit SDKs and program-graph targets share Qamomile semantics without forcing every target through the same execution model.

Explicit compiler targets and Engine-neutral circuit lowering

QamomileCompiler.prepare() now produces a PreparedModule that preserves reachable callable definitions, structured control flow, the call graph, and the classical ABI. Circuit Engines lower this into a verified CircuitProgram before native materialization, while the new HugrTranspiler compiles prepared semantics directly to a HUGR package. The new QurationTranspiler materializes through PyQret and exposes Quration resource compilation; HUGR dependencies are available through the hugr extra, while PyQret must currently be installed separately (#586).

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

@qmc.qkernel
def compiler_example() -> qmc.Bit:
    qubit = qmc.h(qmc.qubit(name="qubit"))
    return qmc.measure(qubit)

prepared = QamomileCompiler().prepare(compiler_example)
transpiler = QiskitTranspiler()
executable = transpiler.transpile(compiler_example)

Why: preserving semantic call and control-flow structure until a target chooses its lowering keeps the core IR Engine-independent, allows HUGR to retain program-graph structure, and gives circuit Engines one verified boundary before constructing SDK-native objects.

Yield-based branch merges

IfOperation now records each merged result with parallel true-branch and false-branch yield values. The former PhiOp pseudo-operation has been removed, while iter_merges() and add_merge() provide one validated accessor path for compiler passes, serialization, dependency analysis, and classical execution (#570, #574).

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

@qmc.qkernel
def measurement_branch_merge() -> tuple[qmc.Bit, qmc.Bit]:
    condition = qmc.measure(qmc.h(qmc.qubit(name="condition")))
    target = qmc.qubit(name="target")
    if condition:
        target = qmc.x(target)
    return condition, qmc.measure(target)

transpiler = QiskitTranspiler()
executable = transpiler.transpile(measurement_branch_merge)

Why: unlike the user-facing features above, this is an independent internal restructuring. Representing branch results as explicit yields aligns the IR with structured control flow, prevents affine quantum values from being double-counted as operands, and keeps merge semantics consistent across compiler consumers.

Bug Fixes

Documentation

Learn More