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.2New 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:
subspace_hamiltonian(samples, hamiltonian)— Z-basis fast path. Vectorised XOR / parity computation; no matrix products. ReturnsH_subonly — the overlap matrix is assumed to be the identity, which holds when the supplied bitstrings are pairwise distinct. Deduplicate before calling, or use the generalised path below if duplicates may occur.generalized_subspace_matrices(samples, bases, hamiltonian)— returns(H_sub, S_sub)for non-orthogonal product-state bases (X / Y / Z mix); also the right entry point when Z-basis samples may include duplicates.solve_subspace(samples, hamiltonian, *, bases=None)— convenience wrapper. Withbases=Noneit buildsH_subvia the Z-basis fast path and runs a standard Hermitian eigendecomposition (np.linalg.eigh) on it (so distinct samples are assumed). Whenbasesis supplied, it solves the generalised problemH_sub v = λ S_sub v, whitening through the overlap eigendecomposition with a rank-deficient fallback governed byoverlap_tol.
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¶
Vector broadcast gates now enforce the affine type rule (#387).
qmc.h(qs)(and the other 11 single-qubit broadcast gates) on aVector[Qubit]previously left the input handle reusable when the return value was dropped, while the scalarqmc.h(q)correctly raisedQubitConsumedError. The Vector broadcast path now consumes its input handle up-front, mirroring_measure_vector_qubitandpauli_evolve. The emitted IR is unchanged — only the type-error side closes a hole.Visualization: empty bound
Dictno longer rendersForItemsas a folded box (#384). When aForItemsloop iterated over aDictbound to{}, the analyzer treated the empty binding as “never bound” and drew the folded-loop box even withfold_loops=False. The empty-but-bound case now collapses to a zero-iteration unfold (aVSkip).Visualization: BinOp operands referencing the loop variable no longer leak the display name (#384). Inside a
ForItemsbody, a BinOp likegamma * Jij(whereJijis the loop-variable bound entry) previously rendered withJijleft as a raw display name. The analyzer now consults the per-iteration_loop_<name>value when formatting BinOp operands, so labels render as0.5*gammaetc. and1*gammasimplifies togammavia the existing BinOp simplifier.
Documentation¶
Documentation site reorganised into four purpose-driven sections (#348). The previous
tutorial/+optimization/+vqa/+collaboration/split is replaced bytutorial/(compiler walk-throughs only),algorithm/(concrete algorithm examples — QAOA variants, VQE, error correction, Hamiltonian simulation, amplitude encoding, QSCI),usage/(per-module how-tos), andintegration/(external-platform notes). Every article carriestags:frontmatter (e.g.[algorithm, encoding, primitive]); per-tag pages and “Browse by tag” chip clouds are generated at build time into a gitignoreddocs/_build_src/so the committed source stays clean. A whitelist (ALLOWED_TAGS, enforced bytests/docs/test_tag_whitelist.py) keeps the taxonomy small.Existing tutorials moved to
algorithm/:hamiltonian_simulation,quantum_error_correction,steane_code,qaoa_graph_partition,qaoa_maxcut,vqe_for_hydrogen. The remaining tutorial numbers were tightened: the former Tutorial 08 (Hermitian decomposition) is now Tutorial 07 and the former Tutorial 09 (compilation & transpilation) is now Tutorial 08.optimization/binary_modelmoved tousage/binary_model;collaboration/qbraid_executormoved tointegration/qbraid_executor.New Möttönen Amplitude Encoding tutorial — four input modes, gate-budget verification, and an analytic expval check.
New Quantum Selected Configuration Interaction (QSCI) tutorial — VQE warm-up + Z-basis sampling + classical subspace diagonalization on the four-qubit transverse-field Ising model.
The QAOA graph-partition tutorial gains a “Visualize the QAOA Circuit” section that uses
MatplotlibDrawer(block).draw(fold_loops=False)to render the inlinedsuperposition_vector,ising_cost, andx_mixerpieces ofqaoa_state(#384).Stale
.ipynboutputs regenerated for QSCI,06_reuse_patterns,hamiltonian_simulation, and08_compilation_and_transpilation(JA). The QSCI tutorial also switches to mystmd’s{cite:p}directive backed by DOI auto-citation, so the Kanno et al. (2023) reference renders from10.48550/arXiv.2302.11320at build time (#380).CLAUDE.mdrefreshed with the current transpiler pipeline diagram (includingvalidate_entrypoint,substitute,resolve_parameter_shapes,unroll_recursion,affine_validate,classical_lowering,validate_symbolic_shapes), the bindings-vs-parameters contract, the IR Abstraction Principle, and the commits / PRs / issues workflow rules (#364).