QAOA components
Quanifi ships five <Fw>QAOA solvers, five <Fw>QAOACircuit fixed-angle
builders, and one framework-neutral QuantumQAOAEvaluator — eleven
processors that together let any QAOA construction (Qiskit, Cirq, Qrisp,
PennyLane, pyQuil) run on any counts engine (Aer, Cirq, Qrisp, PennyLane,
Braket, Q#, pyQuil) and be scored by one shared metric definition. Since
0.2.0 the solvers are train-only: they no longer sample, have no Shots
property, and emit the trained, bound circuit as portable OpenQASM 2.0 —
exactly the way the fixed-angle builders already did. See "Migration from
0.1.0" below if you have saved flows or scripts built against the old
sampling solvers.
Install the whole nifi_extensions/ directory, including its lowercase
helper modules (qaoa_contract.py, qiskit_qaoa.py, cirq_qaoa.py,
pennylane_qaoa.py, qrisp_qaoa.py); copying a single processor file is not
sufficient. pyQuil's construction lives in the existing
pyquil_processor.py/pyquil_components.py, not a new module.
Chain and conventions
flowchart LR
H[Any Hamiltonian/Problem producer] --> S["<Fw>QAOA (trained betas/gammas)"]
H --> B["<Fw>QAOACircuit (given betas/gammas)"]
S --> E["Any counts engine (7): Aer | Cirq | Qrisp | PennyLane | pyQuil | Braket | QSharp"]
B --> E
E --> V[QuantumQAOAEvaluator]
V --> R[QuanifiReport]
Both the solver and the builder of a framework emit the qasm2 content plus
circuit.*/builder.*/hamiltonian.json/qaoa.(layers|betas|gammas...)
attributes — string-identical, because each framework's circuit is
constructed by exactly one function (in its helper module), called with
property angles by the builder and with trained angles by the solver. Any
counts engine reads that qasm2, adds measurement and samples it; all
upstream attributes ride through unchanged (NiFi FlowFile merge).
QuantumQAOAEvaluator then computes every sample-dependent result from the
counts plus hamiltonian.json, and stamps qaoa.builder/qaoa.engine so
each cell of the resulting N×M matrix is self-describing.
Shared conventions across all 11 processors:
- Circuit: Hadamard on every qubit, then for each layer
l = 1..p:U_C(gamma_l) = exp(-i*gamma_l*H)followed byRX(2*beta_l)on every qubit. Identity terms are omitted from the circuit (they contribute only global phase) but always included in energies. The objective is minimised. - Bit order: q0-left everywhere — qubit 0 is the leftmost character of
every bitstring key and
circuit.bit_order/sim.bit_orderboth readq0_left. - Energy of a bitstring
z:E(z) = sum_t c_t * prod_{i in t} (1 - 2*z_i). - Width limits (guardrails, not performance promises): 1–16 qubits, 1–16 layers, 1–4096 Hamiltonian terms, 1–10000 Max Iterations, seed 0–4294967295.
- Only diagonal (I/Z) cost Hamiltonians are accepted; identity-only
Hamiltonians are rejected by every builder and solver (they have no γ
parameters to train or sweep), but are accepted by the evaluator (their
ratios become
1.0— see below). - Input rule shared by builders and solvers. If the incoming FlowFile
already carries a circuit (
circuit.formatis non-blank — the rebuild path), the cost Hamiltonian is read from thehamiltonian.jsonattribute, which is how a trained solver's output can be re-built by any framework's builder via${qaoa.betas}/${qaoa.gammas}. Otherwise the Hamiltonian comes from FlowFile content, and requireshamiltonian.format=sparse_pauli_op_jsonfrom an upstream<Fw>Hamiltonian,MaxCutProblem,QuboToHamiltonianor similar encoder.
Processor reference
| Processor | Input → output | Error attribute |
|---|---|---|
QiskitQAOA |
Diagonal Hamiltonian → trained qasm2 + training attributes | qaoa.error |
CirqQAOA |
Diagonal Hamiltonian → trained qasm2 + training attributes | qaoa.error |
PennylaneQAOA |
Diagonal Hamiltonian → trained qasm2 + training attributes | qaoa.error |
QrispQAOA |
Diagonal Hamiltonian → trained qasm2 + training attributes | qaoa.error |
PyquilQAOA |
Diagonal Hamiltonian → trained qasm2 + training attributes | qaoa.error |
QiskitQAOACircuit |
Diagonal Hamiltonian + Betas/Gammas → bound qasm2 | qaoa.error |
CirqQAOACircuit |
Diagonal Hamiltonian + Betas/Gammas → bound qasm2 | qaoa.error |
PennylaneQAOACircuit |
Diagonal Hamiltonian + Betas/Gammas → bound qasm2 | qaoa.error |
QrispQAOACircuit |
Diagonal Hamiltonian + Betas/Gammas → bound qasm2 | qaoa.error |
PyquilQAOACircuit |
Diagonal Hamiltonian + Betas/Gammas → bound qasm2 | qaoa.error |
QuantumQAOAEvaluator |
Counts (sim.bit_order=q0_left) + Hamiltonian → same counts + QAOA metrics |
qaoa.error |
All properties support FlowFile-attribute Expression Language
(FLOWFILE_ATTRIBUTES scope), for example Betas="${qaoa.betas}".
Validation errors route to failure, preserve the input content, and set
qaoa.error. Every changed processor carries a version bump (all five
solvers and QuantumQAOAEvaluator are 0.2.0/0.1.0 respectively; the four
new builders are 0.1.0; PyquilQAOACircuit was rewritten to 0.2.0).
Solver properties (identical text across all five frameworks)
| Property | Required | Default | Validator | EL scope | Rule |
|---|---|---|---|---|---|
| Layers | yes | 2 |
POSITIVE_INTEGER | FLOWFILE_ATTRIBUTES | 1..16 |
| Optimizer | yes | COBYLA |
allowable list (per framework, below) | none | must be in list |
| Max Iterations | yes | 100 |
POSITIVE_INTEGER | FLOWFILE_ATTRIBUTES | 1..10000 |
| Initial Parameters | yes | random |
NON_EMPTY | FLOWFILE_ATTRIBUTES | random, zeros, or a JSON/comma list of exactly 2p finite angles, betas then gammas |
| Random Seed | no | (unset) | NON_NEGATIVE_INTEGER | FLOWFILE_ATTRIBUTES | unset = nondeterministic; 0..4294967295 |
| Mixer Type (QrispQAOA only, inserted at index 1) | yes | RX |
allowable RX/XY |
none | Qrisp-only |
Per-framework Optimizer lists: QiskitQAOA
COBYLA/SPSA/NELDER_MEAD/L_BFGS_B; CirqQAOA/PennylaneQAOA
COBYLA/NELDER_MEAD/POWELL/L_BFGS_B; QrispQAOA
COBYLA/NELDER_MEAD/POWELL; PyquilQAOA COBYLA/POWELL/L_BFGS_B.
Random Seed seeds initial-parameter generation and any optimizer-internal
sampling; it does not seed the downstream simulator's measurement
sampling, which has its own Random Seed property. A finite result with an
exhausted iteration budget still routes to success, with
qaoa.converged=false and the optimizer's own message; downstream logic can
decide whether to accept an unconverged result. These are local minimizers —
a converged result is not guaranteed to be the global ground energy.
Builder properties (identical text across all five frameworks)
| Property | Required | Default | Validator | EL scope | Rule |
|---|---|---|---|---|---|
| Layers | yes | 1 |
POSITIVE_INTEGER | FLOWFILE_ATTRIBUTES | 1..16 |
| Betas | yes | 0.39269908169872414 |
NON_EMPTY | FLOWFILE_ATTRIBUTES | JSON or comma list, exactly Layers finite floats |
| Gammas | yes | 0.7853981633974483 |
NON_EMPTY | FLOWFILE_ATTRIBUTES | same |
| Mixer Type (QrispQAOACircuit only, appended last) | yes | RX |
allowable RX/XY |
none | Qrisp-only |
The default angles (pi/8, pi/4) are an example setting for demos, not a
claim of optimality for an arbitrary cost.
Attribute contracts
Circuit contract (qaoa_contract.circuit_attributes; every builder and solver)
| Attribute | Value |
|---|---|
circuit.format |
qasm2 |
circuit.qasm2 |
same text as FlowFile content |
circuit.qasm3, circuit.cirq_json |
"" (blanked stale so QuanifiReport prefers qasm3 when a different processor set it) |
circuit.svg |
Cirq's own SVG for Cirq processors; "" otherwise (blanked stale so QuanifiReport prefers a stale SVG only when nothing fresher exists) |
circuit.diagram |
framework-native text diagram |
circuit.num_qubits, circuit.depth, circuit.gate_count, circuit.nonlocal_gates |
from qasm2_profile (identical semantics across all 5 rows — see "Portable qasm2 subset" below) |
circuit.gate_counts |
JSON {gate: count}, sorted keys |
circuit.t_count |
"0" |
circuit.framework, builder.framework |
qiskit / cirq / pennylane / qrisp / pyquil |
builder.component |
the emitting processor's class name |
circuit.bit_order |
q0_left |
circuit.algorithm |
qaoa |
circuit.num_parameters |
"0" (bound, no free parameters remain) |
circuit.marked_state, circuit.num_iterations, circuit.error |
"" |
mime.type |
text/plain |
report.type |
circuit |
PennyLane processors additionally set circuit.global_phase_dropped
("true"/"false") — PennyLane's to_openqasm export drops a gphase
statement if present, and this attribute records whether one was.
Builder/solver qaoa.* contract (qaoa_contract.builder_attributes; every builder and solver)
Blanks every EVALUATOR_KEYS entry (stale-key hygiene — see "Stale-attribute
rules" below), then adds: hamiltonian.json (the raw wire text the circuit
was built from), qaoa.num_qubits, qaoa.layers, qaoa.betas and
qaoa.gammas (each json.dumps([float, ...]), full repr precision,
lossless), qaoa.mixer_type ("RX" or "XY"), qaoa.error="".
Solver-only qaoa.* extension (qaoa_contract.solver_attributes)
| Attribute | Value |
|---|---|
qaoa.framework |
qiskit/cirq/pennylane/qrisp/pyquil |
qaoa.optimal_parameters |
json.dumps(betas + gammas), full precision, no rounding |
qaoa.optimal_value |
repr(float), the optimizer's own final objective |
qaoa.energy_method |
see the table below |
qaoa.energy_shots |
"1024" for QiskitQAOA; "" for the other four |
qaoa.optimizer, qaoa.max_iterations, qaoa.cost_function_evals, qaoa.num_iterations |
num_iterations always equals cost_function_evals |
qaoa.converged |
scipy's result.success for Cirq/PennyLane/pyQuil; evals < maxiter for Qiskit/Qrisp |
qaoa.optimizer_message |
scipy's result.message, else "" |
qaoa.seed |
the seed, or "" if unset — solvers do not write run.seed; that attribute belongs to the downstream engine's own sampling seed |
qaoa.elapsed_seconds, perf.elapsed_seconds |
"{:.4f}" training wall-clock time |
Solvers emit no sim.* keys — those come only from a downstream counts
engine.
qaoa.energy_method per solver
| Solver | energy_method |
Notes |
|---|---|---|
QiskitQAOA |
sampled_statevector |
Trained via qiskit_algorithms.QAOA with a seeded StatevectorSampler. The objective is sampled, not exact — StatevectorSampler.default_shots == 1024. Measured gap between the sampled and exact eigenvalue at optimal_point: 0.030 at 1024 shots, 4.4e-4 at 100000 shots (about 15x slower). qaoa.energy_shots="1024" records this. |
CirqQAOA |
exact_statevector |
Hand-rolled sympy ladder, cirq.Simulator(dtype=complex128) exact expectation. |
PennylaneQAOA |
exact_statevector |
default.qubit analytic qml.expval. |
QrispQAOA |
exact_probabilities |
Qrisp's own QAOAProblem.optimization_routine, exact probabilities that Qrisp itself rounds to 5 decimals (see "Qrisp energy rounding" below). |
PyquilQAOA |
exact_statevector |
pyquil_components.expectation, exact local expectation. |
QuantumQAOAEvaluator contract
One property, Hamiltonian (required, default ${hamiltonian.json},
NON_EMPTY, FLOWFILE_ATTRIBUTES EL) — either the neutral wire JSON
(hamiltonian.json from any upstream builder/solver) or the indexed DSL
(Z0 Z1 - 0.5 Z2 + 1, width inferred from the counts). Validation order:
sim.bit_ordermust equalq0_left(missing or any other value fails).- Parse the counts content: a non-empty JSON object with equal-width binary keys and finite, non-negative, non-bool values (integer counts or probabilities), total > 0.
- Resolve the Hamiltonian (blank → a clear "no cost Hamiltonian" error).
- It must be diagonal — unlike the builders/solvers, identity-only is
allowed here (ratios become
1.0, since there is nothing to distinguish). - Counts width must equal the Hamiltonian's qubit count.
- If
circuit.num_qubitsis present and disagrees, fail.
Metric definitions (content passes through unchanged):
| Attribute | Definition |
|---|---|
qaoa.best_measurement |
the observed key minimising (round(E, 9), key) — lowest energy, ties broken lexicographically so it is comparable across engines/counts totals |
qaoa.best_value |
repr(E(best)) |
qaoa.sampled_expectation |
sum(counts[k] * E(k)) / sum(counts.values()) |
qaoa.exact_minimum, qaoa.exact_maximum |
min/max of E over all 2^n states (includes identity terms) |
qaoa.approximation_ratio |
(Emax - E_best) / (Emax - Emin); 1.0 if Emax - Emin <= 1e-12 |
qaoa.expectation_ratio |
(Emax - sampled_expectation) / (Emax - Emin); 1.0 if degenerate |
qaoa.optimal_probability |
observed mass on states whose energy rounds (9 dp) to the exact minimum |
qaoa.optimal_states |
sorted JSON list of all optimal q0-left states (first 64) |
qaoa.num_optimal_states |
the true count of optimal states, uncapped |
qaoa.shots |
integer total if every count value is integral, else sim.shots or "" |
qaoa.builder, qaoa.engine |
copies of builder.component/sim.component (or "" if absent) |
qaoa.evaluator |
"QuantumQAOAEvaluator" |
qaoa.num_qubits, qaoa.error |
str(n), "" |
report.type, mime.type |
simulation, application/json |
On failure: content is preserved, qaoa.error is set, and every
EVALUATOR_KEYS entry is blanked (a stale success must never survive a
failed re-evaluation).
Stale-attribute rules
NiFi FlowFile merges never drop attributes, so every hop is explicit about what it owns:
- Builders and solvers blank every
EVALUATOR_KEYSentry pluscircuit.qasm3,circuit.cirq_json,circuit.svg(unless they produce one),circuit.marked_state,circuit.num_iterationsandcircuit.error. - Builders never touch solver-owned keys. On the rebuild path (feeding a solver's output into a different framework's builder) those keys still describe the solver that originally trained the angles.
QuantumQAOAEvaluatoroverwrites allEVALUATOR_KEYSon success and blanks them on failure.hamiltonian.json,builder.*andcircuit.*ride through the counts engine hop untouched.
Portable qasm2 subset (the N×M interchange contract)
Every builder and solver emits, and QuantumQAOAEvaluator implicitly
depends on via circuit.num_qubits, a strict portable subset of OpenQASM
2.0: OPENQASM 2.0;, optional include "qelib1.inc";, exactly one
qreg, and gates only from {h, x, rx, ry, rz, cx}. No creg, measure,
barrier, reset, gate, opaque, if or gphase. This is enforced by
qaoa_contract.qasm2_profile() (hand-rolled regex/statement parsing, no
qiskit.qasm2 dependency) before any builder or solver returns, and its
metrics (num_qubits/depth/gate_count/nonlocal_gates) agree with
Qiskit's own QuantumCircuit.depth()/.size() on all five dialects.
Per-framework export facts that motivated this subset:
- Qiskit:
transpile(bound, basis_gates=["h","rx","rz","cx"], optimization_level=0)thenqasm2.dumpsgives exactly{h, cx, rz, rx}in oneqreg.decompose()alone emitsrzz, which is not in the portable set — transpiling is required.QAOAAnsatzis deprecated (anNLocalsubclass, removed in Qiskit 3.0); the non-deprecatedqiskit.circuit.library.qaoa_ansatz()has the same parameter order and F=1, so the builders andQiskitQAOACircuituse it, whileqiskit_algorithms.QAOAstill trains internally with the deprecatedQAOAAnsatz(expect aDeprecationWarningin logs — the unitary and parameter order of both are verified equal). - Cirq:
circuit.to_qasm()gives{h, cx, rz, rx}directly, with angles written aspi*<10 significant digits>plus//comment lines (both handled byqasm2_profile's comment-stripping). - PennyLane:
qml.to_openqasm(qnode, measure_all=False)()gives{h, cx, rz}— the RX mixer is exported ash . rz . h(no nativerxin PennyLane's OpenQASM exporter). Nogphasestatement is emitted even with an identity Hamiltonian term. - Qrisp:
qv.qs.compile().to_qiskit()gives{h, cx, rz, rx}but declares oneqregper qubit (qreg qv_0[1]; qreg qv_1[1]; ...); every builder/solver composes this into a singlen-qubitQuantumCircuitbefore emitting, which is what makes the output importable onQSharpSimulator(the raw multi-qreg export fails there with a register-ambiguity error). The XY mixer additionally exportss, sdg, sx, sxdg(sxis rejected byQSharpSimulator); whenever any non-portable gate appears, the export is rebased withtranspile(..., basis_gates=["h","x","rx","ry","rz","cx"], optimization_level=0)before emission — rebasing only happens when needed, since it also changes the RX export's text. - pyQuil:
quil_qasm.to_qasm2gives{h, cx, rz, rx}in oneqregdirectly.
N×M flow
The 5 builders × 7 engines matrix is the point of this extraction: one qasm2
circuit per builder, built at the same explicit angles across all five
frameworks, runs unmodified on all seven counts engines and is scored by one
QuantumQAOAEvaluator. The generated canvas
(demo/qaoa/qaoa-nxm.json, also documented as "Flow 35" in the
NiFi flow configuration guide)
wires: one QiskitHamiltonian trigger → 5 builders (each Layers=2,
Betas=[-0.67, -0.42], Gammas=[1.14, 1.27]) → each builder's success
fanned out to all 7 engines (35 connections) → each engine's success into
one shared QuantumQAOAEvaluator → one shared QuanifiReport.
The instance is H_NXM = -0.8 Z0 Z1 + 0.5 Z1 Z2 - 0.3 Z0 + 0.2 (3 qubits).
Its unique ground state is 001 (E=-1.4); every one of the 35 cells reaches
qaoa.best_measurement="001" and qaoa.exact_minimum=-1.4 /
qaoa.approximation_ratio=1.0 — the whole point is that cells are scored as
distributions, not by their argmax alone (a test deliberately mirrors one
row's qubit indices and shows it still reports best_measurement="001"
while its Hellinger distance to the reference jumps past 0.3).
Thresholds, measured on the pinned environment:
- The 6 seedable engines (Aer, Cirq, Qrisp, PennyLane, Q#) at 4096 shots,
Random Seed=11: worst measured Hellinger distance to the exact distribution across all 5 rows is 0.0187, well inside the plan's statistical bound for this instance (0.9999-quantile 0.0304 over 50000 multinomial draws). BraketSimulatorhas noRandom Seedproperty and is unseedable. Its column is included in the N×M matrix but compared only statistically (against the same bound above, max observed 0.0316 over 50000 trials) — never for exact-value equality against a saved cell. Regenerating the example canvas twice in a row changes only this column's numbers.PyquilSimulatorcosts about 13.6 ms/shot via its pure-Python PyQVM (no externalquilc/qvmserver); the N×M column therefore runs at 256 shots, not 4096. Measured Hellinger distance: consistently 0.0439 (256 shots) across repeated regenerations, well under its own 0.20 statistical bound for this shot count. Raising its shot count in the canvas without re-timing will make the regeneration/tests noticeably slower (about 3.3s at 256 shots per builder in isolation, 14s at 1024).
Example canvases
Regenerate the local example canvases and reports:
.venv/bin/python tools/build_qaoa_examples.py --run --nxm
or via just qaoa-examples. This produces demo/qaoa/qaoa-lanes.json (10
single-processor lanes, one per QAOA processor type — see the table below),
demo/qaoa/qaoa-nxm.json (the N×M matrix above), and demo/qaoa/index.html
(a static overview linking both snapshots, per-lane result cards, and the
N×M Hellinger table). Like tools/build_pyquil_examples.py, the generator
runs the real processors through the test harness's NiFi API stubs — it does
not contact a provider or a running NiFi instance, and it never hand-edits
demo/**; regenerate through the tool after any change that affects a
lane's output.
| Lane key | Encoder | QAOA processor | Engine |
|---|---|---|---|
qiskit-qaoa |
MaxCutProblem |
QiskitQAOA (Layers 2, Max Iterations 150, Random Seed 11) |
CirqSimulator |
cirq-qaoa |
QuboToHamiltonian |
CirqQAOA (same) |
QrispSimulator |
pennylane-qaoa |
MaxIndependentSetProblem |
PennylaneQAOA (same) |
QSharpSimulator |
qrisp-qaoa |
PortfolioRebalancingProblem |
QrispQAOA (Layers 2, Mixer Type XY, Max Iterations 100, Random Seed 11) |
PennylaneSimulator |
pyquil-qaoa |
QiskitHamiltonian (-0.5 + 0.5 Z0 Z1) |
PyquilQAOA (Layers 1, Optimizer L_BFGS_B, Max Iterations 100, Initial Parameters [-0.3,1], Random Seed 42) |
PyquilSimulator (Shots 256, Random Seed 42) |
qiskit-qaoa-circuit |
QiskitHamiltonian (H_NXM) |
QiskitQAOACircuit (N×M angles) |
QiskitAerSimulator |
cirq-qaoa-circuit |
CirqHamiltonian (H_NXM) |
CirqQAOACircuit (N×M angles) |
QSharpSimulator |
pennylane-qaoa-circuit |
MaxCutProblem |
PennylaneQAOACircuit (defaults) |
QiskitAerSimulator |
qrisp-qaoa-circuit |
QrispHamiltonian (H_NXM) |
QrispQAOACircuit (N×M angles) |
CirqSimulator |
pyquil-qaoa-circuit |
MaxCliqueProblem |
PyquilQAOACircuit (defaults) |
QrispSimulator |
All six seedable engines and every Hamiltonian producer/encoder appear
across these ten lanes; BraketSimulator appears only in the N×M matrix,
per its unseedable-engine rule above. Every lane ends
... → QuantumQAOAEvaluator → QuanifiReport.
Import into NiFi
Follow the same procedure as the pyQuil guide:
install the updated nifi_extensions/ directory (including the QAOA helper
modules above), wait for NiFi to install each processor's declared
dependencies in its own environment, import qaoa-lanes.json and/or
qaoa-nxm.json as process groups, start the downstream processors, then
Run Once each trigger. All processors import stopped (ENABLED). As
with every canvas in this repository, these artifacts are validated
structurally and executed headlessly by the generator — this guide does
not claim live NiFi execution of the N×M canvas.
NiFi deployment note. Six existing processors changed dependencies or
version in this work: QiskitQAOA (dropped qiskit-aer), CirqQAOA,
PennylaneQAOA, QrispQAOA (dropped qiskit-qasm3-import), PyquilQAOA,
and PyquilQAOACircuit (rewritten to 0.2.0). A live NiFi deployment must
delete the cached extension venvs
work/python/extensions/{QiskitQAOA,CirqQAOA,PennylaneQAOA,QrispQAOA,PyquilQAOA,PyquilQAOACircuit}/
so NiFi rebuilds them against the new ProcessorDetails.dependencies;
otherwise the old venv (missing the QAOA helper modules, or carrying a stale
dependency set) is reused. The five new processors
(QiskitQAOACircuit, CirqQAOACircuit, PennylaneQAOACircuit,
QrispQAOACircuit, QuantumQAOAEvaluator) have no prior venv to clear.
This wipe was not performed as part of this work — no live NiFi instance
was touched; it is documented here for whoever deploys the change.
Migration from 0.1.0
If you have flows or scripts built against the pre-0.2.0 solvers:
Shotsis removed from all five solvers. They no longer sample.- Counts now come from any downstream simulator, not from the solver itself. Insert one between the solver and anything that used to read its counts content.
qaoa.best_measurement/qaoa.best_value/qaoa.approximation_ratio/qaoa.exact_minimum/qaoa.shots/run.seedare no longer emitted by the solver. They now come fromQuantumQAOAEvaluator(or, forrun.seed, from the engine you choose), computed downstream of whichever simulator you connect — insertQuantumQAOAEvaluatorafter the simulator wherever your flow or script reads these.QrispQAOA'sInitial Parametersis now canonical beta-then-gamma, matching every other solver and builder (Qrisp's own internal ordering, gamma-then-beta, is converted at the boundary viaqrisp_qaoa.to_qrisp_theta/from_qrisp_theta). A 0.1.0 value written in Qrisp's own order will train the wrong angles under 0.2.0.- The
QrispQAOAmirror-training bug is fixed. The 0.1.0 processor reversed measurement keys in both its training loss and its reported best measurement, which trained and reported the bit-reversed Hamiltonian (verified: forH = -Z0 + 0.3 Z1, it reported best01while its own emitted circuit actually favoured10, P=0.687). 0.2.0 usesqaoa_contract.energy_vector's q0-left indexing directly, with no reversal, by construction. Any 0.1.0 result computed fromQrispQAOAthat matters for correctness should be recomputed, not carried forward. PyquilQAOA'sLayersdefault is now2(was1, unified with the other four solvers) andRandom Seedis now unset by default (was a required42) — set it explicitly if you relied on the old default seed.- A live NiFi deployment needs a venv wipe for the six changed processors — see "NiFi deployment note" above.
Known limitations
BraketSimulatoris unseedable — it has noRandom Seedproperty. It appears only in the N×M matrix (not the ten deterministic single-processor lanes) and its cells are compared statistically, never for exact equality.PyquilSimulatoris the slow column — about 13.6 ms/shot via its pure-Python PyQVM. The N×M matrix samples it at 256 shots (not 4096 like the other six engines) to keep regeneration and the test suite fast; raising this without re-timing will slow both down noticeably.QiskitQAOA's training objective is sampled, not exact — itsStatevectorSamplerdefaults to 1024 shots (qaoa.energy_method= sampled_statevector,qaoa.energy_shots="1024"). This is an intentional, documented characteristic, not a bug: the measured gap to the exact eigenvalue was 0.030 at 1024 shots and 4.4e-4 at 100000 shots (about 15x slower), soQiskitQAOA's energy-consistency tests use a statistical (6-sigma) tolerance rather than an exact one.QrispSimulator'sRandom Seedis best-effort, seeded via the global NumPy RNG (Qrisp's sampler exposes no seed API of its own) — this is a pre-existing, documented characteristic ofQrispSimulatoritself, distinct from theQrispQAOAsolver's determinism (which draws from a local generator and does not touch global state as of the fix described below). Because of this, the generated example canvas comparesQrispSimulator-engine lanes only on their deterministic fields (circuit, Hamiltonian, non-sampling attributes); sampling-derived fields (counts,qaoa.best_measurement, etc.) are checked for shape and validity, not byte-for-byte equality across regenerations. This was found and documented while building the example generator (M7); it is unrelated to and unaffected by the QrispQAOA solver fixes below.- Qrisp's XY mixer is not K-hot constrained — it starts from the
uniform superposition, not a fixed-Hamming-weight state, so it does not
guarantee the "exactly K selected" property some QAOA applications (e.g.
budget-constrained portfolio selection) want from it in isolation; that
constraint comes from how
QrispQAOA/QrispQAOACircuitinitialize the register in practice, not from the mixer alone. It is Qrisp-specific semantics, existing before this work and documented rather than changed here. The XY mixer is not part of the N×M builder rows — those always use the cross-framework RX convention so all five rows share one unitary construction; XY appears only in the single-processorqrisp-qaoa/qrisp-qaoa-circuitlanes andQrispQAOA's own tests. - Qrisp energy rounding. Qrisp's exact-probability measurement backend
already rounds probabilities to 5 decimals internally (max error about
3.2e-6).
QrispQAOA's classical cost function additionally rounds its returned energy to 12 decimals before handing it to the optimizer. This is not cosmetic: Qrisp's backend differs by up to 1 ulp (2.22e-16) between separately compiled circuits evaluated at the same angles, and scipy's Fortran-backedCOBYLA/POWELLoptimizers chaotically amplify that 1-ulp jitter over ~100 iterations into macroscopic (0.06-0.2 rad) parameter drift between otherwise-identical seeded runs. Rounding to 12 decimals removes the drift outright (measured: 40 seeded COBYLA repeats, each run after an unrelated intervening run, gave exactly 0.0 parameter drift) while losing no resolution beyond Qrisp's own 5-decimal probability rounding.NELDER_MEAD(pure Python/NumPy, no Fortran extension) was never affected by this and needed no such fix.
Verification
.venv/bin/python -m pytest tests/test_qaoa*.py tests/test_bit_order.py tests/test_seeds.py tests/test_numeric_guards.py \
tests/test_problem_encoders.py tests/test_qrisp_qaoa_problems.py tests/test_qrisp_cross_framework.py \
tests/test_pyquil_processors.py tests/test_pyquil_examples.py tests/test_reporting.py tests/test_processor_dependencies.py \
tests/test_all_components_group.py tests/test_qrisp_components_group.py -q --tb=short
.venv/bin/python -m pytest -q -m "not slow"
.venv/bin/python -m pytest -q --tb=short
The numbers referenced above are reproduced by the test suite, which is the authoritative record for this extraction.