Quantum Optimization and QAOA

QAOA makes the good answers come up more often when you measure, and two numbers decide by how much. How that works, from one qubit to a real graph, with code you can run and fork.

About the author

Nicolaas Spijker
Nicolaas Spijker
Community manager, Qollab

Nico runs the builder community at Qollab. These concept articles come out of that work: forking the projects people publish and running them. And talking with authors to learn more about their projects.

Quantum optimization means using a quantum computer to find the best of a huge number of possible answers, such as the best way to split a graph into two groups. QAOA, the standard circuit for the job, does not check the answers. It changes how often each one comes out when you measure, and two adjustable angles set by how much.

The smallest version has one qubit, three gates and a measurement, and its two angles take that qubit from a coin toss to a certain answer. Everything on this page is that same circuit on more qubits, sometimes stacked in more than one layer, up to a graph of twenty-four nodes.

What is quantum optimization?

Quantum optimization means rewriting a problem as a score over strings of bits, then building a circuit whose measured strings lean towards better scores. You keep the best string you see. Ordinary code scores each sampled string, and the circuit only supplies the samples.

QAOA, the quantum approximate optimization algorithm, is the standard circuit for the job. It starts with equal weight on every string, and one layer is then two steps. A cost step gives each string a phase, an angle that measurement never shows, set by its score. A mixer step turns those phases into weight, and at the right angles better strings then come out more often.

Each layer has two angles, gamma for the cost step and beta for the mixer step, tuned by an ordinary classical optimizer.

Approximate is in the name because the guarantee, where one has been proved, is a floor. The average score of the samples reaches no less than a fixed fraction of the best possible. What comes back is a good string, not a proof that it is the best one.

Splitting a graph so that as many edges as possible run between the two groups is MaxCut. It is the running example on this page, and every other problem here is rewritten into the same form.

One layer of the MaxCut circuit needs one two-qubit interaction per edge and one rotation per node. Only two numbers in that layer are free to change, the angles.

Cooling slowly

Annealing is what a metalworker does to steel that has to bend without breaking. A blade plunged into water from red heat comes out hard and brittle, because the atoms freeze where they are, defects and all.

Cooled slowly, the same steel settles into a lower-energy arrangement, because at each temperature the atoms still have enough motion to leave a bad position for a better one.

In 1983 Kirkpatrick, Gelatt and Vecchi turned annealing into an algorithm. Simulated annealing starts from a random answer and accepts a worse neighbouring answer with a probability that shrinks as a temperature variable drops. It ends up in a good answer for the same reason the blade does.

Simulated annealing is also one of the classical solvers in Quantum Courier, the Qollab project the 24-node graph named in the opening comes from. There it beats the quantum solvers on the game's vehicle-routing stage.

Adiabatic quantum computation, proposed in 2000, is the quantum version of cooling slowly. A Hamiltonian is a problem's cost written as an energy, and its ground state, the lowest-energy state, encodes the best assignment.

Start in the easy ground state of one Hamiltonian and change it into the problem's; if the change is slow enough, the state stays close to the ground state throughout.

QAOA cuts the adiabatic path into a fixed number of steps and lets a classical optimizer choose how big each step is. Farhi, Goldstone and Gutmann showed that with enough layers the circuit can follow the adiabatic path.

A single layer is the fast-cooling version. On the 24-node graph further down it lifts the average cut from 18.5 edges to 25.5, out of a possible 33.

Where quantum optimization came from

Six dates carry the route from annealing to QAOA and on to the current scaling experiments:

1983Kirkpatrick, Gelatt and Vecchi publish simulated annealing: cool a random answer slowly and it settles into a good one.
1995Goemans and Williamson guarantee 0.878 of the best possible cut with a classical algorithm, a bar that still stands.
2000Farhi, Goldstone, Gutmann and Sipser propose adiabatic quantum computation: change a Hamiltonian slowly and the ground state follows.
2014Farhi, Goldstone and Gutmann introduce QAOA: the slow path cut into layers, with two angles per layer.
2021Google runs QAOA on 23 superconducting qubits; problems that match the chip's wiring improve with depth, problems that do not get worse as they grow.
2024Shaydulin and colleagues report evidence of a scaling advantage for QAOA on a problem where the best exact classical solvers scale worse.

