diff --git a/examples/README.md b/examples/CONTRIBUTING.md
similarity index 92%
rename from examples/README.md
rename to examples/CONTRIBUTING.md
index b9948ce0..c7cac86f 100644
--- a/examples/README.md
+++ b/examples/CONTRIBUTING.md
@@ -1,9 +1,4 @@
-# pytket examples
-
-See the pytket examples built with jupyterbook [here](https://tket.quantinuum.com/examples).
-
-
-## Notes for developers
+# Contributing new notebooks
The sources for all these notebooks are the Python scripts in the `python`
directory. The notebook files are generated from them with the `gen-nb` script
diff --git a/examples/Forest_portability_example.ipynb b/examples/Forest_portability_example.ipynb
index 90e49378..fea497a0 100644
--- a/examples/Forest_portability_example.ipynb
+++ b/examples/Forest_portability_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Code Portability and Intro to Forest"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The quantum hardware landscape is incredibly competitive and rapidly changing. Many full-stack quantum software platforms lock users into them in order to use the associated devices and simulators. This notebook demonstrates how `pytket` can free up your existing high-level code to be used on devices from other providers. We will take a state-preparation and evolution circuit generated using `qiskit`, and enable it to be run on several Rigetti backends.
\n", "
\n", "To use a real hardware device, this notebook should be run from a Rigetti QMI instance. Look [here](https://www.rigetti.com/qcs/docs/intro-to-qcs) for information on how to set this up. Otherwise, make sure you have QuilC and QVM running in server mode. You will need to have `pytket`, `pytket_pyquil`, and `pytket_qiskit` installed, which are all available from PyPI."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We will start by using `qiskit` to build a random initial state over some qubits. (We remove the initial \"reset\" gates from the circuit since these are not recognized by the Forest backends, which assume an all-zero initial state.)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit import QuantumCircuit\n", "from qiskit.quantum_info.states.random import random_statevector"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_qubits = 3\n", "state = random_statevector((1 << n_qubits, 1)).data\n", "state_prep_circ = QuantumCircuit(n_qubits)\n", "state_prep_circ.initialize(state)\n", "state_prep_circ = state_prep_circ.decompose().decompose()\n", "state_prep_circ.data = [\n", " datum for datum in state_prep_circ.data if datum[0].name != \"reset\"\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(state_prep_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now evolve this state under an operator for a given duration."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit.opflow import PauliTrotterEvolution\n", "from qiskit.opflow.primitive_ops import PauliSumOp\n", "from qiskit.quantum_info import Pauli"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["duration = 1.2\n", "op = PauliSumOp.from_list([(\"XXI\", 0.3), (\"YYI\", 0.5), (\"ZZZ\", -0.4)])\n", "evolved_op = (duration * op).exp_i()\n", "evolution_circ = PauliTrotterEvolution(reps=1).convert(evolved_op).to_circuit()\n", "print(evolution_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for op in evolution_circ:\n", " state_prep_circ.append(op)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now that we have a circuit, `pytket` can take this and start operating on it directly. For example, we can apply some basic compilation passes to simplify it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import qiskit_to_tk"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tk_circ = qiskit_to_tk(state_prep_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import (\n", " SequencePass,\n", " CliffordSimp,\n", " DecomposeBoxes,\n", " KAKDecomposition,\n", " SynthesiseTket,\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["DecomposeBoxes().apply(tk_circ)\n", "optimise = SequencePass([KAKDecomposition(), CliffordSimp(False), SynthesiseTket()])\n", "optimise.apply(tk_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Display the optimised circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(tk_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Backends in `pytket` abstract away the differences between different devices and simulators as much as possible, allowing painless switching between them. The `pytket_pyquil` package provides two Backends: `ForestBackend` encapsulates both running on physical devices via Rigetti QCS and simulating those devices on the QVM, and `ForestStateBackend` acts as a wrapper to the pyQuil Wavefunction Simulator.
\n", "
\n", "Both of these still have a few restrictions on the circuits that can be run. Each only supports a subset of the gate types available in `pytket`, and a real device or associated simulation will have restricted qubit connectivity. The Backend objects will contain a default compilation pass that will statisfy these constraints as much as possible, with minimal or no optimisation.
\n", "
\n", "The `ForestStateBackend` will allow us to view the full statevector (wavefunction) expected from a perfect execution of the circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.pyquil import ForestStateBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_backend = ForestStateBackend()\n", "tk_circ = state_backend.get_compiled_circuit(tk_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["handle = state_backend.process_circuit(tk_circ)\n", "state = state_backend.get_result(handle).get_state()\n", "print(state)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For users who are familiar with the Forest SDK, the association of qubits to indices of bitstrings (and consequently the ordering of statevectors) used by default in `pytket` Backends differs from that described in the [Forest docs](http://docs.rigetti.com/en/stable/wavefunction_simulator.html#multi-qubit-basis-enumeration). You can recover the ordering used by the Forest systems with `BackendResult.get_state(tk_circ, basis:pytket.BasisOrder.dlo)` (see our docs on the `BasisOrder` enum for more details)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Connecting to real devices works very similarly. Instead of obtaining the full statevector, we are only able to measure the quantum state and sample from the resulting distribution. Beyond that, the process is pretty much the same.
\n", "
\n", "The following shows how to run the circuit on the \"9q-square\" lattice. The `as_qvm` switch on the `get_qc` method will switch between connecting to the real Aspen device and the QVM, allowing you to test your code with a simulator before you reserve your slot with the device."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tk_circ.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pyquil import get_qc\n", "from pytket.extensions.pyquil import ForestBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aspen_qc = get_qc(\"9q-square\", as_qvm=True)\n", "aspen_backend = ForestBackend(aspen_qc)\n", "tk_circ = aspen_backend.get_compiled_circuit(tk_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["counts = aspen_backend.run_circuit(tk_circ, 2000).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that attempting to connect to a live quantum device (using a `QuantumComputer` constructed with `as_qvm=False`) will fail unless it is running from a QMI instance during a reservation for the named lattice."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Code portability and intro to forest"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The quantum hardware landscape is incredibly competitive and rapidly changing. Many full-stack quantum software platforms lock users into them in order to use the associated devices and simulators. This notebook demonstrates how `pytket` can free up your existing high-level code to be used on devices from other providers. We will take a state-preparation and evolution circuit generated using `qiskit`, and enable it to be run on several Rigetti backends.
\n", "
\n", "To use a real hardware device, this notebook should be run from a Rigetti QMI instance. Look [here](https://www.rigetti.com/qcs/docs/intro-to-qcs) for information on how to set this up. Otherwise, make sure you have QuilC and QVM running in server mode. You will need to have `pytket`, `pytket_pyquil`, and `pytket_qiskit` installed, which are all available from PyPI."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We will start by using `qiskit` to build a random initial state over some qubits. (We remove the initial \"reset\" gates from the circuit since these are not recognized by the Forest backends, which assume an all-zero initial state.)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit import QuantumCircuit\n", "from qiskit.quantum_info.states.random import random_statevector"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_qubits = 3\n", "state = random_statevector((1 << n_qubits, 1)).data\n", "state_prep_circ = QuantumCircuit(n_qubits)\n", "state_prep_circ.initialize(state)\n", "state_prep_circ = state_prep_circ.decompose().decompose()\n", "state_prep_circ.data = [\n", " datum for datum in state_prep_circ.data if datum[0].name != \"reset\"\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(state_prep_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now evolve this state under an operator for a given duration."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit.opflow import PauliTrotterEvolution\n", "from qiskit.opflow.primitive_ops import PauliSumOp\n", "from qiskit.quantum_info import Pauli"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["duration = 1.2\n", "op = PauliSumOp.from_list([(\"XXI\", 0.3), (\"YYI\", 0.5), (\"ZZZ\", -0.4)])\n", "evolved_op = (duration * op).exp_i()\n", "evolution_circ = PauliTrotterEvolution(reps=1).convert(evolved_op).to_circuit()\n", "print(evolution_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for op in evolution_circ:\n", " state_prep_circ.append(op)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now that we have a circuit, `pytket` can take this and start operating on it directly. For example, we can apply some basic compilation passes to simplify it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import qiskit_to_tk"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tk_circ = qiskit_to_tk(state_prep_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import (\n", " SequencePass,\n", " CliffordSimp,\n", " DecomposeBoxes,\n", " KAKDecomposition,\n", " SynthesiseTket,\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["DecomposeBoxes().apply(tk_circ)\n", "optimise = SequencePass([KAKDecomposition(), CliffordSimp(False), SynthesiseTket()])\n", "optimise.apply(tk_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Display the optimised circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(tk_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Backends in `pytket` abstract away the differences between different devices and simulators as much as possible, allowing painless switching between them. The `pytket_pyquil` package provides two Backends: `ForestBackend` encapsulates both running on physical devices via Rigetti QCS and simulating those devices on the QVM, and `ForestStateBackend` acts as a wrapper to the pyQuil Wavefunction Simulator.
\n", "
\n", "Both of these still have a few restrictions on the circuits that can be run. Each only supports a subset of the gate types available in `pytket`, and a real device or associated simulation will have restricted qubit connectivity. The Backend objects will contain a default compilation pass that will statisfy these constraints as much as possible, with minimal or no optimisation.
\n", "
\n", "The `ForestStateBackend` will allow us to view the full statevector (wavefunction) expected from a perfect execution of the circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.pyquil import ForestStateBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_backend = ForestStateBackend()\n", "tk_circ = state_backend.get_compiled_circuit(tk_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["handle = state_backend.process_circuit(tk_circ)\n", "state = state_backend.get_result(handle).get_state()\n", "print(state)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For users who are familiar with the Forest SDK, the association of qubits to indices of bitstrings (and consequently the ordering of statevectors) used by default in `pytket` Backends differs from that described in the [Forest docs](http://docs.rigetti.com/en/stable/wavefunction_simulator.html#multi-qubit-basis-enumeration). You can recover the ordering used by the Forest systems with `BackendResult.get_state(tk_circ, basis:pytket.BasisOrder.dlo)` (see our docs on the `BasisOrder` enum for more details)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Connecting to real devices works very similarly. Instead of obtaining the full statevector, we are only able to measure the quantum state and sample from the resulting distribution. Beyond that, the process is pretty much the same.
\n", "
\n", "The following shows how to run the circuit on the \"9q-square\" lattice. The `as_qvm` switch on the `get_qc` method will switch between connecting to the real Aspen device and the QVM, allowing you to test your code with a simulator before you reserve your slot with the device."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tk_circ.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pyquil import get_qc\n", "from pytket.extensions.pyquil import ForestBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aspen_qc = get_qc(\"9q-square\", as_qvm=True)\n", "aspen_backend = ForestBackend(aspen_qc)\n", "tk_circ = aspen_backend.get_compiled_circuit(tk_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["counts = aspen_backend.run_circuit(tk_circ, 2000).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that attempting to connect to a live quantum device (using a `QuantumComputer` constructed with `as_qvm=False`) will fail unless it is running from a QMI instance during a reservation for the named lattice."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/Getting_started.ipynb b/examples/Getting_started.ipynb
new file mode 100644
index 00000000..fb1ef8d5
--- /dev/null
+++ b/examples/Getting_started.ipynb
@@ -0,0 +1,237 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "f5f6b50c-9dbc-4a85-afe2-1b1ff717d091",
+ "metadata": {},
+ "source": [
+ "# pytket examples"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e6d90a8a-4303-4d6d-9e51-ca8fcec0a2a5",
+ "metadata": {},
+ "source": [
+ "## Building a circuit with the `Circuit` class\n",
+ "\n",
+ "\n",
+ "You can create a circuit by creating an instance of the `Circuit` class and adding gates manually."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "32a81360-0002-4c82-9a9e-a0f90851f34b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket import Circuit\n",
+ "\n",
+ "ghz_circ = Circuit(3)\n",
+ "ghz_circ.H(0)\n",
+ "ghz_circ.CX(0, 1)\n",
+ "ghz_circ.CX(1, 2)\n",
+ "ghz_circ.add_barrier(ghz_circ.qubits)\n",
+ "ghz_circ.measure_all()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "147c590e-89e5-4e4e-9fb9-d10ac5689fec",
+ "metadata": {},
+ "source": [
+ "Now let's draw a nice picture of the circuit with the circuit renderer"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "92df4f4d-487c-4c56-a072-9a1d2e3c07fc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit.display import render_circuit_jupyter\n",
+ "\n",
+ "render_circuit_jupyter(ghz_circ)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bdec557e",
+ "metadata": {},
+ "source": [
+ "See also the [Circuit construction](https://tket.quantinuum.com/user-manual/manual_circuit.html) section of the user manual."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "820b0215-4ca8-450c-bc1a-62bed65e2740",
+ "metadata": {},
+ "source": [
+ "## Build a `Circuit` from a QASM file"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7c9089dc-cf4d-4efd-b90e-5485218f90a5",
+ "metadata": {},
+ "source": [
+ "Alternatively we can import a circuit from a QASM file using [pytket.qasm](https://tket.quantinuum.com/api-docs/qasm.html). There are also functions for generating a circuit from a QASM string or exporting to a qasm file.\n",
+ "\n",
+ "\n",
+ "Note that its also possible to import a circuit from quipper using [pytket.quipper](https://tket.quantinuum.com/api-docs/quipper.html) module."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1bf32877-f5e0-4b09-a12d-7aa8fa7d5d20",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.qasm import circuit_from_qasm\n",
+ "\n",
+ "w_state_circ = circuit_from_qasm(\"qasm/W-state.qasm\")\n",
+ "render_circuit_jupyter(w_state_circ)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ef0c31c5-9203-475b-abe1-5a0b53dc5e87",
+ "metadata": {},
+ "source": [
+ "## Import a circuit from qiskit (or other SDK)\n",
+ "\n",
+ "Its possible to generate a circuit directly from a qiskit `QuantumCircuit` using the [qiskit_to_tk](https://tket.quantinuum.com/extensions/pytket-qiskit/api.html#pytket.extensions.qiskit.tk_to_qiskit) function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "23e753f2-3f6e-4a3d-a2e7-ece869561249",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qiskit import QuantumCircuit\n",
+ "\n",
+ "qiskit_circ = QuantumCircuit(3)\n",
+ "qiskit_circ.h(range(3))\n",
+ "qiskit_circ.ccx(2, 1 ,0)\n",
+ "qiskit_circ.cx(0, 1)\n",
+ "print(qiskit_circ)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5357e5d4-e0d8-4a59-9fe8-426067f411e0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.extensions.qiskit import qiskit_to_tk\n",
+ "\n",
+ "tket_circ = qiskit_to_tk(qiskit_circ)\n",
+ "\n",
+ "render_circuit_jupyter(tket_circ)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7688630f-89e2-48c7-9d1e-53a9dedf088a",
+ "metadata": {},
+ "source": [
+ "Note that pytket and qiskit use opposite qubit ordering conventions. So circuits which look identical may correspond to different unitary operations."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1eee94cb-d7a2-469d-9c48-747a6e9271a1",
+ "metadata": {},
+ "source": [
+ "Circuit conversion functions are also available for [pytket-cirq](https://tket.quantinuum.com/extensions/pytket-cirq/), [pytket-pennylane](https://tket.quantinuum.com/extensions/pytket-pennylane/), [pytket-braket](https://tket.quantinuum.com/extensions/pytket-braket/) and more."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae85113b-5f8c-43b0-93c8-dc89002deece",
+ "metadata": {},
+ "source": [
+ "## Using Backends"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f2684ec5-30d9-4003-ad6a-624ec20bd268",
+ "metadata": {},
+ "source": [
+ "In pytket a `Backend` represents an interface to a quantum device or simulator.\n",
+ "\n",
+ "We will show a simple example of running the `ghz_circ` defined above on the `AerBackend` simulator."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "42f4e1f6-2281-4780-bb11-7132c4be5313",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "render_circuit_jupyter(ghz_circ)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "868189e3-197c-427b-843e-6a991d1cfec9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.extensions.qiskit import AerBackend\n",
+ "\n",
+ "backend = AerBackend()\n",
+ "result = backend.run_circuit(ghz_circ)\n",
+ "print(result.get_counts())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "eadff4bc-a1d3-42d0-bc0e-6bf515f40715",
+ "metadata": {},
+ "source": [
+ "The `AerBackend` simulator is highly idealised having a broad gateset, and no restrictive connectivity or device noise.\n",
+ "\n",
+ "The Hadamard and CX gate are supported operations of the simulator so we can run the GHZ circuit without changing any of the operations. For more realistic cases a compiler will have to solve for the limited gateset of the target backend as well as other backend requirements."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3321855a-683b-4848-81bd-1d238626f45b",
+ "metadata": {},
+ "source": [
+ "See the [Running on Backends](https://tket.quantinuum.com/user-manual/manual_backend.html) section of the user manual and the [backends example notebook](https://tket.quantinuum.com/examples/backends_example.html) for more."
+ ]
+ }
+ ],
+ "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": 5
+}
diff --git a/examples/IBMTokyoArc.png b/examples/IBMTokyoArc.png
deleted file mode 100644
index 12040422..00000000
Binary files a/examples/IBMTokyoArc.png and /dev/null differ
diff --git a/examples/IBMqx5Arc.png b/examples/IBMqx5Arc.png
deleted file mode 100644
index 3ca7b1f4..00000000
Binary files a/examples/IBMqx5Arc.png and /dev/null differ
diff --git a/examples/_config.yml b/examples/_config.yml
index 96da6a3d..07b23418 100644
--- a/examples/_config.yml
+++ b/examples/_config.yml
@@ -12,14 +12,14 @@ sphinx:
execute:
# Exclude some examples from execution (these are still deployed as html pages)
- exclude_patterns: [".venv/*", "Forest_portability_example.ipynb", "backends_example.ipynb", "qiskit_integration.ipynb", "comparing_simulators.ipynb", "expectation_value_example.ipynb", "pytket-qujax_heisenberg_vqe.ipynb", "spam_example.ipynb", "tket_benchmarking.ipynb", "entanglement_swapping.ipynb", "pytket-qujax-classification.ipynb"]
+ exclude_patterns: [".venv/*", "Forest_portability_example.ipynb", "backends_example.ipynb", "qiskit_integration.ipynb", "comparing_simulators.ipynb", "expectation_value_example.ipynb", "pytket-qujax_heisenberg_vqe.ipynb", "spam_example.ipynb", "entanglement_swapping.ipynb", "pytket-qujax-classification.ipynb"]
timeout: 90 # The maximum time (in seconds) each notebook cell is allowed to run.
# Information about where the book exists on the web
repository:
url: https://github.com/CQCL/pytket # Notebook files are located in pytket/examples
path_to_book: docs # Optional path to your book, relative to the repository root
- branch: master # Which branch of the repository should be used when creating links (optional)
+ branch: main # Which branch of the repository should be used when creating links (optional)
# Add GitHub buttons to your book
# See https://jupyterbook.org/customize/config.html#add-a-link-to-your-repository
diff --git a/examples/_toc.yml b/examples/_toc.yml
index d0acdb5d..c26227f1 100644
--- a/examples/_toc.yml
+++ b/examples/_toc.yml
@@ -2,30 +2,38 @@
# Learn more at https://jupyterbook.org/customize/toc.html
format: jb-book
-root: README
-chapters:
-- file: ansatz_sequence_example
-- file: circuit_analysis_example
-- file: circuit_generation_example
-- file: compilation_example
-- file: conditional_gate_example
-- file: contextual_optimization
-- file: creating_backends
-- file: measurement_reduction_example
-- file: mapping_example
-- file: symbolics_example
-- file: ucc_vqe
-- file: pytket-qujax_qaoa
-- file: phase_estimation
-- file: benchmarking/README
-# The following notebooks are not executed
-- file: backends_example
-- file: comparing_simulators
-- file: qiskit_integration
-- file: Forest_portability_example
-- file: pytket-qujax-classification
-- file: expectation_value_example
-- file: entanglement_swapping
-- file: pytket-qujax_heisenberg_vqe
-- file: spam_example
-- file: tket_benchmarking
+root: Getting_started
+parts:
+ - caption: Building Quantum Circuits
+ chapters:
+ - file: circuit_generation_example
+ - file: circuit_analysis_example
+ - file: conditional_gate_example
+ - caption: Backends
+ chapters:
+ - file: backends_example
+ - file: comparing_simulators
+ - file: Forest_portability_example
+ - file: creating_backends
+ - file: qiskit_integration
+ - caption: Circuit Compilation
+ chapters:
+ - file: compilation_example
+ - file: symbolics_example
+ - file: mapping_example
+ - file: ansatz_sequence_example
+ - file: measurement_reduction_example
+ - file: contextual_optimization
+ - caption: Algorithms and Protocols
+ chapters:
+ - file: phase_estimation
+ - file: ucc_vqe
+ - file: pytket-qujax_heisenberg_vqe
+ - file: pytket-qujax-classification
+ - file: pytket-qujax_qaoa
+ - file: expectation_value_example
+ - file: entanglement_swapping
+ - file: spam_example
+ - caption: Contributing
+ chapters:
+ - file: CONTRIBUTING
diff --git a/examples/ansatz_sequence_example.ipynb b/examples/ansatz_sequence_example.ipynb
index db714284..cd250ada 100644
--- a/examples/ansatz_sequence_example.ipynb
+++ b/examples/ansatz_sequence_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Ansatz Sequencing"]}, {"cell_type": "markdown", "metadata": {}, "source": ["When performing variational algorithms like VQE, one common approach to generating circuit ans\u00e4tze is to take an operator $U$ representing excitations and use this to act on a reference state $\\lvert \\phi_0 \\rangle$. One such ansatz is the Unitary Coupled Cluster ansatz. Each excitation, indexed by $j$, within $U$ is given a real coefficient $a_j$ and a parameter $t_j$, such that $U = e^{i \\sum_j \\sum_k a_j t_j P_{jk}}$, where $P_{jk} \\in \\{I, X, Y, Z \\}^{\\otimes n}$. The exact form is dependent on the chosen qubit encoding. This excitation gives us a variational state $\\lvert \\psi (t) \\rangle = U(t) \\lvert \\phi_0 \\rangle$. The operator $U$ must be Trotterised, to give a product of Pauli exponentials, and converted into native quantum gates to create the ansatz circuit.
\n", "
\n", "This notebook will describe how to use an advanced feature of `pytket` to enable automated circuit synthesis for $U$ and reduce circuit depth dramatically.
\n", "
\n", "We must create a `pytket` `QubitPauliOperator`, which represents such an operator $U$, and contains a dictionary from Pauli string $P_{jk}$ to symbolic expression. Here, we make a mock operator ourselves, which resembles the UCCSD excitation operator for the $\\mathrm{H}_2$ molecule using the Jordan-Wigner qubit encoding. In the future, operator generation will be handled automatically using CQC's upcoming software for enterprise quantum chemistry, EUMEN. We also offer conversion to and from the `OpenFermion` `QubitOperator` class, although at the time of writing a `QubitOperator` cannot handle arbitrary symbols.
\n", "
\n", "First, we create a series of `QubitPauliString` objects, which represent each $P_{jk}$."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit import Qubit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["q = [Qubit(i) for i in range(4)]\n", "qps0 = QubitPauliString([q[0], q[1], q[2]], [Pauli.Y, Pauli.Z, Pauli.X])\n", "qps1 = QubitPauliString([q[0], q[1], q[2]], [Pauli.X, Pauli.Z, Pauli.Y])\n", "qps2 = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.X, Pauli.Y])\n", "qps3 = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.Y, Pauli.X])\n", "qps4 = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.X, Pauli.X])\n", "qps5 = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.X, Pauli.X])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now, create some symbolic expressions for the $a_j t_j$ terms."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import fresh_symbol"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbol1 = fresh_symbol(\"s0\")\n", "expr1 = 1.2 * symbol1\n", "symbol2 = fresh_symbol(\"s1\")\n", "expr2 = -0.3 * symbol2"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now create our `QubitPauliOperator`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["dict1 = dict((string, expr1) for string in (qps0, qps1))\n", "dict2 = dict((string, expr2) for string in (qps2, qps3, qps4, qps5))\n", "operator = QubitPauliOperator({**dict1, **dict2})\n", "print(operator)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we can let `pytket` sequence the terms in this operator for us, using a selection of strategies. First, we will create a `Circuit` to generate an example reference state, and then use the `gen_term_sequence_circuit` method to append the Pauli exponentials."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Circuit\n", "from pytket.utils import gen_term_sequence_circuit\n", "from pytket.partition import PauliPartitionStrat, GraphColourMethod"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["reference_circ = Circuit(4).X(1).X(3)\n", "ansatz_circuit = gen_term_sequence_circuit(\n", " operator, reference_circ, PauliPartitionStrat.CommutingSets, GraphColourMethod.Lazy\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This method works by generating a graph of Pauli exponentials and performing graph colouring. Here we have chosen to partition the terms so that exponentials which commute are gathered together, and we have done so using a lazy, greedy graph colouring method.
\n", "
\n", "Alternatively, we could have used the `PauliPartitionStrat.NonConflictingSets`, which puts Pauli exponentials together so that they only require single-qubit gates to be converted into the form $e^{i \\alpha Z \\otimes Z \\otimes ... \\otimes Z}$. This strategy is primarily useful for measurement reduction, a different problem.
\n", "
\n", "We could also have used the `GraphColourMethod.LargestFirst`, which still uses a greedy method, but builds the full graph and iterates through the vertices in descending order of arity. We recommend playing around with the options, but we typically find that the combination of `CommutingSets` and `Lazy` allows the best optimisation.
\n", "
\n", "In general, not all of our exponentials will commute, so the semantics of our circuit depend on the order of our sequencing. As a result, it is important for us to be able to inspect the order we have produced. `pytket` provides functionality to enable this. Each set of commuting exponentials is put into a `CircBox`, which lets us inspect the partitoning."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import OpType"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for command in ansatz_circuit:\n", " if command.op.type == OpType.CircBox:\n", " print(\"New CircBox:\")\n", " for pauli_exp in command.op.get_circuit():\n", " print(\n", " \" {} {} {}\".format(\n", " pauli_exp, pauli_exp.op.get_paulis(), pauli_exp.op.get_phase()\n", " )\n", " )\n", " else:\n", " print(\"Native gate: {}\".format(command))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can convert this circuit into basic gates using a `pytket` `Transform`. This acts in place on the circuit to do rewriting, for gate translation and optimisation. We will start off with a naive decomposition."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.transform import Transform"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["naive_circuit = ansatz_circuit.copy()\n", "Transform.DecomposeBoxes().apply(naive_circuit)\n", "print(naive_circuit.get_commands())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is a jumble of one- and two-qubit gates. We can get some relevant circuit metrics out:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Naive CX Depth: {}\".format(naive_circuit.depth_by_type(OpType.CX)))\n", "print(\"Naive CX Count: {}\".format(naive_circuit.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These metrics can be improved upon significantly by smart compilation. A `Transform` exists precisely for this purpose:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.transform import PauliSynthStrat, CXConfigType"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["smart_circuit = ansatz_circuit.copy()\n", "Transform.UCCSynthesis(PauliSynthStrat.Sets, CXConfigType.Tree).apply(smart_circuit)\n", "print(\"Smart CX Depth: {}\".format(smart_circuit.depth_by_type(OpType.CX)))\n", "print(\"Smart CX Count: {}\".format(smart_circuit.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This `Transform` takes in a `Circuit` with the structure specified above: some arbitrary gates for the reference state, along with several `CircBox` gates containing `PauliExpBox` gates.
\n", "
\n", "We have chosen `PauliSynthStrat.Sets` and `CXConfigType.Tree`. The `PauliSynthStrat` dictates the method for decomposing multiple adjacent Pauli exponentials into basic gates, while the `CXConfigType` dictates the structure of adjacent CX gates.
\n", "
\n", "If we choose a different combination of strategies, we can produce a different output circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["last_circuit = ansatz_circuit.copy()\n", "Transform.UCCSynthesis(PauliSynthStrat.Individual, CXConfigType.Snake).apply(\n", " last_circuit\n", ")\n", "print(last_circuit.get_commands())"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Last CX Depth: {}\".format(last_circuit.depth_by_type(OpType.CX)))\n", "print(\"Last CX Count: {}\".format(last_circuit.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Other than some single-qubit Cliffords we acquired via synthesis, you can check that this gives us the same circuit structure as our `Transform.DecomposeBoxes` method! It is a suboptimal synthesis method.
\n", "
\n", "As with the `gen_term_sequence` method, we recommend playing around with the arguments and seeing what circuits come out. Typically we find that `PauliSynthStrat.Sets` and `CXConfigType.Tree` work the best, although routing can affect this somewhat."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Ansatz sequencing"]}, {"cell_type": "markdown", "metadata": {}, "source": ["When performing variational algorithms like VQE, one common approach to generating circuit ans\u00e4tze is to take an operator $U$ representing excitations and use this to act on a reference state $\\lvert \\phi_0 \\rangle$. One such ansatz is the Unitary Coupled Cluster ansatz. Each excitation, indexed by $j$, within $U$ is given a real coefficient $a_j$ and a parameter $t_j$, such that $U = e^{i \\sum_j \\sum_k a_j t_j P_{jk}}$, where $P_{jk} \\in \\{I, X, Y, Z \\}^{\\otimes n}$. The exact form is dependent on the chosen qubit encoding. This excitation gives us a variational state $\\lvert \\psi (t) \\rangle = U(t) \\lvert \\phi_0 \\rangle$. The operator $U$ must be Trotterised, to give a product of Pauli exponentials, and converted into native quantum gates to create the ansatz circuit.
\n", "
\n", "This notebook will describe how to use an advanced feature of `pytket` to enable automated circuit synthesis for $U$ and reduce circuit depth dramatically.
\n", "
\n", "We must create a `pytket` `QubitPauliOperator`, which represents such an operator $U$, and contains a dictionary from Pauli string $P_{jk}$ to symbolic expression. Here, we make a mock operator ourselves, which resembles the UCCSD excitation operator for the $\\mathrm{H}_2$ molecule using the Jordan-Wigner qubit encoding.
\n", "
\n", "First, we create a series of `QubitPauliString` objects, which represent each $P_{jk}$."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.circuit import Qubit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["q = [Qubit(i) for i in range(4)]\n", "qps0 = QubitPauliString([q[0], q[1], q[2]], [Pauli.Y, Pauli.Z, Pauli.X])\n", "qps1 = QubitPauliString([q[0], q[1], q[2]], [Pauli.X, Pauli.Z, Pauli.Y])\n", "qps2 = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.X, Pauli.Y])\n", "qps3 = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.Y, Pauli.X])\n", "qps4 = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.X, Pauli.X])\n", "qps5 = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.X, Pauli.X])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now, create some symbolic expressions for the $a_j t_j$ terms."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import fresh_symbol"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbol1 = fresh_symbol(\"s0\")\n", "expr1 = 1.2 * symbol1\n", "symbol2 = fresh_symbol(\"s1\")\n", "expr2 = -0.3 * symbol2"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now create our `QubitPauliOperator`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["dict1 = dict((string, expr1) for string in (qps0, qps1))\n", "dict2 = dict((string, expr2) for string in (qps2, qps3, qps4, qps5))\n", "operator = QubitPauliOperator({**dict1, **dict2})\n", "print(operator)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we can let `pytket` sequence the terms in this operator for us, using a selection of strategies. First, we will create a `Circuit` to generate an example reference state, and then use the `gen_term_sequence_circuit` method to append the Pauli exponentials."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Circuit\n", "from pytket.utils import gen_term_sequence_circuit\n", "from pytket.partition import PauliPartitionStrat, GraphColourMethod"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["reference_circ = Circuit(4).X(1).X(3)\n", "ansatz_circuit = gen_term_sequence_circuit(\n", " operator, reference_circ, PauliPartitionStrat.CommutingSets, GraphColourMethod.Lazy\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This method works by generating a graph of Pauli exponentials and performing graph colouring. Here we have chosen to partition the terms so that exponentials which commute are gathered together, and we have done so using a lazy, greedy graph colouring method.
\n", "
\n", "Alternatively, we could have used the `PauliPartitionStrat.NonConflictingSets`, which puts Pauli exponentials together so that they only require single-qubit gates to be converted into the form $e^{i \\alpha Z \\otimes Z \\otimes ... \\otimes Z}$. This strategy is primarily useful for measurement reduction, a different problem.
\n", "
\n", "We could also have used the `GraphColourMethod.LargestFirst`, which still uses a greedy method, but builds the full graph and iterates through the vertices in descending order of arity. We recommend playing around with the options, but we typically find that the combination of `CommutingSets` and `Lazy` allows the best optimisation.
\n", "
\n", "In general, not all of our exponentials will commute, so the semantics of our circuit depend on the order of our sequencing. As a result, it is important for us to be able to inspect the order we have produced. `pytket` provides functionality to enable this. Each set of commuting exponentials is put into a `CircBox`, which lets us inspect the partitoning."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import OpType"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for command in ansatz_circuit:\n", " if command.op.type == OpType.CircBox:\n", " print(\"New CircBox:\")\n", " for pauli_exp in command.op.get_circuit():\n", " print(\n", " \" {} {} {}\".format(\n", " pauli_exp, pauli_exp.op.get_paulis(), pauli_exp.op.get_phase()\n", " )\n", " )\n", " else:\n", " print(\"Native gate: {}\".format(command))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can convert this circuit into basic gates using a `pytket` `Transform`. This acts in place on the circuit to do rewriting, for gate translation and optimisation. We will start off with a naive decomposition."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.transform import Transform"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["naive_circuit = ansatz_circuit.copy()\n", "Transform.DecomposeBoxes().apply(naive_circuit)\n", "print(naive_circuit.get_commands())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is a jumble of one- and two-qubit gates. We can get some relevant circuit metrics out:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Naive CX Depth: {}\".format(naive_circuit.depth_by_type(OpType.CX)))\n", "print(\"Naive CX Count: {}\".format(naive_circuit.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These metrics can be improved upon significantly by smart compilation. A `Transform` exists precisely for this purpose:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.transform import PauliSynthStrat, CXConfigType"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["smart_circuit = ansatz_circuit.copy()\n", "Transform.UCCSynthesis(PauliSynthStrat.Sets, CXConfigType.Tree).apply(smart_circuit)\n", "print(\"Smart CX Depth: {}\".format(smart_circuit.depth_by_type(OpType.CX)))\n", "print(\"Smart CX Count: {}\".format(smart_circuit.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This `Transform` takes in a `Circuit` with the structure specified above: some arbitrary gates for the reference state, along with several `CircBox` gates containing `PauliExpBox` gates.
\n", "
\n", "We have chosen `PauliSynthStrat.Sets` and `CXConfigType.Tree`. The `PauliSynthStrat` dictates the method for decomposing multiple adjacent Pauli exponentials into basic gates, while the `CXConfigType` dictates the structure of adjacent CX gates.
\n", "
\n", "If we choose a different combination of strategies, we can produce a different output circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["last_circuit = ansatz_circuit.copy()\n", "Transform.UCCSynthesis(PauliSynthStrat.Individual, CXConfigType.Snake).apply(\n", " last_circuit\n", ")\n", "print(last_circuit.get_commands())"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Last CX Depth: {}\".format(last_circuit.depth_by_type(OpType.CX)))\n", "print(\"Last CX Count: {}\".format(last_circuit.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Other than some single-qubit Cliffords we acquired via synthesis, you can check that this gives us the same circuit structure as our `Transform.DecomposeBoxes` method! It is a suboptimal synthesis method.
\n", "
\n", "As with the `gen_term_sequence` method, we recommend playing around with the arguments and seeing what circuits come out. Typically we find that `PauliSynthStrat.Sets` and `CXConfigType.Tree` work the best, although routing can affect this somewhat."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/backends_example.ipynb b/examples/backends_example.ipynb
index 1e008562..8ed5b4bc 100644
--- a/examples/backends_example.ipynb
+++ b/examples/backends_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# TKET Backend Tutorial"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This example shows how to use `pytket` to execute quantum circuits on both simulators and real devices, and how to interpret the results. As tket is designed to be platform-agnostic, we have unified the interfaces of different providers as much as possible into the `Backend` class for maximum portability of code. The following is a selection of currently supported backends:
\n", "* ProjectQ simulator
\n", "* Aer simulators (statevector, QASM, and unitary)
\n", "* IBMQ devices
\n", "* Rigetti QCS devices
\n", "* Rigetti QVM (for device simulation or statevector)
\n", "* AQT devices
\n", "* Quantinuum devices
\n", "* Q# simulators"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this notebook we will focus on the Aer, IBMQ and ProjectQ backends.
\n", "
\n", "To get started, we must install the core pytket package and the subpackages required to interface with the desired providers. We will also need the `QubitOperator` class from `openfermion` to construct operators for a later example. To get everything run the following in shell:
\n", "
\n", "`pip install pytket pytket-qiskit pytket-projectq openfermion`
\n", "
\n", "First, import the backends that we will be demonstrating."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import (\n", " AerStateBackend,\n", " AerBackend,\n", " AerUnitaryBackend,\n", " IBMQBackend,\n", " IBMQEmulatorBackend,\n", ")\n", "from pytket.extensions.projectq import ProjectQBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We are also going to be making a circuit to run on these backends, so import the `Circuit` class."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, Qubit"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Below we generate a circuit which will produce a Bell state, assuming the qubits are all initialised in the |0> state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(2)\n", "circ.H(0)\n", "circ.CX(0, 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["As a sanity check, we will use the `AerStateBackend` to verify that `circ` does actually produce a Bell state.
\n", "
\n", "To submit a circuit for excution on a backend we can use `process_circuit` with appropriate arguments. If we have multiple circuits to excecute, we can use `process_circuits` (note the plural), which will attempt to batch up the circuits if possible. Both methods return a `ResultHandle` object per submitted `Circuit` which you can use with result retrieval methods to get the result type you want (as long as that result type is supported by the backend).
\n", "
\n", "Calling `get_state` will return a `numpy` array corresponding to the statevector.
\n", "
\n", "This style of usage is used consistently in the `pytket` backends."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aer_state_b = AerStateBackend()\n", "state_handle = aer_state_b.process_circuit(circ)\n", "statevector = aer_state_b.get_result(state_handle).get_state()\n", "print(statevector)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["As we can see, the output state vector $\\lvert \\psi_{\\mathrm{circ}}\\rangle$ is $(\\lvert00\\rangle + \\lvert11\\rangle)/\\sqrt2$.
\n", "
\n", "This is a symmetric state. For non-symmetric states, we default to an ILO-BE format (increasing lexicographic order of (qu)bit ids, big-endian), but an alternative convention can be specified when retrieving results from backends. See the docs for the `BasisOrder` enum for more information."]}, {"cell_type": "markdown", "metadata": {}, "source": ["A lesser-used simulator available through Qiskit Aer is their unitary simulator. This will be somewhat more expensive to run, but returns the full unitary matrix for the provided circuit. This is useful in the design of small subcircuits that will be used multiple times within other larger circuits - statevector simulators will only test that they act correctly on the $\\lvert 0 \\rangle^{\\otimes n}$ state, which is not enough to guarantee the circuit's correctness.
\n", "
\n", "The `AerUnitaryBackend` provides a convenient access point for this simulator for use with `pytket` circuits. The unitary of the circuit can be retrieved from backends that support it using the `BackendResult.get_unitary` interface. In this example, we chose to use `Backend.run_circuit`, which is equivalent to calling `process_circuit` followed by `get_result`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aer_unitary_b = AerUnitaryBackend()\n", "result = aer_unitary_b.run_circuit(circ)\n", "print(result.get_unitary())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that state vector and unitary simulations are also available in pytket directly. In general, we recommend you use these unless you require another Backend explicitly."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["statevector = circ.get_statevector()\n", "unitary = circ.get_unitary()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now suppose we want to measure this Bell state to get some actual results out, so let's append some `Measure` gates to the circuit. The `Circuit` class has the `measure_all` utility function which appends `Measure` gates on every qubit. All of these results will be written to the default classical register ('c'). This function will automatically add the classical bits to the circuit if they are not already there."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ.measure_all()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can get some measured counts out from the `AerBackend`, which is an interface to the Qiskit Aer QASM simulator. Suppose we would like to get 10 shots out (10 repeats of the circuit and measurement). We can seed the simulator's random-number generator in order to make the results reproducible, using an optional keyword argument to `process_circuit`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aer_b = AerBackend()\n", "handle = aer_b.process_circuit(circ, n_shots=10, seed=1)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["counts = aer_b.get_result(handle).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["What happens if we simulate some noise in our imagined device, using the Qiskit Aer noise model?"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To investigate this, we will require an import from Qiskit. For more information about noise modelling using Qiskit Aer, see the [Qiskit device noise](https://qiskit.org/documentation/apidoc/aer_noise.html) documentation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit.providers.aer.noise import NoiseModel"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_noise_model = NoiseModel()\n", "readout_error = 0.2\n", "for q in range(2):\n", " my_noise_model.add_readout_error(\n", " [[1 - readout_error, readout_error], [readout_error, 1 - readout_error]], [q]\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This simple noise model gives a 20% chance that, upon measurement, a qubit that would otherwise have been measured as $0$ would instead be measured as $1$, and vice versa. Let's see what our shot table looks like with this model:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["noisy_aer_b = AerBackend(my_noise_model)\n", "noisy_handle = noisy_aer_b.process_circuit(circ, n_shots=10, seed=1, valid_check=False)\n", "noisy_counts = noisy_aer_b.get_result(noisy_handle).get_counts()\n", "print(noisy_counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We now have some spurious $01$ and $10$ measurements, which could never happen when measuring a Bell state on a noiseless device.
\n", "
\n", "The `AerBackend` class can accept any Qiskit noise model.
\n", "
\n", "All backends expose a generic `get_result` method which takes a `ResultHandle` and returns the respective result in the form of a `BackendResult` object. This object may hold measured results in the form of shots or counts, or an exact statevector from simulation. Measured results are stored as `OutcomeArray` objects, which compresses measured bit values into 8-bit integers. We can extract the bitwise values using `to_readouts`.
\n", "
\n", "Instead of an assumed ILO or DLO convention, we can use this object to request only the `Bit` measurements we want, in the order we want. Let's try reversing the bits of the noisy results."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend_result = noisy_aer_b.get_result(noisy_handle)\n", "bits = circ.bits"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["outcomes = backend_result.get_counts([bits[1], bits[0]])\n", "print(outcomes)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`BackendResult` objects can be natively serialized to and deserialized from a dictionary. This dictionary can be immediately dumped to `json` for storing results."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.backends.backendresult import BackendResult"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["result_dict = backend_result.to_dict()\n", "print(result_dict)\n", "print(BackendResult.from_dict(result_dict).get_counts())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The last simulator we will demonstrate is the `ProjectQBackend`. ProjectQ offers fast simulation of quantum circuits with built-in support for fast expectation values from operators. The `ProjectQBackend` exposes this functionality to take in OpenFermion `QubitOperator` instances. These are convertible to and from `QubitPauliOperator` instances in Pytket.
\n", "
\n", "Note: ProjectQ can also produce statevectors in the style of `AerStateBackend`, and similarly Aer backends can calculate expectation values directly, consult the relevant documentation to see more.
\n", "
\n", "Let's create an OpenFermion `QubitOperator` object and a new circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = 0.5 * of.QubitOperator(\"X0 X2\") + 0.3 * of.QubitOperator(\"Z0\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ2 = Circuit(3)\n", "circ2.Y(0)\n", "circ2.H(1)\n", "circ2.Rx(0.3, 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We convert the OpenFermion Hamiltonian into a pytket QubitPauliOperator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we can create a `ProjectQBackend` instance and feed it our circuit and `QubitOperator`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["projectq_b = ProjectQBackend()\n", "expectation = projectq_b.get_operator_expectation_value(circ2, hamiltonian_op)\n", "print(expectation)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The last leg of this tour includes running a pytket circuit on an actual quantum computer. To do this, you will need an IBM quantum experience account and have your credentials stored on your computer. See https://quantum-computing.ibm.com to make an account and view available devices and their specs.
\n", "
\n", "Physical devices have much stronger constraints on the form of admissible circuits than simulators. They tend to support a minimal gate set, have restricted connectivity between qubits for two-qubit gates, and can have limited support for classical control flow or conditional gates. This is where we can invoke the tket compiler passes to transform our desired circuit into one that is suitable for the backend.
\n", "
\n", "To check our code works correctly, we can use the `IBMQEmulatorBackend` to run our code exactly as if it were going to run on a real device, but just execute on a simulator (with a basic noise model adapted from the reported device properties)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's create an `IBMQEmulatorBackend` for the `ibmq_manila` device and check if our circuit is valid to be run."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibmq_b_emu = IBMQEmulatorBackend(\"ibmq_manila\")\n", "ibmq_b_emu.valid_circuit(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["It looks like we need to compile this circuit to be compatible with the device. To simplify this procedure, we provide minimal compilation passes designed for each backend (the `default_compilation_pass()` method) which will guarantee compatibility with the device. These may still fail if the input circuit has too many qubits or unsupported usage of conditional gates. The default passes can have their degree of optimisation by changing an integer parameter (optimisation levels 0, 1, 2), and they can be easily composed with any of tket's other optimisation passes for better performance.
\n", "
\n", "For convenience, we also wrap up this pass into the `get_compiled_circuit` method if you just want to compile a single circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["compiled_circ = ibmq_b_emu.get_compiled_circuit(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's create a backend for running on the actual device and check our compiled circuit is valid for this backend too."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibmq_b = IBMQBackend(\"ibmq_manila\")\n", "ibmq_b.valid_circuit(compiled_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We are now good to run this circuit on the device. After submitting, we can use the handle to check on the status of the job, so that we know when results are ready to be retrieved. The `circuit_status` method works for all backends, and returns a `CircuitStatus` object. If we just run `get_result` straight away, the backend will wait for results to complete, blocking any other code from running.
\n", "
\n", "In this notebook we will use the emulated backend `ibmq_b_emu` to illustrate, but the workflow is the same as for the real backend `ibmq_b` (except that the latter will typically take much longer because of the size of the queue)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["quantum_handle = ibmq_b_emu.process_circuit(compiled_circ, n_shots=10)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(ibmq_b_emu.circuit_status(quantum_handle))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["quantum_counts = ibmq_b_emu.get_result(quantum_handle).get_counts()\n", "print(quantum_counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These are from an actual device, so it's impossible to perfectly predict what the results will be. However, because of the problem of noise, it would be unsurprising to find a few $01$ or $10$ results in the table. The circuit is very short, so it should be fairly close to the ideal result.
\n", "
\n", "The devices available through the IBM Q Experience serve jobs one at a time from their respective queues, so a large amount of experiment time can be taken up by waiting for your jobs to reach the front of the queue. `pytket` allows circuits to be submitted to any backend in a single batch using the `process_circuits` method. For the `IBMQBackend`, this will collate the circuits into as few jobs as possible which will all be sent off into the queue for the device. The method returns a `ResultHandle` per submitted circuit, in the order of submission."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuits = []\n", "for i in range(5):\n", " c = Circuit(2)\n", " c.Rx(0.2 * i, 0).CX(0, 1)\n", " c.measure_all()\n", " circuits.append(ibmq_b_emu.get_compiled_circuit(c))\n", "handles = ibmq_b_emu.process_circuits(circuits, n_shots=100)\n", "print(handles)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now retrieve the results and process them. As we measured each circuit in the $Z$-basis, we can obtain the expectation value for the $ZZ$ operator immediately from these measurement results. We can calculate this using the `expectation_from_counts` utility method in `pytket`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import expectation_from_counts"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for handle in handles:\n", " counts = ibmq_b_emu.get_result(handle).get_counts()\n", " exp_val = expectation_from_counts(counts)\n", " print(exp_val)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A `ResultHandle` can be easily stored in its string representaton and later reconstructed using the `from_str` method. For example, we could do something like this:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.backends import ResultHandle"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(2).Rx(0.5, 0).CX(0, 1).measure_all()\n", "c = ibmq_b_emu.get_compiled_circuit(c)\n", "handle = ibmq_b_emu.process_circuit(c, n_shots=10)\n", "handlestring = str(handle)\n", "print(handlestring)\n", "# ... later ...\n", "oldhandle = ResultHandle.from_str(handlestring)\n", "print(ibmq_b_emu.get_result(oldhandle).get_counts())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For backends which support persistent handles (e.g. `IBMQBackend`, `QuantinuumBackend`, `BraketBackend` and `AQTBackend`) you can even stop your python session and use your result handles in a separate script to retrive results when they are ready, by storing the handle strings. For experiments with long queue times, this enables separate job submission and retrieval. Use `Backend.persistent_handles` to check whether a backend supports this feature.
\n", "
\n", "All backends will also cache all results obtained in the current python session, so you can use the `ResultHandle` to retrieve the results many times if you need to reuse the results. Over a long experiment, this can consume a large amount of RAM, so we recommend removing results from the cache when you are done with them. A simple way to achieve this is by calling `Backend.empty_cache` (e.g. at the end of each loop of a variational algorithm), or removing individual results with `Backend.pop_result`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The backends in `pytket` are designed to be as similar to one another as possible. The example above using physical devices can be run entirely on a simulator by swapping out the `IBMQBackend` constructor for any other backend supporting shot outputs (e.g. `AerBackend`, `ProjectQBackend`, `ForestBackend`), or passing it the name of a different device. Furthermore, using pytket it is simple to convert between handling shot tables, counts maps and statevectors.
\n", "
\n", "For more information on backends and other `pytket` features, read our [documentation](https://cqcl.github.io/pytket) or see the other examples on our [GitHub repo](https://github.com/CQCL/tket/pytket/api)."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# TKET backend tutorial"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This example shows how to use `pytket` to execute quantum circuits on both simulators and real devices, and how to interpret the results. As tket is designed to be platform-agnostic, we have unified the interfaces of different providers as much as possible into the `Backend` class for maximum portability of code. The following is a selection of currently supported backends:
\n", "* ProjectQ simulator
\n", "* Aer simulators (statevector, QASM, and unitary)
\n", "* IBMQ devices
\n", "* Rigetti QCS devices
\n", "* Rigetti QVM (for device simulation or statevector)
\n", "* AQT devices
\n", "* Quantinuum devices
\n", "* Q# simulators"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this notebook we will focus on the Aer, IBMQ and ProjectQ backends.
\n", "
\n", "To get started, we must install the core pytket package and the subpackages required to interface with the desired providers. We will also need the `QubitOperator` class from `openfermion` to construct operators for a later example. To get everything run the following in shell:
\n", "
\n", "`pip install pytket pytket-qiskit pytket-projectq openfermion`
\n", "
\n", "First, import the backends that we will be demonstrating."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import (\n", " AerStateBackend,\n", " AerBackend,\n", " AerUnitaryBackend,\n", " IBMQBackend,\n", " IBMQEmulatorBackend,\n", ")\n", "from pytket.extensions.projectq import ProjectQBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We are also going to be making a circuit to run on these backends, so import the `Circuit` class."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, Qubit"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Below we generate a circuit which will produce a Bell state, assuming the qubits are all initialised in the |0> state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(2)\n", "circ.H(0)\n", "circ.CX(0, 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["As a sanity check, we will use the `AerStateBackend` to verify that `circ` does actually produce a Bell state.
\n", "
\n", "To submit a circuit for excution on a backend we can use `process_circuit` with appropriate arguments. If we have multiple circuits to excecute, we can use `process_circuits` (note the plural), which will attempt to batch up the circuits if possible. Both methods return a `ResultHandle` object per submitted `Circuit` which you can use with result retrieval methods to get the result type you want (as long as that result type is supported by the backend).
\n", "
\n", "Calling `get_state` will return a `numpy` array corresponding to the statevector.
\n", "
\n", "This style of usage is used consistently in the `pytket` backends."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aer_state_b = AerStateBackend()\n", "state_handle = aer_state_b.process_circuit(circ)\n", "statevector = aer_state_b.get_result(state_handle).get_state()\n", "print(statevector)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["As we can see, the output state vector $\\lvert \\psi_{\\mathrm{circ}}\\rangle$ is $(\\lvert00\\rangle + \\lvert11\\rangle)/\\sqrt2$.
\n", "
\n", "This is a symmetric state. For non-symmetric states, we default to an ILO-BE format (increasing lexicographic order of (qu)bit ids, big-endian), but an alternative convention can be specified when retrieving results from backends. See the docs for the `BasisOrder` enum for more information."]}, {"cell_type": "markdown", "metadata": {}, "source": ["A lesser-used simulator available through Qiskit Aer is their unitary simulator. This will be somewhat more expensive to run, but returns the full unitary matrix for the provided circuit. This is useful in the design of small subcircuits that will be used multiple times within other larger circuits - statevector simulators will only test that they act correctly on the $\\lvert 0 \\rangle^{\\otimes n}$ state, which is not enough to guarantee the circuit's correctness.
\n", "
\n", "The `AerUnitaryBackend` provides a convenient access point for this simulator for use with `pytket` circuits. The unitary of the circuit can be retrieved from backends that support it using the `BackendResult.get_unitary` interface. In this example, we chose to use `Backend.run_circuit`, which is equivalent to calling `process_circuit` followed by `get_result`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aer_unitary_b = AerUnitaryBackend()\n", "result = aer_unitary_b.run_circuit(circ)\n", "print(result.get_unitary())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that state vector and unitary simulations are also available in pytket directly. In general, we recommend you use these unless you require another Backend explicitly."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["statevector = circ.get_statevector()\n", "unitary = circ.get_unitary()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now suppose we want to measure this Bell state to get some actual results out, so let's append some `Measure` gates to the circuit. The `Circuit` class has the `measure_all` utility function which appends `Measure` gates on every qubit. All of these results will be written to the default classical register ('c'). This function will automatically add the classical bits to the circuit if they are not already there."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ.measure_all()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can get some measured counts out from the `AerBackend`, which is an interface to the Qiskit Aer QASM simulator. Suppose we would like to get 10 shots out (10 repeats of the circuit and measurement). We can seed the simulator's random-number generator in order to make the results reproducible, using an optional keyword argument to `process_circuit`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["aer_b = AerBackend()\n", "handle = aer_b.process_circuit(circ, n_shots=10, seed=1)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["counts = aer_b.get_result(handle).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["What happens if we simulate some noise in our imagined device, using the Qiskit Aer noise model?"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To investigate this, we will require an import from Qiskit. For more information about noise modelling using Qiskit Aer, see the [Qiskit device noise](https://qiskit.org/documentation/apidoc/aer_noise.html) documentation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit.providers.aer.noise import NoiseModel"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["my_noise_model = NoiseModel()\n", "readout_error = 0.2\n", "for q in range(2):\n", " my_noise_model.add_readout_error(\n", " [[1 - readout_error, readout_error], [readout_error, 1 - readout_error]], [q]\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This simple noise model gives a 20% chance that, upon measurement, a qubit that would otherwise have been measured as $0$ would instead be measured as $1$, and vice versa. Let's see what our shot table looks like with this model:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["noisy_aer_b = AerBackend(my_noise_model)\n", "noisy_handle = noisy_aer_b.process_circuit(circ, n_shots=10, seed=1, valid_check=False)\n", "noisy_counts = noisy_aer_b.get_result(noisy_handle).get_counts()\n", "print(noisy_counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We now have some spurious $01$ and $10$ measurements, which could never happen when measuring a Bell state on a noiseless device.
\n", "
\n", "The `AerBackend` class can accept any Qiskit noise model.
\n", "
\n", "All backends expose a generic `get_result` method which takes a `ResultHandle` and returns the respective result in the form of a `BackendResult` object. This object may hold measured results in the form of shots or counts, or an exact statevector from simulation. Measured results are stored as `OutcomeArray` objects, which compresses measured bit values into 8-bit integers. We can extract the bitwise values using `to_readouts`.
\n", "
\n", "Instead of an assumed ILO or DLO convention, we can use this object to request only the `Bit` measurements we want, in the order we want. Let's try reversing the bits of the noisy results."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend_result = noisy_aer_b.get_result(noisy_handle)\n", "bits = circ.bits"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["outcomes = backend_result.get_counts([bits[1], bits[0]])\n", "print(outcomes)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`BackendResult` objects can be natively serialized to and deserialized from a dictionary. This dictionary can be immediately dumped to `json` for storing results."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.backends.backendresult import BackendResult"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["result_dict = backend_result.to_dict()\n", "print(result_dict)\n", "print(BackendResult.from_dict(result_dict).get_counts())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The last simulator we will demonstrate is the `ProjectQBackend`. ProjectQ offers fast simulation of quantum circuits with built-in support for fast expectation values from operators. The `ProjectQBackend` exposes this functionality to take in OpenFermion `QubitOperator` instances. These are convertible to and from `QubitPauliOperator` instances in Pytket.
\n", "
\n", "Note: ProjectQ can also produce statevectors in the style of `AerStateBackend`, and similarly Aer backends can calculate expectation values directly, consult the relevant documentation to see more.
\n", "
\n", "Let's create an OpenFermion `QubitOperator` object and a new circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = 0.5 * of.QubitOperator(\"X0 X2\") + 0.3 * of.QubitOperator(\"Z0\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ2 = Circuit(3)\n", "circ2.Y(0)\n", "circ2.H(1)\n", "circ2.Rx(0.3, 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We convert the OpenFermion Hamiltonian into a pytket QubitPauliOperator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we can create a `ProjectQBackend` instance and feed it our circuit and `QubitOperator`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["projectq_b = ProjectQBackend()\n", "expectation = projectq_b.get_operator_expectation_value(circ2, hamiltonian_op)\n", "print(expectation)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The last leg of this tour includes running a pytket circuit on an actual quantum computer. To do this, you will need an IBM quantum experience account and have your credentials stored on your computer. See https://quantum-computing.ibm.com to make an account and view available devices and their specs.
\n", "
\n", "Physical devices have much stronger constraints on the form of admissible circuits than simulators. They tend to support a minimal gate set, have restricted connectivity between qubits for two-qubit gates, and can have limited support for classical control flow or conditional gates. This is where we can invoke the tket compiler passes to transform our desired circuit into one that is suitable for the backend.
\n", "
\n", "To check our code works correctly, we can use the `IBMQEmulatorBackend` to run our code exactly as if it were going to run on a real device, but just execute on a simulator (with a basic noise model adapted from the reported device properties)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's create an `IBMQEmulatorBackend` for the `ibmq_manila` device and check if our circuit is valid to be run."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibmq_b_emu = IBMQEmulatorBackend(\"ibmq_manila\")\n", "ibmq_b_emu.valid_circuit(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["It looks like we need to compile this circuit to be compatible with the device. To simplify this procedure, we provide minimal compilation passes designed for each backend (the `default_compilation_pass()` method) which will guarantee compatibility with the device. These may still fail if the input circuit has too many qubits or unsupported usage of conditional gates. The default passes can have their degree of optimisation by changing an integer parameter (optimisation levels 0, 1, 2), and they can be easily composed with any of tket's other optimisation passes for better performance.
\n", "
\n", "For convenience, we also wrap up this pass into the `get_compiled_circuit` method if you just want to compile a single circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["compiled_circ = ibmq_b_emu.get_compiled_circuit(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's create a backend for running on the actual device and check our compiled circuit is valid for this backend too."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibmq_b = IBMQBackend(\"ibmq_manila\")\n", "ibmq_b.valid_circuit(compiled_circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We are now good to run this circuit on the device. After submitting, we can use the handle to check on the status of the job, so that we know when results are ready to be retrieved. The `circuit_status` method works for all backends, and returns a `CircuitStatus` object. If we just run `get_result` straight away, the backend will wait for results to complete, blocking any other code from running.
\n", "
\n", "In this notebook we will use the emulated backend `ibmq_b_emu` to illustrate, but the workflow is the same as for the real backend `ibmq_b` (except that the latter will typically take much longer because of the size of the queue)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["quantum_handle = ibmq_b_emu.process_circuit(compiled_circ, n_shots=10)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(ibmq_b_emu.circuit_status(quantum_handle))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["quantum_counts = ibmq_b_emu.get_result(quantum_handle).get_counts()\n", "print(quantum_counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These are from an actual device, so it's impossible to perfectly predict what the results will be. However, because of the problem of noise, it would be unsurprising to find a few $01$ or $10$ results in the table. The circuit is very short, so it should be fairly close to the ideal result.
\n", "
\n", "The devices available through the IBM Q Experience serve jobs one at a time from their respective queues, so a large amount of experiment time can be taken up by waiting for your jobs to reach the front of the queue. `pytket` allows circuits to be submitted to any backend in a single batch using the `process_circuits` method. For the `IBMQBackend`, this will collate the circuits into as few jobs as possible which will all be sent off into the queue for the device. The method returns a `ResultHandle` per submitted circuit, in the order of submission."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circuits = []\n", "for i in range(5):\n", " c = Circuit(2)\n", " c.Rx(0.2 * i, 0).CX(0, 1)\n", " c.measure_all()\n", " circuits.append(ibmq_b_emu.get_compiled_circuit(c))\n", "handles = ibmq_b_emu.process_circuits(circuits, n_shots=100)\n", "print(handles)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now retrieve the results and process them. As we measured each circuit in the $Z$-basis, we can obtain the expectation value for the $ZZ$ operator immediately from these measurement results. We can calculate this using the `expectation_from_counts` utility method in `pytket`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import expectation_from_counts"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for handle in handles:\n", " counts = ibmq_b_emu.get_result(handle).get_counts()\n", " exp_val = expectation_from_counts(counts)\n", " print(exp_val)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A `ResultHandle` can be easily stored in its string representaton and later reconstructed using the `from_str` method. For example, we could do something like this:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.backends import ResultHandle"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(2).Rx(0.5, 0).CX(0, 1).measure_all()\n", "c = ibmq_b_emu.get_compiled_circuit(c)\n", "handle = ibmq_b_emu.process_circuit(c, n_shots=10)\n", "handlestring = str(handle)\n", "print(handlestring)\n", "# ... later ...\n", "oldhandle = ResultHandle.from_str(handlestring)\n", "print(ibmq_b_emu.get_result(oldhandle).get_counts())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For backends which support persistent handles (e.g. `IBMQBackend`, `QuantinuumBackend`, `BraketBackend` and `AQTBackend`) you can even stop your python session and use your result handles in a separate script to retrive results when they are ready, by storing the handle strings. For experiments with long queue times, this enables separate job submission and retrieval. Use `Backend.persistent_handles` to check whether a backend supports this feature.
\n", "
\n", "All backends will also cache all results obtained in the current python session, so you can use the `ResultHandle` to retrieve the results many times if you need to reuse the results. Over a long experiment, this can consume a large amount of RAM, so we recommend removing results from the cache when you are done with them. A simple way to achieve this is by calling `Backend.empty_cache` (e.g. at the end of each loop of a variational algorithm), or removing individual results with `Backend.pop_result`."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The backends in `pytket` are designed to be as similar to one another as possible. The example above using physical devices can be run entirely on a simulator by swapping out the `IBMQBackend` constructor for any other backend supporting shot outputs (e.g. `AerBackend`, `ProjectQBackend`, `ForestBackend`), or passing it the name of a different device. Furthermore, using pytket it is simple to convert between handling shot tables, counts maps and statevectors.
\n", "
\n", "For more information on backends and other `pytket` features, read our [documentation](https://cqcl.github.io/pytket) or see the other examples on our [GitHub repo](https://github.com/CQCL/tket/pytket/api)."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/benchmarking/README.md b/examples/benchmarking/README.md
deleted file mode 100644
index ba3f3ec8..00000000
--- a/examples/benchmarking/README.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# Benchmarking
-
-The circuits used for the benchmarking experiments reported in
-[arxiv:1902:08091](https://arxiv.org/abs/1902.08091) are available in the
-[tket_benchmarking](https://github.com/CQCL/tket_benchmarking) repository
-(in the folder `arxiv-1902-08091`).
diff --git a/examples/comparing_simulators.ipynb b/examples/comparing_simulators.ipynb
index a571e208..0931a019 100644
--- a/examples/comparing_simulators.ipynb
+++ b/examples/comparing_simulators.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Comparison of the simulators available through tket"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this tutorial, we will focus on:
\n", "- exploring the wide array of simulators available through the extension modules for `pytket`;
\n", "- comparing their unique features and capabilities."]}, {"cell_type": "markdown", "metadata": {}, "source": ["This example assumes the reader is familiar with the basics of circuit construction and evaluation.
\n", "
\n", "To run every option in this example, you will need `pytket`, `pytket-qiskit`, `pytket-pyquil`, `pytket-qsharp`, `pytket-qulacs`, and `pytket-projectq`.
\n", "
\n", "With the number of simulator `Backend`s available across the `pytket` extension modules, we are often asked why to use one over another. Surely, any two simulators are equivalent if they are able to sample the circuits in the same way, right? Not quite. In this notebook we go through each of the simulators in turn and describe what sets them apart from others and how to make use of any unique features.
\n", "
\n", "But first, to demonstrate the significant overlap in functionality, we'll just give some examples of common usage for different types of backends."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Sampling simulator usage"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3, 3)\n", "c.Ry(0.7, 0)\n", "c.CX(0, 1)\n", "c.X(2)\n", "c.measure_all()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()\n", "c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c, n_shots=2000)\n", "counts = backend.get_result(handle).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Statevector simulator usage"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerStateBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Build a quantum state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.H(0).CX(0, 1)\n", "c.Rz(0.3, 0)\n", "c.Rz(-0.3, 1)\n", "c.Ry(0.8, 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Examine the statevector:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerStateBackend()\n", "c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c)\n", "state = backend.get_result(handle).get_state()\n", "print(state)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Expectation value usage"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, Qubit\n", "from pytket.extensions.qiskit import AerBackend, AerStateBackend\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Build a quantum state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.H(0).CX(0, 1)\n", "c.Rz(0.3, 0)\n", "c.Rz(-0.3, 1)\n", "c.Ry(0.8, 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define the measurement operator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["xxi = QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X})\n", "zzz = QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z, Qubit(2): Pauli.Z})\n", "op = QubitPauliOperator({xxi: -1.8, zzz: 0.7})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()\n", "c = backend.get_compiled_circuit(c)\n", "exp = backend.get_operator_expectation_value(c, op)\n", "print(exp)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qiskit.AerBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`AerBackend` wraps up the `qasm_simulator` from the Qiskit Aer package. It supports an extremely flexible set of circuits and uses many effective simulation methods making it a great all-purpose sampling simulator.
\n", "
\n", "Unique features:
\n", "- supports mid-circuit measurement and OpenQASM-style conditional gates;
\n", "- encompasses a variety of underlying simulation methods and automatically selects the best one for each circuit (including statevector, density matrix, (extended) stabilizer and matrix product state);
\n", "- can be provided with a `qiskit.providers.Aer.noise.NoiseModel` on instantiation to perform a noisy simulation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or `QubitPauliOperator`s."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerBackend\n", "from itertools import combinations\n", "from qiskit.providers.aer.noise import NoiseModel, depolarizing_error"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Quantum teleportation circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit()\n", "alice = c.add_q_register(\"a\", 2)\n", "bob = c.add_q_register(\"b\", 1)\n", "data = c.add_c_register(\"d\", 2)\n", "final = c.add_c_register(\"f\", 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Start in an interesting state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.Rx(0.3, alice[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Set up a Bell state between Alice and Bob:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.H(alice[1]).CX(alice[1], bob[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Measure Alice's qubits in the Bell basis:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.CX(alice[0], alice[1]).H(alice[0])\n", "c.Measure(alice[0], data[0])\n", "c.Measure(alice[1], data[1])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Correct Bob's qubit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.X(bob[0], condition_bits=[data[0], data[1]], condition_value=1)\n", "c.X(bob[0], condition_bits=[data[0], data[1]], condition_value=3)\n", "c.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=2)\n", "c.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=3)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Measure Bob's qubit to observe the interesting state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.Measure(bob[0], final[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Set up a noisy simulator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["model = NoiseModel()\n", "dep_err = depolarizing_error(0.04, 2)\n", "for i, j in combinations(range(3), r=2):\n", " model.add_quantum_error(dep_err, [\"cx\"], [i, j])\n", " model.add_quantum_error(dep_err, [\"cx\"], [j, i])\n", "backend = AerBackend(noise_model=model)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c, n_shots=2000)\n", "result = backend.get_result(handle)\n", "counts = result.get_counts([final[0]])\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qiskit.AerStateBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`AerStateBackend` provides access to Qiskit Aer's `statevector_simulator`. It supports a similarly large gate set and has competitive speed for statevector simulations.
\n", "
\n", "Useful features:
\n", "- no dependency on external executables, making it easy to install and run on any computer;
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or `QubitPauliOperator`s."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qiskit.AerUnitaryBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Finishing the set of simulators from Qiskit Aer, `AerUnitaryBackend` captures the `unitary_simulator`, allowing for the entire unitary of a pure quantum process to be calculated. This is especially useful for testing small subcircuits that will be used many times in a larger computation.
\n", "
\n", "Unique features:
\n", "- provides the full unitary matrix for a pure quantum circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerUnitaryBackend\n", "from pytket.predicates import NoClassicalControlPredicate"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a simple quantum incrementer:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.CCX(2, 1, 0)\n", "c.CX(2, 1)\n", "c.X(2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Examine the unitary:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerUnitaryBackend()\n", "c = backend.get_compiled_circuit(c)\n", "result = backend.run_circuit(c)\n", "unitary = result.get_unitary()\n", "print(unitary.round(1).real)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.pyquil.ForestBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Whilst it can, with suitable credentials, be used to access the Rigetti QPUs, the `ForestBackend` also features a simulator mode which turns it into a noiseless sampling simulator that matches the constraints of the simulated device (e.g. the same gate set, restricted connectivity, measurement model, etc.). This is useful when playing around with custom compilation strategies to ensure that your final circuits are suitable to run on the device and for checking that your overall program works fine before you invest in reserving a QPU.
\n", "
\n", "Unique features:
\n", "- faithful recreation of the circuit constraints of Rigetti QPUs."]}, {"cell_type": "markdown", "metadata": {}, "source": ["If trying to use the `ForestBackend` locally (i.e. not on a Rigetti QMI), you will need to have `quilc` and `qvm` running as separate processes in server mode. One easy way of doing this is with `docker` (see the `quilc` and `qvm` documentation for alternative methods of running them):
\n", "`docker run --rm -it -p 5555:5555 rigetti/quilc -R`
\n", "`docker run --rm -it -p 5000:5000 rigetti/qvm -S`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.pyquil.ForestStateBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Rigetti `pyquil` package also provides the `WavefunctionSimulator`, which we present as the `ForestStateBackend`. Functionally, it is very similar to the `AerStateBackend` so can be used interchangeably. It does require that `quilc` and `qvm` are running as separate processes when not running on a Rigetti QMI.
\n", "
\n", "Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qsharp.QsharpSimulatorBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QsharpSimulatorBackend` is another basic sampling simulator that is interchangeable with others, using the Microsoft QDK simulator. Note that the `pytket-qsharp` package is dependent on the `dotnet` SDK and `iqsharp` tool. Please consult the `pytket-qsharp` installation instructions for recommendations."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qsharp.QsharpToffoliSimulatorBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Toffoli circuits form a strict fragment of quantum circuits and can be efficiently simulated. The `QsharpToffoliSimulatorBackend` can only operate on these circuits, but scales much better with system size than regular simulators.
\n", "
\n", "Unique features:
\n", "- efficient simulation of Toffoli circuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qsharp import QsharpToffoliSimulatorBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a circuit - start in a basis state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.X(0).X(2)\n", "# Define a circuit - incrementer\n", "c.CCX(2, 1, 0)\n", "c.CX(2, 1)\n", "c.X(2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = QsharpToffoliSimulatorBackend()\n", "c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c, n_shots=10)\n", "counts = backend.get_result(handle).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qsharp.QsharpEstimatorBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QsharpEstimatorBackend` is not strictly a simulator, as it doesn't model the state of the quantum system and try to identify the final state, but instead analyses the circuit to estimate the required resources to run it. It does not support any of the regular outcome types (e.g. shots, counts, statevector), just the summary of the estimated resources.
\n", "
\n", "Unique features:
\n", "- estimates resources to perform the circuit, without actually simulating/running it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "markdown", "metadata": {}, "source": ["from pytket.extensions.qsharp import QsharpEstimatorBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a circuit - start in a basis state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.X(0).X(2)\n", "# Define a circuit - incrementer\n", "c.CCX(2, 1, 0)\n", "c.CX(2, 1)\n", "c.X(2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "markdown", "metadata": {}, "source": ["(disabled because of https://github.com/CQCL/pytket-qsharp/issues/37)
\n", "backend = QsharpEstimatorBackend()
\n", "c = backend.get_compiled_circuit(c)
\n", "handle = backend.process_circuit(c, n_shots=10)
\n", "resources = backend.get_resources(handle)
\n", "print(resources)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qulacs.QulacsBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QulacsBackend` is an all-purpose simulator with both sampling and statevector modes, using the basic CPU simulator from Qulacs.
\n", "
\n", "Unique features:
\n", "- supports both sampling (shots/counts) and complete statevector outputs."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.qulacs.QulacsGPUBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["If the GPU version of Qulacs is installed, the `QulacsGPUBackend` will use that to benefit from even faster speeds. It is very easy to get started with using a GPU, as it only requires a CUDA installation and the `qulacs-gpu` package from `pip`. Functionally, it is identical to the `QulacsBackend`, but potentially faster if you have GPU resources available.
\n", "
\n", "Unique features:
\n", "- GPU support for very fast simulation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `pytket.extensions.projectq.ProjectQBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["ProjectQ is a popular quantum circuit simulator, thanks to its availability and ease of use. It provides a similar level of performance and features to `AerStateBackend`.
\n", "
\n", "Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Comparison of the simulators available through TKET"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this tutorial, we will focus on:
\n", "- exploring the wide array of simulators available through the extension modules for `pytket`;
\n", "- comparing their unique features and capabilities."]}, {"cell_type": "markdown", "metadata": {}, "source": ["This example assumes the reader is familiar with the basics of circuit construction and evaluation.
\n", "
\n", "To run every option in this example, you will need `pytket`, `pytket-qiskit`, `pytket-pyquil`, `pytket-qsharp`, `pytket-qulacs`, and `pytket-projectq`.
\n", "
\n", "With the number of simulator `Backend`s available across the `pytket` extension modules, we are often asked why to use one over another. Surely, any two simulators are equivalent if they are able to sample the circuits in the same way, right? Not quite. In this notebook we go through each of the simulators in turn and describe what sets them apart from others and how to make use of any unique features.
\n", "
\n", "But first, to demonstrate the significant overlap in functionality, we'll just give some examples of common usage for different types of backends."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Sampling simulator usage"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3, 3)\n", "c.Ry(0.7, 0)\n", "c.CX(0, 1)\n", "c.X(2)\n", "c.measure_all()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()\n", "c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c, n_shots=2000)\n", "counts = backend.get_result(handle).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Statevector simulator usage"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerStateBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Build a quantum state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.H(0).CX(0, 1)\n", "c.Rz(0.3, 0)\n", "c.Rz(-0.3, 1)\n", "c.Ry(0.8, 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Examine the statevector:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerStateBackend()\n", "c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c)\n", "state = backend.get_result(handle).get_state()\n", "print(state)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Expectation value usage"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, Qubit\n", "from pytket.extensions.qiskit import AerBackend, AerStateBackend\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Build a quantum state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.H(0).CX(0, 1)\n", "c.Rz(0.3, 0)\n", "c.Rz(-0.3, 1)\n", "c.Ry(0.8, 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define the measurement operator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["xxi = QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X})\n", "zzz = QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z, Qubit(2): Pauli.Z})\n", "op = QubitPauliOperator({xxi: -1.8, zzz: 0.7})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()\n", "c = backend.get_compiled_circuit(c)\n", "exp = backend.get_operator_expectation_value(c, op)\n", "print(exp)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `AerBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`AerBackend` wraps up the `qasm_simulator` from the Qiskit Aer package. It supports an extremely flexible set of circuits and uses many effective simulation methods making it a great all-purpose sampling simulator.
\n", "
\n", "Unique features:
\n", "- supports mid-circuit measurement and OpenQASM-style conditional gates;
\n", "- encompasses a variety of underlying simulation methods and automatically selects the best one for each circuit (including statevector, density matrix, (extended) stabilizer and matrix product state);
\n", "- can be provided with a `qiskit.providers.Aer.noise.NoiseModel` on instantiation to perform a noisy simulation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or `QubitPauliOperator`s."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerBackend\n", "from itertools import combinations\n", "from qiskit.providers.aer.noise import NoiseModel, depolarizing_error"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Quantum teleportation circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit()\n", "alice = c.add_q_register(\"a\", 2)\n", "bob = c.add_q_register(\"b\", 1)\n", "data = c.add_c_register(\"d\", 2)\n", "final = c.add_c_register(\"f\", 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Start in an interesting state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.Rx(0.3, alice[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Set up a Bell state between Alice and Bob:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.H(alice[1]).CX(alice[1], bob[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Measure Alice's qubits in the Bell basis:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.CX(alice[0], alice[1]).H(alice[0])\n", "c.Measure(alice[0], data[0])\n", "c.Measure(alice[1], data[1])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Correct Bob's qubit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.X(bob[0], condition_bits=[data[0], data[1]], condition_value=1)\n", "c.X(bob[0], condition_bits=[data[0], data[1]], condition_value=3)\n", "c.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=2)\n", "c.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=3)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Measure Bob's qubit to observe the interesting state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.Measure(bob[0], final[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Set up a noisy simulator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["model = NoiseModel()\n", "dep_err = depolarizing_error(0.04, 2)\n", "for i, j in combinations(range(3), r=2):\n", " model.add_quantum_error(dep_err, [\"cx\"], [i, j])\n", " model.add_quantum_error(dep_err, [\"cx\"], [j, i])\n", "backend = AerBackend(noise_model=model)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c, n_shots=2000)\n", "result = backend.get_result(handle)\n", "counts = result.get_counts([final[0]])\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `AerStateBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["`AerStateBackend` provides access to Qiskit Aer's `statevector_simulator`. It supports a similarly large gate set and has competitive speed for statevector simulations.
\n", "
\n", "Useful features:
\n", "- no dependency on external executables, making it easy to install and run on any computer;
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or `QubitPauliOperator`s."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `AerUnitaryBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Finishing the set of simulators from Qiskit Aer, `AerUnitaryBackend` captures the `unitary_simulator`, allowing for the entire unitary of a pure quantum process to be calculated. This is especially useful for testing small subcircuits that will be used many times in a larger computation.
\n", "
\n", "Unique features:
\n", "- provides the full unitary matrix for a pure quantum circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qiskit import AerUnitaryBackend\n", "from pytket.predicates import NoClassicalControlPredicate"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a simple quantum incrementer:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.CCX(2, 1, 0)\n", "c.CX(2, 1)\n", "c.X(2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Examine the unitary:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerUnitaryBackend()\n", "c = backend.get_compiled_circuit(c)\n", "result = backend.run_circuit(c)\n", "unitary = result.get_unitary()\n", "print(unitary.round(1).real)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `ForestBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Whilst it can, with suitable credentials, be used to access the Rigetti QPUs, the `ForestBackend` also features a simulator mode which turns it into a noiseless sampling simulator that matches the constraints of the simulated device (e.g. the same gate set, restricted connectivity, measurement model, etc.). This is useful when playing around with custom compilation strategies to ensure that your final circuits are suitable to run on the device and for checking that your overall program works fine before you invest in reserving a QPU.
\n", "
\n", "Unique features:
\n", "- faithful recreation of the circuit constraints of Rigetti QPUs."]}, {"cell_type": "markdown", "metadata": {}, "source": ["If trying to use the `ForestBackend` locally (i.e. not on a Rigetti QMI), you will need to have `quilc` and `qvm` running as separate processes in server mode. One easy way of doing this is with `docker` (see the `quilc` and `qvm` documentation for alternative methods of running them):
\n", "`docker run --rm -it -p 5555:5555 rigetti/quilc -R`
\n", "`docker run --rm -it -p 5000:5000 rigetti/qvm -S`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `ForestStateBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The Rigetti `pyquil` package also provides the `WavefunctionSimulator`, which we present as the `ForestStateBackend`. Functionally, it is very similar to the `AerStateBackend` so can be used interchangeably. It does require that `quilc` and `qvm` are running as separate processes when not running on a Rigetti QMI.
\n", "
\n", "Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `QsharpSimulatorBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QsharpSimulatorBackend` is another basic sampling simulator that is interchangeable with others, using the Microsoft QDK simulator. Note that the `pytket-qsharp` package is dependent on the `dotnet` SDK and `iqsharp` tool. Please consult the `pytket-qsharp` installation instructions for recommendations."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `QsharpToffoliSimulatorBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Toffoli circuits form a strict fragment of quantum circuits and can be efficiently simulated. The `QsharpToffoliSimulatorBackend` can only operate on these circuits, but scales much better with system size than regular simulators.
\n", "
\n", "Unique features:
\n", "- efficient simulation of Toffoli circuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.extensions.qsharp import QsharpToffoliSimulatorBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a circuit - start in a basis state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.X(0).X(2)\n", "# Define a circuit - incrementer\n", "c.CCX(2, 1, 0)\n", "c.CX(2, 1)\n", "c.X(2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = QsharpToffoliSimulatorBackend()\n", "c = backend.get_compiled_circuit(c)\n", "handle = backend.process_circuit(c, n_shots=10)\n", "counts = backend.get_result(handle).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `QsharpEstimatorBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QsharpEstimatorBackend` is not strictly a simulator, as it doesn't model the state of the quantum system and try to identify the final state, but instead analyses the circuit to estimate the required resources to run it. It does not support any of the regular outcome types (e.g. shots, counts, statevector), just the summary of the estimated resources.
\n", "
\n", "Unique features:
\n", "- estimates resources to perform the circuit, without actually simulating/running it."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "markdown", "metadata": {}, "source": ["from pytket.extensions.qsharp import QsharpEstimatorBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Define a circuit - start in a basis state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3)\n", "c.X(0).X(2)\n", "# Define a circuit - incrementer\n", "c.CCX(2, 1, 0)\n", "c.CX(2, 1)\n", "c.X(2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run on the backend:"]}, {"cell_type": "markdown", "metadata": {}, "source": ["(disabled because of https://github.com/CQCL/pytket-qsharp/issues/37)
\n", "backend = QsharpEstimatorBackend()
\n", "c = backend.get_compiled_circuit(c)
\n", "handle = backend.process_circuit(c, n_shots=10)
\n", "resources = backend.get_resources(handle)
\n", "print(resources)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `QulacsBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `QulacsBackend` is an all-purpose simulator with both sampling and statevector modes, using the basic CPU simulator from Qulacs.
\n", "
\n", "Unique features:
\n", "- supports both sampling (shots/counts) and complete statevector outputs."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `QulacsGPUBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["If the GPU version of Qulacs is installed, the `QulacsGPUBackend` will use that to benefit from even faster speeds. It is very easy to get started with using a GPU, as it only requires a CUDA installation and the `qulacs-gpu` package from `pip`. Functionally, it is identical to the `QulacsBackend`, but potentially faster if you have GPU resources available.
\n", "
\n", "Unique features:
\n", "- GPU support for very fast simulation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["## `ProjectQBackend`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["ProjectQ is a popular quantum circuit simulator, thanks to its availability and ease of use. It provides a similar level of performance and features to `AerStateBackend`.
\n", "
\n", "Useful features:
\n", "- support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/conditional_gate_example.ipynb b/examples/conditional_gate_example.ipynb
index 108d62de..a481875c 100644
--- a/examples/conditional_gate_example.ipynb
+++ b/examples/conditional_gate_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Conditional Execution"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Whilst any quantum process can be created by performing \"pure\" operations delaying all measurements to the end, this is not always practical and can greatly increase the resource requirements. It is much more convenient to alternate quantum gates and measurements, especially if we can use the measurement results to determine which gates to apply (we refer to this more generic circuit model as \"mixed\" circuits, against the usual \"pure\" circuits). This is especially crucial for error correcting codes, where the correction gates are applied only if an error is detected.
\n", "
\n", "Measurements on many NISQ devices are often slow and it is hard to maintain other qubits in a quantum state during the measurement operation. Hence they may only support a single round of measurements at the end of the circuit, removing the need for conditional gate support. However, the ability to work with mid-circuit measurement and conditional gates is a feature in high demand for the future, and tket is ready for it.
\n", "
\n", "Not every circuit language specification supports conditional gates in the same way. The most popular circuit model at the moment is that provided by the OpenQASM language. This permits a very restricted model of classical logic, where we can apply a gate conditionally on the exact value of a classical register. There is no facility in the current spec for Boolean logic or classical operations to apply any function to the value prior to the equality check.
\n", "
\n", "For example, quantum teleportation can be performed by the following QASM:
\n", "`OPENQASM 2.0;`
\n", "`include \"qelib1.inc\";`
\n", "`qreg a[2];`
\n", "`qreg b[1];`
\n", "`creg c[2];`
\n", "`// Bell state between Alice and Bob`
\n", "`h a[1];`
\n", "`cx a[1],b[0];`
\n", "`// Bell measurement of Alice's qubits`
\n", "`cx a[0],a[1];`
\n", "`h a[0];`
\n", "`measure a[0] -> c[0];`
\n", "`measure a[1] -> c[1];`
\n", "`// Correction of Bob's qubit`
\n", "`if(c==1) z b[0];`
\n", "`if(c==3) z b[0];`
\n", "`if(c==2) x b[0];`
\n", "`if(c==3) x b[0];`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["tket supports a slightly more general form of conditional gates, where the gate is applied conditionally on the exact value of any list of bits. When adding a gate to a `Circuit` object, pass in the kwargs `condition_bits` and `condition_value` and the gate will only be applied if the state of the bits yields the binary representation of the value."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit()\n", "alice = c.add_q_register(\"a\", 2)\n", "bob = c.add_q_register(\"b\", 1)\n", "cr = c.add_c_register(\"c\", 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Bell state between Alice and Bob:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.H(alice[1])\n", "c.CX(alice[1], bob[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Bell measurement of Alice's qubits:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.CX(alice[0], alice[1])\n", "c.H(alice[0])\n", "c.Measure(alice[0], cr[0])\n", "c.Measure(alice[1], cr[1])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Correction of Bob's qubit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.Z(bob[0], condition_bits=[cr[0]], condition_value=1)\n", "c.X(bob[0], condition_bits=[cr[1]], condition_value=1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Performing individual gates conditionally is sufficient, but can get cumbersome for larger circuits. Fortunately, tket's Box structures can also be performed conditionally, enabling this to be applied to large circuits with ease.
\n", "
\n", "For the sake of example, assume our device struggles to perform $X$ gates. We can surround it by $CX$ gates onto an ancilla, so measuring the ancilla will either result in the identity or $X$ being applied to the target qubit. If we detect that the $X$ fails, we can retry."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import CircBox, Qubit, Bit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["checked_x = Circuit(2, 1)\n", "checked_x.CX(0, 1)\n", "checked_x.X(0)\n", "checked_x.CX(0, 1)\n", "checked_x.Measure(1, 0)\n", "x_box = CircBox(checked_x)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit()\n", "target = Qubit(\"t\", 0)\n", "ancilla = Qubit(\"a\", 0)\n", "success = Bit(\"s\", 0)\n", "c.add_qubit(target)\n", "c.add_qubit(ancilla)\n", "c.add_bit(success)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Try the X gate:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c.add_circbox(x_box, args=[target, ancilla, success])\n", "# Try again if the X failed\n", "c.add_circbox(\n", " x_box, args=[target, ancilla, success], condition_bits=[success], condition_value=0\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["tket is able to apply essential compilation passes on circuits containing conditional gates. This includes decomposing any boxes into primitive gates and rebasing to other gatesets whilst preserving the conditional data."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import DecomposeBoxes, RebaseTket, SequencePass"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["comp_pass = SequencePass([DecomposeBoxes(), RebaseTket()])"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["comp_pass.apply(c)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A tket circuit can be converted to OpenQASM or other languages following the same classical model (e.g. Qiskit) when all conditional gates are dependent on the exact state of a single, whole classical register."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import tk_to_qiskit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["qc = tk_to_qiskit(c)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(qc)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This allows us to test our mixed programs using the `AerBackend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import AerBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(2, 1)\n", "c.Rx(0.3, 0)\n", "c.Measure(0, 0)\n", "# Set qubit 1 to be the opposite result and measure\n", "c.X(1, condition_bits=[0], condition_value=0)\n", "c.Measure(1, 0)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()\n", "c = backend.get_compiled_circuit(c)\n", "counts = backend.run_circuit(c, 1024).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Try out mid-circuit measurement and conditional gate support on the `AerBackend` simulator, or ask about accessing the `QuantinuumBackend` to try on a hardware device."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Conditional gates"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Whilst any quantum process can be created by performing \"pure\" operations delaying all measurements to the end, this is not always practical and can greatly increase the resource requirements. It is much more convenient to alternate quantum gates and measurements, especially if we can use the measurement results to determine which gates to apply (we refer to this more generic circuit model as \"mixed\" circuits, against the usual \"pure\" circuits). This is especially crucial for error correcting codes, where the correction gates are applied only if an error is detected.
\n", "
\n", "Measurements on many NISQ devices are often slow and it is hard to maintain other qubits in a quantum state during the measurement operation. Hence they may only support a single round of measurements at the end of the circuit, removing the need for conditional gate support. However, the ability to work with mid-circuit measurement and conditional gates is a feature in high demand for the future, and tket is ready for it.
\n", "
\n", "Not every circuit language specification supports conditional gates in the same way. The most popular circuit model at the moment is that provided by the OpenQASM language. This permits a very restricted model of classical logic, where we can apply a gate conditionally on the exact value of a classical register. There is no facility in the current spec for Boolean logic or classical operations to apply any function to the value prior to the equality check.
\n", "
\n", "For example, quantum teleportation can be performed by the following QASM:
\n", "`OPENQASM 2.0;`
\n", "`include \"qelib1.inc\";`
\n", "`qreg a[2];`
\n", "`qreg b[1];`
\n", "`creg c[2];`
\n", "`// Bell state between Alice and Bob`
\n", "`h a[1];`
\n", "`cx a[1],b[0];`
\n", "`// Bell measurement of Alice's qubits`
\n", "`cx a[0],a[1];`
\n", "`h a[0];`
\n", "`measure a[0] -> c[0];`
\n", "`measure a[1] -> c[1];`
\n", "`// Correction of Bob's qubit`
\n", "`if(c==1) z b[0];`
\n", "`if(c==3) z b[0];`
\n", "`if(c==2) x b[0];`
\n", "`if(c==3) x b[0];`"]}, {"cell_type": "markdown", "metadata": {}, "source": ["tket supports a slightly more general form of conditional gates, where the gate is applied conditionally on the exact value of any list of bits. When adding a gate to a `Circuit` object, pass in the kwargs `condition_bits` and `condition_value` and the gate will only be applied if the state of the bits yields the binary representation of the value."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit()\n", "alice = circ.add_q_register(\"a\", 2)\n", "bob = circ.add_q_register(\"b\", 1)\n", "cr = circ.add_c_register(\"c\", 2)\n", "# Bell state between Alice and Bob:\n", "circ.H(alice[1])\n", "circ.CX(alice[1], bob[0])\n", "# Bell measurement of Alice's qubits:\n", "circ.CX(alice[0], alice[1])\n", "circ.H(alice[0])\n", "circ.Measure(alice[0], cr[0])\n", "circ.Measure(alice[1], cr[1])\n", "# Correction of Bob's qubit:\n", "circ.Z(bob[0], condition_bits=[cr[0]], condition_value=1)\n", "circ.X(bob[0], condition_bits=[cr[1]], condition_value=1)\n", "render_circuit_jupyter(circ)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Performing individual gates conditionally is sufficient, but can get cumbersome for larger circuits. Fortunately, tket's Box structures can also be performed conditionally, enabling this to be applied to large circuits with ease.
\n", "
\n", "For the sake of example, assume our device struggles to perform $X$ gates. We can surround it by $CX$ gates onto an ancilla, so measuring the ancilla will either result in the identity or $X$ being applied to the target qubit. If we detect that the $X$ fails, we can retry."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import CircBox, Qubit, Bit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["checked_x = Circuit(2, 1)\n", "checked_x.CX(0, 1)\n", "checked_x.X(0)\n", "checked_x.CX(0, 1)\n", "checked_x.Measure(1, 0)\n", "x_box = CircBox(checked_x)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ2 = Circuit()\n", "target = Qubit(\"t\", 0)\n", "ancilla = Qubit(\"a\", 0)\n", "success = Bit(\"s\", 0)\n", "circ2.add_qubit(target)\n", "circ2.add_qubit(ancilla)\n", "circ2.add_bit(success)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Try the X gate:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ2.add_circbox(x_box, args=[target, ancilla, success])\n", "# Try again if the X failed\n", "circ2.add_circbox(\n", " x_box, args=[target, ancilla, success], condition_bits=[success], condition_value=0\n", ")\n", "render_circuit_jupyter(circ2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["tket is able to apply essential compilation passes on circuits containing conditional gates. This includes decomposing any boxes into primitive gates and rebasing to other gatesets whilst preserving the conditional data."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import DecomposeBoxes, RebaseTket, SequencePass"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["comp_pass = SequencePass([DecomposeBoxes(), RebaseTket()])\n", "comp_pass.apply(circ2)\n", "render_circuit_jupyter(circ2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A tket circuit can be converted to OpenQASM or other languages following the same classical model (e.g. Qiskit) when all conditional gates are dependent on the exact state of a single, whole classical register."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import tk_to_qiskit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["qc = tk_to_qiskit(circ2)\n", "print(qc)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This allows us to test our mixed programs using the `AerBackend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import AerBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ3 = Circuit(2, 1)\n", "circ3.Rx(0.3, 0)\n", "circ3.Measure(0, 0)\n", "# Set qubit 1 to be the opposite result and measure\n", "circ3.X(1, condition_bits=[0], condition_value=0)\n", "circ3.Measure(1, 0)\n", "backend = AerBackend()\n", "compiled_circ = backend.get_compiled_circuit(circ3)\n", "render_circuit_jupyter(compiled_circ)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["counts = backend.run_circuit(compiled_circ, 1024).get_counts()\n", "print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Try out mid-circuit measurement and conditional gate support on the `AerBackend` simulator, or ask about accessing the `QuantinuumBackend` to try on a hardware device."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/entanglement_swapping.ipynb b/examples/entanglement_swapping.ipynb
index 6592a721..b5dfd1ad 100644
--- a/examples/entanglement_swapping.ipynb
+++ b/examples/entanglement_swapping.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Iterated Entanglement Swapping using tket"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this tutorial, we will focus on:
\n", "- designing circuits with mid-circuit measurement and conditional gates;
\n", "- utilising noise models in supported simulators."]}, {"cell_type": "markdown", "metadata": {}, "source": ["This example assumes the reader is familiar with the Qubit Teleportation and Entanglement Swapping protocols, and basic models of noise in quantum devices.
\n", "
\n", "To run this example, you will need `pytket`, `pytket-qiskit`, and `plotly` (installed via `pip`). To view the graphs, you will need an intallation of `plotly-orca`.
\n", "
\n", "Current quantum hardware fits into the NISQ (Noisy, Intermediate-Scale Quantum) regime. This noise cannot realistically be combatted using conventional error correcting codes, because of the lack of available qubits, noise levels exceeding the code thresholds, and very few devices available that can perform measurements and corrections mid-circuit. Analysis of how quantum algorithms perform under noisy conditions is a very active research area, as is finding ways to cope with it. Here, we will look at how well we can perform the Entanglement Swapping protocol with different noise levels.
\n", "
\n", "The Entanglement Swapping protocol requires two parties to share Bell pairs with a third party, who applies the Qubit Teleportation protocol to generate a Bell pair between the two parties. The Qubit Teleportation step requires us to be able to measure some qubits and make subsequent corrections to the remaining qubits. There are only a handful of simulators and devices that currently support this, with others restricted to only measuring the qubits at the end of the circuit.
\n", "
\n", "The most popular circuit model with conditional gates at the moment is that provided by the OpenQASM language. This permits a very restricted model of classical logic, where we can apply a gate conditionally on the exact value of a classical register. There is no facility in the current spec for Boolean logic or classical operations to apply any function to the value prior to the equality check. For example, Qubit Teleportation can be performed by the following QASM:
\n", "`OPENQASM 2.0;`
\n", "`include \"qelib1.inc\";`
\n", "`qreg a[2];`
\n", "`qreg b[1];`
\n", "`creg c[2];`
\n", "`// Bell state between Alice and Bob`
\n", "`h a[1];`
\n", "`cx a[1],b[0];`
\n", "`// Bell measurement of Alice's qubits`
\n", "`cx a[0],a[1];`
\n", "`h a[0];`
\n", "`measure a[0] -> c[0];`
\n", "`measure a[1] -> c[1];`
\n", "`// Correction of Bob's qubit`
\n", "`if(c==1) z b[0];`
\n", "`if(c==3) z b[0];`
\n", "`if(c==2) x b[0];`
\n", "`if(c==3) x b[0];`
\n", "
\n", "This corresponds to the following `pytket` code:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["qtel = Circuit()\n", "alice = qtel.add_q_register(\"a\", 2)\n", "bob = qtel.add_q_register(\"b\", 1)\n", "data = qtel.add_c_register(\"d\", 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Bell state between Alice and Bob:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["qtel.H(alice[1])\n", "qtel.CX(alice[1], bob[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Bell measurement of Alice's qubits:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["qtel.CX(alice[0], alice[1])\n", "qtel.H(alice[0])\n", "qtel.Measure(alice[0], data[0])\n", "qtel.Measure(alice[1], data[1])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Correction of Bob's qubit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["qtel.X(bob[0], condition_bits=[data[0], data[1]], condition_value=2)\n", "qtel.X(bob[0], condition_bits=[data[0], data[1]], condition_value=3)\n", "qtel.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=1)\n", "qtel.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=3)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["So to demonstrate the Entanglement Swapping protocol, we just need to run this on one side of a Bell pair."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["es = Circuit()\n", "ava = es.add_q_register(\"a\", 1)\n", "bella = es.add_q_register(\"b\", 2)\n", "charlie = es.add_q_register(\"c\", 1)\n", "data = es.add_c_register(\"d\", 2)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Bell state between Ava and Bella:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["es.H(ava[0])\n", "es.CX(ava[0], bella[0])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Teleport `bella[0]` to `charlie[0]`:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["tel_to_c = qtel.copy()\n", "tel_to_c.rename_units({alice[0]: bella[0], alice[1]: bella[1], bob[0]: charlie[0]})\n", "es.append(tel_to_c)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(es.get_commands())"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's start by running a noiseless simulation of this to verify that what we get looks like a Bell pair."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import AerBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Connect to a simulator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Make a ZZ measurement of the Bell pair:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["bell_test = es.copy()\n", "bell_test.Measure(ava[0], data[0])\n", "bell_test.Measure(charlie[0], data[1])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Run the experiment:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["bell_test = backend.get_compiled_circuit(bell_test)\n", "from pytket.circuit.display import render_circuit_jupyter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(bell_test)\n", "handle = backend.process_circuit(bell_test, n_shots=2000)\n", "counts = backend.get_result(handle).get_counts()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(counts)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is good, we have got roughly 50/50 measurement results of 00 and 11 under the ZZ operator. But there are many other states beyond the Bell state that also generate this distribution, so to gain more confidence in our claim about the state we should make more measurements that also characterise it, i.e. perform state tomography.
\n", "
\n", "Here, we will demonstrate a naive approach to tomography that makes 3^n measurement circuits for an n-qubit state. More elaborate methods also exist."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils import append_pauli_measurement, probs_from_counts\n", "from itertools import product\n", "from scipy.linalg import lstsq, eigh\n", "import numpy as np"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def gen_tomography_circuits(state, qubits, bits):\n", " # Yields {X, Y, Z}^n measurements in lexicographical order\n", " # Only measures qubits, storing the result in bits\n", " # (since we don't care about the ancilla qubits)\n", " assert len(qubits) == len(bits)\n", " for paulis in product([Pauli.X, Pauli.Y, Pauli.Z], repeat=len(qubits)):\n", " circ = state.copy()\n", " for qb, b, p in zip(qubits, bits, paulis):\n", " if p == Pauli.X:\n", " circ.H(qb)\n", " elif p == Pauli.Y:\n", " circ.V(qb)\n", " circ.Measure(qb, b)\n", " yield circ"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def run_tomography_circuits(state, qubits, bits, backend):\n", " circs = list(gen_tomography_circuits(state, qubits, bits))\n", " # Compile and run each circuit\n", " circs = backend.get_compiled_circuits(circs)\n", " handles = backend.process_circuits(circs, n_shots=2000)\n", " # Get the observed measurement probabilities\n", " probs_list = []\n", " for result in backend.get_results(handles):\n", " counts = result.get_counts()\n", " probs = probs_from_counts(counts)\n", " probs_list.append(probs)\n", " return probs_list"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def fit_tomography_outcomes(probs_list, n_qbs):\n", " # Define the density matrices for the basis states\n", " basis = dict()\n", " basis[(Pauli.X, 0)] = np.asarray([[0.5, 0.5], [0.5, 0.5]])\n", " basis[(Pauli.X, 1)] = np.asarray([[0.5, -0.5], [-0.5, 0.5]])\n", " basis[(Pauli.Y, 0)] = np.asarray([[0.5, -0.5j], [0.5j, 0.5]])\n", " basis[(Pauli.Y, 1)] = np.asarray([[0.5, 0.5j], [-0.5j, 0.5]])\n", " basis[(Pauli.Z, 0)] = np.asarray([[1, 0], [0, 0]])\n", " basis[(Pauli.Z, 1)] = np.asarray([[0, 0], [0, 1]])\n", " dim = 2**n_qbs\n", " # Define vector all_probs as a concatenation of probability vectors for each measurement (2**n x 3**n, 1)\n", " # Define matrix all_ops mapping a (vectorised) density matrix to a vector of probabilities for each measurement\n", " # (2**n x 3**n, 2**n x 2**n)\n", " all_probs = []\n", " all_ops = []\n", " for paulis, probs in zip(\n", " product([Pauli.X, Pauli.Y, Pauli.Z], repeat=n_qbs), probs_list\n", " ):\n", " prob_vec = []\n", " meas_ops = []\n", " for outcome in product([0, 1], repeat=n_qbs):\n", " prob_vec.append(probs.get(outcome, 0))\n", " op = np.eye(1, dtype=complex)\n", " for p, o in zip(paulis, outcome):\n", " op = np.kron(op, basis[(p, o)])\n", " meas_ops.append(op.reshape(1, dim * dim).conj())\n", " all_probs.append(np.vstack(prob_vec))\n", " all_ops.append(np.vstack(meas_ops))\n", " # Solve for density matrix by minimising || all_ops * dm - all_probs ||\n", " dm, _, _, _ = lstsq(np.vstack(all_ops), np.vstack(all_probs))\n", " dm = dm.reshape(dim, dim)\n", " # Make density matrix positive semi-definite\n", " v, w = eigh(dm)\n", " for i in range(dim):\n", " if v[i] < 0:\n", " for j in range(i + 1, dim):\n", " v[j] += v[i] / (dim - (i + 1))\n", " v[i] = 0\n", " dm = np.zeros([dim, dim], dtype=complex)\n", " for j in range(dim):\n", " dm += v[j] * np.outer(w[:, j], np.conj(w[:, j]))\n", " # Normalise trace of density matrix\n", " dm /= np.trace(dm)\n", " return dm"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["probs_list = run_tomography_circuits(\n", " es, [ava[0], charlie[0]], [data[0], data[1]], backend\n", ")\n", "dm = fit_tomography_outcomes(probs_list, 2)\n", "print(dm.round(3))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is very close to the true density matrix for a pure Bell state. We can attribute the error here to the sampling error since we only take 2000 samples of each measurement circuit.
\n", "
\n", "To quantify exactly how similar it is to the correct density matrix, we can calculate the fidelity."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from scipy.linalg import sqrtm"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def fidelity(dm0, dm1):\n", " # Calculate the fidelity between two density matrices\n", " sq0 = sqrtm(dm0)\n", " sq1 = sqrtm(dm1)\n", " return np.linalg.norm(sq0.dot(sq1)) ** 2"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["bell_state = np.asarray(\n", " [\n", " [0.5, 0, 0, 0.5],\n", " [0, 0, 0, 0],\n", " [0, 0, 0, 0],\n", " [0.5, 0, 0, 0.5],\n", " ]\n", ")\n", "print(fidelity(dm, bell_state))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This high fidelity is unsurprising since we have a completely noiseless simulation. So the next step is to add some noise to the simulation and observe how the overall fidelity is affected. The `AerBackend` wraps around the Qiskit Aer simulator and can pass on any `qiskit.providers.aer.noise.NoiseModel` to the simulator. Let's start by adding some uniform depolarising noise to each CX gate and some uniform measurement error."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit.providers.aer.noise import NoiseModel, depolarizing_error, ReadoutError"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def make_noise_model(dep_err_rate, ro_err_rate, qubits):\n", " # Define a noise model that applies uniformly to the given qubits\n", " model = NoiseModel()\n", " dep_err = depolarizing_error(dep_err_rate, 2)\n", " ro_err = ReadoutError(\n", " [[1 - ro_err_rate, ro_err_rate], [ro_err_rate, 1 - ro_err_rate]]\n", " )\n", " # Add depolarising error to CX gates between any qubits (implying full connectivity)\n", " for i, j in product(qubits, repeat=2):\n", " if i != j:\n", " model.add_quantum_error(dep_err, [\"cx\"], [i, j])\n", " # Add readout error for each qubit\n", " for i in qubits:\n", " model.add_readout_error(ro_err, qubits=[i])\n", " return model"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_model = make_noise_model(0.03, 0.05, range(4))\n", "backend = AerBackend(noise_model=test_model)\n", "probs_list = run_tomography_circuits(\n", " es, [ava[0], charlie[0]], [data[0], data[1]], backend\n", ")\n", "dm = fit_tomography_outcomes(probs_list, 2)\n", "print(dm.round(3))\n", "print(fidelity(dm, bell_state))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Despite the very small circuit and the relatively small error rates, the fidelity of the final state has reduced considerably.
\n", "
\n", "As far as circuits go, the entanglement swapping protocol is little more than a toy example and is nothing close to the scale of circuits for most interesting quantum computational problems. However, it is possible to iterate the protocol many times to build up a larger computation, allowing us to see the impact of the noise at different scales."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import OpType\n", "from plotly.graph_objects import Scatter, Figure"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def iterated_entanglement_swap(n_iter):\n", " # Iterate the entanglement swapping protocol n_iter times\n", " it_es = Circuit()\n", " ava = it_es.add_q_register(\"a\", 1)\n", " bella = it_es.add_q_register(\"b\", 2)\n", " charlie = it_es.add_q_register(\"c\", 1)\n", " data = it_es.add_c_register(\"d\", 2)\n\n", " # Start with an initial Bell state\n", " it_es.H(ava[0])\n", " it_es.CX(ava[0], bella[0])\n", " for i in range(n_iter):\n", " if i % 2 == 0:\n", " # Teleport bella[0] to charlie[0] to give a Bell pair between ava[0] and charlier[0]\n", " tel_to_c = qtel.copy()\n", " tel_to_c.rename_units(\n", " {alice[0]: bella[0], alice[1]: bella[1], bob[0]: charlie[0]}\n", " )\n", " it_es.append(tel_to_c)\n", " it_es.add_gate(OpType.Reset, [bella[0]])\n", " it_es.add_gate(OpType.Reset, [bella[1]])\n", " else:\n", " # Teleport charlie[0] to bella[0] to give a Bell pair between ava[0] and bella[0]\n", " tel_to_b = qtel.copy()\n", " tel_to_b.rename_units(\n", " {alice[0]: charlie[0], alice[1]: bella[1], bob[0]: bella[0]}\n", " )\n", " it_es.append(tel_to_b)\n", " it_es.add_gate(OpType.Reset, [bella[1]])\n", " it_es.add_gate(OpType.Reset, [charlie[0]])\n", " # Return the circuit and the qubits expected to share a Bell pair\n", " if n_iter % 2 == 0:\n", " return it_es, [ava[0], bella[0]]\n", " else:\n", " return it_es, [ava[0], charlie[0]]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def iterated_noisy_experiment(dep_err_rate, ro_err_rate, max_iter):\n", " # Set up the noisy simulator with the given error rates\n", " test_model = make_noise_model(dep_err_rate, ro_err_rate, range(4))\n", " backend = AerBackend(noise_model=test_model)\n", " # Estimate the fidelity after n iterations, from 0 to max_iter (inclusive)\n", " fid_list = []\n", " for i in range(max_iter + 1):\n", " it_es, qubits = iterated_entanglement_swap(i)\n", " probs_list = run_tomography_circuits(it_es, qubits, [data[0], data[1]], backend)\n", " dm = fit_tomography_outcomes(probs_list, 2)\n", " fid = fidelity(dm, bell_state)\n", " fid_list.append(fid)\n", " return fid_list"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["fig = Figure()\n", "fig.update_layout(\n", " title=\"Iterated Entanglement Swapping under Noise (dep_err = 0.03)\",\n", " xaxis_title=\"Iterations\",\n", " xaxis=dict(range=[0, 10]),\n", " yaxis_title=\"Fidelity\",\n", ")\n", "iter_range = np.arange(11)\n", "for i in range(7):\n", " fids = iterated_noisy_experiment(0.03, 0.025 * i, 10)\n", " plot_data = Scatter(\n", " x=iter_range, y=fids, name=\"ro_err=\" + str(np.round(0.025 * i, 3))\n", " )\n", " fig.add_trace(plot_data)\n", "try:\n", " fig.show(renderer=\"svg\")\n", "except ValueError as e:\n", " print(e) # requires plotly-orca"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["fig = Figure()\n", "fig.update_layout(\n", " title=\"Iterated Entanglement Swapping under Noise (ro_err = 0.05)\",\n", " xaxis_title=\"Iterations\",\n", " xaxis=dict(range=[0, 10]),\n", " yaxis_title=\"Fidelity\",\n", ")\n", "iter_range = np.arange(11)\n", "for i in range(9):\n", " fids = iterated_noisy_experiment(0.01 * i, 0.05, 10)\n", " plot_data = Scatter(\n", " x=iter_range, y=fids, name=\"dep_err=\" + str(np.round(0.01 * i, 3))\n", " )\n", " fig.add_trace(plot_data)\n", "try:\n", " fig.show(renderer=\"svg\")\n", "except ValueError as e:\n", " print(e) # requires plotly-orca"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These graphs are not very surprising, but are still important for seeing that the current error rates of typical NISQ devices become crippling for fidelities very quickly after repeated mid-circuit measurements and corrections (even with this overly-simplified model with uniform noise and no crosstalk or higher error modes). This provides good motivation for the adoption of error mitigation techniques, and for the development of new techniques that are robust to errors in mid-circuit measurements."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Exercises:
\n", "- Vary the fixed noise levels to compare how impactful the depolarising and measurement errors are.
\n", "- Add extra noise characteristics to the noise model to obtain something that more resembles a real device. Possible options include adding error during the reset operations, extending the errors to be non-local, or constructing the noise model from a device's calibration data.
\n", "- Change the circuit from iterated entanglement swapping to iterated applications of a correction circuit from a simple error-correcting code. Do you expect this to be more sensitive to depolarising errors from unitary gates or measurement errors?"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Iterated entanglement swapping"]},{"cell_type":"markdown","metadata":{},"source":["In this tutorial, we will focus on:\n","- designing circuits with mid-circuit measurement and conditional gates;\n","- utilising noise models in supported simulators."]},{"cell_type":"markdown","metadata":{},"source":["This example assumes the reader is familiar with the Qubit Teleportation and Entanglement Swapping protocols, and basic models of noise in quantum devices.\n","\n","To run this example, you will need `pytket`, `pytket-qiskit`, and `plotly` (installed via `pip`). To view the graphs, you will need an intallation of `plotly-orca`.\n","\n","Current quantum hardware fits into the NISQ (Noisy, Intermediate-Scale Quantum) regime. This noise cannot realistically be combatted using conventional error correcting codes, because of the lack of available qubits, noise levels exceeding the code thresholds, and very few devices available that can perform measurements and corrections mid-circuit. Analysis of how quantum algorithms perform under noisy conditions is a very active research area, as is finding ways to cope with it. Here, we will look at how well we can perform the Entanglement Swapping protocol with different noise levels.\n","\n","The Entanglement Swapping protocol requires two parties to share Bell pairs with a third party, who applies the Qubit Teleportation protocol to generate a Bell pair between the two parties. The Qubit Teleportation step requires us to be able to measure some qubits and make subsequent corrections to the remaining qubits. There are only a handful of simulators and devices that currently support this, with others restricted to only measuring the qubits at the end of the circuit.\n","\n","The most popular circuit model with conditional gates at the moment is that provided by the OpenQASM language. This permits a very restricted model of classical logic, where we can apply a gate conditionally on the exact value of a classical register. There is no facility in the current spec for Boolean logic or classical operations to apply any function to the value prior to the equality check. For example, Qubit Teleportation can be performed by the following QASM:\n","`OPENQASM 2.0;`\n","`include \"qelib1.inc\";`\n","`qreg a[2];`\n","`qreg b[1];`\n","`creg c[2];`\n","`// Bell state between Alice and Bob`\n","`h a[1];`\n","`cx a[1],b[0];`\n","`// Bell measurement of Alice's qubits`\n","`cx a[0],a[1];`\n","`h a[0];`\n","`measure a[0] -> c[0];`\n","`measure a[1] -> c[1];`\n","`// Correction of Bob's qubit`\n","`if(c==1) z b[0];`\n","`if(c==3) z b[0];`\n","`if(c==2) x b[0];`\n","`if(c==3) x b[0];`\n","\n","This corresponds to the following `pytket` code:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket import Circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qtel = Circuit()\n","alice = qtel.add_q_register(\"a\", 2)\n","bob = qtel.add_q_register(\"b\", 1)\n","data = qtel.add_c_register(\"d\", 2)"]},{"cell_type":"markdown","metadata":{},"source":["Bell state between Alice and Bob:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qtel.H(alice[1])\n","qtel.CX(alice[1], bob[0])"]},{"cell_type":"markdown","metadata":{},"source":["Bell measurement of Alice's qubits:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qtel.CX(alice[0], alice[1])\n","qtel.H(alice[0])\n","qtel.Measure(alice[0], data[0])\n","qtel.Measure(alice[1], data[1])"]},{"cell_type":"markdown","metadata":{},"source":["Correction of Bob's qubit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qtel.X(bob[0], condition_bits=[data[0], data[1]], condition_value=2)\n","qtel.X(bob[0], condition_bits=[data[0], data[1]], condition_value=3)\n","qtel.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=1)\n","qtel.Z(bob[0], condition_bits=[data[0], data[1]], condition_value=3)"]},{"cell_type":"markdown","metadata":{},"source":["So to demonstrate the Entanglement Swapping protocol, we just need to run this on one side of a Bell pair."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["es = Circuit()\n","ava = es.add_q_register(\"a\", 1)\n","bella = es.add_q_register(\"b\", 2)\n","charlie = es.add_q_register(\"c\", 1)\n","data = es.add_c_register(\"d\", 2)"]},{"cell_type":"markdown","metadata":{},"source":["Bell state between Ava and Bella:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["es.H(ava[0])\n","es.CX(ava[0], bella[0])"]},{"cell_type":"markdown","metadata":{},"source":["Teleport `bella[0]` to `charlie[0]`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["tel_to_c = qtel.copy()\n","tel_to_c.rename_units({alice[0]: bella[0], alice[1]: bella[1], bob[0]: charlie[0]})\n","es.append(tel_to_c)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(es.get_commands())"]},{"cell_type":"markdown","metadata":{},"source":["Let's start by running a noiseless simulation of this to verify that what we get looks like a Bell pair."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.qiskit import AerBackend"]},{"cell_type":"markdown","metadata":{},"source":["Connect to a simulator:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["backend = AerBackend()"]},{"cell_type":"markdown","metadata":{},"source":["Make a ZZ measurement of the Bell pair:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["bell_test = es.copy()\n","bell_test.Measure(ava[0], data[0])\n","bell_test.Measure(charlie[0], data[1])"]},{"cell_type":"markdown","metadata":{},"source":["Run the experiment:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["bell_test = backend.get_compiled_circuit(bell_test)\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(bell_test)\n","handle = backend.process_circuit(bell_test, n_shots=2000)\n","counts = backend.get_result(handle).get_counts()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(counts)"]},{"cell_type":"markdown","metadata":{},"source":["This is good, we have got roughly 50/50 measurement results of 00 and 11 under the ZZ operator. But there are many other states beyond the Bell state that also generate this distribution, so to gain more confidence in our claim about the state we should make more measurements that also characterise it, i.e. perform state tomography.\n","\n","Here, we will demonstrate a naive approach to tomography that makes 3^n measurement circuits for an n-qubit state. More elaborate methods also exist."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.pauli import Pauli, QubitPauliString\n","from pytket.utils import append_pauli_measurement, probs_from_counts\n","from itertools import product\n","from scipy.linalg import lstsq, eigh\n","import numpy as np"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def gen_tomography_circuits(state, qubits, bits):\n"," # Yields {X, Y, Z}^n measurements in lexicographical order\n"," # Only measures qubits, storing the result in bits\n"," # (since we don't care about the ancilla qubits)\n"," assert len(qubits) == len(bits)\n"," for paulis in product([Pauli.X, Pauli.Y, Pauli.Z], repeat=len(qubits)):\n"," circ = state.copy()\n"," for qb, b, p in zip(qubits, bits, paulis):\n"," if p == Pauli.X:\n"," circ.H(qb)\n"," elif p == Pauli.Y:\n"," circ.V(qb)\n"," circ.Measure(qb, b)\n"," yield circ"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def run_tomography_circuits(state, qubits, bits, backend):\n"," circs = list(gen_tomography_circuits(state, qubits, bits))\n"," # Compile and run each circuit\n"," circs = backend.get_compiled_circuits(circs)\n"," handles = backend.process_circuits(circs, n_shots=2000)\n"," # Get the observed measurement probabilities\n"," probs_list = []\n"," for result in backend.get_results(handles):\n"," counts = result.get_counts()\n"," probs = probs_from_counts(counts)\n"," probs_list.append(probs)\n"," return probs_list"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def fit_tomography_outcomes(probs_list, n_qbs):\n"," # Define the density matrices for the basis states\n"," basis = dict()\n"," basis[(Pauli.X, 0)] = np.asarray([[0.5, 0.5], [0.5, 0.5]])\n"," basis[(Pauli.X, 1)] = np.asarray([[0.5, -0.5], [-0.5, 0.5]])\n"," basis[(Pauli.Y, 0)] = np.asarray([[0.5, -0.5j], [0.5j, 0.5]])\n"," basis[(Pauli.Y, 1)] = np.asarray([[0.5, 0.5j], [-0.5j, 0.5]])\n"," basis[(Pauli.Z, 0)] = np.asarray([[1, 0], [0, 0]])\n"," basis[(Pauli.Z, 1)] = np.asarray([[0, 0], [0, 1]])\n"," dim = 2**n_qbs\n"," # Define vector all_probs as a concatenation of probability vectors for each measurement (2**n x 3**n, 1)\n"," # Define matrix all_ops mapping a (vectorised) density matrix to a vector of probabilities for each measurement\n"," # (2**n x 3**n, 2**n x 2**n)\n"," all_probs = []\n"," all_ops = []\n"," for paulis, probs in zip(\n"," product([Pauli.X, Pauli.Y, Pauli.Z], repeat=n_qbs), probs_list\n"," ):\n"," prob_vec = []\n"," meas_ops = []\n"," for outcome in product([0, 1], repeat=n_qbs):\n"," prob_vec.append(probs.get(outcome, 0))\n"," op = np.eye(1, dtype=complex)\n"," for p, o in zip(paulis, outcome):\n"," op = np.kron(op, basis[(p, o)])\n"," meas_ops.append(op.reshape(1, dim * dim).conj())\n"," all_probs.append(np.vstack(prob_vec))\n"," all_ops.append(np.vstack(meas_ops))\n"," # Solve for density matrix by minimising || all_ops * dm - all_probs ||\n"," dm, _, _, _ = lstsq(np.vstack(all_ops), np.vstack(all_probs))\n"," dm = dm.reshape(dim, dim)\n"," # Make density matrix positive semi-definite\n"," v, w = eigh(dm)\n"," for i in range(dim):\n"," if v[i] < 0:\n"," for j in range(i + 1, dim):\n"," v[j] += v[i] / (dim - (i + 1))\n"," v[i] = 0\n"," dm = np.zeros([dim, dim], dtype=complex)\n"," for j in range(dim):\n"," dm += v[j] * np.outer(w[:, j], np.conj(w[:, j]))\n"," # Normalise trace of density matrix\n"," dm /= np.trace(dm)\n"," return dm"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["probs_list = run_tomography_circuits(\n"," es, [ava[0], charlie[0]], [data[0], data[1]], backend\n",")\n","dm = fit_tomography_outcomes(probs_list, 2)\n","print(dm.round(3))"]},{"cell_type":"markdown","metadata":{},"source":["This is very close to the true density matrix for a pure Bell state. We can attribute the error here to the sampling error since we only take 2000 samples of each measurement circuit.\n","\n","To quantify exactly how similar it is to the correct density matrix, we can calculate the fidelity."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from scipy.linalg import sqrtm"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def fidelity(dm0, dm1):\n"," # Calculate the fidelity between two density matrices\n"," sq0 = sqrtm(dm0)\n"," sq1 = sqrtm(dm1)\n"," return np.linalg.norm(sq0.dot(sq1)) ** 2"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["bell_state = np.asarray(\n"," [\n"," [0.5, 0, 0, 0.5],\n"," [0, 0, 0, 0],\n"," [0, 0, 0, 0],\n"," [0.5, 0, 0, 0.5],\n"," ]\n",")\n","print(fidelity(dm, bell_state))"]},{"cell_type":"markdown","metadata":{},"source":["This high fidelity is unsurprising since we have a completely noiseless simulation. So the next step is to add some noise to the simulation and observe how the overall fidelity is affected. The `AerBackend` wraps around the Qiskit Aer simulator and can pass on any `qiskit.providers.aer.noise.NoiseModel` to the simulator. Let's start by adding some uniform depolarising noise to each CX gate and some uniform measurement error."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from qiskit.providers.aer.noise import NoiseModel, depolarizing_error, ReadoutError"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def make_noise_model(dep_err_rate, ro_err_rate, qubits):\n"," # Define a noise model that applies uniformly to the given qubits\n"," model = NoiseModel()\n"," dep_err = depolarizing_error(dep_err_rate, 2)\n"," ro_err = ReadoutError(\n"," [[1 - ro_err_rate, ro_err_rate], [ro_err_rate, 1 - ro_err_rate]]\n"," )\n"," # Add depolarising error to CX gates between any qubits (implying full connectivity)\n"," for i, j in product(qubits, repeat=2):\n"," if i != j:\n"," model.add_quantum_error(dep_err, [\"cx\"], [i, j])\n"," # Add readout error for each qubit\n"," for i in qubits:\n"," model.add_readout_error(ro_err, qubits=[i])\n"," return model"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["test_model = make_noise_model(0.03, 0.05, range(4))\n","backend = AerBackend(noise_model=test_model)\n","probs_list = run_tomography_circuits(\n"," es, [ava[0], charlie[0]], [data[0], data[1]], backend\n",")\n","dm = fit_tomography_outcomes(probs_list, 2)\n","print(dm.round(3))\n","print(fidelity(dm, bell_state))"]},{"cell_type":"markdown","metadata":{},"source":["Despite the very small circuit and the relatively small error rates, the fidelity of the final state has reduced considerably.\n","\n","As far as circuits go, the entanglement swapping protocol is little more than a toy example and is nothing close to the scale of circuits for most interesting quantum computational problems. However, it is possible to iterate the protocol many times to build up a larger computation, allowing us to see the impact of the noise at different scales."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket import OpType\n","from plotly.graph_objects import Scatter, Figure"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def iterated_entanglement_swap(n_iter):\n"," # Iterate the entanglement swapping protocol n_iter times\n"," it_es = Circuit()\n"," ava = it_es.add_q_register(\"a\", 1)\n"," bella = it_es.add_q_register(\"b\", 2)\n"," charlie = it_es.add_q_register(\"c\", 1)\n"," data = it_es.add_c_register(\"d\", 2)\n","\n"," # Start with an initial Bell state\n"," it_es.H(ava[0])\n"," it_es.CX(ava[0], bella[0])\n"," for i in range(n_iter):\n"," if i % 2 == 0:\n"," # Teleport bella[0] to charlie[0] to give a Bell pair between ava[0] and charlier[0]\n"," tel_to_c = qtel.copy()\n"," tel_to_c.rename_units(\n"," {alice[0]: bella[0], alice[1]: bella[1], bob[0]: charlie[0]}\n"," )\n"," it_es.append(tel_to_c)\n"," it_es.add_gate(OpType.Reset, [bella[0]])\n"," it_es.add_gate(OpType.Reset, [bella[1]])\n"," else:\n"," # Teleport charlie[0] to bella[0] to give a Bell pair between ava[0] and bella[0]\n"," tel_to_b = qtel.copy()\n"," tel_to_b.rename_units(\n"," {alice[0]: charlie[0], alice[1]: bella[1], bob[0]: bella[0]}\n"," )\n"," it_es.append(tel_to_b)\n"," it_es.add_gate(OpType.Reset, [bella[1]])\n"," it_es.add_gate(OpType.Reset, [charlie[0]])\n"," # Return the circuit and the qubits expected to share a Bell pair\n"," if n_iter % 2 == 0:\n"," return it_es, [ava[0], bella[0]]\n"," else:\n"," return it_es, [ava[0], charlie[0]]"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def iterated_noisy_experiment(dep_err_rate, ro_err_rate, max_iter):\n"," # Set up the noisy simulator with the given error rates\n"," test_model = make_noise_model(dep_err_rate, ro_err_rate, range(4))\n"," backend = AerBackend(noise_model=test_model)\n"," # Estimate the fidelity after n iterations, from 0 to max_iter (inclusive)\n"," fid_list = []\n"," for i in range(max_iter + 1):\n"," it_es, qubits = iterated_entanglement_swap(i)\n"," probs_list = run_tomography_circuits(it_es, qubits, [data[0], data[1]], backend)\n"," dm = fit_tomography_outcomes(probs_list, 2)\n"," fid = fidelity(dm, bell_state)\n"," fid_list.append(fid)\n"," return fid_list"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["fig = Figure()\n","fig.update_layout(\n"," title=\"Iterated Entanglement Swapping under Noise (dep_err = 0.03)\",\n"," xaxis_title=\"Iterations\",\n"," xaxis=dict(range=[0, 10]),\n"," yaxis_title=\"Fidelity\",\n",")\n","iter_range = np.arange(11)\n","for i in range(7):\n"," fids = iterated_noisy_experiment(0.03, 0.025 * i, 10)\n"," plot_data = Scatter(\n"," x=iter_range, y=fids, name=\"ro_err=\" + str(np.round(0.025 * i, 3))\n"," )\n"," fig.add_trace(plot_data)\n","try:\n"," fig.show(renderer=\"svg\")\n","except ValueError as e:\n"," print(e) # requires plotly-orca"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["fig = Figure()\n","fig.update_layout(\n"," title=\"Iterated Entanglement Swapping under Noise (ro_err = 0.05)\",\n"," xaxis_title=\"Iterations\",\n"," xaxis=dict(range=[0, 10]),\n"," yaxis_title=\"Fidelity\",\n",")\n","iter_range = np.arange(11)\n","for i in range(9):\n"," fids = iterated_noisy_experiment(0.01 * i, 0.05, 10)\n"," plot_data = Scatter(\n"," x=iter_range, y=fids, name=\"dep_err=\" + str(np.round(0.01 * i, 3))\n"," )\n"," fig.add_trace(plot_data)\n","try:\n"," fig.show(renderer=\"svg\")\n","except ValueError as e:\n"," print(e) # requires plotly-orca"]},{"cell_type":"markdown","metadata":{},"source":["These graphs are not very surprising, but are still important for seeing that the current error rates of typical NISQ devices become crippling for fidelities very quickly after repeated mid-circuit measurements and corrections (even with this overly-simplified model with uniform noise and no crosstalk or higher error modes). This provides good motivation for the adoption of error mitigation techniques, and for the development of new techniques that are robust to errors in mid-circuit measurements."]},{"cell_type":"markdown","metadata":{},"source":["Exercises:\n","- Vary the fixed noise levels to compare how impactful the depolarising and measurement errors are.\n","- Add extra noise characteristics to the noise model to obtain something that more resembles a real device. Possible options include adding error during the reset operations, extending the errors to be non-local, or constructing the noise model from a device's calibration data.\n","- Change the circuit from iterated entanglement swapping to iterated applications of a correction circuit from a simple error-correcting code. Do you expect this to be more sensitive to depolarising errors from unitary gates or measurement errors?"]}],"metadata":{"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.6.4"}},"nbformat":4,"nbformat_minor":2}
diff --git a/examples/expectation_value_example.ipynb b/examples/expectation_value_example.ipynb
index 655ce4f6..ba4992dc 100644
--- a/examples/expectation_value_example.ipynb
+++ b/examples/expectation_value_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Expectation Values"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Given a circuit generating a quantum state $\\lvert \\psi \\rangle$, it is very common to have an operator $H$ and ask for the expectation value $\\langle \\psi \\vert H \\vert \\psi \\rangle$. A notable example is in quantum computational chemistry, where $\\lvert \\psi \\rangle$ encodes the wavefunction for the electronic state of a small molecule, and the energy of the molecule can be derived from the expectation value with respect to the molecule's Hamiltonian operator $H$.
\n", "
\n", "This example uses this chemistry scenario to demonstrate the overall procedure for using `pytket` to perform advanced high-level procedures. We build on top of topics covered by several other example notebooks, including circuit generation, optimisation, and using different backends.
\n", "
\n", "There is limited built-in functionality in `pytket` for obtaining expectation values from circuits. This is designed to encourage users to consider their needs for parallelising the processing of circuits, manipulating results (e.g. filtering, adjusting counts to mitigate errors, and other forms of data processing), or more advanced schemes for grouping the terms of the operator into measurement circuits. For this example, suppose that we want to focus on reducing the queueing time for IBM device backends, and filter our shots to eliminate some detected errors.
\n", "
\n", "This notebook makes use of the Qiskit and ProjectQ backend modules `pytket_qiskit` and `pytket_projectq`, as well as the electronic structure module `openfermion`, all three of which should first be installed via `pip`.
\n", "
\n", "We will start by generating an ansatz and Hamiltonian for the chemical of interest. Here, we are just using a simple model of $\\mathrm{H}_2$ with four qubits representing the occupation of four spin orbitals."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, Qubit, Bit\n", "from sympy import symbols"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Generate ansatz and Hamiltonian:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ansatz = Circuit()\n", "qubits = ansatz.add_q_register(\"q\", 4)\n", "args = symbols(\"a0 a1 a2 a3 a4 a5 a6 a7\")\n", "for i in range(4):\n", " ansatz.Ry(args[i], qubits[i])\n", "for i in range(3):\n", " ansatz.CX(qubits[i], qubits[i + 1])\n", "for i in range(4):\n", " ansatz.Ry(args[4 + i], qubits[i])\n", "ansatz.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for command in ansatz:\n", " print(command)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In reality, you would use an expectation value calculation as the objective function for a classical optimisation routine to determine the parameter values for the ground state. For the purposes of this notebook, we will use some predetermined values for the ansatz, already optimised for $\\mathrm{H}_2$."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["arg_values = [\n", " 7.17996183e-02,\n", " 2.95442468e-08,\n", " 1.00000015e00,\n", " 1.00000086e00,\n", " 9.99999826e-01,\n", " 1.00000002e00,\n", " 9.99999954e-01,\n", " 1.13489747e-06,\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ansatz.symbol_substitution(dict(zip(args, arg_values)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can use for example the openfermion library to express an Hamiltonian as a sum of tensors of paulis."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = (\n", " -0.0970662681676282 * of.QubitOperator(\"\")\n", " + -0.045302615503799284 * of.QubitOperator(\"X0 X1 Y2 Y3\")\n", " + 0.045302615503799284 * of.QubitOperator(\"X0 Y1 Y2 X3\")\n", " + 0.045302615503799284 * of.QubitOperator(\"Y0 X1 X2 Y3\")\n", " + -0.045302615503799284 * of.QubitOperator(\"Y0 Y1 X2 X3\")\n", " + 0.17141282644776884 * of.QubitOperator(\"Z0\")\n", " + 0.16868898170361213 * of.QubitOperator(\"Z0 Z1\")\n", " + 0.12062523483390425 * of.QubitOperator(\"Z0 Z2\")\n", " + 0.16592785033770352 * of.QubitOperator(\"Z0 Z3\")\n", " + 0.17141282644776884 * of.QubitOperator(\"Z1\")\n", " + 0.16592785033770352 * of.QubitOperator(\"Z1 Z2\")\n", " + 0.12062523483390425 * of.QubitOperator(\"Z1 Z3\")\n", " + -0.22343153690813597 * of.QubitOperator(\"Z2\")\n", " + 0.17441287612261608 * of.QubitOperator(\"Z2 Z3\")\n", " + -0.22343153690813597 * of.QubitOperator(\"Z3\")\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This can be converted into pytket's QubitPauliOperator type.
\n", "
\n", "The OpenFermion `QubitOperator` class represents the operator by its decomposition into a linear combination of Pauli operators (tensor products of the $I$, $X$, $Y$, and $Z$ matrices).
\n", "
\n", "A `QubitPauliString` is a sparse representation of a Pauli operator with support over some subset of qubits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can simulate this exactly using a statevector simulator like ProjectQ. This has a built-in method for fast calculations of expectation values that works well for small examples like this."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.projectq import ProjectQBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = ProjectQBackend()\n", "ideal_energy = backend.get_operator_expectation_value(ansatz, hamiltonian_op)\n", "print(ideal_energy)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Ideally the state generated by this ansatz will only span the computational basis states with exactly two of the four qubits in state $\\lvert 1 \\rangle$. This is because these basis states correspond to two electrons being present in the molecule.
\n", "
\n", "This ansatz is a hardware-efficient model that is designed to explore a large portion of the Hilbert space with relatively few entangling gates. Unfortunately, with this much freedom, it will regularly generate states that have no physical interpretation such as states spanning multiple basis states corresponding to different numbers of electrons in the system (which we assume is fixed and conserved).
\n", "
\n", "We can mitigate this by using a syndrome qubit that calculates the parity of the other qubits. Post-selecting this syndrome with $\\langle 0 \\rvert$ will project the remaining state onto the subspace of basis states with even parity, increasing the likelihood the observed state will be a physically admissible state.
\n", "
\n", "Even if the ansatz parameters are tuned to give a physical state, real devices have noise and imperfect gates, so in practice we may also measure bad states with a small probability. If this syndrome qubit is measured as 1, it means an error has definitely occurred, so we should discard the shot."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["syn = Qubit(\"synq\", 0)\n", "syn_res = Bit(\"synres\", 0)\n", "ansatz.add_qubit(syn)\n", "ansatz.add_bit(syn_res)\n", "for qb in qubits:\n", " ansatz.CX(qb, syn)\n", "ansatz.Measure(syn, syn_res)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Using this, we can define a filter function which removes the shots which the syndrome qubit detected as erroneous. `BackendResult` objects allow retrieval of shots in any bit order, so we can retrieve the `synres` results separately and use them to filter the shots from the remaining bits. The Backends example notebook describes this in more detail."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from collections import Counter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def filter_shots(backend_result, syn_res_bit):\n", " bits = sorted(backend_result.get_bitlist())\n", " bits.remove(syn_res_bit)\n", " syn_shots = backend_result.get_shots([syn_res])[:, 0]\n", " main_shots = backend_result.get_shots(bits)\n", " return main_shots[syn_shots == 0]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def filter_counts(backend_result, syn_res_bit):\n", " bits = sorted(backend_result.get_bitlist())\n", " syn_index = bits.index(syn_res_bit)\n", " counts = backend_result.get_counts()\n", " filtered_counts = Counter()\n", " for readout, count in counts.items():\n", " if readout[syn_index] == 0:\n", " filtered_readout = tuple(v for i, v in enumerate(readout) if i != syn_index)\n", " filtered_counts[filtered_readout] += count\n", " return filtered_counts"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Depending on which backend we will be using, we will need to compile each circuit we run to conform to the gate set and connectivity constraints. We can define a compilation pass for each backend that optimises the circuit and maps it onto the backend's gate set and connectivity constraints. We don't expect this to change our circuit too much as it is already near-optimal."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import OptimisePhaseGadgets, SequencePass"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def compiler_pass(backend):\n", " return SequencePass([OptimisePhaseGadgets(), backend.default_compilation_pass()])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Given the full statevector, the expectation value can be calculated simply by matrix multiplication. However, with a real quantum system, we cannot observe the full statevector directly. Fortunately, the Pauli decomposition of the operator gives us a sequence of measurements we should apply to obtain the relevant information to reconstruct the expectation value.
\n", "
\n", "The utility method `append_pauli_measurement` takes a single term of a `QubitPauliOperator` (a `QubitPauliString`) and appends measurements in the corresponding bases to obtain the expectation value for that particular Pauli operator. We will want to make a new `Circuit` object for each of the measurements we wish to observe.
\n", ""]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.predicates import CompilationUnit\n", "from pytket.utils import append_pauli_measurement"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def gen_pauli_measurement_circuits(state_circuit, compiler_pass, operator):\n", " # compile main circuit once\n", " state_cu = CompilationUnit(state_circuit)\n", " compiler_pass.apply(state_cu)\n", " compiled_state = state_cu.circuit\n", " final_map = state_cu.final_map\n", " # make a measurement circuit for each pauli\n", " pauli_circuits = []\n", " coeffs = []\n", " energy = 0\n", " for p, c in operator.terms.items():\n", " if p == ():\n", " # constant term\n", " energy += c\n", " else:\n", " # make measurement circuits and compile them\n", " pauli_circ = Circuit(state_circuit.n_qubits - 1) # ignore syndrome qubit\n", " append_pauli_measurement(qps_from_openfermion(p), pauli_circ)\n", " pauli_cu = CompilationUnit(pauli_circ)\n", " compiler_pass.apply(pauli_cu)\n", " pauli_circ = pauli_cu.circuit\n", " init_map = pauli_cu.initial_map\n", " # map measurements onto the placed qubits from the state\n", " rename_map = {\n", " i: final_map[o] for o, i in init_map.items() if o in final_map\n", " }\n", " pauli_circ.rename_units(rename_map)\n", " state_and_measure = compiled_state.copy()\n", " state_and_measure.append(pauli_circ)\n", " pauli_circuits.append(state_and_measure)\n", " coeffs.append(c)\n", " return pauli_circuits, coeffs, energy"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now start composing these together to get our generalisable expectation value function. Passing all of our circuits to `process_circuits` allows them to be submitted to IBM Quantum devices at the same time, giving substantial savings in overall queueing time. Since the backend will cache any results from `Backend.process_circuits`, we will remove the results when we are done with them to prevent memory bloating when this method is called many times."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import expectation_from_shots, expectation_from_counts"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def expectation_value(state_circuit, operator, backend, n_shots):\n", " if backend.supports_expectation:\n", " circuit = state_circuit.copy()\n", " compiled_circuit = backend.get_compiled_circuit(circuit)\n", " return backend.get_operator_expectation_value(\n", " compiled_circuit, qpo_from_openfermion(operator)\n", " )\n", " elif backend.supports_shots:\n", " syn_res_index = state_circuit.bit_readout[syn_res]\n", " pauli_circuits, coeffs, energy = gen_pauli_measurement_circuits(\n", " state_circuit, compiler_pass(backend), operator\n", " )\n", " handles = backend.process_circuits(pauli_circuits, n_shots=n_shots)\n", " for handle, coeff in zip(handles, coeffs):\n", " res = backend.get_result(handle)\n", " filtered = filter_shots(res, syn_res)\n", " energy += coeff * expectation_from_shots(filtered)\n", " backend.pop_result(handle)\n", " return energy\n", " elif backend.supports_counts:\n", " syn_res_index = state_circuit.bit_readout[syn_res]\n", " pauli_circuits, coeffs, energy = gen_pauli_measurement_circuits(\n", " state_circuit, compiler_pass(backend), operator\n", " )\n", " handles = backend.process_circuits(pauli_circuits, n_shots=n_shots)\n", " for handle, coeff in zip(handles, coeffs):\n", " res = backend.get_result(handle)\n", " filtered = filter_counts(res, syn_res)\n", " energy += coeff * expectation_from_counts(filtered)\n", " backend.pop_result(handle)\n", " return energy\n", " else:\n", " raise NotImplementedError(\"Implementation for state to be written\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["...and then run it for our ansatz. `AerBackend` supports faster expectation value from snapshopts (using the `AerBackend.get_operator_expectation_value` method), but this only works when all the qubits in the circuit are default register qubits that go up from 0. So we will need to rename `synq`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import IBMQEmulatorBackend, AerBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ansatz.rename_units({Qubit(\"synq\", 0): Qubit(\"q\", 4)})"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(expectation_value(ansatz, hamiltonian, AerBackend(), 8000))\n", "# Try replacing IBMQEmulatorBackend with IBMQBackend to submit the circuits to a real IBM Quantum device.\n", "print(expectation_value(ansatz, hamiltonian, IBMQEmulatorBackend(\"ibmq_manila\"), 8000))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For basic practice with using pytket backends and their results, try editing the code here to:
\n", "* Extend `expectation_value` to work with statevector backends (e.g. `AerStateBackend`)
\n", "* Remove the row filtering from `filter_shots` and see the effect on the expectation value on a noisy simulation/device
\n", "* Adapt `filter_shots` to be able to filter a counts dictionary and adapt `expectation_value` to calulate the result using the counts summary from the backend (`pytket.utils.expectation_from_counts` will be useful here)"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Expectation values"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Given a circuit generating a quantum state $\\lvert \\psi \\rangle$, it is very common to have an operator $H$ and ask for the expectation value $\\langle \\psi \\vert H \\vert \\psi \\rangle$. A notable example is in quantum computational chemistry, where $\\lvert \\psi \\rangle$ encodes the wavefunction for the electronic state of a small molecule, and the energy of the molecule can be derived from the expectation value with respect to the molecule's Hamiltonian operator $H$.
\n", "
\n", "This example uses this chemistry scenario to demonstrate the overall procedure for using `pytket` to perform advanced high-level procedures. We build on top of topics covered by several other example notebooks, including circuit generation, optimisation, and using different backends.
\n", "
\n", "There is limited built-in functionality in `pytket` for obtaining expectation values from circuits. This is designed to encourage users to consider their needs for parallelising the processing of circuits, manipulating results (e.g. filtering, adjusting counts to mitigate errors, and other forms of data processing), or more advanced schemes for grouping the terms of the operator into measurement circuits. For this example, suppose that we want to focus on reducing the queueing time for IBM device backends, and filter our shots to eliminate some detected errors.
\n", "
\n", "This notebook makes use of the Qiskit and ProjectQ backend modules `pytket_qiskit` and `pytket_projectq`, as well as the electronic structure module `openfermion`, all three of which should first be installed via `pip`.
\n", "
\n", "We will start by generating an ansatz and Hamiltonian for the chemical of interest. Here, we are just using a simple model of $\\mathrm{H}_2$ with four qubits representing the occupation of four spin orbitals."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, Qubit, Bit\n", "from sympy import symbols"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Generate ansatz and Hamiltonian:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ansatz = Circuit()\n", "qubits = ansatz.add_q_register(\"q\", 4)\n", "args = symbols(\"a0 a1 a2 a3 a4 a5 a6 a7\")\n", "for i in range(4):\n", " ansatz.Ry(args[i], qubits[i])\n", "for i in range(3):\n", " ansatz.CX(qubits[i], qubits[i + 1])\n", "for i in range(4):\n", " ansatz.Ry(args[4 + i], qubits[i])\n", "ansatz.measure_all()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for command in ansatz:\n", " print(command)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In reality, you would use an expectation value calculation as the objective function for a classical optimisation routine to determine the parameter values for the ground state. For the purposes of this notebook, we will use some predetermined values for the ansatz, already optimised for $\\mathrm{H}_2$."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["arg_values = [\n", " 7.17996183e-02,\n", " 2.95442468e-08,\n", " 1.00000015e00,\n", " 1.00000086e00,\n", " 9.99999826e-01,\n", " 1.00000002e00,\n", " 9.99999954e-01,\n", " 1.13489747e-06,\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ansatz.symbol_substitution(dict(zip(args, arg_values)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can use for example the openfermion library to express an Hamiltonian as a sum of tensors of paulis."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = (\n", " -0.0970662681676282 * of.QubitOperator(\"\")\n", " + -0.045302615503799284 * of.QubitOperator(\"X0 X1 Y2 Y3\")\n", " + 0.045302615503799284 * of.QubitOperator(\"X0 Y1 Y2 X3\")\n", " + 0.045302615503799284 * of.QubitOperator(\"Y0 X1 X2 Y3\")\n", " + -0.045302615503799284 * of.QubitOperator(\"Y0 Y1 X2 X3\")\n", " + 0.17141282644776884 * of.QubitOperator(\"Z0\")\n", " + 0.16868898170361213 * of.QubitOperator(\"Z0 Z1\")\n", " + 0.12062523483390425 * of.QubitOperator(\"Z0 Z2\")\n", " + 0.16592785033770352 * of.QubitOperator(\"Z0 Z3\")\n", " + 0.17141282644776884 * of.QubitOperator(\"Z1\")\n", " + 0.16592785033770352 * of.QubitOperator(\"Z1 Z2\")\n", " + 0.12062523483390425 * of.QubitOperator(\"Z1 Z3\")\n", " + -0.22343153690813597 * of.QubitOperator(\"Z2\")\n", " + 0.17441287612261608 * of.QubitOperator(\"Z2 Z3\")\n", " + -0.22343153690813597 * of.QubitOperator(\"Z3\")\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This can be converted into pytket's QubitPauliOperator type.
\n", "
\n", "The OpenFermion `QubitOperator` class represents the operator by its decomposition into a linear combination of Pauli operators (tensor products of the $I$, $X$, $Y$, and $Z$ matrices).
\n", "
\n", "A `QubitPauliString` is a sparse representation of a Pauli operator with support over some subset of qubits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can simulate this exactly using a statevector simulator like ProjectQ. This has a built-in method for fast calculations of expectation values that works well for small examples like this."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.projectq import ProjectQBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = ProjectQBackend()\n", "ideal_energy = backend.get_operator_expectation_value(ansatz, hamiltonian_op)\n", "print(ideal_energy)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Ideally the state generated by this ansatz will only span the computational basis states with exactly two of the four qubits in state $\\lvert 1 \\rangle$. This is because these basis states correspond to two electrons being present in the molecule.
\n", "
\n", "This ansatz is a hardware-efficient model that is designed to explore a large portion of the Hilbert space with relatively few entangling gates. Unfortunately, with this much freedom, it will regularly generate states that have no physical interpretation such as states spanning multiple basis states corresponding to different numbers of electrons in the system (which we assume is fixed and conserved).
\n", "
\n", "We can mitigate this by using a syndrome qubit that calculates the parity of the other qubits. Post-selecting this syndrome with $\\langle 0 \\rvert$ will project the remaining state onto the subspace of basis states with even parity, increasing the likelihood the observed state will be a physically admissible state.
\n", "
\n", "Even if the ansatz parameters are tuned to give a physical state, real devices have noise and imperfect gates, so in practice we may also measure bad states with a small probability. If this syndrome qubit is measured as 1, it means an error has definitely occurred, so we should discard the shot."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["syn = Qubit(\"synq\", 0)\n", "syn_res = Bit(\"synres\", 0)\n", "ansatz.add_qubit(syn)\n", "ansatz.add_bit(syn_res)\n", "for qb in qubits:\n", " ansatz.CX(qb, syn)\n", "ansatz.Measure(syn, syn_res)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Using this, we can define a filter function which removes the shots which the syndrome qubit detected as erroneous. `BackendResult` objects allow retrieval of shots in any bit order, so we can retrieve the `synres` results separately and use them to filter the shots from the remaining bits. The Backends example notebook describes this in more detail."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from collections import Counter"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def filter_shots(backend_result, syn_res_bit):\n", " bits = sorted(backend_result.get_bitlist())\n", " bits.remove(syn_res_bit)\n", " syn_shots = backend_result.get_shots([syn_res])[:, 0]\n", " main_shots = backend_result.get_shots(bits)\n", " return main_shots[syn_shots == 0]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def filter_counts(backend_result, syn_res_bit):\n", " bits = sorted(backend_result.get_bitlist())\n", " syn_index = bits.index(syn_res_bit)\n", " counts = backend_result.get_counts()\n", " filtered_counts = Counter()\n", " for readout, count in counts.items():\n", " if readout[syn_index] == 0:\n", " filtered_readout = tuple(v for i, v in enumerate(readout) if i != syn_index)\n", " filtered_counts[filtered_readout] += count\n", " return filtered_counts"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Depending on which backend we will be using, we will need to compile each circuit we run to conform to the gate set and connectivity constraints. We can define a compilation pass for each backend that optimises the circuit and maps it onto the backend's gate set and connectivity constraints. We don't expect this to change our circuit too much as it is already near-optimal."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import OptimisePhaseGadgets, SequencePass"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def compiler_pass(backend):\n", " return SequencePass([OptimisePhaseGadgets(), backend.default_compilation_pass()])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Given the full statevector, the expectation value can be calculated simply by matrix multiplication. However, with a real quantum system, we cannot observe the full statevector directly. Fortunately, the Pauli decomposition of the operator gives us a sequence of measurements we should apply to obtain the relevant information to reconstruct the expectation value.
\n", "
\n", "The utility method `append_pauli_measurement` takes a single term of a `QubitPauliOperator` (a `QubitPauliString`) and appends measurements in the corresponding bases to obtain the expectation value for that particular Pauli operator. We will want to make a new `Circuit` object for each of the measurements we wish to observe.
\n", ""]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.predicates import CompilationUnit\n", "from pytket.utils import append_pauli_measurement"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def gen_pauli_measurement_circuits(state_circuit, compiler_pass, operator):\n", " # compile main circuit once\n", " state_cu = CompilationUnit(state_circuit)\n", " compiler_pass.apply(state_cu)\n", " compiled_state = state_cu.circuit\n", " final_map = state_cu.final_map\n", " # make a measurement circuit for each pauli\n", " pauli_circuits = []\n", " coeffs = []\n", " energy = 0\n", " for p, c in operator.terms.items():\n", " if p == ():\n", " # constant term\n", " energy += c\n", " else:\n", " # make measurement circuits and compile them\n", " pauli_circ = Circuit(state_circuit.n_qubits - 1) # ignore syndrome qubit\n", " append_pauli_measurement(qps_from_openfermion(p), pauli_circ)\n", " pauli_cu = CompilationUnit(pauli_circ)\n", " compiler_pass.apply(pauli_cu)\n", " pauli_circ = pauli_cu.circuit\n", " init_map = pauli_cu.initial_map\n", " # map measurements onto the placed qubits from the state\n", " rename_map = {\n", " i: final_map[o] for o, i in init_map.items() if o in final_map\n", " }\n", " pauli_circ.rename_units(rename_map)\n", " state_and_measure = compiled_state.copy()\n", " state_and_measure.append(pauli_circ)\n", " pauli_circuits.append(state_and_measure)\n", " coeffs.append(c)\n", " return pauli_circuits, coeffs, energy"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can now start composing these together to get our generalisable expectation value function. Passing all of our circuits to `process_circuits` allows them to be submitted to IBM Quantum devices at the same time, giving substantial savings in overall queueing time. Since the backend will cache any results from `Backend.process_circuits`, we will remove the results when we are done with them to prevent memory bloating when this method is called many times."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import expectation_from_shots, expectation_from_counts"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def expectation_value(state_circuit, operator, backend, n_shots):\n", " if backend.supports_expectation:\n", " circuit = state_circuit.copy()\n", " compiled_circuit = backend.get_compiled_circuit(circuit)\n", " return backend.get_operator_expectation_value(\n", " compiled_circuit, qpo_from_openfermion(operator)\n", " )\n", " elif backend.supports_shots:\n", " syn_res_index = state_circuit.bit_readout[syn_res]\n", " pauli_circuits, coeffs, energy = gen_pauli_measurement_circuits(\n", " state_circuit, compiler_pass(backend), operator\n", " )\n", " handles = backend.process_circuits(pauli_circuits, n_shots=n_shots)\n", " for handle, coeff in zip(handles, coeffs):\n", " res = backend.get_result(handle)\n", " filtered = filter_shots(res, syn_res)\n", " energy += coeff * expectation_from_shots(filtered)\n", " backend.pop_result(handle)\n", " return energy\n", " elif backend.supports_counts:\n", " syn_res_index = state_circuit.bit_readout[syn_res]\n", " pauli_circuits, coeffs, energy = gen_pauli_measurement_circuits(\n", " state_circuit, compiler_pass(backend), operator\n", " )\n", " handles = backend.process_circuits(pauli_circuits, n_shots=n_shots)\n", " for handle, coeff in zip(handles, coeffs):\n", " res = backend.get_result(handle)\n", " filtered = filter_counts(res, syn_res)\n", " energy += coeff * expectation_from_counts(filtered)\n", " backend.pop_result(handle)\n", " return energy\n", " else:\n", " raise NotImplementedError(\"Implementation for state to be written\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["...and then run it for our ansatz. `AerBackend` supports faster expectation value from snapshopts (using the `AerBackend.get_operator_expectation_value` method), but this only works when all the qubits in the circuit are default register qubits that go up from 0. So we will need to rename `synq`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import IBMQEmulatorBackend, AerBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ansatz.rename_units({Qubit(\"synq\", 0): Qubit(\"q\", 4)})"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(expectation_value(ansatz, hamiltonian, AerBackend(), 8000))\n", "# Try replacing IBMQEmulatorBackend with IBMQBackend to submit the circuits to a real IBM Quantum device.\n", "print(expectation_value(ansatz, hamiltonian, IBMQEmulatorBackend(\"ibmq_manila\"), 8000))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For basic practice with using pytket backends and their results, try editing the code here to:
\n", "* Extend `expectation_value` to work with statevector backends (e.g. `AerStateBackend`)
\n", "* Remove the row filtering from `filter_shots` and see the effect on the expectation value on a noisy simulation/device
\n", "* Adapt `filter_shots` to be able to filter a counts dictionary and adapt `expectation_value` to calulate the result using the counts summary from the backend (`pytket.utils.expectation_from_counts` will be useful here)"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/h2_5A.json b/examples/h2_5A.json
deleted file mode 100644
index a5f829c0..00000000
--- a/examples/h2_5A.json
+++ /dev/null
@@ -1,409 +0,0 @@
-[
- {
- "string": [],
- "coefficient": [
- -0.5458607027942332,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.03970104575308908,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 2
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.039577818133605905,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 1
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.03970104575308908,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 3
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.039577818133605905,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "Z"
- ],
- [
- [
- "q",
- [
- 2
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.026458859479781598,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 1
- ]
- ],
- "Z"
- ],
- [
- [
- "q",
- [
- 3
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.026458859479781598,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "Z"
- ],
- [
- [
- "q",
- [
- 1
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.11004294256569602,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 1
- ]
- ],
- "Z"
- ],
- [
- [
- "q",
- [
- 2
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.11005517318966757,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "Y"
- ],
- [
- [
- "q",
- [
- 1
- ]
- ],
- "X"
- ],
- [
- [
- "q",
- [
- 2
- ]
- ],
- "X"
- ],
- [
- [
- "q",
- [
- 3
- ]
- ],
- "Y"
- ]
- ],
- "coefficient": [
- 0.08359631370988596,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "X"
- ],
- [
- [
- "q",
- [
- 1
- ]
- ],
- "X"
- ],
- [
- [
- "q",
- [
- 2
- ]
- ],
- "Y"
- ],
- [
- [
- "q",
- [
- 3
- ]
- ],
- "Y"
- ]
- ],
- "coefficient": [
- -0.08359631370988596,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "Y"
- ],
- [
- [
- "q",
- [
- 1
- ]
- ],
- "Y"
- ],
- [
- [
- "q",
- [
- 2
- ]
- ],
- "X"
- ],
- [
- [
- "q",
- [
- 3
- ]
- ],
- "X"
- ]
- ],
- "coefficient": [
- -0.08359631370988596,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "X"
- ],
- [
- [
- "q",
- [
- 1
- ]
- ],
- "Y"
- ],
- [
- [
- "q",
- [
- 2
- ]
- ],
- "Y"
- ],
- [
- [
- "q",
- [
- 3
- ]
- ],
- "X"
- ]
- ],
- "coefficient": [
- 0.08359631370988596,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 0
- ]
- ],
- "Z"
- ],
- [
- [
- "q",
- [
- 3
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.11005517318966757,
- 0.0
- ]
- },
- {
- "string": [
- [
- [
- "q",
- [
- 2
- ]
- ],
- "Z"
- ],
- [
- [
- "q",
- [
- 3
- ]
- ],
- "Z"
- ]
- ],
- "coefficient": [
- 0.11006740930024865,
- 0.0
- ]
- }
-]
\ No newline at end of file
diff --git a/examples/phase_est.png b/examples/images/phase_est.png
similarity index 100%
rename from examples/phase_est.png
rename to examples/images/phase_est.png
diff --git a/examples/qft.png b/examples/images/qft.png
similarity index 100%
rename from examples/qft.png
rename to examples/images/qft.png
diff --git a/examples/maintained-notebooks.txt b/examples/maintained-notebooks.txt
index 19cfebee..7033ba8f 100644
--- a/examples/maintained-notebooks.txt
+++ b/examples/maintained-notebooks.txt
@@ -16,6 +16,7 @@ mapping_example
spam_example
symbolics_example
pytket-qujax_qaoa
+pytket-qujax_heisenberg_vqe
pytket-qujax-classification
ucc_vqe
phase_estimation
diff --git a/examples/mapping_example.ipynb b/examples/mapping_example.ipynb
index bbb4001b..28d8e4d2 100644
--- a/examples/mapping_example.ipynb
+++ b/examples/mapping_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Qubit Mapping and Routing"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this tutorial we will show how the problem of mapping from logical quantum circuits to physically permitted circuits is solved automatically in TKET. The basic examples require only the installation of pytket, ```pip install pytket```."]}, {"cell_type": "markdown", "metadata": {}, "source": ["There is a wide variety of different blueprints for realising quantum computers, including the well known superconducting and ion trap devices. Different devices come with different constraints, such as a limited primitive gate set for universal quantum computing. Often this limited gate set accommodates an additional constraint, that two-qubit gates can not be executed between all pairs of qubits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In software, typically this constraint is presented as a \"connectivity\" graph where vertices connected by an edge represents pairs of physical qubits which two-qubit gates can be executed on. As programmers usually write logical quantum circuits with no sense of architecture (or may want to run their circuit on a range of hardware with different connectivity constraints), most quantum software development kits offer the means to automatically solve this constraint. One common way is to automatically add logical ```SWAP``` gates to a Circuit, changing the position of logical qubits on physical qubits until a two-qubit gate can be realised. This is an active area of research in quantum computing and a problem we discuss in our paper \"On The Qubit Routing Problem\" - arXiv:1902.08091."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In TKET this constraint is represented by the ```Architecture``` class. An Architecture object requires a coupling map to be created, a list of pairs of qubits which defines where two-qubit primitives may be executed. A coupling map can be produced naively by the integer indexing of nodes and edges in some architecture."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.architecture import Architecture\n", "from pytket.circuit import Node"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import networkx as nx\n", "from typing import List, Union, Tuple"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def draw_graph(coupling_map: List[Union[Tuple[int, int], Tuple[Node, Node]]]):\n", " coupling_graph = nx.Graph(coupling_map)\n", " nx.draw(coupling_graph, labels={node: node for node in coupling_graph.nodes()})"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["simple_coupling_map = [(0, 1), (1, 2), (2, 3)]\n", "simple_architecture = Architecture(simple_coupling_map)\n", "draw_graph(simple_coupling_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively we could use the `Node` class to assign our nodes - you will see why this can be helpful later. Lets create an Architecture with an identical graph:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["node_0 = Node(\"e0\", 0)\n", "node_1 = Node(\"e1\", 1)\n", "node_2 = Node(\"e2\", 2)\n", "node_3 = Node(\"e3\", 3)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["id_coupling_map = [(node_0, node_1), (node_1, node_2), (node_2, node_3)]\n", "id_architecture = Architecture(id_coupling_map)\n", "draw_graph(id_coupling_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can also create an ID with an arbitrary-dimensional index. Let us make a 2x2x2 cube:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["node_000 = Node(\"cube\", [0, 0, 0])\n", "node_001 = Node(\"cube\", [0, 0, 1])\n", "node_010 = Node(\"cube\", [0, 1, 0])\n", "node_011 = Node(\"cube\", [0, 1, 1])\n", "node_100 = Node(\"cube\", [1, 0, 0])\n", "node_101 = Node(\"cube\", [1, 0, 1])\n", "node_110 = Node(\"cube\", [1, 1, 0])\n", "node_111 = Node(\"cube\", [1, 1, 1])"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cube_coupling_map = [\n", " (node_000, node_001),\n", " (node_000, node_010),\n", " (node_010, node_011),\n", " (node_001, node_011),\n", " (node_000, node_100),\n", " (node_001, node_101),\n", " (node_010, node_110),\n", " (node_011, node_111),\n", " (node_100, node_101),\n", " (node_100, node_110),\n", " (node_110, node_111),\n", " (node_101, node_111),\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cube_architecture = Architecture(cube_coupling_map)\n", "draw_graph(cube_coupling_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To avoid that tedium though we could just use our SquareGrid Architecture:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.architecture import SquareGrid"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["alternative_cube_architecture = SquareGrid(2, 2, 2)\n", "draw_graph(alternative_cube_architecture.coupling)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The current range of quantum computers are commonly referred to as Noisy-Intermediate-Scale-Quantum devices i.e. NISQ devices. The impact of noise is a primary concern during compilation and incentivizes producing physically permitted circuits that have a minimal number of gates. For this reason benchmarking in this area is often completed by comparing the final number of two-qubit (or particularly SWAP gates) in compiled circuits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["However it is important to remember that adding logical SWAP gates to minimise gate count is not the only way this constraint can be met, with large scale architecture-aware synthesis methods and fidelity aware methods amongst other approaches producing viable physically permitted circuits. It is likely that no SINGLE approach is better for all circuits, but the ability to use different approaches where best fitted will give the best results during compilation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Producing physically valid circuits is completed via the `MappingManager` class, which aims to accomodate a wide range of approaches."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import MappingManager"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A `MappingManager` object requires an `Architecture` object at construction."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager = MappingManager(id_architecture)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["All mapping is done through the `MappingManager.route_circuit` method. The `MappingManager.route_circuit` method has two arguments, the first a Circuit to be routed (which is mutated), the second a `List[RoutingMethodCircuit]` object that defines how the mapping is completed."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Later we will look at defining our own `RoutingMethodCircuit` objects, but initially lets consider one thats already available."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import LexiLabellingMethod, LexiRouteRoutingMethod"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["lexi_label = LexiLabellingMethod()\n", "lexi_route = LexiRouteRoutingMethod(10)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `lexi_route` object here is of little use outside `MappingManager`. Note that it takes a lookahead parameter, which will affect the performance of the method, defining the number of two-qubit gates it considers when finding `SWAP` gates to add."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, OpType\n", "from pytket.circuit import display"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can also look at which logical qubits are interacting."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import Graph"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["Graph(c).get_qubit_graph()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["By running the `MappingManager.route_circuit` method on our circuit `c` with the `LexiLabellingMethod` and `LexiRouteRoutingMethod` objects as an argument, qubits in `c` with some physical requirements will be relabelled and the qubit graph modified (by the addition of SWAP gates and relabelling some CX as BRIDGE gates) such that the qubit graph is isomorphic to some subgraph of the full architecture."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager.route_circuit(c, [lexi_label, lexi_route])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The graph:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["Graph(c).get_qubit_graph()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The resulting circuit may also change if we reduce the lookahead parameter."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "mapping_manager.route_circuit(c, [lexi_label, LexiRouteRoutingMethod(1)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can also pass multiple `RoutingMethod` options for Routing in a ranked List. Each `RoutingMethod` option has a function for checking whether it can usefully modify a subcircuit at a stage in Routing. To choose, each method in the List is checked in order until one returns True. This will be discussed more later."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can aid the mapping procedure by relabelling qubits in advance. This can be completed using the `Placement` class."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.placement import Placement, LinePlacement, GraphPlacement"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The default ```Placement``` assigns logical qubits to physical qubits as they are encountered during routing. ```LinePlacement``` uses a strategy described in https://arxiv.org/abs/1902.08091. ```GraphPlacement``` is described in Section 7.1 of https://arxiv.org/abs/2003.10611. Lets look at how we can use the ```LinePlacement``` class.`"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["line_placement = LinePlacement(id_architecture)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "line_placement.place(c)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that one qubit remains unplaced in this example. `LexiRouteRoutingMethod` will dynamically assign it during mapping."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Different placements will lead to different selections of SWAP gates being added. However each different routed circuit will preserve the original unitary action of the full circuit while respecting connectivity constraints."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager.route_circuit(c, [lexi_label, lexi_route])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The graph:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["Graph(c).get_qubit_graph()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["However, small changes to the depth of lookahead or the original assignment of `Architecture` `Node` can greatly affect the resulting physical circuit for the `LexiRouteRoutingMethod` method. Considering this variance, it should be possible to easily throw additional computational resources at the problem if necessary, which is something TKET is leaning towards with the ability to define custom `RoutingCircuitMethod` objects."]}, {"cell_type": "markdown", "metadata": {}, "source": ["To define a new `RoutingMethodCircuit` method though, we first need to understand how it is used in `MappingManager` and routing. The `MappingManager.route_circuit` method treats the global problem of mapping to physical circuits as many sequential sub-problems. Consider the following problem."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.placement import place_with_map"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(4).CX(0, 1).CX(1, 2).CX(0, 2).CX(0, 3).CX(2, 3).CX(1, 3).CX(0, 1)\n", "naive_map = {\n", " circ.qubits[0]: node_0,\n", " circ.qubits[1]: node_1,\n", " circ.qubits[2]: node_2,\n", " circ.qubits[3]: node_3,\n", "}\n", "place_with_map(circ, naive_map)\n", "Graph(circ).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["So what happens when we run the following?"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager.route_circuit(circ, [lexi_route])\n", "Graph(circ).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Sequential mapping typically works by partitioning the circuit into two, a first partition comprising a connected subcircuit that is physically permitted, a second partition that is not. Therefore, the first thing `MappingManager.route_circuit` does is find this partition for the passed circuit, by iterating through gates in the circuit."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We will construct the partitions ourselves for illustrative purposes. Lets assume we are routing for the four qubit line architecture (qubits are connected to adjacent indices) \"simple_architecture\" we constructed earlier."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_first_partition = Circuit(4).CX(0, 1).CX(1, 2)\n", "place_with_map(circ_first_partition, naive_map)\n", "Graph(circ_first_partition).get_DAG()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_second_partition = Circuit(4).CX(0, 2).CX(0, 3).CX(2, 3).CX(1, 3).CX(0, 1)\n", "place_with_map(circ_second_partition, naive_map)\n", "Graph(circ_second_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that there are gates in the second partition that would be physically permitted, if they were not dependent on other gates that are not."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The next step is to modify the second partition circuit to move it closer being physically permitted. Here the `LexiRouteRoutingMethod` as before will either insert a SWAP gate at the start of the partition, or will substitute a CX gate in the first slice of the partition with a BRIDGE gate."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The option taken by `LexiRouteRoutingethod(1)` is to insert a SWAP gate between the first two nodes of the architecture, swapping their logical states. How does this change the second partition circuit?"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_second_partition = (\n", " Circuit(4).SWAP(0, 1).CX(1, 2).CX(1, 3).CX(2, 3).CX(0, 3).CX(1, 0)\n", ")\n", "place_with_map(circ_second_partition, naive_map)\n", "Graph(circ_second_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Leaving the full circuit as:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["full_circuit = (\n", " Circuit(4).CX(0, 1).CX(1, 2).SWAP(0, 1).CX(1, 2).CX(1, 3).CX(2, 3).CX(0, 3).CX(1, 0)\n", ")\n", "place_with_map(full_circuit, naive_map)\n", "Graph(full_circuit).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["After a modification is made the partition is updated."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first partition:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_first_partition = Circuit(4).CX(0, 1).CX(1, 2).SWAP(0, 1).CX(1, 2)\n", "place_with_map(circ_first_partition, naive_map)\n", "Graph(circ_first_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The second partition:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_second_partition = Circuit(4).CX(1, 3).CX(2, 3).CX(0, 3).CX(1, 0)\n", "place_with_map(circ_second_partition, naive_map)\n", "Graph(circ_second_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This pattern of modification and upating the partition is repeated until the partition has reached the end of the circuit, i.e. the back side of the partition has no gates in it. Also note that the process of updating the partition has been simplified for this example with \"physically permitted\" encapsulating two-qubit gate constraints only - in the future we expect other arity gates to provide constraints that need to be met. Also note that any modification to the second circuit can willfully modify the qubit labelling and a token swapping network will be automatically added to conform to the new labelling."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We now enough about how `MappingManager` works to add our own `RoutingMethodCircuit`. While `LexiRouteRoutingMethod` is implemented in c++ TKET, giving it some advantages, via lambda functions we can define our own `RoutingMethodCircuit` in python."]}, {"cell_type": "markdown", "metadata": {}, "source": ["A python defined `RoutingMethodCircuit` requires three arguments. The first is a function that given a Circuit (the circuit after the partition) and an Architecture, returns a bool (determining whether the new circuit should be substitued in a full routing process), a new Circuit (a modification of the original circuit such as an added SWAP) a Dict between qubits reflecting any relabelling done in the method, and a Dict between qubits giving any implicit permutation of qubits (such as by adding a SWAP). For some clarity (we will write an example later), lets look at an example function declaration."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import Dict"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def route_subcircuit_func(\n", " circuit: Circuit, architecture: Architecture\n", ") -> Tuple[bool, Circuit, Dict[Node, Node], Dict[Node, Node]]:\n", " return ()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first return is a bool which detemrines if a given `RoutingMethodCircuit` is suitable for providing a solution at a given partition. `MappingManager.route_circuit` accepts a List of of `RoutingMethod` defining how solutions are found. At the point the partition circuit is modified, the circuit is passed to `RoutingMethodCircuit.routing_method` which additionally to finding a subcircuit substitution, should determine whether it can or can't helpfully modify the partition boundary circuit, and return True if it can. The first `RoutingMethodCircuit` to return True is then used for modification - meaning the ordering of List elements is important."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The third argument sets the maximum number of gates given in the passed Circuit and the fourth argument sets the maximum depth in the passed Circuit."]}, {"cell_type": "markdown", "metadata": {}, "source": ["`LexiRouteRoutingMethod` will always return True, because it can always find some helpful SWAP to insert, and it can dynamically assign logical to physical qubits. Given this, lets construct a more specialised modification - an architecture-aware decomposition of a distance-2 CRy gate. Lets write our function type declarations for each method:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def distance2_CRy_decomp(\n", " circuit: Circuit, architecture: Architecture\n", ") -> Tuple[bool, Circuit, Dict[Node, Node], Dict[Node, Node]]:\n", " return (False, Circuit(), {}, {})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Where do we start? Lets define a simple scope for our solution: for a single gate in the passed circuit (the circuit after the partition) that has OpType CRy, if the two qubits it's acting on are at distance 2 on the architecture, decompose the gate using BRIDGE gates."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first restriction is to only have a single gate from the first slice - we can achieve this by setting both the maximum depth and size parameters to 1."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The second restriction is for the gate to have OpType CRy and for the qubits to be at distance 2 - we can check this restriction in a `distance2_CRy_check` method."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def distance2_CRy_check(circuit: Circuit, architecture: Architecture) -> bool:\n", " if circuit.n_gates != 1:\n", " raise ValueError(\n", " \"Circuit for CRy check should only have 1 gate, please change parameters of method declaration.\"\n", " )\n", " command = circuit.get_commands()[0]\n", " if command.op.type == OpType.CRy:\n", " # Architecture stores qubits under `Node` identifier\n", " n0 = Node(command.qubits[0].reg_name, command.qubits[0].index)\n", " n1 = Node(command.qubits[1].reg_name, command.qubits[1].index)\n", " # qubits could not be placed in circuit, so check before finding distance\n", " if n0 in architecture.nodes and n1 in architecture.nodes:\n", " # means we can run the decomposition\n", " if architecture.get_distance(n0, n1) == 2:\n", " return True\n", " return False"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `distance2_CRy_check` confirms whether the required restrictions are respected. Given this, if the `distance2_CRy_decomp` method is called we know where to add the decomposition."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def distance2_CRy_decomp(\n", " circuit: Circuit, architecture: Architecture\n", ") -> Tuple[bool, Circuit, Dict[Node, Node], Dict[Node, Node]]:\n", " worthwhile_substitution = distance2_CRy_check(circuit, architecture)\n", " if worthwhile_substitution == False:\n", " return (False, Circuit(), {}, {})\n", " command = circuit.get_commands()[0]\n", " qubits = command.qubits\n", " # Architecture stores qubits under `Node` identifier\n", " n0 = Node(qubits[0].reg_name, qubits[0].index)\n", " n1 = Node(qubits[1].reg_name, qubits[1].index)\n\n", " # need to find connecting node for decomposition\n", " adjacent_nodes_0 = architecture.get_adjacent_nodes(n0)\n", " adjacent_nodes_1 = architecture.get_adjacent_nodes(n1)\n", " connecting_nodes = adjacent_nodes_0.intersection(adjacent_nodes_1)\n", " if len(connecting_nodes) == 0:\n", " raise ValueError(\"Qubits for distance-2 CRy decomp are not at distance 2.\")\n", " connecting_node = connecting_nodes.pop()\n", " c = Circuit()\n\n", " # the \"relabelling map\" empty, and the permutation map is qubit to qubit, so add here\n", " permutation_map = dict()\n", " for q in circuit.qubits:\n", " permutation_map[q] = q\n", " c.add_qubit(q)\n", " # rotation, can assume only parameter as CRy\n", " angle = command.op.params[0]\n", " c.Ry(angle, qubits[1])\n", " # distance-2 CX decomp\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n", " # rotation\n", " c.Ry(-1 * angle, qubits[1])\n", " # distance-2 CX decomp\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n\n", " # the \"relabelling map\" is just qubit to qubit\n", " return (True, c, {}, permutation_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Before turning this into a `RoutingMethod` we can try it ourselves."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c = Circuit(4)\n", "test_c.CRy(0.6, 0, 2)\n", "place_with_map(test_c, naive_map)\n", "Graph(test_c).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["As we can see, our circuit has one CRy gate at distance two away."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(distance2_CRy_check(test_c, id_architecture))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Our method returns True, as expected! We should also test cases where it returns errors or False."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c_false = Circuit(4)\n", "test_c_false.CRy(0.4, 0, 1)\n", "place_with_map(test_c_false, naive_map)\n", "print(distance2_CRy_check(test_c_false, id_architecture))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c_error = Circuit(4)\n", "test_c_error.CRy(0.6, 0, 2)\n", "test_c_error.CRy(0.4, 0, 1)\n", "place_with_map(test_c_error, naive_map)\n", "try:\n", " distance2_CRy_check(test_c_error, id_architecture)\n", "except ValueError:\n", " print(\"Error reached!\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Does the decomposition work?"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c = Circuit(4)\n", "test_c.CRy(0.6, 0, 2)\n", "place_with_map(test_c, naive_map)\n", "decomp = distance2_CRy_decomp(test_c, id_architecture)\n", "display.render_circuit_jupyter(decomp[1])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Great! Our check function and decomposition method are both working. Lets wrap them into a `RoutingMethodCircuit` and try them out."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import RoutingMethodCircuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cry_rmc = RoutingMethodCircuit(distance2_CRy_decomp, 1, 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can use our original `MappingManager` object as it is defined for the same architecture. Lets try it out on a range of circumstances."]}, {"cell_type": "markdown", "metadata": {}, "source": ["If we pass it a full CX circuit without `LexiRouteRoutingMethod`, we should find that `MappingManager` throws an error, as none of the passed methods can route for the given circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "place_with_map(c, naive_map)\n", "try:\n", " mapping_manager.route_circuit(c, [cry_rmc])\n", "except RuntimeError:\n", " print(\"Error reached!\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can add `LexiRouteRoutingMethod` on top:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "place_with_map(c, naive_map)\n", "mapping_manager.route_circuit(c, [cry_rmc, LexiRouteRoutingMethod(10)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["However as there are no CRy gates our new method is unused. We can add one:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CRy(0.6, 0, 2)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "mapping_manager.route_circuit(c, [lexi_label, cry_rmc, LexiRouteRoutingMethod(10)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This time we can see our decomposition! If we reorder the methods though `LexiRouteRoutingMethod` is checked first (and returns True), so our new method is unused. The order is important!"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Finally, lets see what happens if the gate is not at the right distance initially."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CRy(0.6, 0, 3)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "mapping_manager.route_circuit(c, [lexi_label, cry_rmc, LexiRouteRoutingMethod(10)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Above a SWAP gate is inserted by `LexiRouteRoutingMethod` before anything else."]}, {"cell_type": "markdown", "metadata": {}, "source": ["For anyone interested, a simple extension exercise could be to extend this to additionally work for distance-2 CRx and CRz. Alternatively one could improve on the method itself - this approach always decomposes a CRy at distance-2, but is this a good idea?"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Also note that higher performance solutions are coded straight into the TKET c++ codebase. This provides advantages, including that Circuit construction and substitution is unncessary (as with python) as the circuit can be directly modified, however the ability to produce prototypes at the python level is very helpful. If you have a great python implementation but are finding some runtime bottlenecks, why not try implementing it straight into TKET (the code is open source at https://github.com/CQCL/tket)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Besides the `LexiRouteRoutingMethod()` and the `LexiLabellingMethod()` there are other routing methods in pytket, such as the `AASRouteRoutingMethod()` and the corresponding `AASLabellingMethod()`, which are used to route phase-polynomial boxes using architecture-aware synthesis. Usually circuits contain non-phase-polynomial operations as well, so it is a good idea to combine them with the `LexiRouteRoutingMethod()`, as in the following example:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import AASRouteRoutingMethod, AASLabellingMethod\n", "from pytket.circuit import PhasePolyBox, Qubit\n", "import numpy as np"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3, 3)\n", "n_qb = 3\n", "qubit_indices = {Qubit(0): 0, Qubit(1): 1, Qubit(2): 2}\n", "phase_polynomial = {(True, False, True): 0.333, (False, False, True): 0.05}\n", "linear_transformation = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])\n", "p_box = PhasePolyBox(n_qb, qubit_indices, phase_polynomial, linear_transformation)\n", "c.add_phasepolybox(p_box, [0, 1, 2])\n", "c.CX(0, 1).CX(0, 2).CX(1, 2)\n", "display.render_circuit_jupyter(c)\n", "nodes = [Node(\"test\", 0), Node(\"test\", 1), Node(\"test\", 2)]\n", "arch = Architecture([[nodes[0], nodes[1]], [nodes[1], nodes[2]]])\n", "mm = MappingManager(arch)\n", "mm.route_circuit(\n", " c,\n", " [\n", " AASRouteRoutingMethod(1),\n", " LexiLabellingMethod(),\n", " LexiRouteRoutingMethod(),\n", " AASLabellingMethod(),\n", " ],\n", ")\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this case the order of the methods is not very relevant, because in each step of the routing only one of the methods is suitable. In the first part of the circuit the mapping is done without inserting swaps by the AAS method; in the second part one swap gate is added to the circuit."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Qubit mapping and routing"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this tutorial we will show how the problem of mapping from logical quantum circuits to physically permitted circuits is solved automatically in TKET. The basic examples require only the installation of pytket, ```pip install pytket```."]}, {"cell_type": "markdown", "metadata": {}, "source": ["There is a wide variety of different blueprints for realising quantum computers, including the well known superconducting and ion trap devices. Different devices come with different constraints, such as a limited primitive gate set for universal quantum computing. Often this limited gate set accommodates an additional constraint, that two-qubit gates can not be executed between all pairs of qubits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In software, typically this constraint is presented as a \"connectivity\" graph where vertices connected by an edge represents pairs of physical qubits which two-qubit gates can be executed on. As programmers usually write logical quantum circuits with no sense of architecture (or may want to run their circuit on a range of hardware with different connectivity constraints), most quantum software development kits offer the means to automatically solve this constraint. One common way is to automatically add logical ```SWAP``` gates to a Circuit, changing the position of logical qubits on physical qubits until a two-qubit gate can be realised. This is an active area of research in quantum computing and a problem we discuss in our paper \"On The Qubit Routing Problem\" - arXiv:1902.08091."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In TKET this constraint is represented by the ```Architecture``` class. An Architecture object requires a coupling map to be created, a list of pairs of qubits which defines where two-qubit primitives may be executed. A coupling map can be produced naively by the integer indexing of nodes and edges in some architecture."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.architecture import Architecture\n", "from pytket.circuit import Node"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import networkx as nx\n", "from typing import List, Union, Tuple"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def draw_graph(coupling_map: List[Union[Tuple[int, int], Tuple[Node, Node]]]):\n", " coupling_graph = nx.Graph(coupling_map)\n", " nx.draw(coupling_graph, labels={node: node for node in coupling_graph.nodes()})"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["simple_coupling_map = [(0, 1), (1, 2), (2, 3)]\n", "simple_architecture = Architecture(simple_coupling_map)\n", "draw_graph(simple_coupling_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively we could use the `Node` class to assign our nodes - you will see why this can be helpful later. Lets create an Architecture with an identical graph:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["node_0 = Node(\"e0\", 0)\n", "node_1 = Node(\"e1\", 1)\n", "node_2 = Node(\"e2\", 2)\n", "node_3 = Node(\"e3\", 3)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["id_coupling_map = [(node_0, node_1), (node_1, node_2), (node_2, node_3)]\n", "id_architecture = Architecture(id_coupling_map)\n", "draw_graph(id_coupling_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can also create an ID with an arbitrary-dimensional index. Let us make a 2x2x2 cube:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["node_000 = Node(\"cube\", [0, 0, 0])\n", "node_001 = Node(\"cube\", [0, 0, 1])\n", "node_010 = Node(\"cube\", [0, 1, 0])\n", "node_011 = Node(\"cube\", [0, 1, 1])\n", "node_100 = Node(\"cube\", [1, 0, 0])\n", "node_101 = Node(\"cube\", [1, 0, 1])\n", "node_110 = Node(\"cube\", [1, 1, 0])\n", "node_111 = Node(\"cube\", [1, 1, 1])"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cube_coupling_map = [\n", " (node_000, node_001),\n", " (node_000, node_010),\n", " (node_010, node_011),\n", " (node_001, node_011),\n", " (node_000, node_100),\n", " (node_001, node_101),\n", " (node_010, node_110),\n", " (node_011, node_111),\n", " (node_100, node_101),\n", " (node_100, node_110),\n", " (node_110, node_111),\n", " (node_101, node_111),\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cube_architecture = Architecture(cube_coupling_map)\n", "draw_graph(cube_coupling_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To avoid that tedium though we could just use our SquareGrid Architecture:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.architecture import SquareGrid"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["alternative_cube_architecture = SquareGrid(2, 2, 2)\n", "draw_graph(alternative_cube_architecture.coupling)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The current range of quantum computers are commonly referred to as Noisy-Intermediate-Scale-Quantum devices i.e. NISQ devices. The impact of noise is a primary concern during compilation and incentivizes producing physically permitted circuits that have a minimal number of gates. For this reason benchmarking in this area is often completed by comparing the final number of two-qubit (or particularly SWAP gates) in compiled circuits."]}, {"cell_type": "markdown", "metadata": {}, "source": ["However it is important to remember that adding logical SWAP gates to minimise gate count is not the only way this constraint can be met, with large scale architecture-aware synthesis methods and fidelity aware methods amongst other approaches producing viable physically permitted circuits. It is likely that no SINGLE approach is better for all circuits, but the ability to use different approaches where best fitted will give the best results during compilation."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Producing physically valid circuits is completed via the `MappingManager` class, which aims to accomodate a wide range of approaches."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import MappingManager"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A `MappingManager` object requires an `Architecture` object at construction."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager = MappingManager(id_architecture)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["All mapping is done through the `MappingManager.route_circuit` method. The `MappingManager.route_circuit` method has two arguments, the first a Circuit to be routed (which is mutated), the second a `List[RoutingMethodCircuit]` object that defines how the mapping is completed."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Later we will look at defining our own `RoutingMethodCircuit` objects, but initially lets consider one thats already available."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import LexiLabellingMethod, LexiRouteRoutingMethod"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["lexi_label = LexiLabellingMethod()\n", "lexi_route = LexiRouteRoutingMethod(10)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `lexi_route` object here is of little use outside `MappingManager`. Note that it takes a lookahead parameter, which will affect the performance of the method, defining the number of two-qubit gates it considers when finding `SWAP` gates to add."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit, OpType\n", "from pytket.circuit import display"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can also look at which logical qubits are interacting."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils import Graph"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["Graph(c).get_qubit_graph()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["By running the `MappingManager.route_circuit` method on our circuit `c` with the `LexiLabellingMethod` and `LexiRouteRoutingMethod` objects as an argument, qubits in `c` with some physical requirements will be relabelled and the qubit graph modified (by the addition of SWAP gates and relabelling some CX as BRIDGE gates) such that the qubit graph is isomorphic to some subgraph of the full architecture."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager.route_circuit(c, [lexi_label, lexi_route])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The graph:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["Graph(c).get_qubit_graph()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The resulting circuit may also change if we reduce the lookahead parameter."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "mapping_manager.route_circuit(c, [lexi_label, LexiRouteRoutingMethod(1)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can also pass multiple `RoutingMethod` options for Routing in a ranked List. Each `RoutingMethod` option has a function for checking whether it can usefully modify a subcircuit at a stage in Routing. To choose, each method in the List is checked in order until one returns True. This will be discussed more later."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can aid the mapping procedure by relabelling qubits in advance. This can be completed using the `Placement` class."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.placement import Placement, LinePlacement, GraphPlacement"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The default ```Placement``` assigns logical qubits to physical qubits as they are encountered during routing. ```LinePlacement``` uses a strategy described in https://arxiv.org/abs/1902.08091. ```GraphPlacement``` is described in Section 7.1 of https://arxiv.org/abs/2003.10611. Lets look at how we can use the ```LinePlacement``` class.`"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["line_placement = LinePlacement(id_architecture)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "line_placement.place(c)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that one qubit remains unplaced in this example. `LexiRouteRoutingMethod` will dynamically assign it during mapping."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Different placements will lead to different selections of SWAP gates being added. However each different routed circuit will preserve the original unitary action of the full circuit while respecting connectivity constraints."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager.route_circuit(c, [lexi_label, lexi_route])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The graph:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["Graph(c).get_qubit_graph()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["However, small changes to the depth of lookahead or the original assignment of `Architecture` `Node` can greatly affect the resulting physical circuit for the `LexiRouteRoutingMethod` method. Considering this variance, it should be possible to easily throw additional computational resources at the problem if necessary, which is something TKET is leaning towards with the ability to define custom `RoutingCircuitMethod` objects."]}, {"cell_type": "markdown", "metadata": {}, "source": ["To define a new `RoutingMethodCircuit` method though, we first need to understand how it is used in `MappingManager` and routing. The `MappingManager.route_circuit` method treats the global problem of mapping to physical circuits as many sequential sub-problems. Consider the following problem."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.placement import place_with_map"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ = Circuit(4).CX(0, 1).CX(1, 2).CX(0, 2).CX(0, 3).CX(2, 3).CX(1, 3).CX(0, 1)\n", "naive_map = {\n", " circ.qubits[0]: node_0,\n", " circ.qubits[1]: node_1,\n", " circ.qubits[2]: node_2,\n", " circ.qubits[3]: node_3,\n", "}\n", "place_with_map(circ, naive_map)\n", "Graph(circ).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["So what happens when we run the following?"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["mapping_manager.route_circuit(circ, [lexi_route])\n", "Graph(circ).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Sequential mapping typically works by partitioning the circuit into two, a first partition comprising a connected subcircuit that is physically permitted, a second partition that is not. Therefore, the first thing `MappingManager.route_circuit` does is find this partition for the passed circuit, by iterating through gates in the circuit."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We will construct the partitions ourselves for illustrative purposes. Lets assume we are routing for the four qubit line architecture (qubits are connected to adjacent indices) \"simple_architecture\" we constructed earlier."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_first_partition = Circuit(4).CX(0, 1).CX(1, 2)\n", "place_with_map(circ_first_partition, naive_map)\n", "Graph(circ_first_partition).get_DAG()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_second_partition = Circuit(4).CX(0, 2).CX(0, 3).CX(2, 3).CX(1, 3).CX(0, 1)\n", "place_with_map(circ_second_partition, naive_map)\n", "Graph(circ_second_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that there are gates in the second partition that would be physically permitted, if they were not dependent on other gates that are not."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The next step is to modify the second partition circuit to move it closer being physically permitted. Here the `LexiRouteRoutingMethod` as before will either insert a SWAP gate at the start of the partition, or will substitute a CX gate in the first slice of the partition with a BRIDGE gate."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The option taken by `LexiRouteRoutingethod(1)` is to insert a SWAP gate between the first two nodes of the architecture, swapping their logical states. How does this change the second partition circuit?"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_second_partition = (\n", " Circuit(4).SWAP(0, 1).CX(1, 2).CX(1, 3).CX(2, 3).CX(0, 3).CX(1, 0)\n", ")\n", "place_with_map(circ_second_partition, naive_map)\n", "Graph(circ_second_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Leaving the full circuit as:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["full_circuit = (\n", " Circuit(4).CX(0, 1).CX(1, 2).SWAP(0, 1).CX(1, 2).CX(1, 3).CX(2, 3).CX(0, 3).CX(1, 0)\n", ")\n", "place_with_map(full_circuit, naive_map)\n", "Graph(full_circuit).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["After a modification is made the partition is updated."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first partition:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_first_partition = Circuit(4).CX(0, 1).CX(1, 2).SWAP(0, 1).CX(1, 2)\n", "place_with_map(circ_first_partition, naive_map)\n", "Graph(circ_first_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The second partition:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["circ_second_partition = Circuit(4).CX(1, 3).CX(2, 3).CX(0, 3).CX(1, 0)\n", "place_with_map(circ_second_partition, naive_map)\n", "Graph(circ_second_partition).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This pattern of modification and upating the partition is repeated until the partition has reached the end of the circuit, i.e. the back side of the partition has no gates in it. Also note that the process of updating the partition has been simplified for this example with \"physically permitted\" encapsulating two-qubit gate constraints only - in the future we expect other arity gates to provide constraints that need to be met. Also note that any modification to the second circuit can willfully modify the qubit labelling and a token swapping network will be automatically added to conform to the new labelling."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We now enough about how `MappingManager` works to add our own `RoutingMethodCircuit`. While `LexiRouteRoutingMethod` is implemented in c++ TKET, giving it some advantages, via lambda functions we can define our own `RoutingMethodCircuit` in python."]}, {"cell_type": "markdown", "metadata": {}, "source": ["A python defined `RoutingMethodCircuit` requires three arguments. The first is a function that given a Circuit (the circuit after the partition) and an Architecture, returns a bool (determining whether the new circuit should be substitued in a full routing process), a new Circuit (a modification of the original circuit such as an added SWAP) a Dict between qubits reflecting any relabelling done in the method, and a Dict between qubits giving any implicit permutation of qubits (such as by adding a SWAP). For some clarity (we will write an example later), lets look at an example function declaration."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from typing import Dict"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def route_subcircuit_func(\n", " circuit: Circuit, architecture: Architecture\n", ") -> Tuple[bool, Circuit, Dict[Node, Node], Dict[Node, Node]]:\n", " return ()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first return is a bool which detemrines if a given `RoutingMethodCircuit` is suitable for providing a solution at a given partition. `MappingManager.route_circuit` accepts a List of of `RoutingMethod` defining how solutions are found. At the point the partition circuit is modified, the circuit is passed to `RoutingMethodCircuit.routing_method` which additionally to finding a subcircuit substitution, should determine whether it can or can't helpfully modify the partition boundary circuit, and return True if it can. The first `RoutingMethodCircuit` to return True is then used for modification - meaning the ordering of List elements is important."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The third argument sets the maximum number of gates given in the passed Circuit and the fourth argument sets the maximum depth in the passed Circuit."]}, {"cell_type": "markdown", "metadata": {}, "source": ["`LexiRouteRoutingMethod` will always return True, because it can always find some helpful SWAP to insert, and it can dynamically assign logical to physical qubits. Given this, lets construct a more specialised modification - an architecture-aware decomposition of a distance-2 CRy gate. Lets write our function type declarations for each method:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def distance2_CRy_decomp(\n", " circuit: Circuit, architecture: Architecture\n", ") -> Tuple[bool, Circuit, Dict[Node, Node], Dict[Node, Node]]:\n", " return (False, Circuit(), {}, {})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Where do we start? Lets define a simple scope for our solution: for a single gate in the passed circuit (the circuit after the partition) that has OpType CRy, if the two qubits it's acting on are at distance 2 on the architecture, decompose the gate using BRIDGE gates."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The first restriction is to only have a single gate from the first slice - we can achieve this by setting both the maximum depth and size parameters to 1."]}, {"cell_type": "markdown", "metadata": {}, "source": ["The second restriction is for the gate to have OpType CRy and for the qubits to be at distance 2 - we can check this restriction in a `distance2_CRy_check` method."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def distance2_CRy_check(circuit: Circuit, architecture: Architecture) -> bool:\n", " if circuit.n_gates != 1:\n", " raise ValueError(\n", " \"Circuit for CRy check should only have 1 gate, please change parameters of method declaration.\"\n", " )\n", " command = circuit.get_commands()[0]\n", " if command.op.type == OpType.CRy:\n", " # Architecture stores qubits under `Node` identifier\n", " n0 = Node(command.qubits[0].reg_name, command.qubits[0].index)\n", " n1 = Node(command.qubits[1].reg_name, command.qubits[1].index)\n", " # qubits could not be placed in circuit, so check before finding distance\n", " if n0 in architecture.nodes and n1 in architecture.nodes:\n", " # means we can run the decomposition\n", " if architecture.get_distance(n0, n1) == 2:\n", " return True\n", " return False"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `distance2_CRy_check` confirms whether the required restrictions are respected. Given this, if the `distance2_CRy_decomp` method is called we know where to add the decomposition."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def distance2_CRy_decomp(\n", " circuit: Circuit, architecture: Architecture\n", ") -> Tuple[bool, Circuit, Dict[Node, Node], Dict[Node, Node]]:\n", " worthwhile_substitution = distance2_CRy_check(circuit, architecture)\n", " if worthwhile_substitution == False:\n", " return (False, Circuit(), {}, {})\n", " command = circuit.get_commands()[0]\n", " qubits = command.qubits\n", " # Architecture stores qubits under `Node` identifier\n", " n0 = Node(qubits[0].reg_name, qubits[0].index)\n", " n1 = Node(qubits[1].reg_name, qubits[1].index)\n\n", " # need to find connecting node for decomposition\n", " adjacent_nodes_0 = architecture.get_adjacent_nodes(n0)\n", " adjacent_nodes_1 = architecture.get_adjacent_nodes(n1)\n", " connecting_nodes = adjacent_nodes_0.intersection(adjacent_nodes_1)\n", " if len(connecting_nodes) == 0:\n", " raise ValueError(\"Qubits for distance-2 CRy decomp are not at distance 2.\")\n", " connecting_node = connecting_nodes.pop()\n", " c = Circuit()\n\n", " # the \"relabelling map\" empty, and the permutation map is qubit to qubit, so add here\n", " permutation_map = dict()\n", " for q in circuit.qubits:\n", " permutation_map[q] = q\n", " c.add_qubit(q)\n", " # rotation, can assume only parameter as CRy\n", " angle = command.op.params[0]\n", " c.Ry(angle, qubits[1])\n", " # distance-2 CX decomp\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n", " # rotation\n", " c.Ry(-1 * angle, qubits[1])\n", " # distance-2 CX decomp\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n", " c.CX(qubits[0], connecting_node).CX(connecting_node, qubits[1])\n\n", " # the \"relabelling map\" is just qubit to qubit\n", " return (True, c, {}, permutation_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Before turning this into a `RoutingMethod` we can try it ourselves."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c = Circuit(4)\n", "test_c.CRy(0.6, 0, 2)\n", "place_with_map(test_c, naive_map)\n", "Graph(test_c).get_DAG()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["As we can see, our circuit has one CRy gate at distance two away."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(distance2_CRy_check(test_c, id_architecture))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Our method returns True, as expected! We should also test cases where it returns errors or False."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c_false = Circuit(4)\n", "test_c_false.CRy(0.4, 0, 1)\n", "place_with_map(test_c_false, naive_map)\n", "print(distance2_CRy_check(test_c_false, id_architecture))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c_error = Circuit(4)\n", "test_c_error.CRy(0.6, 0, 2)\n", "test_c_error.CRy(0.4, 0, 1)\n", "place_with_map(test_c_error, naive_map)\n", "try:\n", " distance2_CRy_check(test_c_error, id_architecture)\n", "except ValueError:\n", " print(\"Error reached!\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Does the decomposition work?"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_c = Circuit(4)\n", "test_c.CRy(0.6, 0, 2)\n", "place_with_map(test_c, naive_map)\n", "decomp = distance2_CRy_decomp(test_c, id_architecture)\n", "display.render_circuit_jupyter(decomp[1])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Great! Our check function and decomposition method are both working. Lets wrap them into a `RoutingMethodCircuit` and try them out."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import RoutingMethodCircuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cry_rmc = RoutingMethodCircuit(distance2_CRy_decomp, 1, 1)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can use our original `MappingManager` object as it is defined for the same architecture. Lets try it out on a range of circumstances."]}, {"cell_type": "markdown", "metadata": {}, "source": ["If we pass it a full CX circuit without `LexiRouteRoutingMethod`, we should find that `MappingManager` throws an error, as none of the passed methods can route for the given circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "place_with_map(c, naive_map)\n", "try:\n", " mapping_manager.route_circuit(c, [cry_rmc])\n", "except RuntimeError:\n", " print(\"Error reached!\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Alternatively, we can add `LexiRouteRoutingMethod` on top:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "place_with_map(c, naive_map)\n", "mapping_manager.route_circuit(c, [cry_rmc, LexiRouteRoutingMethod(10)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["However as there are no CRy gates our new method is unused. We can add one:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CRy(0.6, 0, 2)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "mapping_manager.route_circuit(c, [lexi_label, cry_rmc, LexiRouteRoutingMethod(10)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This time we can see our decomposition! If we reorder the methods though `LexiRouteRoutingMethod` is checked first (and returns True), so our new method is unused. The order is important!"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Finally, lets see what happens if the gate is not at the right distance initially."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = (\n", " Circuit(4)\n", " .CRy(0.6, 0, 3)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .CX(0, 2)\n", " .CX(0, 3)\n", " .CX(2, 3)\n", " .CX(1, 3)\n", " .CX(0, 1)\n", " .measure_all()\n", ")\n", "mapping_manager.route_circuit(c, [lexi_label, cry_rmc, LexiRouteRoutingMethod(10)])\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Above a SWAP gate is inserted by `LexiRouteRoutingMethod` before anything else."]}, {"cell_type": "markdown", "metadata": {}, "source": ["For anyone interested, a simple extension exercise could be to extend this to additionally work for distance-2 CRx and CRz. Alternatively one could improve on the method itself - this approach always decomposes a CRy at distance-2, but is this a good idea?"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Also note that higher performance solutions are coded straight into the TKET c++ codebase. This provides advantages, including that Circuit construction and substitution is unncessary (as with python) as the circuit can be directly modified, however the ability to produce prototypes at the python level is very helpful. If you have a great python implementation but are finding some runtime bottlenecks, why not try implementing it straight into TKET (the code is open source at https://github.com/CQCL/tket)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Besides the `LexiRouteRoutingMethod()` and the `LexiLabellingMethod()` there are other routing methods in pytket, such as the `AASRouteRoutingMethod()` and the corresponding `AASLabellingMethod()`, which are used to route phase-polynomial boxes using architecture-aware synthesis. Usually circuits contain non-phase-polynomial operations as well, so it is a good idea to combine them with the `LexiRouteRoutingMethod()`, as in the following example:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.mapping import AASRouteRoutingMethod, AASLabellingMethod\n", "from pytket.circuit import PhasePolyBox, Qubit\n", "import numpy as np"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(3, 3)\n", "n_qb = 3\n", "qubit_indices = {Qubit(0): 0, Qubit(1): 1, Qubit(2): 2}\n", "phase_polynomial = {(True, False, True): 0.333, (False, False, True): 0.05}\n", "linear_transformation = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])\n", "p_box = PhasePolyBox(n_qb, qubit_indices, phase_polynomial, linear_transformation)\n", "c.add_phasepolybox(p_box, [0, 1, 2])\n", "c.CX(0, 1).CX(0, 2).CX(1, 2)\n", "display.render_circuit_jupyter(c)\n", "nodes = [Node(\"test\", 0), Node(\"test\", 1), Node(\"test\", 2)]\n", "arch = Architecture([[nodes[0], nodes[1]], [nodes[1], nodes[2]]])\n", "mm = MappingManager(arch)\n", "mm.route_circuit(\n", " c,\n", " [\n", " AASRouteRoutingMethod(1),\n", " LexiLabellingMethod(),\n", " LexiRouteRoutingMethod(),\n", " AASLabellingMethod(),\n", " ],\n", ")\n", "display.render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this case the order of the methods is not very relevant, because in each step of the routing only one of the methods is suitable. In the first part of the circuit the mapping is done without inserting swaps by the AAS method; in the second part one swap gate is added to the circuit."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/measurement_reduction_example.ipynb b/examples/measurement_reduction_example.ipynb
index daaf6b2f..5b6ea777 100644
--- a/examples/measurement_reduction_example.ipynb
+++ b/examples/measurement_reduction_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Advanced Expectation Values and Measurement Reduction"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This notebook is an advanced follow-up to the \"expectation_value_example\" notebook, focussing on reducing the number of circuits required for measurement.
\n", "
\n", "When calculating the expectation value $\\langle \\psi \\vert H \\vert \\psi \\rangle$ of some operator $H$ on a quantum computer, we prepare $\\vert \\psi \\rangle$ using a circuit, and the operator $H$ is first decomposed into a sum of smaller, tractable operators of the form $\\alpha P$, where $P \\in \\mathcal{G}_n$, the multi-qubit Pauli group. Naively, one would obtain the expectation value of each of these smaller operators individually by doing shots on the quantum computer and measuring in the correct Pauli bases. Assuming the device measures only single qubits in the $Z$-basis, this basis change requires single-qubit Clifford gates, which are \"cheaper\" (less noisy and quicker) than entangling gates. The sum of these smaller operator expectation values is then used to obtain the desired $\\langle \\psi \\vert H \\vert \\psi \\rangle$.
\n", "
\n", "However, the scaling of this process can be poor, meaning that many shots are required. Instead, several of these smaller operators can be measured simultaneously, reducing the total number of measurements. For some sets of measurements, it can be done \"for free\", meaning that no extra entangling gates are required to perform simultaneous measurement. For general commuting sets of Pauli measurements, Clifford gates are required for simultaneous measurement, including entangling gates."]}, {"cell_type": "markdown", "metadata": {}, "source": ["There are several strategies for measurement reduction throughout the literature. Examples include https://arxiv.org/abs/1908.06942, https://arxiv.org/abs/1908.08067 and https://arxiv.org/abs/1907.07859."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In `pytket`, we provide tools to perform measurement reduction. The most accessible way is to use the utils method, `get_operator_expectation_value`. This method wraps up some under-the-hood processes to allow users to calculate expectation values, agnostic to the backend, operator, or circuit. In this tutorial we will use the Qiskit Aer simulators via the `AerBackend`, for shots, and the `AerStateBackend`, for statevector simulation.
\n", "
\n", "We use the `QubitPauliOperator` class to represent the operator $H$."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Circuit, Qubit\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils import QubitPauliOperator\n", "from pytket.utils.expectations import get_operator_expectation_value\n", "from pytket.extensions.qiskit import AerBackend, AerStateBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["First, let's get some results on a toy circuit without using any measurement reduction:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["shots_backend = AerBackend()\n", "n_shots = 10000"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(5)\n", "c.H(4)\n", "c.V(2)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = shots_backend.get_compiled_circuit(c)\n", "op = QubitPauliOperator(\n", " {\n", " QubitPauliString([Qubit(0)], [Pauli.Z]): 0.1,\n", " QubitPauliString(\n", " [Qubit(0), Qubit(1), Qubit(2), Qubit(3), Qubit(4)],\n", " [Pauli.Y, Pauli.Z, Pauli.X, Pauli.X, Pauli.Y],\n", " ): 0.4,\n", " QubitPauliString([Qubit(0), Qubit(1)], [Pauli.X, Pauli.X]): 0.2,\n", " }\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["shots_result = get_operator_expectation_value(c, op, shots_backend, n_shots)\n", "print(shots_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The result should be around 0.1, although as the shot simulator is stochastic this will be inexact. Let's test to check what the exact result should be using the statevector simulator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_backend = AerStateBackend()\n", "state_result = get_operator_expectation_value(c, op, state_backend)\n", "print(state_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we can introduce measurement reduction. First we need to choose a strategy:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.partition import PauliPartitionStrat"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This first one only performs measurements on simultaneous Pauli operators when there is no cost incurred to do so."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["strat = PauliPartitionStrat.NonConflictingSets\n", "shots_result = get_operator_expectation_value(c, op, shots_backend, n_shots, strat)\n", "print(shots_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The other strategy we use groups together arbitrary Pauli operators, with the condition that all Pauli operators within a group commute. For an input circuit with $n$ qubits, our method requires the addition of up to $\\frac{n(n-1)}{2}$ $CX$ gates to \"diagonalise\" the Pauli operators, although in practice we find that our techniques tend to give far lower gate overhead than this bound. We describe the procedure in an upcoming paper."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["strat = PauliPartitionStrat.CommutingSets\n", "shots_result = get_operator_expectation_value(c, op, shots_backend, n_shots, strat)\n", "print(shots_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Obviously, the `AerBackend` can be swapped out for the backend of a real machine."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We will now demonstrate how to manually use the methods that are being called by `get_operator_expectation_value`. These methods are primarily intended for internal use, but we show them here for advanced users who may wish to have more information about the number of CX gates being added to each circuit, the number of circuits being run and other diagnostics."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import OpType\n", "from pytket.partition import measurement_reduction"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["id_string = QubitPauliString()\n", "qpt_list = [p for p in op._dict.keys() if (p != id_string)]\n", "setup_1 = measurement_reduction(qpt_list, PauliPartitionStrat.NonConflictingSets)\n", "print(\"Circuits required for measurement: {}\".format(len(setup_1.measurement_circs)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This produced a `MeasurementSetup` object using the `NonConflictingSets` strategy of measurement reduction. This object holds a set of circuits which perform different basis changes, and the measurements associated with these circuits.
\n", "
\n", "There are 3 circuits held within the `MeasurementSetup` object, meaning that our original `QubitOperator` has been reduced from the 5 originally required measurements to 3."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for circ in setup_1.measurement_circs:\n", " print(\"CX gates for measurement: {}\".format(circ.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["No CX gates have been added for any of the required measurements. Now, we will change to the `CommutingSets` strategy."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["setup_2 = measurement_reduction(qpt_list, PauliPartitionStrat.CommutingSets)\n", "print(\"Circuits required for measurement: {}\".format(len(setup_2.measurement_circs)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["There are only 2 circuits required when expanding the scope of allowed simultaneous measurements. However, this comes at a cost:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for circ in setup_2.measurement_circs:\n", " print(\"CX gates for measurement: {}\".format(circ.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A CX gate has been introduced to one of the measurement circuits, to convert to the correct Pauli basis set. On current devices which are extremely constrained in the number of entangling gates, the reduction in number of shots may not be worth the gate overhead."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Advanced expectation values and measurement reduction"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This notebook is an advanced follow-up to the \"expectation_value_example\" notebook, focussing on reducing the number of circuits required for measurement.
\n", "
\n", "When calculating the expectation value $\\langle \\psi \\vert H \\vert \\psi \\rangle$ of some operator $H$ on a quantum computer, we prepare $\\vert \\psi \\rangle$ using a circuit, and the operator $H$ is first decomposed into a sum of smaller, tractable operators of the form $\\alpha P$, where $P \\in \\mathcal{G}_n$, the multi-qubit Pauli group. Naively, one would obtain the expectation value of each of these smaller operators individually by doing shots on the quantum computer and measuring in the correct Pauli bases. Assuming the device measures only single qubits in the $Z$-basis, this basis change requires single-qubit Clifford gates, which are \"cheaper\" (less noisy and quicker) than entangling gates. The sum of these smaller operator expectation values is then used to obtain the desired $\\langle \\psi \\vert H \\vert \\psi \\rangle$.
\n", "
\n", "However, the scaling of this process can be poor, meaning that many shots are required. Instead, several of these smaller operators can be measured simultaneously, reducing the total number of measurements. For some sets of measurements, it can be done \"for free\", meaning that no extra entangling gates are required to perform simultaneous measurement. For general commuting sets of Pauli measurements, Clifford gates are required for simultaneous measurement, including entangling gates."]}, {"cell_type": "markdown", "metadata": {}, "source": ["There are several strategies for measurement reduction throughout the literature. Examples include https://arxiv.org/abs/1908.06942, https://arxiv.org/abs/1908.08067 and https://arxiv.org/abs/1907.07859."]}, {"cell_type": "markdown", "metadata": {}, "source": ["In `pytket`, we provide tools to perform measurement reduction. The most accessible way is to use the utils method, `get_operator_expectation_value`. This method wraps up some under-the-hood processes to allow users to calculate expectation values, agnostic to the backend, operator, or circuit. In this tutorial we will use the Qiskit Aer simulators via the `AerBackend`, for shots, and the `AerStateBackend`, for statevector simulation.
\n", "
\n", "We use the `QubitPauliOperator` class to represent the operator $H$."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Circuit, Qubit\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils import QubitPauliOperator\n", "from pytket.utils.expectations import get_operator_expectation_value\n", "from pytket.extensions.qiskit import AerBackend, AerStateBackend"]}, {"cell_type": "markdown", "metadata": {}, "source": ["First, let's get some results on a toy circuit without using any measurement reduction:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["shots_backend = AerBackend()\n", "n_shots = 10000"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(5)\n", "c.H(4)\n", "c.V(2)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = shots_backend.get_compiled_circuit(c)\n", "op = QubitPauliOperator(\n", " {\n", " QubitPauliString([Qubit(0)], [Pauli.Z]): 0.1,\n", " QubitPauliString(\n", " [Qubit(0), Qubit(1), Qubit(2), Qubit(3), Qubit(4)],\n", " [Pauli.Y, Pauli.Z, Pauli.X, Pauli.X, Pauli.Y],\n", " ): 0.4,\n", " QubitPauliString([Qubit(0), Qubit(1)], [Pauli.X, Pauli.X]): 0.2,\n", " }\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["shots_result = get_operator_expectation_value(c, op, shots_backend, n_shots)\n", "print(shots_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The result should be around 0.1, although as the shot simulator is stochastic this will be inexact. Let's test to check what the exact result should be using the statevector simulator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["state_backend = AerStateBackend()\n", "state_result = get_operator_expectation_value(c, op, state_backend)\n", "print(state_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we can introduce measurement reduction. First we need to choose a strategy:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.partition import PauliPartitionStrat"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This first one only performs measurements on simultaneous Pauli operators when there is no cost incurred to do so."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["strat = PauliPartitionStrat.NonConflictingSets\n", "shots_result = get_operator_expectation_value(c, op, shots_backend, n_shots, strat)\n", "print(shots_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The other strategy we use groups together arbitrary Pauli operators, with the condition that all Pauli operators within a group commute. For an input circuit with $n$ qubits, our method requires the addition of up to $\\frac{n(n-1)}{2}$ $CX$ gates to \"diagonalise\" the Pauli operators, although in practice we find that our techniques tend to give far lower gate overhead than this bound. We describe the procedure in an upcoming paper."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["strat = PauliPartitionStrat.CommutingSets\n", "shots_result = get_operator_expectation_value(c, op, shots_backend, n_shots, strat)\n", "print(shots_result)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Obviously, the `AerBackend` can be swapped out for the backend of a real machine."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We will now demonstrate how to manually use the methods that are being called by `get_operator_expectation_value`. These methods are primarily intended for internal use, but we show them here for advanced users who may wish to have more information about the number of CX gates being added to each circuit, the number of circuits being run and other diagnostics."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import OpType\n", "from pytket.partition import measurement_reduction"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["id_string = QubitPauliString()\n", "qpt_list = [p for p in op._dict.keys() if (p != id_string)]\n", "setup_1 = measurement_reduction(qpt_list, PauliPartitionStrat.NonConflictingSets)\n", "print(\"Circuits required for measurement: {}\".format(len(setup_1.measurement_circs)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This produced a `MeasurementSetup` object using the `NonConflictingSets` strategy of measurement reduction. This object holds a set of circuits which perform different basis changes, and the measurements associated with these circuits.
\n", "
\n", "There are 3 circuits held within the `MeasurementSetup` object, meaning that our original `QubitOperator` has been reduced from the 5 originally required measurements to 3."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for circ in setup_1.measurement_circs:\n", " print(\"CX gates for measurement: {}\".format(circ.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["No CX gates have been added for any of the required measurements. Now, we will change to the `CommutingSets` strategy."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["setup_2 = measurement_reduction(qpt_list, PauliPartitionStrat.CommutingSets)\n", "print(\"Circuits required for measurement: {}\".format(len(setup_2.measurement_circs)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["There are only 2 circuits required when expanding the scope of allowed simultaneous measurements. However, this comes at a cost:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for circ in setup_2.measurement_circs:\n", " print(\"CX gates for measurement: {}\".format(circ.n_gates_of_type(OpType.CX)))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["A CX gate has been introduced to one of the measurement circuits, to convert to the correct Pauli basis set. On current devices which are extremely constrained in the number of entangling gates, the reduction in number of shots may not be worth the gate overhead."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/phase_estimation.ipynb b/examples/phase_estimation.ipynb
index 80c0c9e2..fd6b13e6 100644
--- a/examples/phase_estimation.ipynb
+++ b/examples/phase_estimation.ipynb
@@ -1 +1 @@
-{"cells":[{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["#!/usr/bin/env python\n","# coding: utf-8"]},{"cell_type":"markdown","metadata":{},"source":["# Quantum Phase Estimation using `pytket` Boxes\n","\n","When constructing circuits for quantum algorithms it is useful to think of higher level operations than just individual quantum gates.\n","\n","In `pytket` we can construct circuits using box structures which abstract away the complexity of the underlying circuit.\n","\n","This notebook is intended to complement the [boxes section](https://tket.quantinuum.com/user-manual/manual_circuit.html#boxes) of the user manual which introduces the different box types.\n","\n","To demonstrate boxes in `pytket` we will consider the Quantum Phase Estimation algorithm (QPE). This is an important subroutine in several quantum algorithms including Shor's algorithm and fault-tolerant approaches to quantum chemistry.\n","\n","## Overview of Phase Estimation\n","\n","The Quantum Phase Estimation algorithm can be used to estimate the eigenvalues of some unitary operator $U$ to some desired precision.\n","\n","The eigenvalues of $U$ lie on the unit circle, giving us the following eigenvalue equation\n","\n","$$\n","\\begin{equation}\n","U |\\psi \\rangle = e^{2 \\pi i \\theta} |\\psi\\rangle\\,, \\quad 0 \\leq \\theta \\leq 1\n","\\end{equation}\n","$$\n","\n","Here $|\\psi \\rangle$ is an eigenstate of the operator $U$. In phase estimation we estimate the eigenvalue $e^{2 \\pi i \\theta}$ by approximating $\\theta$.\n","\n","\n","The circuit for Quantum phase estimation is itself composed of several subroutines which we can realise as boxes.\n","\n","![](phase_est.png \"Quantum Phase Estimation Circuit\")"]},{"cell_type":"markdown","metadata":{},"source":["QPE is generally split up into three stages\n","\n","1. Firstly we prepare an initial state in one register. In parallel we prepare a uniform superposition state using Hadamard gates on some ancilla qubits. The number of ancilla qubits determines how precisely we can estimate the phase $\\theta$.\n","\n","2. Secondly we apply successive controlled $U$ gates. This has the effect of \"kicking back\" phases onto the ancilla qubits according to the eigenvalue equation above.\n","\n","3. Finally we apply the inverse Quantum Fourier Transform (QFT). This essentially plays the role of destructive interference, suppressing amplitudes from \"undesirable states\" and hopefully allowing us to measure a single outcome (or a small number of outcomes) with high probability.\n","\n","\n","There is some subtlety around the first point. The initial state used can be an exact eigenstate of $U$ however this may be difficult to prepare if we don't know the eigenvalues of $U$ in advance. Alternatively we could use an initial state that is a linear combination of eigenstates, as the phase estimation will project into the eigenspace of $U$."]},{"cell_type":"markdown","metadata":{},"source":["We also assume that we can implement $U$ with a quantum circuit. In chemistry applications $U$ could be of the form $U=e^{-iHt}$ where $H$ is the Hamiltonian of some system of interest. In the cannonical algorithm, the number of controlled unitaries we apply scales exponentially with the number of ancilla qubits. This allows more precision at the expense of a larger quantum circuit."]},{"cell_type":"markdown","metadata":{},"source":["## The Quantum Fourier Transform"]},{"cell_type":"markdown","metadata":{},"source":["Before considering the other parts of the QPE algorithm, lets focus on the Quantum Fourier Transform (QFT) subroutine.\n","\n","Mathematically, the QFT has the following action.\n","\n","\\begin{equation}\n","QFT : |j\\rangle\\ \\longmapsto \\sum_{k=0}^{N - 1} e^{2 \\pi ijk/N}|k\\rangle, \\quad N= 2^k\n","\\end{equation}\n","\n","This is essentially the Discrete Fourier transform except the input is a quantum state $|j\\rangle$.\n","\n","It is well known that the QFT can be implemented efficiently with a quantum circuit\n","\n","We can build the circuit for the $n$ qubit QFT using $n$ Hadamard gates $\\frac{n}{2}$ swap gates and $\\frac{n(n-1)}{2}$ controlled unitary rotations $\\text{CU1}$.\n","\n","$$\n"," \\begin{equation}\n"," CU1(\\phi) =\n"," \\begin{pmatrix}\n"," I & 0 \\\\\n"," 0 & U1(\\phi)\n"," \\end{pmatrix}\n"," \\,, \\quad\n","U1(\\phi) =\n"," \\begin{pmatrix}\n"," 1 & 0 \\\\\n"," 0 & e^{i \\phi}\n"," \\end{pmatrix}\n"," \\end{equation}\n","$$\n","\n","The circuit for the Quantum Fourier transform on three qubits is the following\n","\n","![](qft.png \"QFT Circuit\")\n","\n","We can build this circuit in `pytket` by adding gate operations manually:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import Circuit\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"markdown","metadata":{},"source":["lets build the QFT for three qubits"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qft3_circ = Circuit(3)\n","qft3_circ.H(0)\n","qft3_circ.CU1(0.5, 1, 0)\n","qft3_circ.CU1(0.25, 2, 0)\n","qft3_circ.H(1)\n","qft3_circ.CU1(0.5, 2, 1)\n","qft3_circ.H(2)\n","qft3_circ.SWAP(0, 2)\n","render_circuit_jupyter(qft3_circ)"]},{"cell_type":"markdown","metadata":{},"source":["We can generalise the quantum Fourier transform to $n$ qubits by iterating over the qubits as follows"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def build_qft_circuit(n_qubits: int) -> Circuit:\n"," circ = Circuit(n_qubits, name=\"QFT\")\n"," for i in range(n_qubits):\n"," circ.H(i)\n"," for j in range(i + 1, n_qubits):\n"," circ.CU1(1 / 2 ** (j - i), j, i)\n"," for k in range(0, n_qubits // 2):\n"," circ.SWAP(k, n_qubits - k - 1)\n"," return circ"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qft4_circ: Circuit = build_qft_circuit(4)\n","render_circuit_jupyter(qft4_circ)"]},{"cell_type":"markdown","metadata":{},"source":["Now that we have the generalised circuit we can wrap it up in a `CircBox` which can then be added to another circuit as a subroutine."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import CircBox"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qft4_box: CircBox = CircBox(qft4_circ)\n","qft_circ = Circuit(4).add_gate(qft4_box, [0, 1, 2, 3])\n","render_circuit_jupyter(qft_circ)"]},{"cell_type":"markdown","metadata":{},"source":["Note how the `CircBox` inherits the name `QFT` from the underlying circuit."]},{"cell_type":"markdown","metadata":{},"source":["Recall that in our phase estimation algorithm we need to use the inverse QFT.\n","\n","$$\n","\\begin{equation}\n","\\text{QFT}^† : \\sum_{k=0}^{N - 1} e^{2 \\pi ijk/N}|k\\rangle \\longmapsto |j\\rangle\\,, \\quad N= 2^k\n","\\end{equation}\n","$$\n","\n","\n","Now that we have the QFT circuit we can obtain the inverse by using `CircBox.dagger`. We can also verify that this is correct by inspecting the circuit inside with `CircBox.get_circuit()`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["inv_qft4_box = qft4_box.dagger\n","render_circuit_jupyter(inv_qft4_box.get_circuit())"]},{"cell_type":"markdown","metadata":{},"source":["## The Controlled Unitary Operations"]},{"cell_type":"markdown","metadata":{},"source":["In the phase estimation algorithm we repeatedly perform controlled unitary operations. In the canonical variant, the number of controlled unitaries will be $2^m - 1$ where $m$ is the number of measurement qubits."]},{"cell_type":"markdown","metadata":{},"source":["The form of $U$ will vary depending on the application. For chemistry or condensed matter physics $U$ typically be the time evolution operator $U(t) = e^{- i H t}$ where $H$ is the problem Hamiltonian."]},{"cell_type":"markdown","metadata":{},"source":["Suppose that we had the following decomposition for $H$ in terms of Pauli strings $P_j$ and complex coefficients $\\alpha_j$.\n","\n","\\begin{equation}\n","H = \\sum_j \\alpha_j P_j\\,, \\quad \\, P_j \\in \\{I, X, Y, Z\\}^{\\otimes n}\n","\\end{equation}\n","\n","Here Pauli strings refers to tensor products of Pauli operators. These strings form an orthonormal basis for $2^n \\times 2^n$ matrices."]},{"cell_type":"markdown","metadata":{},"source":["If we have a Hamiltonian in the form above, we can then implement $U(t)$ as a sequence of Pauli gadget circuits. We can do this with the [PauliExpBox](https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.PauliExpBox) construct in pytket. For more on `PauliExpBox` see the [user manual](https://tket.quantinuum.com/user-manual/manual_circuit.html#pauli-exponential-boxes)."]},{"cell_type":"markdown","metadata":{},"source":["Once we have a circuit to implement our time evolution operator $U(t)$, we can construct the controlled $U(t)$ operations using [QControlBox](https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.QControlBox). If our base unitary is a sequence of `PauliExpBox`(es) then there is some structure we can exploit to simplify our circuit. See this [blog post](https://tket.quantinuum.com/tket-blog/posts/controlled_gates/) on [ConjugationBox](https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.ConjugationBox) for more."]},{"cell_type":"markdown","metadata":{},"source":["In what follows, we will just construct a simplified instance of QPE where the controlled unitaries are just $\\text{CU1}$ gates."]},{"cell_type":"markdown","metadata":{},"source":["## Putting it all together"]},{"cell_type":"markdown","metadata":{},"source":["We can now define a function to build our entire QPE circuit. We can make this function take a state preparation circuit and a unitary circuit as input as well. The function also has the number of measurement qubits as input which will determine the precision of our phase estimate."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import QControlBox"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def build_phase_est_circuit(\n"," n_measurement_qubits: int, state_prep_circuit: Circuit, unitary_circuit: Circuit\n",") -> Circuit:\n"," qpe_circ: Circuit = Circuit()\n"," n_state_prep_qubits = state_prep_circuit.n_qubits\n"," measurement_register = qpe_circ.add_q_register(\"m\", n_measurement_qubits)\n"," state_prep_register = qpe_circ.add_q_register(\"p\", n_state_prep_qubits)\n"," qpe_circ.add_circuit(state_prep_circuit, list(state_prep_register))\n","\n"," # Create a controlled unitary with a single control qubit\n"," unitary_circuit.name = \"U\"\n"," controlled_u_gate = QControlBox(CircBox(unitary_circuit), 1)\n","\n"," # Add Hadamard gates to every qubit in the measurement register\n"," for m_qubit in measurement_register:\n"," qpe_circ.H(m_qubit)\n","\n"," # Add all (2**n_measurement_qubits - 1) of the controlled unitaries sequentially\n"," for m_qubit in range(n_measurement_qubits):\n"," control_index = n_measurement_qubits - m_qubit - 1\n"," control_qubit = [measurement_register[control_index]]\n"," for _ in range(2**m_qubit):\n"," qpe_circ.add_qcontrolbox(\n"," controlled_u_gate, control_qubit + list(state_prep_register)\n"," )\n","\n"," # Finally, append the inverse qft and measure the qubits\n"," qft_box = CircBox(build_qft_circuit(n_measurement_qubits))\n"," inverse_qft_box = qft_box.dagger\n"," qpe_circ.add_circbox(inverse_qft_box, list(measurement_register))\n"," qpe_circ.measure_register(measurement_register, \"c\")\n"," return qpe_circ"]},{"cell_type":"markdown","metadata":{},"source":["## Phase Estimation with a Trivial Eigenstate\n","\n","Lets test our circuit construction by preparing a trivial $|1\\rangle$ eigenstate of the $\\text{U1}$ gate. We can then see if our phase estimation circuit returns the expected eigenvalue."]},{"cell_type":"markdown","metadata":{},"source":["$$\n","\\begin{equation}\n","U1(\\phi)|1\\rangle = e^{i\\phi} = e^{2 \\pi i \\theta} \\implies \\theta = \\frac{\\phi}{2}\n","\\end{equation}\n","$$\n","\n","So we expect that our ideal phase $\\theta$ will be half the input angle $\\phi$ to our $U1$ gate."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["prep_circuit = Circuit(1).X(0)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["input_angle = 0.73 # angle as number of half turns"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["unitary_circuit = Circuit(1).U1(input_angle, 0)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qpe_circ_trivial = build_phase_est_circuit(\n"," 4, state_prep_circuit=prep_circuit, unitary_circuit=unitary_circuit\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(qpe_circ_trivial)"]},{"cell_type":"markdown","metadata":{},"source":["Lets use the noiseless `AerBackend` simulator to run our phase estimation circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.qiskit import AerBackend"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["backend = AerBackend()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["compiled_circ = backend.get_compiled_circuit(qpe_circ_trivial)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_shots = 1000\n","result = backend.run_circuit(compiled_circ, n_shots)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(result.get_counts())"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends.backendresult import BackendResult\n","import matplotlib.pyplot as plt"]},{"cell_type":"markdown","metadata":{},"source":["plotting function for QPE Notebook"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def plot_qpe_results(\n"," sim_result: BackendResult,\n"," n_strings: int = 4,\n"," dark_mode: bool = False,\n"," y_limit: int = 1000,\n",") -> None:\n"," \"\"\"\n"," Plots results in a barchart given a BackendResult. the number of stings displayed\n"," can be specified with the n_strings argument.\n"," \"\"\"\n"," counts_dict = sim_result.get_counts()\n"," sorted_shots = counts_dict.most_common()\n"," n_most_common_strings = sorted_shots[:n_strings]\n"," x_axis_values = [str(entry[0]) for entry in n_most_common_strings] # basis states\n"," y_axis_values = [entry[1] for entry in n_most_common_strings] # counts\n"," if dark_mode:\n"," plt.style.use(\"dark_background\")\n"," fig = plt.figure()\n"," ax = fig.add_axes((0, 0, 0.75, 0.5))\n"," color_list = [\"orange\"] * (len(x_axis_values))\n"," ax.bar(\n"," x=x_axis_values,\n"," height=y_axis_values,\n"," color=color_list,\n"," )\n"," ax.set_title(label=\"Results\")\n"," plt.ylim([0, y_limit])\n"," plt.xlabel(\"Basis State\")\n"," plt.ylabel(\"Number of Shots\")\n"," plt.xticks(rotation=90)\n"," plt.show()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plot_qpe_results(result, y_limit=int(1.2 * n_shots))"]},{"cell_type":"markdown","metadata":{},"source":["As expected we see one outcome with high probability. Lets now extract our approximation of $\\theta$ from our output bitstrings.\n","\n","suppose the $j$ is an integer representation of our most commonly measured bitstring."]},{"cell_type":"markdown","metadata":{},"source":["$$\n","\\begin{equation}\n","\\theta_{estimate} = \\frac{j}{N}\n","\\end{equation}\n","$$"]},{"cell_type":"markdown","metadata":{},"source":["Here $N = 2 ^n$ where $n$ is the number of measurement qubits."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends.backendresult import BackendResult"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def single_phase_from_backendresult(result: BackendResult) -> float:\n"," # Extract most common measurement outcome\n"," basis_state = result.get_counts().most_common()[0][0]\n"," bitstring = \"\".join([str(bit) for bit in basis_state])\n"," integer = int(bitstring, 2)\n","\n"," # Calculate theta estimate\n"," return integer / (2 ** len(bitstring))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["theta = single_phase_from_backendresult(result)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(theta)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(input_angle / 2)"]},{"cell_type":"markdown","metadata":{},"source":["Our output is close to half our input angle $\\phi$ as expected. Lets calculate our error."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["error = round(abs(input_angle - (2 * theta)), 3)\n","print(error)"]},{"cell_type":"markdown","metadata":{},"source":["## Suggestions for further reading\n","\n","In this notebook we have shown the canonical variant of quantum phase estimation. There are several other variants.\n","\n","Quantinuum paper on Bayesian phase estimation -> https://arxiv.org/pdf/2306.16608.pdf\n","Blog post on `ConjugationBox` -> https://tket.quantinuum.com/tket-blog/posts/controlled_gates/ - efficient circuits for controlled Pauli gadgets.\n","\n","As mentioned quantum phase estimation is a subroutine in Shor's algorithm. Read more about how phase estimation is used in period finding."]}],"metadata":{"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.6.4"}},"nbformat":4,"nbformat_minor":2}
+{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Quantum Phase Estimation\n","\n","When constructing circuits for quantum algorithms it is useful to think of higher level operations than just individual quantum gates.\n","\n","In `pytket` we can construct circuits using box structures which abstract away the complexity of the underlying circuit.\n","\n","This notebook is intended to complement the [boxes section](https://tket.quantinuum.com/user-manual/manual_circuit.html#boxes) of the user manual which introduces the different box types.\n","\n","To demonstrate boxes in `pytket` we will consider the Quantum Phase Estimation algorithm (QPE). This is an important subroutine in several quantum algorithms including Shor's algorithm and fault-tolerant approaches to quantum chemistry.\n","\n","## Overview of Phase Estimation\n","\n","The Quantum Phase Estimation algorithm can be used to estimate the eigenvalues of some unitary operator $U$ to some desired precision.\n","\n","The eigenvalues of $U$ lie on the unit circle, giving us the following eigenvalue equation\n","\n","$$\n","\\begin{equation}\n","U |\\psi \\rangle = e^{2 \\pi i \\theta} |\\psi\\rangle\\,, \\quad 0 \\leq \\theta \\leq 1\n","\\end{equation}\n","$$\n","\n","Here $|\\psi \\rangle$ is an eigenstate of the operator $U$. In phase estimation we estimate the eigenvalue $e^{2 \\pi i \\theta}$ by approximating $\\theta$.\n","\n","\n","The circuit for Quantum phase estimation is itself composed of several subroutines which we can realise as boxes.\n","\n","![](images/phase_est.png \"Quantum Phase Estimation Circuit\")"]},{"cell_type":"markdown","metadata":{},"source":["QPE is generally split up into three stages\n","\n","1. Firstly we prepare an initial state in one register. In parallel we prepare a uniform superposition state using Hadamard gates on some ancilla qubits. The number of ancilla qubits determines how precisely we can estimate the phase $\\theta$.\n","\n","2. Secondly we apply successive controlled $U$ gates. This has the effect of \"kicking back\" phases onto the ancilla qubits according to the eigenvalue equation above.\n","\n","3. Finally we apply the inverse Quantum Fourier Transform (QFT). This essentially plays the role of destructive interference, suppressing amplitudes from \"undesirable states\" and hopefully allowing us to measure a single outcome (or a small number of outcomes) with high probability.\n","\n","\n","There is some subtlety around the first point. The initial state used can be an exact eigenstate of $U$ however this may be difficult to prepare if we don't know the eigenvalues of $U$ in advance. Alternatively we could use an initial state that is a linear combination of eigenstates, as the phase estimation will project into the eigenspace of $U$."]},{"cell_type":"markdown","metadata":{},"source":["We also assume that we can implement $U$ with a quantum circuit. In chemistry applications $U$ could be of the form $U=e^{-iHt}$ where $H$ is the Hamiltonian of some system of interest. In the cannonical algorithm, the number of controlled unitaries we apply scales exponentially with the number of ancilla qubits. This allows more precision at the expense of a larger quantum circuit."]},{"cell_type":"markdown","metadata":{},"source":["## The Quantum Fourier Transform"]},{"cell_type":"markdown","metadata":{},"source":["Before considering the other parts of the QPE algorithm, lets focus on the Quantum Fourier Transform (QFT) subroutine.\n","\n","Mathematically, the QFT has the following action.\n","\n","$$\n","\\begin{equation}\n","QFT : |j\\rangle\\ \\longmapsto \\sum_{k=0}^{N - 1} e^{2 \\pi ijk/N}|k\\rangle, \\quad N= 2^k\n","\\end{equation}\n","$$\n","\n","This is essentially the Discrete Fourier transform except the input is a quantum state $|j\\rangle$.\n","\n","It is well known that the QFT can be implemented efficiently with a quantum circuit\n","\n","We can build the circuit for the $n$ qubit QFT using $n$ Hadamard gates $\\frac{n}{2}$ swap gates and $\\frac{n(n-1)}{2}$ controlled unitary rotations $\\text{CU1}$.\n","\n","$$\n"," \\begin{equation}\n"," CU1(\\phi) =\n"," \\begin{pmatrix}\n"," I & 0 \\\\\n"," 0 & U1(\\phi)\n"," \\end{pmatrix}\n"," \\,, \\quad\n","U1(\\phi) =\n"," \\begin{pmatrix}\n"," 1 & 0 \\\\\n"," 0 & e^{i \\phi}\n"," \\end{pmatrix}\n"," \\end{equation}\n","$$\n","\n","The circuit for the Quantum Fourier transform on three qubits is the following\n","\n","![](images/qft.png \"QFT Circuit\")\n","\n","We can build this circuit in `pytket` by adding gate operations manually:"]},{"cell_type":"markdown","metadata":{},"source":["lets build the QFT for three qubits"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import Circuit\n","from pytket.circuit.display import render_circuit_jupyter"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qft3_circ = Circuit(3)\n","qft3_circ.H(0)\n","qft3_circ.CU1(0.5, 1, 0)\n","qft3_circ.CU1(0.25, 2, 0)\n","qft3_circ.H(1)\n","qft3_circ.CU1(0.5, 2, 1)\n","qft3_circ.H(2)\n","qft3_circ.SWAP(0, 2)\n","render_circuit_jupyter(qft3_circ)"]},{"cell_type":"markdown","metadata":{},"source":["We can generalise the quantum Fourier transform to $n$ qubits by iterating over the qubits as follows"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def build_qft_circuit(n_qubits: int) -> Circuit:\n"," circ = Circuit(n_qubits, name=\"QFT\")\n"," for i in range(n_qubits):\n"," circ.H(i)\n"," for j in range(i + 1, n_qubits):\n"," circ.CU1(1 / 2 ** (j - i), j, i)\n"," for k in range(0, n_qubits // 2):\n"," circ.SWAP(k, n_qubits - k - 1)\n"," return circ"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qft4_circ: Circuit = build_qft_circuit(4)\n","render_circuit_jupyter(qft4_circ)"]},{"cell_type":"markdown","metadata":{},"source":["Now that we have the generalised circuit we can wrap it up in a `CircBox` which can then be added to another circuit as a subroutine."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import CircBox"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qft4_box: CircBox = CircBox(qft4_circ)\n","qft_circ = Circuit(4).add_gate(qft4_box, [0, 1, 2, 3])\n","render_circuit_jupyter(qft_circ)"]},{"cell_type":"markdown","metadata":{},"source":["Note how the `CircBox` inherits the name `QFT` from the underlying circuit."]},{"cell_type":"markdown","metadata":{},"source":["Recall that in our phase estimation algorithm we need to use the inverse QFT.\n","\n","$$\n","\\begin{equation}\n","\\text{QFT}^† : \\sum_{k=0}^{N - 1} e^{2 \\pi ijk/N}|k\\rangle \\longmapsto |j\\rangle\\,, \\quad N= 2^k\n","\\end{equation}\n","$$\n","\n","\n","Now that we have the QFT circuit we can obtain the inverse by using `CircBox.dagger`. We can also verify that this is correct by inspecting the circuit inside with `CircBox.get_circuit()`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["inv_qft4_box = qft4_box.dagger\n","render_circuit_jupyter(inv_qft4_box.get_circuit())"]},{"cell_type":"markdown","metadata":{},"source":["## The Controlled Unitary Operations"]},{"cell_type":"markdown","metadata":{},"source":["In the phase estimation algorithm we repeatedly perform controlled unitary operations. In the canonical variant, the number of controlled unitaries will be $2^m - 1$ where $m$ is the number of measurement qubits."]},{"cell_type":"markdown","metadata":{},"source":["The form of $U$ will vary depending on the application. For chemistry or condensed matter physics $U$ typically be the time evolution operator $U(t) = e^{- i H t}$ where $H$ is the problem Hamiltonian."]},{"cell_type":"markdown","metadata":{},"source":["Suppose that we had the following decomposition for $H$ in terms of Pauli strings $P_j$ and complex coefficients $\\alpha_j$.\n","\n","$$\n","\\begin{equation}\n","H = \\sum_j \\alpha_j P_j\\,, \\quad \\, P_j \\in \\{I, \\,X, \\,Y, \\,Z\\}^{\\otimes n}\n","\\end{equation}\n","$$\n","\n","Here Pauli strings refers to tensor products of Pauli operators. These strings form an orthonormal basis for $2^n \\times 2^n$ matrices."]},{"cell_type":"markdown","metadata":{},"source":["If we have a Hamiltonian in the form above, we can then implement $U(t)$ as a sequence of Pauli gadget circuits. We can do this with the [PauliExpBox](https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.PauliExpBox) construct in pytket. For more on `PauliExpBox` see the [user manual](https://tket.quantinuum.com/user-manual/manual_circuit.html#pauli-exponential-boxes)."]},{"cell_type":"markdown","metadata":{},"source":["Once we have a circuit to implement our time evolution operator $U(t)$, we can construct the controlled $U(t)$ operations using [QControlBox](https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.QControlBox). If our base unitary is a sequence of `PauliExpBox`(es) then there is some structure we can exploit to simplify our circuit. See this [blog post](https://tket.quantinuum.com/tket-blog/posts/controlled_gates/) on [ConjugationBox](https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.ConjugationBox) for more."]},{"cell_type":"markdown","metadata":{},"source":["In what follows, we will just construct a simplified instance of QPE where the controlled unitaries are just $\\text{CU1}$ gates."]},{"cell_type":"markdown","metadata":{},"source":["## Putting it all together"]},{"cell_type":"markdown","metadata":{},"source":["We can now define a function to build our entire QPE circuit. We can make this function take a state preparation circuit and a unitary circuit as input as well. The function also has the number of measurement qubits as input which will determine the precision of our phase estimate."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import QControlBox"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def build_phase_est_circuit(\n"," n_measurement_qubits: int, state_prep_circuit: Circuit, unitary_circuit: Circuit\n",") -> Circuit:\n"," qpe_circ: Circuit = Circuit()\n"," n_state_prep_qubits = state_prep_circuit.n_qubits\n"," measurement_register = qpe_circ.add_q_register(\"m\", n_measurement_qubits)\n"," state_prep_register = qpe_circ.add_q_register(\"p\", n_state_prep_qubits)\n"," qpe_circ.add_circuit(state_prep_circuit, list(state_prep_register))\n","\n"," # Create a controlled unitary with a single control qubit\n"," unitary_circuit.name = \"U\"\n"," controlled_u_gate = QControlBox(CircBox(unitary_circuit), 1)\n","\n"," # Add Hadamard gates to every qubit in the measurement register\n"," for m_qubit in measurement_register:\n"," qpe_circ.H(m_qubit)\n","\n"," # Add all (2**n_measurement_qubits - 1) of the controlled unitaries sequentially\n"," for m_qubit in range(n_measurement_qubits):\n"," control_index = n_measurement_qubits - m_qubit - 1\n"," control_qubit = [measurement_register[control_index]]\n"," for _ in range(2**m_qubit):\n"," qpe_circ.add_qcontrolbox(\n"," controlled_u_gate, control_qubit + list(state_prep_register)\n"," )\n","\n"," # Finally, append the inverse qft and measure the qubits\n"," qft_box = CircBox(build_qft_circuit(n_measurement_qubits))\n"," inverse_qft_box = qft_box.dagger\n"," qpe_circ.add_circbox(inverse_qft_box, list(measurement_register))\n"," qpe_circ.measure_register(measurement_register, \"c\")\n"," return qpe_circ"]},{"cell_type":"markdown","metadata":{},"source":["## Phase Estimation with a Trivial Eigenstate\n","\n","Lets test our circuit construction by preparing a trivial $|1\\rangle$ eigenstate of the $\\text{U1}$ gate. We can then see if our phase estimation circuit returns the expected eigenvalue."]},{"cell_type":"markdown","metadata":{},"source":["$$\n","\\begin{equation}\n","U1(\\phi)|1\\rangle = e^{i\\phi} = e^{2 \\pi i \\theta} \\implies \\theta = \\frac{\\phi}{2}\n","\\end{equation}\n","$$\n","\n","So we expect that our ideal phase $\\theta$ will be half the input angle $\\phi$ to our $U1$ gate."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["prep_circuit = Circuit(1).X(0)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["input_angle = 0.73 # angle as number of half turns"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["unitary_circuit = Circuit(1).U1(input_angle, 0)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qpe_circ_trivial = build_phase_est_circuit(\n"," 4, state_prep_circuit=prep_circuit, unitary_circuit=unitary_circuit\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(qpe_circ_trivial)"]},{"cell_type":"markdown","metadata":{},"source":["Lets use the noiseless `AerBackend` simulator to run our phase estimation circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.qiskit import AerBackend"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["backend = AerBackend()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["compiled_circ = backend.get_compiled_circuit(qpe_circ_trivial)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_shots = 1000\n","result = backend.run_circuit(compiled_circ, n_shots)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(result.get_counts())"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends.backendresult import BackendResult\n","import matplotlib.pyplot as plt"]},{"cell_type":"markdown","metadata":{},"source":["plotting function for QPE Notebook"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def plot_qpe_results(\n"," sim_result: BackendResult,\n"," n_strings: int = 4,\n"," dark_mode: bool = False,\n"," y_limit: int = 1000,\n",") -> None:\n"," \"\"\"\n"," Plots results in a barchart given a BackendResult. the number of stings displayed\n"," can be specified with the n_strings argument.\n"," \"\"\"\n"," counts_dict = sim_result.get_counts()\n"," sorted_shots = counts_dict.most_common()\n"," n_most_common_strings = sorted_shots[:n_strings]\n"," x_axis_values = [str(entry[0]) for entry in n_most_common_strings] # basis states\n"," y_axis_values = [entry[1] for entry in n_most_common_strings] # counts\n"," if dark_mode:\n"," plt.style.use(\"dark_background\")\n"," fig = plt.figure()\n"," ax = fig.add_axes((0, 0, 0.75, 0.5))\n"," color_list = [\"orange\"] * (len(x_axis_values))\n"," ax.bar(\n"," x=x_axis_values,\n"," height=y_axis_values,\n"," color=color_list,\n"," )\n"," ax.set_title(label=\"Results\")\n"," plt.ylim([0, y_limit])\n"," plt.xlabel(\"Basis State\")\n"," plt.ylabel(\"Number of Shots\")\n"," plt.show()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plot_qpe_results(result, y_limit=int(1.2 * n_shots))"]},{"cell_type":"markdown","metadata":{},"source":["As expected we see one outcome with high probability. Lets now extract our approximation of $\\theta$ from our output bitstrings.\n","\n","suppose the $j$ is an integer representation of our most commonly measured bitstring."]},{"cell_type":"markdown","metadata":{},"source":["$$\n","\\begin{equation}\n","\\theta_{estimate} = \\frac{j}{N}\n","\\end{equation}\n","$$"]},{"cell_type":"markdown","metadata":{},"source":["Here $N = 2 ^n$ where $n$ is the number of measurement qubits."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends.backendresult import BackendResult"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def single_phase_from_backendresult(result: BackendResult) -> float:\n"," # Extract most common measurement outcome\n"," basis_state = result.get_counts().most_common()[0][0]\n"," bitstring = \"\".join([str(bit) for bit in basis_state])\n"," integer = int(bitstring, 2)\n","\n"," # Calculate theta estimate\n"," return integer / (2 ** len(bitstring))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["theta = single_phase_from_backendresult(result)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(theta)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(input_angle / 2)"]},{"cell_type":"markdown","metadata":{},"source":["Our output is close to half our input angle $\\phi$ as expected. Lets calculate our error."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["error = round(abs(input_angle - (2 * theta)), 3)\n","print(error)"]},{"cell_type":"markdown","metadata":{},"source":["## Suggestions for further reading\n","\n","* Quantinuum paper on Bayesian phase estimation -> https://arxiv.org/pdf/2306.16608.pdf\n","* Blog post on `ConjugationBox` (efficient circuits for controlled gates) -> https://tket.quantinuum.com/blog/posts/controlled_gates/"]}],"metadata":{"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.6.4"}},"nbformat":4,"nbformat_minor":2}
diff --git a/examples/python/Forest_portability_example.py b/examples/python/Forest_portability_example.py
index f834ad6b..af8def7f 100644
--- a/examples/python/Forest_portability_example.py
+++ b/examples/python/Forest_portability_example.py
@@ -1,4 +1,4 @@
-# # Code Portability and Intro to Forest
+# # Code portability and intro to forest
# The quantum hardware landscape is incredibly competitive and rapidly changing. Many full-stack quantum software platforms lock users into them in order to use the associated devices and simulators. This notebook demonstrates how `pytket` can free up your existing high-level code to be used on devices from other providers. We will take a state-preparation and evolution circuit generated using `qiskit`, and enable it to be run on several Rigetti backends.
#
diff --git a/examples/python/ansatz_sequence_example.py b/examples/python/ansatz_sequence_example.py
index fcb7845c..b6b34b91 100644
--- a/examples/python/ansatz_sequence_example.py
+++ b/examples/python/ansatz_sequence_example.py
@@ -1,10 +1,10 @@
-# # Ansatz Sequencing
+# # Ansatz sequencing
# When performing variational algorithms like VQE, one common approach to generating circuit ansätze is to take an operator $U$ representing excitations and use this to act on a reference state $\lvert \phi_0 \rangle$. One such ansatz is the Unitary Coupled Cluster ansatz. Each excitation, indexed by $j$, within $U$ is given a real coefficient $a_j$ and a parameter $t_j$, such that $U = e^{i \sum_j \sum_k a_j t_j P_{jk}}$, where $P_{jk} \in \{I, X, Y, Z \}^{\otimes n}$. The exact form is dependent on the chosen qubit encoding. This excitation gives us a variational state $\lvert \psi (t) \rangle = U(t) \lvert \phi_0 \rangle$. The operator $U$ must be Trotterised, to give a product of Pauli exponentials, and converted into native quantum gates to create the ansatz circuit.
#
# This notebook will describe how to use an advanced feature of `pytket` to enable automated circuit synthesis for $U$ and reduce circuit depth dramatically.
#
-# We must create a `pytket` `QubitPauliOperator`, which represents such an operator $U$, and contains a dictionary from Pauli string $P_{jk}$ to symbolic expression. Here, we make a mock operator ourselves, which resembles the UCCSD excitation operator for the $\mathrm{H}_2$ molecule using the Jordan-Wigner qubit encoding. In the future, operator generation will be handled automatically using CQC's upcoming software for enterprise quantum chemistry, EUMEN. We also offer conversion to and from the `OpenFermion` `QubitOperator` class, although at the time of writing a `QubitOperator` cannot handle arbitrary symbols.
+# We must create a `pytket` `QubitPauliOperator`, which represents such an operator $U$, and contains a dictionary from Pauli string $P_{jk}$ to symbolic expression. Here, we make a mock operator ourselves, which resembles the UCCSD excitation operator for the $\mathrm{H}_2$ molecule using the Jordan-Wigner qubit encoding.
#
# First, we create a series of `QubitPauliString` objects, which represent each $P_{jk}$.
diff --git a/examples/python/backends_example.py b/examples/python/backends_example.py
index fb493e89..4343a272 100644
--- a/examples/python/backends_example.py
+++ b/examples/python/backends_example.py
@@ -1,4 +1,4 @@
-# # TKET Backend Tutorial
+# # TKET backend tutorial
# This example shows how to use `pytket` to execute quantum circuits on both simulators and real devices, and how to interpret the results. As tket is designed to be platform-agnostic, we have unified the interfaces of different providers as much as possible into the `Backend` class for maximum portability of code. The following is a selection of currently supported backends:
# * ProjectQ simulator
diff --git a/examples/python/comparing_simulators.py b/examples/python/comparing_simulators.py
index f301dbb5..221b3889 100644
--- a/examples/python/comparing_simulators.py
+++ b/examples/python/comparing_simulators.py
@@ -1,4 +1,4 @@
-# # Comparison of the simulators available through tket
+# # Comparison of the simulators available through TKET
# In this tutorial, we will focus on:
# - exploring the wide array of simulators available through the extension modules for `pytket`;
@@ -82,7 +82,7 @@
exp = backend.get_operator_expectation_value(c, op)
print(exp)
-# ## `pytket.extensions.qiskit.AerBackend`
+# ## `AerBackend`
# `AerBackend` wraps up the `qasm_simulator` from the Qiskit Aer package. It supports an extremely flexible set of circuits and uses many effective simulation methods making it a great all-purpose sampling simulator.
#
@@ -149,7 +149,7 @@
counts = result.get_counts([final[0]])
print(counts)
-# ## `pytket.extensions.qiskit.AerStateBackend`
+# ## `AerStateBackend`
# `AerStateBackend` provides access to Qiskit Aer's `statevector_simulator`. It supports a similarly large gate set and has competitive speed for statevector simulations.
#
@@ -157,7 +157,7 @@
# - no dependency on external executables, making it easy to install and run on any computer;
# - support for fast expectation value calculations according to `QubitPauliString`s or `QubitPauliOperator`s.
-# ## `pytket.extensions.qiskit.AerUnitaryBackend`
+# ## `AerUnitaryBackend`
# Finishing the set of simulators from Qiskit Aer, `AerUnitaryBackend` captures the `unitary_simulator`, allowing for the entire unitary of a pure quantum process to be calculated. This is especially useful for testing small subcircuits that will be used many times in a larger computation.
#
@@ -183,7 +183,7 @@
unitary = result.get_unitary()
print(unitary.round(1).real)
-# ## `pytket.extensions.pyquil.ForestBackend`
+# ## `ForestBackend`
# Whilst it can, with suitable credentials, be used to access the Rigetti QPUs, the `ForestBackend` also features a simulator mode which turns it into a noiseless sampling simulator that matches the constraints of the simulated device (e.g. the same gate set, restricted connectivity, measurement model, etc.). This is useful when playing around with custom compilation strategies to ensure that your final circuits are suitable to run on the device and for checking that your overall program works fine before you invest in reserving a QPU.
#
@@ -194,18 +194,18 @@
# `docker run --rm -it -p 5555:5555 rigetti/quilc -R`
# `docker run --rm -it -p 5000:5000 rigetti/qvm -S`
-# ## `pytket.extensions.pyquil.ForestStateBackend`
+# ## `ForestStateBackend`
# The Rigetti `pyquil` package also provides the `WavefunctionSimulator`, which we present as the `ForestStateBackend`. Functionally, it is very similar to the `AerStateBackend` so can be used interchangeably. It does require that `quilc` and `qvm` are running as separate processes when not running on a Rigetti QMI.
#
# Useful features:
# - support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s.
-# ## `pytket.extensions.qsharp.QsharpSimulatorBackend`
+# ## `QsharpSimulatorBackend`
# The `QsharpSimulatorBackend` is another basic sampling simulator that is interchangeable with others, using the Microsoft QDK simulator. Note that the `pytket-qsharp` package is dependent on the `dotnet` SDK and `iqsharp` tool. Please consult the `pytket-qsharp` installation instructions for recommendations.
-# ## `pytket.extensions.qsharp.QsharpToffoliSimulatorBackend`
+# ## `QsharpToffoliSimulatorBackend`
# Toffoli circuits form a strict fragment of quantum circuits and can be efficiently simulated. The `QsharpToffoliSimulatorBackend` can only operate on these circuits, but scales much better with system size than regular simulators.
#
@@ -232,7 +232,7 @@
counts = backend.get_result(handle).get_counts()
print(counts)
-# ## `pytket.extensions.qsharp.QsharpEstimatorBackend`
+# ## `QsharpEstimatorBackend`
# The `QsharpEstimatorBackend` is not strictly a simulator, as it doesn't model the state of the quantum system and try to identify the final state, but instead analyses the circuit to estimate the required resources to run it. It does not support any of the regular outcome types (e.g. shots, counts, statevector), just the summary of the estimated resources.
#
@@ -261,7 +261,7 @@
# resources = backend.get_resources(handle)
# print(resources)
-# ## `pytket.extensions.qulacs.QulacsBackend`
+# ## `QulacsBackend`
# The `QulacsBackend` is an all-purpose simulator with both sampling and statevector modes, using the basic CPU simulator from Qulacs.
#
@@ -271,14 +271,14 @@
# Useful features:
# - support for fast expectation value calculations according to `QubitPauliString`s or Hermitian `QubitPauliOperator`s.
-# ## `pytket.extensions.qulacs.QulacsGPUBackend`
+# ## `QulacsGPUBackend`
# If the GPU version of Qulacs is installed, the `QulacsGPUBackend` will use that to benefit from even faster speeds. It is very easy to get started with using a GPU, as it only requires a CUDA installation and the `qulacs-gpu` package from `pip`. Functionally, it is identical to the `QulacsBackend`, but potentially faster if you have GPU resources available.
#
# Unique features:
# - GPU support for very fast simulation.
-# ## `pytket.extensions.projectq.ProjectQBackend`
+# ## `ProjectQBackend`
# ProjectQ is a popular quantum circuit simulator, thanks to its availability and ease of use. It provides a similar level of performance and features to `AerStateBackend`.
#
diff --git a/examples/python/conditional_gate_example.py b/examples/python/conditional_gate_example.py
index 1e268f5b..dad54ebf 100644
--- a/examples/python/conditional_gate_example.py
+++ b/examples/python/conditional_gate_example.py
@@ -1,4 +1,4 @@
-# # Conditional Execution
+# # Conditional gates
# Whilst any quantum process can be created by performing "pure" operations delaying all measurements to the end, this is not always practical and can greatly increase the resource requirements. It is much more convenient to alternate quantum gates and measurements, especially if we can use the measurement results to determine which gates to apply (we refer to this more generic circuit model as "mixed" circuits, against the usual "pure" circuits). This is especially crucial for error correcting codes, where the correction gates are applied only if an error is detected.
#
@@ -29,28 +29,25 @@
# tket supports a slightly more general form of conditional gates, where the gate is applied conditionally on the exact value of any list of bits. When adding a gate to a `Circuit` object, pass in the kwargs `condition_bits` and `condition_value` and the gate will only be applied if the state of the bits yields the binary representation of the value.
from pytket import Circuit
+from pytket.circuit.display import render_circuit_jupyter
-c = Circuit()
-alice = c.add_q_register("a", 2)
-bob = c.add_q_register("b", 1)
-cr = c.add_c_register("c", 2)
-
+circ = Circuit()
+alice = circ.add_q_register("a", 2)
+bob = circ.add_q_register("b", 1)
+cr = circ.add_c_register("c", 2)
# Bell state between Alice and Bob:
-
-c.H(alice[1])
-c.CX(alice[1], bob[0])
-
+circ.H(alice[1])
+circ.CX(alice[1], bob[0])
# Bell measurement of Alice's qubits:
-
-c.CX(alice[0], alice[1])
-c.H(alice[0])
-c.Measure(alice[0], cr[0])
-c.Measure(alice[1], cr[1])
-
+circ.CX(alice[0], alice[1])
+circ.H(alice[0])
+circ.Measure(alice[0], cr[0])
+circ.Measure(alice[1], cr[1])
# Correction of Bob's qubit:
+circ.Z(bob[0], condition_bits=[cr[0]], condition_value=1)
+circ.X(bob[0], condition_bits=[cr[1]], condition_value=1)
+render_circuit_jupyter(circ)
-c.Z(bob[0], condition_bits=[cr[0]], condition_value=1)
-c.X(bob[0], condition_bits=[cr[1]], condition_value=1)
# Performing individual gates conditionally is sufficient, but can get cumbersome for larger circuits. Fortunately, tket's Box structures can also be performed conditionally, enabling this to be applied to large circuits with ease.
#
@@ -65,54 +62,54 @@
checked_x.Measure(1, 0)
x_box = CircBox(checked_x)
-c = Circuit()
+circ2 = Circuit()
target = Qubit("t", 0)
ancilla = Qubit("a", 0)
success = Bit("s", 0)
-c.add_qubit(target)
-c.add_qubit(ancilla)
-c.add_bit(success)
+circ2.add_qubit(target)
+circ2.add_qubit(ancilla)
+circ2.add_bit(success)
# Try the X gate:
-c.add_circbox(x_box, args=[target, ancilla, success])
+circ2.add_circbox(x_box, args=[target, ancilla, success])
# Try again if the X failed
-c.add_circbox(
+circ2.add_circbox(
x_box, args=[target, ancilla, success], condition_bits=[success], condition_value=0
)
+render_circuit_jupyter(circ2)
# tket is able to apply essential compilation passes on circuits containing conditional gates. This includes decomposing any boxes into primitive gates and rebasing to other gatesets whilst preserving the conditional data.
from pytket.passes import DecomposeBoxes, RebaseTket, SequencePass
comp_pass = SequencePass([DecomposeBoxes(), RebaseTket()])
+comp_pass.apply(circ2)
+render_circuit_jupyter(circ2)
-comp_pass.apply(c)
-
-c
# A tket circuit can be converted to OpenQASM or other languages following the same classical model (e.g. Qiskit) when all conditional gates are dependent on the exact state of a single, whole classical register.
from pytket.extensions.qiskit import tk_to_qiskit
-qc = tk_to_qiskit(c)
-
+qc = tk_to_qiskit(circ2)
print(qc)
# This allows us to test our mixed programs using the `AerBackend`.
from pytket.extensions.qiskit import AerBackend
-c = Circuit(2, 1)
-c.Rx(0.3, 0)
-c.Measure(0, 0)
+circ3 = Circuit(2, 1)
+circ3.Rx(0.3, 0)
+circ3.Measure(0, 0)
# Set qubit 1 to be the opposite result and measure
-c.X(1, condition_bits=[0], condition_value=0)
-c.Measure(1, 0)
-
+circ3.X(1, condition_bits=[0], condition_value=0)
+circ3.Measure(1, 0)
backend = AerBackend()
-c = backend.get_compiled_circuit(c)
-counts = backend.run_circuit(c, 1024).get_counts()
+compiled_circ = backend.get_compiled_circuit(circ3)
+render_circuit_jupyter(compiled_circ)
+
+counts = backend.run_circuit(compiled_circ, 1024).get_counts()
print(counts)
# Try out mid-circuit measurement and conditional gate support on the `AerBackend` simulator, or ask about accessing the `QuantinuumBackend` to try on a hardware device.
diff --git a/examples/python/entanglement_swapping.py b/examples/python/entanglement_swapping.py
index a8e1c49f..105d3bfc 100644
--- a/examples/python/entanglement_swapping.py
+++ b/examples/python/entanglement_swapping.py
@@ -1,4 +1,4 @@
-# # Iterated Entanglement Swapping using tket
+# # Iterated entanglement swapping
# In this tutorial, we will focus on:
# - designing circuits with mid-circuit measurement and conditional gates;
diff --git a/examples/python/expectation_value_example.py b/examples/python/expectation_value_example.py
index ad7ac9dc..6c5aa573 100644
--- a/examples/python/expectation_value_example.py
+++ b/examples/python/expectation_value_example.py
@@ -1,4 +1,4 @@
-# # Expectation Values
+# # Expectation values
# Given a circuit generating a quantum state $\lvert \psi \rangle$, it is very common to have an operator $H$ and ask for the expectation value $\langle \psi \vert H \vert \psi \rangle$. A notable example is in quantum computational chemistry, where $\lvert \psi \rangle$ encodes the wavefunction for the electronic state of a small molecule, and the energy of the molecule can be derived from the expectation value with respect to the molecule's Hamiltonian operator $H$.
#
diff --git a/examples/python/mapping_example.py b/examples/python/mapping_example.py
index 9de6c6fb..7f82fb1e 100644
--- a/examples/python/mapping_example.py
+++ b/examples/python/mapping_example.py
@@ -1,4 +1,4 @@
-# # Qubit Mapping and Routing
+# # Qubit mapping and routing
# In this tutorial we will show how the problem of mapping from logical quantum circuits to physically permitted circuits is solved automatically in TKET. The basic examples require only the installation of pytket, ```pip install pytket```.
diff --git a/examples/python/measurement_reduction_example.py b/examples/python/measurement_reduction_example.py
index 7775b643..8385741c 100644
--- a/examples/python/measurement_reduction_example.py
+++ b/examples/python/measurement_reduction_example.py
@@ -1,4 +1,4 @@
-# # Advanced Expectation Values and Measurement Reduction
+# # Advanced expectation values and measurement reduction
# This notebook is an advanced follow-up to the "expectation_value_example" notebook, focussing on reducing the number of circuits required for measurement.
#
diff --git a/examples/python/phase_estimation.py b/examples/python/phase_estimation.py
index 7e9fe0ee..7799a133 100644
--- a/examples/python/phase_estimation.py
+++ b/examples/python/phase_estimation.py
@@ -1,7 +1,4 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-# # Quantum Phase Estimation using `pytket` Boxes
+# # Quantum Phase Estimation
#
# When constructing circuits for quantum algorithms it is useful to think of higher level operations than just individual quantum gates.
#
@@ -28,7 +25,7 @@
#
# The circuit for Quantum phase estimation is itself composed of several subroutines which we can realise as boxes.
#
-# ![](phase_est.png "Quantum Phase Estimation Circuit")
+# ![](images/phase_est.png "Quantum Phase Estimation Circuit")
# QPE is generally split up into three stages
#
@@ -49,9 +46,11 @@
#
# Mathematically, the QFT has the following action.
#
+# $$
# \begin{equation}
# QFT : |j\rangle\ \longmapsto \sum_{k=0}^{N - 1} e^{2 \pi ijk/N}|k\rangle, \quad N= 2^k
# \end{equation}
+# $$
#
# This is essentially the Discrete Fourier transform except the input is a quantum state $|j\rangle$.
#
@@ -77,15 +76,14 @@
#
# The circuit for the Quantum Fourier transform on three qubits is the following
#
-# ![](qft.png "QFT Circuit")
+# ![](images/qft.png "QFT Circuit")
#
# We can build this circuit in `pytket` by adding gate operations manually:
-
+# lets build the QFT for three qubits
from pytket.circuit import Circuit
from pytket.circuit.display import render_circuit_jupyter
-# lets build the QFT for three qubits
qft3_circ = Circuit(3)
qft3_circ.H(0)
qft3_circ.CU1(0.5, 1, 0)
@@ -152,9 +150,11 @@ def build_qft_circuit(n_qubits: int) -> Circuit:
# Suppose that we had the following decomposition for $H$ in terms of Pauli strings $P_j$ and complex coefficients $\alpha_j$.
#
+# $$
# \begin{equation}
-# H = \sum_j \alpha_j P_j\,, \quad \, P_j \in \{I, X, Y, Z\}^{\otimes n}
+# H = \sum_j \alpha_j P_j\,, \quad \, P_j \in \{I, \,X, \,Y, \,Z\}^{\otimes n}
# \end{equation}
+# $$
#
# Here Pauli strings refers to tensor products of Pauli operators. These strings form an orthonormal basis for $2^n \times 2^n$ matrices.
@@ -291,7 +291,6 @@ def plot_qpe_results(
plt.ylim([0, y_limit])
plt.xlabel("Basis State")
plt.ylabel("Number of Shots")
- plt.xticks(rotation=90)
plt.show()
@@ -326,7 +325,6 @@ def single_phase_from_backendresult(result: BackendResult) -> float:
theta = single_phase_from_backendresult(result)
-
print(theta)
@@ -342,9 +340,5 @@ def single_phase_from_backendresult(result: BackendResult) -> float:
# ## Suggestions for further reading
#
-# In this notebook we have shown the canonical variant of quantum phase estimation. There are several other variants.
-#
-# Quantinuum paper on Bayesian phase estimation -> https://arxiv.org/pdf/2306.16608.pdf
-# Blog post on `ConjugationBox` -> https://tket.quantinuum.com/tket-blog/posts/controlled_gates/ - efficient circuits for controlled Pauli gadgets.
-#
-# As mentioned quantum phase estimation is a subroutine in Shor's algorithm. Read more about how phase estimation is used in period finding.
+# * Quantinuum paper on Bayesian phase estimation -> https://arxiv.org/pdf/2306.16608.pdf
+# * Blog post on `ConjugationBox` (efficient circuits for controlled gates) -> https://tket.quantinuum.com/blog/posts/controlled_gates/
diff --git a/examples/python/pytket-qujax-classification.py b/examples/python/pytket-qujax-classification.py
index 44b07a00..1fe8435c 100644
--- a/examples/python/pytket-qujax-classification.py
+++ b/examples/python/pytket-qujax-classification.py
@@ -1,4 +1,4 @@
-# # Binary Classification using pytket-qujax
+# # Binary classification using `pytket-qujax`
from jax import numpy as jnp, random, vmap, value_and_grad, jit
from pytket import Circuit
@@ -57,7 +57,13 @@ def classification_function(x, y):
angles_to_st = tk_to_qujax(c)
# We'll parameterise each angle as
-# $$ \theta_k = b_k + w_k * x_k $$
+#
+# $$
+# \begin{equation}
+# \theta_k = b_k + w_k \, x_k
+# \end{equation}
+# $$
+#
# where $b_k, w_k$ are variational parameters to be learnt and $x_k = x_0$ if $k$ even, $x_k = x_1$ if $k$ odd for a single bivariate input point $(x_0, x_1)$.
n_angles = 3 * n_qubits * depth
@@ -91,20 +97,44 @@ def param_and_x_to_probability(param, x_single):
# For binary classification, the likelihood for our full data set $(x_{1:N}, y_{1:N})$ is
-# $$ p(y_{1:N} \mid b, w, x_{1:N}) = \prod_{i=1}^N p(y_i \mid b, w, x_i) = \prod_{i=1}^N (1 - q_{(b,w)}(x_i))^{\mathbb{I}[y_i = 0]}q_{(b,w)}(x_i)^{\mathbb{I}[y_i = 1]}, $$
+#
+# $$
+# \begin{equation}
+# p(y_{1:N} \mid b, w, x_{1:N}) = \prod_{i=1}^N p(y_i \mid b, w, x_i) = \prod_{i=1}^N (1 - q_{(b,w)}(x_i))^{I[y_i = 0]}q_{(b,w)}(x_i)^{I[y_i = 1]},
+# \end{equation}
+# $$
+#
# where $q_{(b, w)}(x)$ is the probability the quantum circuit classifies input $x$ as donut given variational parameter vectors $(b, w)$. This gives log-likelihood
-# $$ \log p(y_{1:N} \mid b, w, x_{1:N}) = \sum_{i=1}^N \mathbb{I}[y_i = 0] \log(1 - q_{(b,w)}(x_i)) + \mathbb{I}[y_i = 1] \log q_{(b,w)}(x_i), $$
+#
+# $$
+# \begin{equation}
+# \log p(y_{1:N} \mid b, w, x_{1:N}) = \sum_{i=1}^N I[y_i = 0] \log(1 - q_{(b,w)}(x_i)) + I[y_i = 1] \log q_{(b,w)}(x_i),
+# \end{equation}
+# $$
+#
# which we would like to maximise.
#
# Unfortunately, the log-likelihood **cannot** be approximated unbiasedly using shots, that is we can approximate $q_{(b,w)}(x_i)$ unbiasedly but not $\log(q_{(b,w)}(x_i))$.
# Note that in qujax simulations we can use the statetensor to calculate this exactly, but it is still good to keep in mind loss functions that can also be used with shots from a quantum device.
-
+#
# Instead we can minimise an expected distance between shots and data
-#
-# $$ C(b, w, x, y) = \mathbb{E}_{p(y' \mid q_{(b, w)}(x))}[\ell(y', y)] = (1 - q_{(b, w)}(x)) \ell(0, y) + q_{(b, w)}(x)\ell(1, y), $$
-#
-# where $y'$ is a shot, $y$ is a data label and $\ell$ is some distance between bitstrings - here we simply set $\ell(0, 0) = \ell(1, 1) = 0$ and $\ell(0, 1) = \ell(1, 0) = 1$ (which coincides with the Hamming distance for this binary example). The full batch cost function is $C(b, w) = \frac1N \sum_{i=1}^N C(b, w, x_i, y_i)$.
#
+# $$
+# \begin{equation}
+# C(b, w, x, y) = E_{p(y' \mid q_{(b, w)}(x))}[\ell(y', y)] = (1 - q_{(b, w)}(x)) \ell(0, y) + q_{(b, w)}(x)\ell(1, y),
+# \end{equation}
+# $$
+#
+# where $y'$ is a shot, $y$ is a data label and $\ell$ is some distance between bitstrings - here we simply set $\ell(0, 0) = \ell(1, 1) = 0$ and $\ell(0, 1) = \ell(1, 0) = 1$ (which coincides with the Hamming distance for this binary example).
+#
+# The full batch cost function is
+#
+# $$
+# \begin{equation}
+# C(b, w) = \frac1N \sum_{i=1}^N C(b,\, w,\, x_i,\, y_i).
+# \end{equation}
+# $$
+
# Note that to calculate the cost function we need to evaluate the statetensor for every input point $x_i$. If the dataset becomes too large, we can easily minibatch.
diff --git a/examples/python/pytket-qujax_heisenberg_vqe.py b/examples/python/pytket-qujax_heisenberg_vqe.py
index 3c050c9e..8f0ba52c 100644
--- a/examples/python/pytket-qujax_heisenberg_vqe.py
+++ b/examples/python/pytket-qujax_heisenberg_vqe.py
@@ -1,14 +1,16 @@
-# # VQE example with pytket-qujax
+# # VQE example with `pytket-qujax`
from jax import numpy as jnp, random, value_and_grad, jit
from pytket import Circuit
from pytket.circuit.display import render_circuit_jupyter
import matplotlib.pyplot as plt
+
+# ## Let's start with a TKET circuit
+
import qujax
from pytket.extensions.qujax.qujax_convert import tk_to_qujax
-# # Let's start with a tket circuit
# We place barriers to stop tket automatically rearranging gates and we also store the number of circuit parameters as we'll need this later.
@@ -47,7 +49,7 @@ def get_circuit(n_qubits, depth):
circuit, n_params = get_circuit(n_qubits, depth)
render_circuit_jupyter(circuit)
-# # Now let's invoke qujax
+# ## Now let's invoke qujax
# The `pytket.extensions.qujax.tk_to_qujax` function will generate a parameters -> statetensor function for us.
param_to_st = tk_to_qujax(circuit)
@@ -72,7 +74,7 @@ def get_circuit(n_qubits, depth):
sample_probs = jnp.square(jnp.abs(statevector))
plt.bar(jnp.arange(statevector.size), sample_probs)
-# # Cost function
+# ## Cost function
# Now we have our `param_to_st` function we are free to define a cost function that acts on bitstrings (e.g. maxcut) or integers by directly wrapping a function around `param_to_st`. However, cost functions defined via quantum Hamiltonians are a bit more involved.
# Fortunately, we can encode an Hamiltonian in JAX via the `qujax.get_statetensor_to_expectation_func` function which generates a statetensor -> expected value function for us.
@@ -82,7 +84,13 @@ def get_circuit(n_qubits, depth):
# - `coefficients`: A list of floats encoding any coefficients in the Hamiltonian. I.e. `[2.3, 0.8, 1.2]` corresponds to $a=2.3,b=0.8,c=1.2$ above. Must have the same length as the two above arguments.
# More specifically let's consider the problem of finding the ground state of the quantum Heisenberg Hamiltonian
-# $$ H = \sum_{i=1}^{n_\text{qubits}-1} X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1}. $$
+
+# $$
+# \begin{equation}
+# H = \sum_{i=1}^{n_\text{qubits}-1} X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1}.
+# \end{equation}
+# $$
+#
# As described, we define the Hamiltonian via its gate strings, qubit indices and coefficients.
hamiltonian_gates = [["X", "X"], ["Y", "Y"], ["Z", "Z"]] * (n_qubits - 1)
@@ -119,7 +127,7 @@ def get_circuit(n_qubits, depth):
)
param_to_expectation(new_params)
-# # We can now use autodiff for fast, exact gradients within a VQE algorithm
+# ## Exact gradients within a VQE algorithm
# The `param_to_expectation` function we created is a pure JAX function and outputs a scalar. This means we can pass it to `jax.grad` (or even better `jax.value_and_grad`).
cost_and_grad = value_and_grad(param_to_expectation)
@@ -128,7 +136,7 @@ def get_circuit(n_qubits, depth):
cost_and_grad(params)
-# # Now we have all the tools we need to design our VQE!
+# ## Now we have all the tools we need to design our VQE!
# We'll just use vanilla gradient descent with a constant stepsize
@@ -164,7 +172,7 @@ def vqe(init_param, n_steps, stepsize):
# Pretty good!
-# # `jax.jit` speedup
+# ## `jax.jit` speedup
# One last thing... We can significantly speed up the VQE above via the `jax.jit`. In our current implementation, the expensive `cost_and_grad` function is compiled to [XLA](https://www.tensorflow.org/xla) and then executed at each call. By invoking `jax.jit` we ensure that the function is compiled only once (on the first call) and then simply executed at each future call - this is much faster!
cost_and_grad = jit(cost_and_grad)
diff --git a/examples/python/pytket-qujax_qaoa.py b/examples/python/pytket-qujax_qaoa.py
index 05450f3b..d077612f 100644
--- a/examples/python/pytket-qujax_qaoa.py
+++ b/examples/python/pytket-qujax_qaoa.py
@@ -1,4 +1,4 @@
-# # Symbolic circuits with `qujax` and `pytket-qujax`
+# # Symbolic circuits with `pytket-qujax`
# In this notebook we will show how to manipulate symbolic circuits with the `pytket-qujax` extension. In particular, we will consider a QAOA and an Ising Hamiltonian.
from pytket import Circuit
@@ -13,9 +13,16 @@
# # QAOA
# The Quantum Approximate Optimization Algorithm (QAOA), first introduced by [Farhi et al.](https://arxiv.org/pdf/1411.4028.pdf), is a quantum variational algorithm used to solve optimization problems. It consists of a unitary $U(\beta, \gamma)$ formed by alternate repetitions of $U(\beta)=e^{-i\beta H_B}$ and $U(\gamma)=e^{-i\gamma H_P}$, where $H_B$ is the mixing Hamiltonian and $H_P$ the problem Hamiltonian. The goal is to find the optimal parameters that minimize $H_P$.
# Given a depth $d$, the expression of the final unitary is $U(\beta, \gamma) = U(\beta_d)U(\gamma_d)\cdots U(\beta_1)U(\gamma_1)$. Notice that for each repetition the parameters are different.
+#
# ## Problem Hamiltonian
# QAOA uses a problem dependent ansatz. Therefore, we first need to know the problem that we want to solve. In this case we will consider an Ising Hamiltonian with only $Z$ interactions. Given a set of pairs (or qubit indices) $E$, the problem Hamiltonian will be:
-# $$H_P = \sum_{(i, j) \in E}\alpha_{ij}Z_iZ_j,$$
+#
+# $$
+# \begin{equation}
+# H_P = \sum_{(i, j) \in E}\alpha_{ij}Z_iZ_j,
+# \end{equation}
+# $$
+#
# where $\alpha_{ij}$ are the coefficients.
# Let's build our problem Hamiltonian with random coefficients and a set of pairs for a given number of qubits:
@@ -36,7 +43,13 @@
# ## Variational Circuit
# Before constructing the circuit, we still need to select the mixing Hamiltonian. In our case, we will be using $X$ gates in each qubit, so $H_B = \sum_{i=1}^{n}X_i$, where $n$ is the number of qubits. Notice that the unitary $U(\beta)$, given this mixing Hamiltonian, is an $X$ rotation in each qubit with angle $\beta$.
# As for the unitary corresponding to the problem Hamiltonian, $U(\gamma)$, it has the following form:
-# $$U(\gamma)=\prod_{(i, j) \in E}e^{-i\gamma\alpha_{ij}Z_iZ_j}$$
+#
+# $$
+# \begin{equation}
+# U(\gamma)=\prod_{(i, j) \in E}e^{-i\gamma\alpha_{ij}Z_i Z_j}
+# \end{equation}
+# $$
+#
# The operation $e^{-i\gamma\alpha_{ij}Z_iZ_j}$ can be performed using two CNOT gates with qubit $i$ as control and qubit $j$ as target and a $Z$ rotation in qubit $j$ in between them, with angle $\gamma\alpha_{ij}$.
# Finally, the initial state used, in general, with the QAOA is an equal superposition of all the basis states. This can be achieved adding a first layer of Hadamard gates in each qubit at the beginning of the circuit.
diff --git a/examples/python/spam_example.py b/examples/python/spam_example.py
index 012f2f27..899420fe 100644
--- a/examples/python/spam_example.py
+++ b/examples/python/spam_example.py
@@ -1,4 +1,4 @@
-# # Calibration and Correction of State Preparation and Measurement (SPAM)
+# # Calibration and correction of state preparation and measurement (SPAM)
# Quantum Computers available in the NISQ-era are limited by significant sources of device noise which cause errors in computation. One such noise source is errors in the preparation and measurement of quantum states, more commonly know as SPAM.
#
diff --git a/examples/python/ucc_vqe.py b/examples/python/ucc_vqe.py
index b38fb7ef..43b04287 100644
--- a/examples/python/ucc_vqe.py
+++ b/examples/python/ucc_vqe.py
@@ -1,4 +1,4 @@
-# # VQE for Unitary Coupled Cluster using tket
+# # VQE with UCC ansatz
# In this tutorial, we will focus on:
# - building parameterised ansätze for variational algorithms;
diff --git a/examples/pytket-qujax-classification.ipynb b/examples/pytket-qujax-classification.ipynb
index e55499f2..078adb8b 100644
--- a/examples/pytket-qujax-classification.ipynb
+++ b/examples/pytket-qujax-classification.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Binary Classification using pytket-qujax"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from jax import numpy as jnp, random, vmap, value_and_grad, jit\n", "from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter\n", "from pytket.extensions.qujax.qujax_convert import tk_to_qujax\n", "import matplotlib.pyplot as plt"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Define the classification task
\n", "We'll try and learn a _donut_ binary classification function (i.e. a bivariate coordinate is labelled 1 if it is inside the donut and 0 if it is outside)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["inner_rad = 0.25\n", "outer_rad = 0.75"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def classification_function(x, y):\n", " r = jnp.sqrt(x**2 + y**2)\n", " return jnp.where((r > inner_rad) * (r < outer_rad), 1, 0)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["linsp = jnp.linspace(-1, 1, 1000)\n", "Z = vmap(lambda x: vmap(lambda y: classification_function(x, y))(linsp))(linsp)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.contourf(linsp, linsp, Z, cmap=\"Purples\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now let's generate some data for our quantum circuit to learn from"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_data = 1000\n", "x = random.uniform(random.PRNGKey(0), shape=(n_data, 2), minval=-1, maxval=1)\n", "y = classification_function(x[:, 0], x[:, 1])"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.scatter(x[:, 0], x[:, 1], alpha=jnp.where(y, 1, 0.2), s=10)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Quantum circuit time
\n", "We'll use a variant of data re-uploading [P\u00e9rez-Salinas et al](https://doi.org/10.22331/q-2020-02-06-226) to encode the input data, alongside some variational parameters within a quantum circuit classifier"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_qubits = 3\n", "depth = 5"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["c = Circuit(n_qubits)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for layer in range(depth):\n", " for qi in range(n_qubits):\n", " c.Rz(0.0, qi)\n", " c.Ry(0.0, qi)\n", " c.Rz(0.0, qi)\n", " if layer < (depth - 1):\n", " for qi in range(layer, layer + n_qubits - 1, 2):\n", " c.CZ(qi % n_qubits, (qi + 1) % n_qubits)\n", " c.add_barrier(range(n_qubits))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can use `pytket-qujax` to generate our angles-to-statetensor function."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["angles_to_st = tk_to_qujax(c)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We'll parameterise each angle as
\n", "$$ \\theta_k = b_k + w_k * x_k $$
\n", "where $b_k, w_k$ are variational parameters to be learnt and $x_k = x_0$ if $k$ even, $x_k = x_1$ if $k$ odd for a single bivariate input point $(x_0, x_1)$."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_angles = 3 * n_qubits * depth\n", "n_params = 2 * n_angles"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def param_and_x_to_angles(param, x_single):\n", " biases = param[:n_angles]\n", " weights = param[n_angles:]\n", " weights_times_data = jnp.where(\n", " jnp.arange(n_angles) % 2 == 0, weights * x_single[0], weights * x_single[1]\n", " )\n", " angles = biases + weights_times_data\n", " return angles"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param_and_x_to_st = lambda param, x_single: angles_to_st(\n", " param_and_x_to_angles(param, x_single)\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We'll measure the first qubit only (if its 1 we label _donut_, if its 0 we label _not donut_)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def param_and_x_to_probability(param, x_single):\n", " st = param_and_x_to_st(param, x_single)\n", " all_probs = jnp.square(jnp.abs(st))\n", " first_qubit_probs = jnp.sum(all_probs, axis=range(1, n_qubits))\n", " return first_qubit_probs[1]"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For binary classification, the likelihood for our full data set $(x_{1:N}, y_{1:N})$ is
\n", "$$ p(y_{1:N} \\mid b, w, x_{1:N}) = \\prod_{i=1}^N p(y_i \\mid b, w, x_i) = \\prod_{i=1}^N (1 - q_{(b,w)}(x_i))^{\\mathbb{I}[y_i = 0]}q_{(b,w)}(x_i)^{\\mathbb{I}[y_i = 1]}, $$
\n", "where $q_{(b, w)}(x)$ is the probability the quantum circuit classifies input $x$ as donut given variational parameter vectors $(b, w)$. This gives log-likelihood
\n", "$$ \\log p(y_{1:N} \\mid b, w, x_{1:N}) = \\sum_{i=1}^N \\mathbb{I}[y_i = 0] \\log(1 - q_{(b,w)}(x_i)) + \\mathbb{I}[y_i = 1] \\log q_{(b,w)}(x_i), $$
\n", "which we would like to maximise.
\n", "
\n", "Unfortunately, the log-likelihood **cannot** be approximated unbiasedly using shots, that is we can approximate $q_{(b,w)}(x_i)$ unbiasedly but not $\\log(q_{(b,w)}(x_i))$.
\n", "Note that in qujax simulations we can use the statetensor to calculate this exactly, but it is still good to keep in mind loss functions that can also be used with shots from a quantum device."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Instead we can minimise an expected distance between shots and data
\n", "
\n", "$$ C(b, w, x, y) = \\mathbb{E}_{p(y' \\mid q_{(b, w)}(x))}[\\ell(y', y)] = (1 - q_{(b, w)}(x)) \\ell(0, y) + q_{(b, w)}(x)\\ell(1, y), $$
\n", "
\n", "where $y'$ is a shot, $y$ is a data label and $\\ell$ is some distance between bitstrings - here we simply set $\\ell(0, 0) = \\ell(1, 1) = 0$ and $\\ell(0, 1) = \\ell(1, 0) = 1$ (which coincides with the Hamming distance for this binary example). The full batch cost function is $C(b, w) = \\frac1N \\sum_{i=1}^N C(b, w, x_i, y_i)$.
\n", "
\n", "Note that to calculate the cost function we need to evaluate the statetensor for every input point $x_i$. If the dataset becomes too large, we can easily minibatch."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def param_to_cost(param):\n", " donut_probs = vmap(param_and_x_to_probability, in_axes=(None, 0))(param, x)\n", " costs = jnp.where(y, 1 - donut_probs, donut_probs)\n", " return costs.mean()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Ready to descend some gradients?
\n", "We'll just use vanilla gradient descent here"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param_to_cost_and_grad = jit(value_and_grad(param_to_cost))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_iter = 1000\n", "stepsize = 1e-1\n", "param = random.uniform(random.PRNGKey(1), shape=(n_params,), minval=0, maxval=2)\n", "costs = jnp.zeros(n_iter)\n", "for i in range(n_iter):\n", " cost, grad = param_to_cost_and_grad(param)\n", " costs = costs.at[i].set(cost)\n", " param = param - stepsize * grad\n", " print(i, \"Cost: \", cost, end=\"\\r\")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.plot(costs)\n", "plt.xlabel(\"Iteration\")\n", "plt.ylabel(\"Cost\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Visualise trained classifier"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["linsp = jnp.linspace(-1, 1, 100)\n", "Z = vmap(\n", " lambda a: vmap(lambda b: param_and_x_to_probability(param, jnp.array([a, b])))(\n", " linsp\n", " )\n", ")(linsp)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.contourf(linsp, linsp, Z, cmap=\"Purples\", alpha=0.8)\n", "circle_linsp = jnp.linspace(0, 2 * jnp.pi, 100)\n", "plt.plot(inner_rad * jnp.cos(circle_linsp), inner_rad * jnp.sin(circle_linsp), c=\"red\")\n", "plt.plot(outer_rad * jnp.cos(circle_linsp), outer_rad * jnp.sin(circle_linsp), c=\"red\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Looks good, it has clearly grasped the donut shape. Sincerest apologies if you are now hungry! \ud83c\udf69"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Binary classification using `pytket-qujax`"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from jax import numpy as jnp, random, vmap, value_and_grad, jit\n","from pytket import Circuit\n","from pytket.circuit.display import render_circuit_jupyter\n","from pytket.extensions.qujax.qujax_convert import tk_to_qujax\n","import matplotlib.pyplot as plt"]},{"cell_type":"markdown","metadata":{},"source":["# Define the classification task\n","We'll try and learn a _donut_ binary classification function (i.e. a bivariate coordinate is labelled 1 if it is inside the donut and 0 if it is outside)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["inner_rad = 0.25\n","outer_rad = 0.75"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def classification_function(x, y):\n"," r = jnp.sqrt(x**2 + y**2)\n"," return jnp.where((r > inner_rad) * (r < outer_rad), 1, 0)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["linsp = jnp.linspace(-1, 1, 1000)\n","Z = vmap(lambda x: vmap(lambda y: classification_function(x, y))(linsp))(linsp)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.contourf(linsp, linsp, Z, cmap=\"Purples\")"]},{"cell_type":"markdown","metadata":{},"source":["Now let's generate some data for our quantum circuit to learn from"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_data = 1000\n","x = random.uniform(random.PRNGKey(0), shape=(n_data, 2), minval=-1, maxval=1)\n","y = classification_function(x[:, 0], x[:, 1])"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.scatter(x[:, 0], x[:, 1], alpha=jnp.where(y, 1, 0.2), s=10)"]},{"cell_type":"markdown","metadata":{},"source":["# Quantum circuit time\n","We'll use a variant of data re-uploading [Pérez-Salinas et al](https://doi.org/10.22331/q-2020-02-06-226) to encode the input data, alongside some variational parameters within a quantum circuit classifier"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_qubits = 3\n","depth = 5"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = Circuit(n_qubits)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["for layer in range(depth):\n"," for qi in range(n_qubits):\n"," c.Rz(0.0, qi)\n"," c.Ry(0.0, qi)\n"," c.Rz(0.0, qi)\n"," if layer < (depth - 1):\n"," for qi in range(layer, layer + n_qubits - 1, 2):\n"," c.CZ(qi % n_qubits, (qi + 1) % n_qubits)\n"," c.add_barrier(range(n_qubits))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(c)"]},{"cell_type":"markdown","metadata":{},"source":["We can use `pytket-qujax` to generate our angles-to-statetensor function."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["angles_to_st = tk_to_qujax(c)"]},{"cell_type":"markdown","metadata":{},"source":["We'll parameterise each angle as\n","\n","$$\n","\\begin{equation}\n","\\theta_k = b_k + w_k \\, x_k\n","\\end{equation}\n","$$\n","\n","where $b_k, w_k$ are variational parameters to be learnt and $x_k = x_0$ if $k$ even, $x_k = x_1$ if $k$ odd for a single bivariate input point $(x_0, x_1)$."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_angles = 3 * n_qubits * depth\n","n_params = 2 * n_angles"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def param_and_x_to_angles(param, x_single):\n"," biases = param[:n_angles]\n"," weights = param[n_angles:]\n"," weights_times_data = jnp.where(\n"," jnp.arange(n_angles) % 2 == 0, weights * x_single[0], weights * x_single[1]\n"," )\n"," angles = biases + weights_times_data\n"," return angles"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param_and_x_to_st = lambda param, x_single: angles_to_st(\n"," param_and_x_to_angles(param, x_single)\n",")"]},{"cell_type":"markdown","metadata":{},"source":["We'll measure the first qubit only (if its 1 we label _donut_, if its 0 we label _not donut_)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def param_and_x_to_probability(param, x_single):\n"," st = param_and_x_to_st(param, x_single)\n"," all_probs = jnp.square(jnp.abs(st))\n"," first_qubit_probs = jnp.sum(all_probs, axis=range(1, n_qubits))\n"," return first_qubit_probs[1]"]},{"cell_type":"markdown","metadata":{},"source":["For binary classification, the likelihood for our full data set $(x_{1:N}, y_{1:N})$ is\n","\n","$$\n","\\begin{equation}\n","p(y_{1:N} \\mid b, w, x_{1:N}) = \\prod_{i=1}^N p(y_i \\mid b, w, x_i) = \\prod_{i=1}^N (1 - q_{(b,w)}(x_i))^{I[y_i = 0]}q_{(b,w)}(x_i)^{I[y_i = 1]},\n","\\end{equation}\n","$$\n","\n","where $q_{(b, w)}(x)$ is the probability the quantum circuit classifies input $x$ as donut given variational parameter vectors $(b, w)$. This gives log-likelihood\n","\n","$$\n","\\begin{equation}\n"," \\log p(y_{1:N} \\mid b, w, x_{1:N}) = \\sum_{i=1}^N I[y_i = 0] \\log(1 - q_{(b,w)}(x_i)) + I[y_i = 1] \\log q_{(b,w)}(x_i),\n","\\end{equation}\n","$$\n","\n","which we would like to maximise.\n","\n","Unfortunately, the log-likelihood **cannot** be approximated unbiasedly using shots, that is we can approximate $q_{(b,w)}(x_i)$ unbiasedly but not $\\log(q_{(b,w)}(x_i))$.\n","Note that in qujax simulations we can use the statetensor to calculate this exactly, but it is still good to keep in mind loss functions that can also be used with shots from a quantum device.\n","\n","Instead we can minimise an expected distance between shots and data\n","\n","$$\n","\\begin{equation}\n","C(b, w, x, y) = E_{p(y' \\mid q_{(b, w)}(x))}[\\ell(y', y)] = (1 - q_{(b, w)}(x)) \\ell(0, y) + q_{(b, w)}(x)\\ell(1, y),\n","\\end{equation}\n","$$\n","\n","where $y'$ is a shot, $y$ is a data label and $\\ell$ is some distance between bitstrings - here we simply set $\\ell(0, 0) = \\ell(1, 1) = 0$ and $\\ell(0, 1) = \\ell(1, 0) = 1$ (which coincides with the Hamming distance for this binary example).\n","\n"," The full batch cost function is\n","\n","$$\n","\\begin{equation}\n"," C(b, w) = \\frac1N \\sum_{i=1}^N C(b,\\, w,\\, x_i,\\, y_i).\n","\\end{equation}\n","$$"]},{"cell_type":"markdown","metadata":{},"source":["Note that to calculate the cost function we need to evaluate the statetensor for every input point $x_i$. If the dataset becomes too large, we can easily minibatch."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def param_to_cost(param):\n"," donut_probs = vmap(param_and_x_to_probability, in_axes=(None, 0))(param, x)\n"," costs = jnp.where(y, 1 - donut_probs, donut_probs)\n"," return costs.mean()"]},{"cell_type":"markdown","metadata":{},"source":["# Ready to descend some gradients?\n","We'll just use vanilla gradient descent here"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param_to_cost_and_grad = jit(value_and_grad(param_to_cost))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_iter = 1000\n","stepsize = 1e-1\n","param = random.uniform(random.PRNGKey(1), shape=(n_params,), minval=0, maxval=2)\n","costs = jnp.zeros(n_iter)\n","for i in range(n_iter):\n"," cost, grad = param_to_cost_and_grad(param)\n"," costs = costs.at[i].set(cost)\n"," param = param - stepsize * grad\n"," print(i, \"Cost: \", cost, end=\"\\r\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.plot(costs)\n","plt.xlabel(\"Iteration\")\n","plt.ylabel(\"Cost\")"]},{"cell_type":"markdown","metadata":{},"source":["# Visualise trained classifier"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["linsp = jnp.linspace(-1, 1, 100)\n","Z = vmap(\n"," lambda a: vmap(lambda b: param_and_x_to_probability(param, jnp.array([a, b])))(\n"," linsp\n"," )\n",")(linsp)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.contourf(linsp, linsp, Z, cmap=\"Purples\", alpha=0.8)\n","circle_linsp = jnp.linspace(0, 2 * jnp.pi, 100)\n","plt.plot(inner_rad * jnp.cos(circle_linsp), inner_rad * jnp.sin(circle_linsp), c=\"red\")\n","plt.plot(outer_rad * jnp.cos(circle_linsp), outer_rad * jnp.sin(circle_linsp), c=\"red\")"]},{"cell_type":"markdown","metadata":{},"source":["Looks good, it has clearly grasped the donut shape. Sincerest apologies if you are now hungry! 🍩"]}],"metadata":{"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.6.4"}},"nbformat":4,"nbformat_minor":2}
diff --git a/examples/pytket-qujax_heisenberg_vqe.ipynb b/examples/pytket-qujax_heisenberg_vqe.ipynb
index 2ec55022..c3328b3d 100644
--- a/examples/pytket-qujax_heisenberg_vqe.ipynb
+++ b/examples/pytket-qujax_heisenberg_vqe.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter\n", "from jax import numpy as jnp, random, vmap, grad, value_and_grad, jit\n", "import matplotlib.pyplot as plt"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import qujax\n", "from pytket.extensions.qujax import tk_to_qujax"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Let's start with a tket circuit
\n", "We place barriers to stop tket automatically rearranging gates and we also store the number of circuit parameters as we'll need this later."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def get_circuit(n_qubits, depth):\n", " n_params = 2 * n_qubits * (depth + 1)\n", " param = jnp.zeros((n_params,))\n", " circuit = Circuit(n_qubits)\n", " k = 0\n", " for i in range(n_qubits):\n", " circuit.H(i)\n", " for i in range(n_qubits):\n", " circuit.Rx(param[k], i)\n", " k += 1\n", " for i in range(n_qubits):\n", " circuit.Ry(param[k], i)\n", " k += 1\n", " for _ in range(depth):\n", " for i in range(0, n_qubits - 1):\n", " circuit.CZ(i, i + 1)\n", " circuit.add_barrier(range(0, n_qubits))\n", " for i in range(n_qubits):\n", " circuit.Rx(param[k], i)\n", " k += 1\n", " for i in range(n_qubits):\n", " circuit.Ry(param[k], i)\n", " k += 1\n", " return circuit, n_params"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_qubits = 4\n", "depth = 2\n", "circuit, n_params = get_circuit(n_qubits, depth)\n", "render_circuit_jupyter(circuit)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Now let's invoke qujax
\n", "The `pytket.extensions.qujax.tk_to_qujax` function will generate a parameters -> statetensor function for us."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param_to_st = tk_to_qujax(circuit)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's try it out on some random parameters values. Be aware that's JAX's random number generator requires a `jax.random.PRNGkey` every time it's called - more info on that [here](https://jax.readthedocs.io/en/latest/jax.random.html).
\n", "Be aware that we still have convention where parameters are specified as multiples of $\\pi$ - that is in [0,2]."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["params = random.uniform(random.PRNGKey(0), shape=(n_params,), minval=0., maxval=2.)\n", "statetensor = param_to_st(params)\n", "print(statetensor)\n", "print(statetensor.shape)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Note that this function also has an optional second argument where an initiating `statetensor_in` can be provided. If it is not provided it will default to the all 0s state (as we use here)."]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can obtain statevector by simply calling `.flatten()`"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["statevector = statetensor.flatten()\n", "statevector.shape"]}, {"cell_type": "markdown", "metadata": {}, "source": ["And sampling probabilities by squaring the absolute value of the statevector"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sample_probs = jnp.square(jnp.abs(statevector))\n", "plt.bar(jnp.arange(statevector.size), sample_probs);"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Cost function"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now we have our `param_to_st` function we are free to define a cost function that acts on bitstrings (e.g. maxcut) or integers by directly wrapping a function around `param_to_st`. However, cost functions defined via quantum Hamiltonians are a bit more involved.
\n", "Fortunately, we can encode an Hamiltonian in JAX via the `qujax.get_statetensor_to_expectation_func` function which generates a statetensor -> expected value function for us.
\n", "It takes three arguments as input
\n", "- `gate_seq_seq`: A list of string (or array) lists encoding the gates in each term of the Hamiltonian. I.e. `[['X','X'], ['Y','Y'], ['Z','Z']]` corresponds to $H = aX_iX_j + bY_kY_l + cZ_mZ_n$ with qubit indices $i,j,k,l,m,n$ specified in the second argument and coefficients $a,b,c$ specified in the third argument
\n", "- `qubit_inds_seq`: A list of integer lists encoding which qubit indices to apply the aforementioned gates. I.e. `[[0, 1],[0,1],[0,1]]`. Must have the same structure as `gate_seq_seq` above.
\n", "- `coefficients`: A list of floats encoding any coefficients in the Hamiltonian. I.e. `[2.3, 0.8, 1.2]` corresponds to $a=2.3,b=0.8,c=1.2$ above. Must have the same length as the two above arguments."]}, {"cell_type": "markdown", "metadata": {}, "source": ["More specifically let's consider the problem of finding the ground state of the quantum Heisenberg Hamiltonian
\n", "$$ H = \\sum_{i=1}^{n_\\text{qubits}-1} X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1}. $$
\n", "As described, we define the Hamiltonian via its gate strings, qubit indices and coefficients."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_gates = [['X', 'X'], ['Y', 'Y'], ['Z', 'Z']] * (n_qubits - 1)\n", "hamiltonian_qubit_inds = [[int(i), int(i) + 1] for i in jnp.repeat(jnp.arange(n_qubits), 3)]\n", "coefficients = [1.] * len(hamiltonian_qubit_inds)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print('Gates:\\t', hamiltonian_gates)\n", "print('Qubits:\\t', hamiltonian_qubit_inds)\n", "print('Coefficients:\\t', coefficients)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now let's get the Hamiltonian as a pure JAX function"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["st_to_expectation = qujax.get_statetensor_to_expectation_func(hamiltonian_gates,\n", " hamiltonian_qubit_inds,\n", " coefficients)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's check it works on the statetensor we've already generated."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["expected_val = st_to_expectation(statetensor)\n", "expected_val"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Now let's wrap the `param_to_st` and `st_to_expectation` together to give us an all in one `param_to_expectation` cost function."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param_to_expectation = lambda param: st_to_expectation(param_to_st(param))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param_to_expectation(params)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Sanity check that a different, randomly generated set of parameters gives us a new expected value."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["new_params = random.uniform(random.PRNGKey(1), shape=(n_params,), minval=0., maxval=2.)\n", "param_to_expectation(new_params)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# We can now use autodiff for fast, exact gradients within a VQE algorithm
\n", "The `param_to_expectation` function we created is a pure JAX function and outputs a scalar. This means we can pass it to `jax.grad` (or even better `jax.value_and_grad`)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cost_and_grad = value_and_grad(param_to_expectation)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The `cost_and_grad` function returns a tuple with the exact cost value and exact gradient evaluated at the parameters."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cost_and_grad(params)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Now we have all the tools we need to design our VQE!
\n", "We'll just use vanilla gradient descent with a constant stepsize"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def vqe(init_param, n_steps, stepsize):\n", " params = jnp.zeros((n_steps, n_params))\n", " params = params.at[0].set(init_param)\n", " cost_vals = jnp.zeros(n_steps)\n", " cost_vals = cost_vals.at[0].set(param_to_expectation(init_param))\n", " for step in range(1, n_steps):\n", " cost_val, cost_grad = cost_and_grad(params[step - 1])\n", " cost_vals = cost_vals.at[step].set(cost_val)\n", " new_param = params[step - 1] - stepsize * cost_grad\n", " params = params.at[step].set(new_param)\n", " print('Iteration:', step, '\\tCost:', cost_val, end='\\r')\n", " print('\\n')\n", " return params, cost_vals"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Ok enough talking, let's run (and whilst we're at it we'll time it too)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["%time vqe_params, vqe_cost_vals = vqe(params, n_steps=250, stepsize=0.01)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's plot the results..."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.plot(vqe_cost_vals)\n", "plt.xlabel('Iteration')\n", "plt.ylabel('Cost');"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Pretty good!"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# `jax.jit` speedup
\n", "One last thing... We can significantly speed up the VQE above via the `jax.jit`. In our current implementation, the expensive `cost_and_grad` function is compiled to [XLA](https://www.tensorflow.org/xla) and then executed at each call. By invoking `jax.jit` we ensure that the function is compiled only once (on the first call) and then simply executed at each future call - this is much faster!"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cost_and_grad = jit(cost_and_grad)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We'll demonstrate this using the second set of initial parameters we randomly generated (to be sure of no caching)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["%time new_vqe_params, new_vqe_cost_vals = vqe(new_params, n_steps=250, stepsize=0.01)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["That's some speedup!
\n", "But let's also plot the training to be sure it converged correctly"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.plot(new_vqe_cost_vals)\n", "plt.xlabel('Iteration')\n", "plt.ylabel('Cost');"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells":[{"cell_type":"markdown","metadata":{},"source":["# VQE example with `pytket-qujax`"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from jax import numpy as jnp, random, value_and_grad, jit\n","from pytket import Circuit\n","from pytket.circuit.display import render_circuit_jupyter\n","import matplotlib.pyplot as plt"]},{"cell_type":"markdown","metadata":{},"source":["## Let's start with a TKET circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import qujax\n","from pytket.extensions.qujax.qujax_convert import tk_to_qujax"]},{"cell_type":"markdown","metadata":{},"source":["We place barriers to stop tket automatically rearranging gates and we also store the number of circuit parameters as we'll need this later."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def get_circuit(n_qubits, depth):\n"," n_params = 2 * n_qubits * (depth + 1)\n"," param = jnp.zeros((n_params,))\n"," circuit = Circuit(n_qubits)\n"," k = 0\n"," for i in range(n_qubits):\n"," circuit.H(i)\n"," for i in range(n_qubits):\n"," circuit.Rx(param[k], i)\n"," k += 1\n"," for i in range(n_qubits):\n"," circuit.Ry(param[k], i)\n"," k += 1\n"," for _ in range(depth):\n"," for i in range(0, n_qubits - 1):\n"," circuit.CZ(i, i + 1)\n"," circuit.add_barrier(range(0, n_qubits))\n"," for i in range(n_qubits):\n"," circuit.Rx(param[k], i)\n"," k += 1\n"," for i in range(n_qubits):\n"," circuit.Ry(param[k], i)\n"," k += 1\n"," return circuit, n_params"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_qubits = 4\n","depth = 2\n","circuit, n_params = get_circuit(n_qubits, depth)\n","render_circuit_jupyter(circuit)"]},{"cell_type":"markdown","metadata":{},"source":["## Now let's invoke qujax\n","The `pytket.extensions.qujax.tk_to_qujax` function will generate a parameters -> statetensor function for us."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param_to_st = tk_to_qujax(circuit)"]},{"cell_type":"markdown","metadata":{},"source":["Let's try it out on some random parameters values. Be aware that's JAX's random number generator requires a `jax.random.PRNGkey` every time it's called - more info on that [here](https://jax.readthedocs.io/en/latest/jax.random.html).\n","Be aware that we still have convention where parameters are specified as multiples of $\\pi$ - that is in [0,2]."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["params = random.uniform(random.PRNGKey(0), shape=(n_params,), minval=0.0, maxval=2.0)\n","statetensor = param_to_st(params)\n","print(statetensor)\n","print(statetensor.shape)"]},{"cell_type":"markdown","metadata":{},"source":["Note that this function also has an optional second argument where an initiating `statetensor_in` can be provided. If it is not provided it will default to the all 0s state (as we use here)."]},{"cell_type":"markdown","metadata":{},"source":["We can obtain statevector by simply calling `.flatten()`"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["statevector = statetensor.flatten()\n","statevector.shape"]},{"cell_type":"markdown","metadata":{},"source":["And sampling probabilities by squaring the absolute value of the statevector"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["sample_probs = jnp.square(jnp.abs(statevector))\n","plt.bar(jnp.arange(statevector.size), sample_probs)"]},{"cell_type":"markdown","metadata":{},"source":["## Cost function"]},{"cell_type":"markdown","metadata":{},"source":["Now we have our `param_to_st` function we are free to define a cost function that acts on bitstrings (e.g. maxcut) or integers by directly wrapping a function around `param_to_st`. However, cost functions defined via quantum Hamiltonians are a bit more involved.\n","Fortunately, we can encode an Hamiltonian in JAX via the `qujax.get_statetensor_to_expectation_func` function which generates a statetensor -> expected value function for us.\n","It takes three arguments as input\n","- `gate_seq_seq`: A list of string (or array) lists encoding the gates in each term of the Hamiltonian. I.e. `[['X','X'], ['Y','Y'], ['Z','Z']]` corresponds to $H = aX_iX_j + bY_kY_l + cZ_mZ_n$ with qubit indices $i,j,k,l,m,n$ specified in the second argument and coefficients $a,b,c$ specified in the third argument\n","- `qubit_inds_seq`: A list of integer lists encoding which qubit indices to apply the aforementioned gates. I.e. `[[0, 1],[0,1],[0,1]]`. Must have the same structure as `gate_seq_seq` above.\n","- `coefficients`: A list of floats encoding any coefficients in the Hamiltonian. I.e. `[2.3, 0.8, 1.2]` corresponds to $a=2.3,b=0.8,c=1.2$ above. Must have the same length as the two above arguments."]},{"cell_type":"markdown","metadata":{},"source":["More specifically let's consider the problem of finding the ground state of the quantum Heisenberg Hamiltonian"]},{"cell_type":"markdown","metadata":{},"source":["$$\n","\\begin{equation}\n","H = \\sum_{i=1}^{n_\\text{qubits}-1} X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1}.\n","\\end{equation}\n","$$\n","\n","As described, we define the Hamiltonian via its gate strings, qubit indices and coefficients."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["hamiltonian_gates = [[\"X\", \"X\"], [\"Y\", \"Y\"], [\"Z\", \"Z\"]] * (n_qubits - 1)\n","hamiltonian_qubit_inds = [\n"," [int(i), int(i) + 1] for i in jnp.repeat(jnp.arange(n_qubits), 3)\n","]\n","coefficients = [1.0] * len(hamiltonian_qubit_inds)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Gates:\\t\", hamiltonian_gates)\n","print(\"Qubits:\\t\", hamiltonian_qubit_inds)\n","print(\"Coefficients:\\t\", coefficients)"]},{"cell_type":"markdown","metadata":{},"source":["Now let's get the Hamiltonian as a pure JAX function"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["st_to_expectation = qujax.get_statetensor_to_expectation_func(\n"," hamiltonian_gates, hamiltonian_qubit_inds, coefficients\n",")"]},{"cell_type":"markdown","metadata":{},"source":["Let's check it works on the statetensor we've already generated."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["expected_val = st_to_expectation(statetensor)\n","expected_val"]},{"cell_type":"markdown","metadata":{},"source":["Now let's wrap the `param_to_st` and `st_to_expectation` together to give us an all in one `param_to_expectation` cost function."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param_to_expectation = lambda param: st_to_expectation(param_to_st(param))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param_to_expectation(params)"]},{"cell_type":"markdown","metadata":{},"source":["Sanity check that a different, randomly generated set of parameters gives us a new expected value."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["new_params = random.uniform(\n"," random.PRNGKey(1), shape=(n_params,), minval=0.0, maxval=2.0\n",")\n","param_to_expectation(new_params)"]},{"cell_type":"markdown","metadata":{},"source":["## Exact gradients within a VQE algorithm\n","The `param_to_expectation` function we created is a pure JAX function and outputs a scalar. This means we can pass it to `jax.grad` (or even better `jax.value_and_grad`)."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cost_and_grad = value_and_grad(param_to_expectation)"]},{"cell_type":"markdown","metadata":{},"source":["The `cost_and_grad` function returns a tuple with the exact cost value and exact gradient evaluated at the parameters."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cost_and_grad(params)"]},{"cell_type":"markdown","metadata":{},"source":["## Now we have all the tools we need to design our VQE!\n","We'll just use vanilla gradient descent with a constant stepsize"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def vqe(init_param, n_steps, stepsize):\n"," params = jnp.zeros((n_steps, n_params))\n"," params = params.at[0].set(init_param)\n"," cost_vals = jnp.zeros(n_steps)\n"," cost_vals = cost_vals.at[0].set(param_to_expectation(init_param))\n"," for step in range(1, n_steps):\n"," cost_val, cost_grad = cost_and_grad(params[step - 1])\n"," cost_vals = cost_vals.at[step].set(cost_val)\n"," new_param = params[step - 1] - stepsize * cost_grad\n"," params = params.at[step].set(new_param)\n"," print(\"Iteration:\", step, \"\\tCost:\", cost_val, end=\"\\r\")\n"," print(\"\\n\")\n"," return params, cost_vals"]},{"cell_type":"markdown","metadata":{},"source":["Ok enough talking, let's run (and whilst we're at it we'll time it too)"]},{"cell_type":"markdown","metadata":{},"source":["%time"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["vqe_params, vqe_cost_vals = vqe(params, n_steps=250, stepsize=0.01)"]},{"cell_type":"markdown","metadata":{},"source":["Let's plot the results..."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.plot(vqe_cost_vals)\n","plt.xlabel(\"Iteration\")\n","plt.ylabel(\"Cost\")"]},{"cell_type":"markdown","metadata":{},"source":["Pretty good!"]},{"cell_type":"markdown","metadata":{},"source":["## `jax.jit` speedup\n","One last thing... We can significantly speed up the VQE above via the `jax.jit`. In our current implementation, the expensive `cost_and_grad` function is compiled to [XLA](https://www.tensorflow.org/xla) and then executed at each call. By invoking `jax.jit` we ensure that the function is compiled only once (on the first call) and then simply executed at each future call - this is much faster!"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cost_and_grad = jit(cost_and_grad)"]},{"cell_type":"markdown","metadata":{},"source":["We'll demonstrate this using the second set of initial parameters we randomly generated (to be sure of no caching)."]},{"cell_type":"markdown","metadata":{},"source":["%time"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["new_vqe_params, new_vqe_cost_vals = vqe(new_params, n_steps=250, stepsize=0.01)"]},{"cell_type":"markdown","metadata":{},"source":["That's some speedup!\n","But let's also plot the training to be sure it converged correctly"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.plot(new_vqe_cost_vals)\n","plt.xlabel(\"Iteration\")\n","plt.ylabel(\"Cost\")"]}],"metadata":{"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.6.4"}},"nbformat":4,"nbformat_minor":2}
diff --git a/examples/pytket-qujax_qaoa.ipynb b/examples/pytket-qujax_qaoa.ipynb
index 066a6a12..be4466ae 100644
--- a/examples/pytket-qujax_qaoa.ipynb
+++ b/examples/pytket-qujax_qaoa.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Symbolic circuits with `qujax` and `pytket-qujax`
\n", "In this notebook we will show how to manipulate symbolic circuits with the `pytket-qujax` extension. In particular, we will consider a QAOA and an Ising Hamiltonian."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit\n", "from pytket.circuit.display import render_circuit_jupyter\n", "from jax import numpy as jnp, random, value_and_grad, jit\n", "from sympy import Symbol\n", "import matplotlib.pyplot as plt"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import qujax\n", "from pytket.extensions.qujax import tk_to_qujax"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# QAOA
\n", "The Quantum Approximate Optimization Algorithm (QAOA), first introduced by [Farhi et al.](https://arxiv.org/pdf/1411.4028.pdf), is a quantum variational algorithm used to solve optimization problems. It consists of a unitary $U(\\beta, \\gamma)$ formed by alternate repetitions of $U(\\beta)=e^{-i\\beta H_B}$ and $U(\\gamma)=e^{-i\\gamma H_P}$, where $H_B$ is the mixing Hamiltonian and $H_P$ the problem Hamiltonian. The goal is to find the optimal parameters that minimize $H_P$.
\n", "Given a depth $d$, the expression of the final unitary is $U(\\beta, \\gamma) = U(\\beta_d)U(\\gamma_d)\\cdots U(\\beta_1)U(\\gamma_1)$. Notice that for each repetition the parameters are different.
\n", "## Problem Hamiltonian
\n", "QAOA uses a problem dependent ansatz. Therefore, we first need to know the problem that we want to solve. In this case we will consider an Ising Hamiltonian with only $Z$ interactions. Given a set of pairs (or qubit indices) $E$, the problem Hamiltonian will be:
\n", "$$H_P = \\sum_{(i, j) \\in E}\\alpha_{ij}Z_iZ_j,$$
\n", "where $\\alpha_{ij}$ are the coefficients.
\n", "Let's build our problem Hamiltonian with random coefficients and a set of pairs for a given number of qubits:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_qubits = 4\n", "hamiltonian_qubit_inds = [(0, 1), (1, 2), (0, 2), (1, 3)]\n", "hamiltonian_gates = [[\"Z\", \"Z\"]] * (len(hamiltonian_qubit_inds))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Notice that in order to use the random package from jax we first need to define a seeded key"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["seed = 13\n", "key = random.PRNGKey(seed)\n", "coefficients = random.uniform(key, shape=(len(hamiltonian_qubit_inds),))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"Gates:\\t\", hamiltonian_gates)\n", "print(\"Qubits:\\t\", hamiltonian_qubit_inds)\n", "print(\"Coefficients:\\t\", coefficients)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["## Variational Circuit
\n", "Before constructing the circuit, we still need to select the mixing Hamiltonian. In our case, we will be using $X$ gates in each qubit, so $H_B = \\sum_{i=1}^{n}X_i$, where $n$ is the number of qubits. Notice that the unitary $U(\\beta)$, given this mixing Hamiltonian, is an $X$ rotation in each qubit with angle $\\beta$.
\n", "As for the unitary corresponding to the problem Hamiltonian, $U(\\gamma)$, it has the following form:
\n", "$$U(\\gamma)=\\prod_{(i, j) \\in E}e^{-i\\gamma\\alpha_{ij}Z_iZ_j}$$
\n", "The operation $e^{-i\\gamma\\alpha_{ij}Z_iZ_j}$ can be performed using two CNOT gates with qubit $i$ as control and qubit $j$ as target and a $Z$ rotation in qubit $j$ in between them, with angle $\\gamma\\alpha_{ij}$.
\n", "Finally, the initial state used, in general, with the QAOA is an equal superposition of all the basis states. This can be achieved adding a first layer of Hadamard gates in each qubit at the beginning of the circuit."]}, {"cell_type": "markdown", "metadata": {}, "source": ["With all the building blocks, let's construct the symbolic circuit using tket. Notice that in order to define the parameters, we use the ```Symbol``` object from the `sympy` package. More info can be found in this [documentation](https://cqcl.github.io/pytket/manual/manual_circuit.html#symbolic-circuits). In order to later convert the circuit to qujax, we need to return the list of symbolic parameters as well."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qaoa_circuit(n_qubits, depth):\n", " circuit = Circuit(n_qubits)\n", " p_keys = []\n\n", " # Initial State\n", " for i in range(n_qubits):\n", " circuit.H(i)\n", " for d in range(depth):\n", " # Hamiltonian unitary\n", " gamma_d = Symbol(f\"\u03b3_{d}\")\n", " for index in range(len(hamiltonian_qubit_inds)):\n", " pair = hamiltonian_qubit_inds[index]\n", " coef = coefficients[index]\n", " circuit.CX(pair[0], pair[1])\n", " circuit.Rz(gamma_d * coef, pair[1])\n", " circuit.CX(pair[0], pair[1])\n", " circuit.add_barrier(range(0, n_qubits))\n", " p_keys.append(gamma_d)\n\n", " # Mixing unitary\n", " beta_d = Symbol(f\"\u03b2_{d}\")\n", " for i in range(n_qubits):\n", " circuit.Rx(beta_d, i)\n", " p_keys.append(beta_d)\n", " return circuit, p_keys"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["depth = 3\n", "circuit, keys = qaoa_circuit(n_qubits, depth)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["keys"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's check the circuit:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["render_circuit_jupyter(circuit)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Now for `qujax`
\n", "The `pytket.extensions.qujax.tk_to_qujax` function will generate a parameters -> statetensor function for us. However, in order to convert a symbolic circuit we first need to define the `symbol_map`. This object maps each symbol key to their corresponding index. In our case, since the object `keys` contains the symbols in the correct order, we can simply construct the dictionary as follows:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbol_map = {keys[i]: i for i in range(len(keys))}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["symbol_map"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Then, we invoke the `tk_to_qujax` with both the circuit and the symbolic map."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param_to_st = tk_to_qujax(circuit, symbol_map=symbol_map)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["And we also construct the expectation map using the problem Hamiltonian via qujax:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["st_to_expectation = qujax.get_statetensor_to_expectation_func(\n", " hamiltonian_gates, hamiltonian_qubit_inds, coefficients\n", ")"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param_to_expectation = lambda param: st_to_expectation(param_to_st(param))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["# Training process
\n", "We construct a function that, given a parameter vector, returns the value of the cost function and the gradient.
\n", "We also `jit` to avoid recompilation, this means that the expensive `cost_and_grad` function is compiled once into a very fast XLA (C++) function which is then executed at each iteration. Alternatively, we could get the same speedup by replacing our `for` loop with `jax.lax.scan`. You can read more about JIT compilation in the [JAX documentation](https://jax.readthedocs.io/en/latest/jax-101/02-jitting.html)."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cost_and_grad = jit(value_and_grad(param_to_expectation))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["For the training process we'll use vanilla gradient descent with a constant stepsize:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["seed = 123\n", "key = random.PRNGKey(seed)\n", "init_param = random.uniform(key, shape=(len(symbol_map),))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_steps = 150\n", "stepsize = 0.01"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["param = init_param"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["cost_vals = jnp.zeros(n_steps)\n", "cost_vals = cost_vals.at[0].set(param_to_expectation(init_param))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["for step in range(1, n_steps):\n", " cost_val, cost_grad = cost_and_grad(param)\n", " cost_vals = cost_vals.at[step].set(cost_val)\n", " param = param - stepsize * cost_grad\n", " print(\"Iteration:\", step, \"\\tCost:\", cost_val, end=\"\\r\")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Let's visualise the gradient descent"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["plt.plot(cost_vals)\n", "plt.xlabel(\"Iteration\")\n", "plt.ylabel(\"Cost\")"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Symbolic circuits with `pytket-qujax`\n","In this notebook we will show how to manipulate symbolic circuits with the `pytket-qujax` extension. In particular, we will consider a QAOA and an Ising Hamiltonian."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket import Circuit\n","from pytket.circuit.display import render_circuit_jupyter\n","from jax import numpy as jnp, random, value_and_grad, jit\n","from sympy import Symbol\n","import matplotlib.pyplot as plt"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import qujax\n","from pytket.extensions.qujax import tk_to_qujax"]},{"cell_type":"markdown","metadata":{},"source":["# QAOA\n","The Quantum Approximate Optimization Algorithm (QAOA), first introduced by [Farhi et al.](https://arxiv.org/pdf/1411.4028.pdf), is a quantum variational algorithm used to solve optimization problems. It consists of a unitary $U(\\beta, \\gamma)$ formed by alternate repetitions of $U(\\beta)=e^{-i\\beta H_B}$ and $U(\\gamma)=e^{-i\\gamma H_P}$, where $H_B$ is the mixing Hamiltonian and $H_P$ the problem Hamiltonian. The goal is to find the optimal parameters that minimize $H_P$.\n","Given a depth $d$, the expression of the final unitary is $U(\\beta, \\gamma) = U(\\beta_d)U(\\gamma_d)\\cdots U(\\beta_1)U(\\gamma_1)$. Notice that for each repetition the parameters are different.\n","\n","## Problem Hamiltonian\n","QAOA uses a problem dependent ansatz. Therefore, we first need to know the problem that we want to solve. In this case we will consider an Ising Hamiltonian with only $Z$ interactions. Given a set of pairs (or qubit indices) $E$, the problem Hamiltonian will be:\n","\n","$$\n","\\begin{equation}\n","H_P = \\sum_{(i, j) \\in E}\\alpha_{ij}Z_iZ_j,\n","\\end{equation}\n","$$\n","\n","where $\\alpha_{ij}$ are the coefficients.\n","Let's build our problem Hamiltonian with random coefficients and a set of pairs for a given number of qubits:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_qubits = 4\n","hamiltonian_qubit_inds = [(0, 1), (1, 2), (0, 2), (1, 3)]\n","hamiltonian_gates = [[\"Z\", \"Z\"]] * (len(hamiltonian_qubit_inds))"]},{"cell_type":"markdown","metadata":{},"source":["Notice that in order to use the random package from jax we first need to define a seeded key"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["seed = 13\n","key = random.PRNGKey(seed)\n","coefficients = random.uniform(key, shape=(len(hamiltonian_qubit_inds),))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(\"Gates:\\t\", hamiltonian_gates)\n","print(\"Qubits:\\t\", hamiltonian_qubit_inds)\n","print(\"Coefficients:\\t\", coefficients)"]},{"cell_type":"markdown","metadata":{},"source":["## Variational Circuit\n","Before constructing the circuit, we still need to select the mixing Hamiltonian. In our case, we will be using $X$ gates in each qubit, so $H_B = \\sum_{i=1}^{n}X_i$, where $n$ is the number of qubits. Notice that the unitary $U(\\beta)$, given this mixing Hamiltonian, is an $X$ rotation in each qubit with angle $\\beta$.\n","As for the unitary corresponding to the problem Hamiltonian, $U(\\gamma)$, it has the following form:\n","\n","$$\n","\\begin{equation}\n","U(\\gamma)=\\prod_{(i, j) \\in E}e^{-i\\gamma\\alpha_{ij}Z_i Z_j}\n","\\end{equation}\n","$$\n","\n","The operation $e^{-i\\gamma\\alpha_{ij}Z_iZ_j}$ can be performed using two CNOT gates with qubit $i$ as control and qubit $j$ as target and a $Z$ rotation in qubit $j$ in between them, with angle $\\gamma\\alpha_{ij}$.\n","Finally, the initial state used, in general, with the QAOA is an equal superposition of all the basis states. This can be achieved adding a first layer of Hadamard gates in each qubit at the beginning of the circuit."]},{"cell_type":"markdown","metadata":{},"source":["With all the building blocks, let's construct the symbolic circuit using tket. Notice that in order to define the parameters, we use the ```Symbol``` object from the `sympy` package. More info can be found in this [documentation](https://cqcl.github.io/pytket/manual/manual_circuit.html#symbolic-circuits). In order to later convert the circuit to qujax, we need to return the list of symbolic parameters as well."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def qaoa_circuit(n_qubits, depth):\n"," circuit = Circuit(n_qubits)\n"," p_keys = []\n","\n"," # Initial State\n"," for i in range(n_qubits):\n"," circuit.H(i)\n"," for d in range(depth):\n"," # Hamiltonian unitary\n"," gamma_d = Symbol(f\"γ_{d}\")\n"," for index in range(len(hamiltonian_qubit_inds)):\n"," pair = hamiltonian_qubit_inds[index]\n"," coef = coefficients[index]\n"," circuit.CX(pair[0], pair[1])\n"," circuit.Rz(gamma_d * coef, pair[1])\n"," circuit.CX(pair[0], pair[1])\n"," circuit.add_barrier(range(0, n_qubits))\n"," p_keys.append(gamma_d)\n","\n"," # Mixing unitary\n"," beta_d = Symbol(f\"β_{d}\")\n"," for i in range(n_qubits):\n"," circuit.Rx(beta_d, i)\n"," p_keys.append(beta_d)\n"," return circuit, p_keys"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["depth = 3\n","circuit, keys = qaoa_circuit(n_qubits, depth)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["keys"]},{"cell_type":"markdown","metadata":{},"source":["Let's check the circuit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circuit)"]},{"cell_type":"markdown","metadata":{},"source":["# Now for `qujax`\n","The `pytket.extensions.qujax.tk_to_qujax` function will generate a parameters -> statetensor function for us. However, in order to convert a symbolic circuit we first need to define the `symbol_map`. This object maps each symbol key to their corresponding index. In our case, since the object `keys` contains the symbols in the correct order, we can simply construct the dictionary as follows:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["symbol_map = {keys[i]: i for i in range(len(keys))}"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["symbol_map"]},{"cell_type":"markdown","metadata":{},"source":["Then, we invoke the `tk_to_qujax` with both the circuit and the symbolic map."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param_to_st = tk_to_qujax(circuit, symbol_map=symbol_map)"]},{"cell_type":"markdown","metadata":{},"source":["And we also construct the expectation map using the problem Hamiltonian via qujax:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["st_to_expectation = qujax.get_statetensor_to_expectation_func(\n"," hamiltonian_gates, hamiltonian_qubit_inds, coefficients\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param_to_expectation = lambda param: st_to_expectation(param_to_st(param))"]},{"cell_type":"markdown","metadata":{},"source":["# Training process\n","We construct a function that, given a parameter vector, returns the value of the cost function and the gradient.\n","We also `jit` to avoid recompilation, this means that the expensive `cost_and_grad` function is compiled once into a very fast XLA (C++) function which is then executed at each iteration. Alternatively, we could get the same speedup by replacing our `for` loop with `jax.lax.scan`. You can read more about JIT compilation in the [JAX documentation](https://jax.readthedocs.io/en/latest/jax-101/02-jitting.html)."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cost_and_grad = jit(value_and_grad(param_to_expectation))"]},{"cell_type":"markdown","metadata":{},"source":["For the training process we'll use vanilla gradient descent with a constant stepsize:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["seed = 123\n","key = random.PRNGKey(seed)\n","init_param = random.uniform(key, shape=(len(symbol_map),))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["n_steps = 150\n","stepsize = 0.01"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["param = init_param"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cost_vals = jnp.zeros(n_steps)\n","cost_vals = cost_vals.at[0].set(param_to_expectation(init_param))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["for step in range(1, n_steps):\n"," cost_val, cost_grad = cost_and_grad(param)\n"," cost_vals = cost_vals.at[step].set(cost_val)\n"," param = param - stepsize * cost_grad\n"," print(\"Iteration:\", step, \"\\tCost:\", cost_val, end=\"\\r\")"]},{"cell_type":"markdown","metadata":{},"source":["Let's visualise the gradient descent"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["plt.plot(cost_vals)\n","plt.xlabel(\"Iteration\")\n","plt.ylabel(\"Cost\")"]}],"metadata":{"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.6.4"}},"nbformat":4,"nbformat_minor":2}
diff --git a/examples/spam_example.ipynb b/examples/spam_example.ipynb
index 543a515b..bcdce3b9 100644
--- a/examples/spam_example.ipynb
+++ b/examples/spam_example.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Calibration and Correction of State Preparation and Measurement (SPAM)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Quantum Computers available in the NISQ-era are limited by significant sources of device noise which cause errors in computation. One such noise source is errors in the preparation and measurement of quantum states, more commonly know as SPAM.
\n", "
\n", "If device SPAM error can be characterised, then device results can be modified to mitigate the error. Characterisation proceeds by determining overlap between different prepared basis states when measured, and mitigation modifies the distribution over output states of the corrected circuit. No modification of the quantum circuit being corrected is required. The ``` pytket``` ```SpamCorrecter``` class supports characterisation and mitigation of device SPAM error.
\n", "
\n", "In this tutorial we will show how the ```SpamCorrecter``` class can be used to modify real results and improve device performance when running experiments.
\n", "
\n", "This tutorial will require installation of ```pytket```, ```pytket_qiskit``` and ```qiskit```, all available on pip.
\n", "
\n", "First, import the ```SpamCorrecter``` class."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.spam import SpamCorrecter"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The SpamCorrecter class has methods for generating State Preparation and Measurement (SPAM) calibration experiments for pytket backends and correcting counts generated from those same backends.
\n", "
\n", "Let's first mitigate error from a noisy simulation, using a noise model straight from the 5-qubit IBMQ manila device. This will require a preloaded IBMQ account."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit import IBMQ"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["IBMQ.load_account()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import process_characterisation"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibmq_manila_backend = IBMQ.providers()[0].get_backend(\"ibmq_manila\")\n", "pytket_manila_characterisation = process_characterisation(ibmq_manila_backend)\n", "pytket_manila_architecture = pytket_manila_characterisation[\"Architecture\"]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import networkx as nx\n", "import matplotlib.pyplot as plt"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["manila_graph = nx.Graph(pytket_manila_architecture.coupling)\n", "nx.draw(manila_graph, labels={node: node for node in manila_graph.nodes()})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["SPAM correction requires subsets of qubits which are assumed to only have SPAM errors correlated with each other, and no other qubits.
\n", "
\n", "Correlated errors are usually dependent on the connectivity layout of devices, as shown above.
\n", "
\n", "As manila is a small 5-qubit device with few connections, let's assume that all qubits have correlated SPAM errors. The number of calibration circuits produced is exponential in the maximum number of correlated circuits, so finding good subsets of correlated qubits is important for characterising larger devices with smaller experimental overhead.
\n", "
\n", "We can produce an ```IBMQEmulatorBackend``` to run this. This uses a noise model from ```ibmq_manila``` produced using qiskit-aer. We can then execute all calibration circuits through the backend."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import IBMQEmulatorBackend, AerBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 8192\n", "pytket_noisy_sim_backend = IBMQEmulatorBackend(\"ibmq_manila\")\n", "manila_node_subsets = pytket_noisy_sim_backend.backend_info.architecture.nodes\n", "manila_spam = SpamCorrecter([manila_node_subsets], pytket_noisy_sim_backend)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The SpamCorrecter uses these subsets of qubits to produce calibration circuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["calibration_circuits = manila_spam.calibration_circuits()\n", "print(\"Number of calibration circuits: \", len(calibration_circuits))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sim_handles = pytket_noisy_sim_backend.process_circuits(calibration_circuits, n_shots)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Count results from the simulator are then used to calculate the matrices used for SPAM correction for ```ibmq_manila```."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sim_count_results = pytket_noisy_sim_backend.get_results(sim_handles)\n", "manila_spam.calculate_matrices(sim_count_results)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_circuit = (\n", " Circuit(len(pytket_noisy_sim_backend.backend_info.architecture.nodes))\n", " .H(0)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .measure_all()\n", ")\n", "ghz_circuit = pytket_noisy_sim_backend.get_compiled_circuit(ghz_circuit)\n", "ghz_noisy_handle = pytket_noisy_sim_backend.process_circuit(ghz_circuit, n_shots)\n", "ghz_noisy_result = pytket_noisy_sim_backend.get_result(ghz_noisy_handle)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We also run a noiseless simulation so we can compare performance."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pytket_noiseless_sim_backend = AerBackend()\n", "ghz_noiseless_handle = pytket_noiseless_sim_backend.process_circuit(\n", " ghz_circuit, n_shots\n", ")\n", "ghz_noiseless_result = pytket_noiseless_sim_backend.get_result(ghz_noiseless_handle)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Noisy simulator counts are corrected using the ```SpamCorrecter``` objects ```correct_counts``` method.
\n", "
\n", "To correctly amend counts, the ```correct_counts``` method requires a ``ParallelMeasures`` type object, a list of ``Dict[Qubit, Bit]`` where each dictionary denotes a set of Qubit measured in parallel and the Bit their measured values are assigned to.
\n", "
\n", "The ``SpamCorrecter`` class has a helper method ``get_parallel_measure`` for retrieving this object for a Circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_parallel_measure = manila_spam.get_parallel_measure(ghz_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_spam_corrected_result = manila_spam.correct_counts(\n", " ghz_noisy_result, ghz_parallel_measure\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Import and define the Jensen-Shannon divergence, which we will use for comparing performance. The Jensen-Shannon divergence is a symmetric and finite measure of similarity between two probability distributions. A smaller divergence implies more similarity between two probability distributions."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from scipy.stats import entropy\n", "import numpy as np\n", "import itertools"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def binseq(k):\n", " return [\"\".join(x) for x in itertools.product(\"01\", repeat=k)]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def probs_from_counts(result):\n", " counts = result.get_counts()\n", " counts_dict = dict()\n", " for x in counts:\n", " counts_dict[\"\".join(str(e) for e in x)] = counts[x]\n", " converted = []\n", " binary_strings = binseq(len(list(counts.keys())[0]))\n", " for b in binary_strings:\n", " converted.append(counts_dict.get(b, 0))\n", " return converted / np.sum(converted)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def JSD(P, Q):\n", " _P = P / np.linalg.norm(P, ord=1)\n", " _Q = Q / np.linalg.norm(Q, ord=1)\n", " _M = 0.5 * (_P + _Q)\n", " return 0.5 * (entropy(_P, _M) + entropy(_Q, _M))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Convert our counts results to a probability distribution over the basis states for comparison."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_noiseless_probabilities = probs_from_counts(ghz_noiseless_result)\n", "ghz_noisy_probabilities = probs_from_counts(ghz_noisy_result)\n", "ghz_spam_corrected_probabilities = probs_from_counts(ghz_spam_corrected_result)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_noisy_probabilities),\n", ")\n", "print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and spam corrected noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_spam_corrected_probabilities),\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In our noisy simulated case, spam corrected results produced a distribution closer to the expected distribution.
\n", "
\n", "There are two methods available for correcting counts: the default ```bayesian```, and ```invert```. Further information on each method is available at our [documentation](https://cqcl.github.io/tket/pytket/api/utils.html#module-pytket.utils.spam).
\n", "
\n", "Let's look at how the ```invert``` method performs."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_invert_corrected_result = manila_spam.correct_counts(\n", " ghz_noisy_result, ghz_parallel_measure, method=\"invert\"\n", ")\n", "ghz_invert_probabilities = probs_from_counts(ghz_invert_corrected_result)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and Bayesian-corrected noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_spam_corrected_probabilities),\n", ")\n", "print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and invert-corrected noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_invert_probabilities),\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To see how SPAM correction performs on results from a real IBMQ quantum device, try replacing `IBMQEmulatorBackend` with `IBMQBackend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import IBMQBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibm_backend = IBMQBackend(\"ibmq_manila\")"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Calibration and correction of state preparation and measurement (SPAM)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Quantum Computers available in the NISQ-era are limited by significant sources of device noise which cause errors in computation. One such noise source is errors in the preparation and measurement of quantum states, more commonly know as SPAM.
\n", "
\n", "If device SPAM error can be characterised, then device results can be modified to mitigate the error. Characterisation proceeds by determining overlap between different prepared basis states when measured, and mitigation modifies the distribution over output states of the corrected circuit. No modification of the quantum circuit being corrected is required. The ``` pytket``` ```SpamCorrecter``` class supports characterisation and mitigation of device SPAM error.
\n", "
\n", "In this tutorial we will show how the ```SpamCorrecter``` class can be used to modify real results and improve device performance when running experiments.
\n", "
\n", "This tutorial will require installation of ```pytket```, ```pytket_qiskit``` and ```qiskit```, all available on pip.
\n", "
\n", "First, import the ```SpamCorrecter``` class."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.spam import SpamCorrecter"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The SpamCorrecter class has methods for generating State Preparation and Measurement (SPAM) calibration experiments for pytket backends and correcting counts generated from those same backends.
\n", "
\n", "Let's first mitigate error from a noisy simulation, using a noise model straight from the 5-qubit IBMQ manila device. This will require a preloaded IBMQ account."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from qiskit import IBMQ"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["IBMQ.load_account()"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import process_characterisation"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibmq_manila_backend = IBMQ.providers()[0].get_backend(\"ibmq_manila\")\n", "pytket_manila_characterisation = process_characterisation(ibmq_manila_backend)\n", "pytket_manila_architecture = pytket_manila_characterisation[\"Architecture\"]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import networkx as nx\n", "import matplotlib.pyplot as plt"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["manila_graph = nx.Graph(pytket_manila_architecture.coupling)\n", "nx.draw(manila_graph, labels={node: node for node in manila_graph.nodes()})"]}, {"cell_type": "markdown", "metadata": {}, "source": ["SPAM correction requires subsets of qubits which are assumed to only have SPAM errors correlated with each other, and no other qubits.
\n", "
\n", "Correlated errors are usually dependent on the connectivity layout of devices, as shown above.
\n", "
\n", "As manila is a small 5-qubit device with few connections, let's assume that all qubits have correlated SPAM errors. The number of calibration circuits produced is exponential in the maximum number of correlated circuits, so finding good subsets of correlated qubits is important for characterising larger devices with smaller experimental overhead.
\n", "
\n", "We can produce an ```IBMQEmulatorBackend``` to run this. This uses a noise model from ```ibmq_manila``` produced using qiskit-aer. We can then execute all calibration circuits through the backend."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import IBMQEmulatorBackend, AerBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["n_shots = 8192\n", "pytket_noisy_sim_backend = IBMQEmulatorBackend(\"ibmq_manila\")\n", "manila_node_subsets = pytket_noisy_sim_backend.backend_info.architecture.nodes\n", "manila_spam = SpamCorrecter([manila_node_subsets], pytket_noisy_sim_backend)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The SpamCorrecter uses these subsets of qubits to produce calibration circuits."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["calibration_circuits = manila_spam.calibration_circuits()\n", "print(\"Number of calibration circuits: \", len(calibration_circuits))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sim_handles = pytket_noisy_sim_backend.process_circuits(calibration_circuits, n_shots)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Count results from the simulator are then used to calculate the matrices used for SPAM correction for ```ibmq_manila```."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["sim_count_results = pytket_noisy_sim_backend.get_results(sim_handles)\n", "manila_spam.calculate_matrices(sim_count_results)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_circuit = (\n", " Circuit(len(pytket_noisy_sim_backend.backend_info.architecture.nodes))\n", " .H(0)\n", " .CX(0, 1)\n", " .CX(1, 2)\n", " .measure_all()\n", ")\n", "ghz_circuit = pytket_noisy_sim_backend.get_compiled_circuit(ghz_circuit)\n", "ghz_noisy_handle = pytket_noisy_sim_backend.process_circuit(ghz_circuit, n_shots)\n", "ghz_noisy_result = pytket_noisy_sim_backend.get_result(ghz_noisy_handle)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We also run a noiseless simulation so we can compare performance."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pytket_noiseless_sim_backend = AerBackend()\n", "ghz_noiseless_handle = pytket_noiseless_sim_backend.process_circuit(\n", " ghz_circuit, n_shots\n", ")\n", "ghz_noiseless_result = pytket_noiseless_sim_backend.get_result(ghz_noiseless_handle)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Noisy simulator counts are corrected using the ```SpamCorrecter``` objects ```correct_counts``` method.
\n", "
\n", "To correctly amend counts, the ```correct_counts``` method requires a ``ParallelMeasures`` type object, a list of ``Dict[Qubit, Bit]`` where each dictionary denotes a set of Qubit measured in parallel and the Bit their measured values are assigned to.
\n", "
\n", "The ``SpamCorrecter`` class has a helper method ``get_parallel_measure`` for retrieving this object for a Circuit."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_parallel_measure = manila_spam.get_parallel_measure(ghz_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_spam_corrected_result = manila_spam.correct_counts(\n", " ghz_noisy_result, ghz_parallel_measure\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Import and define the Jensen-Shannon divergence, which we will use for comparing performance. The Jensen-Shannon divergence is a symmetric and finite measure of similarity between two probability distributions. A smaller divergence implies more similarity between two probability distributions."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from scipy.stats import entropy\n", "import numpy as np\n", "import itertools"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def binseq(k):\n", " return [\"\".join(x) for x in itertools.product(\"01\", repeat=k)]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def probs_from_counts(result):\n", " counts = result.get_counts()\n", " counts_dict = dict()\n", " for x in counts:\n", " counts_dict[\"\".join(str(e) for e in x)] = counts[x]\n", " converted = []\n", " binary_strings = binseq(len(list(counts.keys())[0]))\n", " for b in binary_strings:\n", " converted.append(counts_dict.get(b, 0))\n", " return converted / np.sum(converted)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def JSD(P, Q):\n", " _P = P / np.linalg.norm(P, ord=1)\n", " _Q = Q / np.linalg.norm(Q, ord=1)\n", " _M = 0.5 * (_P + _Q)\n", " return 0.5 * (entropy(_P, _M) + entropy(_Q, _M))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Convert our counts results to a probability distribution over the basis states for comparison."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_noiseless_probabilities = probs_from_counts(ghz_noiseless_result)\n", "ghz_noisy_probabilities = probs_from_counts(ghz_noisy_result)\n", "ghz_spam_corrected_probabilities = probs_from_counts(ghz_spam_corrected_result)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_noisy_probabilities),\n", ")\n", "print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and spam corrected noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_spam_corrected_probabilities),\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In our noisy simulated case, spam corrected results produced a distribution closer to the expected distribution.
\n", "
\n", "There are two methods available for correcting counts: the default ```bayesian```, and ```invert```. Further information on each method is available at our [documentation](https://cqcl.github.io/tket/pytket/api/utils.html#module-pytket.utils.spam).
\n", "
\n", "Let's look at how the ```invert``` method performs."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ghz_invert_corrected_result = manila_spam.correct_counts(\n", " ghz_noisy_result, ghz_parallel_measure, method=\"invert\"\n", ")\n", "ghz_invert_probabilities = probs_from_counts(ghz_invert_corrected_result)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and Bayesian-corrected noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_spam_corrected_probabilities),\n", ")\n", "print(\n", " \"Jensen-Shannon Divergence between noiseless simulation probability distribution and invert-corrected noisy simulation probability distribution: \",\n", " JSD(ghz_noiseless_probabilities, ghz_invert_probabilities),\n", ")"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To see how SPAM correction performs on results from a real IBMQ quantum device, try replacing `IBMQEmulatorBackend` with `IBMQBackend`."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import IBMQBackend"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["ibm_backend = IBMQBackend(\"ibmq_manila\")"]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
diff --git a/examples/tket_benchmarking.ipynb b/examples/tket_benchmarking.ipynb
deleted file mode 100644
index 0dfe0463..00000000
--- a/examples/tket_benchmarking.ipynb
+++ /dev/null
@@ -1,866 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# tket benchmarking example\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Warning: This notebook is now deprecated for versions from 0.2.0 onwards, and is not going to be updated in ongoing releases.\n",
- "\n",
- "The aim of this example is to show how to run the IBM benchmarking circuits through tket. You will need both `pytket` and `pytket_qiskit` installed from pip before running this turoial. You will also need `pandas` to capture the data.\n",
- "\n",
- "The benchmarking circuits originated from https://github.com/iic-jku/ibm_qx_mapping/tree/master/examples, but there is a copy in pytket in the \"benchmarking\" folder. The initial circuits are written in QASM, meaning that they must be converted to tket's internal representation via Qiskit. Using this script we will compile these circuits through tket and then print a table to terminal containing analysis of the circuits post-compilation.\n",
- "\n",
- "First, begin by importing qiskit and pytket."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "import qiskit\n",
- "import pytket"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We need access to Qiskit's `QuantumCircuit` and the `circuit_to_dag` converter, which will allow us to take the original QASM and retrieve a Qiskit DAGCircuit. There is then a pytket method `dagcircuit_to_tk` to convert this DAGCircuit to tket's internal representation. We also need the pytket method `coupling_to_arc` to convert IBM's architectures to the tket `Architecture` class for the use of routing."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "from qiskit import QuantumCircuit\n",
- "\n",
- "from pytket.extensions.qiskit import qiskit_to_tk"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We need all of the required pytket equipment to allow us to perform clean-up transform passes, to route and to analyse the circuits. Lastly, we require the `pandas` module to hold our data, and \n",
- "`time` to benchmark compilation time per circuit."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "from pytket import Architecture, route\n",
- "from pytket import OpType, Transform\n",
- "import pandas\n",
- "import time"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The architectures used to benchmark for our routing paper were the IBMqx5 and IBMTokyo architectures. The architectures qx5 and Tokyo respectively are shown below (although the diagrams do not show the directedness of the coupling maps). These diagrams are from https://www.research.ibm.com/ibm-q/technology/devices/#ibmqx5. We will now define the coupling map representing both architectures. These will later be converted to directed graphs to be used by tket.\n",
- "![alt text](IBMqx5Arc.png \"Title\")\n",
- "![alt text](IBMTokyoArc.png \"Title2\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [],
- "source": [
- "ibm_devices = {\n",
- " \"ibmqx5\": {\"edges\": [(1, 0), (1, 2), (2, 3), (3, 4), \n",
- " (3, 14), (5, 4), (6, 5), (6, 7), (6, 11), (7, 10), \n",
- " (8, 7), (9, 8), (9, 10), (11, 10), (12, 5), (12, 11), \n",
- " (12, 13), (13, 4), (13, 14), (15, 0), (15, 2), (15, 14)],\n",
- " \"nodes\": 16},\n",
- " \"ibmq_20_tokyo\": {\"edges\": [(0, 1), (0, 5), (1, 2), (1, 6), (1, 7), (2, 3), (2, 6),\n",
- " (2, 7), (3, 4), (3, 8), (3, 9), (4, 8), (4, 9), (5, 6), (5, 10), (5, 11), (6, 7),\n",
- " (6, 10), (6, 11), (7, 8), (7, 12), (7, 13), (8, 9), (8, 12), (8, 13), (9, 14), (10, 11),\n",
- " (10, 15), (11, 12), (11, 16), (11, 17), (12, 13), (12, 16), (12, 17), (13, 14), (13, 18),\n",
- " (13, 19), (14, 18), (14, 19), (15, 16), (16, 17), (17, 18), (18, 19)], \"nodes\": 20}\n",
- "}"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We shall now choose the device and create a directed graph for tket's routing to use."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [],
- "source": [
- "device_name = 'ibmqx5' #'ibm_20_tokyo' ###Note: can also be ran using the Tokyo machine architecture\n",
- " ### or with a user-defined coupling map\n",
- "coupling_map = ibm_devices[device_name][\"edges\"]\n",
- "directed_arc = Architecture(coupling_map)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We now define a method which takes in (1) a QASM file and (2) a directed graph architecture; it returns analysis of the circuit after our transform passes and routing procedure have been completed. It will also print out to terminal the time taken for all the transformations and routing to finish. Changing which optimisations are run in this method will trade off quality of the ouputs for time taken. For example, removing all optimisation passes and just running the routing procedure will give the fastest run time but may leave some redundant gates in the final circuit."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [],
- "source": [
- "def getStats(filename, directed_arc):\n",
- " qc = QuantumCircuit.from_qasm_file(filename)\n",
- " tkcirc = qiskit_to_tk(qc)\n",
- " start_time = time.process_time()\n",
- " Transform.OptimisePhaseGadgets().apply(tkcirc)\n",
- " outcirc = route(tkcirc, directed_arc)\n",
- " # decompose swaps to CX gates and redirect CXs in wrong direction\n",
- " outcirc.decompose_SWAP_to_CX()\n",
- " outcirc.redirect_CX_gates(directed_arc)\n",
- " Transform.OptimisePostRouting().apply(outcirc)\n",
- " \n",
- " time_elapsed = time.process_time() - start_time\n",
- " \n",
- " print(\"Compilation time for circuit \" + str(filename) + \": \" + str(time_elapsed) + \"s\")\n",
- " if outcirc.n_gates==0:\n",
- " return [0,0,0,0,0]\n",
- " ###Returns: [number of vertices, circuit depth, nubmer of CX gates, number of parallel slices of CX gates]\n",
- " return [outcirc.n_gates, outcirc.depth(), outcirc.n_gates_of_type(OpType.CX), \n",
- " outcirc.depth_by_type(OpType.CX), time_elapsed]\n",
- " ###Note: the raw number of vertices in the circuits and the raw depth \n",
- " ### need to have the i/o vertices removed for fair comparisons"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Lastly, we generate the table of input QASM filenames from a csv file using `pandas` and run the circuits through our compiler. The results are printed to terminal by default."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Compilation time for circuit benchmarking/ibmq/xor5_254.qasm: 0.00951000000000013s\n",
- "Time spent compiling so far: 0.00951000000000013\n",
- "Compilation time for circuit benchmarking/ibmq/graycode6_47.qasm: 0.005398000000000014s\n",
- "Time spent compiling so far: 0.014908000000000143\n",
- "Compilation time for circuit benchmarking/ibmq/ex1_226.qasm: 0.008242000000000083s\n",
- "Time spent compiling so far: 0.023150000000000226\n",
- "Compilation time for circuit benchmarking/ibmq/4gt11_84.qasm: 0.015388999999999875s\n",
- "Time spent compiling so far: 0.0385390000000001\n",
- "Compilation time for circuit benchmarking/ibmq/4mod5-v0_20.qasm: 0.020800999999999625s\n",
- "Time spent compiling so far: 0.059339999999999726\n",
- "Compilation time for circuit benchmarking/ibmq/ex-1_166.qasm: 0.017431999999999892s\n",
- "Time spent compiling so far: 0.07677199999999962\n",
- "Compilation time for circuit benchmarking/ibmq/4mod5-v1_22.qasm: 0.01641899999999996s\n",
- "Time spent compiling so far: 0.09319099999999958\n",
- "Compilation time for circuit benchmarking/ibmq/mod5d1_63.qasm: 0.024310000000000276s\n",
- "Time spent compiling so far: 0.11750099999999986\n",
- "Compilation time for circuit benchmarking/ibmq/ham3_102.qasm: 0.016796000000000255s\n",
- "Time spent compiling so far: 0.1342970000000001\n",
- "Compilation time for circuit benchmarking/ibmq/4gt11_83.qasm: 0.01989099999999988s\n",
- "Time spent compiling so far: 0.154188\n",
- "Compilation time for circuit benchmarking/ibmq/4gt11_82.qasm: 0.024681999999999427s\n",
- "Time spent compiling so far: 0.17886999999999942\n",
- "Compilation time for circuit benchmarking/ibmq/rd32-v0_66.qasm: 0.022649000000000363s\n",
- "Time spent compiling so far: 0.20151899999999978\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v0_27.qasm: 0.031051000000000162s\n",
- "Time spent compiling so far: 0.23256999999999994\n",
- "Compilation time for circuit benchmarking/ibmq/4mod5-v1_24.qasm: 0.035730999999999735s\n",
- "Time spent compiling so far: 0.2683009999999997\n",
- "Compilation time for circuit benchmarking/ibmq/4mod5-v0_19.qasm: 0.03168000000000015s\n",
- "Time spent compiling so far: 0.29998099999999983\n",
- "Compilation time for circuit benchmarking/ibmq/mod5mils_65.qasm: 0.033256000000000174s\n",
- "Time spent compiling so far: 0.333237\n",
- "Compilation time for circuit benchmarking/ibmq/rd32-v1_68.qasm: 0.03151800000000016s\n",
- "Time spent compiling so far: 0.36475500000000016\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v1_28.qasm: 0.04489699999999974s\n",
- "Time spent compiling so far: 0.4096519999999999\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v2_33.qasm: 0.03587100000000021s\n",
- "Time spent compiling so far: 0.4455230000000001\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v4_37.qasm: 0.032544999999999824s\n",
- "Time spent compiling so far: 0.47806799999999994\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v3_35.qasm: 0.0310940000000004s\n",
- "Time spent compiling so far: 0.5091620000000003\n",
- "Compilation time for circuit benchmarking/ibmq/3_17_13.qasm: 0.03810599999999997s\n",
- "Time spent compiling so far: 0.5472680000000003\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v1_29.qasm: 0.033971000000000195s\n",
- "Time spent compiling so far: 0.5812390000000005\n",
- "Compilation time for circuit benchmarking/ibmq/miller_11.qasm: 0.05386399999999991s\n",
- "Time spent compiling so far: 0.6351030000000004\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v3_34.qasm: 0.06335800000000003s\n",
- "Time spent compiling so far: 0.6984610000000004\n",
- "Compilation time for circuit benchmarking/ibmq/decod24-v2_43.qasm: 0.05558200000000024s\n",
- "Time spent compiling so far: 0.7540430000000007\n",
- "Compilation time for circuit benchmarking/ibmq/decod24-v0_38.qasm: 0.053290000000000504s\n",
- "Time spent compiling so far: 0.8073330000000012\n",
- "Compilation time for circuit benchmarking/ibmq/mod5d2_64.qasm: 0.04377400000000087s\n",
- "Time spent compiling so far: 0.8511070000000021\n",
- "Compilation time for circuit benchmarking/ibmq/4gt13_92.qasm: 0.07327100000000009s\n",
- "Time spent compiling so far: 0.9243780000000021\n",
- "Compilation time for circuit benchmarking/ibmq/4gt13-v1_93.qasm: 0.0683429999999996s\n",
- "Time spent compiling so far: 0.9927210000000017\n",
- "Compilation time for circuit benchmarking/ibmq/4mod5-v0_18.qasm: 0.05403900000000039s\n",
- "Time spent compiling so far: 1.0467600000000021\n",
- "Compilation time for circuit benchmarking/ibmq/decod24-bdd_294.qasm: 0.07072400000000023s\n",
- "Time spent compiling so far: 1.1174840000000024\n",
- "Compilation time for circuit benchmarking/ibmq/one-two-three-v2_100.qasm: 0.0743349999999996s\n",
- "Time spent compiling so far: 1.191819000000002\n",
- "Compilation time for circuit benchmarking/ibmq/one-two-three-v3_101.qasm: 0.06547600000000031s\n",
- "Time spent compiling so far: 1.2572950000000023\n",
- "Compilation time for circuit benchmarking/ibmq/4mod5-v1_23.qasm: 0.05346799999999963s\n",
- "Time spent compiling so far: 1.310763000000002\n",
- "Compilation time for circuit benchmarking/ibmq/4mod5-bdd_287.qasm: 0.07852100000000029s\n",
- "Time spent compiling so far: 1.3892840000000022\n",
- "Compilation time for circuit benchmarking/ibmq/rd32_270.qasm: 0.061949999999999505s\n",
- "Time spent compiling so far: 1.4512340000000017\n",
- "Compilation time for circuit benchmarking/ibmq/4gt5_75.qasm: 0.0817629999999987s\n",
- "Time spent compiling so far: 1.5329970000000004\n",
- "Compilation time for circuit benchmarking/ibmq/alu-bdd_288.qasm: 0.06410000000000071s\n",
- "Time spent compiling so far: 1.597097000000001\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v0_26.qasm: 0.08118200000000009s\n",
- "Time spent compiling so far: 1.6782790000000012\n",
- "Compilation time for circuit benchmarking/ibmq/decod24-v1_41.qasm: 0.0812450000000009s\n",
- "Time spent compiling so far: 1.759524000000002\n",
- "Compilation time for circuit benchmarking/ibmq/rd53_138.qasm: 0.13351399999999902s\n",
- "Time spent compiling so far: 1.893038000000001\n",
- "Compilation time for circuit benchmarking/ibmq/4gt5_76.qasm: 0.06876499999999997s\n",
- "Time spent compiling so far: 1.961803000000001\n",
- "Compilation time for circuit benchmarking/ibmq/4gt13_91.qasm: 0.08259399999999939s\n",
- "Time spent compiling so far: 2.0443970000000005\n",
- "Compilation time for circuit benchmarking/ibmq/cnt3-5_179.qasm: 0.28147000000000055s\n",
- "Time spent compiling so far: 2.325867000000001\n",
- "Compilation time for circuit benchmarking/ibmq/qft_10.qasm: 0.025814000000000448s\n",
- "Time spent compiling so far: 2.325867000000001\n",
- "Compilation time for circuit benchmarking/ibmq/4gt13_90.qasm: 0.09255399999999803s\n",
- "Time spent compiling so far: 2.418420999999999\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v4_36.qasm: 0.11703999999999937s\n",
- "Time spent compiling so far: 2.5354609999999984\n",
- "Compilation time for circuit benchmarking/ibmq/mini_alu_305.qasm: 0.21232399999999885s\n",
- "Time spent compiling so far: 2.7477849999999973\n",
- "Compilation time for circuit benchmarking/ibmq/ising_model_10.qasm: 0.2612620000000003s\n",
- "Time spent compiling so far: 3.0090469999999976\n",
- "Compilation time for circuit benchmarking/ibmq/ising_model_16.qasm: 0.3769770000000001s\n",
- "Time spent compiling so far: 3.3860239999999977\n",
- "Compilation time for circuit benchmarking/ibmq/ising_model_13.qasm: 0.33491099999999996s\n",
- "Time spent compiling so far: 3.7209349999999977\n",
- "Compilation time for circuit benchmarking/ibmq/4gt5_77.qasm: 0.1293350000000011s\n",
- "Time spent compiling so far: 3.8502699999999987\n",
- "Compilation time for circuit benchmarking/ibmq/sys6-v0_111.qasm: 0.30446699999999893s\n",
- "Time spent compiling so far: 4.154736999999997\n",
- "Compilation time for circuit benchmarking/ibmq/one-two-three-v1_99.qasm: 0.15336699999999936s\n",
- "Time spent compiling so far: 4.308103999999997\n",
- "Compilation time for circuit benchmarking/ibmq/one-two-three-v0_98.qasm: 0.12628400000000006s\n",
- "Time spent compiling so far: 4.434387999999997\n",
- "Compilation time for circuit benchmarking/ibmq/decod24-v3_45.qasm: 0.14908700000000152s\n",
- "Time spent compiling so far: 4.583474999999998\n",
- "Compilation time for circuit benchmarking/ibmq/4gt10-v1_81.qasm: 0.14076600000000106s\n",
- "Time spent compiling so far: 4.724240999999999\n",
- "Compilation time for circuit benchmarking/ibmq/aj-e11_165.qasm: 0.14970000000000105s\n",
- "Time spent compiling so far: 4.873941\n",
- "Compilation time for circuit benchmarking/ibmq/4mod7-v0_94.qasm: 0.16386000000000145s\n",
- "Time spent compiling so far: 5.037801000000002\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v2_32.qasm: 0.15958599999999912s\n",
- "Time spent compiling so far: 5.197387000000001\n",
- "Compilation time for circuit benchmarking/ibmq/rd73_140.qasm: 0.2630720000000011s\n",
- "Time spent compiling so far: 5.460459000000002\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Compilation time for circuit benchmarking/ibmq/4mod7-v1_96.qasm: 0.1598600000000001s\n",
- "Time spent compiling so far: 5.620319000000002\n",
- "Compilation time for circuit benchmarking/ibmq/4gt4-v0_80.qasm: 0.18252500000000182s\n",
- "Time spent compiling so far: 5.802844000000004\n",
- "Compilation time for circuit benchmarking/ibmq/mod10_176.qasm: 0.17993299999999834s\n",
- "Time spent compiling so far: 5.982777000000002\n",
- "Compilation time for circuit benchmarking/ibmq/0410184_169.qasm: 0.6244690000000013s\n",
- "Time spent compiling so far: 6.6072460000000035\n",
- "Compilation time for circuit benchmarking/ibmq/qft_16.qasm: 0.05671399999999949s\n",
- "Time spent compiling so far: 6.6072460000000035\n",
- "Compilation time for circuit benchmarking/ibmq/4gt12-v0_88.qasm: 0.20898000000000394s\n",
- "Time spent compiling so far: 6.8162260000000074\n",
- "Compilation time for circuit benchmarking/ibmq/rd84_142.qasm: 0.7401959999999974s\n",
- "Time spent compiling so far: 7.556422000000005\n",
- "Compilation time for circuit benchmarking/ibmq/rd53_311.qasm: 0.5026390000000021s\n",
- "Time spent compiling so far: 8.059061000000007\n",
- "Compilation time for circuit benchmarking/ibmq/4_49_16.qasm: 0.22887300000000366s\n",
- "Time spent compiling so far: 8.28793400000001\n",
- "Compilation time for circuit benchmarking/ibmq/sym9_146.qasm: 0.5669679999999993s\n",
- "Time spent compiling so far: 8.85490200000001\n",
- "Compilation time for circuit benchmarking/ibmq/4gt12-v1_89.qasm: 0.24625699999999995s\n",
- "Time spent compiling so far: 9.10115900000001\n",
- "Compilation time for circuit benchmarking/ibmq/4gt12-v0_87.qasm: 0.21894999999999598s\n",
- "Time spent compiling so far: 9.320109000000006\n",
- "Compilation time for circuit benchmarking/ibmq/4gt4-v0_79.qasm: 0.19500299999999626s\n",
- "Time spent compiling so far: 9.515112000000002\n",
- "Compilation time for circuit benchmarking/ibmq/hwb4_49.qasm: 0.23632500000000078s\n",
- "Time spent compiling so far: 9.751437000000003\n",
- "Compilation time for circuit benchmarking/ibmq/sym6_316.qasm: 0.6572160000000018s\n",
- "Time spent compiling so far: 10.408653000000005\n",
- "Compilation time for circuit benchmarking/ibmq/4gt12-v0_86.qasm: 0.24052800000000119s\n",
- "Time spent compiling so far: 10.649181000000006\n",
- "Compilation time for circuit benchmarking/ibmq/4gt4-v0_72.qasm: 0.28336000000000183s\n",
- "Time spent compiling so far: 10.932541000000008\n",
- "Compilation time for circuit benchmarking/ibmq/4gt4-v0_78.qasm: 0.2004679999999972s\n",
- "Time spent compiling so far: 11.133009000000005\n",
- "Compilation time for circuit benchmarking/ibmq/mod10_171.qasm: 0.23787799999999848s\n",
- "Time spent compiling so far: 11.370887000000003\n",
- "Compilation time for circuit benchmarking/ibmq/4gt4-v1_74.qasm: 0.28927499999999995s\n",
- "Time spent compiling so far: 11.660162000000003\n",
- "Compilation time for circuit benchmarking/ibmq/rd53_135.qasm: 0.37902099999999805s\n",
- "Time spent compiling so far: 12.039183000000001\n",
- "Compilation time for circuit benchmarking/ibmq/mini-alu_167.qasm: 0.28952900000000525s\n",
- "Time spent compiling so far: 12.328712000000007\n",
- "Compilation time for circuit benchmarking/ibmq/one-two-three-v0_97.qasm: 0.32423s\n",
- "Time spent compiling so far: 12.652942000000007\n",
- "Compilation time for circuit benchmarking/ibmq/ham7_104.qasm: 0.37469400000000164s\n",
- "Time spent compiling so far: 13.027636000000008\n",
- "Compilation time for circuit benchmarking/ibmq/decod24-enable_126.qasm: 0.37364900000000034s\n",
- "Time spent compiling so far: 13.401285000000009\n",
- "Compilation time for circuit benchmarking/ibmq/mod8-10_178.qasm: 0.43175500000000255s\n",
- "Time spent compiling so far: 13.833040000000011\n",
- "Compilation time for circuit benchmarking/ibmq/cnt3-5_180.qasm: 2.2587930000000043s\n",
- "Time spent compiling so far: 16.091833000000015\n",
- "Compilation time for circuit benchmarking/ibmq/ex3_229.qasm: 0.4237730000000006s\n",
- "Time spent compiling so far: 16.515606000000016\n",
- "Compilation time for circuit benchmarking/ibmq/4gt4-v0_73.qasm: 0.4361159999999984s\n",
- "Time spent compiling so far: 16.951722000000014\n",
- "Compilation time for circuit benchmarking/ibmq/mod8-10_177.qasm: 0.4928449999999991s\n",
- "Time spent compiling so far: 17.444567000000013\n",
- "Compilation time for circuit benchmarking/ibmq/C17_204.qasm: 0.6808130000000006s\n",
- "Time spent compiling so far: 18.125380000000014\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v2_31.qasm: 0.465876999999999s\n",
- "Time spent compiling so far: 18.591257000000013\n",
- "Compilation time for circuit benchmarking/ibmq/rd53_131.qasm: 0.6336799999999982s\n",
- "Time spent compiling so far: 19.22493700000001\n",
- "Compilation time for circuit benchmarking/ibmq/alu-v2_30.qasm: 0.5685000000000002s\n",
- "Time spent compiling so far: 19.79343700000001\n",
- "Compilation time for circuit benchmarking/ibmq/mod5adder_127.qasm: 0.5798150000000035s\n",
- "Time spent compiling so far: 20.373252000000015\n",
- "Compilation time for circuit benchmarking/ibmq/rd53_133.qasm: 0.7312700000000021s\n",
- "Time spent compiling so far: 21.104522000000017\n",
- "Compilation time for circuit benchmarking/ibmq/cm82a_208.qasm: 0.7972960000000029s\n",
- "Time spent compiling so far: 21.90181800000002\n",
- "Compilation time for circuit benchmarking/ibmq/majority_239.qasm: 0.7681219999999982s\n",
- "Time spent compiling so far: 22.669940000000018\n",
- "Compilation time for circuit benchmarking/ibmq/ex2_227.qasm: 0.7928779999999946s\n",
- "Time spent compiling so far: 23.462818000000013\n",
- "Compilation time for circuit benchmarking/ibmq/sf_276.qasm: 0.8383489999999938s\n",
- "Time spent compiling so far: 24.301167000000007\n",
- "Compilation time for circuit benchmarking/ibmq/sf_274.qasm: 0.8409819999999968s\n",
- "Time spent compiling so far: 25.142149000000003\n",
- "Compilation time for circuit benchmarking/ibmq/con1_216.qasm: 1.661089000000004s\n",
- "Time spent compiling so far: 26.803238000000007\n",
- "Compilation time for circuit benchmarking/ibmq/wim_266.qasm: 4.542246999999996s\n",
- "Time spent compiling so far: 31.345485000000004\n",
- "Compilation time for circuit benchmarking/ibmq/rd53_130.qasm: 1.323321s\n",
- "Time spent compiling so far: 32.668806000000004\n",
- "Compilation time for circuit benchmarking/ibmq/f2_232.qasm: 1.946520999999997s\n",
- "Time spent compiling so far: 34.615327\n",
- "Compilation time for circuit benchmarking/ibmq/cm152a_212.qasm: 3.305444999999999s\n",
- "Time spent compiling so far: 37.920772\n",
- "Compilation time for circuit benchmarking/ibmq/rd53_251.qasm: 1.9612180000000023s\n",
- "Time spent compiling so far: 39.88199\n",
- "Compilation time for circuit benchmarking/ibmq/hwb5_53.qasm: 1.4080020000000033s\n",
- "Time spent compiling so far: 41.289992000000005\n",
- "Compilation time for circuit benchmarking/ibmq/cm42a_207.qasm: 25.442768s\n",
- "Time spent compiling so far: 66.73276000000001\n",
- "Compilation time for circuit benchmarking/ibmq/pm1_249.qasm: 23.057216999999994s\n",
- "Time spent compiling so far: 89.78997700000001\n",
- "Compilation time for circuit benchmarking/ibmq/dc1_220.qasm: 11.941876999999991s\n",
- "Time spent compiling so far: 101.731854\n",
- "Compilation time for circuit benchmarking/ibmq/squar5_261.qasm: 8.14356699999999s\n",
- "Time spent compiling so far: 109.87542099999999\n",
- "Compilation time for circuit benchmarking/ibmq/z4_268.qasm: 11.094992999999988s\n",
- "Time spent compiling so far: 120.97041399999998\n",
- "Compilation time for circuit benchmarking/ibmq/sqrt8_260.qasm: 12.300466s\n",
- "Time spent compiling so far: 133.27087999999998\n",
- "Compilation time for circuit benchmarking/ibmq/radd_250.qasm: 26.233103999999997s\n",
- "Time spent compiling so far: 159.50398399999997\n",
- "Compilation time for circuit benchmarking/ibmq/adr4_197.qasm: 46.54046200000002s\n",
- "Time spent compiling so far: 206.044446\n",
- "Compilation time for circuit benchmarking/ibmq/sym6_145.qasm: 5.235140999999942s\n",
- "Time spent compiling so far: 211.27958699999994\n",
- "Compilation time for circuit benchmarking/ibmq/misex1_241.qasm: 57.77592099999998s\n",
- "Time spent compiling so far: 269.0555079999999\n",
- "Compilation time for circuit benchmarking/ibmq/rd73_252.qasm: 14.12543599999998s\n",
- "Time spent compiling so far: 283.1809439999999\n",
- "Compilation time for circuit benchmarking/ibmq/cycle10_2_110.qasm: 28.728320000000053s\n",
- "Time spent compiling so far: 311.90926399999995\n",
- "Compilation time for circuit benchmarking/ibmq/hwb6_56.qasm: 9.68947600000007s\n",
- "Time spent compiling so far: 321.59874\n",
- "Compilation time for circuit benchmarking/ibmq/square_root_7.qasm: 64.69754600000005s\n",
- "Time spent compiling so far: 386.29628600000007\n",
- "Compilation time for circuit benchmarking/ibmq/ham15_107.qasm: 588.933307s\n",
- "Time spent compiling so far: 975.229593\n",
- "Compilation time for circuit benchmarking/ibmq/dc2_222.qasm: 61.593504000000166s\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Time spent compiling so far: 1036.8230970000002\n",
- "Compilation time for circuit benchmarking/ibmq/sqn_258.qasm: 28.884308000000146s\n",
- "Time spent compiling so far: 1065.7074050000003\n",
- "Compilation time for circuit benchmarking/ibmq/inc_237.qasm: 106.20392800000013s\n",
- "Time spent compiling so far: 1171.9113330000005\n",
- "Compilation time for circuit benchmarking/ibmq/cm85a_209.qasm: 89.76953500000013s\n",
- "Time spent compiling so far: 1261.6808680000006\n",
- "Compilation time for circuit benchmarking/ibmq/rd84_253.qasm: 66.61587499999996s\n",
- "Time spent compiling so far: 1328.2967430000006\n",
- "Compilation time for circuit benchmarking/ibmq/co14_215.qasm: 128.39122300000008s\n",
- "Time spent compiling so far: 1456.6879660000006\n",
- "Compilation time for circuit benchmarking/ibmq/root_255.qasm: 123.82956399999989s\n",
- "Time spent compiling so far: 1580.5175300000005\n",
- "Compilation time for circuit benchmarking/ibmq/mlp4_245.qasm: 242.6016039999995s\n",
- "Time spent compiling so far: 1823.119134\n",
- "Compilation time for circuit benchmarking/ibmq/urf2_277.qasm: 33.63925399999971s\n",
- "Time spent compiling so far: 1856.7583879999997\n",
- "Compilation time for circuit benchmarking/ibmq/sym9_148.qasm: 64.92562999999973s\n",
- "Time spent compiling so far: 1921.6840179999995\n",
- "Compilation time for circuit benchmarking/ibmq/life_238.qasm: 73.87741500000038s\n",
- "Time spent compiling so far: 1995.5614329999999\n",
- "Compilation time for circuit benchmarking/ibmq/hwb7_59.qasm: 40.89143699999977s\n",
- "Time spent compiling so far: 2036.4528699999996\n",
- "Compilation time for circuit benchmarking/ibmq/max46_240.qasm: 69.40681099999983s\n",
- "Time spent compiling so far: 2105.8596809999995\n",
- "Compilation time for circuit benchmarking/ibmq/clip_206.qasm: 284.1277840000007s\n",
- "Time spent compiling so far: 2389.987465\n",
- "Compilation time for circuit benchmarking/ibmq/9symml_195.qasm: 112.6734349999997s\n",
- "Time spent compiling so far: 2502.6609\n",
- "Compilation time for circuit benchmarking/ibmq/sym9_193.qasm: 112.30117599999949s\n",
- "Time spent compiling so far: 2614.9620759999993\n",
- "Compilation time for circuit benchmarking/ibmq/sao2_257.qasm: 270.47973500000035s\n",
- "Time spent compiling so far: 2885.4418109999997\n",
- "Compilation time for circuit benchmarking/ibmq/dist_223.qasm: 250.32260099999985s\n",
- "Time spent compiling so far: 3135.7644119999995\n",
- "Compilation time for circuit benchmarking/ibmq/urf5_280.qasm: 110.69195600000057s\n",
- "Time spent compiling so far: 3246.456368\n",
- "Compilation time for circuit benchmarking/ibmq/urf1_278.qasm: 124.18942200000038s\n",
- "Time spent compiling so far: 3370.6457900000005\n",
- "Compilation time for circuit benchmarking/ibmq/sym10_262.qasm: 302.3636999999999s\n",
- "Time spent compiling so far: 3673.0094900000004\n",
- "Compilation time for circuit benchmarking/ibmq/hwb8_113.qasm: 154.02841999999964s\n",
- "Time spent compiling so far: 3827.03791\n",
- "Compilation time for circuit benchmarking/ibmq/urf2_152.qasm: 151.88721200000055s\n",
- "Time spent compiling so far: 3978.9251220000006\n",
- "Compilation time for circuit benchmarking/ibmq/urf3_279.qasm: 370.62564999999995s\n",
- "Time spent compiling so far: 4349.5507720000005\n",
- "Compilation time for circuit benchmarking/ibmq/plus63mod4096_163.qasm: 830.132912s\n",
- "Time spent compiling so far: 5179.6836840000005\n",
- "Compilation time for circuit benchmarking/ibmq/urf5_158.qasm: 447.08807799999977s\n",
- "Time spent compiling so far: 5626.771762\n",
- "Compilation time for circuit benchmarking/ibmq/urf6_160.qasm: 1869.587466s\n",
- "Time spent compiling so far: 7496.359228\n",
- "Compilation time for circuit benchmarking/ibmq/urf1_149.qasm: 498.8691209999997s\n",
- "Time spent compiling so far: 7995.228349\n",
- "Compilation time for circuit benchmarking/ibmq/plus63mod8192_164.qasm: 1486.8815559999985s\n",
- "Time spent compiling so far: 9482.109904999998\n",
- "Compilation time for circuit benchmarking/ibmq/hwb9_119.qasm: 618.8746800000008s\n",
- "Time spent compiling so far: 10100.984584999998\n",
- "Compilation time for circuit benchmarking/ibmq/urf3_155.qasm: 1497.5685780000003s\n",
- "Time spent compiling so far: 11598.553162999999\n",
- "Compilation time for circuit benchmarking/ibmq/ground_state_estimation_10.qasm: 142.17033999999876s\n",
- "Time spent compiling so far: 11740.723502999997\n",
- "Compilation time for circuit benchmarking/ibmq/urf4_187.qasm: 1895.1665990000001s\n",
- "Time spent compiling so far: 13635.890101999998\n",
- " Size out Depth out CX count out \\\n",
- "xor5_254.qasm 25 14 8 \n",
- "graycode6_47.qasm 13 9 5 \n",
- "ex1_226.qasm 25 14 8 \n",
- "4gt11_84.qasm 50 32 18 \n",
- "4mod5-v0_20.qasm 50 30 19 \n",
- "ex-1_166.qasm 52 34 18 \n",
- "4mod5-v1_22.qasm 54 32 20 \n",
- "mod5d1_63.qasm 63 39 25 \n",
- "ham3_102.qasm 53 36 19 \n",
- "4gt11_83.qasm 76 47 29 \n",
- "4gt11_82.qasm 95 57 36 \n",
- "rd32-v0_66.qasm 75 51 27 \n",
- "alu-v0_27.qasm 104 61 38 \n",
- "4mod5-v1_24.qasm 89 57 37 \n",
- "4mod5-v0_19.qasm 108 71 37 \n",
- "mod5mils_65.qasm 92 65 34 \n",
- "rd32-v1_68.qasm 75 51 27 \n",
- "alu-v1_28.qasm 108 67 39 \n",
- "alu-v2_33.qasm 95 58 35 \n",
- "alu-v4_37.qasm 100 57 39 \n",
- "alu-v3_35.qasm 100 57 39 \n",
- "3_17_13.qasm 86 59 35 \n",
- "alu-v1_29.qasm 103 61 38 \n",
- "miller_11.qasm 138 90 50 \n",
- "alu-v3_34.qasm 136 90 54 \n",
- "decod24-v2_43.qasm 131 85 49 \n",
- "decod24-v0_38.qasm 127 82 48 \n",
- "mod5d2_64.qasm 145 93 58 \n",
- "4gt13_92.qasm 169 109 63 \n",
- "4gt13-v1_93.qasm 174 115 66 \n",
- "4mod5-v0_18.qasm 180 119 70 \n",
- "decod24-bdd_294.qasm 179 118 69 \n",
- "one-two-three-v2_100.qasm 175 112 68 \n",
- "one-two-three-v3_101.qasm 197 132 72 \n",
- "4mod5-v1_23.qasm 187 123 74 \n",
- "4mod5-bdd_287.qasm 193 124 70 \n",
- "rd32_270.qasm 219 145 84 \n",
- "4gt5_75.qasm 220 140 83 \n",
- "alu-bdd_288.qasm 221 144 83 \n",
- "alu-v0_26.qasm 226 143 83 \n",
- "decod24-v1_41.qasm 216 140 83 \n",
- "rd53_138.qasm 365 185 138 \n",
- "4gt5_76.qasm 254 173 97 \n",
- "4gt13_91.qasm 281 190 104 \n",
- "cnt3-5_179.qasm 557 242 211 \n",
- "qft_10.qasm 0 0 0 \n",
- "4gt13_90.qasm 302 203 111 \n",
- "alu-v4_36.qasm 290 187 109 \n",
- "mini_alu_305.qasm 476 220 183 \n",
- "ising_model_10.qasm 210 41 90 \n",
- "ising_model_16.qasm 372 41 150 \n",
- "ising_model_13.qasm 307 41 120 \n",
- "4gt5_77.qasm 329 210 124 \n",
- "sys6-v0_111.qasm 618 286 236 \n",
- "one-two-three-v1_99.qasm 360 239 134 \n",
- "one-two-three-v0_98.qasm 367 248 140 \n",
- "decod24-v3_45.qasm 374 244 139 \n",
- "4gt10-v1_81.qasm 374 250 144 \n",
- "aj-e11_165.qasm 394 269 150 \n",
- "4mod7-v0_94.qasm 434 290 162 \n",
- "alu-v2_32.qasm 427 279 162 \n",
- "rd73_140.qasm 638 296 245 \n",
- "4mod7-v1_96.qasm 391 263 151 \n",
- "4gt4-v0_80.qasm 450 297 172 \n",
- "mod10_176.qasm 452 291 174 \n",
- "0410184_169.qasm 738 349 289 \n",
- "qft_16.qasm 0 0 0 \n",
- "4gt12-v0_88.qasm 553 336 212 \n",
- "rd84_142.qasm 1081 464 415 \n",
- "rd53_311.qasm 870 457 339 \n",
- "4_49_16.qasm 607 400 226 \n",
- "sym9_146.qasm 936 446 358 \n",
- "4gt12-v1_89.qasm 568 362 217 \n",
- "4gt12-v0_87.qasm 621 392 235 \n",
- "4gt4-v0_79.qasm 572 374 220 \n",
- "hwb4_49.qasm 605 395 228 \n",
- "sym6_316.qasm 859 460 334 \n",
- "4gt12-v0_86.qasm 636 407 242 \n",
- "4gt4-v0_72.qasm 657 400 254 \n",
- "4gt4-v0_78.qasm 594 389 227 \n",
- "mod10_171.qasm 626 413 240 \n",
- "4gt4-v1_74.qasm 690 455 272 \n",
- "rd53_135.qasm 846 522 324 \n",
- "mini-alu_167.qasm 740 487 285 \n",
- "one-two-three-v0_97.qasm 763 522 294 \n",
- "ham7_104.qasm 884 546 343 \n",
- "decod24-enable_126.qasm 922 593 353 \n",
- "mod8-10_178.qasm 1013 676 386 \n",
- "cnt3-5_180.qasm 1393 736 539 \n",
- "ex3_229.qasm 1048 667 406 \n",
- "4gt4-v0_73.qasm 1074 720 414 \n",
- "mod8-10_177.qasm 1140 733 445 \n",
- "C17_204.qasm 1377 876 536 \n",
- "alu-v2_31.qasm 1159 751 441 \n",
- "rd53_131.qasm 1250 763 480 \n",
- "alu-v2_30.qasm 1402 906 546 \n",
- "mod5adder_127.qasm 1538 957 581 \n",
- "rd53_133.qasm 1592 979 612 \n",
- "cm82a_208.qasm 1757 1040 681 \n",
- "majority_239.qasm 1653 1026 633 \n",
- "ex2_227.qasm 1725 1086 659 \n",
- "sf_276.qasm 1975 1308 768 \n",
- "sf_274.qasm 1993 1323 765 \n",
- "con1_216.qasm 2857 1794 1121 \n",
- "wim_266.qasm 2819 1678 1099 \n",
- "rd53_130.qasm 2815 1753 1096 \n",
- "f2_232.qasm 3427 2266 1325 \n",
- "cm152a_212.qasm 3453 2186 1347 \n",
- "rd53_251.qasm 3758 2368 1429 \n",
- "hwb5_53.qasm 3713 2401 1432 \n",
- "cm42a_207.qasm 5181 3014 2007 \n",
- "pm1_249.qasm 5181 3014 2007 \n",
- "dc1_220.qasm 5917 3684 2315 \n",
- "squar5_261.qasm 6377 3854 2492 \n",
- "z4_268.qasm 9164 5471 3563 \n",
- "sqrt8_260.qasm 9106 5562 3542 \n",
- "radd_250.qasm 9462 5688 3661 \n",
- "adr4_197.qasm 10357 6055 4030 \n",
- "sym6_145.qasm 10756 6805 4164 \n",
- "misex1_241.qasm 14534 8925 5673 \n",
- "rd73_252.qasm 15976 9680 6181 \n",
- "cycle10_2_110.qasm 18487 11440 7252 \n",
- "hwb6_56.qasm 18550 11834 7135 \n",
- "square_root_7.qasm 22470 13158 8947 \n",
- "ham15_107.qasm 25862 15757 9989 \n",
- "dc2_222.qasm 29882 18173 11657 \n",
- "sqn_258.qasm 30036 18448 11709 \n",
- "inc_237.qasm 32472 19886 12766 \n",
- "cm85a_209.qasm 35446 22104 13930 \n",
- "rd84_253.qasm 40724 24005 15882 \n",
- "co14_215.qasm 56374 30524 21896 \n",
- "root_255.qasm 52615 30655 20482 \n",
- "mlp4_245.qasm 59832 36288 23318 \n",
- "urf2_277.qasm 66948 39434 26327 \n",
- "sym9_148.qasm 62586 39228 24363 \n",
- "life_238.qasm 68359 41635 26538 \n",
- "hwb7_59.qasm 69520 43352 27136 \n",
- "max46_240.qasm 79067 46649 30769 \n",
- "clip_206.qasm 104519 61937 40950 \n",
- "9symml_195.qasm 104299 63616 40686 \n",
- "sym9_193.qasm 104299 63616 40686 \n",
- "sao2_257.qasm 121374 69339 47336 \n",
- "dist_223.qasm 118143 69156 46178 \n",
- "urf5_280.qasm 154799 92851 60653 \n",
- "urf1_278.qasm 176029 104865 69267 \n",
- "sym10_262.qasm 200584 121977 78116 \n",
- "hwb8_113.qasm 200371 124411 77790 \n",
- "urf2_152.qasm 225731 144286 87475 \n",
- "urf3_279.qasm 418175 246943 164373 \n",
- "plus63mod4096_163.qasm 397164 243811 155060 \n",
- "urf5_158.qasm 456435 286537 177850 \n",
- "urf6_160.qasm 537153 314319 210332 \n",
- "urf1_149.qasm 518233 318657 201568 \n",
- "plus63mod8192_164.qasm 584830 359000 228069 \n",
- "hwb9_119.qasm 616748 382737 239876 \n",
- "urf3_155.qasm 1219715 751497 475140 \n",
- "ground_state_estimation_10.qasm 15 7 3 \n",
- "urf4_187.qasm 1513171 893289 586910 \n",
- "\n",
- " CX depth out Runtime \n",
- "xor5_254.qasm 8 0.009510 \n",
- "graycode6_47.qasm 5 0.005398 \n",
- "ex1_226.qasm 8 0.008242 \n",
- "4gt11_84.qasm 18 0.015389 \n",
- "4mod5-v0_20.qasm 18 0.020801 \n",
- "ex-1_166.qasm 18 0.017432 \n",
- "4mod5-v1_22.qasm 19 0.016419 \n",
- "mod5d1_63.qasm 22 0.024310 \n",
- "ham3_102.qasm 19 0.016796 \n",
- "4gt11_83.qasm 27 0.019891 \n",
- "4gt11_82.qasm 32 0.024682 \n",
- "rd32-v0_66.qasm 27 0.022649 \n",
- "alu-v0_27.qasm 33 0.031051 \n",
- "4mod5-v1_24.qasm 35 0.035731 \n",
- "4mod5-v0_19.qasm 37 0.031680 \n",
- "mod5mils_65.qasm 34 0.033256 \n",
- "rd32-v1_68.qasm 27 0.031518 \n",
- "alu-v1_28.qasm 35 0.044897 \n",
- "alu-v2_33.qasm 31 0.035871 \n",
- "alu-v4_37.qasm 34 0.032545 \n",
- "alu-v3_35.qasm 34 0.031094 \n",
- "3_17_13.qasm 35 0.038106 \n",
- "alu-v1_29.qasm 33 0.033971 \n",
- "miller_11.qasm 50 0.053864 \n",
- "alu-v3_34.qasm 53 0.063358 \n",
- "decod24-v2_43.qasm 49 0.055582 \n",
- "decod24-v0_38.qasm 48 0.053290 \n",
- "mod5d2_64.qasm 56 0.043774 \n",
- "4gt13_92.qasm 58 0.073271 \n",
- "4gt13-v1_93.qasm 62 0.068343 \n",
- "4mod5-v0_18.qasm 70 0.054039 \n",
- "decod24-bdd_294.qasm 68 0.070724 \n",
- "one-two-three-v2_100.qasm 64 0.074335 \n",
- "one-two-three-v3_101.qasm 70 0.065476 \n",
- "4mod5-v1_23.qasm 72 0.053468 \n",
- "4mod5-bdd_287.qasm 69 0.078521 \n",
- "rd32_270.qasm 82 0.061950 \n",
- "4gt5_75.qasm 78 0.081763 \n",
- "alu-bdd_288.qasm 80 0.064100 \n",
- "alu-v0_26.qasm 78 0.081182 \n",
- "decod24-v1_41.qasm 79 0.081245 \n",
- "rd53_138.qasm 103 0.133514 \n",
- "4gt5_76.qasm 93 0.068765 \n",
- "4gt13_91.qasm 101 0.082594 \n",
- "cnt3-5_179.qasm 134 0.281470 \n",
- "qft_10.qasm 0 0.000000 \n",
- "4gt13_90.qasm 108 0.092554 \n",
- "alu-v4_36.qasm 104 0.117040 \n",
- "mini_alu_305.qasm 125 0.212324 \n",
- "ising_model_10.qasm 20 0.261262 \n",
- "ising_model_16.qasm 20 0.376977 \n",
- "ising_model_13.qasm 20 0.334911 \n",
- "4gt5_77.qasm 117 0.129335 \n",
- "sys6-v0_111.qasm 159 0.304467 \n",
- "one-two-three-v1_99.qasm 131 0.153367 \n",
- "one-two-three-v0_98.qasm 135 0.126284 \n",
- "decod24-v3_45.qasm 133 0.149087 \n",
- "4gt10-v1_81.qasm 140 0.140766 \n",
- "aj-e11_165.qasm 145 0.149700 \n",
- "4mod7-v0_94.qasm 158 0.163860 \n",
- "alu-v2_32.qasm 155 0.159586 \n",
- "rd73_140.qasm 166 0.263072 \n",
- "4mod7-v1_96.qasm 145 0.159860 \n",
- "4gt4-v0_80.qasm 163 0.182525 \n",
- "mod10_176.qasm 163 0.179933 \n",
- "0410184_169.qasm 196 0.624469 \n",
- "qft_16.qasm 0 0.000000 \n",
- "4gt12-v0_88.qasm 188 0.208980 \n",
- "rd84_142.qasm 258 0.740196 \n",
- "rd53_311.qasm 254 0.502639 \n",
- "4_49_16.qasm 218 0.228873 \n",
- "sym9_146.qasm 252 0.566968 \n",
- "4gt12-v1_89.qasm 203 0.246257 \n",
- "4gt12-v0_87.qasm 215 0.218950 \n",
- "4gt4-v0_79.qasm 208 0.195003 \n",
- "hwb4_49.qasm 217 0.236325 \n",
- "sym6_316.qasm 257 0.657216 \n",
- "4gt12-v0_86.qasm 223 0.240528 \n",
- "4gt4-v0_72.qasm 227 0.283360 \n",
- "4gt4-v0_78.qasm 216 0.200468 \n",
- "mod10_171.qasm 228 0.237878 \n",
- "4gt4-v1_74.qasm 261 0.289275 \n",
- "rd53_135.qasm 291 0.379021 \n",
- "mini-alu_167.qasm 267 0.289529 \n",
- "one-two-three-v0_97.qasm 289 0.324230 \n",
- "ham7_104.qasm 312 0.374694 \n",
- "decod24-enable_126.qasm 333 0.373649 \n",
- "mod8-10_178.qasm 367 0.431755 \n",
- "cnt3-5_180.qasm 410 2.258793 \n",
- "ex3_229.qasm 372 0.423773 \n",
- "4gt4-v0_73.qasm 394 0.436116 \n",
- "mod8-10_177.qasm 416 0.492845 \n",
- "C17_204.qasm 486 0.680813 \n",
- "alu-v2_31.qasm 415 0.465877 \n",
- "rd53_131.qasm 432 0.633680 \n",
- "alu-v2_30.qasm 504 0.568500 \n",
- "mod5adder_127.qasm 536 0.579815 \n",
- "rd53_133.qasm 553 0.731270 \n",
- "cm82a_208.qasm 579 0.797296 \n",
- "majority_239.qasm 565 0.768122 \n",
- "ex2_227.qasm 604 0.792878 \n",
- "sf_276.qasm 734 0.838349 \n",
- "sf_274.qasm 732 0.840982 \n",
- "con1_216.qasm 993 1.661089 \n",
- "wim_266.qasm 934 4.542247 \n",
- "rd53_130.qasm 980 1.323321 \n",
- "f2_232.qasm 1237 1.946521 \n",
- "cm152a_212.qasm 1225 3.305445 \n",
- "rd53_251.qasm 1290 1.961218 \n",
- "hwb5_53.qasm 1352 1.408002 \n",
- "cm42a_207.qasm 1694 25.442768 \n",
- "pm1_249.qasm 1694 23.057217 \n",
- "dc1_220.qasm 2045 11.941877 \n",
- "squar5_261.qasm 2161 8.143567 \n",
- "z4_268.qasm 3030 11.094993 \n",
- "sqrt8_260.qasm 3093 12.300466 \n",
- "radd_250.qasm 3167 26.233104 \n",
- "adr4_197.qasm 3382 46.540462 \n",
- "sym6_145.qasm 3819 5.235141 \n",
- "misex1_241.qasm 4974 57.775921 \n",
- "rd73_252.qasm 5373 14.125436 \n",
- "cycle10_2_110.qasm 6397 28.728320 \n",
- "hwb6_56.qasm 6513 9.689476 \n",
- "square_root_7.qasm 7446 64.697546 \n",
- "ham15_107.qasm 8728 588.933307 \n",
- "dc2_222.qasm 10098 61.593504 \n",
- "sqn_258.qasm 10273 28.884308 \n",
- "inc_237.qasm 11140 106.203928 \n",
- "cm85a_209.qasm 12366 89.769535 \n",
- "rd84_253.qasm 13405 66.615875 \n",
- "co14_215.qasm 17008 128.391223 \n",
- "root_255.qasm 17076 123.829564 \n",
- "mlp4_245.qasm 20258 242.601604 \n",
- "urf2_277.qasm 22099 33.639254 \n",
- "sym9_148.qasm 21805 64.925630 \n",
- "life_238.qasm 23121 73.877415 \n",
- "hwb7_59.qasm 24091 40.891437 \n",
- "max46_240.qasm 26016 69.406811 \n",
- "clip_206.qasm 34520 284.127784 \n",
- "9symml_195.qasm 35437 112.673435 \n",
- "sym9_193.qasm 35437 112.301176 \n",
- "sao2_257.qasm 38642 270.479735 \n",
- "dist_223.qasm 38660 250.322601 \n",
- "urf5_280.qasm 51913 110.691956 \n",
- "urf1_278.qasm 58728 124.189422 \n",
- "sym10_262.qasm 68141 302.363700 \n",
- "hwb8_113.qasm 69065 154.028420 \n",
- "urf2_152.qasm 79903 151.887212 \n",
- "urf3_279.qasm 138114 370.625650 \n",
- "plus63mod4096_163.qasm 136210 830.132912 \n",
- "urf5_158.qasm 159416 447.088078 \n",
- "urf6_160.qasm 175227 1869.587466 \n",
- "urf1_149.qasm 179176 498.869121 \n",
- "plus63mod8192_164.qasm 200254 1486.881556 \n",
- "hwb9_119.qasm 211857 618.874680 \n",
- "urf3_155.qasm 418559 1497.568578 \n",
- "ground_state_estimation_10.qasm 3 142.170340 \n",
- "urf4_187.qasm 494739 1895.166599 \n"
- ]
- }
- ],
- "source": [
- "test_table = pandas.read_csv(\"benchmarking/IBMQConfig.csv\",index_col=0)\n",
- "test_table = test_table.sort_values(by='Depth in')\n",
- "\n",
- "stat_table = pandas.DataFrame({})\n",
- "total_time = 0\n",
- "\n",
- "for i, (index, row) in enumerate(test_table.iterrows()):\n",
- " filename = row['Filename']\n",
- " new_stats = getStats(filename, directed_arc)\n",
- " total_time += new_stats[4] ###\n",
- " print(\"Time spent compiling so far: \" + str(total_time))\n",
- " new_table_row = pandas.DataFrame.from_dict({index : new_stats}, \n",
- " orient='index', columns=['Size out', 'Depth out', 'CX count out', 'CX depth out','Runtime'])\n",
- " stat_table = stat_table.append(new_table_row)\n",
- "\n",
- "#stat_table.to_csv(\"BenchmarkTket.csv\") ###Note: uncomment this line to print table to csv.\n",
- "with pandas.option_context('display.max_rows', None):\n",
- " print(stat_table)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python [conda env:tket] *",
- "language": "python",
- "name": "conda-env-tket-py"
- },
- "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.6.8"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/examples/ucc_vqe.ipynb b/examples/ucc_vqe.ipynb
index 99250907..7ed8608d 100644
--- a/examples/ucc_vqe.ipynb
+++ b/examples/ucc_vqe.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# VQE for Unitary Coupled Cluster using tket"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this tutorial, we will focus on:
\n", "- building parameterised ans\u00e4tze for variational algorithms;
\n", "- compilation tools for UCC-style ans\u00e4tze."]}, {"cell_type": "markdown", "metadata": {}, "source": ["This example assumes the reader is familiar with the Variational Quantum Eigensolver and its application to electronic structure problems through the Unitary Coupled Cluster approach.
\n", "
\n", "To run this example, you will need `pytket` and `pytket-qiskit`, as well as `openfermion`, `scipy`, and `sympy`.
\n", "
\n", "We will start with a basic implementation and then gradually modify it to make it faster, more general, and less noisy. The final solution is given in full at the bottom of the notebook.
\n", "
\n", "Suppose we have some electronic configuration problem, expressed via a physical Hamiltonian. (The Hamiltonian and excitations in this example were obtained using `qiskit-aqua` version 0.5.2 and `pyscf` for H2, bond length 0.75A, sto3g basis, Jordan-Wigner encoding, with no qubit reduction or orbital freezing.). We express it succinctly using the openfermion library:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = (\n", " -0.8153001706270075 * of.QubitOperator(\"\")\n", " + 0.16988452027940318 * of.QubitOperator(\"Z0\")\n", " + -0.21886306781219608 * of.QubitOperator(\"Z1\")\n", " + 0.16988452027940323 * of.QubitOperator(\"Z2\")\n", " + -0.2188630678121961 * of.QubitOperator(\"Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z0 Z1\")\n", " + 0.16821198673715723 * of.QubitOperator(\"Z0 Z2\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z0 Z3\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z1 Z2\")\n", " + 0.1739537877649417 * of.QubitOperator(\"Z1 Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z2 Z3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 Y2 Y3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 Y2 Y3\")\n", ")\n", "nuclear_repulsion_energy = 0.70556961456"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We would like to define our ansatz for arbitrary parameter values. For simplicity, let's start with a Hardware Efficient Ansatz."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Hardware efficient ansatz:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def hea(params):\n", " ansatz = Circuit(4)\n", " for i in range(4):\n", " ansatz.Ry(params[i], i)\n", " for i in range(3):\n", " ansatz.CX(i, i + 1)\n", " for i in range(4):\n", " ansatz.Ry(params[4 + i], i)\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can use this to build the objective function for our optimisation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import AerBackend\n", "from pytket.utils.expectations import expectation_from_counts"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Naive objective function:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " energy = 0\n", " for term, coeff in hamiltonian.terms.items():\n", " if not term:\n", " energy += coeff\n", " continue\n", " circ = hea(params)\n", " circ.add_c_register(\"c\", len(term))\n", " for i, (q, pauli) in enumerate(term):\n", " if pauli == \"X\":\n", " circ.H(q)\n", " elif pauli == \"Y\":\n", " circ.V(q)\n", " circ.Measure(q, i)\n", " compiled_circ = backend.get_compiled_circuit(circ)\n", " counts = backend.run_circuit(compiled_circ, n_shots=4000).get_counts()\n", " energy += coeff * expectation_from_counts(counts)\n", " return energy + nuclear_repulsion_energy"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This objective function is then run through a classical optimiser to find the set of parameter values that minimise the energy of the system. For the sake of example, we will just run this with a single parameter value."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["arg_values = [\n", " -7.31158201e-02,\n", " -1.64514836e-04,\n", " 1.12585591e-03,\n", " -2.58367544e-03,\n", " 1.00006068e00,\n", " -1.19551357e-03,\n", " 9.99963988e-01,\n", " 2.53283285e-03,\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["energy = objective(arg_values)\n", "print(energy)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The HEA is designed to cram as many orthogonal degrees of freedom into a small circuit as possible to be able to explore a large region of the Hilbert space whilst the circuits themselves can be run with minimal noise. These ans\u00e4tze give virtually-optimal circuits by design, but suffer from an excessive number of variational parameters making convergence slow, barren plateaus where the classical optimiser fails to make progress, and spanning a space where most states lack a physical interpretation. These drawbacks can necessitate adding penalties and may mean that the ansatz cannot actually express the true ground state.
\n", "
\n", "The UCC ansatz, on the other hand, is derived from the electronic configuration. It sacrifices efficiency of the circuit for the guarantee of physical states and the variational parameters all having some meaningful effect, which helps the classical optimisation to converge.
\n", "
\n", "This starts by defining the terms of our single and double excitations. These would usually be generated using the orbital configurations, so we will just use a hard-coded example here for the purposes of demonstration."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Qubit\n", "from pytket.pauli import Pauli, QubitPauliString"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["q = [Qubit(i) for i in range(4)]\n", "xyii = QubitPauliString([q[0], q[1]], [Pauli.X, Pauli.Y])\n", "yxii = QubitPauliString([q[0], q[1]], [Pauli.Y, Pauli.X])\n", "iixy = QubitPauliString([q[2], q[3]], [Pauli.X, Pauli.Y])\n", "iiyx = QubitPauliString([q[2], q[3]], [Pauli.Y, Pauli.X])\n", "xxxy = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.X, Pauli.Y])\n", "xxyx = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.Y, Pauli.X])\n", "xyxx = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.X, Pauli.X])\n", "yxxx = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.X, Pauli.X])\n", "yyyx = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.Y, Pauli.X])\n", "yyxy = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.X, Pauli.Y])\n", "yxyy = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.Y, Pauli.Y])\n", "xyyy = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.Y, Pauli.Y])"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["singles_a = {xyii: 1.0, yxii: -1.0}\n", "singles_b = {iixy: 1.0, iiyx: -1.0}\n", "doubles = {\n", " xxxy: 0.25,\n", " xxyx: -0.25,\n", " xyxx: 0.25,\n", " yxxx: -0.25,\n", " yyyx: -0.25,\n", " yyxy: 0.25,\n", " yxyy: -0.25,\n", " xyyy: 0.25,\n", "}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Building the ansatz circuit itself is often done naively by defining the map from each term down to basic gates and then applying it to each term."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def add_operator_term(circuit: Circuit, term: QubitPauliString, angle: float):\n", " qubits = []\n", " for q, p in term.map.items():\n", " if p != Pauli.I:\n", " qubits.append(q)\n", " if p == Pauli.X:\n", " circuit.H(q)\n", " elif p == Pauli.Y:\n", " circuit.V(q)\n", " for i in range(len(qubits) - 1):\n", " circuit.CX(i, i + 1)\n", " circuit.Rz(angle, len(qubits) - 1)\n", " for i in reversed(range(len(qubits) - 1)):\n", " circuit.CX(i, i + 1)\n", " for q, p in term.map.items():\n", " if p == Pauli.X:\n", " circuit.H(q)\n", " elif p == Pauli.Y:\n", " circuit.Vdg(q)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Unitary Coupled Cluster Singles & Doubles ansatz:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def ucc(params):\n", " ansatz = Circuit(4)\n", " # Set initial reference state\n", " ansatz.X(1).X(3)\n", " # Evolve by excitations\n", " for term, coeff in singles_a.items():\n", " add_operator_term(ansatz, term, coeff * params[0])\n", " for term, coeff in singles_b.items():\n", " add_operator_term(ansatz, term, coeff * params[1])\n", " for term, coeff in doubles.items():\n", " add_operator_term(ansatz, term, coeff * params[2])\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is already quite verbose, but `pytket` has a neat shorthand construction for these operator terms using the `PauliExpBox` construction. We can then decompose these into basic gates using the `DecomposeBoxes` compiler pass."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import PauliExpBox\n", "from pytket.passes import DecomposeBoxes"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def add_excitation(circ, term_dict, param):\n", " for term, coeff in term_dict.items():\n", " qubits, paulis = zip(*term.map.items())\n", " pbox = PauliExpBox(paulis, coeff * param)\n", " circ.add_pauliexpbox(pbox, qubits)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["UCC ansatz with syntactic shortcuts:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def ucc(params):\n", " ansatz = Circuit(4)\n", " ansatz.X(1).X(3)\n", " add_excitation(ansatz, singles_a, params[0])\n", " add_excitation(ansatz, singles_b, params[1])\n", " add_excitation(ansatz, doubles, params[2])\n", " DecomposeBoxes().apply(ansatz)\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The objective function can also be simplified using a utility method for constructing the measurement circuits and processing for expectation value calculations. For that, we convert the Hamiltonian to a pytket QubitPauliOperator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Simplified objective function using utilities:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.expectations import get_operator_expectation_value"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc(params)\n", " return (\n", " get_operator_expectation_value(circ, hamiltonian_op, backend, n_shots=4000)\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["arg_values = [-3.79002933e-05, 2.42964799e-05, 4.63447157e-01]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["energy = objective(arg_values)\n", "print(energy)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is now the simplest form that this operation can take, but it isn't necessarily the most effective. When we decompose the ansatz circuit into basic gates, it is still very expensive. We can employ some of the circuit simplification passes available in `pytket` to reduce its size and improve fidelity in practice.
\n", "
\n", "A good example is to decompose each `PauliExpBox` into basic gates and then apply `FullPeepholeOptimise`, which defines a compilation strategy utilising all of the simplifications in `pytket` that act locally on small regions of a circuit. We can examine the effectiveness by looking at the number of two-qubit gates before and after simplification, which tends to be a good indicator of fidelity for near-term systems where these gates are often slow and inaccurate."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import OpType\n", "from pytket.passes import FullPeepholeOptimise"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_circuit = ucc(arg_values)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count before\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth before\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["FullPeepholeOptimise().apply(test_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count after FPO\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth after FPO\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These simplification techniques are very general and are almost always beneficial to apply to a circuit if you want to eliminate local redundancies. But UCC ans\u00e4tze have extra structure that we can exploit further. They are defined entirely out of exponentiated tensors of Pauli matrices, giving the regular structure described by the `PauliExpBox`es. Under many circumstances, it is more efficient to not synthesise these constructions individually, but simultaneously in groups. The `PauliSimp` pass finds the description of a given circuit as a sequence of `PauliExpBox`es and resynthesises them (by default, in groups of commuting terms). This can cause great change in the overall structure and shape of the circuit, enabling the identification and elimination of non-local redundancy."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import PauliSimp"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_circuit = ucc(arg_values)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count before\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth before\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["PauliSimp().apply(test_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count after PS\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth after PS\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["FullPeepholeOptimise().apply(test_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count after PS+FPO\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth after PS+FPO\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To include this into our routines, we can just add the simplification passes to the objective function. The `get_operator_expectation_value` utility handles compiling to meet the requirements of the backend, so we don't have to worry about that here."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function with circuit simplification:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc(params)\n", " PauliSimp().apply(circ)\n", " FullPeepholeOptimise().apply(circ)\n", " return (\n", " get_operator_expectation_value(circ, hamiltonian_op, backend, n_shots=4000)\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These circuit simplification techniques have tried to preserve the exact unitary of the circuit, but there are ways to change the unitary whilst preserving the correctness of the algorithm as a whole.
\n", "
\n", "For example, the excitation terms are generated by trotterisation of the excitation operator, and the order of the terms does not change the unitary in the limit of many trotter steps, so in this sense we are free to sequence the terms how we like and it is sensible to do this in a way that enables efficient synthesis of the circuit. Prioritising collecting terms into commuting sets is a very beneficial heuristic for this and can be performed using the `gen_term_sequence_circuit` method to group the terms together into collections of `PauliExpBox`es and the `GuidedPauliSimp` pass to utilise these sets for synthesis."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import GuidedPauliSimp\n", "from pytket.utils import gen_term_sequence_circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def ucc(params):\n", " singles_params = {qps: params[0] * coeff for qps, coeff in singles.items()}\n", " doubles_params = {qps: params[1] * coeff for qps, coeff in doubles.items()}\n", " excitation_op = QubitPauliOperator({**singles_params, **doubles_params})\n", " reference_circ = Circuit(4).X(1).X(3)\n", " ansatz = gen_term_sequence_circuit(excitation_op, reference_circ)\n", " GuidedPauliSimp().apply(ansatz)\n", " FullPeepholeOptimise().apply(ansatz)\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Adding these simplification routines doesn't come for free. Compiling and simplifying the circuit to achieve the best results possible can be a difficult task, which can take some time for the classical computer to perform.
\n", "
\n", "During a VQE run, we will call this objective function many times and run many measurement circuits within each, but the circuits that are run on the quantum computer are almost identical, having the same gate structure but with different gate parameters and measurements. We have already exploited this within the body of the objective function by simplifying the ansatz circuit before we call `get_operator_expectation_value`, so it is only done once per objective calculation rather than once per measurement circuit.
\n", "
\n", "We can go even further by simplifying it once outside of the objective function, and then instantiating the simplified ansatz with the parameter values needed. For this, we will construct the UCC ansatz circuit using symbolic (parametric) gates."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from sympy import symbols"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Symbolic UCC ansatz generation:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["syms = symbols(\"p0 p1 p2\")\n", "singles_a_syms = {qps: syms[0] * coeff for qps, coeff in singles_a.items()}\n", "singles_b_syms = {qps: syms[1] * coeff for qps, coeff in singles_b.items()}\n", "doubles_syms = {qps: syms[2] * coeff for qps, coeff in doubles.items()}\n", "excitation_op = QubitPauliOperator({**singles_a_syms, **singles_b_syms, **doubles_syms})\n", "ucc_ref = Circuit(4).X(1).X(3)\n", "ucc = gen_term_sequence_circuit(excitation_op, ucc_ref)\n", "GuidedPauliSimp().apply(ucc)\n", "FullPeepholeOptimise().apply(ucc)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function using the symbolic ansatz:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc.copy()\n", " sym_map = dict(zip(syms, params))\n", " circ.symbol_substitution(sym_map)\n", " return (\n", " get_operator_expectation_value(circ, hamiltonian_op, backend, n_shots=4000)\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We have now got some very good use of `pytket` for simplifying each individual circuit used in our experiment and for minimising the amount of time spent compiling, but there is still more we can do in terms of reducing the amount of work the quantum computer has to do. Currently, each (non-trivial) term in our measurement hamiltonian is measured by a different circuit within each expectation value calculation. Measurement reduction techniques exist for identifying when these observables commute and hence can be simultaneously measured, reducing the number of circuits required for the full expectation value calculation.
\n", "
\n", "This is built in to the `get_operator_expectation_value` method and can be applied by specifying a way to partition the measuremrnt terms. `PauliPartitionStrat.CommutingSets` can greatly reduce the number of measurement circuits by combining any number of terms that mutually commute. However, this involves potentially adding an arbitrary Clifford circuit to change the basis of the measurements which can be costly on NISQ devices, so `PauliPartitionStrat.NonConflictingSets` trades off some of the reduction in circuit number to guarantee that only single-qubit gates are introduced."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.partition import PauliPartitionStrat"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function using measurement reduction:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc.copy()\n", " sym_map = dict(zip(syms, params))\n", " circ.symbol_substitution(sym_map)\n", " return (\n", " get_operator_expectation_value(\n", " circ,\n", " operator,\n", " backend,\n", " n_shots=4000,\n", " partition_strat=PauliPartitionStrat.CommutingSets,\n", " )\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["At this point, we have completely transformed how our VQE objective function works, improving its resilience to noise, cutting the number of circuits run, and maintaining fast runtimes. In doing this, we have explored a number of the features `pytket` offers that are beneficial to VQE and the UCC method:
\n", "- high-level syntactic constructs for evolution operators;
\n", "- utility methods for easy expectation value calculations;
\n", "- both generic and domain-specific circuit simplification methods;
\n", "- symbolic circuit compilation;
\n", "- measurement reduction for expectation value calculations."]}, {"cell_type": "markdown", "metadata": {}, "source": ["For the sake of completeness, the following gives the full code for the final solution, including passing the objective function to a classical optimiser to find the ground state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of\n", "from scipy.optimize import minimize\n", "from sympy import symbols"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import AerBackend\n", "from pytket.circuit import Circuit, Qubit\n", "from pytket.partition import PauliPartitionStrat\n", "from pytket.passes import GuidedPauliSimp, FullPeepholeOptimise\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils import get_operator_expectation_value, gen_term_sequence_circuit\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Obtain electronic Hamiltonian:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = (\n", " -0.8153001706270075 * of.QubitOperator(\"\")\n", " + 0.16988452027940318 * of.QubitOperator(\"Z0\")\n", " + -0.21886306781219608 * of.QubitOperator(\"Z1\")\n", " + 0.16988452027940323 * of.QubitOperator(\"Z2\")\n", " + -0.2188630678121961 * of.QubitOperator(\"Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z0 Z1\")\n", " + 0.16821198673715723 * of.QubitOperator(\"Z0 Z2\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z0 Z3\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z1 Z2\")\n", " + 0.1739537877649417 * of.QubitOperator(\"Z1 Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z2 Z3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 Y2 Y3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 Y2 Y3\")\n", ")\n", "nuclear_repulsion_energy = 0.70556961456"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Obtain terms for single and double excitations:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["q = [Qubit(i) for i in range(4)]\n", "xyii = QubitPauliString([q[0], q[1]], [Pauli.X, Pauli.Y])\n", "yxii = QubitPauliString([q[0], q[1]], [Pauli.Y, Pauli.X])\n", "iixy = QubitPauliString([q[2], q[3]], [Pauli.X, Pauli.Y])\n", "iiyx = QubitPauliString([q[2], q[3]], [Pauli.Y, Pauli.X])\n", "xxxy = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.X, Pauli.Y])\n", "xxyx = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.Y, Pauli.X])\n", "xyxx = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.X, Pauli.X])\n", "yxxx = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.X, Pauli.X])\n", "yyyx = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.Y, Pauli.X])\n", "yyxy = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.X, Pauli.Y])\n", "yxyy = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.Y, Pauli.Y])\n", "xyyy = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.Y, Pauli.Y])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Symbolic UCC ansatz generation:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["syms = symbols(\"p0 p1 p2\")\n", "singles_syms = {xyii: syms[0], yxii: -syms[0], iixy: syms[1], iiyx: -syms[1]}\n", "doubles_syms = {\n", " xxxy: 0.25 * syms[2],\n", " xxyx: -0.25 * syms[2],\n", " xyxx: 0.25 * syms[2],\n", " yxxx: -0.25 * syms[2],\n", " yyyx: -0.25 * syms[2],\n", " yyxy: 0.25 * syms[2],\n", " yxyy: -0.25 * syms[2],\n", " xyyy: 0.25 * syms[2],\n", "}\n", "excitation_op = QubitPauliOperator({**singles_syms, **doubles_syms})\n", "ucc_ref = Circuit(4).X(0).X(2)\n", "ucc = gen_term_sequence_circuit(excitation_op, ucc_ref)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Circuit simplification:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["GuidedPauliSimp().apply(ucc)\n", "FullPeepholeOptimise().apply(ucc)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Connect to a simulator/device:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc.copy()\n", " sym_map = dict(zip(syms, params))\n", " circ.symbol_substitution(sym_map)\n", " return (\n", " get_operator_expectation_value(\n", " circ,\n", " hamiltonian_op,\n", " backend,\n", " n_shots=4000,\n", " partition_strat=PauliPartitionStrat.CommutingSets,\n", " )\n", " + nuclear_repulsion_energy\n", " ).real"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Optimise against the objective function:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["initial_params = [1e-4, 1e-4, 4e-1]\n", "# #result = minimize(objective, initial_params, method=\"Nelder-Mead\")\n", "# #print(\"Final parameter values\", result.x)\n", "# #print(\"Final energy value\", result.fun)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Exercises:
\n", "- Replace the `get_operator_expectation_value` call with its implementation and use this to pull the analysis for measurement reduction outside of the objective function, so our circuits can be fully determined and compiled once. This means that the `symbol_substitution` method will need to be applied to each measurement circuit instead of just the state preparation circuit.
\n", "- Use the `SpamCorrecter` class to add some mitigation of the measurement errors. Start by running the characterisation circuits first, before your main VQE loop, then apply the mitigation to each of the circuits run within the objective function.
\n", "- Change the `backend` by passing in a `Qiskit` `NoiseModel` to simulate a noisy device. Compare the accuracy of the objective function both with and without the circuit simplification. Try running a classical optimiser over the objective function and compare the convergence rates with different noise models. If you have access to a QPU, try changing the `backend` to connect to that and compare the results to the simulator."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# VQE with UCC ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["In this tutorial, we will focus on:
\n", "- building parameterised ans\u00e4tze for variational algorithms;
\n", "- compilation tools for UCC-style ans\u00e4tze."]}, {"cell_type": "markdown", "metadata": {}, "source": ["This example assumes the reader is familiar with the Variational Quantum Eigensolver and its application to electronic structure problems through the Unitary Coupled Cluster approach.
\n", "
\n", "To run this example, you will need `pytket` and `pytket-qiskit`, as well as `openfermion`, `scipy`, and `sympy`.
\n", "
\n", "We will start with a basic implementation and then gradually modify it to make it faster, more general, and less noisy. The final solution is given in full at the bottom of the notebook.
\n", "
\n", "Suppose we have some electronic configuration problem, expressed via a physical Hamiltonian. (The Hamiltonian and excitations in this example were obtained using `qiskit-aqua` version 0.5.2 and `pyscf` for H2, bond length 0.75A, sto3g basis, Jordan-Wigner encoding, with no qubit reduction or orbital freezing.). We express it succinctly using the openfermion library:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = (\n", " -0.8153001706270075 * of.QubitOperator(\"\")\n", " + 0.16988452027940318 * of.QubitOperator(\"Z0\")\n", " + -0.21886306781219608 * of.QubitOperator(\"Z1\")\n", " + 0.16988452027940323 * of.QubitOperator(\"Z2\")\n", " + -0.2188630678121961 * of.QubitOperator(\"Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z0 Z1\")\n", " + 0.16821198673715723 * of.QubitOperator(\"Z0 Z2\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z0 Z3\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z1 Z2\")\n", " + 0.1739537877649417 * of.QubitOperator(\"Z1 Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z2 Z3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 Y2 Y3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 Y2 Y3\")\n", ")\n", "nuclear_repulsion_energy = 0.70556961456"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We would like to define our ansatz for arbitrary parameter values. For simplicity, let's start with a Hardware Efficient Ansatz."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import Circuit"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Hardware efficient ansatz:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def hea(params):\n", " ansatz = Circuit(4)\n", " for i in range(4):\n", " ansatz.Ry(params[i], i)\n", " for i in range(3):\n", " ansatz.CX(i, i + 1)\n", " for i in range(4):\n", " ansatz.Ry(params[4 + i], i)\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We can use this to build the objective function for our optimisation."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import AerBackend\n", "from pytket.utils.expectations import expectation_from_counts"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Naive objective function:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " energy = 0\n", " for term, coeff in hamiltonian.terms.items():\n", " if not term:\n", " energy += coeff\n", " continue\n", " circ = hea(params)\n", " circ.add_c_register(\"c\", len(term))\n", " for i, (q, pauli) in enumerate(term):\n", " if pauli == \"X\":\n", " circ.H(q)\n", " elif pauli == \"Y\":\n", " circ.V(q)\n", " circ.Measure(q, i)\n", " compiled_circ = backend.get_compiled_circuit(circ)\n", " counts = backend.run_circuit(compiled_circ, n_shots=4000).get_counts()\n", " energy += coeff * expectation_from_counts(counts)\n", " return energy + nuclear_repulsion_energy"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This objective function is then run through a classical optimiser to find the set of parameter values that minimise the energy of the system. For the sake of example, we will just run this with a single parameter value."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["arg_values = [\n", " -7.31158201e-02,\n", " -1.64514836e-04,\n", " 1.12585591e-03,\n", " -2.58367544e-03,\n", " 1.00006068e00,\n", " -1.19551357e-03,\n", " 9.99963988e-01,\n", " 2.53283285e-03,\n", "]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["energy = objective(arg_values)\n", "print(energy)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The HEA is designed to cram as many orthogonal degrees of freedom into a small circuit as possible to be able to explore a large region of the Hilbert space whilst the circuits themselves can be run with minimal noise. These ans\u00e4tze give virtually-optimal circuits by design, but suffer from an excessive number of variational parameters making convergence slow, barren plateaus where the classical optimiser fails to make progress, and spanning a space where most states lack a physical interpretation. These drawbacks can necessitate adding penalties and may mean that the ansatz cannot actually express the true ground state.
\n", "
\n", "The UCC ansatz, on the other hand, is derived from the electronic configuration. It sacrifices efficiency of the circuit for the guarantee of physical states and the variational parameters all having some meaningful effect, which helps the classical optimisation to converge.
\n", "
\n", "This starts by defining the terms of our single and double excitations. These would usually be generated using the orbital configurations, so we will just use a hard-coded example here for the purposes of demonstration."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import Qubit\n", "from pytket.pauli import Pauli, QubitPauliString"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["q = [Qubit(i) for i in range(4)]\n", "xyii = QubitPauliString([q[0], q[1]], [Pauli.X, Pauli.Y])\n", "yxii = QubitPauliString([q[0], q[1]], [Pauli.Y, Pauli.X])\n", "iixy = QubitPauliString([q[2], q[3]], [Pauli.X, Pauli.Y])\n", "iiyx = QubitPauliString([q[2], q[3]], [Pauli.Y, Pauli.X])\n", "xxxy = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.X, Pauli.Y])\n", "xxyx = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.Y, Pauli.X])\n", "xyxx = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.X, Pauli.X])\n", "yxxx = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.X, Pauli.X])\n", "yyyx = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.Y, Pauli.X])\n", "yyxy = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.X, Pauli.Y])\n", "yxyy = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.Y, Pauli.Y])\n", "xyyy = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.Y, Pauli.Y])"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["singles_a = {xyii: 1.0, yxii: -1.0}\n", "singles_b = {iixy: 1.0, iiyx: -1.0}\n", "doubles = {\n", " xxxy: 0.25,\n", " xxyx: -0.25,\n", " xyxx: 0.25,\n", " yxxx: -0.25,\n", " yyyx: -0.25,\n", " yyxy: 0.25,\n", " yxyy: -0.25,\n", " xyyy: 0.25,\n", "}"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Building the ansatz circuit itself is often done naively by defining the map from each term down to basic gates and then applying it to each term."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def add_operator_term(circuit: Circuit, term: QubitPauliString, angle: float):\n", " qubits = []\n", " for q, p in term.map.items():\n", " if p != Pauli.I:\n", " qubits.append(q)\n", " if p == Pauli.X:\n", " circuit.H(q)\n", " elif p == Pauli.Y:\n", " circuit.V(q)\n", " for i in range(len(qubits) - 1):\n", " circuit.CX(i, i + 1)\n", " circuit.Rz(angle, len(qubits) - 1)\n", " for i in reversed(range(len(qubits) - 1)):\n", " circuit.CX(i, i + 1)\n", " for q, p in term.map.items():\n", " if p == Pauli.X:\n", " circuit.H(q)\n", " elif p == Pauli.Y:\n", " circuit.Vdg(q)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Unitary Coupled Cluster Singles & Doubles ansatz:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def ucc(params):\n", " ansatz = Circuit(4)\n", " # Set initial reference state\n", " ansatz.X(1).X(3)\n", " # Evolve by excitations\n", " for term, coeff in singles_a.items():\n", " add_operator_term(ansatz, term, coeff * params[0])\n", " for term, coeff in singles_b.items():\n", " add_operator_term(ansatz, term, coeff * params[1])\n", " for term, coeff in doubles.items():\n", " add_operator_term(ansatz, term, coeff * params[2])\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is already quite verbose, but `pytket` has a neat shorthand construction for these operator terms using the `PauliExpBox` construction. We can then decompose these into basic gates using the `DecomposeBoxes` compiler pass."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.circuit import PauliExpBox\n", "from pytket.passes import DecomposeBoxes"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def add_excitation(circ, term_dict, param):\n", " for term, coeff in term_dict.items():\n", " qubits, paulis = zip(*term.map.items())\n", " pbox = PauliExpBox(paulis, coeff * param)\n", " circ.add_pauliexpbox(pbox, qubits)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["UCC ansatz with syntactic shortcuts:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def ucc(params):\n", " ansatz = Circuit(4)\n", " ansatz.X(1).X(3)\n", " add_excitation(ansatz, singles_a, params[0])\n", " add_excitation(ansatz, singles_b, params[1])\n", " add_excitation(ansatz, doubles, params[2])\n", " DecomposeBoxes().apply(ansatz)\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["The objective function can also be simplified using a utility method for constructing the measurement circuits and processing for expectation value calculations. For that, we convert the Hamiltonian to a pytket QubitPauliOperator:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Simplified objective function using utilities:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.utils.expectations import get_operator_expectation_value"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc(params)\n", " return (\n", " get_operator_expectation_value(circ, hamiltonian_op, backend, n_shots=4000)\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["arg_values = [-3.79002933e-05, 2.42964799e-05, 4.63447157e-01]"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["energy = objective(arg_values)\n", "print(energy)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["This is now the simplest form that this operation can take, but it isn't necessarily the most effective. When we decompose the ansatz circuit into basic gates, it is still very expensive. We can employ some of the circuit simplification passes available in `pytket` to reduce its size and improve fidelity in practice.
\n", "
\n", "A good example is to decompose each `PauliExpBox` into basic gates and then apply `FullPeepholeOptimise`, which defines a compilation strategy utilising all of the simplifications in `pytket` that act locally on small regions of a circuit. We can examine the effectiveness by looking at the number of two-qubit gates before and after simplification, which tends to be a good indicator of fidelity for near-term systems where these gates are often slow and inaccurate."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket import OpType\n", "from pytket.passes import FullPeepholeOptimise"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_circuit = ucc(arg_values)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count before\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth before\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["FullPeepholeOptimise().apply(test_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count after FPO\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth after FPO\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These simplification techniques are very general and are almost always beneficial to apply to a circuit if you want to eliminate local redundancies. But UCC ans\u00e4tze have extra structure that we can exploit further. They are defined entirely out of exponentiated tensors of Pauli matrices, giving the regular structure described by the `PauliExpBox`es. Under many circumstances, it is more efficient to not synthesise these constructions individually, but simultaneously in groups. The `PauliSimp` pass finds the description of a given circuit as a sequence of `PauliExpBox`es and resynthesises them (by default, in groups of commuting terms). This can cause great change in the overall structure and shape of the circuit, enabling the identification and elimination of non-local redundancy."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import PauliSimp"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["test_circuit = ucc(arg_values)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count before\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth before\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["PauliSimp().apply(test_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count after PS\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth after PS\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["FullPeepholeOptimise().apply(test_circuit)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["print(\"CX count after PS+FPO\", test_circuit.n_gates_of_type(OpType.CX))\n", "print(\"CX depth after PS+FPO\", test_circuit.depth_by_type(OpType.CX))"]}, {"cell_type": "markdown", "metadata": {}, "source": ["To include this into our routines, we can just add the simplification passes to the objective function. The `get_operator_expectation_value` utility handles compiling to meet the requirements of the backend, so we don't have to worry about that here."]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function with circuit simplification:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc(params)\n", " PauliSimp().apply(circ)\n", " FullPeepholeOptimise().apply(circ)\n", " return (\n", " get_operator_expectation_value(circ, hamiltonian_op, backend, n_shots=4000)\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["These circuit simplification techniques have tried to preserve the exact unitary of the circuit, but there are ways to change the unitary whilst preserving the correctness of the algorithm as a whole.
\n", "
\n", "For example, the excitation terms are generated by trotterisation of the excitation operator, and the order of the terms does not change the unitary in the limit of many trotter steps, so in this sense we are free to sequence the terms how we like and it is sensible to do this in a way that enables efficient synthesis of the circuit. Prioritising collecting terms into commuting sets is a very beneficial heuristic for this and can be performed using the `gen_term_sequence_circuit` method to group the terms together into collections of `PauliExpBox`es and the `GuidedPauliSimp` pass to utilise these sets for synthesis."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.passes import GuidedPauliSimp\n", "from pytket.utils import gen_term_sequence_circuit"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def ucc(params):\n", " singles_params = {qps: params[0] * coeff for qps, coeff in singles.items()}\n", " doubles_params = {qps: params[1] * coeff for qps, coeff in doubles.items()}\n", " excitation_op = QubitPauliOperator({**singles_params, **doubles_params})\n", " reference_circ = Circuit(4).X(1).X(3)\n", " ansatz = gen_term_sequence_circuit(excitation_op, reference_circ)\n", " GuidedPauliSimp().apply(ansatz)\n", " FullPeepholeOptimise().apply(ansatz)\n", " return ansatz"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Adding these simplification routines doesn't come for free. Compiling and simplifying the circuit to achieve the best results possible can be a difficult task, which can take some time for the classical computer to perform.
\n", "
\n", "During a VQE run, we will call this objective function many times and run many measurement circuits within each, but the circuits that are run on the quantum computer are almost identical, having the same gate structure but with different gate parameters and measurements. We have already exploited this within the body of the objective function by simplifying the ansatz circuit before we call `get_operator_expectation_value`, so it is only done once per objective calculation rather than once per measurement circuit.
\n", "
\n", "We can go even further by simplifying it once outside of the objective function, and then instantiating the simplified ansatz with the parameter values needed. For this, we will construct the UCC ansatz circuit using symbolic (parametric) gates."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from sympy import symbols"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Symbolic UCC ansatz generation:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["syms = symbols(\"p0 p1 p2\")\n", "singles_a_syms = {qps: syms[0] * coeff for qps, coeff in singles_a.items()}\n", "singles_b_syms = {qps: syms[1] * coeff for qps, coeff in singles_b.items()}\n", "doubles_syms = {qps: syms[2] * coeff for qps, coeff in doubles.items()}\n", "excitation_op = QubitPauliOperator({**singles_a_syms, **singles_b_syms, **doubles_syms})\n", "ucc_ref = Circuit(4).X(1).X(3)\n", "ucc = gen_term_sequence_circuit(excitation_op, ucc_ref)\n", "GuidedPauliSimp().apply(ucc)\n", "FullPeepholeOptimise().apply(ucc)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function using the symbolic ansatz:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc.copy()\n", " sym_map = dict(zip(syms, params))\n", " circ.symbol_substitution(sym_map)\n", " return (\n", " get_operator_expectation_value(circ, hamiltonian_op, backend, n_shots=4000)\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["We have now got some very good use of `pytket` for simplifying each individual circuit used in our experiment and for minimising the amount of time spent compiling, but there is still more we can do in terms of reducing the amount of work the quantum computer has to do. Currently, each (non-trivial) term in our measurement hamiltonian is measured by a different circuit within each expectation value calculation. Measurement reduction techniques exist for identifying when these observables commute and hence can be simultaneously measured, reducing the number of circuits required for the full expectation value calculation.
\n", "
\n", "This is built in to the `get_operator_expectation_value` method and can be applied by specifying a way to partition the measuremrnt terms. `PauliPartitionStrat.CommutingSets` can greatly reduce the number of measurement circuits by combining any number of terms that mutually commute. However, this involves potentially adding an arbitrary Clifford circuit to change the basis of the measurements which can be costly on NISQ devices, so `PauliPartitionStrat.NonConflictingSets` trades off some of the reduction in circuit number to guarantee that only single-qubit gates are introduced."]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.partition import PauliPartitionStrat"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function using measurement reduction:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc.copy()\n", " sym_map = dict(zip(syms, params))\n", " circ.symbol_substitution(sym_map)\n", " return (\n", " get_operator_expectation_value(\n", " circ,\n", " operator,\n", " backend,\n", " n_shots=4000,\n", " partition_strat=PauliPartitionStrat.CommutingSets,\n", " )\n", " + nuclear_repulsion_energy\n", " )"]}, {"cell_type": "markdown", "metadata": {}, "source": ["At this point, we have completely transformed how our VQE objective function works, improving its resilience to noise, cutting the number of circuits run, and maintaining fast runtimes. In doing this, we have explored a number of the features `pytket` offers that are beneficial to VQE and the UCC method:
\n", "- high-level syntactic constructs for evolution operators;
\n", "- utility methods for easy expectation value calculations;
\n", "- both generic and domain-specific circuit simplification methods;
\n", "- symbolic circuit compilation;
\n", "- measurement reduction for expectation value calculations."]}, {"cell_type": "markdown", "metadata": {}, "source": ["For the sake of completeness, the following gives the full code for the final solution, including passing the objective function to a classical optimiser to find the ground state:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["import openfermion as of\n", "from scipy.optimize import minimize\n", "from sympy import symbols"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["from pytket.extensions.qiskit import AerBackend\n", "from pytket.circuit import Circuit, Qubit\n", "from pytket.partition import PauliPartitionStrat\n", "from pytket.passes import GuidedPauliSimp, FullPeepholeOptimise\n", "from pytket.pauli import Pauli, QubitPauliString\n", "from pytket.utils import get_operator_expectation_value, gen_term_sequence_circuit\n", "from pytket.utils.operators import QubitPauliOperator"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Obtain electronic Hamiltonian:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian = (\n", " -0.8153001706270075 * of.QubitOperator(\"\")\n", " + 0.16988452027940318 * of.QubitOperator(\"Z0\")\n", " + -0.21886306781219608 * of.QubitOperator(\"Z1\")\n", " + 0.16988452027940323 * of.QubitOperator(\"Z2\")\n", " + -0.2188630678121961 * of.QubitOperator(\"Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z0 Z1\")\n", " + 0.16821198673715723 * of.QubitOperator(\"Z0 Z2\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z0 Z3\")\n", " + 0.16549431486978672 * of.QubitOperator(\"Z1 Z2\")\n", " + 0.1739537877649417 * of.QubitOperator(\"Z1 Z3\")\n", " + 0.12005143072546047 * of.QubitOperator(\"Z2 Z3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"X0 X1 Y2 Y3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 X2 X3\")\n", " + 0.04544288414432624 * of.QubitOperator(\"Y0 Y1 Y2 Y3\")\n", ")\n", "nuclear_repulsion_energy = 0.70556961456"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qps_from_openfermion(paulis):\n", " \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n", " qlist = []\n", " plist = []\n", " for q, p in paulis:\n", " qlist.append(Qubit(q))\n", " plist.append(pauli_sym[p])\n", " return QubitPauliString(qlist, plist)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def qpo_from_openfermion(openf_op):\n", " \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n", " tk_op = dict()\n", " for term, coeff in openf_op.terms.items():\n", " string = qps_from_openfermion(term)\n", " tk_op[string] = coeff\n", " return QubitPauliOperator(tk_op)"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Obtain terms for single and double excitations:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["q = [Qubit(i) for i in range(4)]\n", "xyii = QubitPauliString([q[0], q[1]], [Pauli.X, Pauli.Y])\n", "yxii = QubitPauliString([q[0], q[1]], [Pauli.Y, Pauli.X])\n", "iixy = QubitPauliString([q[2], q[3]], [Pauli.X, Pauli.Y])\n", "iiyx = QubitPauliString([q[2], q[3]], [Pauli.Y, Pauli.X])\n", "xxxy = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.X, Pauli.Y])\n", "xxyx = QubitPauliString(q, [Pauli.X, Pauli.X, Pauli.Y, Pauli.X])\n", "xyxx = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.X, Pauli.X])\n", "yxxx = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.X, Pauli.X])\n", "yyyx = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.Y, Pauli.X])\n", "yyxy = QubitPauliString(q, [Pauli.Y, Pauli.Y, Pauli.X, Pauli.Y])\n", "yxyy = QubitPauliString(q, [Pauli.Y, Pauli.X, Pauli.Y, Pauli.Y])\n", "xyyy = QubitPauliString(q, [Pauli.X, Pauli.Y, Pauli.Y, Pauli.Y])"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Symbolic UCC ansatz generation:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["syms = symbols(\"p0 p1 p2\")\n", "singles_syms = {xyii: syms[0], yxii: -syms[0], iixy: syms[1], iiyx: -syms[1]}\n", "doubles_syms = {\n", " xxxy: 0.25 * syms[2],\n", " xxyx: -0.25 * syms[2],\n", " xyxx: 0.25 * syms[2],\n", " yxxx: -0.25 * syms[2],\n", " yyyx: -0.25 * syms[2],\n", " yyxy: 0.25 * syms[2],\n", " yxyy: -0.25 * syms[2],\n", " xyyy: 0.25 * syms[2],\n", "}\n", "excitation_op = QubitPauliOperator({**singles_syms, **doubles_syms})\n", "ucc_ref = Circuit(4).X(0).X(2)\n", "ucc = gen_term_sequence_circuit(excitation_op, ucc_ref)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Circuit simplification:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["GuidedPauliSimp().apply(ucc)\n", "FullPeepholeOptimise().apply(ucc)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Connect to a simulator/device:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["backend = AerBackend()"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Objective function:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["def objective(params):\n", " circ = ucc.copy()\n", " sym_map = dict(zip(syms, params))\n", " circ.symbol_substitution(sym_map)\n", " return (\n", " get_operator_expectation_value(\n", " circ,\n", " hamiltonian_op,\n", " backend,\n", " n_shots=4000,\n", " partition_strat=PauliPartitionStrat.CommutingSets,\n", " )\n", " + nuclear_repulsion_energy\n", " ).real"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Optimise against the objective function:"]}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": ["initial_params = [1e-4, 1e-4, 4e-1]\n", "# #result = minimize(objective, initial_params, method=\"Nelder-Mead\")\n", "# #print(\"Final parameter values\", result.x)\n", "# #print(\"Final energy value\", result.fun)"]}, {"cell_type": "markdown", "metadata": {}, "source": ["Exercises:
\n", "- Replace the `get_operator_expectation_value` call with its implementation and use this to pull the analysis for measurement reduction outside of the objective function, so our circuits can be fully determined and compiled once. This means that the `symbol_substitution` method will need to be applied to each measurement circuit instead of just the state preparation circuit.
\n", "- Use the `SpamCorrecter` class to add some mitigation of the measurement errors. Start by running the characterisation circuits first, before your main VQE loop, then apply the mitigation to each of the circuits run within the objective function.
\n", "- Change the `backend` by passing in a `Qiskit` `NoiseModel` to simulate a noisy device. Compare the accuracy of the objective function both with and without the circuit simplification. Try running a classical optimiser over the objective function and compare the convergence rates with different noise models. If you have access to a QPU, try changing the `backend` to connect to that and compare the results to the simulator."]}], "metadata": {"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.6.4"}}, "nbformat": 4, "nbformat_minor": 2}
\ No newline at end of file