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

Qamomile v0.12.0 ships a built-in Suzuki–Trotter time evolution (orders 1, 2, and any even order ≥ 4), a new qamomile.linalg module that turns dense Hermitian matrices into exact Pauli Hamiltonians, and the compiler features that make those algorithms expressible as natural Python: self-recursive @qkernel, Vector[Observable] parameters, and bool parameters. In the quantum-optimization modules, MathematicalProblemConverter.decode() is now polymorphic — converters built from an ommx.v1.Instance return an ommx.v1.SampleSet so feasibility, original-objective, and per-constraint diagnostics are available directly through the OMMX API. Three new tutorials cover the new surface end-to-end, and two more tutorials add quantum error correction coverage (Shor’s 9-qubit code and the Steane [[7,1,3]] code).

pip install qamomile==0.12.0

Breaking Changes

New Features

Suzuki–Trotter time evolution as a built-in algorithm

qamomile.circuit.algorithm.trotterized_time_evolution applies exp(-i · gamma · H) to a register, where H = sum_k hamiltonian[k]. The order argument selects the formula: 1 for Lie–Trotter, 2 for Strang, or any positive even integer for Suzuki’s fractal recursion. The public helper validates the inputs and delegates to internal @qkernel building blocks; under a concrete order binding, the self-recursive Suzuki step is unrolled so the emitted program contains no recursive calls. Ordinary loop structure such as the outer Trotter step loop may still be emitted as a backend loop when the backend supports it (#337, #342).

import qamomile.circuit as qmc
import qamomile.observable as qm_o
from qamomile.circuit.algorithm import trotterized_time_evolution
from qamomile.qiskit import QiskitTranspiler


@qmc.qkernel
def rabi(
    Hs: qmc.Vector[qmc.Observable],
    gamma: qmc.Float,
    order: qmc.UInt,
    step: qmc.UInt,
) -> qmc.Vector[qmc.Bit]:
    qreg = qmc.qubit_array(1, "q")
    qreg = trotterized_time_evolution(qreg, Hs, order, gamma, step)
    return qmc.measure(qreg)


# H = (omega/2) Z + (Omega/2) X — non-commuting halves so Trotter error is real.
Hs = [0.5 * 1.2 * qm_o.Z(0), 0.5 * 0.8 * qm_o.X(0)]

transpiler = QiskitTranspiler()
exe = transpiler.transpile(
    rabi, bindings={"Hs": Hs, "order": 4, "step": 8, "gamma": 1.0}
)

See Tutorial 07 for a full Rabi-oscillation walkthrough that also verifies the textbook Δt^{2k} convergence rate of S_k.

qamomile.linalg.HermitianMatrixHamiltonian

The new qamomile.linalg module turns a dense 2**n × 2**n Hermitian NumPy array into an exact Pauli Hamiltonian via the Fast Walsh–Hadamard Transform in O(n · 4**n). The result plugs straight into pauli_evolve, expval, and the optimization helpers. The companion Hamiltonian.to_numpy() round-trips back to a dense matrix (#336).

import numpy as np
from qamomile.linalg import HermitianMatrix

X = np.array([[0, 1], [1, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
I2 = np.eye(2, dtype=complex)

# Two-site transverse-field Ising: -Z0 Z1 - 0.7 (X0 + X1)
M = -np.kron(Z, Z) - 0.7 * (np.kron(I2, X) + np.kron(X, I2))

H = HermitianMatrix(M).to_hamiltonian()

HermitianMatrix validates on construction (2D square, power-of-two dimension, and Hermitian according to np.allclose(matrix, matrix.conj().T, atol=atol)), supports the usual + / - / scalar * / / arithmetic, and its qubit 0 corresponds to the least-significant bit of the computational-basis index — matching Qiskit. Because the Hermitian check uses NumPy’s default relative tolerance, very large matrix entries can admit small relative asymmetries even when they are larger than atol; to_hamiltonian() still rejects decompositions with non-negligible imaginary Pauli coefficients. See Tutorial 08.

Polymorphic decode() for OMMX round-trip

MathematicalProblemConverter.decode() now returns the type that matches how the converter was built. Converters built from an ommx.v1.Instance return an ommx.v1.SampleSet evaluated against the original (un-penalized) instance, so feasibility, objective, and per-constraint violations are available through OMMX’s own API (.summary, .summary_with_constraints, .best_feasible, .feasible, .objectives). Converters built from a BinaryModel keep returning a BinarySampleSet (#353).

The new decode_to_binary_sampleset() is the public escape hatch for callers that need the QUBO-domain BinarySampleSet — for example, to drive a classical optimizer with the penalty-included energy. Internally decode() calls it and routes the result through OMMX’s evaluate_samples for converters built from an ommx.v1.Instance.

The concrete QRAC converters (QRAC21Converter, QRAC31Converter, QRAC32Converter, and QRACSpaceEfficientConverter) follow the same convention in decode(rounded_spins_list): the QRAC-rounded spin assignments are wrapped into a synthetic sample result and routed through the base class’s polymorphic decode, so QAOA, FQAOA, and QRAO share one downstream API. Converters built from an ommx.v1.Instance are also safe to call repeatedly now — the Instance is deep-copied on construction (see Breaking Changes), so the caller’s instance is left untouched.

from qamomile.optimization.qaoa import QAOAConverter
from qamomile.qiskit import QiskitTranspiler

# Assume ommx_instance, gammas, and betas are prepared as in the tutorial.
transpiler = QiskitTranspiler()
converter = QAOAConverter(ommx_instance)        # built from ommx.v1.Instance
exe = converter.transpile(transpiler, p=2)

# sample_result is obtained by running exe.sample(...).result() — see tutorial.
sample_set = converter.decode(sample_result)      # ommx.v1.SampleSet
print(sample_set.best_feasible.objective)

# Need the QUBO-domain energy for a classical optimizer instead?
binary_ss = converter.decode_to_binary_sampleset(sample_result)

See the updated graph-partition tutorial for an end-to-end run that uses both return shapes.

Internal Changes

The Trotter feature above motivated a cluster of compiler/frontend improvements. Some are direct requirements for writing trotterized_time_evolution naturally, and others came from hardening the same compile-time control-flow path. They are all independently usable.

Self-recursive @qkernel

A @qkernel may now call itself directly. The transpiler resolves the self-call by iterating the inline + partial-eval passes under the concrete bindings, so the emitted program contains no recursive CallBlock structure once the recursion driver has folded to a base case (#337).

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


@qmc.qkernel
def suzuki_step(
    q: qmc.Vector[qmc.Qubit],
    hamiltonian: qmc.Vector[qmc.Observable],
    order: qmc.UInt,
    dt: qmc.Float,
) -> qmc.Vector[qmc.Qubit]:
    if order == 2:
        # Strang base case: palindromic sweep over Hamiltonian terms.
        last = hamiltonian.shape[0] - 1
        half_dt = 0.5 * dt
        for i in qmc.range(last):
            q = qmc.pauli_evolve(q, hamiltonian[i], half_dt)
        q = qmc.pauli_evolve(q, hamiltonian[last], dt)
        for j in qmc.range(last):
            rev = last - 1 - j
            q = qmc.pauli_evolve(q, hamiltonian[rev], half_dt)
    else:
        # Suzuki recursion: S_{2k} built from five S_{2k-2} blocks.
        p = 1.0 / (4.0 - 4.0 ** (1.0 / (order - 1)))
        w = 1.0 - 4.0 * p
        q = suzuki_step(q, hamiltonian, order - 2, p * dt)
        q = suzuki_step(q, hamiltonian, order - 2, p * dt)
        q = suzuki_step(q, hamiltonian, order - 2, w * dt)
        q = suzuki_step(q, hamiltonian, order - 2, p * dt)
        q = suzuki_step(q, hamiltonian, order - 2, p * dt)
    return q


@qmc.qkernel
def suzuki_demo(
    hamiltonian: qmc.Vector[qmc.Observable],
    order: qmc.UInt,
    dt: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
    # suzuki_step expects a register; measure the whole register and return it.
    qreg = qmc.qubit_array(1, "q")
    qreg = suzuki_step(qreg, hamiltonian, order, dt)
    return qmc.measure(qreg)


Hs = [qm_o.Z(0), qm_o.X(0)]
transpiler = QiskitTranspiler()
exe = transpiler.transpile(
    suzuki_demo, bindings={"hamiltonian": Hs, "order": 4, "dt": 0.3}
)

Why: the Suzuki fractal S_{2k}(Δt) = S_{2k-2}(p_k Δt)² · S_{2k-2}((1-4p_k)Δt) · S_{2k-2}(p_k Δt)² is most naturally written as a kernel that calls itself with order - 2. Without the unroll loop, that recursion would not terminate at compile time.

order (or any other parameter that drives the recursion depth) must be bound to a compile-time constant when transpiling, so the base-case if can fold and stop the unroll.

Vector[Observable] parameter type

pauli_evolve now accepts an observable parameter, and @qkernel accepts Vector[qmc.Observable] as a parameter type bound at transpile time (#340).

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


@qmc.qkernel
def step(
    Hs: qmc.Vector[qmc.Observable],
    dt: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
    qreg = qmc.qubit_array(1, "q")
    for i in qmc.range(Hs.shape[0]):
        qreg = qmc.pauli_evolve(qreg, Hs[i], dt)
    return qmc.measure(qreg)


# Bind the term list at transpile time:
QiskitTranspiler().transpile(
    step, bindings={"Hs": [qm_o.Z(0), qm_o.X(0)], "dt": 0.3}
)

Why: a Trotter step iterates over the sub-Hamiltonians H_k of H = sum_k H_k, so the term list has to enter the kernel as a parameter — without Vector[Observable] you would have to bake the specific H into the kernel source and re-trace per Hamiltonian.

bool parameter type

@qkernel now accepts bool parameters alongside the existing scalar handle types (#346).

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


@qmc.qkernel
def maybe_h(q: qmc.Qubit, do_it: bool) -> qmc.Qubit:
    if do_it:
        q = qmc.h(q)
    return q


@qmc.qkernel
def maybe_h_entry(do_it: bool) -> qmc.Bit:
    q = qmc.qubit(name="q")
    q = maybe_h(q, do_it)
    return qmc.measure(q)


# Bind the bool flag at transpile time:
QiskitTranspiler().transpile(maybe_h_entry, bindings={"do_it": True})

Why: the Trotter work hardened @qkernel’s handling of if-statements that branch on a compile-time-resolvable condition. Once if was solid, accepting bool as a parameter type was the natural completion of the surface — bool is the type that if-conditions most directly want, and there was no reason to keep it second-class.

MLIR-style IR pretty-printer

qamomile.circuit.ir.pretty_print_block produces a human-readable dump of a Block at any pipeline stage (HIERARCHICAL / AFFINE / ANALYZED), useful for debugging the transpiler. Unlike the items above, this is not part of the Trotter feature — it landed alongside the new compilation tutorial (Tutorial 09), which uses it to make the pipeline visible to the reader. The output is for human inspection only and may change between releases.

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


@qmc.qkernel
def ghz(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
    q = qmc.qubit_array(n, "q")
    q[0] = qmc.h(q[0])
    for i in qmc.range(1, n):
        q[0], q[i] = qmc.cx(q[0], q[i])
    return qmc.measure(q)


block = QiskitTranspiler().to_block(ghz, bindings={"n": 3})
print(pretty_print_block(block))

Bug Fixes

Documentation

Learn More