The 2014 paper carried a guarantee: on any graph where every node has three edges, one QAOA layer's samples average at least 0.6924 times the best possible cut.

A month later the same three authors showed one layer beating the best known classical guarantee on a constraint problem called E3LIN2. By May 2015 a classical algorithm had a better guarantee; the lead had lasted five months, and the QAOA bound itself still stands.

What one layer does

A qubit carries two numbers for each outcome it can give. Its weight is how often that outcome comes back when you measure. Its phase is an angle that measurement never shows, and the cost step works on that one. Two qubits carry both numbers for four outcomes, twenty-four qubits for sixteen million.

One QAOA layer is two steps. The cost step touches only the phases; the mixer step is where the weights change.

In the cost step, every outcome's phase turns by an amount set by that outcome's score. A good outcome and a bad one end up with different phases, and nothing else has happened: measure now and the counts are exactly what they were before.

In the mixer step, phase differences become weight differences. Outcomes whose phases were turned one way gain weight and outcomes turned the other way lose it. The two angles decide how far the phases turn and how much weight follows: gamma for the cost step, beta for the mixer step.

On one qubit, with a score that prefers 1, the panels below show each step, and what smaller angles do:

Four panels of two bars each, for the weight on outcome 0 and outcome 1, with a small dial above each bar for its phase: equal bars and matching dials at the start, the dial over 1 turned after the cost step, all the weight on 1 after the mixer step, and 85 percent on 1 at smaller angles
One layer on one qubit. The cost step turns a dial and moves no bar; the mixer step moves the bars, and smaller angles move them less. The dials are drawn only while the phase is what changes.

Both bars start at one half with the same phase. After the cost step the hand over 1 has turned and both bars are where they were. After the mixer step the weight has moved, and at the right pair of angles all of it lands on 1; at smaller angles, less of it. Measure a thousand times and the counts follow the bars.

The one-qubit circuit in the next section is the first three panels as three gates, then a measurement that reads the bars.

The smallest circuit you can make

A cost that prefers 1 is one rz, the mixer is one rx, and measuring reads the bars:

one_qubit_qaoa.py Python · Playground versionOpen in Playground ↗
# 'backend' is pre-created for you in the Qollab Playground.
from math import pi
from qiskit import QuantumCircuit
1gamma, beta = -pi / 4, pi / 4
qc = QuantumCircuit(1, 1)
2qc.h(0)
3qc.rz(2 * gamma, 0)
4qc.rx(2 * beta, 0)
qc.measure(0, 0)
5print(backend.run(qc, shots=1000).result().get_counts())
  1. 1The two angles. This pair is exact for one qubit: every shot will read 1.
  2. 2Equal weight on 0 and 1, the starting point of every QAOA circuit on this page.
  3. 3The cost step. It writes the score into the phase: the 1 half of the state gets a different phase from the 0 half. Nothing in the counts has changed yet. Qiskit's rotation gates take twice the angle, a convention every listing here follows.
  4. 4The mixer step. It turns that phase difference into a weight difference.
  5. 5{'1': 1000}: every shot reads 1. Set gamma to +π/4 and every shot reads 0; halve either angle and 1 comes back about 85% of the time.
Run on QollabBackend

One qubit has nothing to cut, so the score there is a preference. The first real problem is one edge between two nodes: it is cut when the two bits differ, so 01 and 10 are the answers and 00 and 11 are not. The circuit adds one two-qubit interaction for the edge, written as a three-gate sandwich, and keeps its two angles:

one_edge_qaoa.py Python · Playground versionOpen in Playground ↗
# 'backend' is pre-created for you in the Qollab Playground.
from math import pi
from qiskit import QuantumCircuit
1gamma, beta = -pi / 4, pi / 8
qc = QuantumCircuit(2, 2)
2qc.h([0, 1])
3qc.cx(0, 1)
qc.rz(2 * gamma, 1)
qc.cx(0, 1)
4qc.rx(2 * beta, [0, 1])
qc.measure([0, 1], [0, 1])
5print(backend.run(qc, shots=1000).result().get_counts())
  1. 1The two angles. This pair is exact for one edge: every shot will cut it. Change either one and the counts change with it.
  2. 2Even weight on all four strings: 00, 01, 10, 11. The standard QAOA circuit starts here.
  3. 3The cost step for one edge. The cx, rz, cx sandwich shifts the phase of a string by an amount that depends on whether its two bits differ. Cut strings get one phase, uncut strings the other.
  4. 4The mixer step. A rotation on each qubit that turns the phase difference the cost step set into a weight difference. Phases alone change no count; this is the step that makes them count.
  5. 5{'01': 503, '10': 497}: about half each, and no 00 or 11 at all. The circuit never proposes an uncut edge.
