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

Qamomile v0.12.2 adds two algorithmic primitives — Möttönen amplitude encoding for arbitrary state preparation and sample-based subspace diagonalization (QSCI) — plus quality-of-life improvements to the compiler frontend: qmc.controlled now accepts built-in gate functions like qmc.ry directly without an explicit @qkernel wrapper. Quantum optimization gains a LocalSearch post-processor that operates directly on BinaryModel or ommx.v1.Instance. The documentation site has been reorganised into four purpose-driven sections (tutorial/, algorithm/, usage/, integration/) with tag-based cross-discovery, and ships two new tutorials covering the new primitives.

pip install qamomile==0.12.2

New Features

Möttönen amplitude encoding

qamomile.circuit.algorithm.amplitude_encoding(qubits, amplitudes) prepares an n-qubit state with amplitudes proportional to a given non-zero complex vector a (the input is normalised automatically before encoding) from |0⟩^⊗n, using the uniformly-controlled-rotation construction of Möttönen, Vartiainen, Bergholm and Salomaa (arXiv:quant-ph/0407010). Real amplitudes (signed) use a single Ry cascade (2^n - 1 rotations, 2^n - 2 CNOTs); complex amplitudes add a second Rz cascade. Complex inputs with identically-zero imaginary part are silently coerced to the cheaper real path (#383).

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


@qmc.qkernel
def prepare_real() -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(2, "q")
    q = amplitude_encoding(q, [1.0, 2.0, 3.0, 4.0])
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(prepare_real)

The same amplitude_encoding helper also accepts a Vector[Float] kernel parameter whose values are known at compile time:

@qmc.qkernel
def prepare_via_binding(amps: qmc.Vector[qmc.Float]) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(2, "q")
    q = amplitude_encoding(q, amps)
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(
    prepare_via_binding, bindings={"amps": [1.0, 2.0, 3.0, 4.0]}
)

For hybrid optimisation loops that need to re-bind a single compiled circuit to many amplitude vectors at run time, use amplitude_encoding_from_angles(q, ry_angles, rz_angles=None) with pre-computed Möttönen angles. The classical angle helpers compute_mottonen_amplitude_encoding_ry_angles and compute_mottonen_amplitude_encoding_rz_angles are exported from qamomile.linalg and take the amplitude vector directly:

import qamomile.circuit as qmc
from qamomile.circuit.algorithm import amplitude_encoding_from_angles
from qamomile.linalg import (
    compute_mottonen_amplitude_encoding_ry_angles,
    compute_mottonen_amplitude_encoding_rz_angles,
)
from qamomile.qiskit import QiskitTranspiler


@qmc.qkernel
def prepare_from_angles(
    ry_a: qmc.Vector[qmc.Float], rz_a: qmc.Vector[qmc.Float]
) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(2, "q")
    q = amplitude_encoding_from_angles(q, ry_a, rz_a)
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(
    prepare_from_angles, parameters=["ry_a", "rz_a"]
)

# Re-bind on every iteration of the hybrid loop:
amps = [1 + 0j, 1j, -1 + 0j, -1j]
ry_angles = compute_mottonen_amplitude_encoding_ry_angles(amps).tolist()
rz_angles = compute_mottonen_amplitude_encoding_rz_angles(amps).tolist()

For details, see Möttönen Amplitude Encoding.

Sample-based subspace diagonalization (QSCI)

A new qamomile.linalg.subspace module turns a finite set of bitstring samples — drawn from a quantum state in the Z basis, or product states in mixed X/Y/Z Pauli eigenbases — into a projected Hamiltonian and (where needed) overlap matrix, then solves the resulting (generalised) eigenvalue problem on the classical side. This is the building block for Quantum Selected Configuration Interaction (QSCI; Kanno et al., arXiv:2302.11320) and related sample-based variational methods, where the variational principle guarantees E_QSCI ≥ E_exact even for noisy quantum inputs (#376).

Three public helpers are exposed from qamomile.linalg:

import qamomile.observable as qm_o
from qamomile.linalg import solve_subspace

n = 4
H = qm_o.Hamiltonian(num_qubits=n)
for i in range(n - 1):
    H += qm_o.Z(i) * qm_o.Z(i + 1) * (-1.0)
for i in range(n):
    H += qm_o.X(i) * (-0.7)

samples = [
    (0, 0, 0, 0), (1, 1, 1, 1), (1, 0, 0, 0),
    (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1),
]
eigvals, eigvecs = solve_subspace(samples, H)
print(f"ground-state estimate: {eigvals[0]:+.6f}")

Qubit 0 is the least-significant bit of the computational-basis index, matching Hamiltonian.to_numpy() and HermitianMatrix. The Y-eigenstate convention is |Y, 0⟩ = |+i⟩ (i.e. Y |+i⟩ = +|+i⟩), consistent with the basis-rotation pulse S† H used to map a Y measurement onto Z.

For details, see Quantum Selected Configuration Interaction (QSCI).

qmc.controlled accepts built-in gate functions

qmc.controlled previously required a @qmc.qkernel-wrapped callable, forcing a one-line boilerplate wrapper for every primitive you wanted to control. It now accepts plain built-in gate functions (qmc.rx, qmc.h, qmc.cp, …) directly, inspects the signature, and auto-synthesises the wrapper internally. The synthesised wrapper is cached per callable, so the only real cost is on the first call (#374).

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


controlled_ry = qmc.controlled(qmc.ry)


@qmc.qkernel
def controlled_ry_demo(angle: qmc.Float) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(2, "q")
    q[0] = qmc.h(q[0])
    q[0], q[1] = controlled_ry(q[0], q[1], angle=angle)
    return qmc.measure(q)


exe = QiskitTranspiler().transpile(controlled_ry_demo, parameters=["angle"])

The classical argument keyword (angle=... above) is the underlying gate’s own parameter name, not a generated one — qmc.rx / qmc.ry / qmc.rz use angle, qmc.p uses theta. qmc.controlled continues to accept hand-written @qmc.qkernel callables unchanged for cases that need custom logic (e.g. multiple primitive gates fused into a single controlled block).

LocalSearch post-processor on BinaryModel

qamomile.optimization.post_process.LocalSearch is a greedy single-bit-flip local-search energy minimizer for binary optimisation models. It accepts either a BinaryModel (SPIN or BINARY vartype, arbitrary order including HUBO) or an ommx.v1.Instance. Internally it converts to a SPIN representation for the search, then maps the result back to the caller’s original vartype. Two flip-selection strategies are exposed via LocalSearchMethod: BEST (largest single-step energy decrease) and FIRST (first improving flip in index order) (#332).

from qamomile.optimization.binary_model.model import BinaryModel
from qamomile.optimization.post_process import LocalSearch, LocalSearchMethod

model = BinaryModel.from_ising(
    linear={0: 0.5},
    quad={(0, 1): -1.0, (1, 2): -1.0},
)
ls = LocalSearch(model)
sample_set = ls.run(initial_state=[1, 1, -1], method=LocalSearchMethod.BEST)

When constructed from a BinaryModel, run() returns a BinarySampleSet with a single sample in the model’s original vartype. When constructed from an ommx.v1.Instance, run() returns an ommx.v1.Solution evaluated against the caller’s original (still-constrained) instance — feasibility and per-constraint diagnostics flow through OMMX’s own API. On an Instance-built LocalSearch, run() also accepts an ommx.v1.Solution as initial_state to warm-start from another solver; passing a Solution on a BinaryModel-built LocalSearch raises ValueError because there is no instance to interpret the Solution against. The caller’s instance is never mutated: LocalSearch deep-copies on construction before calling to_hubo(), so slack variables added during HUBO lowering never leak back.

Internal Changes

Public get_size for Vector length introspection

qamomile.circuit.frontend.handle.get_size(arr) is now a supported public helper for reading the qubit / element count of a Vector[Qubit] / Vector[Float] handle at trace time. It returns the size as a Python int once the leading shape entry has been resolved to a compile-time constant — either a literal int from qmc.qubit_array(N, ...) or a UInt handle whose underlying Value is constant (set by uint(literal), _create_bound_input, or partial evaluation). When the leading dimension is still symbolic, get_size raises ValueError, so callers that want to handle unresolved sizes must catch it explicitly. The same helper was previously a private _get_size inside stdlib/qft and is now used from both qft and the new amplitude_encoding module, so authors of new high-level algorithm building blocks have one canonical way to ask “how big is this register?”.

Why: the Möttönen cascade depth and the QFT swap layout both need to read the register size at trace time. Promoting the helper out of stdlib avoids a second copy in algorithm/ and gives third-party algorithm authors the same primitive the bundled algorithms use.

Bug Fixes

Documentation

Learn More