{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Quantum Software (with PyZX!): Part 2\n", "\n", "_Aleks Kissinger | 2023_\n", "\n", "Welcome to the second in a series of practicals which will build up from basics to using the ZX calculus to do some interesting and potentially useful stuff on a real quantum computer.\n", "\n", "This problem sheet is designed to go with the [Quantum Software](https://www.cs.ox.ac.uk/teaching/courses/2022-2023/qsoft/) course taught in Oxford, but if you came across it another way, you are more than welcome to give it a go! In part 1, we saw how to:\n", "1. construct quantum circuits and ZX-diagrams programmatically\n", "2. evaluate/simulate them numerically\n", "3. do basic diagrammatic reasoning with software\n", "\n", "In this part, we will use some of the techniques from part 1 to do _automated_ diagrammatic reasoning and circuit optimisation with the ZX-calculus. We will also be translating the circuits that come out into a format that is useable by IBM's [qiskit](https://qiskit.org/) library, and running those circuits on a real quantum computer!\n", "\n", "_These problem sheets, like PyZX itself are released under the [Apache 2](https://github.com/Quantomatic/pyzx/blob/master/LICENSE) open-source license. Feel free to use, copy, and modify them at will, e.g. to use your own course._" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "%pip install \"pyzx @ git+https://github.com/Quantomatic/pyzx.git\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys, os, math, random\n", "from fractions import Fraction\n", "\n", "import pyzx as zx\n", "from pyzx import print_matrix\n", "from pyzx.basicrules import *\n", "\n", "from qiskit.providers.fake_provider import FakeAthens\n", "from qiskit import QuantumCircuit, Aer, IBMQ, execute\n", "from qiskit.compiler import assemble\n", "from qiskit.tools.monitor import job_monitor\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Z = zx.VertexType.Z\n", "X = zx.VertexType.X\n", "B = zx.VertexType.BOUNDARY\n", "SE = zx.EdgeType.SIMPLE\n", "HE = zx.EdgeType.HADAMARD" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the next few questions, we will use to rules from part 1 to do automatic optimisation of quantum circuits. To provide a baseline for comparison, we will start by writing a naive circuit optimiser, which performs basic gate cancellations.\n", "\n", "Here's a bit of a template with some comments to get us started:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def circuit_optimizer(circ):\n", " c = circ.copy()\n", " \n", " # keep looping until we return a circuit\n", " while True:\n", " new_gates = []\n", " \n", " # search through the gates in circuit c in order\n", " for g in c.gates:\n", " \n", " # if current and last gate are both Z-phase gates...\n", " if (len(new_gates) > 0 and\n", " isinstance(new_gates[-1], zx.gates.ZPhase) and\n", " isinstance(g, zx.gates.ZPhase) and\n", " new_gates[-1].target == g.target):\n", " \n", " # ...combine the phases into a single phase gate\n", " g1 = zx.gates.ZPhase(g.target, new_gates[-1].phase + g.phase)\n", " new_gates[-1] = g1\n", " \n", " # otherwise just save the current gate\n", " else:\n", " new_gates.append(g)\n", " \n", " # if we actually got a smaller circuit...\n", " if len(new_gates) < len(c.gates):\n", " c.gates = new_gates # ...save it and continue\n", " else:\n", " return c # ...otherwise return the circuit" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This code will combine the phases in adjacent `ZPhase` gates (i.e. gates produced by `s`, `t` or `rz(.)` in QASM).\n", "\n", "Lets see it in action:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "demo_circ = zx.qasm(\"\"\"\n", "qreg q[3];\n", "\n", "t q[0];\n", "t q[0];\n", "t q[0];\n", "cx q[0], q[1];\n", "cx q[1], q[2];\n", "cx q[1], q[2];\n", "cx q[0], q[1];\n", "t q[2];\n", "\"\"\")\n", "demo_opt = circuit_optimizer(demo_circ)\n", "\n", "print(\"original:\")\n", "zx.draw(demo_circ)\n", "print(\"optimised:\")\n", "zx.draw(demo_opt)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This works okay, but it could be much better." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.1\n", "\n", "Make a new function `circuit_optimizer2` that improves on the one above by:\n", " 1. cancelling adjacent pairs of CNOT and Hadamard gates\n", " 2. adding together the phases in adjacent `XPhase` gates (`rx(.)` gates in QASM)\n", " 3. removing X or Z phase gates with a phase of `0`\n", "\n", "Apply your function to a test circuit and check that it preserves the overall unitary using `zx.compare_tensors`.\n", "\n", "Note: By \"adjacent gates\", we mean gates that are next to each other in the `gates` list with the same `target` property (in the case of Hadamard gates) and the same `control` and `target` properties (for CNOT gates) .\n", "\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.2\n", "\n", "Use `zx.generate.CNOT_HAD_PHASE_circuit` (see [Documentation](https://pyzx.readthedocs.io/en/latest/api.html#generating-circuits)) to generate 100 random circuits with 4, 5, and 6 qubits and gate depth 20. Test your `circuit_optimizer2` function is producing the correct output circuit for each using `zx.compare_tensors`.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we'll adopt an alternative approach, based on doing ZX-calculus simplifications. First we'll implement a simplifier using some of the techniques we learning in part 1." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.3\n", "\n", "Implement a function called `zx_simplify` that performs spider fusion and removes identities as much as possible on a ZX-diagram. Test your simplifier on random circuits using `zx.tensor_compare` as in question 2.2.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now comes the tricky part! If we start with a circuit, we can call `c.to_graph()` to get a ZX-diagram, and do all sorts of simplifications using the rules of the ZX-calculus. But how do we get a circuit back out?\n", "\n", "In the general case, this is still an open problem! Solving it in special cases has been the subject of two recent papers:\n", " * [Graph-theoretic Simplification of Quantum Circuits with the ZX-calculus](https://arxiv.org/abs/1902.03178)\n", " * [There and back again: A circuit extraction tale](https://arxiv.org/abs/2003.01664)\n", "\n", "In our case, we started with a circuit and only applied spider-fusion and identity-removal laws. This makes circuit extraction relatively simple: we just need to \"unfuse\" some spiders again to get gates back out.\n", "\n", "Written more algorithmically, we can do the following. Start with a graph and an empty circuit `circ`, and pull spiders and edges out of the graph and turn them into gates in `circ`. That is:\n", " 1. If any of outputs of the graph are connected to Hadamard edges, prepend a Hadamard gate to `circ` and change to normal edges.\n", " 2. If any vertices adjacent to an output are Z or X vertices with degree 2, prepend the appropriate kind of phase gate to `circ`, remove the vertex from the graph, and go to step 1. Otherwise continue.\n", " 3. If any pairs of vertices are both adjacent to outputs and are connected by an edge, delete that edge and prepend the appropriate kind of 2-qubit gate to `circ`. For example, if vertex 1 is a Z vertex and vertex 2 is an X vertex, prepend a CNOT gate to `circ`. If we removed an edge, go back to step 2. Otherwise continue.\n", " 4. If we have managed to remove all Z and X nodes in the previous steps, we will be left just with wires. The only thing left to do is insert SWAP gates at the beginning of the circuit to account for any crossing wires in the graph. If we did NOT remove all of the Z or X spiders, the extraction fails.\n", "\n", "PyZX has a function for this called `zx.extract_simple`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.4\n", "\n", "Implement a function called `zx_circuit_optimizer` that first simplifies the ZX-diagram of a circuit using `zx_simplify` then extracts the result. Test your optimizer on random circuits using `zx.compare_tensors` as in question 2.2.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.5\n", "\n", "Benchmark the performance of `circuit_optimizer2` and `zx_circuit_optimizer`. That is, provide statistics for how well the optimizer performs at (1) reducing the total gate count and (2) reducing the 2-qubit gate count, for random circuits of various sizes.\n", "\n", "There is no \"right or wrong\" answer to this question, but your results should be informative and you should briefly describe your methodology. For useful methods to analyse circuits, you may wish to have a look at the [documentation](https://pyzx.readthedocs.io/en/latest/api.html).\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Running on a real quantum computer\n", "\n", "Now things are about to get real. We're going to run our circuits on real hardware using the IBM Quantum Experience.\n", "\n", "We'll first perform a bit of setup. If you are already running this in the IBM Quantum Experience, the following code will probably just work. If you are running this notebook locally, you will need to set up an IBMid and run a tiny bit of Python code to save your credentials on your computer. Instructions are [here](https://github.com/Qiskit/qiskit-ibmq-provider/#updating-your-ibm-q-experience-credentials). You only need to call `IBMQ.save_account('XXXXXX')` once per computer, then you can delete this from your Jupyter notebook (I don't need to see your personal API key :) )." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "simulator = Aer.get_backend(\"qasm_simulator\")\n", "noisy_simulator = FakeAthens()\n", "try:\n", " IBMQ.load_account()\n", " provider = IBMQ.get_provider(hub=\"ibm-q\", group=\"open\", project=\"main\")\n", " backend = provider.get_backend(\"ibmq_athens\")\n", "except:\n", " print(\"Error:\", sys.exc_info()[1])\n", " print(\"Setting backend to qasm_simulator\")\n", " backend = noisy_simulator" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As always, it takes a bit of effort to get different systems to talk to each other. In our case, we need a function for translating PyZX-flavoured circuits in IBM-flavoured (qiskit) circuits. Luckily, these contain pretty much the same data, so the translation is straightforward." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def pyzx_to_qiskit(circ):\n", " # converts all gates to CNOT, CZ, HAD, ZPhase, and XPhase\n", " circ = circ.to_basic_gates()\n", " q = circ.qubits\n", " ibm_circ = QuantumCircuit(q, q)\n", " for g in circ.gates:\n", " if isinstance(g, zx.gates.CNOT): ibm_circ.cnot(g.control, g.target)\n", " elif isinstance(g, zx.gates.CZ): ibm_circ.cz(g.control, g.target)\n", " elif isinstance(g, zx.gates.HAD): ibm_circ.h(g.target)\n", " elif isinstance(g, zx.gates.ZPhase): ibm_circ.rz(math.pi * g.phase, g.target)\n", " elif isinstance(g, zx.gates.XPhase): ibm_circ.rx(math.pi * g.phase, g.target)\n", " \n", " # measure everything\n", " ibm_circ.measure(range(q), range(q))\n", " return ibm_circ" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "zx.draw(demo_circ)\n", "ibm_circ = pyzx_to_qiskit(demo_circ)\n", "display(ibm_circ.draw())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`execute` is a function that takes a (IBM-flavoured) circuit and a `backend` (e.g. a simulator or a real computer) returns a `job` object which contains the status (and eventually the results) of a computation. See the [qiskit docs](https://qiskit.org/documentation/apidoc/execute.html) for an example of executing a circuit with the simulator. For the simulator, a job involving a small circuit should be done pretty much instantly.\n", "\n", "Once a job is done, you can get the raw data out with `job.result().get_counts()`. Your circuit is run many times (these are called `shots`). `get_counts()` simply tells you how many times each measurement result was observed." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.6\n", "\n", "Build a circuit using `zx.qasm`, convert it to qiskit format, and run it using the `simulator` backend. Use the result of `get_counts()` to compute the probability of each outcome, and compare to the probabilities computed in PyZX (as in Question 1.6 of Problem Sheet 4).\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Like in the old days of classical computers, you'll need to wait in line if you want to run something on a quantum computer. You can see if a job is done by running `job_monitor(job)`. I suggest you do something else while you wait. :)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.7\n", "\n", "Build a circuit using `zx.qasm`, convert it to qiskit format, and run it using a real quantum computer as the backend. Again, use the result of `get_counts()` to compute the probability of each outcome, and compare to the probabilities computed in PyZX.\n", "\n", "Note: You should call the `decompose()` method of the qiskit circuit to make sure the circuit consists only of gates supported by the hardware. Also, 2-qubit gates can only appear between qubits that are physically connected. Choose your quantum computer and view its connectivity using the \"Dashboard\" on IBM Quantum Experience. Pay attention to how many jobs are queued, since this indicates how long you'll need to wait.\n", "\n", "

Although it is obviously more fun to run things on a real quantum computer, if you can't get it to work (or are pressed for time), you can use `noisy_simulator` as the backend for this question and the next one. It should have approximately the same behaviour as the \"ibmq_athens\" device.

\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Question 2.8\n", "\n", "Optimise your circuit using one or more of the methods from before and try running it on the hardware again. Do your results improve?\n", "\n", "

Note that performance varies a great deal from device to device (and even between runs), so your results with a real quantum computer may vary.

\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "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.10.6" } }, "nbformat": 4, "nbformat_minor": 4 }