Run on QollabBackend

Flip the sign of gamma and the counts flip with it: all 00 and 11, an uncut edge on every shot. Set both angles to zero and the four strings come back at a quarter each. Halve gamma to −π/8 and the cut strings carry 85% of the weight instead of all of it.

All four results come from the same gates; only the two numbers changed. The angles set what a QAOA circuit computes, and for one edge the average cut at every pair of angles fits in one figure:

A heatmap of the expected cut for the one-edge circuit over every pair of angles, with two purple peaks at expected cut one and two pale troughs at zero
Expected cut of the one-edge circuit at every pair of angles. Two peaks reach 1.0, two troughs reach 0.0, and the lines through the centre sit at a coin toss. The circled pairs are exact.

Every row and column through the centre is a coin toss. At beta = 0 the mixer does nothing and the cost step's phases never reach the counts. At gamma = 0 there are no phases to turn into weight.

The circuit only does anything when both angles are away from zero, and which peak or trough you land in decides whether it finds the cut or avoids it.

The official showcase-qaoa-qubo example is this same problem, written as maximize x + y - 2xy and handed to Qiskit's QAOA class with the COBYLA optimizer in charge of the angles. Run it and 01 and 10 carry almost all the weight, which is COBYLA finding one of the two peaks on its own.

QUBO with QAOAu/qollab · Python + QiskitQUBO (Quadratic Unconstrained Binary Optimization) is the standard form for combinatorial problems on quantum computers, and QAOA (Quantum Approximate Optimization Algorithm) is the leading quantum heuristic for solving it. This example models a two-variable QUBO with qiskit-optimization, solves it with QAOA from qiskit-algorithms, and plots the sampled solutions.

Turning a problem into a cost

Between one edge and a real graph sits the bookkeeping. The problem has to become a number that a string of bits can be scored by, and the score has to become the phases in the cost step. The standard form, used by every project on this page, is a QUBO. Pick 0 or 1 for each variable to minimize a polynomial with single terms, pairwise terms and nothing higher.

Flip the sign and a score to maximize becomes a cost to minimize, which is why MaxCut fits. Qollab's showcase-qubo-exact, one of the official Playground examples, builds a QUBO with three variables and prints the whole landscape:

qubo_exact.py Python · Playground versionOpen in Playground ↗
# minimize -x - 2y - 3z + 2xy + 2yz
# (each variable wants to be 1, but x-y and y-z "repel" each other)
qp = QuadraticProgram("qubo")
qp.binary_var("x")
qp.binary_var("y")
qp.binary_var("z")
1qp.minimize(linear={"x": -1, "y": -2, "z": -3}, quadratic={("x", "y"): 2, ("y", "z"): 2})
print(qp.prettyprint())
# Brute-force all 8 assignments - only possible because the problem is tiny
print("\n x y z   objective")
2for bits in product((0, 1), repeat=3):
    value = qp.objective.evaluate(list(bits))
    print(f" {bits[0]} {bits[1]} {bits[2]}   {value:+.0f}")
  1. 1Each single term rewards a variable for being 1. Each pair term is a penalty for two of them being 1 together. A graph's edges are exactly a set of pair terms.
  2. 2Eight strings for three bits. The loop scores every one, which is the thing no solver can do once the string is long: at sixty variables the table has more rows than there have been seconds since the Big Bang.
Run on QollabBackend

The table the example prints has eight rows, and one of them, 1 0 1, reaches −4:

 x y z   objective
 0 0 0   +0
 0 0 1   -3
 0 1 0   -2
 0 1 1   -3
 1 0 0   -1
 1 0 1   -4
 1 1 0   -1
 1 1 1   -2

No other row gets there. That number is the ground truth for this example, and every quantum run below has one like it. The file's own header comment says so: it is "the trusted baseline every quantum solver gets measured against".

