Qamomile v0.12.7 lets you pass a Python dictionary of coefficients into a @qkernel as a runtime parameter and index it by a loop variable — angles[i] inside the kernel, re-bound to different values on each execution — and adds the % operator on UInt handles. This release also adds a set of transpile-time checks that reject several constructs which used to transpile into wrong circuits — such as allocating a fresh qubit for a variable inside a branch without consuming the previous one, or passing the same Vector[Qubit] as two arguments of a kernel call — each now raising a specific error instead of producing a wrong circuit. Controlled qmc.pauli_evolve is now supported on QURI Parts, and it no longer drops the global phase of a Hamiltonian’s constant term, so controlled evolution of a constant-offset Hamiltonian is now correct on Qiskit, QURI Parts, and CUDA-Q.
pip install qamomile==0.12.7Breaking Changes¶
This release adds transpile-time checks that reject programs which previously transpiled but produced a circuit that did not match the Python source. Each pattern below now raises a specific error at transpile time.
Allocating a fresh qubit for a variable inside a branch or loop body, without consuming the previous one, now raises
QubitRebindError. Writingq = qmc.qubit(...)inside a measurement-drivenif, or inside afor/whilebody, used to transpile and silently dropq’s previous state (#561).@qmc.qkernel def kernel() -> qmc.Bit: q = qmc.qubit("q") p = qmc.qubit("p") cond = qmc.measure(p) if cond: q = qmc.qubit("fresh") # QubitRebindError: q's previous state was never consumed return qmc.measure(q)Updating a classical variable across loop iterations now raises
ValidationError. A@qkernelforbody is traced once, so an accumulator liketotal = total + iused to have its right-hand side frozen to the pre-loop value (#552).A classical array element that reads another element of the same array inside a loop now raises
ValidationError. Inside a loop,arr[i] = arr[i - 1]used to read a pre-loop snapshot ofarr— so earlier iterations’ writes were never observed — and silently produced a wrong circuit (#545).Allocating a multi-dimensional qubit array is now rejected up front.
qmc.qubit_array((2, 3), ...)and other rank-≥2 quantum shapes used to be accepted and then produce a wrong circuit or crash later. They now raiseNotImplementedErrorat construction. ClassicalMatrix/Tensorarguments are unaffected (#558).Passing a kernel argument of the wrong type or shape now raises
TypeError. Passing a scalarQubitto aVector[Qubit]parameter used to leak an unrelatedAttributeError, and passing aQubitto aFloatparameter used to silently emitrx(0). Arguments are now checked against the declared parameter type at every call site — plain kernel calls,qmc.control, andqmc.inverse(#473).Passing a Python
boolwhere a plain int is required now raisesTypeError. Becauseboolsubclassesint,q[True],q[False:2], andqmc.uint(True)used to be silently accepted as1/0. Array indices, slice bounds, andqmc.uintnow rejectbool(#539).Passing the same qubit array as two arguments of a kernel call now raises
QubitConsumedError.sub(qs, qs)— or two overlapping views — used to map both parameters onto the same physical qubits, which produced a wrong circuit or later surfaced as a rawduplicate qubiterror from the SDK. The alias is now rejected at the call site (#543).
New Features¶
Dictionary coefficients as runtime parameters¶
A Dict[K, Float] kernel argument can now be kept as a runtime parameter and indexed by a key inside the kernel — including by a qmc.range / qmc.items loop variable. Each looked-up key becomes one runtime parameter of the emitted circuit named "<dict>[<key>]" (e.g. angles[0], coeffs[(0, 1)]), so the same transpiled executable can be re-bound with different dictionary values on each execution. Declare it by listing the argument in parameters=[...]. Symbolic key components must be UInt handles, while a fully constant key against a compile-time-bound dictionary folds to a constant (#556, #562).
import numpy as np
import qamomile.circuit as qmc
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def rx_by_dict(n: qmc.UInt, angles: qmc.Dict[qmc.UInt, qmc.Float]) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(n, name="q")
for i in qmc.range(n):
q[i] = qmc.rx(q[i], angle=angles[i]) # subscript a runtime-parameter dict
return qmc.measure(q)
transpiler = QiskitTranspiler()
executable = transpiler.transpile(rx_by_dict, bindings={"n": 3}, parameters=["angles"])
# executable.parameter_names -> ['angles[0]', 'angles[1]', 'angles[2]']
# re-bind the dict per execution:
# executable.sample(transpiler.executor(), shots=256,
# bindings={"angles": {0: np.pi, 1: 0.0, 2: np.pi}})Modulo operator on UInt¶
UInt handles now support the % operator (and int % UInt). This makes index expressions such as i % 2 inside a qmc.range loop transpile directly instead of raising a TypeError at trace time (#466).
import qamomile.circuit as qmc
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def even_indices() -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(4, name="q")
for i in qmc.range(4):
if i % 2 == 0: # modulo on a loop variable
q[i] = qmc.x(q[i])
return qmc.measure(q)
transpiler = QiskitTranspiler()
executable = transpiler.transpile(even_indices)[((1, 0, 1, 0), 256)]qmc.pauli_evolve accepts a Hamiltonian smaller than the target¶
qmc.pauli_evolve now accepts a Hamiltonian that acts on fewer qubits than the target Vector[Qubit]. Each Pauli term addresses array elements positionally (a term on index i acts on q[i]), and the qubits beyond the Hamiltonian’s size evolve under the identity. A Hamiltonian acting on more qubits than the target now fails with a clear EmitError at transpile time (#471).
import qamomile.circuit as qmc
import qamomile.observable as qm_o
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def evolve(h: qmc.Observable, t: qmc.Float) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(2, name="q")
q = qmc.h(q)
q = qmc.pauli_evolve(q, h, t) # h acts on 1 qubit; q[1] evolves under the identity
return qmc.measure(q)
transpiler = QiskitTranspiler()
executable = transpiler.transpile(evolve, bindings={"h": qm_o.Z(0), "t": 0.5})Internal Changes¶
Hamiltonian serialization round-trip¶
The IR serializer now understands bound Hamiltonians. A compiled Block that carries operator data — for example a Vector[Observable] argument bound to a list of Pauli Hamiltonians, held in a ParamSlot.bound_value — used to silently lose that data when passed through dump_json / load_json (and the msgpack pair). As of this release, each Hamiltonian round-trips back identical (#475, #548).
import qamomile.circuit as qmc
import qamomile.observable as qm_o
from qamomile.circuit.transpiler.passes.inline import InlinePass
from qamomile.circuit.ir.serialize import dump_json, load_json
@qmc.qkernel
def trotter(q: qmc.Vector[qmc.Qubit], hs: qmc.Vector[qmc.Observable], t: qmc.Float) -> qmc.Vector[qmc.Bit]:
for i in qmc.range(hs.shape[0]):
q = qmc.pauli_evolve(q, hs[i], t)
return qmc.measure(q)
hamiltonians = [1.2 * qm_o.Z(0), 0.8 * qm_o.X(0)]
block = InlinePass().run(trotter.build(hs=hamiltonians, parameters=["t"]))
restored = load_json(dump_json(block))
slot = next(s for s in restored.param_slots if s.name == "hs")
print(slot.bound_value)[Hamiltonian((Z0,): 1.2), Hamiltonian((X0,): 0.8)]Bug Fixes¶
Controlled
qmc.pauli_evolveno longer drops the constant term’s global phase. A Hamiltonian’s constant (identity) term contributes a global phaseexp(-i·gamma·c), which the emitter used to drop. On a standalone evolution this is unobservable, but underqmc.controlthat phase becomes a real relative phase on the controls-on subspace — so controlled evolution of any constant-offset Hamiltonian produced a wrong result. The constant is now re-applied under control on Qiskit (#471, #547) and CUDA-Q (#540), and a shared controlled lowering makes controlledpauli_evolvetranspile on QURI Parts (#537). A non-real constant now raises a clearEmitError.Controlled sub-kernels over a whole
Vector[Qubit]now emit on every SDK. The shared controlled-U fallback used to route every inner gate to a single target and reject multi-target inner blocks and nested controlled-U operations; it now maps each inner qubit onto the caller’s targets, so a controlled multi-qubit sub-kernel (such as the modular-arithmetic shift) transpiles on QURI Parts and CUDA-Q as well as Qiskit (#472).Controlled gates with a symbolic array-index control qubit now use the right wire. When the control is a binding-driven array element (
q[control_index]), the physical qubit is now resolved through the qubit map instead of allocating a fresh one, which previously misaligned the control and silently produced a wrong circuit (#541).Möttönen amplitude-encoding / inverse state-prep wrappers route qubits correctly. A qubit-resolution bug in the inverse state-preparation wrapper produced wrong qubit routing (#465).
Out-of-range array indices during loop unrolling fail fast. An out-of-range or shape-mismatched array index encountered while unrolling a loop now raises a clear
EmitErrorinstead of silently producing a wrong circuit (#477).canonicalizepreserves the classical parameter manifest. Canonicalization used to dropBlock.param_slots; the manifest now survives, so content hashing and serialization of a canonicalized block keep the parameter contract (#476).Sizing a qubit array from a classical array element now resolves correctly. When the size of a qubit array comes from an element of a classical
Vector[UInt]—qmc.qubit_array(sizes[i], ...)— the size used to be resolved wrongly and allocate the wrong number of qubits. It now resolves correctly through slices and indexed elements, keeps a size of 0, and rejects invalid sizes (negative sizes, unsupported negative indices) with a clear error (#456).A
QFixedvalue made withqmc.castkeeps pointing at the right qubits through IR transforms.qmc.cast(q, qmc.QFixed, ...)reinterprets aVector[Qubit]as a fixed-point value, and the cast internally remembers which qubits it wraps. Passing the kernel through inlining, canonicalization, or compile-time-iflowering used to fail to remap that reference, so the cast could point at the wrong qubits. It now keeps pointing at the right qubits through all of these transforms (#468).Deriving a loop count from a runtime parameter now gives a clear error. A
qmc.range(...)count must be known at compile time so the loop can be unrolled. Using a runtime parameter (or one of its array elements) — a value only fixed at execution time — as the count used to fail with an obscure, unrelated error. It now raises a clear error early in transpilation, telling you to supply that value throughbindings(#557).
Documentation¶
Estimating Nanosheet Material Properties with the Multidimensional Quantum Fourier Transform — a new algorithm article applying a multidimensional QFT to a materials-science estimation problem (#405).
CUDA-Q Support — a new integration page for transpiling and executing Qamomile kernels on the CUDA-Q SDK (#440).
Qiskit Support — a new integration page for the Qiskit SDK (#464).
The documentation site was redesigned, with reworked landing pages and card metadata, and a new Installation guide (#461).