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

Qamomile v0.12.4 renames the public API for controlling gates and quantum kernels from qmc.controlled to qmc.control and strengthens it. A single controlled gate can now take several control arguments at once, and the new control_indices keyword selects which slots of the control register act as active controls; the number of controls can also be given as a symbolic value. A new tutorial walks through the whole controlled-gate workflow end to end. On the optimization side, BinaryModel.from_higher_ising builds a BinaryModel from an Ising model with degree-3-and-higher interaction terms. It also closes a previously missing gap: you can now negate a Float handle with -theta.

pip install qamomile==0.12.4

Breaking Changes

qmc.controlled is now qmc.control

The controlled-gate entry point is renamed from qmc.controlled to qmc.control; qmc.controlled is no longer importable. The signature is unchanged — qmc.control(gate, num_controls=1) — so the migration is a mechanical rename at every call site. The ControlledGate object returned by the call keeps its class name (#413).

# before (v0.12.3)
cg = qmc.controlled(qmc.x, num_controls=2)

# after (v0.12.4)
cg = qmc.control(qmc.x, num_controls=2)

Controlled-gate index selection is now symbolic-mode-only

ControlledGate.__call__ no longer accepts the target_indices keyword, and controlled_indices is renamed to control_indices. The previous concrete index-spec form — selecting target / control slots of a single Vector by passing index lists alongside a concrete num_controls — is removed. Subset selection now lives in symbolic mode via control_indices (see New Features), so call sites that passed target_indices= / controlled_indices= must move the selection into a symbolic-num_controls call (#413).

New Features

Controlled gates: a more expressive qmc.control

qmc.control(gate, num_controls=...) turns any built-in gate (qmc.x, qmc.rx, qmc.p, ...) or user-defined @qmc.qkernel into its controlled version. Controlling a built-in without a hand-written wrapper has been possible since v0.12.2; this release renames the API to qmc.control and expands its functionality.

The following is a concrete-mode example (num_controls is a Python int):

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

# CCY is Y with two controls; defined without a wrapper kernel.
ccy = qmc.control(qmc.y, num_controls=2)


@qmc.qkernel
def demo() -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(3, name="q")
    q[0] = qmc.x(q[0])
    q[1] = qmc.x(q[1])
    q[0], q[1], q[2] = ccy(q[0], q[1], q[2])
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(demo)

In symbolic mode (num_controls is a qmc.UInt handle or a UInt expression such as n - 1) the control count is resolved at transpile time from bindings. New in this release, symbolic mode accepts a multi-argument control prefix — several positional controls (scalar Qubit, VectorView, Vector[Qubit], or a mix) whose qubit counts sum to num_controls — and a control_indices keyword that selects which slots of a single control register wire in as active controls while the rest pass through untouched:

@qmc.qkernel
def subset_demo(n: qmc.UInt, k_ctrls: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    pool = qmc.qubit_array(n, name="pool")
    tgt = qmc.qubit(name="tgt")
    pool[0] = qmc.x(pool[0])
    pool[1] = qmc.x(pool[1])
    pool[3] = qmc.x(pool[3])  # pool[2] stays |0> — it is the inactive slot
    # Only slots 0, 1, 3 wire in as controls; pool[2] passes through untouched.
    cg = qmc.control(qmc.x, num_controls=k_ctrls)
    pool, tgt = cg(pool, tgt, control_indices=[0, 1, 3])
    return qmc.measure(pool)


exe = QiskitTranspiler().transpile(subset_demo, bindings={"n": 4, "k_ctrls": 3})

See Tutorial 04 — Controlling Gates and Sub-Kernels for the end-to-end walk-through.

Higher-order Ising models: BinaryModel.from_higher_ising

BinaryModel.from_higher_ising(higher_ising, constant=0.0, simplify=False) builds a SPIN-type BinaryModel from a dict mapping index tuples to coefficients, with no upper bound on the interaction degree — cubic (s_i s_j s_k), quartic, and beyond. Repeated indices within a term are reduced using the spin identity s_i**2 = 1 (so (0, 0, 2) collapses to (2,)), and the indices are renumbered to a contiguous range (#382).

from qamomile.optimization.binary_model import BinaryModel

# Linear, quadratic, and a cubic (degree-3) interaction term.
higher_ising = {(0,): 1.0, (0, 1): 2.0, (0, 1, 3): 4.0}
model = BinaryModel.from_higher_ising(higher_ising, constant=8.0)
print(model.num_bits, model.coefficients)
3 {(0,): 1.0, (0, 1): 2.0, (0, 1, 2): 4.0}

Unary negation of Float handles

A Float handle now supports unary minus, so you can write -theta directly inside a @qmc.qkernel instead of 0.0 - theta. The negation lowers to a multiplication by -1.0, so when theta is bound at compile time it folds into a literal angle in the emitted circuit (#401).

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


@qmc.qkernel
def neg_cancel(theta: qmc.Float) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(1, name="q")
    q = qmc.ry(q, theta)
    q = qmc.ry(q, -theta)  # unary minus on a Float handle; the rotations cancel
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(neg_cancel, bindings={"theta": 1.0})

Internal Changes

Two internal changes round out the release. The first supports the controlled-gate work above; the second is independent.

Controlled-gate IR consolidation

The IR previously carried a separate IndexSpecControlledU operation for index-selected controlled gates, alongside ConcreteControlledU and SymbolicControlledU. This release retires IndexSpecControlledU and folds its capability into SymbolicControlledU, which gains two fields: num_control_args (how many leading operands form the control prefix, for the multi-argument form) and control_indices (the selected control slots, or None for the whole register). The JSON / msgpack wire format gains corresponding additive fields; because an old payload decodes to num_control_args=1 / control_indices=None, SCHEMA_VERSION stays at 1 and previously serialized blocks still load.

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


@qmc.qkernel
def mcx_pool(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    ctrls = qmc.qubit_array(n, name="ctrls")
    tgt = qmc.qubit(name="tgt")
    cg = qmc.control(qmc.x, num_controls=n)  # symbolic whole-pool control
    ctrls, tgt = cg(ctrls, tgt)
    return qmc.measure(ctrls)


affine = QiskitTranspiler().inline(mcx_pool.build(n=3))
restored = load_msgpack(dump_msgpack(affine))

Why: the symbolic multi-argument control prefix and control_indices selection in New Features need a single IR operation that represents both the whole-register and subset cases without a second op type. Consolidating onto SymbolicControlledU keeps the emit passes (Qiskit, QURI Parts, CUDA-Q) on one controlled-gate code path instead of branching on three operations (#413).

Earlier, broader quantum-rebind validation

Forbidden reassignment of a quantum variable — rebinding a Qubit / Vector[Qubit] to a different value, overwriting a parameter with a fresh allocation, chained assignments, mismatched tuple unpacking — is now detected when the @qmc.qkernel is defined and raises QubitRebindError immediately, instead of surfacing later during build() / transpilation. The analyzer covers more of these patterns than before, and the message names the offending pattern and a suggested fix.

Why: unlike the item above, this is not part of the controlled-gate work. A rebind violation is a structural mistake in the kernel source, so reporting it at definition time — with a concrete pattern-and-fix message — catches the bug at the point the author can act on it, rather than after an unrelated transpile step. Kernels that previously slipped a rebind violation past the analyzer now fail at definition time (#403).

Bug Fixes

Documentation

Learn More