To become phases, the 0/1 variables become spins. The substitution is x = (1 - z) / 2: a bit that is 0 becomes a spin that is +1 and a bit that is 1 becomes −1. A pair term x·y becomes (1 − z_x − z_y + z_x·z_y) / 4: a constant, a term on each spin, and a z·z term.

The z·z term is the product of the two ±1 signs that two measured bits map to. That product is what the cx, rz, cx sandwich in the one-edge circuit gives a phase to. So an edge of the graph becomes one two-qubit interaction in the circuit, and a variable's own reward becomes a single-qubit rotation, the rz from the one-qubit circuit.

QUBO, solved exactlyu/qollab · Python + QiskitHow to express a combinatorial problem as a QUBO (Quadratic Unconstrained Binary Optimization) with qiskit-optimization, inspect the full solution landscape by brute force, and solve it through the same Ising-Hamiltonian route that quantum algorithms use, with an exact classical eigensolver as a trusted baseline.

The gates in these circuits

Four gates and one instruction cover every excerpt on this page.

h

Hadamard

Takes a qubit sitting at 0 and leaves it weighted evenly between 0 and 1. Applied to every qubit it puts equal weight on every string at once, which is where each circuit here begins.

cx

CNOT

Flips the second qubit when the first is 1. When the first qubit is in superposition and the second sits at 0, this is what ties the two into one shared state.

Here the CNOT appears in a pair around an rz. That sandwich is the standard way to write a two-qubit phase using only gates the hardware provides, and there is one per edge.

rz

A rotation that changes no count on its own. It moves the phase, the relative angle between the two halves of a qubit's state.

Inside the sandwich the rz gives every string a phase that depends on the edge being cut or not. On a register, the strings that differ at this qubit are the ones that pick up different phases. Added up, one per edge, these phases are the whole cost step.

rx

A rotation about a different axis. On a qubit with equal weight on 0 and 1 and no relative phase, which is what the opening Hadamard leaves, it changes no count at all.

Once the cost step has set a phase between the two halves, the same rotation moves weight from one outcome to the other. How much moves depends on that phase.

This is the mixer, and it is the only step after the opening Hadamards that changes how much weight each string carries. The cost step as a whole leaves every string's weight where it was and changes only its phase. Without the mixer, those phases would never show up in a measurement.

measure

Reads a qubit into an ordinary bit and forces it to a definite 0 or 1. One run gives one string, so every sampled circuit here runs thousands of times and the answer is read off the tally.

Two angles, sixteen million splits

Dr. Siti Fariya's Quantum Courier is a browser game that races classical and quantum solvers across five logistics problems, and its fourth stage is MaxCut on twenty-four nodes. The Playground version is the script the project publishes for that stage's run on IonQ Forte.

The script's circuit is the one-edge circuit above with the cost sandwich repeated for every edge and the mixer repeated for every node:

quantum_courier_maxcut_forte.py Python · Playground versionOpen in Playground ↗
def qaoa_circuit(gamma, beta):
    qc = QuantumCircuit(N_NODES, N_NODES)
    # Initial state: equal superposition
1    qc.h(range(N_NODES))
    # Cost layer: e^{-i gamma C}
2    for (u, v) in EDGES:
        qc.cx(u, v)
        qc.rz(2 * gamma, v)
        qc.cx(u, v)
    # Mixer layer: e^{-i beta B}
3    for q in range(N_NODES):
        qc.rx(2 * beta, q)
    qc.measure(range(N_NODES), range(N_NODES))
    return qc
  1. 1Twenty-four Hadamards, and the register carries equal weight on all 16,777,216 splits.
  2. 2One sandwich per edge, thirty-seven of them. Each shifts a split's phase by an amount that depends on whether that edge is cut, so a split's total phase depends only on how many edges it cuts.
  3. 3One rx per node, the same mixer as before. Still the only place weight moves between splits.
Run on QollabBackend

The graph is the one from the top of the page: twenty-four nodes, thirty-seven edges, a ring with chords across it. Scoring all 16,777,216 splits takes a laptop under a second. A random split cuts 18.5 edges on average, no split cuts more than 33, and exactly six splits reach that.

Quantum Courier's 24-node graph drawn on a ring with chords, nodes coloured by an optimal split, 33 solid edges crossing between the groups and 4 dashed edges staying inside one
The instance Quantum Courier ships, coloured by one of its six best splits. Thirty-three of the thirty-seven edges cross between the groups.

Scoring is classical code. For each measured string the script counts the edges whose endpoints landed on different sides, then averages over the shots:

quantum_courier_maxcut_forte.py Python · Playground versionOpen in Playground ↗
def cut_value(bitstring):
    # Qiskit returns bitstrings in reverse: bit i lives at position N-1-i
1    bits = [int(b) for b in bitstring[::-1]]
2    return sum(1 for (u, v) in EDGES if bits[u] != bits[v])
def expected_cut(counts, shots):
3    return sum(cut_value(bs) * c for bs, c in counts.items()) / shots
  1. 1A measured string is one split of the graph. Reversing it is a Qiskit convention: qubit 0 is the last character.
  2. 2The cut: one point per edge whose ends disagree. This is the score, and it never touches a qubit.
  3. 3The average over every shot, which is the number an optimizer steers by.
Run on QollabBackend

On an exact simulator, which keeps the weight of every split rather than sampling, the circuit's output distribution can be computed for any pair of angles. Because every split can be scored, the figure below does that for two.

Grey is a random split, or equally the circuit with both angles at zero. Purple is the pair tuned for this graph, gamma = -0.30, beta = 0.39.

Two overlaid bar charts of how often each cut value appears: a grey bell centred on 18.5 for random splits and a purple bell centred near 25.5 for the tuned circuit, with the best possible value of 33 marked
How often each cut value comes up. A random split averages 18.5 edges. One tuned QAOA layer averages 25.5, and the best possible split, 33, moves from one shot in 2.8 million to one in a thousand.

One layer moves weight off the bad splits and onto the good ones, and the bell slides seven edges to the right. Nothing in the circuit finds the best split and hands it over; the best split becomes a more frequent sample.

A sample from the purple bell is optimal about once in a thousand shots, against once in 2.8 million from the grey one. In a 4,096-shot job the best split therefore shows up about four times, and the best string in the tally is the best split on almost every run.

The table below is one layer on the same 24-node, 37-edge instance at four pairs of angles, every row derived from the exact distribution. In it, gamma is the cost angle and beta the mixer angle. The best possible cut is 33:

angles (gamma, beta)average cutchance of a 33-edge cut, per sampleexpected best cut in 4,096 shots
0, 0 (a random split)18.51 in 2,800,00029
−π/4, π/8 (exact for one edge)18.51 in 34,000,00028
−0.30, 0.39 (tuned for this graph)25.51 in 98033
+0.30, 0.39 (same size, other sign)11.5under 1 in a trillion22

On twenty-four nodes the pair that is exact on one edge leaves the average at 18.5, the same as a random split, and makes the best split rarer. Which angles are best depends on how many edges meet at each node, so angles do not transfer from a toy to a real instance.

And the sign pairing matters as much as the size. With gamma flipped the same circuit averages 11.5 edges, well below a random split, because it is now concentrating weight on the splits that cut the fewest edges.

Quantum CourierQuantum CourierDr. Siti Fariya · u/Sitifar · Python + QiskitDr. Siti Fariya built a browser game that races classical and quantum solvers across five real logistics problems, and shows honestly which one wins, and why.

The loop runs on the simulator

Finding the purple pair is the classical optimizer's job, and each step of that job is a full run of the circuit. Nothing quantum happens in the loop. Starting from a guess, the optimizer runs the circuit at that pair and reads the average cut. Then it nudges the pair and runs again: fifty times in Courier's benchmark and the official example, a few hundred in the Oracle.

When the budget runs out the optimizer returns the best pair it saw. Quantum Courier's script defines the objective that loop calls.

For a single Forte job the script skips the loop and fixes GAMMA_STAR and BETA_STAR before submitting, with a comment that says so. In a fork, those two constants are where the pairs from the table go.

The objective itself wraps one run:

quantum_courier_maxcut_forte.py Python · Playground versionOpen in Playground ↗
def objective(params):
    """Negative expected cut — minimised by COBYLA."""
1    bound = template.assign_parameters({gamma_p: params[0], beta_p: params[1]})
2    job = backend.run(bound, shots=SHOTS)
    counts = job.result().get_counts()
