{"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\n","from typing import List"]},{"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"," :type qubits: List[Qubit]\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"," :type qps: QubitPauliString\n"," :param angle: Angle of rotation in radians\n"," :type angle: float\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"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["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"," :type qubits: List[Qubit]\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"," :type qps: QubitPauliString\n"," :param angle: Angle of rotation in radians\n"," :type angle: float\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"," :type circ: MyCircuit\n"," :return: The final statevector\n"," :rtype: np.ndarray\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"," :type tkc: Circuit\n"," :return: An equivalent MyCircuit object\n"," :rtype: MyCircuit\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 abstract `Backend` [API reference](https://cqcl.github.io/tket/pytket/api/backends.html#pytket.backends.Backend)."]},{"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"," :rtype: List[Predicate]\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"," :type optimisation_level: int, optional\n"," :return: Compilation pass guaranteeing required predicates.\n"," :rtype: BasePass\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"," :rtype: _ResultIdTuple\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"," :type circuits: Iterable[Circuit]\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"," :type n_shots: Optional[int], optional\n"," :param valid_check: Explicitly check that all circuits satisfy all required\n"," predicates to run on the backend. Defaults to True\n"," :type valid_check: bool, optional\n"," :return: Handles to results for each input circuit, as an interable in\n"," the same order as the circuits.\n"," :rtype: List[ResultHandle]\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"," :type circ: MyCircuit\n"," :param qubits: The set of qubits to measure\n"," :type qubits: Set[Qubit]\n"," :param n_shots: The number of samples to take\n"," :type n_shots: int\n"," :param seed: Seed for the random number generator, defaults to no seed\n"," :type seed: Optional[int], optional\n"," :return: Table of shots; each row is a shot, columns are qubit readouts in ascending Qubit order\n"," :rtype: np.ndarray\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"," :type tkc: Circuit\n"," :return: An equivalent MyCircuit object and a map from measured Qubit to the Bit containing the result\n"," :rtype: Tuple[MyCircuit, Dict[Qubit, Bit]]\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"," :rtype: List[Predicate]\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"," :type circuits: Iterable[Circuit]\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"," :type n_shots: Optional[int], optional\n"," :param valid_check: Explicitly check that all circuits satisfy all required\n"," predicates to run on the backend. Defaults to True\n"," :type valid_check: bool, optional\n"," :return: Handles to results for each input circuit, as an interable in\n"," the same order as the circuits.\n"," :rtype: List[ResultHandle]\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}