diff --git a/exercises/module1_graphs_embedding_EXERCISES.ipynb b/exercises/module1_graphs_embedding_EXERCISES.ipynb new file mode 100644 index 0000000..9667d9f --- /dev/null +++ b/exercises/module1_graphs_embedding_EXERCISES.ipynb @@ -0,0 +1,298 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "713028f0", + "metadata": {}, + "source": [ + "# QoolQit Exercises — Module 1\n", + "## Graphs and Embedding\n", + "\n", + "Welcome to this hands-on introduction to **QoolQit**, the Python library for\n", + "algorithm development in the Rydberg analog model. Across four self-contained\n", + "modules you will learn all the building blocks of a QoolQit application, and\n", + "assemble them in a quantum application in the final module:\n", + "\n", + "| Module | Topic |\n", + "|--------|-------|\n", + "| 1 | Graphs and Embedding |\n", + "| 2 | Register, Drive and Quantum Programs |\n", + "| 3 | Compilation and Execution |\n", + "| 4 | Putting it all together: solving a QUBO |\n", + "\n", + "### In this module you will learn\n", + "- How to create graphs with the `DataGraph` class (pre-defined layouts,\n", + " random graphs, graphs from raw data)\n", + "- The difference between *abstract* graphs and graphs *with coordinates*\n", + "- How to give coordinates to an abstract graph.\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": "6226deb9", + "metadata": {}, + "source": [ + "## 1. The `DataGraph` class\n", + "\n", + "In QoolQit, problems and atom layouts are described by graphs. The\n", + "`DataGraph` class (a subclass of `networkx.Graph`) is the central data\n", + "structure: it can hold **connectivity** (edges), **coordinates** (positions\n", + "of the nodes in the plane), and node and edge **weights**." + ] + }, + { + "cell_type": "markdown", + "id": "f89ebcdc", + "metadata": {}, + "source": [ + "### ✏️ Exercise 1.1 — Pre-defined graph layouts\n", + "\n", + "Create and draw three graphs:\n", + "1. `g_line`: a **line** graph with 5 nodes;\n", + "2. `g_circle`: a **circle** graph with 6 nodes and `spacing=1.0`;\n", + "3. `g_square`: a **square** grid graph with `m=3` rows and `n=3` columns.\n", + "\n", + "Use each graph's `.draw()` method to visualize it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af9b7821", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: create the three graphs\n", + "g_line = ...\n", + "g_circle = ...\n", + "g_square = ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e506aeb8", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 4))\n", + "\n", + "# TODO: print the graphs on the subplots\n", + "# ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eca26aea", + "metadata": {}, + "outputs": [], + "source": [ + "# ✅ Check\n", + "assert g_line.number_of_nodes() == 5\n", + "assert g_circle.number_of_nodes() == 6\n", + "assert g_square.number_of_nodes() == 9\n", + "print(\"All three graphs look right!\")" + ] + }, + { + "cell_type": "markdown", + "id": "a9b0848c", + "metadata": {}, + "source": [ + "## 2. Abstract graphs vs. graphs with coordinates\n", + "\n", + "Not every graph has coordinates. What is required is only their connectivity: edges and nodes. \n", + "A **random Erdős–Rényi graph**, for\n", + "instance, is purely *abstract*: it defines which nodes are connected, but\n", + "says nothing about where the nodes sit in the plane. Since neutral atoms live\n", + "in real space, sooner or later every graph needs coordinates. That is the\n", + "job of *embedding* (Section 4).\n", + "\n", + "Useful properties to interrogate a graph: `has_coords`, `has_edges`,\n", + "`has_node_weights`, `has_edge_weights`." + ] + }, + { + "cell_type": "markdown", + "id": "e3ffa662", + "metadata": {}, + "source": [ + "### ✏️ Exercise 1.2 — An abstract random graph\n", + "\n", + "1. Create `g_er`, an Erdős–Rényi random graph with `n=8` nodes, edge\n", + " probability `p=0.4` and `seed=3` (for reproducibility).\n", + "2. Print whether it has coordinates (`has_coords`) and how many edges it has.\n", + "3. Try to compute `g_er.min_distance()` inside a `try/except Exception` block\n", + " and print the error — distances make no sense without coordinates!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61d92e10", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: create the random ER graph\n", + "g_er = ...\n", + "\n", + "print(\"Has coordinates:\", ...)\n", + "print(\"Number of edges:\", g_er.number_of_edges())\n", + "\n", + "try:\n", + " g_er.min_distance()\n", + "except AttributeError as err:\n", + " print(\"As expected, this fails:\", err)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "724d4d0a", + "metadata": {}, + "outputs": [], + "source": [ + "# ✅ Check\n", + "assert g_er.number_of_nodes() == 8\n", + "assert not g_er.has_coords, \"An ER graph should be abstract (no coordinates)\"\n", + "print(\"Correct — g_er is an abstract graph.\")" + ] + }, + { + "cell_type": "markdown", + "id": "2a2264fd", + "metadata": {}, + "source": [ + "## 3. Embedding: assign coordinates to an abstract graph or interaction matrix\n", + "\n", + "**Embedding** is the process of assigning coordinates to a graph. QoolQit can take an\n", + " abstract graph and return the same graph *with coordinates* or take a symmetric matrix of *desired interactions* and return a graph whose node positions physically realize them (next section).\n" + ] + }, + { + "cell_type": "markdown", + "id": "d7a6df28", + "metadata": {}, + "source": [ + "### ✏️ Exercise 1.4 — Spring-layout embedding of the random graph\n", + "\n", + "1. Import `SpringLayoutEmbedder` from `qoolqit.embedding` and instantiate it.\n", + "2. Embed the abstract graph `g_er` from Exercise 1.2 into `g_er_embedded`.\n", + "3. Verify the embedded graph now has coordinates, print its `min_distance()`,\n", + " and draw it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0314e34", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: instantiate the embedder and embed g_er\n", + "embedder = ...\n", + "g_er_embedded = ...\n", + "\n", + "print(\"Has coordinates:\", g_er_embedded.has_coords)\n", + "print(\"Minimum distance:\", g_er_embedded.min_distance())\n", + "g_er_embedded.draw()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f8d3fcb", + "metadata": {}, + "outputs": [], + "source": [ + "# ✅ Check\n", + "assert g_er_embedded.has_coords, \"The embedded graph should have coordinates\"\n", + "print(\"Spring-layout embedding successful!\")" + ] + }, + { + "cell_type": "markdown", + "id": "de059158", + "metadata": {}, + "source": [ + "### ✏️ Exercise 1.5 — Embed a target interaction matrix\n", + "\n", + "1. Define the symmetric 3×3 target matrix\n", + " `M = [[0, 1, 0.3], [1, 0, 0.5], [0.3, 0.5, 0]]` as a NumPy array.\n", + "2. Instantiate an `InteractionEmbedder` (from `qoolqit.embedding`) and embed\n", + " `M` into `g_int`.\n", + "3. Draw `g_int`, then print its `interactions()` dictionary side by side with\n", + " the corresponding entries of `M`. How close did the embedder get?\n", + "\n", + "> 💡 The `interactions()` method computes $1/r^6$ for each pair of nodes from\n", + "> the coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b2d9b52", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "# TODO: define the target interaction matrix\n", + "M = np.array(...)\n", + "\n", + "# TODO: embed it\n", + "g_int = ...\n", + "\n", + "g_int.draw()\n", + "\n", + "for (i, j), J in g_int.interactions().items():\n", + " print(f\"pair ({i},{j}): J = {J:.4f} target M = {M[i, j]:.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "881fa9d1", + "metadata": {}, + "outputs": [], + "source": [ + "# ✅ Check — every realized interaction within 5% of its target\n", + "for (i, j), J in g_int.interactions().items():\n", + " assert abs(J - M[i, j]) < 0.05, f\"pair ({i},{j}) is off: {J} vs {M[i, j]}\"\n", + "print(\"Interaction embedding matches the target matrix!\")" + ] + }, + { + "cell_type": "markdown", + "id": "f8f2abe6", + "metadata": {}, + "source": [ + "### Next module\n", + "Graphs describe *data* and *geometry*. In **Module 2** we turn geometry into\n", + "physics: the `Register` of atoms, the time-dependent `Drive`, and the\n", + "`QuantumProgram` that combines them." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}