3    return -expected_cut(counts, SHOTS)
  1. 1One candidate pair of angles goes into the circuit template.
  2. 2One evaluation is one job of 4,096 shots. On hardware that is a queue, a wait and a bill, and an optimizer typically wants fifty of them.
  3. 3The average cut, negated because COBYLA minimizes. The optimizer never sees a qubit, only this number.
Run on QollabBackend

Fifty evaluations at 4,096 shots is 204,800 shots for two numbers, and on a shared machine each evaluation waits in a queue. So both hardware projects on this page tune their angles on a simulator and send the finished circuit to the machine once.

Jamie Dominguez's Quantum Systemic Oracle picks a portfolio of eight crypto assets the same way. Its QUBO is built from market data, and the next section takes that cost apart.

The circuit it sends to the machine has fourteen qubits: one decision qubit per asset, plus six that carry market state. The tuning runs on the eight asset qubits alone, inside the Playground file, with a Nelder-Mead optimizer written in plain Python.

Below is how that optimizer scores each candidate pair. In it, h holds one number per asset and J_pairs one number per pair of assets: the two kinds of term the cost is made of.

Qollab.py Python · Playground versionOpen in Playground ↗
def optimize_angles(h, J_pairs, n, layers) -> Tuple[List[float], List[float], float]:
    """Best (gammas, betas) by multi-start Nelder-Mead on <H_C>."""
    H = build_hamiltonian(h, J_pairs, n)
    def energy(angles: List[float]) -> float:
        gammas, betas = angles[:layers], angles[layers:]
        sv = Statevector.from_instruction(
1            asset_only_qaoa_circuit(gammas, betas, h, J_pairs, n))
2        return float(np.real(sv.expectation_value(H)))
  1. 1The full state of the eight asset qubits, computed exactly. No shots and no noise, which is what makes each step cheap enough to take hundreds of them.
  2. 2The average cost under that state, read straight off the weight of every string. A real machine can only estimate this number from a tally.
Run on QollabBackend

On the simulator the objective is exact. On hardware it is an estimate, and how good an estimate is a number you can derive. At the tuned pair a single sample's cut has a standard deviation of 2.6 edges. Over 4,096 shots the average is known to about 0.04 of an edge, one standard error.

Shot noise is a small part of what makes the loop hard. Most of the difficulty is that every evaluation is a job, and that the landscape being searched has the shape of the one-edge figure above with more ridges.

Three ways of running a circuit appear on this page. An exact simulator keeps every weight and needs no shots; the Oracle's tuner uses one. A sampled simulator returns a tally the way hardware does, with shot noise but none of the device's.

The machine itself returns a tally with the device's noise in it. Courier's objective, which always asks for shots, runs on whichever of the last two its backend is pointed at.

Constraints become penalties

The Oracle's problem is portfolio selection: which of eight crypto assets to hold, given their expected returns and how their prices move together. A budget rule says how many to hold, and a QUBO has no constraints by definition, so the rule has to become part of the cost. The file does it like this:

Qollab.py Python · Playground versionOpen in Playground ↗
Q = [[0.0] * n for _ in range(n)]
for i in range(n):
1    Q[i][i] += lam * var[i] - mu[i] - ALPHA_POLY * poly[i] + BETA_FUNDING * fnd[i]
    Q[i][i] += ALPHA_DISAGREE * disagree[i] - ALPHA_SIDEWAYS * sideways[i] + macro_tilt
    for j in range(n):
        if i != j:
2            Q[i][j] += lam * corr[i][j] * math.sqrt(var[i] * var[j])
for i in range(n):
3    Q[i][i] += BUDGET_PENALTY * (1 - 2 * k_target)
    for j in range(n):
        if i != j:
            Q[i][j] += BUDGET_PENALTY
  1. 1The single terms: one number per asset from its own return, risk and market signals.
  2. 2The pair terms. Two assets that move together are penalised for being held together, which is Markowitz's diversification written as a QUBO.
  3. 3The budget rule, folded into the cost rather than checked outside it: a term per asset that depends on the target count k_target, and a term per pair that charges for holding two assets at once. The number of assets held is priced, not enforced.
Run on QollabBackend

Converting to spins uses the substitution from before, x = (1 - z) / 2, named in the file's own comment, and the file then rescales the result, a step the one-edge circuit did not need:

