Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

QAOA for Graph Partitioning

Tags: algorithm optimization variational

This tutorial demonstrates how to solve the graph partitioning problem using the Quantum Approximate Optimization Algorithm (QAOA) with Qamomile.

The workflow is:

JijModeling problem → problem.eval() → QAOAConverter → transpile → sample → decode
  1. Formulate the problem with JijModeling.

  2. Create an instance with concrete data.

  3. Use QAOAConverter to build the QAOA circuit and Hamiltonian.

  4. Optimize the variational parameters with a classical optimizer.

  5. Sample the optimized circuit and decode the results.

Problem Formulation

Given an undirected graph G=(V,E)G = (V, E), the goal is to partition the vertices into two groups of equal size while minimizing the number of edges between the two groups.

Objective:

min⁡∑(u,v)∈E[xu(1−xv)+xv(1−xu)]\min \sum_{(u,v) \in E} \bigl[x_u(1 - x_v) + x_v(1 - x_u)\bigr]

Constraint:

∑u∈Vxu=∣V∣2\sum_{u \in V} x_u = \frac{|V|}{2}

where xu∈{0,1}x_u \in \{0, 1\} indicates which partition vertex uu belongs to.

Define the Problem with JijModeling

Loading...

Graph Instance

We use a fixed 8-node graph with 16 edges for reproducibility.

<Figure size 500x500 with 1 Axes>

Create the Instance

We extract the edge list from the graph and evaluate the JijModeling problem with the concrete data.

Set Up the QAOAConverter

QAOAConverter takes an OMMX instance and internally:

  1. Converts the problem to a QUBO (Quadratic Unconstrained Binary Optimization) form.

  2. Transforms from BINARY variables to SPIN variables.

  3. Builds the cost Hamiltonian as a sum of Pauli-Z operators.

Because the original problem has a constraint, the QUBO formulation folds it into the objective as a penalty term. This means the energy values from the decoded samples are not the original objective (number of cut edges) — they include the penalty. We will need to check feasibility and compute the true objective separately.

Hamiltonian((Z0, Z1): 1.0, (Z0, Z5): 1.0, (Z0, Z6): 1.0, (Z0, Z7): 1.0, (Z1, Z6): 1.0, (Z2, Z4): 1.0, (Z2, Z5): 1.0, (Z2, Z7): 1.0, (Z3, Z4): 1.0, (Z3, Z6): 1.0, (Z3, Z7): 1.0, (Z4, Z7): 1.0)

Transpile to an Executable Circuit

converter.transpile() builds a QAOA ansatz circuit with p layers and compiles it into an ExecutableProgram. The variational parameters gammas (cost layer) and betas (mixer layer) remain as runtime parameters.

Visualize the QAOA Circuit

QAOAConverter._transpile_quadratic() internally builds the sampling qkernel below and feeds it to the transpiler. For visualization we restate that qkernel verbatim, then trace it through the early pipeline (Transpiler.to_block to lower it into an IR Block, Transpiler.inline to expand the sub-qkernel calls) before handing the resulting Block to MatplotlibDrawer.draw. This renders exactly the Block structure the converter works with internally, with p=2 for readability.

<Figure size 10086x656 with 1 Axes>

Inspecting the Building Blocks

Inside qaoa_sampling we call qaoa_state, and qaoa_state itself is the composition of three smaller qkernels:

  • superposition_vector(n) — applies H to every qubit, preparing the initial state ∣+⟩⊗n|+\rangle^{\otimes n}.

  • ising_cost(quad, linear, q, gamma) — the cost layer: one RZZ per quadratic entry in quad and one RZ per linear entry in linear. The graph-partition spin model has no linear terms (linear={}), so only RZZ gates appear.

  • x_mixer(q, beta) — the mixer layer: applies RX(2\beta) to every qubit.

qaoa_layers(p, ...) is just the alternation of ising_cost and x_mixer, repeated p times. Below we render each piece on its own.

<Figure size 600x656 with 1 Axes>
<Figure size 3832x656 with 1 Axes>
<Figure size 600x656 with 1 Axes>

Optimize the QAOA Parameters

We use executable.sample() to evaluate the cost at each iteration of the classical optimizer. The optimizer explores different gammas and betas to minimize the mean energy of the sampled bitstrings.

Optimized cost: 15.799
Optimal params: [np.float64(0.8478), np.float64(-0.2839), np.float64(3.0681), np.float64(0.4712), np.float64(2.4944), np.float64(1.8724), np.float64(2.516), np.float64(0.743), np.float64(2.8569), np.float64(1.0257)]
Function evaluations: 103
<Figure size 800x400 with 1 Axes>

Sample with Optimized Parameters

With the optimized parameters, we sample the circuit to collect candidate solutions as bitstrings.

Analyze the Results

Feasibility Check

QAOA samples are candidate solutions — they are not guaranteed to satisfy the original constraints. The constraint ∑xu=∣V∣/2\sum x_u = |V|/2 was folded into the QUBO as a penalty, so infeasible bitstrings can still appear in the output.

SampleSet.summary is a DataFrame with one row per shot whose feasible column already encodes feasibility against the original constraints, so we can read the feasible-shot count straight off it.

Feasible samples: 530 / 1000 (53.0%)

Best Feasible Solution

SampleSet.best_feasible returns the feasible sample with the best (here: smallest) objective. Because the converter was built from an OMMX Instance, the reported objective is the original un-penalized one — i.e. the number of cut edges itself — and the decision-variable values come back keyed by the original variable IDs in decision_variables_df.

Best feasible solution: {0: 0, 1: 0, 2: 0, 3: 0, 4: 1, 5: 1, 6: 1, 7: 1}
Cut edges:             6

Objective Value Distribution

We plot the distribution of the true objective value (cut edges) for feasible samples only. summary already has the original objective per shot, so a value_counts() on the feasible slice gives the histogram directly.

<Figure size 800x400 with 1 Axes>

Visualize the Best Partition

We color the graph nodes according to the best feasible partition found by QAOA.

<Figure size 500x500 with 1 Axes>