\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", "

" ] }, { "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", "

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.

" ] }, { "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 }