Qollab.py Python · Playground versionOpen in Playground ↗
# QUBO -> Ising via x_i = (1 - Z_i) / 2
h = [0.0] * n
J = [[0.0] * n for _ in range(n)]
for i in range(n):
1    h[i] += -Q[i][i] / 2.0
    for j in range(n):
        if i != j:
            h[i] += -Q[i][j] / 4.0
2            J[i][j] += Q[i][j] / 4.0
J_pairs = [{"i": i, "j": j, "value": J[i][j] + J[j][i]}
           for i in range(n) for j in range(i + 1, n)]
# Normalize so the largest |coeff| ~ 1 (keeps QAOA angles meaningful; the
# classical optimum and reported costs always use the raw Q matrix).
scale = max([abs(x) for x in h] + [abs(p["value"]) for p in J_pairs] + [1e-9])
return {
    "lambda": lam,
    "target_k": k_target,
    "Q_matrix": Q,
3    "ising_h": [x / scale for x in h],
    "ising_J_pairs": [{"i": p["i"], "j": p["j"], "value": p["value"] / scale} for p in J_pairs],
}
  1. 1Each variable's own reward becomes a field on one spin: a single-qubit rz in the cost step.
  2. 2Each pair term becomes a coupling between two spins: one cx, rz, cx sandwich.
  3. 3Every field and coupling divided by the largest of them, so the biggest coefficient is about 1. gamma multiplies every coefficient, so without this the useful range of gamma would depend on the units the returns were quoted in.
Run on QollabBackend

With fields and couplings in hand, the layer itself is the Courier loop with one extra line, in its general form:

Qollab.py Python · Playground versionOpen in Playground ↗
def apply_cost_layer(qc, gamma, h, J_pairs, n):
    for i in range(n):
1        qc.rz(2.0 * gamma * h[i], i)
    for pair in J_pairs:
        i, j, v = pair["i"], pair["j"], pair["value"]
        qc.cx(i, j)
2        qc.rz(2.0 * gamma * v, j)
        qc.cx(i, j)
def apply_mixer_layer(qc, beta, n):
    for i in range(n):
3        qc.rx(2.0 * beta, i)
  1. 1The field on each spin, scaled by gamma. MaxCut had none of these; a portfolio has one per asset.
  2. 2The coupling, scaled by gamma and by its own strength. On the MaxCut graph every coupling was the same size; here each pair has its own.
  3. 3The mixer does not change with the problem. Only the cost step knows what is being optimized.
Run on QollabBackend

Every pair of the eight assets gets a coupling, twenty-eight in all, so one cost step is 56 cx gates, the figure the project's README gives for one layer.

The six extra qubits the project measures alongside the eight asset qubits carry market state and are prepared with single-qubit gates only. Its showcase describes that as the choice that keeps the circuit growing by one qubit per asset rather than per signal.

Quantum Systemic OracleQuantum Systemic OracleJamie Dominguez · u/jamie · Python + QiskitBlockchain oracle that publishes a quantum-computed 'systemic risk score' for crypto markets every day. Smart contracts and prediction markets can use the quantum-derived risk data as a primitive. Commoditizing quantum compute as an on-chain data feed.

What approximate means

Two numbers set the expectations for one layer of QAOA. Farhi, Goldstone and Gutmann's guarantee is an average cut of at least 0.6924 times the best possible on any graph where every node has three edges. Goemans and Williamson's classical algorithm from 1995 guarantees 0.878.

On the Courier graph the tuned layer averages 25.5 of a possible 33, which is 0.77. A greedy local search from a random split, the simplest classical heuristic there is, averages about 31 on the same graph and finds 33 within a few restarts, in milliseconds.

So on this graph the average sample from one layer is worse than what a classical heuristic returns. On any problem small enough to score exhaustively, that comparison can be made exactly.

Quantum Courier's README says which stages the classical solvers win, and keeps the stage where four QAOA variants lost on Forte to classical simulated annealing.

A QAOA run therefore reports the best string in the tally, checked classically, rather than the average. The Oracle's file shows that step; ranked is its tally with the strings below a noise floor already removed:

