{"cells":[{"cell_type":"markdown","metadata":{},"source":["# TKET `Backend` tutorial\n","\n","**Download this notebook -> {nb-download}`backends_example.ipynb`**"]},{"cell_type":"markdown","metadata":{},"source":["This example shows how to use `pytket` to execute quantum circuits on both simulators and real devices, and how to interpret the results. As tket is designed to be platform-agnostic, we have unified the interfaces of different providers as much as possible into the `Backend` class for maximum portability of code."]},{"cell_type":"markdown","metadata":{},"source":["For the full list of supported backends see the pytket [extensions index page](https://tket.quantinuum.com/api-docs/extensions)."]},{"cell_type":"markdown","metadata":{},"source":["In this notebook we will focus on the Aer, IBMQ and ProjectQ backends.
\n","
\n","To get started, we must install the core pytket package and the subpackages required to interface with the desired providers. We will also need the `QubitOperator` class from `openfermion` to construct operators for a later example. To get everything run the following in shell:
\n","
\n","`pip install pytket pytket-qiskit pytket-projectq openfermion`
\n","
\n","First, import the backends that we will be demonstrating."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.extensions.qiskit import (\n"," AerStateBackend,\n"," AerBackend,\n"," AerUnitaryBackend,\n"," IBMQBackend,\n"," IBMQEmulatorBackend,\n",")\n","from pytket.extensions.projectq import ProjectQBackend"]},{"cell_type":"markdown","metadata":{},"source":["We are also going to be making a circuit to run on these backends, so import the `Circuit` class."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket import Circuit, Qubit"]},{"cell_type":"markdown","metadata":{},"source":["Below we generate a circuit which will produce a Bell state, assuming the qubits are all initialised in the |0> state:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ = Circuit(2)\n","circ.H(0)\n","circ.CX(0, 1)"]},{"cell_type":"markdown","metadata":{},"source":["As a sanity check, we will use the `AerStateBackend` to verify that `circ` does actually produce a Bell state.
\n","
\n","To submit a circuit for excution on a backend we can use `process_circuit` with appropriate arguments. If we have multiple circuits to excecute, we can use `process_circuits` (note the plural), which will attempt to batch up the circuits if possible. Both methods return a `ResultHandle` object per submitted `Circuit` which you can use with result retrieval methods to get the result type you want (as long as that result type is supported by the backend).
\n","
\n","Calling `get_state` will return a `numpy` array corresponding to the statevector.
\n","
\n","This style of usage is used consistently in the `pytket` backends."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["aer_state_b = AerStateBackend()\n","state_handle = aer_state_b.process_circuit(circ)\n","statevector = aer_state_b.get_result(state_handle).get_state()\n","print(statevector)"]},{"cell_type":"markdown","metadata":{},"source":["As we can see, the output state vector $\\lvert \\psi_{\\mathrm{circ}}\\rangle$ is $(\\lvert00\\rangle + \\lvert11\\rangle)/\\sqrt2$.
\n","
\n","This is a symmetric state. For non-symmetric states, we default to an ILO-BE format (increasing lexicographic order of (qu)bit ids, big-endian), but an alternative convention can be specified when retrieving results from backends. See the docs for the `BasisOrder` enum for more information."]},{"cell_type":"markdown","metadata":{},"source":["A lesser-used simulator available through Qiskit Aer is their unitary simulator. This will be somewhat more expensive to run, but returns the full unitary matrix for the provided circuit. This is useful in the design of small subcircuits that will be used multiple times within other larger circuits - statevector simulators will only test that they act correctly on the $\\lvert 0 \\rangle^{\\otimes n}$ state, which is not enough to guarantee the circuit's correctness.
\n","
\n","The `AerUnitaryBackend` provides a convenient access point for this simulator for use with `pytket` circuits. The unitary of the circuit can be retrieved from backends that support it using the `BackendResult.get_unitary` interface. In this example, we chose to use `Backend.run_circuit`, which is equivalent to calling `process_circuit` followed by `get_result`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["aer_unitary_b = AerUnitaryBackend()\n","result = aer_unitary_b.run_circuit(circ)\n","print(result.get_unitary())"]},{"cell_type":"markdown","metadata":{},"source":["Note that state vector and unitary simulations are also available in pytket directly. In general, we recommend you use these unless you require another Backend explicitly."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["statevector = circ.get_statevector()\n","unitary = circ.get_unitary()"]},{"cell_type":"markdown","metadata":{},"source":["Now suppose we want to measure this Bell state to get some actual results out, so let's append some `Measure` gates to the circuit. The `Circuit` class has the `measure_all` utility function which appends `Measure` gates on every qubit. All of these results will be written to the default classical register ('c'). This function will automatically add the classical bits to the circuit if they are not already there."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ.measure_all()"]},{"cell_type":"markdown","metadata":{},"source":["We can get some measured counts out from the `AerBackend`, which is an interface to the Qiskit Aer QASM simulator. Suppose we would like to get 10 shots out (10 repeats of the circuit and measurement). We can seed the simulator's random-number generator in order to make the results reproducible, using an optional keyword argument to `process_circuit`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["aer_b = AerBackend()\n","handle = aer_b.process_circuit(circ, n_shots=10, seed=1)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["counts = aer_b.get_result(handle).get_counts()\n","print(counts)"]},{"cell_type":"markdown","metadata":{},"source":["What happens if we simulate some noise in our imagined device, using the Qiskit Aer noise model?"]},{"cell_type":"markdown","metadata":{},"source":["To investigate this, we will require an import from Qiskit. For more information about noise modelling using Qiskit Aer, see the [Qiskit device noise](https://qiskit.org/documentation/apidoc/aer_noise.html) documentation."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from qiskit_aer.noise import NoiseModel"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["my_noise_model = NoiseModel()\n","readout_error = 0.2\n","for q in range(2):\n"," my_noise_model.add_readout_error(\n"," [[1 - readout_error, readout_error], [readout_error, 1 - readout_error]], [q]\n"," )"]},{"cell_type":"markdown","metadata":{},"source":["This simple noise model gives a 20% chance that, upon measurement, a qubit that would otherwise have been measured as $0$ would instead be measured as $1$, and vice versa. Let's see what our shot table looks like with this model:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["noisy_aer_b = AerBackend(my_noise_model)\n","noisy_handle = noisy_aer_b.process_circuit(circ, n_shots=10, seed=1, valid_check=False)\n","noisy_counts = noisy_aer_b.get_result(noisy_handle).get_counts()\n","print(noisy_counts)"]},{"cell_type":"markdown","metadata":{},"source":["We now have some spurious $01$ and $10$ measurements, which could never happen when measuring a Bell state on a noiseless device.
\n","
\n","The `AerBackend` class can accept any Qiskit noise model.
\n","
\n","All backends expose a generic `get_result` method which takes a `ResultHandle` and returns the respective result in the form of a `BackendResult` object. This object may hold measured results in the form of shots or counts, or an exact statevector from simulation. Measured results are stored as `OutcomeArray` objects, which compresses measured bit values into 8-bit integers. We can extract the bitwise values using `to_readouts`.
\n","
\n","Instead of an assumed ILO or DLO convention, we can use this object to request only the `Bit` measurements we want, in the order we want. Let's try reversing the bits of the noisy results."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["backend_result = noisy_aer_b.get_result(noisy_handle)\n","bits = circ.bits"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["outcomes = backend_result.get_counts([bits[1], bits[0]])\n","print(outcomes)"]},{"cell_type":"markdown","metadata":{},"source":["`BackendResult` objects can be natively serialized to and deserialized from a dictionary. This dictionary can be immediately dumped to `json` for storing results."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends.backendresult import BackendResult"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["result_dict = backend_result.to_dict()\n","print(result_dict)\n","print(BackendResult.from_dict(result_dict).get_counts())"]},{"cell_type":"markdown","metadata":{},"source":["The last simulator we will demonstrate is the `ProjectQBackend`. ProjectQ offers fast simulation of quantum circuits with built-in support for fast expectation values from operators. The `ProjectQBackend` exposes this functionality to take in OpenFermion `QubitOperator` instances. These are convertible to and from `QubitPauliOperator` instances in Pytket.
\n","
\n","Note: ProjectQ can also produce statevectors in the style of `AerStateBackend`, and similarly Aer backends can calculate expectation values directly, consult the relevant documentation to see more.
\n","
\n","Let's create an OpenFermion `QubitOperator` object and a new circuit:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import openfermion as of"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["hamiltonian = 0.5 * of.QubitOperator(\"X0 X2\") + 0.3 * of.QubitOperator(\"Z0\")"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circ2 = Circuit(3)\n","circ2.Y(0)\n","circ2.H(1)\n","circ2.Rx(0.3, 2)"]},{"cell_type":"markdown","metadata":{},"source":["We convert the OpenFermion Hamiltonian into a pytket QubitPauliOperator:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.pauli import Pauli, QubitPauliString\n","from pytket.utils.operators import QubitPauliOperator"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pauli_sym = {\"I\": Pauli.I, \"X\": Pauli.X, \"Y\": Pauli.Y, \"Z\": Pauli.Z}"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def qps_from_openfermion(paulis):\n"," \"\"\"Convert OpenFermion tensor of Paulis to pytket QubitPauliString.\"\"\"\n"," qlist = []\n"," plist = []\n"," for q, p in paulis:\n"," qlist.append(Qubit(q))\n"," plist.append(pauli_sym[p])\n"," return QubitPauliString(qlist, plist)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["def qpo_from_openfermion(openf_op):\n"," \"\"\"Convert OpenFermion QubitOperator to pytket QubitPauliOperator.\"\"\"\n"," tk_op = dict()\n"," for term, coeff in openf_op.terms.items():\n"," string = qps_from_openfermion(term)\n"," tk_op[string] = coeff\n"," return QubitPauliOperator(tk_op)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["hamiltonian_op = qpo_from_openfermion(hamiltonian)"]},{"cell_type":"markdown","metadata":{},"source":["Now we can create a `ProjectQBackend` instance and feed it our circuit and `QubitOperator`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.utils.operators import QubitPauliOperator"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["projectq_b = ProjectQBackend()\n","expectation = projectq_b.get_operator_expectation_value(circ2, hamiltonian_op)\n","print(expectation)"]},{"cell_type":"markdown","metadata":{},"source":["The last leg of this tour includes running a pytket circuit on an actual quantum computer. To do this, you will need an IBM quantum experience account and have your credentials stored on your computer. See https://quantum-computing.ibm.com to make an account and view available devices and their specs.
\n","
\n","Physical devices have much stronger constraints on the form of admissible circuits than simulators. They tend to support a minimal gate set, have restricted connectivity between qubits for two-qubit gates, and can have limited support for classical control flow or conditional gates. This is where we can invoke the tket compiler passes to transform our desired circuit into one that is suitable for the backend.
\n","
\n","To check our code works correctly, we can use the `IBMQEmulatorBackend` to run our code exactly as if it were going to run on a real device, but just execute on a simulator (with a basic noise model adapted from the reported device properties)."]},{"cell_type":"markdown","metadata":{},"source":["Let's create an `IBMQEmulatorBackend` for the `ibmq_manila` device and check if our circuit is valid to be run."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["ibmq_b_emu = IBMQEmulatorBackend(\"ibmq_manila\")\n","ibmq_b_emu.valid_circuit(circ)"]},{"cell_type":"markdown","metadata":{},"source":["It looks like we need to compile this circuit to be compatible with the device. To simplify this procedure, we provide minimal compilation passes designed for each backend (the `default_compilation_pass()` method) which will guarantee compatibility with the device. These may still fail if the input circuit has too many qubits or unsupported usage of conditional gates. The default passes can have their degree of optimisation by changing an integer parameter (optimisation levels 0, 1, 2), and they can be easily composed with any of tket's other optimisation passes for better performance.
\n","
\n","For convenience, we also wrap up this pass into the `get_compiled_circuit` method if you just want to compile a single circuit."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["compiled_circ = ibmq_b_emu.get_compiled_circuit(circ)"]},{"cell_type":"markdown","metadata":{},"source":["Let's create a backend for running on the actual device and check our compiled circuit is valid for this backend too."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["ibmq_b = IBMQBackend(\"ibmq_manila\")\n","ibmq_b.valid_circuit(compiled_circ)"]},{"cell_type":"markdown","metadata":{},"source":["We are now good to run this circuit on the device. After submitting, we can use the handle to check on the status of the job, so that we know when results are ready to be retrieved. The `circuit_status` method works for all backends, and returns a `CircuitStatus` object. If we just run `get_result` straight away, the backend will wait for results to complete, blocking any other code from running.
\n","
\n","In this notebook we will use the emulated backend `ibmq_b_emu` to illustrate, but the workflow is the same as for the real backend `ibmq_b` (except that the latter will typically take much longer because of the size of the queue)."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["quantum_handle = ibmq_b_emu.process_circuit(compiled_circ, n_shots=10)"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["print(ibmq_b_emu.circuit_status(quantum_handle))"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["quantum_counts = ibmq_b_emu.get_result(quantum_handle).get_counts()\n","print(quantum_counts)"]},{"cell_type":"markdown","metadata":{},"source":["These are from an actual device, so it's impossible to perfectly predict what the results will be. However, because of the problem of noise, it would be unsurprising to find a few $01$ or $10$ results in the table. The circuit is very short, so it should be fairly close to the ideal result.
\n","
\n","The devices available through the IBM Q Experience serve jobs one at a time from their respective queues, so a large amount of experiment time can be taken up by waiting for your jobs to reach the front of the queue. `pytket` allows circuits to be submitted to any backend in a single batch using the `process_circuits` method. For the `IBMQBackend`, this will collate the circuits into as few jobs as possible which will all be sent off into the queue for the device. The method returns a `ResultHandle` per submitted circuit, in the order of submission."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["circuits = []\n","for i in range(5):\n"," c = Circuit(2)\n"," c.Rx(0.2 * i, 0).CX(0, 1)\n"," c.measure_all()\n"," circuits.append(ibmq_b_emu.get_compiled_circuit(c))\n","handles = ibmq_b_emu.process_circuits(circuits, n_shots=100)\n","print(handles)"]},{"cell_type":"markdown","metadata":{},"source":["We can now retrieve the results and process them. As we measured each circuit in the $Z$-basis, we can obtain the expectation value for the $ZZ$ operator immediately from these measurement results. We can calculate this using the `expectation_from_counts` utility method in `pytket`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.utils import expectation_from_counts"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["for handle in handles:\n"," counts = ibmq_b_emu.get_result(handle).get_counts()\n"," exp_val = expectation_from_counts(counts)\n"," print(exp_val)"]},{"cell_type":"markdown","metadata":{},"source":["A `ResultHandle` can be easily stored in its string representaton and later reconstructed using the `from_str` method. For example, we could do something like this:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["from pytket.backends import ResultHandle"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["c = Circuit(2).Rx(0.5, 0).CX(0, 1).measure_all()\n","c = ibmq_b_emu.get_compiled_circuit(c)\n","handle = ibmq_b_emu.process_circuit(c, n_shots=10)\n","handlestring = str(handle)\n","print(handlestring)\n","# ... later ...\n","oldhandle = ResultHandle.from_str(handlestring)\n","print(ibmq_b_emu.get_result(oldhandle).get_counts())"]},{"cell_type":"markdown","metadata":{},"source":["For backends which support persistent handles (e.g. `IBMQBackend`, `QuantinuumBackend`, `BraketBackend` and `AQTBackend`) you can even stop your python session and use your result handles in a separate script to retrive results when they are ready, by storing the handle strings. For experiments with long queue times, this enables separate job submission and retrieval. Use `Backend.persistent_handles` to check whether a backend supports this feature.
\n","
\n","All backends will also cache all results obtained in the current python session, so you can use the `ResultHandle` to retrieve the results many times if you need to reuse the results. Over a long experiment, this can consume a large amount of RAM, so we recommend removing results from the cache when you are done with them. A simple way to achieve this is by calling `Backend.empty_cache` (e.g. at the end of each loop of a variational algorithm), or removing individual results with `Backend.pop_result`."]},{"cell_type":"markdown","metadata":{},"source":["The backends in `pytket` are designed to be as similar to one another as possible. The example above using physical devices can be run entirely on a simulator by swapping out the `IBMQBackend` constructor for any other backend supporting shot outputs (e.g. `AerBackend`, `ProjectQBackend`, `ForestBackend`), or passing it the name of a different device. Furthermore, using pytket it is simple to convert between handling shot tables, counts maps and statevectors.
\n","
\n","For more information on backends and other `pytket` features, read our [documentation](https://tket.quantinuum.com/api-docs/) or see the other pytket examples."]}],"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}