{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "a005e247",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-05T16:35:50.284982Z",
"iopub.status.busy": "2026-08-05T16:35:50.284807Z",
"iopub.status.idle": "2026-08-05T16:35:50.733741Z",
"shell.execute_reply": "2026-08-05T16:35:50.733410Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"['Ket v0.10.1',\n",
" 'libket v0.7.1 [rustc 1.97.0 (2d8144b78 2026-07-07) x86_64-unknown-linux-gnu]',\n",
" 'kbw v0.5.1 [rustc 1.97.0 (2d8144b78 2026-07-07) x86_64-unknown-linux-gnu]']"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from typing import Callable\n",
"from functools import partial\n",
"\n",
"from scipy.optimize import minimize, OptimizeResult\n",
"from IPython.display import display\n",
"\n",
"import plotly.io as pio\n",
"pio.renderers.default = \"notebook_connected\"\n",
"\n",
"from ket import *\n",
"from ket import ket_version\n",
"\n",
"ket_version()"
]
},
{
"cell_type": "markdown",
"id": "7ce6969b",
"metadata": {},
"source": [
"# Quantum Approximate Optimization Algorithm (QAOA)\n",
"\n",
"The **Quantum Approximate Optimization Algorithm (QAOA)** is a hybrid quantum-classical\n",
"variational algorithm designed to find approximate solutions to combinatorial optimization problems.\n",
"Tailored for near-term (NISQ) devices, QAOA functions as a discretized version of adiabatic quantum computing.\n",
"\n",
"At its core, the algorithm prepares a uniform initial quantum state $|+\\rangle^{\\otimes n}$, which then undergoes\n",
"$p$ alternating layers of unitary evolution driven by two complementary Hamiltonians. Finally, the circuit is\n",
"measured and the evolution parameters are fine-tuned by a classical optimizer.\n",
"\n",
"To understand how QAOA operates, let's explore its three main building blocks:"
]
},
{
"cell_type": "markdown",
"id": "d281d2de",
"metadata": {},
"source": [
"## 1. Problem Hamiltonian ($H_C$)\n",
"\n",
"This block encodes the classical objective (or cost) function of our optimization problem into a quantum format.\n",
"\n",
"- **Role:** The decision variables of the problem are mapped to Pauli-Z operators in the computational basis. The optimal solution corresponds to the lowest energy eigenstate (the ground state) of this Hamiltonian.\n",
"\n",
"- **Circuit Evolution:** The quantum state evolves under the problem Hamiltonian according to the operator: $\\hat U_C(\\gamma_k) = e^{-i\\gamma_k H_C}$"
]
},
{
"cell_type": "markdown",
"id": "83b2cc21",
"metadata": {},
"source": [
"## 2. Mixer Hamiltonian ($H_M$)\n",
"\n",
"To effectively explore the solution space and prevent the system from getting stuck, we introduce the mixer Hamiltonian.\n",
"\n",
"- **Role:** The fundamental requirement for $H_M$ is that it **must not commute** with the problem Hamiltonian $H_C$. The standard implementation uses a sum of Pauli-X operators acting on all qubits.\n",
"\n",
"- **Circuit Evolution:** The mixer creates transitions between different computational basis configurations, driven by the operator: $\\hat U_M(\\beta_k) = e^{-i\\beta_k H_M}$"
]
},
{
"cell_type": "markdown",
"id": "16c57984",
"metadata": {},
"source": [
"## 3. Classical Optimizer\n",
"\n",
"The iterative search for optimal parameters is handed over to a classical processor.\n",
"\n",
"- **Role:** By measuring the final state of the quantum circuit, we obtain the expected value of the cost function: $F(\\gamma, \\beta) = \\langle\\psi(\\gamma, \\beta)|H_C|\\psi(\\gamma, \\beta)\\rangle$. The classical optimizer evaluates this outcome and computes a new set of angles, $\\gamma$ and $\\beta$, aiming to minimize this expected value in the next iteration.\n",
"\n",
"- **Significance:** Once the optimizer converges on the ideal parameters $(\\gamma, \\beta)$, the quantum circuit will output the state encoding the approximate solution with high probability. Selecting a robust classical optimization routine is crucial to avoiding local minima and mitigating challenges like _barren plateaus_."
]
},
{
"cell_type": "markdown",
"id": "4a682a55",
"metadata": {},
"source": [
"## Implementation with Ket\n",
"\n",
"Let's implement the base QAOA structure using `ket`. We define functions to:\n",
"\n",
"1. Apply a single QAOA layer (`qaoa_layer`).\n",
"2. Build the full $p$-layer ansatz (`ansatz`).\n",
"3. Execute the complete hybrid optimization loop (`qaoa`).\n",
"\n",
"Ket's {func}`~ket.gates.evolve` function accepts a {class}`~ket.expv.Hamiltonian` object and automatically\n",
"decomposes the time-evolution operator $e^{-i\\theta H}$ into native quantum gates, making the implementation\n",
"concise and readable."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "530bd12f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-05T16:35:50.735220Z",
"iopub.status.busy": "2026-08-05T16:35:50.735097Z",
"iopub.status.idle": "2026-08-05T16:35:50.739132Z",
"shell.execute_reply": "2026-08-05T16:35:50.738790Z"
}
},
"outputs": [],
"source": [
"def qaoa_layer(\n",
" problem_h: Callable[[Quant], Hamiltonian],\n",
" mixer_h: Callable[[Quant], Hamiltonian],\n",
" qubits: Quant,\n",
" gamma: float,\n",
" beta: float,\n",
"):\n",
" \"\"\"\n",
" Applies a single layer (p=1) of the QAOA Ansatz to the provided qubits.\n",
"\n",
" Args:\n",
" problem_h: Function that takes the allocated qubits and returns the problem Hamiltonian (cost function).\n",
" mixer_h: Function that takes the allocated qubits and returns the mixer Hamiltonian.\n",
" qubits: Reference to the qubits allocated in the current process.\n",
" gamma: Variational parameter associated with the evolution of the problem Hamiltonian.\n",
" beta: Variational parameter associated with the evolution of the mixer Hamiltonian.\n",
" \"\"\"\n",
" evolve(problem_h(qubits) * gamma)\n",
" evolve(mixer_h(qubits) * beta)\n",
"\n",
"\n",
"def ansatz(\n",
" problem_h: Callable[[Quant], Hamiltonian],\n",
" mixer_h: Callable[[Quant], Hamiltonian],\n",
" initial_state: Callable[[any], None],\n",
" qubits: Quant,\n",
" gamma: list[float],\n",
" beta: list[float],\n",
"):\n",
" \"\"\"\n",
" Constructs the complete QAOA quantum circuit, applying the initial state\n",
" and the 'p' layers of alternating evolution.\n",
"\n",
" Args:\n",
" problem_h: Problem Hamiltonian generator function.\n",
" mixer_h: Mixer Hamiltonian generator function.\n",
" initial_state: Function that prepares the appropriate initial state on the qubits\n",
" (e.g., uniform superposition with H gates, or Dicke state for XY Mixer).\n",
" qubits: Reference to the circuit qubits.\n",
" gamma: List with the 'p' variational gamma parameters.\n",
" beta: List with the 'p' variational beta parameters.\n",
" \"\"\"\n",
" initial_state(qubits)\n",
" for g, b in zip(gamma, beta):\n",
" qaoa_layer(problem_h, mixer_h, qubits, g, b)\n",
"\n",
"\n",
"def qaoa(\n",
" problem_h: Callable[[Quant], Hamiltonian],\n",
" mixer_h: Callable[[Quant], Hamiltonian],\n",
" initial_state: Callable[[any], None],\n",
" num_qubits: int,\n",
" p: int,\n",
" dt: float = 0.5,\n",
" method: str = \"COBYLA\",\n",
") -> tuple[OptimizeResult, Samples]:\n",
" \"\"\"\n",
" Executes the QAOA hybrid (classical-quantum) optimization loop.\n",
"\n",
" Initializes parameters inspired by a quantum annealing process\n",
" (increasing gamma, decreasing beta) and uses a classical optimizer to\n",
" find the configuration that minimizes the expected value of the problem Hamiltonian.\n",
"\n",
" Args:\n",
" problem_h: Problem Hamiltonian generator function.\n",
" mixer_h: Mixer Hamiltonian generator function.\n",
" initial_state: Initial state preparation function.\n",
" num_qubits: Total number of qubits required for the problem.\n",
" p: Number of layers (depth) of the QAOA Ansatz.\n",
" dt: Scale factor for the parameter initialization heuristic (default: 0.5).\n",
" method: SciPy classical optimization algorithm to use (default: \"COBYLA\").\n",
"\n",
" Returns:\n",
" A tuple containing:\n",
" - The OptimizeResult object returned by SciPy with optimization details.\n",
" - The final sampling of the optimized state (measurements and counts).\n",
" \"\"\"\n",
"\n",
" def objective(parameters, final=False):\n",
" gamma = parameters[:p]\n",
" beta = parameters[p:]\n",
"\n",
" # Instantiates a new Ket Process\n",
" process = Process(num_qubits=num_qubits, simulator=\"dense\")\n",
" qubits = process.alloc(num_qubits)\n",
" ansatz(problem_h, mixer_h, initial_state, qubits, gamma, beta)\n",
"\n",
" if final:\n",
" # Samples the final state to retrieve likely solutions\n",
" return sample(qubits, 100)\n",
"\n",
" # Returns the energy (expected value) to the classical optimizer\n",
" result = exp_value(problem_h(qubits)).get()\n",
" return result\n",
"\n",
" # Initialization heuristic based on annealing\n",
" first_gamma = [i / p * dt for i in range(1, p + 1)] # Increasing\n",
" first_beta = [(1 - i / p) * dt for i in range(0, p)] # Decreasing\n",
"\n",
" # Classical optimization loop\n",
" res = minimize(\n",
" objective,\n",
" first_gamma + first_beta, # Gamma comes first, Beta second\n",
" method=method,\n",
" )\n",
"\n",
" # Execute the circuit one last time with optimal parameters to get the probability distribution\n",
" final_sample = objective(res.x, final=True)\n",
"\n",
" return res, final_sample"
]
},
{
"cell_type": "markdown",
"id": "7542e93e",
"metadata": {},
"source": [
"## Example 1: Max-Cut\n",
"\n",
"The **Max-Cut** problem is one of the most widely used benchmarks for quantum optimization algorithms.\n",
"It is an NP-Hard graph problem: finding an exact solution becomes classically intractable as the number\n",
"of vertices grows.\n",
"\n",
"Given an undirected graph $G = (V, E)$, the objective is to partition the vertices into two disjoint\n",
"sets $A$ and $B$ such that the number of edges crossing the partition is maximized.\n",
"\n",
"To model this on a quantum computer, we map each graph vertex to a qubit:\n",
"\n",
"- State $|0\\rangle \\rightarrow$ Vertex belongs to set $A$.\n",
"- State $|1\\rangle \\rightarrow$ Vertex belongs to set $B$."
]
},
{
"cell_type": "markdown",
"id": "7a904b24",
"metadata": {},
"source": [
"### Max-Cut Hamiltonian Formulation\n",
"\n",
"For QAOA, we need to mathematically formulate a cost function that evaluates the quality of our cut using Pauli operators. The Pauli-Z operator is perfect for this:\n",
"\n",
"- $\\left<0|Z|0\\right> = +1$\n",
"- $\\left<1|Z|1\\right> = -1$\n",
"\n",
"For any two vertices connected by an edge $(u, v)$, their interaction is measured by the product $Z_u Z_v$:\n",
"\n",
"- If both vertices are in the **same set** ($\\left|00\\right>$ or $\\left|11\\right>$), the product $Z_u Z_v$ is $+1$.\n",
"- If they are in **different sets** ($\\left|01\\right>$ or $\\left|10\\right>$), the product $Z_u Z_v$ is $-1$.\n",
"\n",
"We can express the contribution of a single edge to our cut as $\\frac{1}{2}(1 - Z_u Z_v)$. This evaluates to $1$ if the edge is cut, and $0$ if it is not.\n",
"\n",
"The total Hamiltonian is the sum of these expressions across all edges in the graph. Since classical optimizers typically aim to **minimize** a function, we multiply the Hamiltonian by $-1$. This ensures that the state representing the maximum number of cuts corresponds to the lowest possible energy (the ground state):\n",
"\n",
"$$H_C = -\\frac{1}{2} \\sum_{(u,\\ v) \\in E} (1 - Z_u Z_v)$$"
]
},
{
"cell_type": "markdown",
"id": "plot_intro",
"metadata": {},
"source": [
"### Graph Visualization\n",
"\n",
"Before solving, let's visualize our problem graph using Plotly for an interactive view.\n",
"We define a helper function that draws the graph and highlights cut edges based on a given\n",
"partition of vertices."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "maxcut_plotly_func",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-05T16:35:50.740088Z",
"iopub.status.busy": "2026-08-05T16:35:50.740018Z",
"iopub.status.idle": "2026-08-05T16:35:50.744165Z",
"shell.execute_reply": "2026-08-05T16:35:50.743836Z"
}
},
"outputs": [],
"source": [
"def plot_maxcut_graph(\n",
" graph_edges: list[tuple[int, int]],\n",
" num_nodes: int,\n",
" qubits_state: list[int] | int,\n",
" title: str = \"Max-Cut Graph\",\n",
"):\n",
" \"\"\"\n",
" Plots the Max-Cut problem graph using Plotly, coloring nodes according to their\n",
" partition assignment and highlighting the cut edges.\n",
"\n",
" Args:\n",
" graph_edges: List of tuples representing the graph edges.\n",
" num_nodes: Total number of nodes/qubits.\n",
" qubits_state: List where the index is the node and the value is the state (0 or 1).\n",
" title: Plot title.\n",
" \"\"\"\n",
" import networkx as nx\n",
" import plotly.graph_objects as go\n",
"\n",
" if isinstance(qubits_state, int):\n",
" qubits_state = list(map(int, f\"{qubits_state:0{num_nodes}b}\"))\n",
"\n",
" G = nx.Graph()\n",
" G.add_nodes_from(range(num_nodes))\n",
" G.add_edges_from(graph_edges)\n",
" pos = nx.spring_layout(G, seed=42)\n",
"\n",
" # --- Edge traces ---\n",
" cut_edge_x, cut_edge_y = [], []\n",
" non_cut_edge_x, non_cut_edge_y = [], []\n",
" for u, v in graph_edges:\n",
" x0, y0 = pos[u]\n",
" x1, y1 = pos[v]\n",
" if qubits_state[u] != qubits_state[v]:\n",
" cut_edge_x += [x0, x1, None]\n",
" cut_edge_y += [y0, y1, None]\n",
" else:\n",
" non_cut_edge_x += [x0, x1, None]\n",
" non_cut_edge_y += [y0, y1, None]\n",
"\n",
" edge_traces = [\n",
" go.Scatter(\n",
" x=cut_edge_x,\n",
" y=cut_edge_y,\n",
" mode=\"lines\",\n",
" line=dict(width=4, color=\"#e74c3c\"),\n",
" name=\"Cut edge\",\n",
" hoverinfo=\"none\",\n",
" ),\n",
" go.Scatter(\n",
" x=non_cut_edge_x,\n",
" y=non_cut_edge_y,\n",
" mode=\"lines\",\n",
" line=dict(width=2, color=\"#aaaaaa\", dash=\"dash\"),\n",
" name=\"Non-cut edge\",\n",
" hoverinfo=\"none\",\n",
" ),\n",
" ]\n",
"\n",
" # --- Node traces ---\n",
" node_x = [pos[n][0] for n in G.nodes()]\n",
" node_y = [pos[n][1] for n in G.nodes()]\n",
" node_colors = [\"#e67e22\" if qubits_state[n] == 1 else \"#3498db\" for n in G.nodes()]\n",
" node_text = [\n",
" f\"Node {n}
Set {'B' if qubits_state[n] == 1 else 'A'} (|{qubits_state[n]}⟩)\"\n",
" for n in G.nodes()\n",
" ]\n",
" node_labels = [str(n) for n in G.nodes()]\n",
"\n",
" node_trace = go.Scatter(\n",
" x=node_x,\n",
" y=node_y,\n",
" mode=\"markers+text\",\n",
" hoverinfo=\"text\",\n",
" hovertext=node_text,\n",
" text=node_labels,\n",
" textposition=\"middle center\",\n",
" textfont=dict(size=14, color=\"white\"),\n",
" marker=dict(\n",
" size=40,\n",
" color=node_colors,\n",
" line=dict(width=2, color=\"#333\"),\n",
" ),\n",
" name=\"Nodes\",\n",
" showlegend=False,\n",
" )\n",
"\n",
" num_cuts = sum(1 for u, v in graph_edges if qubits_state[u] != qubits_state[v])\n",
"\n",
" fig = go.Figure(\n",
" data=edge_traces + [node_trace],\n",
" layout=go.Layout(\n",
" title=dict(\n",
" text=f\"{title} — {num_cuts}/{len(graph_edges)} edges cut\", x=0.5\n",
" ),\n",
" showlegend=True,\n",
" hovermode=\"closest\",\n",
" margin=dict(b=20, l=5, r=5, t=50),\n",
" xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),\n",
" yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),\n",
" legend=dict(x=0, y=1),\n",
" ),\n",
" )\n",
" fig.show()"
]
},
{
"cell_type": "markdown",
"id": "prob_instance",
"metadata": {},
"source": [
"### Problem Instance\n",
"\n",
"Let's define our test graph: 5 nodes and 6 edges. The initial partition shown below\n",
"is just a sample — the QAOA optimizer will find the best one."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "ffb1cf5b",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-05T16:35:50.744913Z",
"iopub.status.busy": "2026-08-05T16:35:50.744850Z",
"iopub.status.idle": "2026-08-05T16:35:52.108820Z",
"shell.execute_reply": "2026-08-05T16:35:52.107722Z"
}
},
"outputs": [
{
"data": {
"text/html": [
" \n",
" \n",
" "
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"