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.0Breaking Changes¶
qamomile.optimization.utilsis removed. The lone helperis_close_zerohas moved to the package-privateqamomile._utilsso it can be reused outside the quantum-optimization modules (e.g. byqamomile.linalg). If you imported it directly, switch your call sites to use the local equivalent (math.isclose(value, 0.0, abs_tol=1e-15)) —qamomile._utilsis intentionally private and not part of the supported API.MathematicalProblemConverter.decode()return type now depends on how the converter was built (#353). Converters constructed from anommx.v1.Instance(QAOAConverter,FQAOAConverter,QRACConverter, …) used to return aBinarySampleSet; they now return anommx.v1.SampleSet. Call sites that still need aBinarySampleSetshould switch to the new publicdecode_to_binary_sampleset(). Converters built from aBinaryModelare unchanged — they continue to return aBinarySampleSet.The
ommx.v1.Instancepassed toMathematicalProblemConverter/FQAOAConverteris no longer mutated (#353).Instance.to_qubo()rewrites the instance it is called on (it appends slack decision variables for non-binary vars and absorbs constraints into the objective via the penalty method), and the converters previously did this on the caller’s instance, silently changing it. The constructors now take a bytes-roundtrip deep copy at the entry point and runto_qubo()on the copy, so the caller’sInstanceis left untouched.
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.HermitianMatrix → Hamiltonian¶
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¶
pauli_evolvenow preserves SSAlogical_id(#354).pauli_evolveused to mint a freshArrayValuefor its result instead of bumping the SSA version of the input register vianext_version(), breaking the logical-id chain that every other gate op preserves. The user-visible symptom: a@qkernelthat called another@qkernelcontainingpauli_evolve(e.g.trotterized_time_evolution) and then calledqmc.measure(qreg)in the outer kernel silently dropped the measure —sample()returned[(None, count), ...]with an emptyclbit_map. The known workaround was to extract a single element viaqmc.measure(qreg[0]); that is no longer required, and the examples above use the naturalqmc.measure(qreg)form. A companion inline-pass fix propagates callee shape-dim UUIDs into the outervalue_mapso the resource allocator resolves the post-call register’s size correctly.Transpiler.transpile()rejects overlappingbindingsandparameters(#359). Passing the same name in bothbindingsandparameterswas ambiguous (placeholder vs runtime symbol) and silently miscompiled control-flow predicates that depended on parameter-array elements. The transpiler now raisesValueErrorup-front. Internally, the three classical-op fold sites (BinOp/CompOp/CondOp/NotOp) share a singleeval_utils.fold_classical_ophelper, so the runtime-parameter guard cannot drift across call sites.Visualization polish: Toffoli (CCX) renders with CX-style controls and a target-X; folded loop blocks now mark participating wires with control-style dots;
Sdg/Tdgget proper TeX labels;BinOparguments are expanded inCallBlocklabels; IR-internal placeholder names (?, tmp names) no longer leak into rendered labels.Loop affect analysis correctly handles nested control flow;
forloops detect all qubits when an array is passed to aCallBlock.Hamiltonian.to_numpy()performance: Pauli matrices are hoisted to module level so they are not rebuilt on every call.
Documentation¶
New Tutorial 07 — Hamiltonian Simulation (Suzuki–Trotter on the Rabi model).
New Tutorial 08 — Hermitian Decomposition (
HermitianMatrix.to_hamiltonian()end-to-end).New Tutorial 09 — Compilation & Transpilation, a deeper pipeline walkthrough including a QFixed measurement-lowering case study.
New Tutorial 10 — Quantum Error Correction (Shor’s 9-qubit code, syndrome measurement) (#352).
New Tutorial 11 — Steane Code (the
[[7,1,3]]CSS code with transversal Hadamard) (#352).New VQE for Hydrogen Molecule tutorial under
vqa/(#318).The MaxCut tutorial has been reformulated to start from spin variables and now seeds
AerSimulatorand NumPy with an explicit reproducibility caveat (#356).Index page now includes a chamomile-origin and pronunciation note.