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

Qamomile v0.12.3 lets you apply Python-style slicing (q[1::2], q[lo:hi], ...) to a Vector[Qubit] inside @qkernel. The resulting VectorView behaves like an ordinary Vector[Qubit] — gates, measurements, and helper-kernel calls all accept it. Two new algorithm helpers ship alongside: commutator(a, b) for Pauli-string Hamiltonians, and computational_basis_state for runtime-bits state preparation. There are also internal IR updates that lay the groundwork for treating a compiled @qkernel as a portable subgraph of an outer DSL’s computation graph.

pip install qamomile==0.12.3

Breaking Changes

QubitBorrowConflictError for live-borrow conflicts

A new QubitBorrowConflictError (subclass of AffineTypeError) is raised when a qubit slot is inaccessible because another live handle currently borrows it — an outstanding slice view, an unreturned element borrow, etc. The distinguishing semantics from QubitConsumedError is reversibility: a borrow can be returned (q[0:3] = a, qubits[0] = q0), restoring access, whereas a consumed slot is gone forever. The one pre-existing site that changes class is the “element already borrowed” check in ArrayBase._get_element, which previously emitted QubitConsumedError and now emits QubitBorrowConflictError. The four remaining raise sites (two more in ArrayBase, two in VectorView._wrap) are new with this release and cover slice-view borrow conflicts. Callers that explicitly catch QubitConsumedError for the element-already-borrowed scenario must switch to catching QubitBorrowConflictError, or catch the common base AffineTypeError (#395).

New Features

Python-style Vector slicing

Vector[Qubit] now supports Python slicing syntax (q[lo:hi], q[lo:hi:step], q[::step]) directly inside @qkernel. The result is a VectorView that lowers into a first-class SliceArrayOperation in the IR and executes correctly on Qiskit, QURI Parts, and CUDA-Q. Slice bounds may be Python literals, scalar handles (UInt), or arithmetic expressions over them — including bounds that are only resolvable after bindings are applied (q[lo:hi] with lo, hi in bindings) (#357).

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


@qmc.qkernel
def alternating() -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(6, name="q")
    evens = q[0::2]
    odds = q[1::2]
    for i in qmc.range(evens.shape[0]):
        evens[i] = qmc.h(evens[i])
        evens[i], odds[i] = qmc.cx(evens[i], odds[i])
    q[0::2] = evens
    q[1::2] = odds
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(alternating)

A new SliceBorrowCheckPass in the transpile pipeline catches slice-related affine errors: accessing q[1] after measure(q[1::2]) raises QubitConsumedError, taking two overlapping views without returning the first raises QubitBorrowConflictError, and bounds exceeding the parent length are auto-clamped. Negative steps and negative indices (q[::-1], q[-2:]) are not supported.

See Tutorial 03 — Vector Slicing for the end-to-end walk-through.

commutator(a, b) for Pauli-Hamiltonians

qamomile.observable.commutator(a, b) returns the commutator [A, B] = A B - B A of two Pauli-string Hamiltonians. The implementation uses the qubit-parity rule — two Pauli strings anticommute iff the number of qubits on which they carry different non-identity Paulis is odd — to drop every commuting term pair before any product is built. This is cheaper per pair than expanding a * b - b * a and cancelling, and the result is a fully simplified Hamiltonian ready for inspection or comparison against an analytic value (#394).

import qamomile.observable as qm_o

omega, Omega = 1.0, 0.3
Hz = 0.5 * omega * qm_o.Z(0)
Hx = 0.5 * Omega * qm_o.X(0)

comm = qm_o.commutator(Hz, Hx)
print(comm)
Hamiltonian((Y0,): 0.15j)

The Hamiltonian Simulation tutorial gains a new section deriving [H_z, H_x] = i ω Ω / 2 · Y as the textbook motivation for the Trotter splitting it already covers.

computational_basis_state algorithm helper

qamomile.circuit.algorithm.computational_basis_state(q, bits) prepares the computational basis state |bits⟩ from |0⟩^⊗n by applying Rx(π · bits[i]) to each qubit — the identity when bits[i] == 0, an X (up to a global phase) when bits[i] == 1. The crucial property is that bits may be left as a runtime parameter at transpile time: a runtime if bits[i]: x(...) is not emittable by the supported backends, but the parameterised rotation is. This is the missing piece for hybrid loops that re-bind the initial state on every iteration (#361).

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


@qmc.qkernel
def prepare_basis(
    n: qmc.UInt,
    bits: qmc.Vector[qmc.UInt],
) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, name="q")
    q = computational_basis_state(q, bits)
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(
    prepare_basis, bindings={"n": 3}, parameters=["bits"]
)

n is bound at compile time so the for i in qmc.range(n) body inside computational_basis_state has a concrete gate count; bits survives as a runtime parameter and is supplied at execution time.

See the new Quantum-enhanced MCMC tutorial for an end-to-end use case (classical Metropolis–Hastings with a Trotterised proposal driven by trotterized_time_evolution, with the basis state re-bound at every step).

Internal Changes

The IR gains three primitives that, taken together, build out the infrastructure for treating a compiled @qkernel as a portable subgraph of an outer DSL’s computation graph. They are independently usable.

Canonical form and content hash

qamomile.circuit.ir.canonicalize(block) re-numbers every Value.uuid / Value.logical_id in a Block from a deterministic counter and rewrites every UUID reference embedded in operations and value metadata (CastMetadata, QFixedMetadata, ArrayRuntimeMetadata, CastOperation.qubit_mapping). Two structurally-identical Blocks built from independent runs of the trace now compare equal under the canonical form, and content_hash(block) returns a stable SHA-256 hex digest suitable for content-addressable caching and IR diffing. Display-only fields (Block.name, Block.output_names, Value.name) are intentionally excluded from the hash so two kernels with different function names hash equally when their structure matches. Scope is BlockKind.AFFINE and BlockKind.ANALYZED; HIERARCHICAL blocks must be inlined first (#389).

import qamomile.circuit as qmc
from qamomile.circuit.ir import canonicalize, content_hash
from qamomile.qiskit import QiskitTranspiler


@qmc.qkernel
def bell() -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(2, name="q")
    q[0] = qmc.h(q[0])
    q[0], q[1] = qmc.cx(q[0], q[1])
    return qmc.measure(q)


affine = QiskitTranspiler().inline(bell.block)
print(content_hash(canonicalize(affine)))

Why: this is the IR-level identity primitive that every later “portable subgraph” feature depends on. Canonical form gives every passed-around IR a build-independent name; without it, hybrid runners and IR caches cannot tell that two independently-built copies of the same kernel are equivalent.

Block.param_slots parameter manifest

Every Block now carries param_slots: tuple[ParamSlot, ...], one entry per classical kernel argument, recording (name, type, kind, ndim, default, bound_value, differentiable). kind is RUNTIME_PARAMETER when the slot survives the pipeline as a runtime parameter and COMPILE_TIME_BOUND when the slot was folded by bindings or a Python signature default. The manifest survives the early pipeline (inline, substitute, resolve_parameter_shapes), so downstream readers can recover the kernel’s full classical contract from the IR alone — no external Python-side side-car required (#390).

import qamomile.circuit as qmc


@qmc.qkernel
def vqe_layer(theta: qmc.Float, depth: qmc.UInt = 2) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(2, name="q")
    for _ in qmc.range(depth):
        q = qmc.ry(q, theta)
    return qmc.measure(q)


block = vqe_layer.build(parameters=["theta"])
for slot in block.param_slots:
    print(slot.name, slot.kind.value, slot.ndim, slot.bound_value)
theta runtime_parameter 0 None
depth compile_time_bound 0 2

Why: after partial_eval folds a binding into a constant, the IR alone cannot distinguish “this constant came from a binding” from “this constant was a literal in the source”. For an outer DSL that calls a compiled @qkernel repeatedly with different bindings, that distinction is load-bearing — without param_slots the receiver cannot rebind. Qubit and Vector[Qubit] inputs are not part of the manifest; they continue to live in Block.input_values.

JSON / msgpack wire format for Blocks

qamomile.circuit.ir.serialize exposes dump_json / load_json and dump_msgpack / load_msgpack (with to_dict / from_dict for tooling). Both encoders write a top-level envelope {"schema_version": <int>, "block": <block dict>}; the current version is SCHEMA_VERSION = 1. Numpy payloads (e.g. ParamSlot.bound_value arrays) are wrapped with an explicit dtype allow-list; msgpack passes the bytes through natively, so msgpack output is typically more compact than JSON for numpy-heavy IR (#391).

import qamomile.circuit as qmc
from qamomile.circuit.ir.serialize import dump_msgpack, load_msgpack
from qamomile.qiskit import QiskitTranspiler


@qmc.qkernel
def superposition() -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(2, name="q")
    q = qmc.h(q)
    return qmc.measure(q)


affine = QiskitTranspiler().inline(superposition.block)
blob = dump_msgpack(affine)
restored = load_msgpack(blob)

Transpiler.inline() removes CallBlockOperations and advances the block to BlockKind.AFFINE, which is the input contract for both canonicalize above and the wire-format encoders here. (kernel.block returns the raw HIERARCHICAL trace; you must inline it before serializing or hashing.)

Why: this is the actual wire format that the canonical-form and parameter-manifest work above were preparing for. The decoder never does dynamic class resolution (every $type tag is routed through a hard-coded factory map), so loading is safe in the same sense as plain JSON / msgpack — no pickle-style arbitrary code execution. Scope is BlockKind.AFFINE and BlockKind.ANALYZED at the top level; nested HIERARCHICAL blocks embedded inside ControlledUOperation.block or CompositeGateOperation.implementation_block may legitimately pass through.

Bug Fixes

Documentation

Learn More