diff --git a/docs/examples/algorithms_and_protocols/ansatz_sequence_example.ipynb b/docs/examples/algorithms_and_protocols/ansatz_sequence_example.ipynb
index 3b6e4496..6517a1f8 100644
--- a/docs/examples/algorithms_and_protocols/ansatz_sequence_example.ipynb
+++ b/docs/examples/algorithms_and_protocols/ansatz_sequence_example.ipynb
@@ -1 +1,264 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Ansatz sequencing\n","\n","**Download this notebook - {nb-download}`ansatz_sequence_example.ipynb`**"]},{"cell_type":"markdown","metadata":{},"source":["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.
\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}
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Ansatz sequencing\n",
+ "\n",
+ "**Download this notebook - {nb-download}`ansatz_sequence_example.ipynb`**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "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.
\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": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.pauli import Pauli, QubitPauliString\n",
+ "from pytket.circuit import Qubit\n",
+ "\n",
+ "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": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import fresh_symbol\n",
+ "\n",
+ "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\n",
+ "\n",
+ "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": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import Circuit\n",
+ "from pytket.utils import gen_term_sequence_circuit\n",
+ "from pytket.partition import PauliPartitionStrat, GraphColourMethod\n",
+ "\n",
+ "reference_circ = Circuit(4).X(1).X(3)\n",
+ "\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\n",
+ "\n",
+ "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",
+ " f\" {pauli_exp} {pauli_exp.op.get_paulis()} {pauli_exp.op.get_phase()}\"\n",
+ " )\n",
+ " else:\n",
+ " print(f\"Native gate: {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\n",
+ "\n",
+ "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(f\"Naive CX Count: {naive_circuit.depth_by_type(OpType.CX)}\")\n",
+ "print(f\"Naive CX Count: {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\n",
+ "\n",
+ "smart_circuit = ansatz_circuit.copy()\n",
+ "Transform.UCCSynthesis(PauliSynthStrat.Sets, CXConfigType.Tree).apply(smart_circuit)\n",
+ "print(f\"Smart CX Depth: {smart_circuit.depth_by_type(OpType.CX)}\")\n",
+ "print(f\"Smart CX Count: {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(f\"Last CX Depth: {last_circuit.depth_by_type(OpType.CX)}\")\n",
+ "print(f\"Last CX count: {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": ".venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/examples/algorithms_and_protocols/entanglement_swapping.ipynb b/docs/examples/algorithms_and_protocols/entanglement_swapping.ipynb
index 090ecab7..af4418b0 100644
--- a/docs/examples/algorithms_and_protocols/entanglement_swapping.ipynb
+++ b/docs/examples/algorithms_and_protocols/entanglement_swapping.ipynb
@@ -1 +1,660 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Iterated entanglement swapping\n","\n","**Download this notebook - {nb-download}`entanglement_swapping.ipynb`**"]},{"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_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}
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Iterated entanglement swapping\n",
+ "\n",
+ "**Download this notebook - {nb-download}`entanglement_swapping.ipynb`**"
+ ]
+ },
+ {
+ "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\n",
+ "\n",
+ "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": "markdown",
+ "metadata": {},
+ "source": [
+ "Connect to a simulator:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.extensions.qiskit import AerBackend\n",
+ "\n",
+ "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)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit.display import render_circuit_jupyter as draw\n",
+ "\n",
+ "draw(bell_test)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "handle = backend.process_circuit(bell_test, n_shots=2000)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "counts = backend.get_result(handle).get_counts()\n",
+ "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\n",
+ "from pytket.utils import probs_from_counts\n",
+ "from itertools import product\n",
+ "from scipy.linalg import lstsq, eigh\n",
+ "import numpy as np\n",
+ "\n",
+ "\n",
+ "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\n",
+ "\n",
+ "\n",
+ "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_aer.noise import NoiseModel, depolarizing_error, ReadoutError\n",
+ "\n",
+ "\n",
+ "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\n",
+ "\n",
+ "\n",
+ "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/docs/examples/algorithms_and_protocols/measurement_reduction_example.ipynb b/docs/examples/algorithms_and_protocols/measurement_reduction_example.ipynb
index 54046d0f..3a08ac51 100644
--- a/docs/examples/algorithms_and_protocols/measurement_reduction_example.ipynb
+++ b/docs/examples/algorithms_and_protocols/measurement_reduction_example.ipynb
@@ -1 +1,283 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Advanced expectation values and measurement reduction\n","\n","**Download this notebook - {nb-download}`measurement_reduction_example.ipynb`**"]},{"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}
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Advanced expectation values and measurement reduction\n",
+ "\n",
+ "**Download this notebook - {nb-download}`measurement_reduction_example.ipynb`**"
+ ]
+ },
+ {
+ "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": "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": [
+ "from pytket.partition import PauliPartitionStrat\n",
+ "\n",
+ "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\n",
+ "\n",
+ "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(f\"CX gates for measurement: {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(f\"Circuits required for measurement: {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(f\"CX gates for measurement: {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
+}
diff --git a/docs/examples/algorithms_and_protocols/phase_estimation.ipynb b/docs/examples/algorithms_and_protocols/phase_estimation.ipynb
index bfe00347..e2ef31c9 100644
--- a/docs/examples/algorithms_and_protocols/phase_estimation.ipynb
+++ b/docs/examples/algorithms_and_protocols/phase_estimation.ipynb
@@ -250,6 +250,7 @@
"source": [
"from pytket.circuit import QControlBox, DiagonalBox\n",
"\n",
+ "\n",
"def build_phase_estimation_circuit(\n",
" n_measurement_qubits: int, state_prep: CircBox, unitary: CircBox | DiagonalBox\n",
") -> Circuit:\n",
@@ -350,7 +351,7 @@
"from pytket.circuit import DiagonalBox\n",
"import numpy as np\n",
"\n",
- "u_diagonal = np.array([1, 1, np.e**(1j * np.pi/4), np.e**(1j * np.pi/8)])\n",
+ "u_diagonal = np.array([1, 1, np.e ** (1j * np.pi / 4), np.e ** (1j * np.pi / 8)])\n",
"d_box = DiagonalBox(u_diagonal)"
]
},
@@ -368,9 +369,8 @@
"outputs": [],
"source": [
"qpe_circ_trivial = build_phase_estimation_circuit(\n",
- " n_measurement_qubits=4,\n",
- " state_prep=prep_box, \n",
- " unitary=d_box)"
+ " n_measurement_qubits=4, state_prep=prep_box, unitary=d_box\n",
+ ")"
]
},
{
@@ -451,6 +451,7 @@
"from pytket.backends.backendresult import BackendResult\n",
"import matplotlib.pyplot as plt\n",
"\n",
+ "\n",
"def plot_qpe_results(\n",
" sim_result: BackendResult,\n",
" n_strings: int = 4,\n",
@@ -469,7 +470,7 @@
"\n",
" if dark_mode:\n",
" plt.style.use(\"dark_background\")\n",
- " \n",
+ "\n",
" fig = plt.figure()\n",
" ax = fig.add_axes((0, 0, 0.4, 0.5))\n",
" color_list = [\"orange\"] * (len(x_axis_values))\n",
@@ -527,6 +528,7 @@
"source": [
"from pytket.backends.backendresult import BackendResult\n",
"\n",
+ "\n",
"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",
diff --git a/docs/examples/algorithms_and_protocols/ucc_vqe.ipynb b/docs/examples/algorithms_and_protocols/ucc_vqe.ipynb
index c01acf5a..8ed6bb6a 100644
--- a/docs/examples/algorithms_and_protocols/ucc_vqe.ipynb
+++ b/docs/examples/algorithms_and_protocols/ucc_vqe.ipynb
@@ -1 +1,1099 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# VQE with UCC ansatz\n","\n","**Download this notebook - {nb-download}`ucc_vqe.ipynb`**"]},{"cell_type":"markdown","metadata":{},"source":["In this tutorial, we will focus on:
\n","- building parameterised ansätze for variational algorithms;
\n","- compilation tools for UCC-style ansätze."]},{"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ätze 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_gate(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ätze 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}
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# VQE with UCC ansatz\n",
+ "\n",
+ "**Download this notebook - {nb-download}`ucc_vqe.ipynb`**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In this tutorial, we will focus on:
\n",
+ "- building parameterised ansätze for variational algorithms;
\n",
+ "- compilation tools for UCC-style ansätze."
+ ]
+ },
+ {
+ "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ätze 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_gate(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ätze 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 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.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
+}
diff --git a/docs/examples/backends/comparing_simulators.ipynb b/docs/examples/backends/comparing_simulators.ipynb
index 44e380f1..1cd4886e 100644
--- a/docs/examples/backends/comparing_simulators.ipynb
+++ b/docs/examples/backends/comparing_simulators.ipynb
@@ -1 +1,624 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Comparison of the simulators available through TKET\n","\n","**Download this notebook - {nb-download}`comparing_simulators.ipynb`**"]},{"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-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_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":["Run on the backend:"]},{"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}
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Comparison of the simulators available through TKET\n",
+ "\n",
+ "**Download this notebook - {nb-download}`comparing_simulators.ipynb`**"
+ ]
+ },
+ {
+ "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-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_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"
+ ]
+ },
+ {
+ "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": [
+ "Run on the backend:"
+ ]
+ },
+ {
+ "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
+}
diff --git a/docs/examples/backends/creating_backends.ipynb b/docs/examples/backends/creating_backends.ipynb
index d4a58c30..107d7d66 100644
--- a/docs/examples/backends/creating_backends.ipynb
+++ b/docs/examples/backends/creating_backends.ipynb
@@ -1 +1,1110 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# How to create your own `Backend` using `pytket`\n","\n","**Download this notebook - {nb-download}`creating_backends.ipynb`**"]},{"cell_type":"markdown","metadata":{},"source":["In this tutorial, we will focus on:\n","- the components of the abstract `Backend` class;\n","- adaptations for statevector simulation versus measurement sampling."]},{"cell_type":"markdown","metadata":{},"source":["To run this example, you will only need the core `pytket` package.\n","\n","The `pytket` framework currently has direct integration with the largest variety of devices and simulators out of any quantum software platform, but this is certainly not a complete collection. New quantum backends are frequently being rolled out as more device manufacturers bring their machines online and advanced in simulation research give rise to many purpose-built simulators for fast execution of specific circuit fragments.\n","\n","If you have something that can take circuits (i.e. a sequence of gates) and run/simulate them, adding integration with `pytket` connects it to a great number of users and enables existing software solutions to immediately take advantage of your new backend. This reach is further extended beyond just software written with `pytket` by exploiting its integration with the rest of the quantum software ecosystem, such as via the `TketBackend` wrapper to use the new backend within Qiskit projects.\n","\n","This notebook will take a toy simulator and demonstrate how to write each component of the `Backend` class to make it work with the rest of `pytket`. We'll start by defining the internal representation of a circuit that our simulator will use. Rather than common gates, this example will use exponentiated Pauli tensors ($e^{-i \\theta P}$ for $P \\in \\{I, X, Y, Z\\}^n$) as its basic operation, which are universal for unitary circuits. To keep it simple, we will ignore measurements for now and just consider unitaries."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket import Qubit\n","from pytket.pauli import QubitPauliString"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["class MyCircuit:\n"," \"\"\"A minimal representation of a unitary circuit\"\"\"\n"," def __init__(self, qubits: list[Qubit]):\n"," \"\"\"Creates a circuit over some set of qubits\n"," :param qubits: The list of qubits in the circuit\n"," \"\"\"\n"," self.qubits = sorted(qubits, reverse=True)\n"," self.gates = list()\n"," def add_gate(self, qps: QubitPauliString, angle: float):\n"," \"\"\"Adds a gate to the end of the circuit e^{-0.5i * qps * angle}\n"," :param qps: Pauli string to rotate around\n"," :param angle: Angle of rotation in radians\n"," \"\"\"\n"," self.gates.append((qps, angle))"]},{"cell_type":"markdown","metadata":{},"source":["To simulate these, it is enough to generate the matrix of these exponentials and apply them in sequence to our initial state. Calculating these matrix exponentials is easy since we can exploit the following property: if an operator $A$ satisfies $A^2 = I$, then $e^{i\\theta A} = \\mathrm{cos}(\\theta)I + i \\mathrm{sin}(\\theta) A$. This works for any tensor of Pauli matrices. Furthermore, since each Pauli matrix is some combination of a diagonal matrix and a permutation matrix, they benefit greatly from a sparse matrix representation, which we can obtain from the `QubitPauliString`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","\n","class MySimulator:\n"," \"\"\"A minimal statevector simulator for unitary circuits\"\"\"\n"," def __init__(self, qubits: list[Qubit]):\n"," \"\"\"Initialise a statevector, setting all qubits to the |0❭ state.\n"," We treat qubits[0] as the most-significant qubit\n"," :param qubits: The list of qubits in the circuit\n"," \"\"\"\n"," self._qubits = qubits\n"," self._qstate = np.zeros((2 ** len(qubits),), dtype=complex)\n"," self._qstate[0] = 1.0\n"," def apply_Pauli_rot(self, qps: QubitPauliString, angle: float):\n"," \"\"\"Applies e^{-0.5i * qps * angle} to the state\n"," :param qps: Pauli to rotate around\n"," :param angle: Angle of rotation in radians\n"," \"\"\"\n"," pauli_tensor = qps.to_sparse_matrix(self._qubits)\n"," exponent = -0.5 * angle\n"," self._qstate = np.cos(exponent) * self._qstate + 1j * np.sin(\n"," exponent\n"," ) * pauli_tensor.dot(self._qstate)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def run_mycircuit(circ: MyCircuit) -> np.ndarray:\n"," \"\"\"Gives the state after applying the circuit to the all-|0❭ state\n"," :param circ: The circuit to simulate\n"," :return: The final statevector\n"," \"\"\"\n"," sim = MySimulator(circ.qubits)\n"," for qps, angle in circ.gates:\n"," sim.apply_Pauli_rot(qps, angle)\n"," return sim._qstate"]},{"cell_type":"markdown","metadata":{},"source":["And that's all we need for a basic simulator! We can check that this works by trying to generate a Bell state (up to global phase)."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.pauli import Pauli"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["q = [Qubit(0), Qubit(1)]\n","circ = MyCircuit(q)\n","# Hadamard on Qubit(0)\n","circ.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)\n","circ.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)\n","circ.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)\n","# CX with control Qubit(0) and target Qubit(1)\n","circ.add_gate(QubitPauliString(Qubit(0), Pauli.Z), -np.pi / 2)\n","circ.add_gate(QubitPauliString(Qubit(1), Pauli.X), -np.pi / 2)\n","circ.add_gate(QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.X}), np.pi / 2)\n","print(run_mycircuit(circ))"]},{"cell_type":"markdown","metadata":{},"source":["A useful first step to integrating this is to define a conversion from the `pytket.Circuit` class to the `MyCircuit` class. In most cases, this will just amount to converting one gate at a time by a simple syntax map. We need not specify how to convert every possible `OpType`, since we can rely on the compilation passes in `pytket` to map the circuit into the required gate set as long as it is universal. For this example, the definitions of `OpType.Rx`, `OpType.Ry`, `OpType.Rz`, and `OpType.ZZMax` all match the form of a single Pauli exponential."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket import Circuit, OpType"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def tk_to_mycircuit(tkc: Circuit) -> MyCircuit:\n"," \"\"\"Convert a pytket Circuit to a MyCircuit object.\n"," Supports Rz, Rx, Ry, and ZZMax gates.\n"," :param tkc: The Circuit to convert\n"," :return: An equivalent MyCircuit object\n"," \"\"\"\n"," circ = MyCircuit(tkc.qubits)\n"," for command in tkc:\n"," optype = command.op.type\n"," if optype == OpType.Rx:\n"," circ.add_gate(\n"," QubitPauliString(command.args[0], Pauli.X), np.pi * command.op.params[0]\n"," )\n"," elif optype == OpType.Ry:\n"," circ.add_gate(\n"," QubitPauliString(command.args[0], Pauli.Y), np.pi * command.op.params[0]\n"," )\n"," elif optype == OpType.Rz:\n"," circ.add_gate(\n"," QubitPauliString(command.args[0], Pauli.Z), np.pi * command.op.params[0]\n"," )\n"," elif optype == OpType.ZZMax:\n"," circ.add_gate(\n"," QubitPauliString(command.args, [Pauli.Z, Pauli.Z]), np.pi * 0.5\n"," )\n"," else:\n"," raise ValueError(\"Cannot convert optype to MyCircuit: \", optype)\n"," return circ"]},{"cell_type":"markdown","metadata":{},"source":["Now we turn to the `Backend` class. This provides a uniform API to submit `Circuit` objects for evaluation, typically returning either a statevector or a set of measurement shots. It also captures all of the information needed for compilation and asynchronous job management.\n","\n","We will make a subclass of `Backend` for our statevector simulator. The `_supports_state` flag lets the methods of the abstract `Backend` class know that this implementation supports statevector simulation. We also set `_persistent_handles` to `False` since this `Backend` will not be able to retrieve results from a previous Python session.\n","\n","Since we do not need to connect to a remote process for the simulator, the constructor doesn't need to set anything up. The base `Backend` constructor will initialise the `_cache` field for storing job data."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends import Backend"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["class MyBackend(Backend):\n"," \"\"\"A pytket Backend wrapping around the MySimulator statevector simulator\"\"\"\n"," _supports_state = True\n"," _persistent_handles = False\n"," def __init__(self):\n"," \"\"\"Create a new instance of the MyBackend class\"\"\"\n"," super().__init__()"]},{"cell_type":"markdown","metadata":{},"source":["Most `Backend`s will only support a small fragment of the `Circuit` language, either through implementation limitations or since a specific presentation is universal. It is helpful to keep this information in the `Backend` object itself so that users can clearly see how a `Circuit` needs to look before it can be successfully run. The `Predicate` classes in `pytket` can capture many common restrictions. The idea behind the `required_predicates` list is that any `Circuit` satisfying every `Predicate` in the list can be run on the `Backend` successfully as it is.\n","\n","However, a typical high-level user will not be writing `Circuit`s that satisfies all of the `required_predicates`, preferring instead to use the model that is most natural for the algorithm they are implementing. Providing a `default_compilation_pass` gives users an easy starting point for compiling an arbitrary `Circuit` into a form that can be executed (when not blocked by paradigm constraints like `NoMidMeasurePredicate` or `NoClassicalControlPredicate` that cannot easily be solved by compilation).\n","\n","You can provide several options using the `optimisation_level` argument. We tend to use `0` for very basic compilation with no optimisation applied, `1` for the inclusion of fast optimisations (e.g. `SynthesiseIBM` is a pre-defined sequence of optimisation passes that scales well with circuit size), and `2` for heavier optimisation (e.g. `FullPeepholeOptimise` incorporates `SynthesiseTket` alongside some extra passes that may take longer for large circuits).\n","\n","When designing these compilation pass sequences for a given `Backend`, it can be a good idea to start with the passes that solve individual constraints from `required_predicates` (like `FullMappingPass` for `ConnectivityPredicate` or `RebaseX` for `GateSetPredicate`), and find an ordering such that no later pass invalidates the work of an earlier one.\n","\n","For `MyBackend`, we will need to enforce that our circuits are expressed entirely in terms of `OpType.Rx`, `OpType.Ry`, `OpType.Rz`, and `OpType.ZZMax` gates which we can solve using `RebaseCustom`. Note that we omit `OpType.Measure` since we can only run pure quantum circuits.\n","\n","The standard docstrings for these and other abstract methods can be seen in the documentation for the {py:class}`~pytket.backends.Backend` abstract base class ."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.predicates import Predicate, GateSetPredicate, NoClassicalBitsPredicate\n","from pytket.passes import (\n"," BasePass,\n"," SequencePass,\n"," DecomposeBoxes,\n"," SynthesiseTket,\n"," FullPeepholeOptimise,\n"," RebaseCustom,\n"," SquashCustom,\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["@property\n","def required_predicates(self) -> list[Predicate]:\n"," \"\"\"\n"," The minimum set of predicates that a circuit must satisfy before it can\n"," be successfully run on this backend.\n"," :return: Required predicates.\n"," \"\"\"\n"," preds = [\n"," NoClassicalBitsPredicate(),\n"," GateSetPredicate(\n"," {\n"," OpType.Rx,\n"," OpType.Ry,\n"," OpType.Rz,\n"," OpType.ZZMax,\n"," }\n"," ),\n"," ]\n"," return preds"]},{"cell_type":"markdown","metadata":{},"source":["Every `Backend` must define a rebasing method, which will normally be called from its default compilation passes (see below), but which may also be called independently. Given the target gate set, it is usually straightforward to define this using the `RebaseCustom` pass, with a couple of helpers defining rebase of an `OpType.CX` and a general `OpType.TK1` gate:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cx_circ = Circuit(2)\n","cx_circ.Sdg(0)\n","cx_circ.V(1)\n","cx_circ.Sdg(1)\n","cx_circ.Vdg(1)\n","cx_circ.ZZMax(0, 1)\n","cx_circ.Vdg(1)\n","cx_circ.Sdg(1)\n","cx_circ.add_phase(0.5)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def sq(a, b, c):\n"," circ = Circuit(1)\n"," if c != 0:\n"," circ.Rz(c, 0)\n"," if b != 0:\n"," circ.Rx(b, 0)\n"," if a != 0:\n"," circ.Rz(a, 0)\n"," return circ"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["rebase = RebaseCustom({OpType.Rx, OpType.Ry, OpType.Rz, OpType.ZZMax}, cx_circ, sq)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def default_compilation_pass(self, optimisation_level: int = 1) -> BasePass:\n"," \"\"\"\n"," A suggested compilation pass that will guarantee the resulting circuit\n"," will be suitable to run on this backend with as few preconditions as\n"," possible.\n"," :param optimisation_level: The level of optimisation to perform during\n"," compilation. Level 0 just solves the device constraints without\n"," optimising. Level 1 additionally performs some light optimisations.\n"," Level 2 adds more intensive optimisations that can increase compilation\n"," time for large circuits. Defaults to 1.\n"," :return: Compilation pass guaranteeing required predicates.\n"," \"\"\"\n"," assert optimisation_level in range(3)\n"," squash = SquashCustom({OpType.Rz, OpType.Rx, OpType.Ry}, sq)\n"," seq = [DecomposeBoxes()] # Decompose boxes into basic gates\n"," if optimisation_level == 1:\n"," seq.append(SynthesiseTket()) # Optional fast optimisation\n"," elif optimisation_level == 2:\n"," seq.append(FullPeepholeOptimise()) # Optional heavy optimisation\n"," seq.append(rebase) # Map to target gate set\n"," if optimisation_level != 0:\n"," seq.append(squash) # Optionally simplify 1qb gate chains within this gate set\n"," return SequencePass(seq)"]},{"cell_type":"markdown","metadata":{},"source":["The `backend_info` property is used for storing various properties of a backend. By default it provides all device information useful for compilation. Typically we would make it return a class attribute `self._backend_info` that we initialise on construction, but we will define it at point of request here. We use a `FullyConnected` architecture producing an `Architecture` object with couplings between 4 qubits."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends.backendinfo import BackendInfo\n","from pytket.architecture import FullyConnected"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["@property\n","def backend_info(self) -> BackendInfo:\n"," return BackendInfo(\n"," \"MyBackend\",\n"," \"MySimulator\",\n"," \"1.0\",\n"," FullyConnected(4),\n"," {\n"," OpType.Rx,\n"," OpType.Ry,\n"," OpType.Rz,\n"," OpType.ZZMax,\n"," OpType.Measure,\n"," },\n"," supports_midcircuit_measurement=False,\n"," misc={\"characterisation\": None},\n"," )"]},{"cell_type":"markdown","metadata":{},"source":["Asynchronous job management is all managed through the `ResultHandle` associated with a particular `Circuit` that has been submitted. We can use it to inspect the status of the job to see if it has completed, or to look up the results if they are available.\n","\n","For devices, `circuit_status` should query the job to see if it is in a queue, currently being executed, completed successfully, etc. The `CircuitStatus` class is mostly driven by the `StatusEnum` values, but can also contain messages to give more detailed feedback if available. For our simulator, we are not running things asynchronously, so a `Circuit` has either not been run or it will have been completed.\n","\n","Since a device API will probably define its own data type for job handles, the `ResultHandle` definition is flexible enough to cover many possible data types so you can likely use the underlying job handle as the `ResultHandle`. The `_result_id_type` property specifies what data type a `ResultHandle` for this `Backend` should look like. Since our simulator has no underlying job handle, we can just use a UUID string."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends import ResultHandle, CircuitStatus, StatusEnum, CircuitNotRunError\n","from pytket.backends.resulthandle import _ResultIdTuple"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["@property\n","def _result_id_type(self) -> _ResultIdTuple:\n"," \"\"\"Identifier type signature for ResultHandle for this backend.\n"," :return: Type signature (tuple of hashable types)\n"," \"\"\"\n"," return (str,)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def circuit_status(self, handle: ResultHandle) -> CircuitStatus:\n"," \"\"\"\n"," Return a CircuitStatus reporting the status of the circuit execution\n"," corresponding to the ResultHandle\n"," \"\"\"\n"," if handle in self._cache:\n"," return CircuitStatus(StatusEnum.COMPLETED)\n"," raise CircuitNotRunError(handle)"]},{"cell_type":"markdown","metadata":{},"source":["And finally, we have the method that actually submits a job for execution. `process_circuits` should take a collection of (compiled) `Circuit` objects, process them and return a `ResultHandle` for each `Circuit`. If execution is synchronous, then this can simply wait until it is finished, store the result in `_cache` and return. For backends that support asynchronous jobs, you will need to set up an event to format and store the result on completion.\n","\n","It is recommended to use the `valid_check` parameter to control a call to `Backend._check_all_circuits()`, which will raise an exception if any of the circuits do not satisfy everything in `required_predicates`.\n","\n","The `_cache` fields stores all of the information about current jobs that have been run. When a job has finished execution, the results are expected to be stored in `_cache[handle][\"result\"]`, though it can also be used to store other data about the job such as some information about the `Circuit` required to properly format the results. Methods like `Backend.get_result()` and `Backend.empty_cache()` expect to interact with the results of a given job in this way.\n","\n","The final output of the execution is stored in a `BackendResult` object. This captures enough information about the results to reinterpret it in numerous ways, such as requesting the statevector in a specific qubit ordering or converting a complete shot table to a summary of the counts. If we create a `BackendResult` with quantum data (e.g. a statevector or unitary), we must provide the `Qubit` ids in order from most-significant to least-significant with regards to indexing the state. Similarly, creating one with classical readouts (e.g. a shot table or counts summary), we give the `Bit` ids in the order they appear in a readout (left-to-right).\n","\n","For a statevector simulation, we should also take into account the global phase stored in the `Circuit` object and any implicit qubit permutations, since these become observable when inspecting the quantum state. We can handle the qubit permutation by changing the order in which we pass the `Qubit` ids into the `BackendResult` object."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends.backendresult import BackendResult\n","from pytket.utils.results import KwargTypes\n","from typing import Iterable, Optional\n","from uuid import uuid4"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def process_circuits(\n"," self,\n"," circuits: Iterable[Circuit],\n"," n_shots: Optional[int] = None,\n"," valid_check: bool = True,\n"," **kwargs: KwargTypes,\n",") -> list[ResultHandle]:\n"," \"\"\"\n"," Submit circuits to the backend for running. The results will be stored\n"," in the backend's result cache to be retrieved by the corresponding\n"," get_ method.\n"," Use keyword arguments to specify parameters to be used in submitting circuits\n"," See specific Backend derived class for available parameters, from the following\n"," list:\n"," * `seed`: RNG seed for simulators\n"," :param circuits: Circuits to process on the backend.\n"," :param n_shots: Number of shots to run per circuit. None is to be used\n"," for state/unitary simulators. Defaults to None.\n"," :param valid_check: Explicitly check that all circuits satisfy all required\n"," predicates to run on the backend. Defaults to True\n"," :return: Handles to results for each input circuit, as an interable in\n"," the same order as the circuits.\n"," \"\"\"\n"," circuit_list = list(circuits)\n"," if valid_check:\n"," self._check_all_circuits(circuit_list)\n"," handle_list = []\n"," for circuit in circuit_list:\n"," handle = ResultHandle(str(uuid4()))\n"," mycirc = tk_to_mycircuit(circuit)\n"," state = run_mycircuit(mycirc)\n"," state *= np.exp(1j * np.pi * circuit.phase)\n"," implicit_perm = circuit.implicit_qubit_permutation()\n"," res_qubits = [implicit_perm[qb] for qb in sorted(circuit.qubits, reverse=True)]\n"," res = BackendResult(q_bits=res_qubits, state=state)\n"," self._cache[handle] = {\"result\": res}\n"," handle_list.append(handle)\n"," return handle_list"]},{"cell_type":"markdown","metadata":{},"source":["Let's redefine our `MyBackend` class to use these methods to finish it off."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["class MyBackend(Backend):\n"," \"\"\"A pytket Backend wrapping around the MySimulator statevector simulator\"\"\"\n"," _supports_state = True\n"," _persistent_handles = False\n"," def __init__(self):\n"," \"\"\"Create a new instance of the MyBackend class\"\"\"\n"," super().__init__()\n"," required_predicates = required_predicates\n"," rebase_pass = rebase\n"," default_compilation_pass = default_compilation_pass\n"," _result_id_type = _result_id_type\n"," circuit_status = circuit_status\n"," process_circuits = process_circuits"]},{"cell_type":"markdown","metadata":{},"source":["Our new `Backend` subclass is now complete, so let's test it out. If you are planning on maintaining a backend class, it is recommended to set up some unit tests. The following tests will cover basic operation and integration with `pytket` utilities."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import BasisOrder, Unitary1qBox\n","from pytket.passes import CliffordSimp\n","from pytket.utils import get_operator_expectation_value\n","from pytket.utils.operators import QubitPauliOperator\n","import pytest"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_bell() -> None:\n"," c = Circuit(2)\n"," c.H(0)\n"," c.CX(0, 1)\n"," b = MyBackend()\n"," c = b.get_compiled_circuit(c)\n"," h = b.process_circuit(c)\n"," assert np.allclose(\n"," b.get_result(h).get_state(), np.asarray([1, 0, 0, 1]) * 1 / np.sqrt(2)\n"," )"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_basisorder() -> None:\n"," c = Circuit(2)\n"," c.X(1)\n"," b = MyBackend()\n"," c = b.get_compiled_circuit(c)\n"," h = b.process_circuit(c)\n"," r = b.get_result(h)\n"," assert np.allclose(r.get_state(), np.asarray([0, 1, 0, 0]))\n"," assert np.allclose(r.get_state(basis=BasisOrder.dlo), np.asarray([0, 0, 1, 0]))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_implicit_perm() -> None:\n"," c = Circuit(2)\n"," c.CX(0, 1)\n"," c.CX(1, 0)\n"," c.Ry(0.1, 1)\n"," c1 = c.copy()\n"," CliffordSimp().apply(c1)\n"," b = MyBackend()\n"," c = b.get_compiled_circuit(c, optimisation_level=1)\n"," c1 = b.get_compiled_circuit(c1, optimisation_level=1)\n"," assert c.implicit_qubit_permutation() != c1.implicit_qubit_permutation()\n"," h, h1 = b.process_circuits([c, c1])\n"," r, r1 = b.get_results([h, h1])\n"," for bo in [BasisOrder.ilo, BasisOrder.dlo]:\n"," s = r.get_state(basis=bo)\n"," s1 = r1.get_state(basis=bo)\n"," assert np.allclose(s, s1)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_compilation_pass() -> None:\n"," b = MyBackend()\n"," for opt_level in range(3):\n"," c = Circuit(2)\n"," c.CX(0, 1)\n"," u = np.asarray([[0, 1], [-1j, 0]])\n"," c.add_unitary1qbox(Unitary1qBox(u), 1)\n"," c.CX(0, 1)\n"," c.add_gate(OpType.CRz, 0.35, [1, 0])\n"," assert not (b.valid_circuit(c))\n"," c = b.get_compiled_circuit(c, optimisation_level=opt_level)\n"," assert b.valid_circuit(c)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_invalid_measures() -> None:\n"," c = Circuit(2)\n"," c.H(0).CX(0, 1).measure_all()\n"," b = MyBackend()\n"," c = b.get_compiled_circuit(c)\n"," assert not (b.valid_circuit(c))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_expectation_value() -> None:\n"," c = Circuit(2)\n"," c.H(0)\n"," c.CX(0, 1)\n"," op = QubitPauliOperator(\n"," {\n"," QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z}): 1.0,\n"," QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.3,\n"," QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Y}): 0.8j,\n"," QubitPauliString({Qubit(0): Pauli.Y}): -0.4j,\n"," }\n"," )\n"," b = MyBackend()\n"," c = b.get_compiled_circuit(c)\n"," assert get_operator_expectation_value(c, op, b) == pytest.approx(1.3)"]},{"cell_type":"markdown","metadata":{},"source":["Explicit calls are needed for this notebook. Normally pytest will just find these \"test_X\" methods when run from the command line:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["test_bell()\n","test_basisorder()\n","test_implicit_perm()\n","test_compilation_pass()\n","test_invalid_measures()\n","test_expectation_value()"]},{"cell_type":"markdown","metadata":{},"source":["To show how this compares to a sampling simulator, let's extend our simulator to handle end-of-circuit measurements."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from typing import Set"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def sample_mycircuit(\n"," circ: MyCircuit, qubits: Set[Qubit], n_shots: int, seed: Optional[int] = None\n",") -> np.ndarray:\n"," \"\"\"Run the circuit on the all-|0❭ state and measures a set of qubits\n"," :param circ: The circuit to simulate\n"," :param qubits: The set of qubits to measure\n"," :param n_shots: The number of samples to take\n"," :param seed: Seed for the random number generator, defaults to no seed\n"," :return: Table of shots; each row is a shot, columns are qubit readouts in ascending Qubit order\n"," \"\"\"\n"," state = run_mycircuit(circ)\n"," cumulative_probs = (state * state.conjugate()).cumsum()\n"," if seed is not None:\n"," np.random.seed(seed)\n"," shots = np.zeros((n_shots, len(circ.qubits)))\n"," for s in range(n_shots):\n"," # Pick a random point in the distribution\n"," point = np.random.uniform(0.0, 1.0)\n"," # Find the corresponding readout\n"," index = np.searchsorted(cumulative_probs, point)\n"," # Convert index to a binary array\n"," # `bin` maps e.g. index 6 to '0b110'\n"," # So we ignore the first two symbols and add leading 0s to make it a fixed length\n"," bitstring = bin(index)[2:].zfill(len(circ.qubits))\n"," shots[s] = np.asarray([int(b) for b in bitstring])\n"," filtered = np.zeros((n_shots, len(qubits)))\n"," target = 0\n"," for col, q in enumerate(circ.qubits):\n"," if q in qubits:\n"," filtered[:, target] = shots[:, col]\n"," target += 1\n"," return filtered"]},{"cell_type":"markdown","metadata":{},"source":["Since `MyCircuit` doesn't have a representation for measurement gates, our converter must return both the `MyCircuit` object and some way of capturing the measurements. Since we will also want to know how they map into our `Bit` ids, the simplest option is just a dictionary from `Qubit` to `Bit`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket import Bit\n","from typing import Dict, Tuple"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def tk_to_mymeasures(tkc: Circuit) -> Tuple[MyCircuit, Dict[Qubit, Bit]]:\n"," \"\"\"Convert a pytket Circuit to a MyCircuit object and a measurement map.\n"," Supports Rz, Rx, Ry, and ZZMax gates, as well as end-of-circuit measurements.\n"," :param tkc: The Circuit to convert\n"," :return: An equivalent MyCircuit object and a map from measured Qubit to the Bit containing the result\n"," \"\"\"\n"," circ = MyCircuit(tkc.qubits)\n"," measure_map = dict()\n"," measured_units = (\n"," set()\n"," ) # Track measured Qubits/used Bits to identify mid-circuit measurement\n"," for command in tkc:\n"," for u in command.args:\n"," if u in measured_units:\n"," raise ValueError(\"Circuit contains a mid-circuit measurement\")\n"," optype = command.op.type\n"," if optype == OpType.Rx:\n"," circ.add_gate(\n"," QubitPauliString(command.args[0], Pauli.X), np.pi * command.op.params[0]\n"," )\n"," elif optype == OpType.Ry:\n"," circ.add_gate(\n"," QubitPauliString(command.args[0], Pauli.Y), np.pi * command.op.params[0]\n"," )\n"," elif optype == OpType.Rz:\n"," circ.add_gate(\n"," QubitPauliString(command.args[0], Pauli.Z), np.pi * command.op.params[0]\n"," )\n"," elif optype == OpType.ZZMax:\n"," circ.add_gate(\n"," QubitPauliString(command.args, [Pauli.Z, Pauli.Z]), np.pi * 0.5\n"," )\n"," elif optype == OpType.Measure:\n"," measure_map[command.args[0]] = command.args[1]\n"," measured_units.add(command.args[0])\n"," measured_units.add(command.args[1])\n"," else:\n"," raise ValueError(\"Cannot convert optype to MyCircuit: \", optype)\n"," return circ, measure_map"]},{"cell_type":"markdown","metadata":{},"source":["To build a `Backend` subclass for this sampling simulator, we only need to change how we write `required_predicates` and `process_circuits`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.predicates import NoMidMeasurePredicate, NoClassicalControlPredicate\n","from pytket.utils.outcomearray import OutcomeArray"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["class MySampler(Backend):\n"," \"\"\"A pytket Backend wrapping around the MySimulator simulator with readout sampling\"\"\"\n"," _supports_shots = True\n"," _supports_counts = True\n"," _persistent_handles = False\n"," def __init__(self):\n"," \"\"\"Create a new instance of the MySampler class\"\"\"\n"," super().__init__()\n"," rebase_pass = rebase\n"," default_compilation_pass = default_compilation_pass\n"," _result_id_type = _result_id_type\n"," circuit_status = circuit_status\n"," @property\n"," def required_predicates(self) -> list[Predicate]:\n"," \"\"\"\n"," The minimum set of predicates that a circuit must satisfy before it can\n"," be successfully run on this backend.\n"," :return: Required predicates.\n"," \"\"\"\n"," preds = [\n"," NoClassicalControlPredicate(),\n"," NoMidMeasurePredicate(),\n"," GateSetPredicate(\n"," {\n"," OpType.Rx,\n"," OpType.Ry,\n"," OpType.Rz,\n"," OpType.ZZMax,\n"," OpType.Measure,\n"," }\n"," ),\n"," ]\n"," return preds\n"," def process_circuits(\n"," self,\n"," circuits: Iterable[Circuit],\n"," n_shots: Optional[int] = None,\n"," valid_check: bool = True,\n"," **kwargs: KwargTypes,\n"," ) -> list[ResultHandle]:\n"," \"\"\"\n"," Submit circuits to the backend for running. The results will be stored\n"," in the backend's result cache to be retrieved by the corresponding\n"," get_ method.\n"," Use keyword arguments to specify parameters to be used in submitting circuits\n"," See specific Backend derived class for available parameters, from the following\n"," list:\n"," * `seed`: RNG seed for simulators\n"," :param circuits: Circuits to process on the backend.\n"," :param n_shots: Number of shots to run per circuit. None is to be used\n"," for state/unitary simulators. Defaults to None.\n"," :param valid_check: Explicitly check that all circuits satisfy all required\n"," predicates to run on the backend. Defaults to True\n"," :return: Handles to results for each input circuit, as an interable in\n"," the same order as the circuits.\n"," \"\"\"\n"," circuit_list = list(circuits)\n"," if valid_check:\n"," self._check_all_circuits(circuit_list)\n"," handle_list = []\n"," for circuit in circuit_list:\n"," handle = ResultHandle(str(uuid4()))\n"," mycirc, measure_map = tk_to_mymeasures(circuit)\n"," qubit_list, bit_list = zip(*measure_map.items())\n"," qubit_shots = sample_mycircuit(\n"," mycirc, set(qubit_list), n_shots, kwargs.get(\"seed\")\n"," )\n"," # Pad shot table with 0 columns for unused bits\n"," all_shots = np.zeros((n_shots, len(circuit.bits)), dtype=int)\n"," all_shots[:, : len(qubit_list)] = qubit_shots\n"," res_bits = [measure_map[q] for q in sorted(qubit_list, reverse=True)]\n"," for b in circuit.bits:\n"," if b not in bit_list:\n"," res_bits.append(b)\n"," res = BackendResult(\n"," c_bits=res_bits, shots=OutcomeArray.from_readouts(all_shots)\n"," )\n"," self._cache[handle] = {\"result\": res}\n"," handle_list.append(handle)\n"," return handle_list"]},{"cell_type":"markdown","metadata":{},"source":["Likewise, we run some basic tests to make sure it works."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_sampler_bell() -> None:\n"," c = Circuit(2, 2)\n"," c.H(0)\n"," c.CX(0, 1)\n"," c.measure_all()\n"," b = MySampler()\n"," c = b.get_compiled_circuit(c)\n"," res = b.run_circuit(c, n_shots=10, seed=3)\n"," assert res.get_shots().shape == (10, 2)\n"," assert res.get_counts() == {(0, 0): 5, (1, 1): 5}"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_sampler_basisorder() -> None:\n"," c = Circuit(2, 2)\n"," c.X(1)\n"," c.measure_all()\n"," b = MySampler()\n"," c = b.get_compiled_circuit(c)\n"," res = b.run_circuit(c, n_shots=10, seed=0)\n"," assert res.get_counts() == {(0, 1): 10}\n"," assert res.get_counts(basis=BasisOrder.dlo) == {(1, 0): 10}"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_sampler_compilation_pass() -> None:\n"," b = MySampler()\n"," for opt_level in range(3):\n"," c = Circuit(2)\n"," c.CX(0, 1)\n"," u = np.asarray([[0, 1], [-1j, 0]])\n"," c.add_unitary1qbox(Unitary1qBox(u), 1)\n"," c.CX(0, 1)\n"," c.add_gate(OpType.CRz, 0.35, [1, 0])\n"," c.measure_all()\n"," assert not (b.valid_circuit(c))\n"," c = b.get_compiled_circuit(c, optimisation_level=opt_level)\n"," assert b.valid_circuit(c)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def test_sampler_expectation_value() -> None:\n"," c = Circuit(2)\n"," c.H(0)\n"," c.CX(0, 1)\n"," op = QubitPauliOperator(\n"," {\n"," QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z}): 1.0,\n"," QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.3,\n"," QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Y}): 0.8j,\n"," QubitPauliString({Qubit(0): Pauli.Y}): -0.4j,\n"," }\n"," )\n"," b = MySampler()\n"," c = b.get_compiled_circuit(c)\n"," expectation = get_operator_expectation_value(c, op, b, n_shots=2000, seed=0)\n"," assert (np.real(expectation), np.imag(expectation)) == pytest.approx(\n"," (1.3, 0.0), abs=0.1\n"," )"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["test_sampler_bell()\n","test_sampler_basisorder()\n","test_sampler_compilation_pass()\n","test_sampler_expectation_value()"]},{"cell_type":"markdown","metadata":{},"source":["Exercises:\n","- Add some extra gate definitions to the simulator and expand the accepted gate set of the backends. Start with some that are easily represented as exponentiated Pauli tensors like `OpType.YYPhase`. For a challenge, try adding `OpType.CCX` efficiently (it is possible to encode it using seven Pauli rotations).\n","- Restrict the simulator to a limited qubit connectivity. Express this in the backends by modifying the `Architecture` property of the `BackendInfo` attribute object and adding to the `required_predicates`. Adjust the `default_compilation_pass` to solve for the connectivity.\n","- The file `creating_backends_exercise.py` extends the simulators above to allow for mid-circuit measurement and conditional gates using a binary decision tree. Implement an appropriate converter and `Backend` class for this 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}
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# How to create your own `Backend` using `pytket`\n",
+ "\n",
+ "**Download this notebook - {nb-download}`creating_backends.ipynb`**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In this tutorial, we will focus on:\n",
+ "- the components of the abstract `Backend` class;\n",
+ "- adaptations for statevector simulation versus measurement sampling."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To run this example, you will only need the core `pytket` package.\n",
+ "\n",
+ "The `pytket` framework currently has direct integration with the largest variety of devices and simulators out of any quantum software platform, but this is certainly not a complete collection. New quantum backends are frequently being rolled out as more device manufacturers bring their machines online and advanced in simulation research give rise to many purpose-built simulators for fast execution of specific circuit fragments.\n",
+ "\n",
+ "If you have something that can take circuits (i.e. a sequence of gates) and run/simulate them, adding integration with `pytket` connects it to a great number of users and enables existing software solutions to immediately take advantage of your new backend. This reach is further extended beyond just software written with `pytket` by exploiting its integration with the rest of the quantum software ecosystem, such as via the `TketBackend` wrapper to use the new backend within Qiskit projects.\n",
+ "\n",
+ "This notebook will take a toy simulator and demonstrate how to write each component of the `Backend` class to make it work with the rest of `pytket`. We'll start by defining the internal representation of a circuit that our simulator will use. Rather than common gates, this example will use exponentiated Pauli tensors ($e^{-i \\theta P}$ for $P \\in \\{I, X, Y, Z\\}^n$) as its basic operation, which are universal for unitary circuits. To keep it simple, we will ignore measurements for now and just consider unitaries."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket import Qubit\n",
+ "from pytket.pauli import QubitPauliString"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class MyCircuit:\n",
+ " \"\"\"A minimal representation of a unitary circuit\"\"\"\n",
+ "\n",
+ " def __init__(self, qubits: list[Qubit]):\n",
+ " \"\"\"Creates a circuit over some set of qubits\n",
+ " :param qubits: The list of qubits in the circuit\n",
+ " \"\"\"\n",
+ " self.qubits = sorted(qubits, reverse=True)\n",
+ " self.gates = list()\n",
+ "\n",
+ " def add_gate(self, qps: QubitPauliString, angle: float):\n",
+ " \"\"\"Adds a gate to the end of the circuit e^{-0.5i * qps * angle}\n",
+ " :param qps: Pauli string to rotate around\n",
+ " :param angle: Angle of rotation in radians\n",
+ " \"\"\"\n",
+ " self.gates.append((qps, angle))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To simulate these, it is enough to generate the matrix of these exponentials and apply them in sequence to our initial state. Calculating these matrix exponentials is easy since we can exploit the following property: if an operator $A$ satisfies $A^2 = I$, then $e^{i\\theta A} = \\mathrm{cos}(\\theta)I + i \\mathrm{sin}(\\theta) A$. This works for any tensor of Pauli matrices. Furthermore, since each Pauli matrix is some combination of a diagonal matrix and a permutation matrix, they benefit greatly from a sparse matrix representation, which we can obtain from the `QubitPauliString`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "\n",
+ "\n",
+ "class MySimulator:\n",
+ " \"\"\"A minimal statevector simulator for unitary circuits\"\"\"\n",
+ "\n",
+ " def __init__(self, qubits: list[Qubit]):\n",
+ " \"\"\"Initialise a statevector, setting all qubits to the |0❭ state.\n",
+ " We treat qubits[0] as the most-significant qubit\n",
+ " :param qubits: The list of qubits in the circuit\n",
+ " \"\"\"\n",
+ " self._qubits = qubits\n",
+ " self._qstate = np.zeros((2 ** len(qubits),), dtype=complex)\n",
+ " self._qstate[0] = 1.0\n",
+ "\n",
+ " def apply_Pauli_rot(self, qps: QubitPauliString, angle: float):\n",
+ " \"\"\"Applies e^{-0.5i * qps * angle} to the state\n",
+ " :param qps: Pauli to rotate around\n",
+ " :param angle: Angle of rotation in radians\n",
+ " \"\"\"\n",
+ " pauli_tensor = qps.to_sparse_matrix(self._qubits)\n",
+ " exponent = -0.5 * angle\n",
+ " self._qstate = np.cos(exponent) * self._qstate + 1j * np.sin(\n",
+ " exponent\n",
+ " ) * pauli_tensor.dot(self._qstate)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def run_mycircuit(circ: MyCircuit) -> np.ndarray:\n",
+ " \"\"\"Gives the state after applying the circuit to the all-|0❭ state\n",
+ " :param circ: The circuit to simulate\n",
+ " :return: The final statevector\n",
+ " \"\"\"\n",
+ " sim = MySimulator(circ.qubits)\n",
+ " for qps, angle in circ.gates:\n",
+ " sim.apply_Pauli_rot(qps, angle)\n",
+ " return sim._qstate"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "And that's all we need for a basic simulator! We can check that this works by trying to generate a Bell state (up to global phase)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.pauli import Pauli"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "q = [Qubit(0), Qubit(1)]\n",
+ "circ = MyCircuit(q)\n",
+ "# Hadamard on Qubit(0)\n",
+ "circ.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)\n",
+ "circ.add_gate(QubitPauliString(Qubit(0), Pauli.X), np.pi / 2)\n",
+ "circ.add_gate(QubitPauliString(Qubit(0), Pauli.Z), np.pi / 2)\n",
+ "# CX with control Qubit(0) and target Qubit(1)\n",
+ "circ.add_gate(QubitPauliString(Qubit(0), Pauli.Z), -np.pi / 2)\n",
+ "circ.add_gate(QubitPauliString(Qubit(1), Pauli.X), -np.pi / 2)\n",
+ "circ.add_gate(QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.X}), np.pi / 2)\n",
+ "print(run_mycircuit(circ))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A useful first step to integrating this is to define a conversion from the `pytket.Circuit` class to the `MyCircuit` class. In most cases, this will just amount to converting one gate at a time by a simple syntax map. We need not specify how to convert every possible `OpType`, since we can rely on the compilation passes in `pytket` to map the circuit into the required gate set as long as it is universal. For this example, the definitions of `OpType.Rx`, `OpType.Ry`, `OpType.Rz`, and `OpType.ZZMax` all match the form of a single Pauli exponential."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket import Circuit, OpType"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def tk_to_mycircuit(tkc: Circuit) -> MyCircuit:\n",
+ " \"\"\"Convert a pytket Circuit to a MyCircuit object.\n",
+ " Supports Rz, Rx, Ry, and ZZMax gates.\n",
+ " :param tkc: The Circuit to convert\n",
+ " :return: An equivalent MyCircuit object\n",
+ " \"\"\"\n",
+ " circ = MyCircuit(tkc.qubits)\n",
+ " for command in tkc:\n",
+ " optype = command.op.type\n",
+ " if optype == OpType.Rx:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args[0], Pauli.X), np.pi * command.op.params[0]\n",
+ " )\n",
+ " elif optype == OpType.Ry:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args[0], Pauli.Y), np.pi * command.op.params[0]\n",
+ " )\n",
+ " elif optype == OpType.Rz:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args[0], Pauli.Z), np.pi * command.op.params[0]\n",
+ " )\n",
+ " elif optype == OpType.ZZMax:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args, [Pauli.Z, Pauli.Z]), np.pi * 0.5\n",
+ " )\n",
+ " else:\n",
+ " raise ValueError(\"Cannot convert optype to MyCircuit: \", optype)\n",
+ " return circ"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now we turn to the `Backend` class. This provides a uniform API to submit `Circuit` objects for evaluation, typically returning either a statevector or a set of measurement shots. It also captures all of the information needed for compilation and asynchronous job management.\n",
+ "\n",
+ "We will make a subclass of `Backend` for our statevector simulator. The `_supports_state` flag lets the methods of the abstract `Backend` class know that this implementation supports statevector simulation. We also set `_persistent_handles` to `False` since this `Backend` will not be able to retrieve results from a previous Python session.\n",
+ "\n",
+ "Since we do not need to connect to a remote process for the simulator, the constructor doesn't need to set anything up. The base `Backend` constructor will initialise the `_cache` field for storing job data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.backends import Backend"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class MyBackend(Backend):\n",
+ " \"\"\"A pytket Backend wrapping around the MySimulator statevector simulator\"\"\"\n",
+ "\n",
+ " _supports_state = True\n",
+ " _persistent_handles = False\n",
+ "\n",
+ " def __init__(self):\n",
+ " \"\"\"Create a new instance of the MyBackend class\"\"\"\n",
+ " super().__init__()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Most `Backend`s will only support a small fragment of the `Circuit` language, either through implementation limitations or since a specific presentation is universal. It is helpful to keep this information in the `Backend` object itself so that users can clearly see how a `Circuit` needs to look before it can be successfully run. The `Predicate` classes in `pytket` can capture many common restrictions. The idea behind the `required_predicates` list is that any `Circuit` satisfying every `Predicate` in the list can be run on the `Backend` successfully as it is.\n",
+ "\n",
+ "However, a typical high-level user will not be writing `Circuit`s that satisfies all of the `required_predicates`, preferring instead to use the model that is most natural for the algorithm they are implementing. Providing a `default_compilation_pass` gives users an easy starting point for compiling an arbitrary `Circuit` into a form that can be executed (when not blocked by paradigm constraints like `NoMidMeasurePredicate` or `NoClassicalControlPredicate` that cannot easily be solved by compilation).\n",
+ "\n",
+ "You can provide several options using the `optimisation_level` argument. We tend to use `0` for very basic compilation with no optimisation applied, `1` for the inclusion of fast optimisations (e.g. `SynthesiseIBM` is a pre-defined sequence of optimisation passes that scales well with circuit size), and `2` for heavier optimisation (e.g. `FullPeepholeOptimise` incorporates `SynthesiseTket` alongside some extra passes that may take longer for large circuits).\n",
+ "\n",
+ "When designing these compilation pass sequences for a given `Backend`, it can be a good idea to start with the passes that solve individual constraints from `required_predicates` (like `FullMappingPass` for `ConnectivityPredicate` or `RebaseX` for `GateSetPredicate`), and find an ordering such that no later pass invalidates the work of an earlier one.\n",
+ "\n",
+ "For `MyBackend`, we will need to enforce that our circuits are expressed entirely in terms of `OpType.Rx`, `OpType.Ry`, `OpType.Rz`, and `OpType.ZZMax` gates which we can solve using `RebaseCustom`. Note that we omit `OpType.Measure` since we can only run pure quantum circuits.\n",
+ "\n",
+ "The standard docstrings for these and other abstract methods can be seen in the documentation for the {py:class}`~pytket.backends.Backend` abstract base class ."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.predicates import Predicate, GateSetPredicate, NoClassicalBitsPredicate\n",
+ "from pytket.passes import (\n",
+ " BasePass,\n",
+ " SequencePass,\n",
+ " DecomposeBoxes,\n",
+ " SynthesiseTket,\n",
+ " FullPeepholeOptimise,\n",
+ " RebaseCustom,\n",
+ " SquashCustom,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@property\n",
+ "def required_predicates(self) -> list[Predicate]:\n",
+ " \"\"\"\n",
+ " The minimum set of predicates that a circuit must satisfy before it can\n",
+ " be successfully run on this backend.\n",
+ " :return: Required predicates.\n",
+ " \"\"\"\n",
+ " preds = [\n",
+ " NoClassicalBitsPredicate(),\n",
+ " GateSetPredicate(\n",
+ " {\n",
+ " OpType.Rx,\n",
+ " OpType.Ry,\n",
+ " OpType.Rz,\n",
+ " OpType.ZZMax,\n",
+ " }\n",
+ " ),\n",
+ " ]\n",
+ " return preds"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Every `Backend` must define a rebasing method, which will normally be called from its default compilation passes (see below), but which may also be called independently. Given the target gate set, it is usually straightforward to define this using the `RebaseCustom` pass, with a couple of helpers defining rebase of an `OpType.CX` and a general `OpType.TK1` gate:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cx_circ = Circuit(2)\n",
+ "cx_circ.Sdg(0)\n",
+ "cx_circ.V(1)\n",
+ "cx_circ.Sdg(1)\n",
+ "cx_circ.Vdg(1)\n",
+ "cx_circ.ZZMax(0, 1)\n",
+ "cx_circ.Vdg(1)\n",
+ "cx_circ.Sdg(1)\n",
+ "cx_circ.add_phase(0.5)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def sq(a, b, c):\n",
+ " circ = Circuit(1)\n",
+ " if c != 0:\n",
+ " circ.Rz(c, 0)\n",
+ " if b != 0:\n",
+ " circ.Rx(b, 0)\n",
+ " if a != 0:\n",
+ " circ.Rz(a, 0)\n",
+ " return circ"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "rebase = RebaseCustom({OpType.Rx, OpType.Ry, OpType.Rz, OpType.ZZMax}, cx_circ, sq)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def default_compilation_pass(self, optimisation_level: int = 1) -> BasePass:\n",
+ " \"\"\"\n",
+ " A suggested compilation pass that will guarantee the resulting circuit\n",
+ " will be suitable to run on this backend with as few preconditions as\n",
+ " possible.\n",
+ " :param optimisation_level: The level of optimisation to perform during\n",
+ " compilation. Level 0 just solves the device constraints without\n",
+ " optimising. Level 1 additionally performs some light optimisations.\n",
+ " Level 2 adds more intensive optimisations that can increase compilation\n",
+ " time for large circuits. Defaults to 1.\n",
+ " :return: Compilation pass guaranteeing required predicates.\n",
+ " \"\"\"\n",
+ " assert optimisation_level in range(3)\n",
+ " squash = SquashCustom({OpType.Rz, OpType.Rx, OpType.Ry}, sq)\n",
+ " seq = [DecomposeBoxes()] # Decompose boxes into basic gates\n",
+ " if optimisation_level == 1:\n",
+ " seq.append(SynthesiseTket()) # Optional fast optimisation\n",
+ " elif optimisation_level == 2:\n",
+ " seq.append(FullPeepholeOptimise()) # Optional heavy optimisation\n",
+ " seq.append(rebase) # Map to target gate set\n",
+ " if optimisation_level != 0:\n",
+ " seq.append(squash) # Optionally simplify 1qb gate chains within this gate set\n",
+ " return SequencePass(seq)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `backend_info` property is used for storing various properties of a backend. By default it provides all device information useful for compilation. Typically we would make it return a class attribute `self._backend_info` that we initialise on construction, but we will define it at point of request here. We use a `FullyConnected` architecture producing an `Architecture` object with couplings between 4 qubits."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.backends.backendinfo import BackendInfo\n",
+ "from pytket.architecture import FullyConnected"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@property\n",
+ "def backend_info(self) -> BackendInfo:\n",
+ " return BackendInfo(\n",
+ " \"MyBackend\",\n",
+ " \"MySimulator\",\n",
+ " \"1.0\",\n",
+ " FullyConnected(4),\n",
+ " {\n",
+ " OpType.Rx,\n",
+ " OpType.Ry,\n",
+ " OpType.Rz,\n",
+ " OpType.ZZMax,\n",
+ " OpType.Measure,\n",
+ " },\n",
+ " supports_midcircuit_measurement=False,\n",
+ " misc={\"characterisation\": None},\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Asynchronous job management is all managed through the `ResultHandle` associated with a particular `Circuit` that has been submitted. We can use it to inspect the status of the job to see if it has completed, or to look up the results if they are available.\n",
+ "\n",
+ "For devices, `circuit_status` should query the job to see if it is in a queue, currently being executed, completed successfully, etc. The `CircuitStatus` class is mostly driven by the `StatusEnum` values, but can also contain messages to give more detailed feedback if available. For our simulator, we are not running things asynchronously, so a `Circuit` has either not been run or it will have been completed.\n",
+ "\n",
+ "Since a device API will probably define its own data type for job handles, the `ResultHandle` definition is flexible enough to cover many possible data types so you can likely use the underlying job handle as the `ResultHandle`. The `_result_id_type` property specifies what data type a `ResultHandle` for this `Backend` should look like. Since our simulator has no underlying job handle, we can just use a UUID string."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.backends import ResultHandle, CircuitStatus, StatusEnum, CircuitNotRunError\n",
+ "from pytket.backends.resulthandle import _ResultIdTuple"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@property\n",
+ "def _result_id_type(self) -> _ResultIdTuple:\n",
+ " \"\"\"Identifier type signature for ResultHandle for this backend.\n",
+ " :return: Type signature (tuple of hashable types)\n",
+ " \"\"\"\n",
+ " return (str,)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def circuit_status(self, handle: ResultHandle) -> CircuitStatus:\n",
+ " \"\"\"\n",
+ " Return a CircuitStatus reporting the status of the circuit execution\n",
+ " corresponding to the ResultHandle\n",
+ " \"\"\"\n",
+ " if handle in self._cache:\n",
+ " return CircuitStatus(StatusEnum.COMPLETED)\n",
+ " raise CircuitNotRunError(handle)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "And finally, we have the method that actually submits a job for execution. `process_circuits` should take a collection of (compiled) `Circuit` objects, process them and return a `ResultHandle` for each `Circuit`. If execution is synchronous, then this can simply wait until it is finished, store the result in `_cache` and return. For backends that support asynchronous jobs, you will need to set up an event to format and store the result on completion.\n",
+ "\n",
+ "It is recommended to use the `valid_check` parameter to control a call to `Backend._check_all_circuits()`, which will raise an exception if any of the circuits do not satisfy everything in `required_predicates`.\n",
+ "\n",
+ "The `_cache` fields stores all of the information about current jobs that have been run. When a job has finished execution, the results are expected to be stored in `_cache[handle][\"result\"]`, though it can also be used to store other data about the job such as some information about the `Circuit` required to properly format the results. Methods like `Backend.get_result()` and `Backend.empty_cache()` expect to interact with the results of a given job in this way.\n",
+ "\n",
+ "The final output of the execution is stored in a `BackendResult` object. This captures enough information about the results to reinterpret it in numerous ways, such as requesting the statevector in a specific qubit ordering or converting a complete shot table to a summary of the counts. If we create a `BackendResult` with quantum data (e.g. a statevector or unitary), we must provide the `Qubit` ids in order from most-significant to least-significant with regards to indexing the state. Similarly, creating one with classical readouts (e.g. a shot table or counts summary), we give the `Bit` ids in the order they appear in a readout (left-to-right).\n",
+ "\n",
+ "For a statevector simulation, we should also take into account the global phase stored in the `Circuit` object and any implicit qubit permutations, since these become observable when inspecting the quantum state. We can handle the qubit permutation by changing the order in which we pass the `Qubit` ids into the `BackendResult` object."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.backends.backendresult import BackendResult\n",
+ "from pytket.utils.results import KwargTypes\n",
+ "from typing import Iterable, Optional\n",
+ "from uuid import uuid4"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def process_circuits(\n",
+ " self,\n",
+ " circuits: Iterable[Circuit],\n",
+ " n_shots: Optional[int] = None,\n",
+ " valid_check: bool = True,\n",
+ " **kwargs: KwargTypes,\n",
+ ") -> list[ResultHandle]:\n",
+ " \"\"\"\n",
+ " Submit circuits to the backend for running. The results will be stored\n",
+ " in the backend's result cache to be retrieved by the corresponding\n",
+ " get_ method.\n",
+ " Use keyword arguments to specify parameters to be used in submitting circuits\n",
+ " See specific Backend derived class for available parameters, from the following\n",
+ " list:\n",
+ " * `seed`: RNG seed for simulators\n",
+ " :param circuits: Circuits to process on the backend.\n",
+ " :param n_shots: Number of shots to run per circuit. None is to be used\n",
+ " for state/unitary simulators. Defaults to None.\n",
+ " :param valid_check: Explicitly check that all circuits satisfy all required\n",
+ " predicates to run on the backend. Defaults to True\n",
+ " :return: Handles to results for each input circuit, as an interable in\n",
+ " the same order as the circuits.\n",
+ " \"\"\"\n",
+ " circuit_list = list(circuits)\n",
+ " if valid_check:\n",
+ " self._check_all_circuits(circuit_list)\n",
+ " handle_list = []\n",
+ " for circuit in circuit_list:\n",
+ " handle = ResultHandle(str(uuid4()))\n",
+ " mycirc = tk_to_mycircuit(circuit)\n",
+ " state = run_mycircuit(mycirc)\n",
+ " state *= np.exp(1j * np.pi * circuit.phase)\n",
+ " implicit_perm = circuit.implicit_qubit_permutation()\n",
+ " res_qubits = [implicit_perm[qb] for qb in sorted(circuit.qubits, reverse=True)]\n",
+ " res = BackendResult(q_bits=res_qubits, state=state)\n",
+ " self._cache[handle] = {\"result\": res}\n",
+ " handle_list.append(handle)\n",
+ " return handle_list"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's redefine our `MyBackend` class to use these methods to finish it off."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class MyBackend(Backend):\n",
+ " \"\"\"A pytket Backend wrapping around the MySimulator statevector simulator\"\"\"\n",
+ "\n",
+ " _supports_state = True\n",
+ " _persistent_handles = False\n",
+ "\n",
+ " def __init__(self):\n",
+ " \"\"\"Create a new instance of the MyBackend class\"\"\"\n",
+ " super().__init__()\n",
+ "\n",
+ " required_predicates = required_predicates\n",
+ " rebase_pass = rebase\n",
+ " default_compilation_pass = default_compilation_pass\n",
+ " _result_id_type = _result_id_type\n",
+ " circuit_status = circuit_status\n",
+ " process_circuits = process_circuits"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Our new `Backend` subclass is now complete, so let's test it out. If you are planning on maintaining a backend class, it is recommended to set up some unit tests. The following tests will cover basic operation and integration with `pytket` utilities."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import BasisOrder, Unitary1qBox\n",
+ "from pytket.passes import CliffordSimp\n",
+ "from pytket.utils import get_operator_expectation_value\n",
+ "from pytket.utils.operators import QubitPauliOperator\n",
+ "import pytest"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_bell() -> None:\n",
+ " c = Circuit(2)\n",
+ " c.H(0)\n",
+ " c.CX(0, 1)\n",
+ " b = MyBackend()\n",
+ " c = b.get_compiled_circuit(c)\n",
+ " h = b.process_circuit(c)\n",
+ " assert np.allclose(\n",
+ " b.get_result(h).get_state(), np.asarray([1, 0, 0, 1]) * 1 / np.sqrt(2)\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_basisorder() -> None:\n",
+ " c = Circuit(2)\n",
+ " c.X(1)\n",
+ " b = MyBackend()\n",
+ " c = b.get_compiled_circuit(c)\n",
+ " h = b.process_circuit(c)\n",
+ " r = b.get_result(h)\n",
+ " assert np.allclose(r.get_state(), np.asarray([0, 1, 0, 0]))\n",
+ " assert np.allclose(r.get_state(basis=BasisOrder.dlo), np.asarray([0, 0, 1, 0]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_implicit_perm() -> None:\n",
+ " c = Circuit(2)\n",
+ " c.CX(0, 1)\n",
+ " c.CX(1, 0)\n",
+ " c.Ry(0.1, 1)\n",
+ " c1 = c.copy()\n",
+ " CliffordSimp().apply(c1)\n",
+ " b = MyBackend()\n",
+ " c = b.get_compiled_circuit(c, optimisation_level=1)\n",
+ " c1 = b.get_compiled_circuit(c1, optimisation_level=1)\n",
+ " assert c.implicit_qubit_permutation() != c1.implicit_qubit_permutation()\n",
+ " h, h1 = b.process_circuits([c, c1])\n",
+ " r, r1 = b.get_results([h, h1])\n",
+ " for bo in [BasisOrder.ilo, BasisOrder.dlo]:\n",
+ " s = r.get_state(basis=bo)\n",
+ " s1 = r1.get_state(basis=bo)\n",
+ " assert np.allclose(s, s1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_compilation_pass() -> None:\n",
+ " b = MyBackend()\n",
+ " for opt_level in range(3):\n",
+ " c = Circuit(2)\n",
+ " c.CX(0, 1)\n",
+ " u = np.asarray([[0, 1], [-1j, 0]])\n",
+ " c.add_unitary1qbox(Unitary1qBox(u), 1)\n",
+ " c.CX(0, 1)\n",
+ " c.add_gate(OpType.CRz, 0.35, [1, 0])\n",
+ " assert not (b.valid_circuit(c))\n",
+ " c = b.get_compiled_circuit(c, optimisation_level=opt_level)\n",
+ " assert b.valid_circuit(c)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_invalid_measures() -> None:\n",
+ " c = Circuit(2)\n",
+ " c.H(0).CX(0, 1).measure_all()\n",
+ " b = MyBackend()\n",
+ " c = b.get_compiled_circuit(c)\n",
+ " assert not (b.valid_circuit(c))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_expectation_value() -> None:\n",
+ " c = Circuit(2)\n",
+ " c.H(0)\n",
+ " c.CX(0, 1)\n",
+ " op = QubitPauliOperator(\n",
+ " {\n",
+ " QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z}): 1.0,\n",
+ " QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.3,\n",
+ " QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Y}): 0.8j,\n",
+ " QubitPauliString({Qubit(0): Pauli.Y}): -0.4j,\n",
+ " }\n",
+ " )\n",
+ " b = MyBackend()\n",
+ " c = b.get_compiled_circuit(c)\n",
+ " assert get_operator_expectation_value(c, op, b) == pytest.approx(1.3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Explicit calls are needed for this notebook. Normally pytest will just find these \"test_X\" methods when run from the command line:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test_bell()\n",
+ "test_basisorder()\n",
+ "test_implicit_perm()\n",
+ "test_compilation_pass()\n",
+ "test_invalid_measures()\n",
+ "test_expectation_value()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To show how this compares to a sampling simulator, let's extend our simulator to handle end-of-circuit measurements."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import Set"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def sample_mycircuit(\n",
+ " circ: MyCircuit, qubits: Set[Qubit], n_shots: int, seed: Optional[int] = None\n",
+ ") -> np.ndarray:\n",
+ " \"\"\"Run the circuit on the all-|0❭ state and measures a set of qubits\n",
+ " :param circ: The circuit to simulate\n",
+ " :param qubits: The set of qubits to measure\n",
+ " :param n_shots: The number of samples to take\n",
+ " :param seed: Seed for the random number generator, defaults to no seed\n",
+ " :return: Table of shots; each row is a shot, columns are qubit readouts in ascending Qubit order\n",
+ " \"\"\"\n",
+ " state = run_mycircuit(circ)\n",
+ " cumulative_probs = (state * state.conjugate()).cumsum()\n",
+ " if seed is not None:\n",
+ " np.random.seed(seed)\n",
+ " shots = np.zeros((n_shots, len(circ.qubits)))\n",
+ " for s in range(n_shots):\n",
+ " # Pick a random point in the distribution\n",
+ " point = np.random.uniform(0.0, 1.0)\n",
+ " # Find the corresponding readout\n",
+ " index = np.searchsorted(cumulative_probs, point)\n",
+ " # Convert index to a binary array\n",
+ " # `bin` maps e.g. index 6 to '0b110'\n",
+ " # So we ignore the first two symbols and add leading 0s to make it a fixed length\n",
+ " bitstring = bin(index)[2:].zfill(len(circ.qubits))\n",
+ " shots[s] = np.asarray([int(b) for b in bitstring])\n",
+ " filtered = np.zeros((n_shots, len(qubits)))\n",
+ " target = 0\n",
+ " for col, q in enumerate(circ.qubits):\n",
+ " if q in qubits:\n",
+ " filtered[:, target] = shots[:, col]\n",
+ " target += 1\n",
+ " return filtered"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Since `MyCircuit` doesn't have a representation for measurement gates, our converter must return both the `MyCircuit` object and some way of capturing the measurements. Since we will also want to know how they map into our `Bit` ids, the simplest option is just a dictionary from `Qubit` to `Bit`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket import Bit\n",
+ "from typing import Dict, Tuple"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def tk_to_mymeasures(tkc: Circuit) -> Tuple[MyCircuit, Dict[Qubit, Bit]]:\n",
+ " \"\"\"Convert a pytket Circuit to a MyCircuit object and a measurement map.\n",
+ " Supports Rz, Rx, Ry, and ZZMax gates, as well as end-of-circuit measurements.\n",
+ " :param tkc: The Circuit to convert\n",
+ " :return: An equivalent MyCircuit object and a map from measured Qubit to the Bit containing the result\n",
+ " \"\"\"\n",
+ " circ = MyCircuit(tkc.qubits)\n",
+ " measure_map = dict()\n",
+ " measured_units = (\n",
+ " set()\n",
+ " ) # Track measured Qubits/used Bits to identify mid-circuit measurement\n",
+ " for command in tkc:\n",
+ " for u in command.args:\n",
+ " if u in measured_units:\n",
+ " raise ValueError(\"Circuit contains a mid-circuit measurement\")\n",
+ " optype = command.op.type\n",
+ " if optype == OpType.Rx:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args[0], Pauli.X), np.pi * command.op.params[0]\n",
+ " )\n",
+ " elif optype == OpType.Ry:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args[0], Pauli.Y), np.pi * command.op.params[0]\n",
+ " )\n",
+ " elif optype == OpType.Rz:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args[0], Pauli.Z), np.pi * command.op.params[0]\n",
+ " )\n",
+ " elif optype == OpType.ZZMax:\n",
+ " circ.add_gate(\n",
+ " QubitPauliString(command.args, [Pauli.Z, Pauli.Z]), np.pi * 0.5\n",
+ " )\n",
+ " elif optype == OpType.Measure:\n",
+ " measure_map[command.args[0]] = command.args[1]\n",
+ " measured_units.add(command.args[0])\n",
+ " measured_units.add(command.args[1])\n",
+ " else:\n",
+ " raise ValueError(\"Cannot convert optype to MyCircuit: \", optype)\n",
+ " return circ, measure_map"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To build a `Backend` subclass for this sampling simulator, we only need to change how we write `required_predicates` and `process_circuits`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.predicates import NoMidMeasurePredicate, NoClassicalControlPredicate\n",
+ "from pytket.utils.outcomearray import OutcomeArray"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class MySampler(Backend):\n",
+ " \"\"\"A pytket Backend wrapping around the MySimulator simulator with readout sampling\"\"\"\n",
+ "\n",
+ " _supports_shots = True\n",
+ " _supports_counts = True\n",
+ " _persistent_handles = False\n",
+ "\n",
+ " def __init__(self):\n",
+ " \"\"\"Create a new instance of the MySampler class\"\"\"\n",
+ " super().__init__()\n",
+ "\n",
+ " rebase_pass = rebase\n",
+ " default_compilation_pass = default_compilation_pass\n",
+ " _result_id_type = _result_id_type\n",
+ " circuit_status = circuit_status\n",
+ "\n",
+ " @property\n",
+ " def required_predicates(self) -> list[Predicate]:\n",
+ " \"\"\"\n",
+ " The minimum set of predicates that a circuit must satisfy before it can\n",
+ " be successfully run on this backend.\n",
+ " :return: Required predicates.\n",
+ " \"\"\"\n",
+ " preds = [\n",
+ " NoClassicalControlPredicate(),\n",
+ " NoMidMeasurePredicate(),\n",
+ " GateSetPredicate(\n",
+ " {\n",
+ " OpType.Rx,\n",
+ " OpType.Ry,\n",
+ " OpType.Rz,\n",
+ " OpType.ZZMax,\n",
+ " OpType.Measure,\n",
+ " }\n",
+ " ),\n",
+ " ]\n",
+ " return preds\n",
+ "\n",
+ " def process_circuits(\n",
+ " self,\n",
+ " circuits: Iterable[Circuit],\n",
+ " n_shots: Optional[int] = None,\n",
+ " valid_check: bool = True,\n",
+ " **kwargs: KwargTypes,\n",
+ " ) -> list[ResultHandle]:\n",
+ " \"\"\"\n",
+ " Submit circuits to the backend for running. The results will be stored\n",
+ " in the backend's result cache to be retrieved by the corresponding\n",
+ " get_ method.\n",
+ " Use keyword arguments to specify parameters to be used in submitting circuits\n",
+ " See specific Backend derived class for available parameters, from the following\n",
+ " list:\n",
+ " * `seed`: RNG seed for simulators\n",
+ " :param circuits: Circuits to process on the backend.\n",
+ " :param n_shots: Number of shots to run per circuit. None is to be used\n",
+ " for state/unitary simulators. Defaults to None.\n",
+ " :param valid_check: Explicitly check that all circuits satisfy all required\n",
+ " predicates to run on the backend. Defaults to True\n",
+ " :return: Handles to results for each input circuit, as an interable in\n",
+ " the same order as the circuits.\n",
+ " \"\"\"\n",
+ " circuit_list = list(circuits)\n",
+ " if valid_check:\n",
+ " self._check_all_circuits(circuit_list)\n",
+ " handle_list = []\n",
+ " for circuit in circuit_list:\n",
+ " handle = ResultHandle(str(uuid4()))\n",
+ " mycirc, measure_map = tk_to_mymeasures(circuit)\n",
+ " qubit_list, bit_list = zip(*measure_map.items())\n",
+ " qubit_shots = sample_mycircuit(\n",
+ " mycirc, set(qubit_list), n_shots, kwargs.get(\"seed\")\n",
+ " )\n",
+ " # Pad shot table with 0 columns for unused bits\n",
+ " all_shots = np.zeros((n_shots, len(circuit.bits)), dtype=int)\n",
+ " all_shots[:, : len(qubit_list)] = qubit_shots\n",
+ " res_bits = [measure_map[q] for q in sorted(qubit_list, reverse=True)]\n",
+ " for b in circuit.bits:\n",
+ " if b not in bit_list:\n",
+ " res_bits.append(b)\n",
+ " res = BackendResult(\n",
+ " c_bits=res_bits, shots=OutcomeArray.from_readouts(all_shots)\n",
+ " )\n",
+ " self._cache[handle] = {\"result\": res}\n",
+ " handle_list.append(handle)\n",
+ " return handle_list"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Likewise, we run some basic tests to make sure it works."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_sampler_bell() -> None:\n",
+ " c = Circuit(2, 2)\n",
+ " c.H(0)\n",
+ " c.CX(0, 1)\n",
+ " c.measure_all()\n",
+ " b = MySampler()\n",
+ " c = b.get_compiled_circuit(c)\n",
+ " res = b.run_circuit(c, n_shots=10, seed=3)\n",
+ " assert res.get_shots().shape == (10, 2)\n",
+ " assert res.get_counts() == {(0, 0): 5, (1, 1): 5}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_sampler_basisorder() -> None:\n",
+ " c = Circuit(2, 2)\n",
+ " c.X(1)\n",
+ " c.measure_all()\n",
+ " b = MySampler()\n",
+ " c = b.get_compiled_circuit(c)\n",
+ " res = b.run_circuit(c, n_shots=10, seed=0)\n",
+ " assert res.get_counts() == {(0, 1): 10}\n",
+ " assert res.get_counts(basis=BasisOrder.dlo) == {(1, 0): 10}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_sampler_compilation_pass() -> None:\n",
+ " b = MySampler()\n",
+ " for opt_level in range(3):\n",
+ " c = Circuit(2)\n",
+ " c.CX(0, 1)\n",
+ " u = np.asarray([[0, 1], [-1j, 0]])\n",
+ " c.add_unitary1qbox(Unitary1qBox(u), 1)\n",
+ " c.CX(0, 1)\n",
+ " c.add_gate(OpType.CRz, 0.35, [1, 0])\n",
+ " c.measure_all()\n",
+ " assert not (b.valid_circuit(c))\n",
+ " c = b.get_compiled_circuit(c, optimisation_level=opt_level)\n",
+ " assert b.valid_circuit(c)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def test_sampler_expectation_value() -> None:\n",
+ " c = Circuit(2)\n",
+ " c.H(0)\n",
+ " c.CX(0, 1)\n",
+ " op = QubitPauliOperator(\n",
+ " {\n",
+ " QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Z}): 1.0,\n",
+ " QubitPauliString({Qubit(0): Pauli.X, Qubit(1): Pauli.X}): 0.3,\n",
+ " QubitPauliString({Qubit(0): Pauli.Z, Qubit(1): Pauli.Y}): 0.8j,\n",
+ " QubitPauliString({Qubit(0): Pauli.Y}): -0.4j,\n",
+ " }\n",
+ " )\n",
+ " b = MySampler()\n",
+ " c = b.get_compiled_circuit(c)\n",
+ " expectation = get_operator_expectation_value(c, op, b, n_shots=2000, seed=0)\n",
+ " assert (np.real(expectation), np.imag(expectation)) == pytest.approx(\n",
+ " (1.3, 0.0), abs=0.1\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test_sampler_bell()\n",
+ "test_sampler_basisorder()\n",
+ "test_sampler_compilation_pass()\n",
+ "test_sampler_expectation_value()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Exercises:\n",
+ "- Add some extra gate definitions to the simulator and expand the accepted gate set of the backends. Start with some that are easily represented as exponentiated Pauli tensors like `OpType.YYPhase`. For a challenge, try adding `OpType.CCX` efficiently (it is possible to encode it using seven Pauli rotations).\n",
+ "- Restrict the simulator to a limited qubit connectivity. Express this in the backends by modifying the `Architecture` property of the `BackendInfo` attribute object and adding to the `required_predicates`. Adjust the `default_compilation_pass` to solve for the connectivity.\n",
+ "- The file `creating_backends_exercise.py` extends the simulators above to allow for mid-circuit measurement and conditional gates using a binary decision tree. Implement an appropriate converter and `Backend` class for this 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
+}
diff --git a/docs/examples/backends/qiskit_integration.ipynb b/docs/examples/backends/qiskit_integration.ipynb
index 3739b503..e3ecbe5b 100644
--- a/docs/examples/backends/qiskit_integration.ipynb
+++ b/docs/examples/backends/qiskit_integration.ipynb
@@ -1 +1,231 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Integrating `pytket` into Qiskit software"]},{"cell_type":"markdown","metadata":{},"source":["**Download this notebook - {nb-download}`qiskit_integration.ipynb`**"]},{"cell_type":"markdown","metadata":{},"source":["In this tutorial, we will focus on:\n","- Using `pytket` for compilation or providing devices/simulators within Qiskit workflows;\n","- Adapting Qiskit code to use `pytket` directly."]},{"cell_type":"markdown","metadata":{},"source":["See the [pytket-qiskit docs](inv:pytket-qiskit:std:doc#index) for more information."]},{"cell_type":"markdown","metadata":{},"source":["This example assumes some familiarity with the Qiskit algorithms library. We have chosen a small variational quantum eigensolver (VQE) for our example, but the same principles apply to a wide range of quantum algorithms.\n","\n","To run this example, you will need `pytket-qiskit`, as well as the separate `qiskit-optimization` package. You will also need IBMQ credentials stored on your local machine.\n","\n","Qiskit has risen to prominence as the most popular platform for the development of quantum software, providing an open source, full-stack solution with a large feature list and extensive examples from the developers and community. For many researchers who have already invested in building a large codebase built on top of Qiskit, the idea of switching entirely to a new platform can look like a time-sink and may require reversion to take advantage of the new tools that get regularly added to Qiskit.\n","\n","The interoperability provided by [pytket-qiskit](inv:pytket-qiskit:std:doc#index) allows Qiskit users to start taking advantage of some of the unique features of [pytket](inv:pytket:std:doc#index) without having to completely rewrite their software."]},{"cell_type":"markdown","metadata":{},"source":["Let's take as an example an ansatz for computing the ground-state energy of a hydrogen molecule."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from qiskit.quantum_info import SparsePauliOp\n","\n","H2_op = SparsePauliOp.from_list(\n"," [\n"," (\"II\", -1.052373245772859),\n"," (\"IZ\", 0.39793742484318045),\n"," (\"ZI\", -0.39793742484318045),\n"," (\"ZZ\", -0.01128010425623538),\n"," (\"XX\", 0.18093119978423156),\n"," ]\n",")"]},{"cell_type":"markdown","metadata":{},"source":["First let's use qiskit's NumPyEigensolver to compute the exact answer:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from qiskit.algorithms.eigensolvers import NumPyEigensolver\n","\n","es = NumPyEigensolver(k=1)\n","exact_result = es.compute_eigenvalues(H2_op).eigenvalues[0].real\n","print(\"Exact result:\", exact_result)"]},{"cell_type":"markdown","metadata":{},"source":["The following function will attempt to find an approximation to this using VQE, given a qiskit BackendEstimator on which to run circuits:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from qiskit.algorithms.minimum_eigensolvers.vqe import VQE\n","from qiskit.algorithms.optimizers import SPSA\n","from qiskit.circuit.library import EfficientSU2\n","\n","def vqe_solve(op, maxiter, qestimator):\n"," optimizer = SPSA(maxiter=maxiter)\n"," ansatz = EfficientSU2(op.num_qubits, entanglement=\"linear\")\n"," vqe = VQE(estimator=qestimator, ansatz=ansatz, optimizer=optimizer)\n"," return vqe.compute_minimum_eigenvalue(op).eigenvalue"]},{"cell_type":"markdown","metadata":{},"source":["We will run this on a pytket {py:class}`~pytket.extensions.qiskit.IBMQEmulatorBackend`. This is a noisy simulator whose characteristics match those of the real device, in this case \"ibmq_belem\" (a 5-qubit machine). The characteristics are retrieved from the device when the backend is constructed, so we must first load our IBM account. Circuits will be compiled to match the connectivity of the device and simulated using a basic noise model [constructed from the device parameters](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.NoiseModel.html)."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.qiskit import IBMQEmulatorBackend\n","\n","b_emu = IBMQEmulatorBackend(\"ibm_kyiv\", instance=\"ibm-q/open/main\")"]},{"cell_type":"markdown","metadata":{},"source":["Most qiskit algorithms require a qiskit `primitive` as input; this in turn is constructed from a `qiskit.providers.Backend`. The [TketBackend](inv:#*.TketBackend) class wraps a pytket backend as a `qiskit.providers.Backend`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.qiskit.tket_backend import TketBackend\n","from qiskit.primitives import BackendEstimator\n","\n","qis_backend = TketBackend(b_emu)\n","qestimator = BackendEstimator(qis_backend, options={\"shots\": 8192})"]},{"cell_type":"markdown","metadata":{},"source":["Note that we could have used any other pytket shots backend instead of `b_emu` here. The `pytket` extension modules provide an interface to a wide variety of devices and simulators from different quantum software platforms.
\n","
\n","We can now run the VQE algorithm. In this example we use only 50 iterations, but greater accuracy may be achieved by increasing this number:"]},{"cell_type":"markdown","metadata":{},"source":["#print(\"VQE result:\", vqe_solve(H2_op, 50, qestimator))"]},{"cell_type":"markdown","metadata":{},"source":["Another way to improve the accuracy of results is to apply optimisations to the circuit in an attempt to reduce the overall noise. When we construct our qiskit backend, we can pass in a pytket compilation pass as an additional parameter. There is a wide range of options here; we recommend the device-specific default compilation pass, provided by each tket backend. This pass will ensure that all the hardware constraints of the device are met. We can enable tket's most aggressive optimisation level by setting the parameter `optimisation_level=2`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qis_backend2 = TketBackend(b_emu, b_emu.default_compilation_pass(optimisation_level=2))\n","qestimator2 = BackendEstimator(qis_backend2, options={\"shots\": 8192})"]},{"cell_type":"markdown","metadata":{},"source":["Let's run the optimisation again:"]},{"cell_type":"markdown","metadata":{},"source":["#print(\"VQE result (with optimisation):\", vqe_solve(H2_op, 50, qestimator2))"]},{"cell_type":"markdown","metadata":{},"source":["These are small two-qubit circuits, so the improvement may be small, but with larger, more complex circuits, the reduction in noise from compilation will make a greater difference and allow VQE experiments to converge with fewer iterations."]}],"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": [
+ "# Integrating `pytket` into Qiskit software"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "**Download this notebook - {nb-download}`qiskit_integration.ipynb`**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In this tutorial, we will focus on:\n",
+ "- Using `pytket` for compilation or providing devices/simulators within Qiskit workflows;\n",
+ "- Adapting Qiskit code to use `pytket` directly."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "See the [pytket-qiskit docs](inv:pytket-qiskit:std:doc#index) for more information."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This example assumes some familiarity with the Qiskit algorithms library. We have chosen a small variational quantum eigensolver (VQE) for our example, but the same principles apply to a wide range of quantum algorithms.\n",
+ "\n",
+ "To run this example, you will need `pytket-qiskit`, as well as the separate `qiskit-optimization` package. You will also need IBMQ credentials stored on your local machine.\n",
+ "\n",
+ "Qiskit has risen to prominence as the most popular platform for the development of quantum software, providing an open source, full-stack solution with a large feature list and extensive examples from the developers and community. For many researchers who have already invested in building a large codebase built on top of Qiskit, the idea of switching entirely to a new platform can look like a time-sink and may require reversion to take advantage of the new tools that get regularly added to Qiskit.\n",
+ "\n",
+ "The interoperability provided by [pytket-qiskit](inv:pytket-qiskit:std:doc#index) allows Qiskit users to start taking advantage of some of the unique features of [pytket](inv:pytket:std:doc#index) without having to completely rewrite their software."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's take as an example an ansatz for computing the ground-state energy of a hydrogen molecule."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qiskit.quantum_info import SparsePauliOp\n",
+ "\n",
+ "H2_op = SparsePauliOp.from_list(\n",
+ " [\n",
+ " (\"II\", -1.052373245772859),\n",
+ " (\"IZ\", 0.39793742484318045),\n",
+ " (\"ZI\", -0.39793742484318045),\n",
+ " (\"ZZ\", -0.01128010425623538),\n",
+ " (\"XX\", 0.18093119978423156),\n",
+ " ]\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "First let's use qiskit's NumPyEigensolver to compute the exact answer:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qiskit.algorithms.eigensolvers import NumPyEigensolver\n",
+ "\n",
+ "es = NumPyEigensolver(k=1)\n",
+ "exact_result = es.compute_eigenvalues(H2_op).eigenvalues[0].real\n",
+ "print(\"Exact result:\", exact_result)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The following function will attempt to find an approximation to this using VQE, given a qiskit BackendEstimator on which to run circuits:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qiskit.algorithms.minimum_eigensolvers.vqe import VQE\n",
+ "from qiskit.algorithms.optimizers import SPSA\n",
+ "from qiskit.circuit.library import EfficientSU2\n",
+ "\n",
+ "\n",
+ "def vqe_solve(op, maxiter, qestimator):\n",
+ " optimizer = SPSA(maxiter=maxiter)\n",
+ " ansatz = EfficientSU2(op.num_qubits, entanglement=\"linear\")\n",
+ " vqe = VQE(estimator=qestimator, ansatz=ansatz, optimizer=optimizer)\n",
+ " return vqe.compute_minimum_eigenvalue(op).eigenvalue"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will run this on a pytket {py:class}`~pytket.extensions.qiskit.IBMQEmulatorBackend`. This is a noisy simulator whose characteristics match those of the real device, in this case \"ibmq_belem\" (a 5-qubit machine). The characteristics are retrieved from the device when the backend is constructed, so we must first load our IBM account. Circuits will be compiled to match the connectivity of the device and simulated using a basic noise model [constructed from the device parameters](https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.NoiseModel.html)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.extensions.qiskit import IBMQEmulatorBackend\n",
+ "\n",
+ "b_emu = IBMQEmulatorBackend(\"ibm_kyiv\", instance=\"ibm-q/open/main\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Most qiskit algorithms require a qiskit `primitive` as input; this in turn is constructed from a `qiskit.providers.Backend`. The [TketBackend](inv:#*.TketBackend) class wraps a pytket backend as a `qiskit.providers.Backend`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.extensions.qiskit.tket_backend import TketBackend\n",
+ "from qiskit.primitives import BackendEstimator\n",
+ "\n",
+ "qis_backend = TketBackend(b_emu)\n",
+ "qestimator = BackendEstimator(qis_backend, options={\"shots\": 8192})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that we could have used any other pytket shots backend instead of `b_emu` here. The `pytket` extension modules provide an interface to a wide variety of devices and simulators from different quantum software platforms.
\n",
+ "
\n",
+ "We can now run the VQE algorithm. In this example we use only 50 iterations, but greater accuracy may be achieved by increasing this number:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#print(\"VQE result:\", vqe_solve(H2_op, 50, qestimator))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Another way to improve the accuracy of results is to apply optimisations to the circuit in an attempt to reduce the overall noise. When we construct our qiskit backend, we can pass in a pytket compilation pass as an additional parameter. There is a wide range of options here; we recommend the device-specific default compilation pass, provided by each tket backend. This pass will ensure that all the hardware constraints of the device are met. We can enable tket's most aggressive optimisation level by setting the parameter `optimisation_level=2`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "qis_backend2 = TketBackend(b_emu, b_emu.default_compilation_pass(optimisation_level=2))\n",
+ "qestimator2 = BackendEstimator(qis_backend2, options={\"shots\": 8192})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's run the optimisation again:"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#print(\"VQE result (with optimisation):\", vqe_solve(H2_op, 50, qestimator2))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "These are small two-qubit circuits, so the improvement may be small, but with larger, more complex circuits, the reduction in noise from compilation will make a greater difference and allow VQE experiments to converge with fewer iterations."
+ ]
+ }
+ ],
+ "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/docs/examples/circuit_compilation/compilation_example.ipynb b/docs/examples/circuit_compilation/compilation_example.ipynb
index fabf9f29..13942727 100644
--- a/docs/examples/circuit_compilation/compilation_example.ipynb
+++ b/docs/examples/circuit_compilation/compilation_example.ipynb
@@ -1 +1,750 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Compilation passes\n","\n","**Download this notebook - {nb-download}`compilation_example.ipynb`**"]},{"cell_type":"markdown","metadata":{},"source":["There are numerous ways to optimize circuits in `pytket`. In this notebook we will introduce the basics of compilation passes and how to combine and apply them.
\n","
\n","We assume familiarity with the `pytket` `Circuit` class. The objective is to transform one `Circuit` into another, equivalent, `Circuit`, that:
\n","* satisfies the connectivity constraints of a given architecture;
\n","* satisfies some further user-defined constraints (such as restricted gate sets);
\n","* minimizes some cost function (such as CX count)."]},{"cell_type":"markdown","metadata":{},"source":["## Passes"]},{"cell_type":"markdown","metadata":{},"source":["The basic mechanism of compilation is the 'pass', which is a transform that can be applied to a circuit. There is an extensive library of passes in `pytket`, and several standard ways in which they can be combined to form new passes. For example:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import DecomposeMultiQubitsCX"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pass1 = DecomposeMultiQubitsCX()"]},{"cell_type":"markdown","metadata":{},"source":["This pass converts all multi-qubit gates into CX and single-qubit gates. So let's create a circuit containing some non-CX multi-qubit gates:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import Circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(3)\n","circ.CRz(0.5, 0, 1)\n","circ.T(2)\n","circ.CSWAP(2, 0, 1)"]},{"cell_type":"markdown","metadata":{},"source":["In order to apply a pass to a circuit, we must first create a `CompilationUnit` from it. We can think of this as a 'bridge' between the circuit and the pass. The `CompilationUnit` is constructed from the circuit; the pass is applied to the `CompilationUnit`; and the transformed circuit is extracted from the `CompilationUnit`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.predicates import CompilationUnit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cu = CompilationUnit(circ)\n","pass1.apply(cu)\n","circ1 = cu.circuit"]},{"cell_type":"markdown","metadata":{},"source":["Let's have a look at the result of the transformation:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(circ1.get_commands())"]},{"cell_type":"markdown","metadata":{},"source":["## Predicates"]},{"cell_type":"markdown","metadata":{},"source":["Every `CompilationUnit` has associated with it a set of 'predicates', which describe target properties that can be checked against the circuit. There are many types of predicates available in `pytket`. For example, the `GateSetPredicate` checks whether all gates in a circuit belong to a particular set:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.predicates import GateSetPredicate\n","from pytket.circuit import OpType"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pred1 = GateSetPredicate({OpType.Rz, OpType.T, OpType.Tdg, OpType.H, OpType.CX})"]},{"cell_type":"markdown","metadata":{},"source":["When we construct a `CompilationUnit`, we may pass a list of target predicates as well as the circuit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cu = CompilationUnit(circ, [pred1])"]},{"cell_type":"markdown","metadata":{},"source":["To check whether the circuit associated to a `CompilationUnit` satisfies its target predicates, we can call the `check_all_predicates()` method:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cu.check_all_predicates()"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pass1.apply(cu)\n","cu.check_all_predicates()"]},{"cell_type":"markdown","metadata":{},"source":["We can also directly check whether a given circuit satisfies a given predicate, using the predicate's `verify()` method:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pred1.verify(circ1)"]},{"cell_type":"markdown","metadata":{},"source":["### In-place compilation"]},{"cell_type":"markdown","metadata":{},"source":["The example above produced a new circuit, leaving the original circuit untouched. It is also possible to apply a pass to a circuit in-place:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["DecomposeMultiQubitsCX().apply(circ)\n","print(circ.get_commands())"]},{"cell_type":"markdown","metadata":{},"source":["## Combining passes"]},{"cell_type":"markdown","metadata":{},"source":["There are various ways to combine the elementary passes into more complex ones.
\n","
\n","To combine several passes in sequence, we use a `SequencePass`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import SequencePass, OptimisePhaseGadgets"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["seqpass = SequencePass([DecomposeMultiQubitsCX(), OptimisePhaseGadgets()])"]},{"cell_type":"markdown","metadata":{},"source":["This pass will apply the two transforms in succession:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cu = CompilationUnit(circ)\n","seqpass.apply(cu)\n","circ1 = cu.circuit\n","print(circ1.get_commands())"]},{"cell_type":"markdown","metadata":{},"source":["The `apply()` method for an elementary pass returns a boolean indicating whether or not the pass had any effect on the circuit. For a `SequencePass`, the return value indicates whether _any_ of the constituent passes had some effect.
\n","
\n","A `RepeatPass` repeatedly calls `apply()` on a pass until it returns `False`, indicating that there was no effect:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import CommuteThroughMultis, RemoveRedundancies, RepeatPass"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["seqpass = SequencePass([CommuteThroughMultis(), RemoveRedundancies()])\n","reppass = RepeatPass(seqpass)"]},{"cell_type":"markdown","metadata":{},"source":["This pass will repeatedly apply `CommuteThroughMultis` (which commutes single-qubit operations through multi-qubit operations where possible towards the start of the circuit) and `RemoveRedundancies` (which cancels inverse pairs, merges coaxial rotations and removes redundant gates before measurement) until neither pass has any effect on the circuit.
\n","
\n","Let's use `pytket`'s built-in visualizer to see the effect on a 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":["circ = Circuit(3)\n","circ.X(0).Y(1).CX(0, 1).Z(0).Rx(1.3, 1).CX(0, 1).Rz(0.4, 0).Ry(0.53, 0).H(1).H(2).Rx(\n"," 1.5, 2\n",").Rx(0.5, 2).H(2)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cu = CompilationUnit(circ)\n","reppass.apply(cu)\n","circ1 = cu.circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ1)"]},{"cell_type":"markdown","metadata":{},"source":["If we want to repeat a pass until the circuit satisfies some desired property, we first define a boolean function to test for that property, and then pass this function to the constructor of a `RepeatUntilSatisfied` pass:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import RepeatUntilSatisfiedPass"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def no_CX(circ):\n"," return circ.n_gates_of_type(OpType.CX) == 0"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = (\n"," Circuit(2)\n"," .CX(0, 1)\n"," .X(1)\n"," .CX(0, 1)\n"," .X(1)\n"," .CX(0, 1)\n"," .X(1)\n"," .CX(0, 1)\n"," .Z(1)\n"," .CX(1, 0)\n"," .Z(1)\n"," .CX(1, 0)\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["custom_pass = RepeatUntilSatisfiedPass(seqpass, no_CX)\n","cu = CompilationUnit(circ)\n","custom_pass.apply(cu)\n","circ1 = cu.circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ1)"]},{"cell_type":"markdown","metadata":{},"source":["The `RepeatWithMetricPass` provides another way of generating more sophisticated passes. This is defined in terms of a cost function and another pass type; the pass is applied repeatedly until the cost function stops decreasing.
\n","
\n","For example, suppose we wish to associate a cost to each gate in out circuit, with $n$-qubit gates having a cost of $n^2$:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def cost(circ):\n"," return sum(pow(len(x.args), 2) for x in circ)"]},{"cell_type":"markdown","metadata":{},"source":["Let's construct a new circuit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(2)\n","circ.CX(0, 1).X(1).Y(0).CX(0, 1).X(1).Z(0).CX(0, 1).X(1).Y(0).CX(0, 1).Z(1).CX(1, 0).Z(\n"," 1\n",").X(0).CX(1, 0)"]},{"cell_type":"markdown","metadata":{},"source":["We will repeatedly apply `CommuteThroughMultis`, `DecomposeMultiQubitsCX` and `RemoveRedundancies` until the `cost` function stops decreasing:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import RepeatWithMetricPass"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pass1 = SequencePass(\n"," [CommuteThroughMultis(), DecomposeMultiQubitsCX(), RemoveRedundancies()]\n",")\n","pass2 = RepeatWithMetricPass(pass1, cost)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cu = CompilationUnit(circ)\n","pass2.apply(cu)\n","print(cu.circuit.get_commands())"]},{"cell_type":"markdown","metadata":{},"source":["## Targeting architectures"]},{"cell_type":"markdown","metadata":{},"source":["If we are given a target architecture, we can generate passes tailored to it.
\n","
\n","In `pytket` an architecture is defined by a connectivity graph, i.e. a list of pairs of qubits capable of executing two-qubit operations. For example, we can represent a 5-qubit linear architecture, with qubits labelled `n[i]`, as follows:"]},{"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":["n = [Node(\"n\", i) for i in range(5)]"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["arc = Architecture([[n[0], n[1]], [n[1], n[2]], [n[2], n[3]], [n[3], n[4]]])"]},{"cell_type":"markdown","metadata":{},"source":["Suppose we have a circuit that we wish to run on this architecture:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(5)\n","circ.CX(0, 1)\n","circ.H(0)\n","circ.Z(1)\n","circ.CX(0, 3)\n","circ.Rx(1.5, 3)\n","circ.CX(2, 4)\n","circ.X(2)\n","circ.CX(1, 4)\n","circ.CX(0, 4)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ)"]},{"cell_type":"markdown","metadata":{},"source":["A mapping pass lets us rewrite this circuit for our architecture:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import DefaultMappingPass"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["mapper = DefaultMappingPass(arc)\n","cu = CompilationUnit(circ)\n","mapper.apply(cu)\n","circ1 = cu.circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ1)"]},{"cell_type":"markdown","metadata":{},"source":["If we want to decompose all SWAP and BRIDGE gates to CX gates in the final circuit, we can use another pass:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import DecomposeSwapsToCXs"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pass1 = DecomposeSwapsToCXs(arc)\n","pass1.apply(cu)\n","circ2 = cu.circuit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["render_circuit_jupyter(circ2)"]},{"cell_type":"markdown","metadata":{},"source":["Note that the pass we just ran also performed some clean-up: the SWAP gate was decomposed into three CX gates, one of which was cancelled by a preceding CX gate; the cancelling gates were removed from the circuit.
\n","
\n","Every compilation pass has associated sets of preconditions and postconditions on the circuit. If all preconditions are satisfied before the pass, all postconditions are guaranteed to be satisfied afterwards. When we apply a pass to a circuit, we can optionally pass `SafetyMode.Audit` as the second parameter; this will tell the pass to check all preconditions explicitly. By default, there is only limited checking of preconditions and `pytket` relies on the programmer assuring these.
\n","
\n","For example, the `NoClassicalControl` predicate is a precondition of the `PauliSimp` pass. Let's add a classically controlled gate to our circuit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.passes import PauliSimp, SafetyMode\n","from pytket.circuit import Qubit, Bit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["q = [Qubit(\"q\", i) for i in range(5)]\n","c = Bit(\"c\")\n","circ.add_bit(c)\n","circ.Measure(q[3], c)\n","circ.CY(q[0], q[1], condition_bits=[c], condition_value=1)\n","cu = CompilationUnit(circ)\n","try:\n"," PauliSimp().apply(cu, safety_mode=SafetyMode.Audit)\n","except RuntimeError as e:\n"," print(\"Error:\", str(e))"]},{"cell_type":"markdown","metadata":{},"source":["The preconditions and postconditions of all the elementary predicates are documented in their string representations:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["PauliSimp()"]},{"cell_type":"markdown","metadata":{},"source":["## Backends and default passes"]},{"cell_type":"markdown","metadata":{},"source":["A `pytket` `Backend` may have a default compilation pass, which will guarantee that the circuit can run on it. This is given by the `default_compilation_pass` property. For example, the default pass for Qiskit's `AerBackend` just converts all gates to U1, U2, U3 and CX:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.qiskit import AerBackend"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["b = AerBackend()\n","b.default_compilation_pass"]},{"cell_type":"markdown","metadata":{},"source":["To compile a circuit using the default pass of a `Backend` we can simply use the `get_compiled_circuit()` method:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(2).X(0).Y(1).CRz(0.5, 1, 0)\n","circ1 = b.get_compiled_circuit(circ)\n","render_circuit_jupyter(circ1)"]},{"cell_type":"markdown","metadata":{},"source":["Every `Backend` will have a certain set of requirements that must be met by any circuit in order to run. These are exposed via the `required_predicates` property:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["b.required_predicates"]},{"cell_type":"markdown","metadata":{},"source":["We can test whether a given circuit satisfies these requirements using the `valid_circuit()` method:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["b.valid_circuit(circ)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["b.valid_circuit(circ1)"]}],"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": [
+ "# Compilation passes\n",
+ "\n",
+ "**Download this notebook - {nb-download}`compilation_example.ipynb`**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "There are numerous ways to optimize circuits in `pytket`. In this notebook we will introduce the basics of compilation passes and how to combine and apply them.\n",
+ "\n",
+ "We assume familiarity with the `pytket` `Circuit` class. The objective is to transform one `Circuit` into another, equivalent, `Circuit`, that:\n",
+ "* satisfies the connectivity constraints of a given architecture;\n",
+ "* satisfies some further user-defined constraints (such as restricted gate sets);\n",
+ "* minimizes some cost function (such as CX count)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Passes"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The basic mechanism of compilation is the 'pass', which is a transform that can be applied to a circuit. There is an extensive library of passes in `pytket`, and several standard ways in which they can be combined to form new passes. For example:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import DecomposeMultiQubitsCX\n",
+ "\n",
+ "pass1 = DecomposeMultiQubitsCX()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This pass converts all multi-qubit gates into CX and single-qubit gates. So let's create a circuit containing some non-CX multi-qubit gates:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import Circuit\n",
+ "\n",
+ "circ = Circuit(3)\n",
+ "circ.CRz(0.5, 0, 1)\n",
+ "circ.T(2)\n",
+ "circ.CSWAP(2, 0, 1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In order to apply a pass to a circuit, we must first create a `CompilationUnit` from it. We can think of this as a 'bridge' between the circuit and the pass. The `CompilationUnit` is constructed from the circuit; the pass is applied to the `CompilationUnit`; and the transformed circuit is extracted from the `CompilationUnit`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.predicates import CompilationUnit\n",
+ "\n",
+ "cu = CompilationUnit(circ)\n",
+ "pass1.apply(cu)\n",
+ "circ1 = cu.circuit"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's have a look at the result of the transformation:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(circ1.get_commands())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Predicates"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Every `CompilationUnit` has associated with it a set of 'predicates', which describe target properties that can be checked against the circuit. There are many types of predicates available in `pytket`. For example, the `GateSetPredicate` checks whether all gates in a circuit belong to a particular set:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.predicates import GateSetPredicate\n",
+ "from pytket.circuit import OpType\n",
+ "\n",
+ "pred1 = GateSetPredicate({OpType.Rz, OpType.T, OpType.Tdg, OpType.H, OpType.CX})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "When we construct a `CompilationUnit`, we may pass a list of target predicates as well as the circuit:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cu = CompilationUnit(circ, [pred1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To check whether the circuit associated to a `CompilationUnit` satisfies its target predicates, we can call the `check_all_predicates()` method:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cu.check_all_predicates()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "pass1.apply(cu)\n",
+ "cu.check_all_predicates()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also directly check whether a given circuit satisfies a given predicate, using the predicate's `verify()` method:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "pred1.verify(circ1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### In-place compilation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The example above produced a new circuit, leaving the original circuit untouched. It is also possible to apply a pass to a circuit in-place:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "DecomposeMultiQubitsCX().apply(circ)\n",
+ "print(circ.get_commands())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Combining passes"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "There are various ways to combine the elementary passes into more complex ones.
\n",
+ "
\n",
+ "To combine several passes in sequence, we use a `SequencePass`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import SequencePass, OptimisePhaseGadgets\n",
+ "\n",
+ "seqpass = SequencePass([DecomposeMultiQubitsCX(), OptimisePhaseGadgets()])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This pass will apply the two transforms in succession:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cu = CompilationUnit(circ)\n",
+ "seqpass.apply(cu)\n",
+ "circ1 = cu.circuit\n",
+ "print(circ1.get_commands())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `apply()` method for an elementary pass returns a boolean indicating whether or not the pass had any effect on the circuit. For a `SequencePass`, the return value indicates whether _any_ of the constituent passes had some effect.
\n",
+ "
\n",
+ "A `RepeatPass` repeatedly calls `apply()` on a pass until it returns `False`, indicating that there was no effect:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import CommuteThroughMultis, RemoveRedundancies, RepeatPass\n",
+ "\n",
+ "seqpass = SequencePass([CommuteThroughMultis(), RemoveRedundancies()])\n",
+ "reppass = RepeatPass(seqpass)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This pass will repeatedly apply `CommuteThroughMultis` (which commutes single-qubit operations through multi-qubit operations where possible towards the start of the circuit) and `RemoveRedundancies` (which cancels inverse pairs, merges coaxial rotations and removes redundant gates before measurement) until neither pass has any effect on the circuit.
\n",
+ "
\n",
+ "Let's use `pytket`'s built-in visualizer to see the effect on a circuit:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit.display import render_circuit_jupyter as draw\n",
+ "\n",
+ "circ = Circuit(3)\n",
+ "circ.X(0).Y(1).CX(0, 1).Z(0).Rx(1.3, 1).CX(0, 1).Rz(0.4, 0).Ry(0.53, 0).H(1).H(2).Rx(\n",
+ " 1.5, 2\n",
+ ").Rx(0.5, 2).H(2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(circ)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cu = CompilationUnit(circ)\n",
+ "reppass.apply(cu)\n",
+ "circ1 = cu.circuit"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(circ1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we want to repeat a pass until the circuit satisfies some desired property, we first define a boolean function to test for that property, and then pass this function to the constructor of a `RepeatUntilSatisfied` pass:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import RepeatUntilSatisfiedPass"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def no_CX(circ):\n",
+ " return circ.n_gates_of_type(OpType.CX) == 0"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ = (\n",
+ " Circuit(2)\n",
+ " .CX(0, 1)\n",
+ " .X(1)\n",
+ " .CX(0, 1)\n",
+ " .X(1)\n",
+ " .CX(0, 1)\n",
+ " .X(1)\n",
+ " .CX(0, 1)\n",
+ " .Z(1)\n",
+ " .CX(1, 0)\n",
+ " .Z(1)\n",
+ " .CX(1, 0)\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "custom_pass = RepeatUntilSatisfiedPass(seqpass, no_CX)\n",
+ "cu = CompilationUnit(circ)\n",
+ "custom_pass.apply(cu)\n",
+ "circ1 = cu.circuit"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(circ1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `RepeatWithMetricPass` provides another way of generating more sophisticated passes. This is defined in terms of a cost function and another pass type; the pass is applied repeatedly until the cost function stops decreasing.
\n",
+ "
\n",
+ "For example, suppose we wish to associate a cost to each gate in out circuit, with $n$-qubit gates having a cost of $n^2$:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def cost(circ):\n",
+ " return sum(pow(len(x.args), 2) for x in circ)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's construct a new circuit:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ = Circuit(2)\n",
+ "circ.CX(0, 1).X(1).Y(0).CX(0, 1).X(1).Z(0).CX(0, 1).X(1).Y(0).CX(0, 1).Z(1).CX(1, 0).Z(\n",
+ " 1\n",
+ ").X(0).CX(1, 0)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will repeatedly apply `CommuteThroughMultis`, `DecomposeMultiQubitsCX` and `RemoveRedundancies` until the `cost` function stops decreasing:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import RepeatWithMetricPass\n",
+ "\n",
+ "pass1 = SequencePass(\n",
+ " [CommuteThroughMultis(), DecomposeMultiQubitsCX(), RemoveRedundancies()]\n",
+ ")\n",
+ "pass2 = RepeatWithMetricPass(pass1, cost)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cu = CompilationUnit(circ)\n",
+ "pass2.apply(cu)\n",
+ "print(cu.circuit.get_commands())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Targeting architectures"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we are given a target architecture, we can generate passes tailored to it.\n",
+ "\n",
+ "In `pytket` an architecture is defined by a connectivity graph, i.e. a list of pairs of qubits capable of executing two-qubit operations. For example, we can represent a 5-qubit linear architecture, with qubits labelled `n[i]`, as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.architecture import Architecture\n",
+ "from pytket.circuit import Node\n",
+ "\n",
+ "n = [Node(\"n\", i) for i in range(5)]\n",
+ "\n",
+ "arc = Architecture([[n[0], n[1]], [n[1], n[2]], [n[2], n[3]], [n[3], n[4]]])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Suppose we have a circuit that we wish to run on this architecture:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ = Circuit(5)\n",
+ "circ.CX(0, 1)\n",
+ "circ.H(0)\n",
+ "circ.Z(1)\n",
+ "circ.CX(0, 3)\n",
+ "circ.Rx(1.5, 3)\n",
+ "circ.CX(2, 4)\n",
+ "circ.X(2)\n",
+ "circ.CX(1, 4)\n",
+ "circ.CX(0, 4)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(circ)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A mapping pass lets us rewrite this circuit for our architecture:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import DefaultMappingPass\n",
+ "\n",
+ "mapper = DefaultMappingPass(arc)\n",
+ "cu = CompilationUnit(circ)\n",
+ "mapper.apply(cu)\n",
+ "circ1 = cu.circuit"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(circ1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If we want to decompose all SWAP and BRIDGE gates to CX gates in the final circuit, we can use another pass:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import DecomposeSwapsToCXs\n",
+ "\n",
+ "pass1 = DecomposeSwapsToCXs(arc)\n",
+ "pass1.apply(cu)\n",
+ "circ2 = cu.circuit"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(circ2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that the pass we just ran also performed some clean-up: the SWAP gate was decomposed into three CX gates, one of which was cancelled by a preceding CX gate; the cancelling gates were removed from the circuit.
\n",
+ "
\n",
+ "Every compilation pass has associated sets of preconditions and postconditions on the circuit. If all preconditions are satisfied before the pass, all postconditions are guaranteed to be satisfied afterwards. When we apply a pass to a circuit, we can optionally pass `SafetyMode.Audit` as the second parameter; this will tell the pass to check all preconditions explicitly. By default, there is only limited checking of preconditions and `pytket` relies on the programmer assuring these.
\n",
+ "
\n",
+ "For example, the `NoClassicalControl` predicate is a precondition of the `PauliSimp` pass. Let's add a classically controlled gate to our circuit:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.passes import PauliSimp, SafetyMode\n",
+ "from pytket.circuit import Qubit, Bit\n",
+ "\n",
+ "q = [Qubit(\"q\", i) for i in range(5)]\n",
+ "c = Bit(\"c\")\n",
+ "circ.add_bit(c)\n",
+ "circ.Measure(q[3], c)\n",
+ "circ.CY(q[0], q[1], condition_bits=[c], condition_value=1)\n",
+ "cu = CompilationUnit(circ)\n",
+ "try:\n",
+ " PauliSimp().apply(cu, safety_mode=SafetyMode.Audit)\n",
+ "except RuntimeError as e:\n",
+ " print(\"Error:\", str(e))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The preconditions and postconditions of all the elementary predicates are documented in their string representations:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "PauliSimp()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Backends and default passes"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A `pytket` `Backend` may have a default compilation pass, which will guarantee that the circuit can run on it. This is given by the `default_compilation_pass` property. For example, the default pass for Qiskit's `AerBackend` just converts all gates to U1, U2, U3 and CX:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.extensions.qiskit import AerBackend\n",
+ "\n",
+ "b = AerBackend()\n",
+ "b.default_compilation_pass"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To compile a circuit using the default pass of a `Backend` we can simply use the `get_compiled_circuit()` method:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ = Circuit(2).X(0).Y(1).CRz(0.5, 1, 0)\n",
+ "circ1 = b.get_compiled_circuit(circ)\n",
+ "draw(circ1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Every `Backend` will have a certain set of requirements that must be met by any circuit in order to run. These are exposed via the `required_predicates` property:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "b.required_predicates"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can test whether a given circuit satisfies these requirements using the `valid_circuit()` method:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "b.valid_circuit(circ)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "b.valid_circuit(circ1)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": ".venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/examples/circuit_compilation/contextual_optimisation.ipynb b/docs/examples/circuit_compilation/contextual_optimisation.ipynb
index 287d524a..459a7380 100644
--- a/docs/examples/circuit_compilation/contextual_optimisation.ipynb
+++ b/docs/examples/circuit_compilation/contextual_optimisation.ipynb
@@ -39,6 +39,7 @@
"from pytket.circuit import Circuit\n",
"from random import random, randrange, seed\n",
"\n",
+ "\n",
"def random_sparse_ansatz(n_qubits, n_layers, p, rng_seed=None):\n",
" seed(rng_seed)\n",
" circ = Circuit(n_qubits)\n",
diff --git a/docs/examples/circuit_compilation/mapping_example.ipynb b/docs/examples/circuit_compilation/mapping_example.ipynb
index a004fda2..209e7d93 100644
--- a/docs/examples/circuit_compilation/mapping_example.ipynb
+++ b/docs/examples/circuit_compilation/mapping_example.ipynb
@@ -1 +1,1222 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Qubit mapping and routing\n","\n","**Download this notebook - {nb-download}`mapping_example.ipynb`**"]},{"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}
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Qubit mapping and routing\n",
+ "\n",
+ "**Download this notebook - {nb-download}`mapping_example.ipynb`**"
+ ]
+ },
+ {
+ "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 LinePlacement"
+ ]
+ },
+ {
+ "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
+}
diff --git a/docs/examples/circuit_construction/circuit_generation_example.ipynb b/docs/examples/circuit_construction/circuit_generation_example.ipynb
index e4ae1111..65e4eba6 100644
--- a/docs/examples/circuit_construction/circuit_generation_example.ipynb
+++ b/docs/examples/circuit_construction/circuit_generation_example.ipynb
@@ -1 +1,998 @@
-{"cells":[{"cell_type":"markdown","metadata":{},"source":["# Circuit generation\n","\n","**Download this notebook - {nb-download}`circuit_generation_example.ipynb`**"]},{"cell_type":"markdown","metadata":{},"source":["This notebook will provide a brief introduction to some of the more advanced methods of circuit generation available in `pytket`, including:\n","* how to address wires and registers;\n","* reading in circuits from QASM and Quipper ASCII files;\n","* various types of 'boxes';\n","* composition of circuits (both 'horizontally' and 'vertically');\n","* use of symbolic gate parameters;\n","* representation of classically controlled gates."]},{"cell_type":"markdown","metadata":{},"source":["## Wires, unit IDs and registers"]},{"cell_type":"markdown","metadata":{},"source":["Let's get started by constructing a circuit with 3 qubits and 2 classical bits:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import Circuit\n","from pytket.circuit.display import render_circuit_jupyter as draw"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(1, 2)\n","print(circ.qubits)\n","print(circ.bits)"]},{"cell_type":"markdown","metadata":{},"source":["The qubits have automatically been assigned to a register with name `q` and indices 0, 1 and 2, while the bits have been assigned to a register with name `c` and indices 0 and 1.\n","\n","We can give these units arbitrary names and indices of arbitrary dimension:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import Qubit"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["new_q1 = Qubit(\"alpha\", 0)\n","new_q2 = Qubit(\"beta\", 2, 1)\n","circ.add_qubit(new_q1)\n","circ.add_qubit(new_q2)\n","print(circ.qubits)"]},{"cell_type":"markdown","metadata":{},"source":["We can also add a new register of qubits in one go:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["delta_reg = circ.add_q_register(\"delta\", 2)\n","print(circ.qubits)"]},{"cell_type":"markdown","metadata":{},"source":["Similar commands are available for classical bits.\n","\n","We can add gates to the circuit as follows:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ.CX(delta_reg[0], delta_reg[1])"]},{"cell_type":"markdown","metadata":{},"source":["This command appends a CX gate with control `q[0]` and target `q[1]`. Note that the integer arguments are automatically converted to the default unit IDs. For simple circuits it is often easiest to stick to the default register and refer to the qubits by integers. To add gates to our own named units, we simply pass the `Qubit` (or classical `Bit`) as an argument. (We can't mix the two conventions in one command, however.)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ.H(new_q1)\n","circ.CX(Qubit(\"q\", 0), new_q2)\n","circ.Rz(0.5, new_q2)"]},{"cell_type":"markdown","metadata":{},"source":["Let's have a look at our circuit using the interactive circuit renderer:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit.display import render_circuit_jupyter as draw\n","\n","draw(circ)"]},{"cell_type":"markdown","metadata":{},"source":["## Exporting to and importing from standard formats"]},{"cell_type":"markdown","metadata":{},"source":["We can export a `Circuit` to a file in QASM format. Conversely, if we have such a file we can import it into `pytket`. There are some limitations on the circuits that can be converted: for example, multi-dimensional indices (as in `beta` and `gamma` above) are not allowed.\n","\n","Here is a simple example:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.qasm import circuit_from_qasm, circuit_to_qasm"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(3, 1)\n","circ.H(0)\n","circ.CX(0, 1)\n","circ.CX(1, 2)\n","circ.Rz(0.25, 2)\n","circ.Measure(2, 0)\n","draw(circ)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["qasmfile = \"../qasm/c.qasm\"\n","circuit_to_qasm(circ, qasmfile)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with open(qasmfile, encoding=\"utf-8\") as f:\n"," print(f.read())"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c1 = circuit_from_qasm(qasmfile)\n","circ == c1"]},{"cell_type":"markdown","metadata":{},"source":["We can also import files in the Quipper ASCII format:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.quipper import circuit_from_quipper"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["quipfile = \"c.quip\"\n","with open(quipfile, \"w\", encoding=\"utf-8\") as f:\n"," f.write(\n"," \"\"\"Inputs: 0:Qbit, 1:Qbit\n","QGate[\"W\"](0,1)\n","QGate[\"omega\"](1)\n","QGate[\"swap\"](0,1)\n","QGate[\"W\"]*(1,0)\n","Outputs: 0:Qbit, 1:Qbit\n","\"\"\"\n"," )"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = circuit_from_quipper(quipfile)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["Note that the Quipper gates that are not supported directly in `pytket` (`W` and `omega`) are translated into equivalent sequences of `pytket` gates. See the [pytket.quipper](inv:pytket:*:doc#quipper) docs for more.\n","\n","Quipper subroutines are also supported, corresponding to `CircBox` operations in `pytket`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["with open(quipfile, \"w\", encoding=\"utf-8\") as f:\n"," f.write(\n"," \"\"\"Inputs: 0:Qbit, 1:Qbit, 2:Qbit\n","QGate[\"H\"](0)\n","Subroutine(x2)[\"sub\", shape \"([Q,Q],())\"] (2,1) -> (2,1)\n","QGate[\"H\"](1)\n","Outputs: 0:Qbit, 1:Qbit, 2:Qbit\n","\n","Subroutine: \"sub\"\n","Shape: \"([Q,Q],())\"\n","Controllable: no\n","Inputs: 0:Qbit, 1:Qbit\n","QGate[\"Y\"](0)\n","QGate[\"not\"](1) with controls=[+0]\n","QGate[\"Z\"](1)\n","Outputs: 0:Qbit, 1:Qbit\n","\"\"\"\n"," )"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = circuit_from_quipper(quipfile)\n","\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["## Boxes in `pytket`"]},{"cell_type":"markdown","metadata":{},"source":["The `CircBox` is an example of a `pytket` 'box', which is a reusable encapsulation of a circuit inside another. We can recover the circuit 'inside' the box using the `CircBox.get_circuit()` method:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["cmds = c.get_commands()\n","boxed_circuit = cmds[1].op.get_circuit()\n","draw(boxed_circuit)"]},{"cell_type":"markdown","metadata":{},"source":["The `CircBox` is the most general type of box, implementing an arbitrary circuit. But `pytket` supports several other useful box types:\n","* {py:class}`~pytket.circuit.Unitary1qBox` (implementing an arbitrary $2 \\times 2$ unitary matrix);\n","* {py:class}`~pytket.circuit.Unitary2qBox` (implementing an arbitrary $4 \\times 4$ unitary matrix);\n","* {py:class}`~pytket.circuit.ExpBox` (implementing $e^{itA}$ for an arbitrary $4 \\times 4$ hermitian matrix $A$ and parameter $t$);\n","* {py:class}`~pytket.circuit.PauliExpBox` (implementing $e^{-\\frac{1}{2} i \\pi t (\\sigma_0 \\otimes \\sigma_1 \\otimes \\cdots)}$ for arbitrary Pauli operators $\\sigma_i \\in \\{\\mathrm{I}, \\mathrm{X}, \\mathrm{Y}, \\mathrm{Z}\\}$ and parameter $t$)."]},{"cell_type":"markdown","metadata":{},"source":["An example will illustrate how these various box types are added to a circuit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import numpy as np\n","from math import sqrt\n","from pytket.circuit import CircBox, ExpBox, PauliExpBox, Unitary1qBox, Unitary2qBox\n","from pytket.pauli import Pauli"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["boxycirc = Circuit(3)"]},{"cell_type":"markdown","metadata":{},"source":["Add a `CircBox`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["subcirc = Circuit(2, name=\"MY BOX\")\n","subcirc.X(0).Y(1).CZ(0, 1)\n","cbox = CircBox(subcirc)\n","boxycirc.add_gate(cbox, args=[Qubit(0), Qubit(1)])"]},{"cell_type":"markdown","metadata":{},"source":["Add a `Unitary1qBox`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["m1 = np.asarray([[1 / 2, sqrt(3) / 2], [sqrt(3) / 2, -1 / 2]])\n","m1box = Unitary1qBox(m1)\n","boxycirc.add_unitary1qbox(m1box, 2)"]},{"cell_type":"markdown","metadata":{},"source":["Add a `Unitary2qBox`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["m2 = np.asarray([[0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1], [1, 0, 0, 0]])\n","m2box = Unitary2qBox(m2)\n","boxycirc.add_unitary2qbox(m2box, 1, 2)"]},{"cell_type":"markdown","metadata":{},"source":["Add an `ExpBox`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["A = np.asarray(\n"," [[1, 2, 3, 4 + 1j], [2, 0, 1j, -1], [3, -1j, 2, 1j], [4 - 1j, -1, -1j, 1]]\n",")\n","ebox = ExpBox(A, 0.5)\n","boxycirc.add_expbox(ebox, 0, 1)"]},{"cell_type":"markdown","metadata":{},"source":["Add a `PauliExpBox`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pbox = PauliExpBox([Pauli.X, Pauli.Z, Pauli.X], 0.75)\n","boxycirc.add_gate(pbox, [0, 1, 2])"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["draw(boxycirc)"]},{"cell_type":"markdown","metadata":{},"source":["Try clicking on boxes in the diagram above to get information about the underlying subroutine."]},{"cell_type":"markdown","metadata":{},"source":["The `get_circuit()` method is available for all box types, and returns a `Circuit` object. For example if we look inside the `ExpBox`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["draw(ebox.get_circuit())"]},{"cell_type":"markdown","metadata":{},"source":["## Circuit composition"]},{"cell_type":"markdown","metadata":{},"source":["For more discussion of circuit composition see the corresponding section of the [manual](https://docs.quantinuum.com/tket/user-guide/manual/manual_circuit.html#composing-circuits)."]},{"cell_type":"markdown","metadata":{},"source":["Circuits can be composed either serially, whereby wires are joined together, or in parallel, using the `Circuit.append()` command.\n","\n","For a simple illustration of serial composition, let's create two circuits with compatible set of wires, and append the second to the first:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = Circuit(2)\n","c.CX(0, 1)\n","draw(c)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c1 = Circuit(2)\n","c1.CZ(1, 0)\n","draw(c1)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c.append(c1)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["In the above example, there was a one-to-one match between the unit IDs in the two circuits, and they were matched up accordingly. The same applied with named unit IDs:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["x, y = Qubit(\"x\"), Qubit(\"y\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = Circuit()\n","c.add_qubit(x)\n","c.add_qubit(y)\n","c.CX(x, y)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c1 = Circuit()\n","c1.add_qubit(x)\n","c1.add_qubit(y)\n","c1.CZ(y, x)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c.append(c1)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["If either circuit contains wires not matching any wires in the other, those are added to the other circuit before composition:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["z = Qubit(\"z\")\n","c1.add_qubit(z)\n","c1.CY(y, z)\n","c.append(c1)\n","print(c.qubits)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["If the sets of unit IDs for the two circuits are disjoint, then the composition is entirely parallel.\n","\n","What if we want to serially compose two circuits having different sets of `Qubit`? In that case, we can use the `rename_units()` method on one or other of them to bring them into line. This method takes a dictionary mapping current unit IDs to new one:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c2 = Circuit()\n","c2.add_q_register(\"w\", 3)\n","w = [Qubit(\"w\", i) for i in range(3)]\n","c2.H(w[0]).CX(w[0], w[1]).CRz(0.25, w[1], w[2])"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c.rename_units({x: w[0], y: w[1], z: w[2]})"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c.append(c2)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["## Symbolic parameters"]},{"cell_type":"markdown","metadata":{},"source":["Many of the gates supported by `pytket` are parametrized by one or more phase parameters, which represent rotations in multiples of $\\pi$. For example, $\\mathrm{Rz}(\\frac{1}{2})$ represents a quarter turn, i.e. a rotation of $\\pi/2$, about the Z axis. If we know the values of these parameters we can add the gates directly to our circuit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = Circuit(1)\n","c.Rz(0.5, 0)"]},{"cell_type":"markdown","metadata":{},"source":["However, we may wish to construct and manipulate circuits containing such parametrized gates without specifying the values. This allows us to do calculations in a general setting, only later substituting values for the parameters.\n","\n","Thus `pytket` allows us to specify any of the parameters as symbols. All manipulations (such as combination and cancellation of gates) are performed on the symbolic representation:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from sympy import Symbol\n","\n","a = Symbol(\"a\")\n","c.Rz(a, 0)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["When we apply any transformation to this circuit, the symbolic parameter is preserved in the result:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.transform import Transform\n","\n","Transform.RemoveRedundancies().apply(c)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["To substitute values for symbols, we use the `Circuit.symbol_substitution()` method, supplying a dictionary from symbols to values:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c.symbol_substitution({a: 0.75})"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["We can also substitute symbols for other symbols:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["b = Symbol(\"b\")\n","c = Circuit(1)\n","c.Rz(a + b, 0)\n","c.symbol_substitution({b: 2 * a})\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["## Custom gates"]},{"cell_type":"markdown","metadata":{},"source":["We can define custom parametrized gates in `pytket` by first setting up a circuit containing symbolic parameters and then converting this to a parametrized operation type:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import CustomGateDef\n","\n","a = Symbol(\"a\")\n","b = Symbol(\"b\")\n","setup = Circuit(3)\n","setup.CX(0, 1)\n","setup.Rz(a + 0.5, 2)\n","setup.CRz(b, 0, 2)\n","my_gate = CustomGateDef.define(\"g\", setup, [a, b])\n","c = Circuit(4)\n","c.add_custom_gate(my_gate, [0.2, 1.3], [0, 3, 1])\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["Custom gates can also receive symbolic parameters:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["x = Symbol(\"x\")\n","c.add_custom_gate(my_gate, [x, 1.0], [0, 1, 2])\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["## Decomposing boxes and custom gates"]},{"cell_type":"markdown","metadata":{},"source":["Having defined a circuit containing custom gates, we may now want to decompose it into elementary gates. The `DecomposeBoxes()` transform allows us to do this:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["Transform.DecomposeBoxes().apply(c)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["The same transform works on circuits composed of arbitrary boxes. Let's try it on a copy of the circuit we built up earlier out of various box types."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = boxycirc.copy()\n","Transform.DecomposeBoxes().apply(c)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["Note that the unitaries have been decomposed into elementary gates."]},{"cell_type":"markdown","metadata":{},"source":["## Classical controls"]},{"cell_type":"markdown","metadata":{},"source":["Most of the examples above involve only pure quantum gates. However, `pytket` can also represent gates whose operation is conditional on one or more classical inputs.\n","\n","For example, suppose we want to run the complex circuit `c` we've just constructed, then measure qubits 0 and 1, and finally apply an $\\mathrm{Rz}(\\frac{1}{2})$ rotation to qubit 2 if and only if the measurements were 0 and 1 respectively.\n","\n","First, we'll add two classical wires to the circuit to store the measurement results:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.circuit import Bit\n","\n","c.add_c_register(\"m\", 2)\n","m = [Bit(\"m\", i) for i in range(2)]"]},{"cell_type":"markdown","metadata":{},"source":["Classically conditioned operations depend on all their inputs being 1. Since we want to condition on `m[0]` being 0, we must first apply an X gate to its qubit, and then measure:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["q = [Qubit(\"q\", i) for i in range(3)]\n","c.X(q[0])\n","c.Measure(q[0], m[0])\n","c.Measure(q[1], m[1])"]},{"cell_type":"markdown","metadata":{},"source":["Finally we add the classically conditioned Rz operation"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c.Rz(0.5, q[2], condition_bits=[m[0], m[1]], condition_value=3)\n","draw(c)"]},{"cell_type":"markdown","metadata":{},"source":["Note that many of the transforms and compilation passes will not accept circuits that contain classical controls."]}],"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": [
+ "# Circuit generation\n",
+ "\n",
+ "**Download this notebook - {nb-download}`circuit_generation_example.ipynb`**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This notebook will provide a brief introduction to some of the more advanced methods of circuit generation available in `pytket`, including:\n",
+ "* how to address wires and registers;\n",
+ "* reading in circuits from QASM and Quipper ASCII files;\n",
+ "* various types of 'boxes';\n",
+ "* composition of circuits (both 'horizontally' and 'vertically');\n",
+ "* use of symbolic gate parameters;\n",
+ "* representation of classically controlled gates."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Wires, unit IDs and registers"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's get started by constructing a circuit with 3 qubits and 2 classical bits:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import Circuit\n",
+ "from pytket.circuit.display import render_circuit_jupyter as draw"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ = Circuit(1, 2)\n",
+ "print(circ.qubits)\n",
+ "print(circ.bits)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The qubits have automatically been assigned to a register with name `q` and indices 0, 1 and 2, while the bits have been assigned to a register with name `c` and indices 0 and 1.\n",
+ "\n",
+ "We can give these units arbitrary names and indices of arbitrary dimension:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import Qubit"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "new_q1 = Qubit(\"alpha\", 0)\n",
+ "new_q2 = Qubit(\"beta\", 2, 1)\n",
+ "circ.add_qubit(new_q1)\n",
+ "circ.add_qubit(new_q2)\n",
+ "print(circ.qubits)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also add a new register of qubits in one go:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "delta_reg = circ.add_q_register(\"delta\", 2)\n",
+ "print(circ.qubits)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Similar commands are available for classical bits.\n",
+ "\n",
+ "We can add gates to the circuit as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ.CX(delta_reg[0], delta_reg[1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This command appends a CX gate with control `q[0]` and target `q[1]`. Note that the integer arguments are automatically converted to the default unit IDs. For simple circuits it is often easiest to stick to the default register and refer to the qubits by integers. To add gates to our own named units, we simply pass the `Qubit` (or classical `Bit`) as an argument. (We can't mix the two conventions in one command, however.)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ.H(new_q1)\n",
+ "circ.CX(Qubit(\"q\", 0), new_q2)\n",
+ "circ.Rz(0.5, new_q2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's have a look at our circuit using the interactive circuit renderer:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\n",
+ "draw(circ)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Exporting to and importing from standard formats"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can export a `Circuit` to a file in QASM format. Conversely, if we have such a file we can import it into `pytket`. There are some limitations on the circuits that can be converted: for example, multi-dimensional indices (as in `beta` and `gamma` above) are not allowed.\n",
+ "\n",
+ "Here is a simple example:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.qasm import circuit_from_qasm, circuit_to_qasm"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "circ = Circuit(3, 1)\n",
+ "circ.H(0)\n",
+ "circ.CX(0, 1)\n",
+ "circ.CX(1, 2)\n",
+ "circ.Rz(0.25, 2)\n",
+ "circ.Measure(2, 0)\n",
+ "draw(circ)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "qasmfile = \"../qasm/c.qasm\"\n",
+ "circuit_to_qasm(circ, qasmfile)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "with open(qasmfile, encoding=\"utf-8\") as f:\n",
+ " print(f.read())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c1 = circuit_from_qasm(qasmfile)\n",
+ "circ == c1"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also import files in the Quipper ASCII format:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.quipper import circuit_from_quipper"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "quipfile = \"c.quip\"\n",
+ "with open(quipfile, \"w\", encoding=\"utf-8\") as f:\n",
+ " f.write(\n",
+ " \"\"\"Inputs: 0:Qbit, 1:Qbit\n",
+ "QGate[\"W\"](0,1)\n",
+ "QGate[\"omega\"](1)\n",
+ "QGate[\"swap\"](0,1)\n",
+ "QGate[\"W\"]*(1,0)\n",
+ "Outputs: 0:Qbit, 1:Qbit\n",
+ "\"\"\"\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c = circuit_from_quipper(quipfile)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that the Quipper gates that are not supported directly in `pytket` (`W` and `omega`) are translated into equivalent sequences of `pytket` gates. See the [pytket.quipper](inv:pytket:*:doc#quipper) docs for more.\n",
+ "\n",
+ "Quipper subroutines are also supported, corresponding to `CircBox` operations in `pytket`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "with open(quipfile, \"w\", encoding=\"utf-8\") as f:\n",
+ " f.write(\n",
+ " \"\"\"Inputs: 0:Qbit, 1:Qbit, 2:Qbit\n",
+ "QGate[\"H\"](0)\n",
+ "Subroutine(x2)[\"sub\", shape \"([Q,Q],())\"] (2,1) -> (2,1)\n",
+ "QGate[\"H\"](1)\n",
+ "Outputs: 0:Qbit, 1:Qbit, 2:Qbit\n",
+ "\n",
+ "Subroutine: \"sub\"\n",
+ "Shape: \"([Q,Q],())\"\n",
+ "Controllable: no\n",
+ "Inputs: 0:Qbit, 1:Qbit\n",
+ "QGate[\"Y\"](0)\n",
+ "QGate[\"not\"](1) with controls=[+0]\n",
+ "QGate[\"Z\"](1)\n",
+ "Outputs: 0:Qbit, 1:Qbit\n",
+ "\"\"\"\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c = circuit_from_quipper(quipfile)\n",
+ "\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Boxes in `pytket`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `CircBox` is an example of a `pytket` 'box', which is a reusable encapsulation of a circuit inside another. We can recover the circuit 'inside' the box using the `CircBox.get_circuit()` method:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cmds = c.get_commands()\n",
+ "boxed_circuit = cmds[1].op.get_circuit()\n",
+ "draw(boxed_circuit)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `CircBox` is the most general type of box, implementing an arbitrary circuit. But `pytket` supports several other useful box types:\n",
+ "* {py:class}`~pytket.circuit.Unitary1qBox` (implementing an arbitrary $2 \\times 2$ unitary matrix);\n",
+ "* {py:class}`~pytket.circuit.Unitary2qBox` (implementing an arbitrary $4 \\times 4$ unitary matrix);\n",
+ "* {py:class}`~pytket.circuit.ExpBox` (implementing $e^{itA}$ for an arbitrary $4 \\times 4$ hermitian matrix $A$ and parameter $t$);\n",
+ "* {py:class}`~pytket.circuit.PauliExpBox` (implementing $e^{-\\frac{1}{2} i \\pi t (\\sigma_0 \\otimes \\sigma_1 \\otimes \\cdots)}$ for arbitrary Pauli operators $\\sigma_i \\in \\{\\mathrm{I}, \\mathrm{X}, \\mathrm{Y}, \\mathrm{Z}\\}$ and parameter $t$)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "An example will illustrate how these various box types are added to a circuit:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "from math import sqrt\n",
+ "from pytket.circuit import CircBox, ExpBox, PauliExpBox, Unitary1qBox, Unitary2qBox\n",
+ "from pytket.pauli import Pauli"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "boxycirc = Circuit(3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Add a `CircBox`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "subcirc = Circuit(2, name=\"MY BOX\")\n",
+ "subcirc.X(0).Y(1).CZ(0, 1)\n",
+ "cbox = CircBox(subcirc)\n",
+ "boxycirc.add_gate(cbox, args=[Qubit(0), Qubit(1)])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Add a `Unitary1qBox`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m1 = np.asarray([[1 / 2, sqrt(3) / 2], [sqrt(3) / 2, -1 / 2]])\n",
+ "m1box = Unitary1qBox(m1)\n",
+ "boxycirc.add_unitary1qbox(m1box, 2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Add a `Unitary2qBox`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "m2 = np.asarray([[0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1], [1, 0, 0, 0]])\n",
+ "m2box = Unitary2qBox(m2)\n",
+ "boxycirc.add_unitary2qbox(m2box, 1, 2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Add an `ExpBox`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "A = np.asarray(\n",
+ " [[1, 2, 3, 4 + 1j], [2, 0, 1j, -1], [3, -1j, 2, 1j], [4 - 1j, -1, -1j, 1]]\n",
+ ")\n",
+ "ebox = ExpBox(A, 0.5)\n",
+ "boxycirc.add_expbox(ebox, 0, 1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Add a `PauliExpBox`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "pbox = PauliExpBox([Pauli.X, Pauli.Z, Pauli.X], 0.75)\n",
+ "boxycirc.add_gate(pbox, [0, 1, 2])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(boxycirc)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Try clicking on boxes in the diagram above to get information about the underlying subroutine."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `get_circuit()` method is available for all box types, and returns a `Circuit` object. For example if we look inside the `ExpBox`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(ebox.get_circuit())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Circuit composition"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For more discussion of circuit composition see the corresponding section of the [manual](https://docs.quantinuum.com/tket/user-guide/manual/manual_circuit.html#composing-circuits)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Circuits can be composed either serially, whereby wires are joined together, or in parallel, using the `Circuit.append()` command.\n",
+ "\n",
+ "For a simple illustration of serial composition, let's create two circuits with compatible set of wires, and append the second to the first:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c = Circuit(2)\n",
+ "c.CX(0, 1)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c1 = Circuit(2)\n",
+ "c1.CZ(1, 0)\n",
+ "draw(c1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c.append(c1)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the above example, there was a one-to-one match between the unit IDs in the two circuits, and they were matched up accordingly. The same applied with named unit IDs:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x, y = Qubit(\"x\"), Qubit(\"y\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c = Circuit()\n",
+ "c.add_qubit(x)\n",
+ "c.add_qubit(y)\n",
+ "c.CX(x, y)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c1 = Circuit()\n",
+ "c1.add_qubit(x)\n",
+ "c1.add_qubit(y)\n",
+ "c1.CZ(y, x)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c.append(c1)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If either circuit contains wires not matching any wires in the other, those are added to the other circuit before composition:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "z = Qubit(\"z\")\n",
+ "c1.add_qubit(z)\n",
+ "c1.CY(y, z)\n",
+ "c.append(c1)\n",
+ "print(c.qubits)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If the sets of unit IDs for the two circuits are disjoint, then the composition is entirely parallel.\n",
+ "\n",
+ "What if we want to serially compose two circuits having different sets of `Qubit`? In that case, we can use the `rename_units()` method on one or other of them to bring them into line. This method takes a dictionary mapping current unit IDs to new one:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c2 = Circuit()\n",
+ "c2.add_q_register(\"w\", 3)\n",
+ "w = [Qubit(\"w\", i) for i in range(3)]\n",
+ "c2.H(w[0]).CX(w[0], w[1]).CRz(0.25, w[1], w[2])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c.rename_units({x: w[0], y: w[1], z: w[2]})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c.append(c2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Symbolic parameters"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Many of the gates supported by `pytket` are parametrized by one or more phase parameters, which represent rotations in multiples of $\\pi$. For example, $\\mathrm{Rz}(\\frac{1}{2})$ represents a quarter turn, i.e. a rotation of $\\pi/2$, about the Z axis. If we know the values of these parameters we can add the gates directly to our circuit:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c = Circuit(1)\n",
+ "c.Rz(0.5, 0)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "However, we may wish to construct and manipulate circuits containing such parametrized gates without specifying the values. This allows us to do calculations in a general setting, only later substituting values for the parameters.\n",
+ "\n",
+ "Thus `pytket` allows us to specify any of the parameters as symbols. All manipulations (such as combination and cancellation of gates) are performed on the symbolic representation:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from sympy import Symbol\n",
+ "\n",
+ "a = Symbol(\"a\")\n",
+ "c.Rz(a, 0)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "When we apply any transformation to this circuit, the symbolic parameter is preserved in the result:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.transform import Transform\n",
+ "\n",
+ "Transform.RemoveRedundancies().apply(c)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To substitute values for symbols, we use the `Circuit.symbol_substitution()` method, supplying a dictionary from symbols to values:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c.symbol_substitution({a: 0.75})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can also substitute symbols for other symbols:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "b = Symbol(\"b\")\n",
+ "c = Circuit(1)\n",
+ "c.Rz(a + b, 0)\n",
+ "c.symbol_substitution({b: 2 * a})\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Custom gates"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can define custom parametrized gates in `pytket` by first setting up a circuit containing symbolic parameters and then converting this to a parametrized operation type:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import CustomGateDef\n",
+ "\n",
+ "a = Symbol(\"a\")\n",
+ "b = Symbol(\"b\")\n",
+ "setup = Circuit(3)\n",
+ "setup.CX(0, 1)\n",
+ "setup.Rz(a + 0.5, 2)\n",
+ "setup.CRz(b, 0, 2)\n",
+ "my_gate = CustomGateDef.define(\"g\", setup, [a, b])\n",
+ "c = Circuit(4)\n",
+ "c.add_custom_gate(my_gate, [0.2, 1.3], [0, 3, 1])\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Custom gates can also receive symbolic parameters:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "x = Symbol(\"x\")\n",
+ "c.add_custom_gate(my_gate, [x, 1.0], [0, 1, 2])\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Decomposing boxes and custom gates"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Having defined a circuit containing custom gates, we may now want to decompose it into elementary gates. The `DecomposeBoxes()` transform allows us to do this:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Transform.DecomposeBoxes().apply(c)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The same transform works on circuits composed of arbitrary boxes. Let's try it on a copy of the circuit we built up earlier out of various box types."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c = boxycirc.copy()\n",
+ "Transform.DecomposeBoxes().apply(c)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that the unitaries have been decomposed into elementary gates."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Classical controls"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Most of the examples above involve only pure quantum gates. However, `pytket` can also represent gates whose operation is conditional on one or more classical inputs.\n",
+ "\n",
+ "For example, suppose we want to run the complex circuit `c` we've just constructed, then measure qubits 0 and 1, and finally apply an $\\mathrm{Rz}(\\frac{1}{2})$ rotation to qubit 2 if and only if the measurements were 0 and 1 respectively.\n",
+ "\n",
+ "First, we'll add two classical wires to the circuit to store the measurement results:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pytket.circuit import Bit\n",
+ "\n",
+ "c.add_c_register(\"m\", 2)\n",
+ "m = [Bit(\"m\", i) for i in range(2)]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Classically conditioned operations depend on all their inputs being 1. Since we want to condition on `m[0]` being 0, we must first apply an X gate to its qubit, and then measure:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "q = [Qubit(\"q\", i) for i in range(3)]\n",
+ "c.X(q[0])\n",
+ "c.Measure(q[0], m[0])\n",
+ "c.Measure(q[1], m[1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally we add the classically conditioned Rz operation"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "c.Rz(0.5, q[2], condition_bits=[m[0], m[1]], condition_value=3)\n",
+ "draw(c)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that many of the transforms and compilation passes will not accept circuits that contain classical controls."
+ ]
+ }
+ ],
+ "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/docs/examples/creating_backends_exercise.py b/docs/examples/creating_backends_exercise.py
index a783ad53..146ca8c4 100644
--- a/docs/examples/creating_backends_exercise.py
+++ b/docs/examples/creating_backends_exercise.py
@@ -1,4 +1,4 @@
-from pytket.circuit import OpType, Qubit, Bit, UnitID
+from pytket.circuit import Qubit, Bit, UnitID
from pytket.pauli import Pauli, QubitPauliString
from binarytree import Node
diff --git a/docs/examples/oxfordQIS.py b/docs/examples/oxfordQIS.py
index 9400c10d..953356ea 100644
--- a/docs/examples/oxfordQIS.py
+++ b/docs/examples/oxfordQIS.py
@@ -3,13 +3,14 @@
from pytket.circuit import Circuit, PauliExpBox, Pauli
from pytket.predicates import CompilationUnit
-from pytket.passes import DecomposeBoxes, PauliSimp, SequencePass
+from pytket.passes import DecomposeBoxes
from pytket.utils import expectation_from_counts
-from pytket.extensions.qiskit import AerBackend, AerStateBackend
+from pytket.extensions.qiskit import AerBackend
from sympy import Symbol
from scipy.optimize import minimize
-from openfermion import QubitOperator, FermionOperator, jordan_wigner
+from openfermion import QubitOperator, FermionOperator
+
# Generate a parametric ansatz
def h2_JW_sto3g_ansatz():