diff --git a/azure-quantum/requirements-pulser.txt b/azure-quantum/requirements-pulser.txt new file mode 100644 index 00000000..01a9065c --- /dev/null +++ b/azure-quantum/requirements-pulser.txt @@ -0,0 +1 @@ +pulser>=0.18,<0.19 \ No newline at end of file diff --git a/samples/hello-world/HW-pasqal-pulser.ipynb b/samples/hello-world/HW-pasqal-pulser.ipynb new file mode 100644 index 00000000..854d8b38 --- /dev/null +++ b/samples/hello-world/HW-pasqal-pulser.ipynb @@ -0,0 +1,319 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 👋🌍 Hello, world: Submit a Pulser job to Pasqal\n", + "\n", + "In this notebook, we'll review the basics of Azure Quantum by submitting a simple *job*, or quantum program, to [Pasqal](https://pasqal.com/). We will use [Pulser](https://pulser.readthedocs.io/) to define a quantum job that can run on Pasqal's hardware." + ], + "id": "8493267a95ed8cbd" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "## Submit a simple job to Pasqal using Azure Quantum\n", + "Azure Quantum provides several ways to express quantum programs. In this example we are using Pulser, the quantum computing library of Pasqal to program arrays of neutral atoms. All code in this example will be written in Python.\n", + "\n", + "Let's begin. When you see a code block, hover over it and click the triangle play-button to execute it. To avoid any compilation issues, this should be done in order from top to bottom." + ], + "id": "808523d9ed21b01b" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": "### 1. Connect to the Azure Quantum workspace\n", + "id": "658fc276abc991eb" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azure.quantum import Workspace\n", + "\n", + "# Your `resource_id` and `location` should be available on the Overview page of your Quantum Workspace.\n", + "\n", + "workspace = Workspace(\n", + " resource_id = \"\",\n", + " location = \"\",\n", + ")" + ], + "id": "4f3047eec84a00af" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": "Let's see whether the Pasqal provider is enabled in this workspace with the following command:\n", + "id": "d3ad6d0abf4de566" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "jupyter": { + "outputs_hidden": false, + "source_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + }, + "outputs": [], + "source": [ + "print(\"This workspace's targets:\")\n", + "for target in workspace.get_targets():\n", + " print(\"-\", target.name)" + ], + "id": "95dbec7c0e4f59df" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "### ❕ Do you see pasqal.sim.emu-tn in your list of targets? If so, you're ready to keep going.\n", + "\n", + "Don't see it? You may need to add Pasqal to your workspace to run this sample. Navigate to the **Providers** page in the portal and click **+Add** to add the Pasqal provider. Don't worry, there's a free credits plan available." + ], + "id": "64dc34808344e811" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "## Pasqal: The quantum provider\n", + "Azure Quantum partners with third-party companies to deliver solutions to quantum jobs. These company offerings are called *providers*. Each provider can offer multiple *targets* with different capabilities. See the table below for Pasqal's targets.\n", + "\n", + "| Target name | Target ID | Number of qubits | Description |\n", + "|-------------|----------------------|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|\n", + "| Emu-TN | `pasqal.sim.emu-tn` | 100 qubits 1D and 2D networks | Simulates the time-evolution of a quantum state using the Schrödinger equation corresponding to the actions that the lasers perform. |\n", + "| Fresnel1 | `pasqal.qpu.fresnel` | 100 qubits | PASQAL's neutral atoms quantum computer. |\n", + "\n", + "For this example, we will use `pasqal.sim.emu-tn`. To learn more about Pasqal's targets, check out our [documentation](https://learn.microsoft.com/azure/quantum/provider-pasqal)." + ], + "id": "32fb070cacaba76d" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Build the quantum program\n", + "\n", + "Let's create a simple Pulser Sequence to run on an array of neutral atoms (a Register)." + ], + "id": "a58addf868b244dd" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "import math\n", + "import pulser\n", + "\n", + "reg = pulser.Register.rectangle(1, 2, spacing=8, prefix=\"atom\")\n", + "reg.draw()\n", + "\n", + "duration = 1000 # Typical: ~1 µsec\n", + "pi_pulse = pulser.Pulse.ConstantDetuning(\n", + " pulser.BlackmanWaveform(duration, math.pi), 0.0, 0.0\n", + ")\n", + "\n", + "seq = pulser.Sequence(reg, pulser.DigitalAnalogDevice)\n", + "\n", + "seq.declare_channel(\"ryd\", \"rydberg_local\", \"atom0\")\n", + "\n", + "seq.add(pi_pulse, \"ryd\")\n", + "seq.target(\"atom1\", \"ryd\")\n", + "seq.add(pi_pulse, \"ryd\")\n", + "seq.measure()\n", + "\n", + "seq.draw()" + ], + "id": "9681bbcd96eee008" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "Each pulse acts on a set of atoms at a certain moment of time. The entire `Sequence` is stored by Pulser and can then be either simulated or sent to a real device.\n", + "\n", + "We've just built a sequence sending the same π-pulse to two atoms, sequentially, using the same channel. With Pasqal's idealized simulator, we will be able to simulate the quantum evolution of both atoms over time." + ], + "id": "c3d23fc53155e243" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### 3. Submit the quantum program to Pasqal", + "id": "95f8578dfb2660f4" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Using the Pasqal simulator target, call \"run\" to submit the job. We'll\n", + "# use 100 repetitions (simulated runs).\n", + "target = workspace.get_targets(name=\"pasqal.sim.emu-tn\")\n", + "job = target.submit(\n", + " input_data=dict(sequence_builder=seq.to_abstract_repr()),\n", + " shots=100,\n", + " input_data_format=\"pasqal.pulser.v1\", \n", + " output_data_format=\"pasqal.pulser-results.v1\",\n", + " name=\"Pasqal sequence\",\n", + ")\n", + "\n", + "# Print the job ID.\n", + "print(\"Job id:\", job.id)" + ], + "id": "ed2e79aac388baa9" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "The job ID can be used to retrieve the results later using the [get_job method](https://learn.microsoft.com/python/azure-quantum/azure.quantum.workspace?#azure-quantum-workspace-get-job) or by viewing it under the **Job management** section of the portal." + ], + "id": "bb4c8c91778444da" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "### 4. Obtain the job results\n", + "Let's await the job execution by calling `job.get_results()`. This may take a minute or so ⏳. Your job is being packaged and sent to Pasqal, where it will wait its turn to be run." + ], + "id": "f2111a90ca2d482a" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Await job results.\n", + "print(\"Awaiting job results...\")\n", + "result = job.get_results()\n", + "\n", + "# To view each measured qubit state, you can print the result.\n", + "print(\"Job finished. Measure count for each state:\")\n", + "print(result)" + ], + "id": "4bf1813d0e966f42" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": "**See the result above? Congratulations, you've submitted a Pasqal job with Azure Quantum! 👏**", + "id": "a13268e76598db7b" + }, + { + "cell_type": "markdown", + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + }, + "source": [ + "### 5. Next steps\n", + "Next, you can try running a program on Pasqal's hardware target. Just replace `pasqal.sim.emu-tn` with `pasqal.qpu.fresnel`. Don't worry - your work here is automatically saved.\n", + "\n", + "To learn more about submitting Pulser sequences to Azure Quantum, review [this documentation](https://learn.microsoft.com/en-us/azure/quantum/quickstart-microsoft-provider-format?tabs=tabid-portal%2Ctabid-pyquil#submit-a-circuit-to-pasqal-using-pulser-sdk).\n", + "\n", + "To learn more about job pricing, review the [Azure Quantum documentation on job costs](https://learn.microsoft.com/en-us/azure/quantum/pricing#pasqal)." + ], + "id": "2e02e1b62a75939e" + } + ], + "metadata": { + "kernel_info": { + "name": "python3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "nteract": { + "version": "nteract-front-end@1.0.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/samples/quantum-approximation-optimization/qaoa-for-quadratic-unconstrained-binary-optimization.ipynb b/samples/quantum-approximation-optimization/qaoa-for-quadratic-unconstrained-binary-optimization.ipynb new file mode 100644 index 00000000..188b149b --- /dev/null +++ b/samples/quantum-approximation-optimization/qaoa-for-quadratic-unconstrained-binary-optimization.ipynb @@ -0,0 +1,791 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# QAOA and QAA to solve a QUBO problem" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Introduction " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this tutorial, we illustrate how to solve a Quadratic Unconstrained Binary Optimization (QUBO) instance using an ensemble of Rydberg atoms in analog mode.\n", + "\n", + "QUBO has been extensively studied [Glover, et al., 2018](https://arxiv.org/pdf/1811.11538.pdf) and is used to model and solve numerous categories of optimization problems including important instances of network flows, scheduling, max-cut, max-clique, vertex cover and other graph and management science problems, integrating them into a unified modeling framework.\n", + "\n", + "Mathematically, a QUBO instance consists of a symmetric matrix $Q$ of size $(N \\times N)$, and the optimization problem associated with it is to find the bitstring $z=(z_1, \\dots, z_N) \\in \\{0, 1 \\}^N$ that minimizes the quantity\n", + "$$f(z) = z^{T}Qz$$ \n", + "\n", + "\n", + "In this tutorial, we will demonstrate how a QUBO instance can be mapped and solved using neutral atoms." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from pulser import Pulse, Sequence, Register\n", + "from pulser_simulation import QutipEmulator\n", + "from pulser.devices import DigitalAnalogDevice\n", + "from pulser.waveforms import InterpolatedWaveform\n", + "from scipy.optimize import minimize\n", + "from scipy.spatial.distance import pdist, squareform" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Suppose we are given the following QUBO matrix $Q$:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "Q = np.array(\n", + " [\n", + " [-10.0, 19.7365809, 19.7365809, 5.42015853, 5.42015853],\n", + " [19.7365809, -10.0, 20.67626392, 0.17675796, 0.85604541],\n", + " [19.7365809, 20.67626392, -10.0, 0.85604541, 0.17675796],\n", + " [5.42015853, 0.17675796, 0.85604541, -10.0, 0.32306662],\n", + " [5.42015853, 0.85604541, 0.17675796, 0.32306662, -10.0],\n", + " ]\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because the QUBO is small, we can classically check all solutions and mark the optimal ones. This will help us later in the tutorial to visualize the quality of our quantum approach." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "bitstrings = [np.binary_repr(i, len(Q)) for i in range(2 ** len(Q))]\n", + "costs = []\n", + "# this takes exponential time with the dimension of the QUBO\n", + "for b in bitstrings:\n", + " z = np.array(list(b), dtype=int)\n", + " cost_qutip_emulator = z.T @ Q @ z\n", + " costs.append(cost_qutip_emulator)\n", + "zipped = zip(bitstrings, costs)\n", + "sort_zipped = sorted(zipped, key=lambda x: x[1])\n", + "print(sort_zipped[:3])" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This QUBO admits `01011` and `00111` as optimal solutions." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Embedding a QUBO onto an atomic register" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now illustrate how to use Pulser to embbed the QUBO matrix $Q$ on a neutral-atom device.\n", + "\n", + "The key idea is to encode the off-diagonal terms of $Q$ by using the Rydberg interaction between atoms. As the interaction $U$ depends on the pairwise distance ($U=C_6/r_{ij}^6$) between atoms $i$ and $j$, we attempt to find the optimal positions of the atoms in the Register that replicate best the off-diagonal terms of $Q$:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def evaluate_mapping(new_coords, *args):\n", + " \"\"\"Cost function to minimize. Ideally, the pairwise\n", + " distances are conserved\"\"\"\n", + " Q, shape = args\n", + " new_coords = np.reshape(new_coords, shape)\n", + " new_Q = squareform(DigitalAnalogDevice.interaction_coeff / pdist(new_coords) ** 6)\n", + " return np.linalg.norm(new_Q - Q)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "shape = (len(Q), 2)\n", + "costs = []\n", + "np.random.seed(0)\n", + "x0 = np.random.random(shape).flatten()\n", + "res = minimize(\n", + " evaluate_mapping,\n", + " x0,\n", + " args=(Q, shape),\n", + " method=\"Nelder-Mead\",\n", + " tol=1e-6,\n", + " options={\"maxiter\": 200000, \"maxfev\": None},\n", + ")\n", + "coords = np.reshape(res.x, (len(Q), 2))" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then plot the obtained coordinates in a Register using:" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "qubits = {str(i): c for i, c in enumerate(coords)}\n", + "reg = Register(qubits)\n", + "reg.draw(\n", + " blockade_radius=DigitalAnalogDevice.rydberg_blockade_radius(1.0),\n", + " draw_graph=False,\n", + " draw_half_radius=True,\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Building the quantum algorithm " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that the QUBO $Q$ is encoded in the Register, we can peprare the following Ising Hamiltonian $H_Q$:\n", + "\n", + "$$ H_Q= \\sum_{i=1}^N \\frac{\\hbar\\Omega}{2} \\sigma_i^x - \\sum_{i=1}^N \\frac{\\hbar \\delta}{2} \\sigma_i^z+\\sum_{j \\lt i}\\frac{C_6}{|\\textbf{r}_i-\\textbf{r}_j|^{6}} n_i n_j. $$\n", + "\n", + "In the case where our mapping of the atoms is perfect, the last sum replicates exactly the off-diagonal terms of $Q$. In that case, the next step is to prepare the ground-state of $H_Q$ to output the optimal bitstrings.\n", + "\n", + "To do so we present two different approaches, namely the Quantum Approximation Optimization Algorithm (QAOA) and the Quantum Adiabatic Algorithm (QAA) that have been introduced to prepare ground-states of Hamiltonians." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### QAOA" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This algorithm (see [Farhi, et al., 2014](https://arxiv.org/pdf/1411.4028.pdf)) has gained a lot of traction lately as a gate-based quantum algorithm. It has shown promising results in a number of applications and yields decent results for low-depth circuits.\n", + "\n", + "All atoms are initially in the groundstate $|00\\dots0\\rangle$ of the `ground-rydberg` basis. We then apply $p$ layers of alternating non-commutative Hamiltonians. The first one, called the mixing Hamiltonian $H_M$, is realized by taking $\\Omega = 1$ rad/µs, and $\\delta = 0$ rad/µs in the Hamiltonian equation. The second Hamiltonian $H_Q$ is realized with $\\Omega =0$ rad/µs and $\\delta = 1.$ rad/µs. $H_M$ and $H_Q$ are applied turn in turn with parameters $\\tau$ and $t$ respectively. A classical optimizer is then used to estimate the optimal parameters. \n", + "\n", + "Instead of creating a new `Sequence` everytime the quantum loop is called, we are going to create a parametrized `Sequence` and give that to the quantum loop." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "LAYERS = 2\n", + "\n", + "# Parametrized sequence\n", + "seq = Sequence(reg, DigitalAnalogDevice)\n", + "seq.declare_channel(\"ch0\", \"rydberg_global\")\n", + "\n", + "t_list = seq.declare_variable(\"t_list\", size=LAYERS)\n", + "s_list = seq.declare_variable(\"s_list\", size=LAYERS)\n", + "\n", + "for t, s in zip(t_list, s_list):\n", + " pulse_1 = Pulse.ConstantPulse(1000 * t, 1.0, 0.0, 0)\n", + " pulse_2 = Pulse.ConstantPulse(1000 * s, 0.0, 1.0, 0)\n", + "\n", + " seq.add(pulse_1, \"ch0\")\n", + " seq.add(pulse_2, \"ch0\")\n", + "\n", + "seq.measure(\"ground-rydberg\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once we have the parameters that we want to apply, we use the `.build()` method to assign these values into a `assigned_seq` sequence. It is this sequence which is simulated every time the quantum loop is called." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Experimentally, we don't have access to the state vector $|\\psi\\rangle$. We therefore make it more realistic by taking samples from the state vector that results from running the simulation with `simul.run()`. This is done with the built-in method `results.sample_final_state()`, in which we add the measurement basis which was declared at the end of the sequence, and the number of samples desired. Currently, the repetition rate of the machine is $5$ Hz." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def quantum_loop(parameters):\n", + " params = np.array(parameters)\n", + " t_params, s_params = np.reshape(params.astype(int), (2, LAYERS))\n", + " assigned_seq = seq.build(t_list=t_params, s_list=s_params)\n", + " simul = QutipEmulator.from_sequence(assigned_seq, sampling_rate=0.01)\n", + " results = simul.run()\n", + " count_dict = results.sample_final_state() # sample from the state vector\n", + " return count_dict" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "np.random.seed(123) # ensures reproducibility of the tutorial\n", + "guess = {\n", + " \"t\": np.random.uniform(8, 10, LAYERS),\n", + " \"s\": np.random.uniform(1, 3, LAYERS),\n", + "}" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "example_dict = quantum_loop(np.r_[guess[\"t\"], guess[\"s\"]])" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can then plot the distribution of the samples, to see the most frequent bitstrings sampled." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def plot_distribution(C):\n", + " C = dict(sorted(C.items(), key=lambda item: item[1], reverse=True))\n", + " indexes = [\"01011\", \"00111\"] # QUBO solutions\n", + " color_dict = {key: \"r\" if key in indexes else \"g\" for key in C}\n", + " plt.figure(figsize=(12, 6))\n", + " plt.xlabel(\"bitstrings\")\n", + " plt.ylabel(\"counts\")\n", + " plt.bar(C.keys(), C.values(), width=0.5, color=color_dict.values())\n", + " plt.xticks(rotation=\"vertical\")\n", + " plt.show()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "plot_distribution(example_dict)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The bitstrings `01011` and `00111` (in red) correspond to the two optimal solutions (calculated at the beginning of the notebook). The goal of QAOA is to choregraph interferences between the basis states, in order to maximize the frequency of the optimal solution states. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Optimization " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We estimate the cost of a sampled state vector by making an average over the samples. This is done by taking the corresponding bitstring ${\\bf z}=(z_1, \\ldots, z_N)$ and calculating\n", + "\n", + "$$\n", + "C({\\bf z}) = {\\bf z}^\\top \\cdot Q \\cdot {\\bf z}\n", + "$$\n", + "\n", + "Determining the cost of a given bitstring takes polynomial time. The average estimate is then used in the classical loop to optimize the variational parameters $\\tau$ and $t$." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def get_cost_colouring(bitstring, Q):\n", + " z = np.array(list(bitstring), dtype=int)\n", + " cost = z.T @ Q @ z\n", + " return cost\n", + "\n", + "\n", + "def get_cost(counter, Q):\n", + " cost = sum(counter[key] * get_cost_colouring(key, Q) for key in counter)\n", + " return cost / sum(counter.values()) # Divide by total samples" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To perform a minimization loop, we define the following function that will be called at each step by SciPy. `*args` enables to pass the QUBO value, and `params` contains the trial value to score, which changes at each step." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def func(param, *args):\n", + " Q = args[0]\n", + " C = quantum_loop(param)\n", + " cost = get_cost(C, Q)\n", + " return cost" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### QAOA for depth $p = 2$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now use a classical optimizer `minimize` in order to find the best variational parameters. This function takes as arguments `func`, the QUBO $Q$ and an initial point `x0` for the simplex in Nelder-Mead minimization. As the optimizer might get trapped in local minima, we repeat the optimization 20 times and select the parameters that yield the best approximation ratio.\n", + "\n", + "Note: the next cell may take up to 2 minutes to complete." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "scores = []\n", + "params = []\n", + "for repetition in range(20):\n", + " guess = {\n", + " \"t\": np.random.uniform(1, 10, LAYERS),\n", + " \"s\": np.random.uniform(1, 10, LAYERS),\n", + " }\n", + "\n", + " try:\n", + " res = minimize(\n", + " func,\n", + " args=Q,\n", + " x0=np.r_[guess[\"t\"], guess[\"s\"]],\n", + " method=\"Nelder-Mead\",\n", + " tol=1e-5,\n", + " options={\"maxiter\": 10},\n", + " )\n", + " scores.append(res.fun)\n", + " params.append(res.x)\n", + " except Exception as e:\n", + " pass" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now plot the sample that we woud obtain using the optimal variational parameters." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "optimal_count_dict = quantum_loop(params[np.argmin(scores)])\n", + "plot_distribution(optimal_count_dict)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "QAOA is capable of finding good variational parameters $\\tau$ and $t$. Now, sampling from this final state $|\\psi(t_{f})\\rangle$ will return both optimal strings with high probability." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, using QAOA to solve the problem is not the best idea; it's difficult to yield a >90% quality solution without going to high depths of the QAOA, implying that the growing closed-loop optimization can rapidly become expensive, with no guarantee of convergence. ", + "We therefore propose to investigate another approach called the Quantum Adiabatic Algorithm (QAA). ", + "This method is exclusively analog and can be well implemented on Pasqal's Fresnel machines that are tailored to solve analog quantum jobs. ", + "It is fast, reliable and shows optimal convergence to the solution." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quantum Adiabatic Algorithm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The idea behind the adiabatic algorithm (see [Albash, Lidar, 2018](https://arxiv.org/pdf/1611.04471.pdf)) is to slowly evolve the system from an easy-to-prepare groundstate to the groundstate of $H_Q$. If done slowly enough, the system of atoms stays in the instantaneous ground-state.\n", + "\n", + "In our case, we continuously vary the parameters $\\Omega(t), \\delta(t)$ in time, starting with $\\Omega(0)=0, \\delta(0)<0$ and ending with $\\Omega(0)=0, \\delta>0$. The ground-state of $H(0)$ corresponds to the initial state $|00000\\rangle$ and the ground-state of $H(t_f)$ corresponds to the ground-state of $H_Q$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "The Rydberg blockade radius is directly linked to the Rabi frequency $\\Omega$ and is obtained using `DigitalAnalogDevice.rydberg_blockade_radius()`. In this notebook, $\\Omega$ is initially fixed to a frequency of 1 rad/µs. We can therefore build the adjacency matrix $A$ of $G$ in the following way:" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To ensure that we are not exciting the system to states that are too excited, we keep $\\Omega \\in [0, \\Omega_{\\text{max}}]$, and choose $\\Omega_{\\text{max}}$ as the median of the values of Q to ensures that the adiabatic path is efficient." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# We choose a median value between the min and the max\n", + "Omega = np.median(Q[Q > 0].flatten())\n", + "delta_0 = -5 # just has to be negative\n", + "delta_f = -delta_0 # just has to be positive\n", + "T = 4000 # time in ns, we choose a time long enough to ensure the propagation of information in the system" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "adiabatic_pulse = Pulse(\n", + " InterpolatedWaveform(T, [1e-9, Omega, 1e-9]),\n", + " InterpolatedWaveform(T, [delta_0, 0, delta_f]),\n", + " 0,\n", + ")\n", + "seq = Sequence(reg, DigitalAnalogDevice)\n", + "seq.declare_channel(\"ising\", \"rydberg_global\")\n", + "seq.add(adiabatic_pulse, \"ising\")\n", + "seq.draw()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "simul = QutipEmulator.from_sequence(seq)\n", + "results = simul.run()\n", + "final = results.get_final_state()\n", + "count_dict = results.sample_final_state()\n", + "print(results.sample_final_state())" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "plot_distribution(count_dict)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "See how fast and performant this method is! In only a few micro-seconds, we find an excellent solution." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### How does the time evolution affect the quality of the results?" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "cost_qutip_emulator = []\n", + "for T in 1000 * np.linspace(1, 10, 10):\n", + " seq = Sequence(reg, DigitalAnalogDevice)\n", + " seq.declare_channel(\"ising\", \"rydberg_global\")\n", + " adiabatic_pulse = Pulse(\n", + " InterpolatedWaveform(T, [1e-9, Omega, 1e-9]),\n", + " InterpolatedWaveform(T, [delta_0, 0, delta_f]),\n", + " 0,\n", + " )\n", + " seq.add(adiabatic_pulse, \"ising\")\n", + " simul = QutipEmulator.from_sequence(seq)\n", + " results = simul.run()\n", + " final = results.get_final_state()\n", + " count_dict = results.sample_final_state()\n", + " cost_qutip_emulator.append(get_cost(count_dict, Q) / 3)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def plot_cost(cost, title):\n", + " plt.figure(figsize=(12, 6))\n", + " plt.plot(range(1, 11), np.array(cost), \"--o\")\n", + " plt.xlabel(\"total time evolution (µs)\", fontsize=14)\n", + " plt.ylabel(\"cost\", fontsize=14)\n", + " plt.title(title, fontsize=16)\n", + " plt.show()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "plot_cost(cost_qutip_emulator, \"Qutip emulator\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## 4. Now let's run against Pasqal's emulator" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import json\n", + "\n", + "def prepare_input_data(seq):\n", + " input_data = {}\n", + " input_data[\"sequence_builder\"] = json.loads(seq.to_abstract_repr())\n", + " return json.dumps(input_data)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def submit_job(target, seq):\n", + " job = target.submit(\n", + " input_data=prepare_input_data(seq), \n", + " input_data_format=\"pasqal.pulser.v1\", \n", + " output_data_format=\"pasqal.pulser-results.v1\",\n", + " name=\"Pasqal sequence\",\n", + " shots=100,\n", + " )\n", + "\n", + " print(f\"Queued job: {job.id}\")\n", + " job.wait_until_completed()\n", + " print(f\"Job completed with state: {job.details.status}\")\n", + " result = job.get_results()\n", + "\n", + " return result" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Please set your `resource_id` and `location` below." + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from azure.quantum import Workspace\n", + "workspace = Workspace(\n", + " resource_id = \"\",\n", + " location = \"\",\n", + ")\n", + "target = workspace.get_targets(name=\"pasqal.sim.emu-tn\")\n", + "\n", + "if isinstance(target, list):\n", + " if len(target) == 0:\n", + " raise RuntimeError(\"No emulators found for that name\")\n", + " raise RuntimeError(target)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Note: the next cell may take up to 5 minutes to complete." + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Open a session on the backend to group all jobs together\n", + "from azure.quantum.job.session import SessionJobFailurePolicy\n", + "\n", + "with target.open_session(name=\"QUBO Problem\", job_failure_policy=SessionJobFailurePolicy.CONTINUE) as session:\n", + " # Compute the time evolution on PASQAL emulator\n", + " cost_emutn = []\n", + " try:\n", + " for T in 1000 * np.linspace(1, 10, 10):\n", + " seq = Sequence(reg, DigitalAnalogDevice)\n", + " seq.declare_channel(\"ising\", \"rydberg_global\")\n", + " adiabatic_pulse = Pulse(\n", + " InterpolatedWaveform(T, [1e-9, Omega, 1e-9]),\n", + " InterpolatedWaveform(T, [delta_0, 0, delta_f]),\n", + " 0,\n", + " )\n", + " seq.add(adiabatic_pulse, \"ising\")\n", + " \n", + " from collections import Counter\n", + " histogram_results = submit_job(target, seq)\n", + " count_dict = Counter(histogram_results)\n", + " \n", + " cost_emutn.append(get_cost(count_dict, Q) / 3)\n", + " \n", + " except Exception as e:\n", + " print(e)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "plot_cost(cost_emutn, \"Pasqal Tensor Network-based emulator\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "#### Finally, let's compare the results of the two emulators" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def plot_costs(title, costs):\n", + " plt.figure(figsize=(12, 6))\n", + " for label, cost in costs.items():\n", + " plt.plot(range(1, 11), np.array(cost), \"--o\", label=label)\n", + " plt.xlabel(\"total time evolution (µs)\", fontsize=14)\n", + " plt.ylabel(\"cost\", fontsize=14)\n", + " plt.title(title, fontsize=16)\n", + " plt.legend()\n", + " plt.show()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "costs = {\n", + " 'Qutip emulator': cost_qutip_emulator,\n", + " 'Pasqal emulator': cost_emutn\n", + "}\n", + "plot_costs(\"Qutip emulator vs Pasqal tensor network-based emulator\", costs)" + ], + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "celltoolbar": "Tags", + "interpreter": { + "hash": "949777d72b0d2535278d3dc13498b2535136f6dfe0678499012e853ee9abcab1" + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + }, + "vscode": { + "interpreter": { + "hash": "e088768f7ff7b4294439f8ed10f7eed9e3b885124bc20d9d06cc2a37b1883330" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}