Qollab.py Python · Playground versionOpen in Playground ↗
nonempty = [(b, c) for b, c in ranked if any(b)]
rec_pool = nonempty or ranked
1rec_bits, rec_count = min(rec_pool, key=lambda kv: classical_cost(list(kv[0]), Q_matrix))
top_bits = list(rec_bits)
top_held = [ASSETS[i] for i, b in enumerate(top_bits) if b]
top_cost = classical_cost(top_bits, Q_matrix)
2gap = (top_cost - opt_cost) / abs(opt_cost) * 100.0 if opt_cost else 0.0
  1. 1Every sampled portfolio above the noise floor is re-scored with the classical cost function, and the cheapest non-empty one, if any, is the recommendation. The most frequent string is only reported, never trusted on its own.
  2. 2The gap to the brute-force optimum over all 256 portfolios. At eight assets the true answer is checkable, so the quantum run is graded rather than believed.
Run on QollabBackend

The gap to the optimum feeds a solver-quality signal that contributes 5% of the Oracle's published index. Its README describes the result as a headline number that never hinges on solver perfection but degrades with it.

QAOA's case rests on two things: strings long enough that the brute-force table disappears, and layers deep enough that the average climbs. Both are questions about hardware.

Depth that helps on the simulator and hurts on the machine

Each extra layer adds another cost step and another mixer step with their own pair of angles. On an ideal simulator the best achievable average never falls as layers are added, and usually rises. How many layers there are is called p.

In the Oracle's file p is one constant, QAOA_LAYERS = 2, and the whole circuit is a loop over that constant:

Qollab.py Python · Playground versionOpen in Playground ↗
def asset_only_qaoa_circuit(gammas, betas, h, J_pairs, n):
    qc = QuantumCircuit(n)
    for i in range(n):
        qc.h(i)
1    for layer in range(len(gammas)):
        apply_cost_layer(qc, gammas[layer], h, J_pairs, n)
        apply_mixer_layer(qc, betas[layer], n)
    return qc
  1. 1One pass per layer, each with its own gamma and beta. Two layers means four angles to tune. The eight-asset cost step is 56 cx gates, so two layers are 112 and three are 168.
Run on QollabBackend

The project tested one, two and three layers on IonQ Forte and wrote down what happened. In its own words the binding constraint was two-qubit gate error rather than circuit depth, meaning the number of gate layers the compiled circuit runs in sequence. About ninety two-qubit gates was the envelope.

One layer, at 56 gates, performed best. Deeper circuits "look marginally better on the ideal simulator but flatten toward uniform on the QPU", the quantum processor.

Every two-qubit gate carries an error, the errors compound, and past a certain count the distribution the circuit was shaping is lost to the noise. On the Oracle's runs it flattened back towards the uniform one the circuit started from.

Every QAOA run on today's hardware trades layers against fidelity. More layers can raise the ceiling and cost fidelity on the machine, and the layer count that wins on the simulator is rarely the one that wins on the machine.

Google's 2021 experiment found the same trade on a different machine. On its superconducting chip, problems whose graph matched the qubit wiring improved with more layers. Problems that needed extra gates to route around the wiring got worse as they grew.

A trapped-ion machine connects every qubit to every other, which is why the Oracle's twenty-eight couplings need no routing, and why its limit is the gate count itself.

The working rule: tune and debug on the simulator, then use the machine for the one question it alone answers. Does the distribution you shaped survive the gate count at the layer count you chose?

Start with one qubit

A quantum optimizer is a circuit whose shape is fixed by the problem and whose behaviour is fixed by a handful of angles. The problem becomes a cost, the cost becomes phases, and a mixer turns the phases into weight. A classical loop chooses the angles that push the weight towards the answers you want.

What comes back is a tally, and the answer you keep is the best string in it, scored by ordinary code.

Start with the one-qubit circuit. Fork the QUBO-with-QAOA example, paste the one-qubit listing over its code, and run it: what comes back can be predicted before the run. Set gamma to zero and 0 and 1 come back about equally; set it back to −π/4 and every shot reads 1, with every gate unchanged.

Then the one-edge listing the same way: at −π/4 and π/8 only 01 and 10 come back. Then Quantum Courier's script at the pairs from the table above.

Change one angle, then change the graph.

Fork the example, paste the one-edge listing over its code, and move gamma from −π/4 towards zero, reading the cut off the counts before the run confirms it. Then add a third node and an edge, and find the new pair. Everything here is open and yours to build on.

Stay in the loop.

Get the latest tutorials, demos, and project showcases straight to your inbox. No noise, just the good stuff.