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.0Breaking Changes¶
Qiskit is now an optional dependency. A base
pip install qamomile==0.13.0installs the Engine-independent compiler core. To install Qiskit and the circuit-visualization dependencies used by the examples in these release notes, runpip install "qamomile[qiskit,visualization]==0.13.0"(#599).The class-based composite-gate API has been removed.
CompositeGate,QFT,IQFT, and similar APIs are no longer public. Define named reusable bodies with@qmc.composite_gate. The decorator returns an ordinaryQKernel, so it uses the samebuild(),draw(),estimate_resources(),qmc.control, andqmc.inversepaths as other qkernels.The state-preparation module moved from
qamomile.circuit.algorithm.state_preparationtoqamomile.circuit.stdlib.state_preparation. The top-levelqmc.amplitude_encoding,qmc.amplitude_encoding_from_angles, andqmc.computational_basis_stateimports remain available (#571, #586).qmc.amplitude_encodingis now a state-preparation API that does not specify a synthesis method. It uses native state preparation when the Engine provides it and otherwise uses Möttönen state preparation. Useqmc.mottonen_amplitude_encodingwhen you want to request Möttönen state preparation explicitly (#602).The resource-estimation data model has changed.
GateCount,count_gates,qubits_counter, and theqamomile.circuit.estimator.algorithmicmodules were removed. Useqkernel.estimate_resources()orqmc.estimate_resources()and the newResourceEstimatefields for width, gates, depth, calls, assumptions, and estimate quality. The compatibility aliasesestimate.qubits,estimate.gates.t_gates,estimate.gates.clifford_gates, andestimate.gates.rotation_gatesremain, but direct construction and theto_dict()schema have changed (#571).Block-level JSON and MessagePack serialization has been removed.
dump_json,load_json,dump_msgpack,load_msgpack,to_dict, andfrom_dictare no longer exposed fromqamomile.circuit.ir.serialize, and v0.12.7 payloads are not migrated. Useqamomile.circuit.serialization.serialize()anddeserialize()to persist an unbound qkernel as deterministic Protobuf bytes (#595).Engine emitter and artifact classes are no longer part of the supported public API.
QuriPartsEmitPass,CudaqEmitPass,CudaqKernelEmitter,CudaqKernelArtifact, andBoundCudaqKernelArtifactwere removed from their package exports. Continue to useQiskitTranspiler,QuriPartsTranspiler, orCudaqTranspilerand their executors (#586).SliceBorrowViolationErrorhas been removed. Post-fold slice failures now report the semantic cause withQubitBorrowConflictErrororQubitConsumedError; unsupported ownership propagation across control flow raisesValidationError(#591).
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¶
Measurement-derived classical outputs are no longer dropped. A comparison, arithmetic expression, or logical expression derived from measurements and returned from a qkernel now runs in host-side post-processing when appropriate instead of resolving to
None. This path also works on Engines without dynamic-circuit support (#546).Compile-time branches inside controlled qkernels are folded before emission. Binding-driven
ifstatements nested inqmc.controlnow select the correct branch on Qiskit, QURI Parts, and CUDA-Q instead of reaching Engine emission as unsupported classical control flow (#579).Qiskit Aer no longer crashes on empty-parameter multiplexers produced by native state preparation.
QiskitExecutorrecursively checks the transpiled circuit, including nested control-flow blocks, and decomposes only the empty-parametermultiplexerform before Aer execution. Valid parameter-bearing multiplexers are left unchanged (#601).QURI Parts multi-controlled gates now scale linearly in the control count. The fallback uses a clean-ancilla Toffoli cascade instead of constructing an exponentially large dense unitary. Runtime-parameterized multi-controlled rotations and control widths beyond the former dense-matrix cap now transpile; the added ancillas are uncomputed before execution completes (#555).
Compiler and Engine contracts fail earlier and more consistently. Invalid scalar, array, and dictionary bindings now receive shape and type validation; runtime parameters retain deterministic ordering; canonical identity includes NumPy shape; and controlled, inverse, measurement, and Pauli-evolution paths have aligned checks across Qiskit, QURI Parts, CUDA-Q, HUGR, qBraid, and Quration (#599).
Documentation¶
Tutorial 04 — Controlled Gates now covers value-activated controls, SELECT multiplexers, and global-phase semantics (#593, #597).
Tutorial 05 — Resource Estimation reflects body-derived symbolic estimates and the redesigned resource model (#588).
Tutorial 06 — Execution Models clarifies qkernel return order,
Vectormeasurement index order, and the least-significant-bit convention for tuple outcomes (#581).Tutorial 10 — Compilation and Transpilation documents callable invocation, the reconstructed transpiler pipeline, if-merge yields, and semantic slice-borrow errors (#578, #588, #592).
Möttönen Amplitude Encoding now distinguishes method-independent state preparation from the explicit Möttönen APIs and demonstrates transpile-time amplitude binding, runtime angle rebinding, fidelity checks, and resource formulas (#602).
The Qiskit, QURI Parts, and CUDA-Q integration pages were updated for the current transpiler and installation surfaces (#588, #599).