From 33c594245917d44731623138ba1714fe54ede8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 12 Feb 2024 14:54:19 +0100 Subject: [PATCH] Raw content dump for steps 2 and 3. The code has not been tested yet. --- how_tos/qiskit_patterns.ipynb | 349 +++++++++++++++++++++++++++++++++- 1 file changed, 339 insertions(+), 10 deletions(-) diff --git a/how_tos/qiskit_patterns.ipynb b/how_tos/qiskit_patterns.ipynb index 83b2648..23521f4 100644 --- a/how_tos/qiskit_patterns.ipynb +++ b/how_tos/qiskit_patterns.ipynb @@ -14,8 +14,12 @@ "3. Execute using Qiskit Runtime primitives\n", "4. Post-process, return result in classical format\n", "\n", - "In Step 1. we will take a combinatorial optimization problem and formulate it in terms of finding the ground state of an Ising Hamiltonian. This reformulated prblem can be understood by a quantum computer. In Step 2. we will prepare the quantum circuits to execute on the quantum computer.\n", - "In Step 3. we will call the `Sampler` primitive in Qiskit to draw samples from the quantum circuits that Step 2. prepared. Finally, under Step 4. we convert the samples from Step 3. into the solution of our combinatorial optimization problem. " + "We will apply the patterns to the context of **combinatorial optimization** and show how to solve a problem using the **Quantum Approximate Optimization Algorithm (QAOA)**, a hybrid (quantum-classical) iterative method. \n", + "\n", + "- In Step 1. we will take a combinatorial optimization problem and formulate it in terms of finding the ground state of an Ising Hamiltonian. This reformulated problem can be understood by a quantum computer.\n", + "- In Step 2. we will prepare the quantum circuits to execute on the quantum computer.\n", + "- In Step 3. we will iterativelly call the `Sampler` primitive in Qiskit to draw samples from the quantum circuits that Step 2. prepared., and use those samples in the loss function of our algorithmic routine\n", + "- Finally, under Step 4. we convert the samples from Step 3. into the solution of our combinatorial optimization problem. " ] }, { @@ -36,26 +40,35 @@ "\\begin{align}\n", "\\min_{x\\in \\{0, 1\\}^n}x^T Q x,\n", "\\end{align}\n", + "\n", "where $Q$ is a $n\\times n$ matrix of real numbers. To start, we will convert the binary variables $x_i$ to variables $z_i\\in\\{-1, 1\\}$ by doing\n", + "\n", "\\begin{align}\n", "x_i = \\frac{1-z_i}{2}.\n", "\\end{align}\n", + "\n", "Here, for example, we see that if $x_i$ is $0$ then $z_i$ is $1$. When we substitute the $x_i$'s for the $z_i$'s in the QUBO above, we obtain the equivalent formulations for our optimization task\n", + "\n", "\\begin{align}\n", "\\min_{x\\in\\{0,1\\}^n} x^TQx\\Longleftrightarrow \\min_{z\\in\\{-1,1\\}^n}z^TQz + b^Tz\n", "\\end{align}\n", + "\n", "The details of the computation are shown in Appendix A below. Here, $b$ depends on $Q$. Note that to obtain $z^TQz + b^Tz$ we dropped an irrelevant factor of 1/4 and a constant offset of $n^2$ which do not play a role in the optimization. Now, to obtain a quantum formulation of the problem we promot the $z_i$ variables to a Pauli $Z$ matrix, i.e., a $2\\times 2$ matrix of the form\n", + "\n", "\\begin{align}\n", "Z_i = \\begin{pmatrix}1 & 0 \\\\ 0 & -1\\end{pmatrix}.\n", "\\end{align}\n", + "\n", "When we substitute these matrices in the QUBO above we obtain the following Hamiltonian\n", + "\n", "\\begin{align}\n", "H_C=\\sum_{ij}Q_{ij}Z_iZ_j + \\sum_i b_iZ_i.\n", "\\end{align}\n", - "We refer to this Hamiltonian as the cost function Hamiltonian. It has the property that its gound state corresponds to the solution that minimizes the cost function $f(x)$.\n", - "Therefore, to solve out optimization problem we now need to prepare the ground state of $H_C$ (or a state with a high overlap with it) on the quantum computer. Then, sampling from this state will, with a high probability, yield the solution to $min f(x)$.\n", "\n", - "**TODO** Load a file with the LP and convert to an Ising Hamiltonian to feed into step 2." + "We refer to this Hamiltonian as the **cost function Hamiltonian**. It has the property that its gound state corresponds to the solution that **minimizes the cost function $f(x)$**.\n", + "Therefore, to solve our optimization problem we now need to prepare the ground state of $H_C$ (or a state with a high overlap with it) on the quantum computer. Then, sampling from this state will, with a high probability, yield the solution to $min f(x)$.\n", + "\n", + "**TODO** Load a file with the LP and convert to an Ising Hamiltonian to feed into step 2:" ] }, { @@ -65,7 +78,216 @@ "source": [ "## 2. Optimize problem for quantum execution\n", "\n", - "**TODO** Take the Ising Hamiltonian from Step 1. and make circuits." + "(*In Step 2. we will prepare the quantum circuits to execute on the quantum computer.*)" + ] + }, + { + "cell_type": "markdown", + "id": "239f361f", + "metadata": {}, + "source": [ + "The Hamiltonian obtained from step 1 contains the quantum definition of our problem. To run QAOA, we need to convert this definition into a parametric circuit form. This will be the reference state for our iterative quantum execution. Step 2 of the Qiskit Patterns focuses on tuning the circuit for optimal quantum execution through **transpilation**. The Qiskit library offers a series of **Transpilation Passes** that cater to a wide range of circuit transformations. We don't only want to get a circuit, but we want to make sure that the circuit is **optimized** for our purpose. In the context of QAOA, we care about matching the qubit configuration of the quantum chip to the graph defined by the cost function Hamiltonian. By manually tuning this configuration, we can ensure that the final layout will not involve unnecessary operations that would increase the noise level of our samples." + ] + }, + { + "cell_type": "markdown", + "id": "293450a9", + "metadata": {}, + "source": [ + "**TODO**: Take the Ising Hamiltonian from Step 1. We are loading a dummy Hamiltonian in the meantime" + ] + }, + { + "cell_type": "markdown", + "id": "c8e7ec51", + "metadata": {}, + "source": [ + "The QAOA ansatz (circuit) is composed of a cost operator and a mixer operator applied in an alternating fashion to an initial state. The cost operator corresponds to the **cost Hamiltonian** defined in step 1, and is expressed as a weighted sum of pauli terms." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca933c52", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "import networkx as nx\n", + "from qopt_best_practices.utils import build_max_cut_graph, build_max_cut_paulis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca4744e3", + "metadata": {}, + "outputs": [], + "source": [ + "# get dummy Hamiltonian\n", + "graph_file = \"data/graph_2layers_0seed.json\"\n", + "\n", + "with open(graph_file, \"r\") as file:\n", + " data = json.load(file)\n", + "\n", + "dummy_graph = nx.from_edgelist(data[\"Original graph\"])\n", + "dummy_hamiltonian = build_max_cut_paulis(dummy_graph)\n", + "\n", + "# inject dummy Hamiltonian into workflow\n", + "graph = dummy_graph\n", + "hamiltonian = dummy_hamiltonian\n", + "print(hamiltonian)" + ] + }, + { + "cell_type": "markdown", + "id": "27670563", + "metadata": {}, + "source": [ + "### 2.1. Define SWAP strategy\n", + "\n", + "- What is a swap strategy\n", + "- Why is it relevant\n", + "- How do we apply it -> transpiler pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ea7c0fc", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler.passes.routing.commuting_2q_gate_routing import SwapStrategy\n", + "\n", + "swap_strategy = SwapStrategy.from_line([i for i in range(num_qubits)])" + ] + }, + { + "cell_type": "markdown", + "id": "e2e7784f", + "metadata": {}, + "source": [ + "### 2.2 Build QAOA circuit applying previously defined SWAP strategy\n", + "\n", + "IDEA: modify `create_qaoa_swap_circuit` to accept pass manager as input instead of constructing pass manager under the hood" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81ff677a", + "metadata": {}, + "outputs": [], + "source": [ + "from qopt_best_practices.swap_strategies import create_qaoa_swap_circuit\n", + "\n", + "# we define the edge_coloring map so that RZZGates are positioned next to SWAP gates to exploit CX cancellations\n", + "edge_coloring = {(idx, idx + 1): (idx + 1) % 2 for idx in range(qaoa_hamiltonian.num_qubits)}\n", + "\n", + "qaoa_circ = create_qaoa_swap_circuit(qaoa_hamiltonian, swap_strategy, edge_coloring, qaoa_layers=2)\n", + "\n", + "qaoa_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "dbb69c86", + "metadata": {}, + "source": [ + "### 2.3 Define layout from backend" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f5c7270", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService(channel='ibm_quantum')\n", + "backend = service.get_backend('ibm_sherbrooke')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a166725", + "metadata": {}, + "outputs": [], + "source": [ + "from qopt_best_practices.qubit_selection import BackendEvaluator\n", + "\n", + "path_finder = BackendEvaluator(backend)\n", + "\n", + "# the Backend Evaluator accepts custom subset definitions and metrics,\n", + "# but defaults to finding the line with the best fidelity\n", + "path, fidelity, num_subsets = path_finder.evaluate(num_qubits)\n", + "\n", + "print(\"Best path: \", path)\n", + "print(\"Best path fidelity\", fidelity)\n", + "print(\"Num. evaluated paths\", num_subsets)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "835116b1", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler import Layout\n", + "\n", + "initial_layout = Layout.from_intlist(path, qaoa_circ.qregs[0])" + ] + }, + { + "cell_type": "markdown", + "id": "f9a4d236", + "metadata": {}, + "source": [ + "### 2.4 Apply layout and other transpiler passes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abcb2c3d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler import CouplingMap, PassManager\n", + "from qiskit.transpiler.passes import (\n", + " FullAncillaAllocation,\n", + " EnlargeWithAncilla,\n", + " ApplyLayout,\n", + " SetLayout,\n", + ")\n", + "\n", + "from qiskit import transpile\n", + "\n", + "basis_gates = [\"rz\", \"sx\", \"x\", \"cx\"]\n", + "\n", + "backend_cmap = CouplingMap(backend.configuration().coupling_map)\n", + "\n", + "pass_manager_post = PassManager(\n", + " [\n", + " SetLayout(initial_layout),\n", + " FullAncillaAllocation(backend_cmap),\n", + " EnlargeWithAncilla(),\n", + " ApplyLayout(),\n", + " ]\n", + ")\n", + "\n", + "# Map to initial_layout and finally enlarge with ancilla.\n", + "qaoa_circ = pass_manager_post.run(qaoa_circ)\n", + "\n", + "# Now transpile to sx, rz, x, cx basis\n", + "qaoa_circ = transpile(qaoa_circ, basis_gates=basis_gates)\n", + "qaoa_circ.draw(\"mpl\", idle_wires=False)" ] }, { @@ -73,9 +295,116 @@ "id": "af6e4202-b3b7-4878-8e94-5b2a6de4db93", "metadata": {}, "source": [ - "## 3. Execute using Qiskit Runtime primitives\n", + "## 3. Execute using Qiskit Runtime primitives" + ] + }, + { + "cell_type": "markdown", + "id": "e1bcf48c", + "metadata": {}, + "source": [ + "### 3.1 Define Sampler primitive" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4104e6d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime import Sampler, Options\n", + "\n", + "options = Options()\n", + "options.transpiler.skip_transpilation = True\n", + "\n", + "sampler = Sampler(backend=backend, options=options)" + ] + }, + { + "cell_type": "markdown", + "id": "a578868f", + "metadata": {}, + "source": [ + "### 3.2 Define cost function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "104fd21b", + "metadata": {}, + "outputs": [], + "source": [ + "from qopt_best_practices.cost_function import evaluate_sparse_pauli\n", + "\n", + "\n", + "def cost_func_sampler(params, ansatz, hamiltonian, sampler):\n", + "\n", + " job = sampler.run(ansatz, params)\n", + " sampler_result = job.result()\n", + " sampled = sampler_result.quasi_dists[0]\n", + "\n", + " # a dictionary containing: {state: (measurement probability, value)}\n", + " evaluated = {\n", + " state: (probability, evaluate_sparse_pauli(state, hamiltonian))\n", + " for state, probability in sampled.items()\n", + " }\n", + "\n", + " result = sum(probability * value for probability, value in evaluated.values())\n", + "\n", + " return result" + ] + }, + { + "cell_type": "markdown", + "id": "f07cd144", + "metadata": {}, + "source": [ + "### 3.3 Define initial point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9fe1cae", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "# TQA initialization parameters\n", + "dt = 0.75\n", + "p = 2 # 2 qaoa layers\n", + "grid = np.arange(1, p + 1) - 0.5\n", + "init_params = np.concatenate((1 - grid * dt / p, grid * dt / p))\n", + "print(init_params)" + ] + }, + { + "cell_type": "markdown", + "id": "d2c6e7e3", + "metadata": {}, + "source": [ + "### 3.4 Run optimization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39da8ddc", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.optimize import minimize\n", "\n", - "**TODO** Call the sampler on the circuits form Step 2." + "result = minimize(\n", + " cost_func_sampler,\n", + " init_params,\n", + " args=(qaoa_circ, qaoa_hamiltonian, sampler),\n", + " method=\"COBYLA\",\n", + ")\n", + "print(result)\n" ] }, { @@ -134,7 +463,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -148,7 +477,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" + "version": "3.10.2" } }, "nbformat": 4,