Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
399 changes: 399 additions & 0 deletions exercises/module4_capstone_qubo_EXERCISES.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,399 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "e6824407",
"metadata": {},
"source": [
"# QoolQit Exercises — Module 4 (Capstone)\n",
"## Putting it all together: solving a QUBO problem\n",
"\n",
"Time to assemble everything from Modules 1–3 into a complete application:\n",
"solving a **QUBO** (Quadratic Unconstrained Binary Optimization) problem on a\n",
"neutral-atom quantum computer. Every step of the pipeline is one exercise —\n",
"each uses a tool you already practiced:\n",
"\n",
"```\n",
"QUBO matrix ──▶ classical baseline (NumPy)\n",
" │\n",
" ▶ DataGraph.from_matrix (Module 1)\n",
" ▶ InteractionEmbedder (Module 1)\n",
" ▶ Register.from_graph (Module 2)\n",
" ▶ annealing Drive (Module 2)\n",
" ▶ QuantumProgram + compile_to (Modules 2–3)\n",
" ▶ LocalEmulator + histogram (Module 3)\n",
"```\n",
"\n",
"\n",
"> **How to use this notebook.** \n",
"> - Cells marked **✏️ Exercise** contain gaps\n",
"> indicated by `...` or `# TODO` — replace them with working code following\n",
"> the instructions. \n",
"> - Cells marked **✅ Check** verify your answer: run them\n",
"> after completing the exercise. Everything else is provided and runs as-is.\n",
"> A separate **solution notebook** will be published.\n",
">\n",
"> **API note:** we use qoolqit version 1.4"
]
},
{
"cell_type": "markdown",
"id": "17dd00c7",
"metadata": {},
"source": [
"## 1. The problem\n",
"\n",
"A QUBO instance on $N$ variables is a symmetric $N\\times N$ matrix $Q$.\n",
"Solving it means finding the bitstring $z \\in \\{0,1\\}^N$ minimizing\n",
"\n",
"$$\n",
"f(z) = z^TQz= \\sum_i Q_{ii} z_i + \\sum_{i<j} Q_{ij} z_i z_j .\n",
"$$\n",
"\n",
"Diagonal entries are *linear* costs (paid when $z_i = 1$); off-diagonal\n",
"entries are *pairwise* costs (paid when both bits are 1). Our instance:\n",
"\n",
"$$ Q= \\begin{pmatrix}\n",
"-10.0 & 0.52870372 & 0.52870372 & 0 & 0 \\\\\n",
"0.52870372 & -10.0 & 21.0 & 5.61708608 & 0 \\\\\n",
"0.52870372 & 21.0 & -10.0 & 0 & 5.61708608 \\\\\n",
"0 & 5.61708608 & 0 & -10.0 & 0 \\\\\n",
"0 & 0 & 5.61708608 & 0 & -10.0\n",
"\\end{pmatrix} $$\n",
"\n",
"**Why atoms can solve this natively.** In the Rydberg Hamiltonian with the\n",
"drive off ($\\Omega = 0$),\n",
"\n",
"$$\n",
"H = - \\sum_i \\delta_i \\hat{n}_i + \\sum_{i<j} \\tilde{J}_{ij} \\hat{n}_i \\hat{n}_j ,\n",
"$$\n",
"\n",
"which is *exactly* $f(z)$ under the dictionary\n",
"$Q_{ii} \\leftrightarrow -\\delta_i$, $Q_{ij} \\leftrightarrow \\tilde J_{ij}$,\n",
"$z_i = \\hat n_i$. The **annealing protocol** exploits the adiabatic theorem:\n",
"start in the ground state $|0\\rangle^{\\otimes N}$ of an easy Hamiltonian,\n",
"evolve *slowly* while morphing it into the QUBO Hamiltonian, and the system\n",
"ends in the QUBO's ground state — the optimal bitstring — which we read out\n",
"by measuring."
]
},
{
"cell_type": "markdown",
"id": "bb36a421",
"metadata": {},
"source": [
"### ✏️ Exercise 4.1 — Classical baseline by brute force\n",
"\n",
"With $N=5$ there are only $2^5 = 32$ candidates, so we can compute the exact\n",
"optimum classically as ground truth (this scales exponentially — the whole\n",
"point of a quantum approach for large $N$!).\n",
"\n",
"1. Enumerate all bitstrings (💡 `np.binary_repr(i, width)`).\n",
"2. Compute the cost of each candidate as $z^T\\,\\mathrm{triu}(Q)\\,z$\n",
" (💡 `np.triu` counts each pair once, matching $\\sum_{i<j}$).\n",
"3. Sort by cost and save the **two best** bitstrings in\n",
" `first_two_best_solutions`, printing them with their costs."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3e519327",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"Q = np.array(\n",
" [\n",
" [-10.0, 0.52870372, 0.52870372, 0, 0],\n",
" [0.52870372, -10.0, 21.0, 5.61708608, 0],\n",
" [0.52870372, 21.0, -10.0, 0, 5.61708608],\n",
" [0, 5.61708608, 0, -10.0, 0],\n",
" [0, 0, 5.61708608, 0, -10.0],\n",
" ]\n",
")\n",
"\n",
"# TODO: enumerate all 2**N candidate bitstrings (as strings)\n",
"solution_candidates = np.array([... for i in range(...)])\n",
"\n",
"# TODO: costs z^T triu(Q) z\n",
"solution_candidates_list = np.array([... for b in solution_candidates])\n",
"costs = np.array([... for z in solution_candidates_list])\n",
"\n",
"# TODO: sort and keep the two best\n",
"idx_sort = ...\n",
"sorted_costs = ...\n",
"sorted_solutions = ...\n",
"\n",
"print(\"Two best minimizers: \", sorted_solutions[:2])\n",
"print(\"Respective costs: \", sorted_costs[:2])\n",
"first_two_best_solutions = sorted_solutions[:2]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1a47c4e2",
"metadata": {},
"outputs": [],
"source": [
"# ✅ Check — the optimum is a degenerate pair\n",
"assert set(first_two_best_solutions) == {\"11011\", \"10111\"}\n",
"assert np.isclose(sorted_costs[0], sorted_costs[1])\n",
"print(\"Classical optimum found:\", first_two_best_solutions)\n",
"# (The degeneracy reflects a symmetry of Q: swapping variables 1<->2 and\n",
"# 3<->4 simultaneously leaves Q invariant and maps one optimum to the other.)"
]
},
{
"cell_type": "markdown",
"id": "d75ea118",
"metadata": {},
"source": [
"### ✏️ Exercise 4.2 — Load and embed the problem *(Module 1 tools)*\n",
"\n",
"1. Since the QUBO is **scale invariant**, normalize it: `Q = Q / Q.max()`\n",
" (this matches the embedder's convention $\\max \\tilde J = 1$ and\n",
" simplifies the drive design).\n",
"2. *(Optional but instructive)* Build `DataGraph.from_matrix(Q.copy())` and\n",
" draw it — the QUBO *is* a weighted graph.\n",
"3. Embed the matrix with an `InteractionEmbedder` into `embedded_graph`,\n",
" draw it, and print each realized interaction next to its target `Q[i, j]`.\n",
" The large couplings should match closely; exact zeros can only be\n",
" approximated (atoms at finite distance always interact a little)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d138668e",
"metadata": {},
"outputs": [],
"source": [
"from qoolqit import DataGraph\n",
"\n",
"# TODO 1: normalize the QUBO (scale invariance)\n",
"Q = ...\n",
"\n",
"# TODO 2 (optional): the QUBO as a weighted graph\n",
"graph = DataGraph.from_matrix(Q.copy())\n",
"graph.draw()\n",
"\n",
"# TODO 3: embed the interaction matrix\n",
"embedded_graph = ...\n",
"embedded_graph.draw()\n",
"\n",
"for (i, j), J in embedded_graph.interactions().items():\n",
" print(f\"pair ({i},{j}): J = {J:.4f} target Q = {Q[i, j]:.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "9f8199ff",
"metadata": {},
"source": [
"### ✏️ Exercise 4.3 — Register and annealing Drive *(Module 2 tools)*\n",
"\n",
"1. Build the register directly from the embedded graph:\n",
" `Register.from_graph(embedded_graph)`.\n",
"2. Choose the annealing parameters:\n",
" - `omega`: the **median** of the strictly positive entries of `Q`\n",
" (a good handwavy value for the peak amplitude);\n",
" - `delta_i = -1.0` (initial detuning: with $\\Omega=0$ and $\\delta<0$,\n",
" $|0\\rangle^{\\otimes N}$ is the unique ground state);\n",
" - `delta_f = -np.diag(Q)[0]` (final detuning matching the QUBO diagonal\n",
" under $Q_{ii} \\leftrightarrow -\\delta_i$; all diagonal entries are\n",
" equal here).\n",
"3. Build the schedule with `T = 40` (safely adiabatic, $\\tilde t \\gg 1$):\n",
" a trapezoidal `PiecewiseLinearWaveform` amplitude\n",
" ($0 \\to \\omega \\to \\omega \\to 0$ over $[T/4, T/2, T/4]$ — exactly\n",
" Exercise 2.2!) and a `RampWaveform` detuning from `delta_i` to `delta_f`.\n",
"4. Assemble the `Drive` and `draw()` it: check the boundary conditions of the\n",
" annealing protocol at $t=0$ and $t=T$."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "888008b9",
"metadata": {},
"outputs": [],
"source": [
"# TODO 1: the register from the embedded graph\n",
"register = ...\n",
"\n",
"# TODO 2: annealing parameters\n",
"omega = ...\n",
"delta_i = ...\n",
"delta_f = ...\n",
"\n",
"# TODO 3: annealing schedule\n",
"T = 40\n",
"wf_amp = ...\n",
"wf_det = ...\n",
"\n",
"# TODO 4: the drive\n",
"drive = ...\n",
"drive.draw()"
]
},
{
"cell_type": "markdown",
"id": "63e7a867",
"metadata": {},
"source": [
"### ✏️ Exercise 4.4 — Program, compilation and execution *(Module 3 tools)*\n",
"\n",
"1. Assemble the `QuantumProgram` and compile it to an `AnalogDevice()`.\n",
"2. Run it on a `LocalEmulator` and store the measured\n",
" `results.final_bitstrings` in `counts`.\n",
"3. Print the five most common bitstrings (`counts.most_common(5)`)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "77dd3eb6",
"metadata": {},
"outputs": [],
"source": [
"# TODO: assemble and compile\n",
"program = ...\n",
"program.compile_to(device=..., profile=\"max_energy\")\n",
"\n",
"# TODO: run and collect counts\n",
"emulator = ...\n",
"counts = ...\n",
"\n",
"print(counts.most_common(5))"
]
},
{
"cell_type": "markdown",
"id": "da8d6c7b",
"metadata": {},
"source": [
"### Plotting helper (provided)\n",
"\n",
"Histogram of the sampled bitstrings; the classical optima from Exercise 4.1\n",
"are highlighted in **green**. Run as-is."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7fa23740",
"metadata": {},
"outputs": [],
"source": [
"from collections import Counter\n",
"\n",
"import matplotlib.pyplot as plt\n",
"\n",
"\n",
"def plot_distribution(counter, solutions, bins=10):\n",
" \"\"\"Histogram of sampled bitstrings; known optimal `solutions` in green.\"\"\"\n",
" counter = Counter(counter)\n",
" counter = dict(counter.most_common(bins))\n",
" color = [\n",
" \"tab:green\" if key in solutions.tolist() else \"tab:blue\" for key in counter\n",
" ]\n",
" _, ax = plt.subplots()\n",
" ax.set_xlabel(\"Bitstrings\")\n",
" ax.set_ylabel(\"Counts\")\n",
" ax.bar(\n",
" range(len(counter)), counter.values(), color=color, tick_label=counter.keys()\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "e5e4ed4b",
"metadata": {},
"source": [
"### ✏️ Exercise 4.5 — Analyze… and improve!\n",
"\n",
"1. Plot the distribution with the optima highlighted. **Look carefully**: the\n",
" green bars are high, but is the *top* bar green? With this schedule the\n",
" evolution is not adiabatic enough, and a suboptimal bitstring can win.\n",
"2. Now use the trick from the tutorial: recompile with\n",
" `device_max_duration_ratio=1`, stretching the schedule to the device's\n",
" **maximum allowed duration** (slower ⇒ more adiabatic). Re-run and plot\n",
" again. The optima should now dominate clearly."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5686356d",
"metadata": {},
"outputs": [],
"source": [
"# TODO 1: plot the first result\n",
"plot_distribution(..., ...)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10987e66",
"metadata": {},
"outputs": [],
"source": [
"# TODO 2: recompile stretched to the device's maximum duration, re-run, re-plot\n",
"program.compile_to(device=..., profile=\"max_energy\", device_max_duration_ratio=...)\n",
"counts_slow = ...\n",
"plot_distribution(..., ...)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "da80ea6e",
"metadata": {},
"outputs": [],
"source": [
"# ✅ Check — the two most sampled bitstrings are the classical optima\n",
"top2 = {b for b, _ in counts_slow.most_common(2)}\n",
"assert top2 == set(first_two_best_solutions), f\"Top-2 sampled {top2} != optima\"\n",
"print(\"🎉 QUBO solved: the classical optima are the most sampled bitstrings!\")"
]
},
{
"cell_type": "markdown",
"id": "b74c6661",
"metadata": {},
"source": [
"## Going further\n",
"\n",
"**Ideas to explore**\n",
"- Generate your own QUBO from a geometry (place atoms, compute $1/r^6$\n",
" couplings, add a diagonal) and check the pipeline solves it.\n",
"- Shorten `T` and watch the solution quality degrade — quantify adiabaticity.\n",
"- Trigger the two classic `CompilationError`s from Module 3 with this\n",
" register (amplitude too large; register too big).\n",
"\n",
"## 🎓 Congratulations!\n",
"\n",
"You built a complete neutral-atom application from first principles:\n",
"**graphs → embedding → register → drive → program → compilation → execution\n",
"→ verified quantum solution.** Every tool you used generalizes far beyond\n",
"QUBOs — happy experimenting with QoolQit!"
]
},
{
"cell_type": "markdown",
"id": "bf2d866c",
"metadata": {},
"source": []
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading