From 25ea7ff1be5042cda396ce64563c1f3600a2943c Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 13:01:54 +0100
Subject: [PATCH 01/16] port to markdown, fix jupyter-execute and jupyter-input
---
docs/manual/manual_assertion.md | 170 +++
docs/manual/manual_assertion.rst | 161 ---
docs/manual/manual_backend.md | 850 +++++++++++
docs/manual/manual_backend.rst | 765 ----------
.../{manual_circuit.rst => manual_circuit.md} | 1050 ++++++++------
docs/manual/manual_compiler.md | 1266 +++++++++++++++++
docs/manual/manual_compiler.rst | 1168 ---------------
.../{manual_intro.rst => manual_intro.md} | 104 +-
docs/manual/manual_noise.md | 543 +++++++
docs/manual/manual_noise.rst | 511 -------
docs/manual/{manual_zx.rst => manual_zx.md} | 370 ++---
11 files changed, 3700 insertions(+), 3258 deletions(-)
create mode 100644 docs/manual/manual_assertion.md
delete mode 100644 docs/manual/manual_assertion.rst
create mode 100644 docs/manual/manual_backend.md
delete mode 100644 docs/manual/manual_backend.rst
rename docs/manual/{manual_circuit.rst => manual_circuit.md} (54%)
create mode 100644 docs/manual/manual_compiler.md
delete mode 100644 docs/manual/manual_compiler.rst
rename docs/manual/{manual_intro.rst => manual_intro.md} (50%)
create mode 100644 docs/manual/manual_noise.md
delete mode 100644 docs/manual/manual_noise.rst
rename docs/manual/{manual_zx.rst => manual_zx.md} (61%)
diff --git a/docs/manual/manual_assertion.md b/docs/manual/manual_assertion.md
new file mode 100644
index 00000000..3d7c3d3a
--- /dev/null
+++ b/docs/manual/manual_assertion.md
@@ -0,0 +1,170 @@
+---
+file_format: mystnb
+---
+
+# Assertion
+
+In quantum computing, an assertion is a predefined predicate which can let us test whether an experimentally prepared quantum state is in a specified subspace of the state space.
+
+In addition to detecting defects, the assertion schemes in `pytket` automatically correct the state if there is no assertion error.
+This property can be potentially exploited to help error mitigation or nondeterministically preparing a quantum state.
+
+`pytket` provides two ways to construct an assertion, by a projector matrix or by a set of Pauli stabilisers.
+The former can be used to assert arbitrary subspaces, but note that we currently only support 2x2, 4x4, and 8x8 matrices.
+The latter is useful for asserting that the prepared state lies in a subspace spanned by some stabiliser states.
+
+When applied to a circuit, the assertion is inserted as a {py:class}`~pytket.circuit.ProjectorAssertionBox` or a {py:class}`~pytket.circuit.StabiliserAssertionBox`, and then synthesized into a set of gates and measurements by the {py:class}`~pytket.passes.DecomposeBoxes` pass. Be aware that an ancilla qubit might be required for the assertion.
+The results of these measurements will be used later on to determine the outcome of the assertion.
+
+To test the circuit, compile and process the circuit using a {py:class}`~pytket.backends.Backend` that supports mid-circuit measurement and reset (e.g. {py:class}`~pytket.extensions.qiskit.AerBackend` from `pytket-qiskit`).
+Once a {py:class}`~pytket.backends.backendresult.BackendResult` object is retrieved, the outcome of the assertion can be checked with the {py:meth}`~pytket.backends.backendresult.BackendResult.get_debug_info` method.
+
+## Projector-based
+
+Projector-based assertion utilises the simple fact that the outcome of a projective measurement can be used to determine if a quantum state is in a specified subspace of the state space.
+The method implemented in pytket transforms an arbitrary projective measurement into measurements on the computational basis [^cite_gushu2020].
+However, this approach is not without limitations. Projectors in general require $2^{n} \times 2^{n}$ matrices to represent them; hence it becomes impractical when the size of the asserted subspace is large.
+Moreover, the transformation technique we have adapted requires synthesis for arbitrary unitary matrices. Since `pytket` currently only supports synthesis for 1, 2, and 3 qubit unitaries, the projectors are limited to 2x2, 4x4, and 8x8 matrices.
+
+To start asserting with a projector, one should first compute the projector matrix for the target subspace. If the rank of the projector is larger than $2^{n-1}$ ($n$ is the number of qubits), an ancilla qubit should be provided to the {py:meth}`~pytket.circuit.Circuit.add_assertion()` method.
+A special unsupported case arises when asserting a 3-qubit subspace whose projector has a rank larger than $2^{3-1}$.
+
+In the following example, we try to prepare a Bell state along with a state obtained by applying an $\mathrm{Rx}(0.3)$ rotation to $|0\rangle$; we then use projectors to assert that the circuit construction is correct.
+
+
+```{code-cell} ipython3
+
+from pytket.circuit import ProjectorAssertionBox, Circuit
+from pytket.extensions.qiskit import AerBackend
+import numpy as np
+import math
+
+# construct a circuit that prepares a Bell state for qubits [0,1]
+# and a Rx(0.3)|0> state for qubit 2
+circ = Circuit(3)
+circ.H(0).CX(0,1).Rx(0.03,2) # A bug in the circuit
+
+# prepare a backend
+backend = AerBackend()
+
+# prepare a projector for the Bell state
+bell_projector = np.array([
+ [0.5, 0, 0, 0.5],
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ [0.5, 0, 0, 0.5],
+])
+# prepare a projector for the state Rx(0.3)|0>
+rx_projector = np.array([
+ [math.cos(0.15*math.pi) ** 2, 0.5j*math.sin(0.3*math.pi)],
+ [-0.5j*math.sin(0.3*math.pi), math.sin(0.15*math.pi) ** 2]
+])
+
+# add the assertions
+circ.add_assertion(ProjectorAssertionBox(bell_projector), [0,1], name="|bell>")
+circ.add_assertion(ProjectorAssertionBox(rx_projector), [2], name="Rx(0.3)|0>")
+
+# compile and run the circuit
+compiled_circ = backend.get_compiled_circuit(circ)
+res_handle = backend.process_circuit(compiled_circ,n_shots=100)
+re = backend.get_result(res_handle)
+re.get_debug_info()
+```
+
+Without the presence of noise, if a state is in the target subspace, then its associated assertion will succeed with certainty; on the other hand, an assertion failure indicates that the state is not in the target subspace.
+In order to really test the program, the debug circuit should be run multiple times to ensure an accurate conclusion. The {py:class}`dict` object returned by {py:meth}`~pytket.backends.backendresult.BackendResult.get_debug_info` suggests that the Bell state assertion succeeded for all the 100 shots; hence we are confident that the construction for the Bell state is correct.
+On the contrary, the assertion named "Rx(0.3)|0>" failed for some shots; this means that the construction for that state is incorrect.
+
+If there is noise in the device, which is the case for all devices in the NISQ era, then the results can be much less clear. The following example demonstrates what the assertion outcome will look like if we compile and run the debug circuit with a noisy backend.
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from qiskit_aer.noise import NoiseModel
+from qiskit import IBMQ
+IBMQ.load_account()
+
+# prepare a noisy backend
+backend = AerBackend(NoiseModel.from_backend(IBMQ.providers()[0].get_backend('ibmq_manila')))
+
+# compile the previously constructed circuit
+compiled_circ = backend.get_compiled_circuit(circ)
+res_handle = backend.process_circuit(compiled_circ,n_shots=100)
+re = backend.get_result(res_handle)
+re.get_debug_info()
+```
+
+
+```
+{'|bell>': 0.95, '|Rx(0.3)>': 0.98}
+```
+
+## Stabiliser-based
+
+A stabiliser subspace is a subspace that can be uniquely determined by a stabiliser subgroup.
+Since all Pauli operators in a stabiliser subgroup have +/- 1 eigenvalues, we can verify if a quantum state is in the +1 eigenspace of such a Pauli operator by repeatedly measuring the following circuit [^cite_niel2010].
+
+
+```{code-cell} ipython3
+
+from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
+from qiskit.circuit.library.standard_gates import HGate, XGate
+
+qc = QuantumCircuit(2,1)
+qc.h(0)
+u = XGate("Pauli operator").control(1)
+qc.append(u, [0,1])
+qc.h(0)
+qc.measure([0], [0])
+qc.draw()
+```
+
+To verify if a quantum state is in a stabiliser subspace such a circuit is needed for each Pauli operator so they can be later measured to check if the state falls into the intersection of the +1 eigenspaces.
+
+To assert using stabilisers, one should provide `pytket` with a set of Pauli operators that uniquely determines the target subspace. The smallest such sets are the generating sets of the stabiliser subgroup stabilising the subspace.
+These generating sets only contain at most $n$ Pauli operators for a n-qubit subspace. For example, it is known that the set {"XX", "ZZ"} is a generating set for the stabiliser subgroup that stabilises the Bell state.
+
+The following code demonstrates how we use the generating set for the Bell state to assert a circuit construction.
+
+
+```{code-cell} ipython3
+
+from pytket.circuit import StabiliserAssertionBox, Circuit, Qubit
+from pytket.extensions.qiskit import AerBackend
+
+# prepare a Bell state
+circ = Circuit(2)
+circ.H(0).CX(0,1)
+
+# add an ancilla qubit for this assertion
+circ.add_qubit(Qubit(2))
+
+# define the generating set
+stabilisers = ["XX", "ZZ"]
+
+circ.add_assertion(StabiliserAssertionBox(stabilisers), [0,1], ancilla=2, name="|bell>")
+
+backend = AerBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
+res_handle = backend.process_circuit(compiled_circ,n_shots=100)
+res = backend.get_result(res_handle)
+res.get_debug_info()
+```
+
+A {py:class}`~pytket.circuit.StabiliserAssertionBox` can also be constructed with a {py:class}`~pytket.pauli.PauliStabiliser`:
+
+
+```{code-cell} ipython3
+
+from pytket.pauli import PauliStabiliser, Pauli
+
+stabilisers = [PauliStabiliser([Pauli.X, Pauli.X], 1), PauliStabiliser([Pauli.Z, Pauli.Z], 1)]
+s = StabiliserAssertionBox(stabilisers)
+```
+
+[^cite_gushu2020]: Gushu, L., Li, Z., Nengkun, Y., Yufei, D., Mingsheng, Y. and Yuan, X., 2020. Proq: Projection-based Runtime Assertions for Debugging on a Quantum Computer. arXiv preprint arXiv:1911.12855.
+
+[^cite_niel2010]: Nielsen, M.A. and Chuang, I.L., 2010. Quantum computation and quantum information. Cambridge University Press, p.188.
diff --git a/docs/manual/manual_assertion.rst b/docs/manual/manual_assertion.rst
deleted file mode 100644
index 9ea79e7f..00000000
--- a/docs/manual/manual_assertion.rst
+++ /dev/null
@@ -1,161 +0,0 @@
-***********************************
-Assertion
-***********************************
-
-In quantum computing, an assertion is a predefined predicate which can let us test whether an experimentally prepared quantum state is in a specified subspace of the state space.
-
-In addition to detecting defects, the assertion schemes in ``pytket`` automatically correct the state if there is no assertion error.
-This property can be potentially exploited to help error mitigation or nondeterministically preparing a quantum state.
-
-``pytket`` provides two ways to construct an assertion, by a projector matrix or by a set of Pauli stabilisers.
-The former can be used to assert arbitrary subspaces, but note that we currently only support 2x2, 4x4, and 8x8 matrices.
-The latter is useful for asserting that the prepared state lies in a subspace spanned by some stabiliser states.
-
-When applied to a circuit, the assertion is inserted as a :py:class:`~pytket.circuit.ProjectorAssertionBox` or a :py:class:`~pytket.circuit.StabiliserAssertionBox`, and then synthesized into a set of gates and measurements by the :py:class:`~pytket.passes.DecomposeBoxes` pass. Be aware that an ancilla qubit might be required for the assertion.
-The results of these measurements will be used later on to determine the outcome of the assertion.
-
-To test the circuit, compile and process the circuit using a :py:class:`~pytket.backends.Backend` that supports mid-circuit measurement and reset (e.g. :py:class:`~pytket.extensions.qiskit.AerBackend` from ``pytket-qiskit``).
-Once a :py:class:`~pytket.backends.backendresult.BackendResult` object is retrieved, the outcome of the assertion can be checked with the :py:meth:`~pytket.backends.backendresult.BackendResult.get_debug_info` method.
-
-
-Projector-based
----------------
-
-Projector-based assertion utilises the simple fact that the outcome of a projective measurement can be used to determine if a quantum state is in a specified subspace of the state space.
-The method implemented in pytket transforms an arbitrary projective measurement into measurements on the computational basis [Gushu2020]_.
-However, this approach is not without limitations. Projectors in general require :math:`2^{n} \times 2^{n}` matrices to represent them; hence it becomes impractical when the size of the asserted subspace is large.
-Moreover, the transformation technique we have adapted requires synthesis for arbitrary unitary matrices. Since ``pytket`` currently only supports synthesis for 1, 2, and 3 qubit unitaries, the projectors are limited to 2x2, 4x4, and 8x8 matrices.
-
-To start asserting with a projector, one should first compute the projector matrix for the target subspace. If the rank of the projector is larger than :math:`2^{n-1}` (:math:`n` is the number of qubits), an ancilla qubit should be provided to the :py:meth:`~pytket.circuit.Circuit.add_assertion()` method.
-A special unsupported case arises when asserting a 3-qubit subspace whose projector has a rank larger than :math:`2^{3-1}`.
-
-In the following example, we try to prepare a Bell state along with a state obtained by applying an :math:`\mathrm{Rx}(0.3)` rotation to :math:`|0\rangle`; we then use projectors to assert that the circuit construction is correct.
-
-.. jupyter-execute::
-
- from pytket.circuit import ProjectorAssertionBox, Circuit
- from pytket.extensions.qiskit import AerBackend
- import numpy as np
- import math
-
- # construct a circuit that prepares a Bell state for qubits [0,1]
- # and a Rx(0.3)|0> state for qubit 2
- circ = Circuit(3)
- circ.H(0).CX(0,1).Rx(0.03,2) # A bug in the circuit
-
- # prepare a backend
- backend = AerBackend()
-
- # prepare a projector for the Bell state
- bell_projector = np.array([
- [0.5, 0, 0, 0.5],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- [0.5, 0, 0, 0.5],
- ])
- # prepare a projector for the state Rx(0.3)|0>
- rx_projector = np.array([
- [math.cos(0.15*math.pi) ** 2, 0.5j*math.sin(0.3*math.pi)],
- [-0.5j*math.sin(0.3*math.pi), math.sin(0.15*math.pi) ** 2]
- ])
-
- # add the assertions
- circ.add_assertion(ProjectorAssertionBox(bell_projector), [0,1], name="|bell>")
- circ.add_assertion(ProjectorAssertionBox(rx_projector), [2], name="Rx(0.3)|0>")
-
- # compile and run the circuit
- compiled_circ = backend.get_compiled_circuit(circ)
- res_handle = backend.process_circuit(compiled_circ,n_shots=100)
- re = backend.get_result(res_handle)
- re.get_debug_info()
-
-Without the presence of noise, if a state is in the target subspace, then its associated assertion will succeed with certainty; on the other hand, an assertion failure indicates that the state is not in the target subspace.
-In order to really test the program, the debug circuit should be run multiple times to ensure an accurate conclusion. The :py:class:`dict` object returned by :py:meth:`~pytket.backends.backendresult.BackendResult.get_debug_info` suggests that the Bell state assertion succeeded for all the 100 shots; hence we are confident that the construction for the Bell state is correct.
-On the contrary, the assertion named "Rx(0.3)|0>" failed for some shots; this means that the construction for that state is incorrect.
-
-If there is noise in the device, which is the case for all devices in the NISQ era, then the results can be much less clear. The following example demonstrates what the assertion outcome will look like if we compile and run the debug circuit with a noisy backend.
-
-
-.. jupyter-input::
-
- from qiskit_aer.noise import NoiseModel
- from qiskit import IBMQ
- IBMQ.load_account()
-
- # prepare a noisy backend
- backend = AerBackend(NoiseModel.from_backend(IBMQ.providers()[0].get_backend('ibmq_manila')))
-
- # compile the previously constructed circuit
- compiled_circ = backend.get_compiled_circuit(circ)
- res_handle = backend.process_circuit(compiled_circ,n_shots=100)
- re = backend.get_result(res_handle)
- re.get_debug_info()
-
-.. jupyter-output::
-
- {'|bell>': 0.95, '|Rx(0.3)>': 0.98}
-
-
-Stabiliser-based
---------------------------
-
-A stabiliser subspace is a subspace that can be uniquely determined by a stabiliser subgroup.
-Since all Pauli operators in a stabiliser subgroup have +/- 1 eigenvalues, we can verify if a quantum state is in the +1 eigenspace of such a Pauli operator by repeatedly measuring the following circuit [Niel2010]_.
-
-.. jupyter-execute::
- :hide-code:
-
- from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
- from qiskit.circuit.library.standard_gates import HGate, XGate
-
- qc = QuantumCircuit(2,1)
- qc.h(0)
- u = XGate("Pauli operator").control(1)
- qc.append(u, [0,1])
- qc.h(0)
- qc.measure([0], [0])
- qc.draw()
-
-To verify if a quantum state is in a stabiliser subspace such a circuit is needed for each Pauli operator so they can be later measured to check if the state falls into the intersection of the +1 eigenspaces.
-
-To assert using stabilisers, one should provide ``pytket`` with a set of Pauli operators that uniquely determines the target subspace. The smallest such sets are the generating sets of the stabiliser subgroup stabilising the subspace.
-These generating sets only contain at most :math:`n` Pauli operators for a n-qubit subspace. For example, it is known that the set {"XX", "ZZ"} is a generating set for the stabiliser subgroup that stabilises the Bell state.
-
-The following code demonstrates how we use the generating set for the Bell state to assert a circuit construction.
-
-.. jupyter-execute::
-
- from pytket.circuit import StabiliserAssertionBox, Circuit, Qubit
- from pytket.extensions.qiskit import AerBackend
-
- # prepare a Bell state
- circ = Circuit(2)
- circ.H(0).CX(0,1)
-
- # add an ancilla qubit for this assertion
- circ.add_qubit(Qubit(2))
-
- # define the generating set
- stabilisers = ["XX", "ZZ"]
-
- circ.add_assertion(StabiliserAssertionBox(stabilisers), [0,1], ancilla=2, name="|bell>")
-
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- res_handle = backend.process_circuit(compiled_circ,n_shots=100)
- res = backend.get_result(res_handle)
- res.get_debug_info()
-
-
-A :py:class:`~pytket.circuit.StabiliserAssertionBox` can also be constructed with a :py:class:`~pytket.pauli.PauliStabiliser`:
-
-.. jupyter-execute::
-
- from pytket.pauli import PauliStabiliser, Pauli
-
- stabilisers = [PauliStabiliser([Pauli.X, Pauli.X], 1), PauliStabiliser([Pauli.Z, Pauli.Z], 1)]
- s = StabiliserAssertionBox(stabilisers)
-
-
-.. [Gushu2020] Gushu, L., Li, Z., Nengkun, Y., Yufei, D., Mingsheng, Y. and Yuan, X., 2020. Proq: Projection-based Runtime Assertions for Debugging on a Quantum Computer. arXiv preprint arXiv:1911.12855.
-.. [Niel2010] Nielsen, M.A. and Chuang, I.L., 2010. Quantum computation and quantum information. Cambridge University Press, p.188.
diff --git a/docs/manual/manual_backend.md b/docs/manual/manual_backend.md
new file mode 100644
index 00000000..f5ec6662
--- /dev/null
+++ b/docs/manual/manual_backend.md
@@ -0,0 +1,850 @@
+---
+file_format: mystnb
+---
+# Running on Backends
+
+% Co-processor model of QC; circuits are the units of tasks
+
+The interaction model for quantum computing in the near future is set to follow the co-processor model: there is a main program running on the classical host computer which routinely sends off jobs to a specialist device that can handle that class of computation efficiently, similar to interacting with GPUs and cloud HPC resources. We have already seen how to use `pytket` to describe a job to be performed with the {py:class}`~pytket.circuit.Circuit` class; the next step is to look at how we interact with the co-processor to run it.
+
+% Backends manage sending the circuits to be processed (by simulator or device) and retrieving results; general workflow of compile, process, retrieve
+
+A {py:class}`~pytket.backends.Backend` represents a connection to some co-processor instance, which can be either quantum hardware or a simulator. It presents a uniform interface for submitting {py:class}`~pytket.circuit.Circuit` s to be processed and retrieving the results, allowing the general workflow of "generate, compile, process, retrieve, interpret" to be performed with little dependence on the specific co-processor used. This is to promote the development of platform-independent software, helping the code that you write to be more future-proof and encouraging exploration of the ever-changing landscape of quantum hardware solutions.
+
+With the wide variety of {py:class}`~pytket.backends.Backend` s available, they naturally have very different capabilities and limitations. The class is designed to open up this information so that it is easy to examine at runtime and make hardware-dependent choices as needed for maximum performance, whilst providing a basic abstract model that is easy for proof-of-concept testing.
+
+No {py:class}`~pytket.backends.Backend` s are currently provided with the core [pytket](https://tket.quantinuum.com/api-docs/) package, but most extension modules will add simulators or devices from the given provider, such as the {py:class}`~pytket.extensions.qiskit.AerBackend` and {py:class}`~pytket.extensions.qiskit.IBMQBackend` with [pytket-qiskit](https://tket.quantinuum.com/extensions/pytket-qiskit/) or the {py:class}`~pytket.extensions.quantinuum.QuantinuumBackend` with [pytket-quantinuum](https://tket.quantinuum.com/extensions/pytket-quantinuum/).
+
+## Backend Requirements
+
+% Not every circuit can be run immediately on a device or simulator; restrictions put in place for ease of implementation or limitations of engineering or noise
+
+% Devices and simulators are designed to only support a small gate set, but since they are universal, it is enough to compile to them
+
+Every device and simulator will have some restrictions to allow for a simpler implementation or because of the limits of engineering or noise within a device. For example, devices and simulators are typically designed to only support a small (but universal) gate set, so a {py:class}`~pytket.circuit.Circuit` containing other gate types could not be run immediately. However, as long as the fragment supported is universal, it is enough to be able to compile down to a semantically-equivalent {py:class}`~pytket.circuit.Circuit` which satisfies the requirements, for example, by translating each unknown gate into sequences of known gates.
+
+% Other common restrictions are on the number and connectivity of qubits - a multi-qubit gate may only be possible to perform on adjacent qubits on the architecture
+
+Other common restrictions presented by QPUs include the number of available qubits and their connectivity (multi-qubit gates may only be performed between adjacent qubits on the architecture). Measurements may also be noisy or take a long time on some QPUs, leading to the destruction or decoherence of any remaining quantum state, so they are artificially restricted to only happen in a single layer at the end of execution and mid-circuit measurements are rejected. More extremely, some classes of classical simulators will reject measurements entirely as they are designed to simulate pure quantum circuits (for example, when looking to yield a statevector or unitary deterministically).
+
+% Each restriction on the circuits is captured by a `Predicate`
+
+% Querying the requirements of a given backend
+
+Each {py:class}`~pytket.backends.Backend` object is aware of the restrictions of the underlying device or simulator, encoding them as a collection of {py:class}`~pytket.predictate.Predicate` s. Each {py:class}`~pytket.predictate.Predicate` is essentially a Boolean property of a {py:class}`~pytket.circuit.Circuit` which must return `True` for the {py:class}`~pytket.circuit.Circuit` to successfully run. The set of {py:class}`~pytket.predicates.Predicate` s required by a {py:class}`~pytket.backends.Backend` can be queried with {py:attr}`~pytket.backends.Backend.required_predicates`.
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket.extensions.qiskit import IBMQBackend, AerStateBackend
+
+dev_b = IBMQBackend("ibmq_quito")
+sim_b = AerStateBackend()
+print(dev_b.required_predicates)
+print(sim_b.required_predicates)
+```
+
+
+```
+
+ [NoClassicalControlPredicate, NoFastFeedforwardPredicate, NoMidMeasurePredicate, NoSymbolsPredicate, GateSetPredicate:{ U1 noop U2 CX Barrier Measure U3 }, DirectednessPredicate:{ Nodes: 5, Edges: 8 }]
+ [NoClassicalControlPredicate, NoFastFeedforwardPredicate, GateSetPredicate:{ CU1 CZ CX Unitary2qBox Sdg U1 Unitary1qBox SWAP S U2 CCX Y U3 Z X T noop Tdg Reset H }]
+```
+
+% Can check if a circuit satisfies all requirements with `valid_circuit`
+
+% `get_compiled_circuit` modifies a circuit to try to satisfy all backend requirements if possible (restrictions on measurements or conditional gate support may not be fixed by compilation)
+
+Knowing the requirements of each {py:class}`~pytket.backends.Backend` is handy in case it has consequences for how you design a {py:class}`~pytket.circuit.Circuit`, but can generally be abstracted away. Calling {py:meth}`~pytket.backends.Backend.valid_circuit()` can check whether or not a {py:class}`~pytket.circuit.Circuit` satisfies every requirement to run on the {py:class}`~pytket.backends.Backend`, and if it is not immediately valid then {py:meth}`~pytket.backends.Backend.get_compiled_circuit` will try to solve all of the remaining constraints when possible (note that restrictions on measurements or conditional gate support may not be fixed by compilation), and return a new {py:class}`~pytket.circuit.Circuit`.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+ from pytket.extensions.qiskit import AerBackend
+
+ circ = Circuit(3, 2)
+ circ.H(0).Ry(0.25, 1)
+ circ.add_gate(OpType.CnRy, [0.74], [0, 1, 2]) # CnRy not in AerBackend gate set
+ circ.measure_all()
+
+ backend = AerBackend()
+ print("Circuit valid for AerBackend?", backend.valid_circuit(circ))
+ compiled_circ = backend.get_compiled_circuit(circ) # Compile circuit to AerBackend
+
+ print("Compiled circuit valid for AerBackend?", backend.valid_circuit(compiled_circ))
+```
+
+Now that we can prepare our {py:class}`~pytket.circuit.Circuit` s to be suitable for a given {py:class}`~pytket.backends.Backend`, we can send them off to be run and examine the results. This is always done by calling {py:meth}`~pytket.backends.Backend.process_circuit()` which sends a {py:class}`~pytket.circuit.Circuit` for execution and returns a {py:class}`~pytket.backends.resulthandle.ResultHandle` as an identifier for the job which can later be used to retrieve the actual results once the job has finished.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerStateBackend
+
+ circ = Circuit(2, 2)
+ circ.Rx(0.3, 0).Ry(0.5, 1).CRz(-0.6, 1, 0)
+ backend = AerStateBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+ handle = backend.process_circuit(compiled_circ)
+```
+
+The exact arguments to {py:meth}`~pytket.backends.Backend.process_circuit` and the means of retrieving results back are dependent on the type of data the {py:class}`~pytket.backends.Backend` can produce and whether it samples measurements or calculates the internal state of the quantum system.
+
+## Shots and Sampling
+
+% On real devices, cannot directly inspect the statevector of quantum system, so only classical output is the results of measurements
+
+% Measurements are not deterministic, so each run samples from some distribution; refer to each full run of the circuit from the initial state as a "shot"
+
+Running a {py:class}`~pytket.circuit.Circuit` on a quantum computer invovles applying the instructions to some quantum system to modify its state. Whilst we know that this state will form a vector (or linear map) in some Hilbert space, we cannot directly inspect it and obtain a complex vector/matrix to return to the classical host process. The best we can achieve is performing measurements to collapse the state and obtain a bit of information in the process. Since the measurements are not deterministic, each run of the {py:class}`~pytket.circuit.Circuit` samples from some distribution. By obtaining many *shots* (the classical readout from each full run of the {py:class}`~pytket.circuit.Circuit` from the initial state), we can start to predict what the underlying measurement distrubution looks like.
+
+% Retrieve table of results using `get_shots`; rows are shots (in order of execution), columns are bits (in ILO)
+
+The interaction with a QPU (or a simulator that tries to imitate a device by sampling from the underlying complex statevector) is focused around requesting shots for a given {py:class}`~pytket.circuit.Circuit`. The number of shots required is passed to {py:meth}`~pytket.backends.Backend.process_circuit()`. The result is retrieved using {py:meth}`~pytket.backends.Backend.get_result()`; and the shots are then given as a table from {py:meth}`~pytket.backends.BackendResult.get_shots()`: each row of the table describes a shot in the order of execution, and the columns are the classical bits from the {py:class}`~pytket.circuit.Circuit`.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerBackend
+
+ circ = Circuit(2, 2)
+ circ.H(0).X(1).measure_all()
+ backend = AerBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+
+ handle = backend.process_circuit(compiled_circ, n_shots=20)
+ shots = backend.get_result(handle).get_shots()
+ print(shots)
+```
+
+% Often interested in probabilities of each measurement outcome, so need many shots for high precision
+
+% Even if we expect a single peak in distribution, will want many shots to account for noise
+
+For most applications, we are interested in the probability of each measurement outcome, so we need many shots for each experiment for high precision (it is quite typical to ask for several thousand or more). Even if we expect a single sharp peak in the distribution, as is the case from many of the popular textbook quantum algorithms (Deutsch-Jozsa, Bernstein-Vazirani, Shor, etc.), we will generally want to take many shots to help account for noise.
+
+% If we don't need order of results, can get summary of counts using `get_counts`
+
+If we don't care about the temporal order of the shots, we can instead retrieve a compact summary of the frequencies of observed results. The dictionary returned by {py:meth}`~pytket.backends.BackendResult.get_counts` maps tuples of bits to the number of shots that gave that result (keys only exist in the dictionary if this is non-zero). If probabilities are preferred to frequencies, we can apply the utility method {py:meth}`~pytket.backends.BackendResult.probs_from_counts()`.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerBackend
+ from pytket.utils import probs_from_counts
+
+ circ = Circuit(2, 2)
+ circ.H(0).X(1).measure_all()
+ backend = AerBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+
+ handle = backend.process_circuit(compiled_circ, n_shots=2000)
+ counts = backend.get_result(handle).get_counts()
+ print(counts)
+
+ print(probs_from_counts(counts))
+```
+
+:::{note}
+{py:class}`~pytket.backends.Backend.process_circuit` returns a handle to the computation to perform the quantum computation asynchronously. Non-blocking operations are essential when running circuits on remote devices, as submission and queuing times can be long. The handle may then be used to retrieve the results with {py:class}`~pytket.backends.Backend.get_result`. If asynchronous computation is not needed, for example when running on a local simulator, pytket provides the shortcut {py:meth}`pytket.backends.Backend.run_circuit` that will immediately execute the circuit and return a {py:class}`~pytket.backends.backendresult.BackendResult`.
+:::
+
+## Statevector and Unitary Simulation with TKET Backends
+
+% Any form of sampling introduces non-deterministic error, so for better accuracy we will want the exact state of the physical system; some simulators will provide direct access to this
+
+% `get_state` gives full representation of that system's state in the 2^n-dimensional complex Hilbert space
+
+Any form of sampling from a distribution will introduce sampling error and (unless it is a seeded simulator) non-deterministic results, whereas we could get much better accuracy and repeatability if we have the exact state of the underlying physical quantum system. Some simulators will provide direct access to this. The {py:meth}`~pytket.backends.BackendResult.get_state` method will give the full representation of the physical state as a vector in the $2^n$-dimensional Hilbert space, whenever the underlying simulator provides this.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerStateBackend
+
+ circ = Circuit(3)
+ circ.H(0).CX(0, 1).S(1).X(2)
+ backend = AerStateBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+
+ state = backend.run_circuit(compiled_circ).get_state()
+ print(state.round(5))
+```
+
+:::{note}
+We have rounded the results here because simulators typically introduce a small amount of floating-point error, so killing near-zero entries gives a much more readable representation.
+:::
+
+% `get_unitary` treats circuit with open inputs and gives map on 2^n-dimensional complex Hilbert space
+
+The majority of {py:class}`~pytket.backends.Backend` s will run the {py:class}`~pytket.circuit.Circuit` on the initial state $|0\rangle^{\otimes n}$. However, because we can form the composition of {py:class}`~pytket.circuit.Circuit` s, we want to be able to test them with open inputs. When the {py:class}`~pytket.circuit.Circuit` is purely quantum, we can represent its effect as an open circuit by a unitary matrix acting on the $2^n$-dimensional Hilbert space. The {py:class}`~pytket.extensions.qiskit.AerUnitaryBackend` from `pytket-qiskit` is designed exactly for this.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerUnitaryBackend
+
+ circ = Circuit(2)
+ circ.H(0).CX(0, 1)
+ backend = AerUnitaryBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+
+ unitary = backend.run_circuit(compiled_circ).get_unitary()
+ print(unitary.round(5))
+```
+
+% Useful for obtaining high-precision results as well as verifying correctness of circuits
+
+% Utilities for mapping between shots/counts/probabilities/state and comparing statevectors/unitaries up to global phase
+
+Whilst the drive for quantum hardware is driven by the limited scalability of simulators, using statevector and unitary simulators will see long-term practical use to obtain high-precision results as well as for verifying the correctness of circuit designs. For the latter, we can assert that they match some expected reference state, but simply comparing the vectors/matrices may be too strict a test given that they could differ by a global phase but still be operationally equivalent. The utility methods {py:meth}`~pytket.utils.compare_statevectors()` and {py:meth}`~pytket.utils.compare_unitaries()` will compare two vectors/matrices for approximate equivalence accounting for global phase.
+
+
+```{code-cell} ipython3
+
+ from pytket.utils.results import compare_statevectors
+ import numpy as np
+
+ ref_state = np.asarray([1, 0, 1, 0]) / np.sqrt(2.) # |+0>
+ gph_state = np.asarray([1, 0, 1, 0]) * 1j / np.sqrt(2.) # i|+0>
+ prm_state = np.asarray([1, 1, 0, 0]) / np.sqrt(2.) # |0+>
+
+ print(compare_statevectors(ref_state, gph_state)) # Differ by global phase
+ print(compare_statevectors(ref_state, prm_state)) # Differ by qubit permutation
+```
+
+% Warning that interactions with classical data (conditional gates and measurements) or deliberately collapsing the state (Collapse and Reset) do not yield a deterministic result in this Hilbert space, so will be rejected
+
+Be warned that simulating any {py:class}`~pytket.circuit.Circuit` that interacts with classical data (e.g. conditional gates and measurements) or deliberately collapses the quantum state (e.g. `OpType.Collapse` and `OpType.Reset`) would not yield a deterministic result in the system's Hilbert space, so these will be rejected by the {py:class}`~pytket.backends.Backend`.
+
+## Interpreting Results
+
+Once we have obtained these results, we still have the matter of understanding what they mean. This corresponds to asking "which (qu)bit is which in this data structure?"
+
+% Ordering of basis elements/readouts (ILO vs DLO; requesting custom order)
+
+By default, the bits in readouts (shots and counts) are ordered in Increasing Lexicographical Order (ILO) with respect to their {py:class}`~pytket.unit_id.UnitID` s. That is, the register `c` will be listed completely before any bits in register `d`, and within each register the indices are given in increasing order. Many quantum software platforms including Qiskit and pyQuil will natively use the reverse order (Decreasing Lexicographical Order - DLO), so users familiar with them may wish to request that the order is changed when retrieving results.
+
+
+```{code-cell} ipython3
+
+ from pytket.circuit import Circuit, BasisOrder
+ from pytket.extensions.qiskit import AerBackend
+
+ circ = Circuit(2, 2)
+ circ.X(1).measure_all() # write 0 to c[0] and 1 to c[1]
+ backend = AerBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+ handle = backend.process_circuit(compiled_circ, n_shots=10)
+ result = backend.get_result(handle)
+
+ print(result.get_counts()) # ILO gives (c[0], c[1]) == (0, 1)
+ print(result.get_counts(basis=BasisOrder.dlo)) # DLO gives (c[1], c[0]) == (1, 0)
+```
+
+The choice of ILO or DLO defines the ordering of a bit sequence, but this can still be interpreted into the index of a statevector in two ways: by mapping the bits to a big-endian (BE) or little-endian (LE) integer. Every statevector and unitary in `pytket` uses a BE encoding (if LE is preferred, note that the ILO-LE interpretation gives the same result as DLO-BE for statevectors and unitaries, so just change the `basis` argument accordingly). The ILO-BE convention gives unitaries of individual gates as they typically appear in common textbooks [^cite_niel2001].
+
+
+```{code-cell} ipython3
+
+ from pytket.circuit import Circuit, BasisOrder
+ from pytket.extensions.qiskit import AerUnitaryBackend
+
+ circ = Circuit(2)
+ circ.CX(0, 1)
+ backend = AerUnitaryBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+ handle = backend.process_circuit(compiled_circ)
+ result = backend.get_result(handle)
+
+ print(result.get_unitary())
+ print(result.get_unitary(basis=BasisOrder.dlo))
+```
+
+Suppose that we only care about a subset of the measurements used in a {py:class}`~pytket.circuit.Circuit`. A shot table is a `numpy.ndarray`, so it can be filtered by column selections. To identify which columns need to be retained/removed, we are able to predict their column indices from the {py:class}`~pytket.circuit.Circuit` object. {py:attr}`pytket.Circuit.bit_readout` maps {py:class}`~pytket.unit_id.Bit` s to their column index (assuming the ILO convention).
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, Bit
+ from pytket.extensions.qiskit import AerBackend
+ from pytket.utils import expectation_from_shots
+
+ circ = Circuit(3, 3)
+ circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
+
+ circ.H(1) # Measure ZXY operator qubit-wise
+ circ.Rx(0.5, 2)
+ circ.measure_all()
+
+ backend = AerBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+ handle = backend.process_circuit(compiled_circ, 2000)
+ shots = backend.get_result(handle).get_shots()
+
+ # To extract the expectation value for ZIY, we only want to consider bits c[0] and c[2]
+ bitmap = compiled_circ.bit_readout
+ shots = shots[:, [bitmap[Bit(0)], bitmap[Bit(2)]]]
+ print(expectation_from_shots(shots))
+```
+
+If measurements occur at the end of the {py:class}`~pytket.circuit.Circuit`, then we can associate each measurement to the qubit that was measured. {py:attr}`~pytket.circuit.Circuit.qubit_readout` gives the equivalent map to column indices for {py:class}`~pytket.unit_id.Qubit` s, and {py:attr}`~pytket.circuit.Circuit.qubit_to_bit_map` relates each measured {py:class}`~pytket.unit_id.Qubit` to the {py:class}`~pytket.unit_id.Bit` that holds the corresponding measurement result.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, Qubit, Bit
+ circ = Circuit(3, 2)
+ circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2)
+ circ.Measure(0, 0)
+ circ.Measure(2, 1)
+
+ print(circ.bit_readout)
+ print(circ.qubit_readout)
+ print(circ.qubit_to_bit_map)
+```
+
+For more control over the bits extracted from the results, we can instead call {py:class}`~pytket.backends.Backend.get_result()`. The {py:class}`~pytket.backends.backendresult.BackendResult` object returned wraps up all the information returned from the experiment and allows it to be projected into any preferred way of viewing it. In particular, we can provide the list of {py:class}`~pytket.unit_id.Bit` s we want to look at in the shot table/counts dictionary, and given the exact permutation we want (and similarly for the permutation of {py:class}`~pytket.unit_id.Qubit` s for statevectors/unitaries).
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, Bit, Qubit
+ from pytket.extensions.qiskit import AerBackend, AerStateBackend
+
+ circ = Circuit(3)
+ circ.H(0).Ry(-0.3, 2)
+ state_b = AerStateBackend()
+ circ = state_b.get_compiled_circuit(circ)
+ handle = state_b.process_circuit(circ)
+
+ # Make q[1] the most-significant qubit, so interesting state uses consecutive coefficients
+ result = state_b.get_result(handle)
+ print(result.get_state([Qubit(1), Qubit(0), Qubit(2)]))
+
+ circ.measure_all()
+ shot_b = AerBackend()
+ circ = shot_b.get_compiled_circuit(circ)
+ handle = shot_b.process_circuit(circ, n_shots=2000)
+ result = shot_b.get_result(handle)
+
+ # Marginalise out q[0] from counts
+ print(result.get_counts())
+ print(result.get_counts([Bit(1), Bit(2)]))
+```
+
+## Expectation Value Calculations
+
+One of the most common calculations performed with a quantum state $\left| \psi \right>$ is to obtain an expectation value $\langle \psi | H | \psi \rangle$. For many applications, the operator $H$ can be expressed as a tensor product of Pauli matrices, or a linear combination of these. Given any (pure quantum) {py:class}`~pytket.circuit.Circuit` and any {py:class}`~pytket.backends.Backend`, the utility methods {py:meth}`~pytket.backends.Backend.get_pauli_expectation_value()` and {py:meth}`~pytket.backends.Backend.get_operator_expectation_value()` will generate the expectation value of the state under some operator using whatever results the {py:class}`~pytket.backends.Backend` supports. This includes adding measurements in the appropriate basis (if needed by the {py:class}`~pytket.backends.Backend`), running {py:class}`~pytket.backends.Backend.get_compiled_circuit()`, and obtaining and interpreting the results. For operators with many terms, it can optionally perform some basic measurement reduction techniques to cut down the number of {py:class}`~pytket.circuit.Circuit` s actually run by measuring multiple terms with simultaneous measurements in the same {py:class}`~pytket.circuit.Circuit`.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, Qubit
+ from pytket.extensions.qiskit import AerBackend
+ from pytket.partition import PauliPartitionStrat
+ from pytket.pauli import Pauli, QubitPauliString
+ from pytket.utils import get_pauli_expectation_value, get_operator_expectation_value
+ from pytket.utils.operators import QubitPauliOperator
+
+ circ = Circuit(3)
+ circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
+ backend = AerBackend()
+
+ zxy = QubitPauliString({
+ Qubit(0) : Pauli.Z,
+ Qubit(1) : Pauli.X,
+ Qubit(2) : Pauli.Y})
+ xzi = QubitPauliString({
+ Qubit(0) : Pauli.X,
+ Qubit(1) : Pauli.Z})
+ op = QubitPauliOperator({
+ QubitPauliString() : 0.3,
+ zxy : -1,
+ xzi : 1})
+ print(get_pauli_expectation_value(
+ circ,
+ zxy,
+ backend,
+ n_shots=2000))
+ print(get_operator_expectation_value(
+ circ,
+ op,
+ backend,
+ n_shots=2000,
+ partition_strat=PauliPartitionStrat.CommutingSets))
+```
+
+If you want a greater level of control over the procedure, then you may wish to write your own method for calculating $\langle \psi | H | \psi \rangle$. This is simple multiplication if we are given the statevector $| \psi \rangle$, but is slightly more complicated for measured systems. Since each measurement projects into either the subspace of +1 or -1 eigenvectors, we can assign +1 to each `0` readout and -1 to each `1` readout and take the average across all shots. When the desired operator is given by the product of multiple measurements, the contribution of +1 or -1 is dependent on the parity (XOR) of each measurement result in that shot. `pytket` provides some utility functions to wrap up this calculation and apply it to either a shot table ({py:meth}`~pytket.utils.expectation_from_shots()`) or a counts dictionary ({py:meth}`~pytket.utils.expectation_from_counts()`).
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerBackend
+ from pytket.utils import expectation_from_counts
+
+ circ = Circuit(3, 3)
+ circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
+
+ circ.H(1) # Want to measure expectation for Pauli ZXY
+ circ.Rx(0.5, 2) # Measure ZII, IXI, IIY separately
+ circ.measure_all()
+
+ backend = AerBackend()
+ compiled_circ = backend.get_compiled_circuit(circ)
+ handle = backend.process_circuit(compiled_circ, 2000)
+ counts = backend.get_result(handle).get_counts()
+ print(counts)
+ print(expectation_from_counts(counts))
+```
+
+% Obtaining indices of specific bits/qubits of interest using `bit_readout` and `qubit_readout` or `qubit_to_bit_map`, and filtering results
+
+:::{note}
+{py:meth}`~pytket.utils.expectation_from_shots()` and {py:meth}`~pytket.utils.expectation_from_counts()` take into account every classical bit in the results object. If the expectation value of interest is a product of only a subset of the measurements in the {py:class}`~pytket.circuit.Circuit` (as is the case when simultaneously measuring several commuting operators), then you will want to filter/marginalise out the ignored bits when performing this calculation.
+:::
+
+## Guidance for Writing Hardware-Agnostic Code
+
+Writing code for experiments that can be retargeted to different {py:class}`~pytket.backends.Backend` s can be a challenge, but has many great payoffs for long-term developments. Being able to experiment with new devices and simulators helps to identify which is best for the needs of the experiment and how this changes with the experiment parameters (such as size of chemical molecule being simulated, or choice of model to train for a neural network). Being able to react to changes in device availability helps get your results faster when contesting against queues for device access or downtime for maintenance, in addition to moving on if a device is retired from live service and taking advantage of the newest devices as soon as they come online. This is especially important in the near future as there is no clear frontrunner in terms of device, manufacturer, or even fundamental quantum technology, and the rate at which they are improving performance and scale is so high that it is essential to not get left behind with old systems.
+
+One of the major counter-arguments against developing hardware-agnostic experiments is that the manual incorporation of the target architecture's connectivity and noise characteristics into the circuit design and choice of error mitigation/detection/correction strategies obtains the optimal performance from the device. The truth is that hardware characteristics are highly variable over time, invalidating noise models after only a few hours [^cite_wils2020] and requiring regular recalibration. Over the lifetime of the device, this could lead to some qubits or couplers becoming so ineffective that they are removed from the system by the providers, giving drastic changes to the connectivity and admissible circuit designs. The instability of the experiment designs is difficult to argue when the optimal performance on one of today's devices is likely to be surpassed by an average performance on another device a short time after.
+
+We have already seen that devices and simulators will have different sets of requirements on the {py:class}`~pytket.circuit.Circuit` s they can accept and different types of results they can return, so hardware-agnosticism will not always come for free. The trick is to spot these differences and handle them on-the-fly at runtime. The design of the {py:class}`~pytket.backends.Backend` class in `pytket` aims to expose the fundamental requirements that require consideration for circuit design, compilation, and result interpretation in such a way that they can easily be queried at runtime to dynamically adapt the experiment procedure. All other aspects of backend interaction that are shared between different {py:class}`~pytket.backends.Backend` s are then unified for ease of integration. In practice, the constraints of the algorithm might limit the choice of {py:class}`~pytket.backends.Backend`, or we might choose to forego the ability to run on statevector simulators so that we only have to define the algorithm to calculate using counts, but we can still be agnostic within these categories.
+
+% Consider whether you will want to use backends with different requirements on measurements, e.g. for using both statevector simulators and real devices; maybe build state prep and measurement circuits separately
+
+The first point in an experiment where you might have to act differently between {py:class}`~pytket.backends.Backend` s is during {py:class}`~pytket.circuit.Circuit` construction. A {py:class}`~pytket.backends.Backend` may support a non-universal fragment of the {py:class}`~pytket.circuit.Circuit` language, typically relating to their interaction with classical data -- either full interaction, no mid-circuit measurement and conditional operations, or no measurement at all for statevector and unitary simulators. If the algorithm chosen requires mid-circuit measurement, then we must sacrifice some freedom of choice of {py:class}`~pytket.backends.Backend` to accommodate this. For safety, it could be beneficial to include assertions that the {py:class}`~pytket.backends.Backend` provided meets the expectations of the algorithm.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerBackend #, AerStateBackend
+ from pytket.predicates import NoMidMeasurePredicate
+
+ backend = AerBackend() # Choose backend in one place
+ # backend = AerStateBackend() # A backend that is incompatible with the experiment
+
+ # For algorithms using mid-circuit measurement, we can assert this is valid
+ qkd = Circuit(1, 3)
+ qkd.H(0).Measure(0, 0) # Prepare a random bit in the Z basis
+ qkd.H(0).Measure(0, 1).H(0) # Eavesdropper measures in the X basis
+ qkd.Measure(0, 2) # Recipient measures in the Z basis
+
+ assert backend.supports_counts # Using AerStateBackend would fail at this check
+ assert NoMidMeasurePredicate() not in backend.required_predicates
+ compiled_qkd = backend.get_compiled_circuit(qkd)
+ handle = backend.process_circuit(compiled_qkd, n_shots=1000)
+ print(backend.get_result(handle).get_counts())
+```
+
+:::{note}
+The same effect can be achieved by `assert backend.valid_circuit(qkd)` after compilation. However, when designing the compilation procedure manually, it is unclear whether a failure for this assertion would come from the incompatibility of the {py:class}`~pytket.backends.Backend` for the experiment or from the compilation failing.
+:::
+
+Otherwise, a practical solution around different measurement requirements is to separate the design into "state circuits" and "measurement circuits". At the point of running on the {py:class}`~pytket.backends.Backend`, we can then choose to either just send the state circuit for statevector calculations or compose it with the measurement circuits to run on sampling {py:class}`~pytket.backends.Backend` s.
+
+% Use `supports_X` to inspect type of backend used at runtime, or look at the requirements to see if measurements/conditionals are supported
+
+At runtime, we can check whether a particular result type is supported using the {py:attr}`Backend.supports_X` properties (such as {py:attr}`~pytket.backends.Backend.supports_counts`), whereas restrictions on the {py:class}`~pytket.circuit.Circuit` s supported can be inspected with {py:attr}`~pytket.backends.Backend.required_predicates`.
+
+% Compile generically, making use of `get_compiled_circuit`
+
+Whilst the demands of each {py:class}`~pytket.backends.Backend` on the properties of the {py:class}`~pytket.circuit.Circuit` necessitate different compilation procedures, using the default compilation sequences provided with {py:class}`~pytket.backends.Backend.get_compiled_circuit` handles compiling generically.
+
+% All backends can `process_circuit` identically
+
+Similarly, every {py:class}`~pytket.backends.Backend` can use {py:meth}`~pytket.backends.Backend.process_circuit` identically. Additional {py:class}`~pytket.backends.Backend`-specific arguments (such as the number of shots required or the seed for a simulator) will just be ignored if passed to a {py:class}`~pytket.backends.Backend` that does not use them.
+
+% Case split on retrieval again to handle statevector separately from samplers
+
+For the final steps of retrieving and interpreting the results, it suffices to just case-split on the form of data we can retrieve again with {py:attr}`Backend.supports_X`.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerBackend #, AerStateBackend
+ from pytket.utils import expectation_from_counts
+ import numpy as np
+
+ backend = AerBackend() # Choose backend in one place
+ # backend = AerStateBackend() # Alternative backend with different requirements and result type
+
+ # For many algorithms, we can separate the state preparation from measurements
+ circ = Circuit(2) # Apply e^{0.135 i pi XY} to the initial state
+ circ.H(0).V(1).CX(0, 1).Rz(-0.27, 1).CX(0, 1).H(0).Vdg(1)
+ measure = Circuit(2, 2) # Measure the YZ operator via YI and IZ
+ measure.V(0).measure_all()
+
+ if backend.supports_counts:
+ circ.append(measure)
+
+ circ = backend.get_compiled_circuit(circ)
+ handle = backend.process_circuit(circ, n_shots=2000)
+
+ expectation = 0
+ if backend.supports_state:
+ yz = np.asarray([
+ [0, 0, -1j, 0],
+ [0, 0, 0, 1j],
+ [1j, 0, 0, 0],
+ [0, -1j, 0, 0]])
+ svec = backend.get_result(handle).get_state()
+ expectation = np.vdot(svec, yz.dot(svec))
+ else:
+ counts = backend.get_result(handle).get_counts()
+ expectation = expectation_from_counts(counts)
+
+ print(expectation)
+
+```
+
+## Batch Submission
+
+% Some devices are accessed via queues or long-latency connections; batching helps reduce the amount of time spent waiting by pipelining
+
+Current public-access quantum computers tend to implement either a queueing or a reservation system for mediating access. Whilst the queue-based access model gives relative fairness, guarantees availability, and maximises throughput and utilisation of the hardware, it also presents a big problem to the user with regards to latency. Whenever a circuit is submitted, not only must the user wait for it to be run on the hardware, but they must wait longer for their turn before it can even start running. This can end up dominating the time taken for the overall experiment, especially when demand is high for a particular device.
+
+We can mitigate the problem of high queue latency by batching many {py:class}`~pytket.circuit.Circuit` s together. This means that we only have to wait the queue time once, since after the first {py:class}`~pytket.circuit.Circuit` is run the next one is run immediately rather than joining the end of the queue.
+
+% Advisable to generate as many circuits of interest as possible, storing how to interpret the results of each one, then send them off together
+
+% After processing, interpret the results by querying the result handles
+
+To maximise the benefits of batch submission, it is advisable to generate as many of your {py:class}`~pytket.circuit.Circuit` s as possible at the same time to send them all off together. This is possible when, for example, generating every measurement circuit for an expectation value calculation, or sampling several parameter values from a local neighbourhood in a variational procedure. The method {py:class}`~pytket.backends.Backend.process_circuits()` (plural) will then submit all the provided {py:class}`~pytket.circuit.Circuit` s simultaneously and return a {py:class}`~pytket.backends.resulthandle.ResultHandle` for each {py:class}`~pytket.circuit.Circuit` to allow each result to be extracted individually for interpretation. Since there is no longer a single {py:class}`~pytket.circuit.Circuit` being handled from start to finish, it may be necessary to store additional data to record how to interpret them, like the set of {py:class}`~pytket.unit_id.Bit` s to extract for each {py:class}`~pytket.circuit.Circuit` or the coefficient to multiply the expectation value by.
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.utils import expectation_from_counts
+
+backend = IBMQBackend("ibmq_quito")
+
+state = Circuit(3)
+state.H(0).CX(0, 1).CX(1, 2).X(0)
+
+# Compute expectation value for -0.3i ZZZ + 0.8 XZZ + 1.2 XXX
+zzz = Circuit(3, 3)
+zzz.measure_all()
+xzz = Circuit(3, 3)
+xzz.H(0).measure_all()
+xxx = Circuit(3, 3)
+xxx.H(0).H(1).H(2).measure_all()
+
+circ_list = []
+for m in [zzz, xzz, xxx]:
+ c = state.copy()
+ c.append(m)
+ circ_list.append(c)
+
+coeff_list = [
+ -0.3j, # ZZZ
+ 0.8, # XZZ
+ 1.2 # XXX
+]
+circ_list = backend.get_compiled_circuits(circ_list)
+
+handle_list = backend.process_circuits(circ_list, n_shots=2000)
+result_list = backend.get_results(handle_list)
+
+expectation = 0
+for coeff, result in zip(coeff_list, result_list):
+ counts = result.get_counts()
+ expectation += coeff * expectation_from_counts(counts)
+
+print(expectation)
+```
+
+
+```
+(1.2047999999999999-0.0015000000000000013j)
+```
+
+:::{note}
+Currently, only some devices (e.g. those from IBMQ, Quantinuum and Amazon Braket) support a queue model and benefit from this methodology, though more may adopt this in future. The {py:class}`~pytket.extensions.qiskit.AerBackend` simulator and the {py:class}`~pytket.extensions.quantinuum.QuantinuumBackend` can take advantage of batch submission for parallelisation. In other cases, {py:class}`~pytket.backends.Backend.process_circuits` will just loop through each {py:class}`~pytket.circuit.Circuit` in turn.
+:::
+
+## Embedding into Qiskit
+
+Not only is the goal of tket to be a device-agnostic platform, but also interface-agnostic, so users are not obliged to have to work entirely in tket to benefit from the wide range of devices supported. For example, Qiskit is currently the most widely adopted quantum software development platform, providing its own modules for building and compiling circuits, submitting to backends, applying error mitigation techniques and combining these into higher-level algorithms. Each {py:class}`~pytket.backends.Backend` in `pytket` can be wrapped up to imitate a Qiskit backend, allowing the benefits of tket to be felt in existing Qiskit projects with minimal work.
+
+Below we show how the {py:class}`~pytket.extensions.cirq.CirqStateSampleBackend` from the `pytket-cirq` extension can be used with its {py:meth}`default_compilation_pass` directly in qiskit.
+
+
+```{code-cell} ipython3
+
+from qiskit.primitives import BackendSampler
+from qiskit_algorithms import Grover, AmplificationProblem
+from qiskit.circuit import QuantumCircuit
+
+from pytket.extensions.cirq import CirqStateSampleBackend
+from pytket.extensions.qiskit.tket_backend import TketBackend
+
+cirq_simulator = CirqStateSampleBackend()
+backend = TketBackend(cirq_simulator, cirq_simulator.default_compilation_pass())
+sampler_options = {"shots":1024}
+qsampler = BackendSampler(backend, options=sampler_options)
+
+oracle = QuantumCircuit(2)
+oracle.cz(0, 1)
+
+def is_good_state(bitstr):
+ return sum(map(int, bitstr)) == 2
+
+problem = AmplificationProblem(oracle=oracle, is_good_state=is_good_state)
+grover = Grover(sampler=qsampler)
+result = grover.amplify(problem)
+print("Top measurement:", result.top_measurement)
+```
+
+:::{note}
+Since Qiskit may not be able to solve all of the constraints of the chosen device/simulator, some compilation may be required after a circuit is passed to the {py:class}`TketBackend`, or it may just be preferable to do so to take advantage of the sophisticated compilation solutions provided in `pytket`. Upon constructing the {py:class}`TketBackend`, you can provide a `pytket` compilation pass to apply to each circuit, e.g. `TketBackend(backend, backend.default_compilation_pass())`. Some experimentation may be required to find a combination of `qiskit.transpiler.PassManager` and `pytket` compilation passes that executes successfully.
+:::
+
+% Pytket Assistant
+
+% ----------------
+
+% Goals of the assistant
+
+% How to set up and push/pull results
+
+## Advanced Backend Topics
+
+### Simulator Support for Expectation Values
+
+% May be faster to apply simple expectation values within the internal representation of simulator
+
+% `supports_expectation`
+
+% Examples of Pauli and operator expectations
+
+Some simulators will have dedicated support for fast expectation value calculations. In this special case, they will provide extra methods {py:class}`~pytket.backends.Backend.get_pauli_expectation_value()` and {py:class}`~pytket.backends.Backend.get_operator_expectation_value()`, which take a {py:class}`~pytket.circuit.Circuit` and some operator and directly return the expectation value. Again, we can check whether a {py:class}`~pytket.backends.Backend` has this feature with {py:attr}`~pytket.backends.Backend.supports_expectation`.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, Qubit
+ from pytket.extensions.qiskit import AerStateBackend
+ from pytket.pauli import Pauli, QubitPauliString
+ from pytket.utils.operators import QubitPauliOperator
+
+ backend = AerStateBackend()
+
+ state = Circuit(3)
+ state.H(0).CX(0, 1).V(2)
+
+ xxy = QubitPauliString({
+ Qubit(0) : Pauli.X,
+ Qubit(1) : Pauli.X,
+ Qubit(2) : Pauli.Y})
+ zzi = QubitPauliString({
+ Qubit(0) : Pauli.Z,
+ Qubit(1) : Pauli.Z})
+ iiz = QubitPauliString({
+ Qubit(2) : Pauli.Z})
+ op = QubitPauliOperator({
+ QubitPauliString() : -0.5,
+ xxy : 0.7,
+ zzi : 1.4,
+ iiz : 3.2})
+
+ assert backend.supports_expectation
+ state = backend.get_compiled_circuit(state)
+ print(backend.get_pauli_expectation_value(state, xxy))
+ print(backend.get_operator_expectation_value(state, op))
+```
+
+### Asynchronous Job Submission
+
+% Checking circuit status
+
+% Blocking on retrieval
+
+In the near future, as we look to more sophisticated algorithms and larger problem instances, the quantity and size of {py:class}`~pytket.circuit.Circuit` s to be run per experiment and the number of shots required to obtain satisfactory precision will mean the time taken for the quantum computation could exceed that of the classical computation. At this point, the overall algorithm can be sped up by maintaining maximum throughput on the quantum device and minimising how often the quantum device is left idle whilst the classical system is determining the next {py:class}`~pytket.circuit.Circuit` s to send. This can be achieved by writing your algorithm to operate asynchronously.
+
+The intended semantics of the {py:meth}`~pytket.backends.Backend` methods are designed to enable asynchronous execution of quantum programs whenever admissible from the underlying API provided by the device/simulator. {py:meth}`~pytket.backends.Backend.process_circuit()` and {py:meth}`~pytket.backends.Backend.process_circuits()` will submit the {py:class}`~pytket.circuit.Circuit` (s) and immediately return.
+
+The progress can be checked by querying {py:meth}`~pytket.backends.Backend.circuit_status()`. If this returns a {py:class}`~pytket.backends.status.CircuitStatus` matching `StatusEnum.COMPLETED`, then {py:meth}`~pytket.backends.Backend.get_X()` will obtain the results and return immediately, otherwise it will block the thread and wait until the results are available.
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+import asyncio
+from pytket import Circuit
+from pytket.backends import StatusEnum
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.utils import expectation_from_counts
+
+backend = IBMQBackend("ibmq_quito")
+
+state = Circuit(3)
+state.H(0).CX(0, 1).CX(1, 2).X(0)
+
+# Compute expectation value for -0.3i ZZZ + 0.8 XZZ + 1.2 XXX
+zzz = Circuit(3, 3)
+zzz.measure_all()
+xzz = Circuit(3, 3)
+xzz.H(0).measure_all()
+xxx = Circuit(3, 3)
+xxx.H(0).H(1).H(2).measure_all()
+
+circ_list = []
+for m in [zzz, xzz, xxx]:
+ c = state.copy()
+ c.append(m)
+ circ_list.append(backend.get_compiled_circuit(c))
+
+coeff_list = [
+ -0.3j, # ZZZ
+ 0.8, # XZZ
+ 1.2 # XXX
+]
+
+handle_list = backend.process_circuits(circ_list, n_shots=2000)
+
+async def check_at_intervals(backend, handle, interval):
+ while True:
+ await asyncio.sleep(interval)
+ status = backend.circuit_status(handle)
+ if status.status in (StatusEnum.COMPLETED, StatusEnum.ERROR):
+ return status
+
+async def expectation(backend, handle, coeff):
+ await check_at_intervals(backend, handle, 5)
+ counts = backend.get_result(handle).get_counts()
+ return coeff * expectation_from_counts(counts)
+
+async def main():
+ task_set = set([asyncio.create_task(expectation(backend, h, c)) for h, c in zip(handle_list, coeff_list)])
+ done, pending = await asyncio.wait(task_set, return_when=asyncio.ALL_COMPLETED)
+ sum = 0
+ for t in done:
+ sum += await t
+
+ print(sum)
+
+asyncio.run(main())
+```
+
+
+```
+(1.2087999999999999-0.002400000000000002j)
+```
+
+In some cases you may want to end execution early, perhaps because it is taking too long or you already have all the data you need. You can use the {py:meth}`~pytket.backends.Backend.cancel()` method to cancel the job for a given {py:class}`~pytket.backends.resulthandle.ResultHandle`. This is recommended to help reduce load on the devices if you no longer need to run the submitted jobs.
+
+:::{note}
+Asynchronous submission is currently available with the {py:class}`~pytket.extensions.qiskit.IBMQBackend`, {py:class}`~pytket.extensions.quantinuum.QuantinuumBackend`, {py:class}`BraketBackend` and {py:class}`~pytket.extensions.qiskit.AerBackend`. It will be extended to others in future updates.
+:::
+
+### Persistent Handles
+
+Being able to split your processing into distinct procedures for {py:class}`~pytket.circuit.Circuit` generation and result interpretation can help improve throughput on the quantum device, but it can also provide a way to split the processing between different Python sessions. This may be desirable when the classical computation to interpret the results and determine the next experiment parameters is sufficiently intensive that we would prefer to perform it offline and only reserve a quantum device once we are ready to run more. Furthermore, resuming with previously-generated results could benefit repeatability of experiments and better error-safety since the logged results can be saved and reused.
+
+Some {py:class}`~pytket.backends.Backend` s support persistent handles, in that the {py:class}`~pytket.backends.resulthandle.ResultHandle` object can be stored and the associated results obtained from another instance of the same {py:class}`~pytket.backends.Backend` in a different session. This is indicated by the boolean `persistent_handles` property of the {py:class}`~pytket.backends.Backend`. Use of persistent handles can greatly reduce the amount of logging you would need to do to take advantage of this workflow.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+from pytket import Circuit
+from pytket.extensions.qiskit import IBMQBackend
+
+backend = IBMQBackend("ibmq_quito")
+
+circ = Circuit(3, 3)
+circ.X(1).CZ(0, 1).CX(1, 2).measure_all()
+circ = backend.get_compiled_circuit(circ)
+handle = backend.process_circuit(circ, n_shots=1000)
+
+# assert backend.persistent_handles
+print(str(handle))
+counts = backend.get_result(handle).get_counts()
+print(counts)
+```
+
+
+```
+('5e8f3dcbbb7d8500119cfbf6', 0)
+{(0, 1, 1): 1000}
+```
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket.backends import ResultHandle
+from pytket.extensions.qiskit import IBMQBackend
+
+backend = IBMQBackend("ibmq_quito")
+
+handle = ResultHandle.from_str("('5e8f3dcbbb7d8500119cfbf6', 0)")
+counts = backend.get_result(handle).get_counts()
+print(counts)
+```
+
+
+```
+
+{(0, 1, 1): 1000}
+
+```
+
+### Result Serialization
+
+When performing experiments using {py:class}`~pytket.backends.Backend` s, it is often useful to be able to easily store and retrieve the results for later analysis or backup.
+This can be achieved using native serialiaztion and deserialization of {py:class}`~pytket.backends.backendresult.BackendResult` objects from JSON compatible dictionaries, using the {py:meth}`~pytket.backends.backendresult.BackendResult.to_dict()` and {py:meth}`~pytket.backends.backendresult.BackendResult.from_dict()` methods.
+
+
+```{code-cell} ipython3
+
+import tempfile
+import json
+from pytket import Circuit
+from pytket.backends.backendresult import BackendResult
+from pytket.extensions.qiskit import AerBackend
+
+circ = Circuit(2, 2)
+circ.H(0).CX(0, 1).measure_all()
+
+backend = AerBackend()
+handle = backend.process_circuit(circ, 10)
+res = backend.get_result(handle)
+
+with tempfile.TemporaryFile('w+') as fp:
+ json.dump(res.to_dict(), fp)
+ fp.seek(0)
+ new_res = BackendResult.from_dict(json.load(fp))
+
+print(new_res.get_counts())
+```
+
+[^cite_niel2001]: Nielsen, M.A. and Chuang, I.L., 2001. Quantum computation and quantum information. Phys. Today, 54(2), p.60.
+
+[^cite_wils2020]: Wilson, E., Singh, S. and Mueller, F., 2020. Just-in-time Quantum Circuit Transpilation Reduces Noise. arXiv preprint arXiv:2005.12820.
diff --git a/docs/manual/manual_backend.rst b/docs/manual/manual_backend.rst
deleted file mode 100644
index a202b479..00000000
--- a/docs/manual/manual_backend.rst
+++ /dev/null
@@ -1,765 +0,0 @@
-*******************
-Running on Backends
-*******************
-
-.. Co-processor model of QC; circuits are the units of tasks
-
-The interaction model for quantum computing in the near future is set to follow the co-processor model: there is a main program running on the classical host computer which routinely sends off jobs to a specialist device that can handle that class of computation efficiently, similar to interacting with GPUs and cloud HPC resources. We have already seen how to use ``pytket`` to describe a job to be performed with the :py:class:`~pytket.circuit.Circuit` class; the next step is to look at how we interact with the co-processor to run it.
-
-.. Backends manage sending the circuits to be processed (by simulator or device) and retrieving results; general workflow of compile, process, retrieve
-
-
-A :py:class:`~pytket.backends.Backend` represents a connection to some co-processor instance, which can be either quantum hardware or a simulator. It presents a uniform interface for submitting :py:class:`~pytket.circuit.Circuit` s to be processed and retrieving the results, allowing the general workflow of "generate, compile, process, retrieve, interpret" to be performed with little dependence on the specific co-processor used. This is to promote the development of platform-independent software, helping the code that you write to be more future-proof and encouraging exploration of the ever-changing landscape of quantum hardware solutions.
-
-With the wide variety of :py:class:`~pytket.backends.Backend` s available, they naturally have very different capabilities and limitations. The class is designed to open up this information so that it is easy to examine at runtime and make hardware-dependent choices as needed for maximum performance, whilst providing a basic abstract model that is easy for proof-of-concept testing.
-
-No :py:class:`~pytket.backends.Backend` s are currently provided with the core `pytket `_ package, but most extension modules will add simulators or devices from the given provider, such as the :py:class:`~pytket.extensions.qiskit.AerBackend` and :py:class:`~pytket.extensions.qiskit.IBMQBackend` with `pytket-qiskit `_ or the :py:class:`~pytket.extensions.quantinuum.QuantinuumBackend` with `pytket-quantinuum `_.
-
-Backend Requirements
---------------------
-
-.. Not every circuit can be run immediately on a device or simulator; restrictions put in place for ease of implementation or limitations of engineering or noise
-.. Devices and simulators are designed to only support a small gate set, but since they are universal, it is enough to compile to them
-
-Every device and simulator will have some restrictions to allow for a simpler implementation or because of the limits of engineering or noise within a device. For example, devices and simulators are typically designed to only support a small (but universal) gate set, so a :py:class:`~pytket.circuit.Circuit` containing other gate types could not be run immediately. However, as long as the fragment supported is universal, it is enough to be able to compile down to a semantically-equivalent :py:class:`~pytket.circuit.Circuit` which satisfies the requirements, for example, by translating each unknown gate into sequences of known gates.
-
-.. Other common restrictions are on the number and connectivity of qubits - a multi-qubit gate may only be possible to perform on adjacent qubits on the architecture
-
-Other common restrictions presented by QPUs include the number of available qubits and their connectivity (multi-qubit gates may only be performed between adjacent qubits on the architecture). Measurements may also be noisy or take a long time on some QPUs, leading to the destruction or decoherence of any remaining quantum state, so they are artificially restricted to only happen in a single layer at the end of execution and mid-circuit measurements are rejected. More extremely, some classes of classical simulators will reject measurements entirely as they are designed to simulate pure quantum circuits (for example, when looking to yield a statevector or unitary deterministically).
-
-.. Each restriction on the circuits is captured by a `Predicate`
-.. Querying the requirements of a given backend
-
-Each :py:class:`~pytket.backends.Backend` object is aware of the restrictions of the underlying device or simulator, encoding them as a collection of :py:class:`~pytket.predictate.Predicate` s. Each :py:class:`~pytket.predictate.Predicate` is essentially a Boolean property of a :py:class:`~pytket.circuit.Circuit` which must return ``True`` for the :py:class:`~pytket.circuit.Circuit` to successfully run. The set of :py:class:`~pytket.predicates.Predicate` s required by a :py:class:`~pytket.backends.Backend` can be queried with :py:attr:`~pytket.backends.Backend.required_predicates`.
-
-.. jupyter-input::
-
- from pytket.extensions.qiskit import IBMQBackend, AerStateBackend
-
- dev_b = IBMQBackend("ibmq_quito")
- sim_b = AerStateBackend()
- print(dev_b.required_predicates)
- print(sim_b.required_predicates)
-
-.. jupyter-output::
-
- [NoClassicalControlPredicate, NoFastFeedforwardPredicate, NoMidMeasurePredicate, NoSymbolsPredicate, GateSetPredicate:{ U1 noop U2 CX Barrier Measure U3 }, DirectednessPredicate:{ Nodes: 5, Edges: 8 }]
- [NoClassicalControlPredicate, NoFastFeedforwardPredicate, GateSetPredicate:{ CU1 CZ CX Unitary2qBox Sdg U1 Unitary1qBox SWAP S U2 CCX Y U3 Z X T noop Tdg Reset H }]
-
-.. Can check if a circuit satisfies all requirements with `valid_circuit`
-.. `get_compiled_circuit` modifies a circuit to try to satisfy all backend requirements if possible (restrictions on measurements or conditional gate support may not be fixed by compilation)
-
-Knowing the requirements of each :py:class:`~pytket.backends.Backend` is handy in case it has consequences for how you design a :py:class:`~pytket.circuit.Circuit`, but can generally be abstracted away. Calling :py:meth:`~pytket.backends.Backend.valid_circuit()` can check whether or not a :py:class:`~pytket.circuit.Circuit` satisfies every requirement to run on the :py:class:`~pytket.backends.Backend`, and if it is not immediately valid then :py:meth:`~pytket.backends.Backend.get_compiled_circuit` will try to solve all of the remaining constraints when possible (note that restrictions on measurements or conditional gate support may not be fixed by compilation), and return a new :py:class:`~pytket.circuit.Circuit`.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.extensions.qiskit import AerBackend
-
- circ = Circuit(3, 2)
- circ.H(0).Ry(0.25, 1)
- circ.add_gate(OpType.CnRy, [0.74], [0, 1, 2]) # CnRy not in AerBackend gate set
- circ.measure_all()
-
- backend = AerBackend()
- print("Circuit valid for AerBackend?", backend.valid_circuit(circ))
- compiled_circ = backend.get_compiled_circuit(circ) # Compile circuit to AerBackend
-
- print("Compiled circuit valid for AerBackend?", backend.valid_circuit(compiled_circ))
-
-Now that we can prepare our :py:class:`~pytket.circuit.Circuit` s to be suitable for a given :py:class:`~pytket.backends.Backend`, we can send them off to be run and examine the results. This is always done by calling :py:meth:`~pytket.backends.Backend.process_circuit()` which sends a :py:class:`~pytket.circuit.Circuit` for execution and returns a :py:class:`~pytket.backends.resulthandle.ResultHandle` as an identifier for the job which can later be used to retrieve the actual results once the job has finished.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerStateBackend
-
- circ = Circuit(2, 2)
- circ.Rx(0.3, 0).Ry(0.5, 1).CRz(-0.6, 1, 0)
- backend = AerStateBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ)
-
-The exact arguments to :py:meth:`~pytket.backends.Backend.process_circuit` and the means of retrieving results back are dependent on the type of data the :py:class:`~pytket.backends.Backend` can produce and whether it samples measurements or calculates the internal state of the quantum system.
-
-Shots and Sampling
-------------------
-
-.. On real devices, cannot directly inspect the statevector of quantum system, so only classical output is the results of measurements
-.. Measurements are not deterministic, so each run samples from some distribution; refer to each full run of the circuit from the initial state as a "shot"
-
-Running a :py:class:`~pytket.circuit.Circuit` on a quantum computer invovles applying the instructions to some quantum system to modify its state. Whilst we know that this state will form a vector (or linear map) in some Hilbert space, we cannot directly inspect it and obtain a complex vector/matrix to return to the classical host process. The best we can achieve is performing measurements to collapse the state and obtain a bit of information in the process. Since the measurements are not deterministic, each run of the :py:class:`~pytket.circuit.Circuit` samples from some distribution. By obtaining many *shots* (the classical readout from each full run of the :py:class:`~pytket.circuit.Circuit` from the initial state), we can start to predict what the underlying measurement distrubution looks like.
-
-.. Retrieve table of results using `get_shots`; rows are shots (in order of execution), columns are bits (in ILO)
-
-The interaction with a QPU (or a simulator that tries to imitate a device by sampling from the underlying complex statevector) is focused around requesting shots for a given :py:class:`~pytket.circuit.Circuit`. The number of shots required is passed to :py:meth:`~pytket.backends.Backend.process_circuit()`. The result is retrieved using :py:meth:`~pytket.backends.Backend.get_result()`; and the shots are then given as a table from :py:meth:`~pytket.backends.BackendResult.get_shots()`: each row of the table describes a shot in the order of execution, and the columns are the classical bits from the :py:class:`~pytket.circuit.Circuit`.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
-
- circ = Circuit(2, 2)
- circ.H(0).X(1).measure_all()
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
-
- handle = backend.process_circuit(compiled_circ, n_shots=20)
- shots = backend.get_result(handle).get_shots()
- print(shots)
-
-.. Often interested in probabilities of each measurement outcome, so need many shots for high precision
-.. Even if we expect a single peak in distribution, will want many shots to account for noise
-
-For most applications, we are interested in the probability of each measurement outcome, so we need many shots for each experiment for high precision (it is quite typical to ask for several thousand or more). Even if we expect a single sharp peak in the distribution, as is the case from many of the popular textbook quantum algorithms (Deutsch-Jozsa, Bernstein-Vazirani, Shor, etc.), we will generally want to take many shots to help account for noise.
-
-.. If we don't need order of results, can get summary of counts using `get_counts`
-
-If we don't care about the temporal order of the shots, we can instead retrieve a compact summary of the frequencies of observed results. The dictionary returned by :py:meth:`~pytket.backends.BackendResult.get_counts` maps tuples of bits to the number of shots that gave that result (keys only exist in the dictionary if this is non-zero). If probabilities are preferred to frequencies, we can apply the utility method :py:meth:`~pytket.backends.BackendResult.probs_from_counts()`.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
- from pytket.utils import probs_from_counts
-
- circ = Circuit(2, 2)
- circ.H(0).X(1).measure_all()
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
-
- handle = backend.process_circuit(compiled_circ, n_shots=2000)
- counts = backend.get_result(handle).get_counts()
- print(counts)
-
- print(probs_from_counts(counts))
-
-.. note:: :py:class:`~pytket.backends.Backend.process_circuit` returns a handle to the computation to perform the quantum computation asynchronously. Non-blocking operations are essential when running circuits on remote devices, as submission and queuing times can be long. The handle may then be used to retrieve the results with :py:class:`~pytket.backends.Backend.get_result`. If asynchronous computation is not needed, for example when running on a local simulator, pytket provides the shortcut :py:meth:`pytket.backends.Backend.run_circuit` that will immediately execute the circuit and return a :py:class:`~pytket.backends.backendresult.BackendResult`.
-
-Statevector and Unitary Simulation with TKET Backends
------------------------------------------------------
-
-.. Any form of sampling introduces non-deterministic error, so for better accuracy we will want the exact state of the physical system; some simulators will provide direct access to this
-.. `get_state` gives full representation of that system's state in the 2^n-dimensional complex Hilbert space
-
-Any form of sampling from a distribution will introduce sampling error and (unless it is a seeded simulator) non-deterministic results, whereas we could get much better accuracy and repeatability if we have the exact state of the underlying physical quantum system. Some simulators will provide direct access to this. The :py:meth:`~pytket.backends.BackendResult.get_state` method will give the full representation of the physical state as a vector in the :math:`2^n`-dimensional Hilbert space, whenever the underlying simulator provides this.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerStateBackend
-
- circ = Circuit(3)
- circ.H(0).CX(0, 1).S(1).X(2)
- backend = AerStateBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
-
- state = backend.run_circuit(compiled_circ).get_state()
- print(state.round(5))
-
-.. note:: We have rounded the results here because simulators typically introduce a small amount of floating-point error, so killing near-zero entries gives a much more readable representation.
-
-.. `get_unitary` treats circuit with open inputs and gives map on 2^n-dimensional complex Hilbert space
-
-The majority of :py:class:`~pytket.backends.Backend` s will run the :py:class:`~pytket.circuit.Circuit` on the initial state :math:`|0\rangle^{\otimes n}`. However, because we can form the composition of :py:class:`~pytket.circuit.Circuit` s, we want to be able to test them with open inputs. When the :py:class:`~pytket.circuit.Circuit` is purely quantum, we can represent its effect as an open circuit by a unitary matrix acting on the :math:`2^n`-dimensional Hilbert space. The :py:class:`~pytket.extensions.qiskit.AerUnitaryBackend` from ``pytket-qiskit`` is designed exactly for this.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerUnitaryBackend
-
- circ = Circuit(2)
- circ.H(0).CX(0, 1)
- backend = AerUnitaryBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
-
- unitary = backend.run_circuit(compiled_circ).get_unitary()
- print(unitary.round(5))
-
-.. Useful for obtaining high-precision results as well as verifying correctness of circuits
-.. Utilities for mapping between shots/counts/probabilities/state and comparing statevectors/unitaries up to global phase
-
-Whilst the drive for quantum hardware is driven by the limited scalability of simulators, using statevector and unitary simulators will see long-term practical use to obtain high-precision results as well as for verifying the correctness of circuit designs. For the latter, we can assert that they match some expected reference state, but simply comparing the vectors/matrices may be too strict a test given that they could differ by a global phase but still be operationally equivalent. The utility methods :py:meth:`~pytket.utils.compare_statevectors()` and :py:meth:`~pytket.utils.compare_unitaries()` will compare two vectors/matrices for approximate equivalence accounting for global phase.
-
-.. jupyter-execute::
-
- from pytket.utils.results import compare_statevectors
- import numpy as np
-
- ref_state = np.asarray([1, 0, 1, 0]) / np.sqrt(2.) # |+0>
- gph_state = np.asarray([1, 0, 1, 0]) * 1j / np.sqrt(2.) # i|+0>
- prm_state = np.asarray([1, 1, 0, 0]) / np.sqrt(2.) # |0+>
-
- print(compare_statevectors(ref_state, gph_state)) # Differ by global phase
- print(compare_statevectors(ref_state, prm_state)) # Differ by qubit permutation
-
-.. Warning that interactions with classical data (conditional gates and measurements) or deliberately collapsing the state (Collapse and Reset) do not yield a deterministic result in this Hilbert space, so will be rejected
-
-Be warned that simulating any :py:class:`~pytket.circuit.Circuit` that interacts with classical data (e.g. conditional gates and measurements) or deliberately collapses the quantum state (e.g. ``OpType.Collapse`` and ``OpType.Reset``) would not yield a deterministic result in the system's Hilbert space, so these will be rejected by the :py:class:`~pytket.backends.Backend`.
-
-Interpreting Results
---------------------
-
-Once we have obtained these results, we still have the matter of understanding what they mean. This corresponds to asking "which (qu)bit is which in this data structure?"
-
-.. Ordering of basis elements/readouts (ILO vs DLO; requesting custom order)
-
-By default, the bits in readouts (shots and counts) are ordered in Increasing Lexicographical Order (ILO) with respect to their :py:class:`~pytket.unit_id.UnitID` s. That is, the register ``c`` will be listed completely before any bits in register ``d``, and within each register the indices are given in increasing order. Many quantum software platforms including Qiskit and pyQuil will natively use the reverse order (Decreasing Lexicographical Order - DLO), so users familiar with them may wish to request that the order is changed when retrieving results.
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, BasisOrder
- from pytket.extensions.qiskit import AerBackend
-
- circ = Circuit(2, 2)
- circ.X(1).measure_all() # write 0 to c[0] and 1 to c[1]
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, n_shots=10)
- result = backend.get_result(handle)
-
- print(result.get_counts()) # ILO gives (c[0], c[1]) == (0, 1)
- print(result.get_counts(basis=BasisOrder.dlo)) # DLO gives (c[1], c[0]) == (1, 0)
-
-The choice of ILO or DLO defines the ordering of a bit sequence, but this can still be interpreted into the index of a statevector in two ways: by mapping the bits to a big-endian (BE) or little-endian (LE) integer. Every statevector and unitary in ``pytket`` uses a BE encoding (if LE is preferred, note that the ILO-LE interpretation gives the same result as DLO-BE for statevectors and unitaries, so just change the ``basis`` argument accordingly). The ILO-BE convention gives unitaries of individual gates as they typically appear in common textbooks [Niel2001]_.
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, BasisOrder
- from pytket.extensions.qiskit import AerUnitaryBackend
-
- circ = Circuit(2)
- circ.CX(0, 1)
- backend = AerUnitaryBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ)
- result = backend.get_result(handle)
-
- print(result.get_unitary())
- print(result.get_unitary(basis=BasisOrder.dlo))
-
-Suppose that we only care about a subset of the measurements used in a :py:class:`~pytket.circuit.Circuit`. A shot table is a ``numpy.ndarray``, so it can be filtered by column selections. To identify which columns need to be retained/removed, we are able to predict their column indices from the :py:class:`~pytket.circuit.Circuit` object. :py:attr:`pytket.Circuit.bit_readout` maps :py:class:`~pytket.unit_id.Bit` s to their column index (assuming the ILO convention).
-
-.. jupyter-execute::
-
- from pytket import Circuit, Bit
- from pytket.extensions.qiskit import AerBackend
- from pytket.utils import expectation_from_shots
-
- circ = Circuit(3, 3)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
-
- circ.H(1) # Measure ZXY operator qubit-wise
- circ.Rx(0.5, 2)
- circ.measure_all()
-
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, 2000)
- shots = backend.get_result(handle).get_shots()
-
- # To extract the expectation value for ZIY, we only want to consider bits c[0] and c[2]
- bitmap = compiled_circ.bit_readout
- shots = shots[:, [bitmap[Bit(0)], bitmap[Bit(2)]]]
- print(expectation_from_shots(shots))
-
-If measurements occur at the end of the :py:class:`~pytket.circuit.Circuit`, then we can associate each measurement to the qubit that was measured. :py:attr:`~pytket.circuit.Circuit.qubit_readout` gives the equivalent map to column indices for :py:class:`~pytket.unit_id.Qubit` s, and :py:attr:`~pytket.circuit.Circuit.qubit_to_bit_map` relates each measured :py:class:`~pytket.unit_id.Qubit` to the :py:class:`~pytket.unit_id.Bit` that holds the corresponding measurement result.
-
-.. jupyter-execute::
-
- from pytket import Circuit, Qubit, Bit
- circ = Circuit(3, 2)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2)
- circ.Measure(0, 0)
- circ.Measure(2, 1)
-
- print(circ.bit_readout)
- print(circ.qubit_readout)
- print(circ.qubit_to_bit_map)
-
-For more control over the bits extracted from the results, we can instead call :py:class:`~pytket.backends.Backend.get_result()`. The :py:class:`~pytket.backends.backendresult.BackendResult` object returned wraps up all the information returned from the experiment and allows it to be projected into any preferred way of viewing it. In particular, we can provide the list of :py:class:`~pytket.unit_id.Bit` s we want to look at in the shot table/counts dictionary, and given the exact permutation we want (and similarly for the permutation of :py:class:`~pytket.unit_id.Qubit` s for statevectors/unitaries).
-
-.. jupyter-execute::
-
- from pytket import Circuit, Bit, Qubit
- from pytket.extensions.qiskit import AerBackend, AerStateBackend
-
- circ = Circuit(3)
- circ.H(0).Ry(-0.3, 2)
- state_b = AerStateBackend()
- circ = state_b.get_compiled_circuit(circ)
- handle = state_b.process_circuit(circ)
-
- # Make q[1] the most-significant qubit, so interesting state uses consecutive coefficients
- result = state_b.get_result(handle)
- print(result.get_state([Qubit(1), Qubit(0), Qubit(2)]))
-
- circ.measure_all()
- shot_b = AerBackend()
- circ = shot_b.get_compiled_circuit(circ)
- handle = shot_b.process_circuit(circ, n_shots=2000)
- result = shot_b.get_result(handle)
-
- # Marginalise out q[0] from counts
- print(result.get_counts())
- print(result.get_counts([Bit(1), Bit(2)]))
-
-Expectation Value Calculations
-------------------------------
-
-One of the most common calculations performed with a quantum state :math:`\left| \psi \right>` is to obtain an expectation value :math:`\langle \psi | H | \psi \rangle`. For many applications, the operator :math:`H` can be expressed as a tensor product of Pauli matrices, or a linear combination of these. Given any (pure quantum) :py:class:`~pytket.circuit.Circuit` and any :py:class:`~pytket.backends.Backend`, the utility methods :py:meth:`~pytket.backends.Backend.get_pauli_expectation_value()` and :py:meth:`~pytket.backends.Backend.get_operator_expectation_value()` will generate the expectation value of the state under some operator using whatever results the :py:class:`~pytket.backends.Backend` supports. This includes adding measurements in the appropriate basis (if needed by the :py:class:`~pytket.backends.Backend`), running :py:class:`~pytket.backends.Backend.get_compiled_circuit()`, and obtaining and interpreting the results. For operators with many terms, it can optionally perform some basic measurement reduction techniques to cut down the number of :py:class:`~pytket.circuit.Circuit` s actually run by measuring multiple terms with simultaneous measurements in the same :py:class:`~pytket.circuit.Circuit`.
-
-.. jupyter-execute::
-
- from pytket import Circuit, Qubit
- from pytket.extensions.qiskit import AerBackend
- from pytket.partition import PauliPartitionStrat
- from pytket.pauli import Pauli, QubitPauliString
- from pytket.utils import get_pauli_expectation_value, get_operator_expectation_value
- from pytket.utils.operators import QubitPauliOperator
-
- circ = Circuit(3)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
- backend = AerBackend()
-
- zxy = QubitPauliString({
- Qubit(0) : Pauli.Z,
- Qubit(1) : Pauli.X,
- Qubit(2) : Pauli.Y})
- xzi = QubitPauliString({
- Qubit(0) : Pauli.X,
- Qubit(1) : Pauli.Z})
- op = QubitPauliOperator({
- QubitPauliString() : 0.3,
- zxy : -1,
- xzi : 1})
- print(get_pauli_expectation_value(
- circ,
- zxy,
- backend,
- n_shots=2000))
- print(get_operator_expectation_value(
- circ,
- op,
- backend,
- n_shots=2000,
- partition_strat=PauliPartitionStrat.CommutingSets))
-
-If you want a greater level of control over the procedure, then you may wish to write your own method for calculating :math:`\langle \psi | H | \psi \rangle`. This is simple multiplication if we are given the statevector :math:`| \psi \rangle`, but is slightly more complicated for measured systems. Since each measurement projects into either the subspace of +1 or -1 eigenvectors, we can assign +1 to each ``0`` readout and -1 to each ``1`` readout and take the average across all shots. When the desired operator is given by the product of multiple measurements, the contribution of +1 or -1 is dependent on the parity (XOR) of each measurement result in that shot. ``pytket`` provides some utility functions to wrap up this calculation and apply it to either a shot table (:py:meth:`~pytket.utils.expectation_from_shots()`) or a counts dictionary (:py:meth:`~pytket.utils.expectation_from_counts()`).
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
- from pytket.utils import expectation_from_counts
-
- circ = Circuit(3, 3)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
-
- circ.H(1) # Want to measure expectation for Pauli ZXY
- circ.Rx(0.5, 2) # Measure ZII, IXI, IIY separately
- circ.measure_all()
-
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, 2000)
- counts = backend.get_result(handle).get_counts()
- print(counts)
- print(expectation_from_counts(counts))
-
-.. Obtaining indices of specific bits/qubits of interest using `bit_readout` and `qubit_readout` or `qubit_to_bit_map`, and filtering results
-
-.. note:: :py:meth:`~pytket.utils.expectation_from_shots()` and :py:meth:`~pytket.utils.expectation_from_counts()` take into account every classical bit in the results object. If the expectation value of interest is a product of only a subset of the measurements in the :py:class:`~pytket.circuit.Circuit` (as is the case when simultaneously measuring several commuting operators), then you will want to filter/marginalise out the ignored bits when performing this calculation.
-
-Guidance for Writing Hardware-Agnostic Code
--------------------------------------------
-
-Writing code for experiments that can be retargeted to different :py:class:`~pytket.backends.Backend` s can be a challenge, but has many great payoffs for long-term developments. Being able to experiment with new devices and simulators helps to identify which is best for the needs of the experiment and how this changes with the experiment parameters (such as size of chemical molecule being simulated, or choice of model to train for a neural network). Being able to react to changes in device availability helps get your results faster when contesting against queues for device access or downtime for maintenance, in addition to moving on if a device is retired from live service and taking advantage of the newest devices as soon as they come online. This is especially important in the near future as there is no clear frontrunner in terms of device, manufacturer, or even fundamental quantum technology, and the rate at which they are improving performance and scale is so high that it is essential to not get left behind with old systems.
-
-One of the major counter-arguments against developing hardware-agnostic experiments is that the manual incorporation of the target architecture's connectivity and noise characteristics into the circuit design and choice of error mitigation/detection/correction strategies obtains the optimal performance from the device. The truth is that hardware characteristics are highly variable over time, invalidating noise models after only a few hours [Wils2020]_ and requiring regular recalibration. Over the lifetime of the device, this could lead to some qubits or couplers becoming so ineffective that they are removed from the system by the providers, giving drastic changes to the connectivity and admissible circuit designs. The instability of the experiment designs is difficult to argue when the optimal performance on one of today's devices is likely to be surpassed by an average performance on another device a short time after.
-
-We have already seen that devices and simulators will have different sets of requirements on the :py:class:`~pytket.circuit.Circuit` s they can accept and different types of results they can return, so hardware-agnosticism will not always come for free. The trick is to spot these differences and handle them on-the-fly at runtime. The design of the :py:class:`~pytket.backends.Backend` class in ``pytket`` aims to expose the fundamental requirements that require consideration for circuit design, compilation, and result interpretation in such a way that they can easily be queried at runtime to dynamically adapt the experiment procedure. All other aspects of backend interaction that are shared between different :py:class:`~pytket.backends.Backend` s are then unified for ease of integration. In practice, the constraints of the algorithm might limit the choice of :py:class:`~pytket.backends.Backend`, or we might choose to forego the ability to run on statevector simulators so that we only have to define the algorithm to calculate using counts, but we can still be agnostic within these categories.
-
-.. Consider whether you will want to use backends with different requirements on measurements, e.g. for using both statevector simulators and real devices; maybe build state prep and measurement circuits separately
-
-The first point in an experiment where you might have to act differently between :py:class:`~pytket.backends.Backend` s is during :py:class:`~pytket.circuit.Circuit` construction. A :py:class:`~pytket.backends.Backend` may support a non-universal fragment of the :py:class:`~pytket.circuit.Circuit` language, typically relating to their interaction with classical data -- either full interaction, no mid-circuit measurement and conditional operations, or no measurement at all for statevector and unitary simulators. If the algorithm chosen requires mid-circuit measurement, then we must sacrifice some freedom of choice of :py:class:`~pytket.backends.Backend` to accommodate this. For safety, it could be beneficial to include assertions that the :py:class:`~pytket.backends.Backend` provided meets the expectations of the algorithm.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend #, AerStateBackend
- from pytket.predicates import NoMidMeasurePredicate
-
- backend = AerBackend() # Choose backend in one place
- # backend = AerStateBackend() # A backend that is incompatible with the experiment
-
- # For algorithms using mid-circuit measurement, we can assert this is valid
- qkd = Circuit(1, 3)
- qkd.H(0).Measure(0, 0) # Prepare a random bit in the Z basis
- qkd.H(0).Measure(0, 1).H(0) # Eavesdropper measures in the X basis
- qkd.Measure(0, 2) # Recipient measures in the Z basis
-
- assert backend.supports_counts # Using AerStateBackend would fail at this check
- assert NoMidMeasurePredicate() not in backend.required_predicates
- compiled_qkd = backend.get_compiled_circuit(qkd)
- handle = backend.process_circuit(compiled_qkd, n_shots=1000)
- print(backend.get_result(handle).get_counts())
-
-.. note:: The same effect can be achieved by ``assert backend.valid_circuit(qkd)`` after compilation. However, when designing the compilation procedure manually, it is unclear whether a failure for this assertion would come from the incompatibility of the :py:class:`~pytket.backends.Backend` for the experiment or from the compilation failing.
-
-Otherwise, a practical solution around different measurement requirements is to separate the design into "state circuits" and "measurement circuits". At the point of running on the :py:class:`~pytket.backends.Backend`, we can then choose to either just send the state circuit for statevector calculations or compose it with the measurement circuits to run on sampling :py:class:`~pytket.backends.Backend` s.
-
-.. Use `supports_X` to inspect type of backend used at runtime, or look at the requirements to see if measurements/conditionals are supported
-
-At runtime, we can check whether a particular result type is supported using the :py:attr:`Backend.supports_X` properties (such as :py:attr:`~pytket.backends.Backend.supports_counts`), whereas restrictions on the :py:class:`~pytket.circuit.Circuit` s supported can be inspected with :py:attr:`~pytket.backends.Backend.required_predicates`.
-
-.. Compile generically, making use of `get_compiled_circuit`
-
-Whilst the demands of each :py:class:`~pytket.backends.Backend` on the properties of the :py:class:`~pytket.circuit.Circuit` necessitate different compilation procedures, using the default compilation sequences provided with :py:class:`~pytket.backends.Backend.get_compiled_circuit` handles compiling generically.
-
-.. All backends can `process_circuit` identically
-
-Similarly, every :py:class:`~pytket.backends.Backend` can use :py:meth:`~pytket.backends.Backend.process_circuit` identically. Additional :py:class:`~pytket.backends.Backend`-specific arguments (such as the number of shots required or the seed for a simulator) will just be ignored if passed to a :py:class:`~pytket.backends.Backend` that does not use them.
-
-.. Case split on retrieval again to handle statevector separately from samplers
-
-For the final steps of retrieving and interpreting the results, it suffices to just case-split on the form of data we can retrieve again with :py:attr:`Backend.supports_X`.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend #, AerStateBackend
- from pytket.utils import expectation_from_counts
- import numpy as np
-
- backend = AerBackend() # Choose backend in one place
- # backend = AerStateBackend() # Alternative backend with different requirements and result type
-
- # For many algorithms, we can separate the state preparation from measurements
- circ = Circuit(2) # Apply e^{0.135 i pi XY} to the initial state
- circ.H(0).V(1).CX(0, 1).Rz(-0.27, 1).CX(0, 1).H(0).Vdg(1)
- measure = Circuit(2, 2) # Measure the YZ operator via YI and IZ
- measure.V(0).measure_all()
-
- if backend.supports_counts:
- circ.append(measure)
-
- circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(circ, n_shots=2000)
-
- expectation = 0
- if backend.supports_state:
- yz = np.asarray([
- [0, 0, -1j, 0],
- [0, 0, 0, 1j],
- [1j, 0, 0, 0],
- [0, -1j, 0, 0]])
- svec = backend.get_result(handle).get_state()
- expectation = np.vdot(svec, yz.dot(svec))
- else:
- counts = backend.get_result(handle).get_counts()
- expectation = expectation_from_counts(counts)
-
- print(expectation)
-
-
-Batch Submission
-----------------
-
-.. Some devices are accessed via queues or long-latency connections; batching helps reduce the amount of time spent waiting by pipelining
-
-Current public-access quantum computers tend to implement either a queueing or a reservation system for mediating access. Whilst the queue-based access model gives relative fairness, guarantees availability, and maximises throughput and utilisation of the hardware, it also presents a big problem to the user with regards to latency. Whenever a circuit is submitted, not only must the user wait for it to be run on the hardware, but they must wait longer for their turn before it can even start running. This can end up dominating the time taken for the overall experiment, especially when demand is high for a particular device.
-
-We can mitigate the problem of high queue latency by batching many :py:class:`~pytket.circuit.Circuit` s together. This means that we only have to wait the queue time once, since after the first :py:class:`~pytket.circuit.Circuit` is run the next one is run immediately rather than joining the end of the queue.
-
-.. Advisable to generate as many circuits of interest as possible, storing how to interpret the results of each one, then send them off together
-.. After processing, interpret the results by querying the result handles
-
-To maximise the benefits of batch submission, it is advisable to generate as many of your :py:class:`~pytket.circuit.Circuit` s as possible at the same time to send them all off together. This is possible when, for example, generating every measurement circuit for an expectation value calculation, or sampling several parameter values from a local neighbourhood in a variational procedure. The method :py:class:`~pytket.backends.Backend.process_circuits()` (plural) will then submit all the provided :py:class:`~pytket.circuit.Circuit` s simultaneously and return a :py:class:`~pytket.backends.resulthandle.ResultHandle` for each :py:class:`~pytket.circuit.Circuit` to allow each result to be extracted individually for interpretation. Since there is no longer a single :py:class:`~pytket.circuit.Circuit` being handled from start to finish, it may be necessary to store additional data to record how to interpret them, like the set of :py:class:`~pytket.unit_id.Bit` s to extract for each :py:class:`~pytket.circuit.Circuit` or the coefficient to multiply the expectation value by.
-
-.. jupyter-input::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.utils import expectation_from_counts
-
- backend = IBMQBackend("ibmq_quito")
-
- state = Circuit(3)
- state.H(0).CX(0, 1).CX(1, 2).X(0)
-
- # Compute expectation value for -0.3i ZZZ + 0.8 XZZ + 1.2 XXX
- zzz = Circuit(3, 3)
- zzz.measure_all()
- xzz = Circuit(3, 3)
- xzz.H(0).measure_all()
- xxx = Circuit(3, 3)
- xxx.H(0).H(1).H(2).measure_all()
-
- circ_list = []
- for m in [zzz, xzz, xxx]:
- c = state.copy()
- c.append(m)
- circ_list.append(c)
-
- coeff_list = [
- -0.3j, # ZZZ
- 0.8, # XZZ
- 1.2 # XXX
- ]
- circ_list = backend.get_compiled_circuits(circ_list)
-
- handle_list = backend.process_circuits(circ_list, n_shots=2000)
- result_list = backend.get_results(handle_list)
-
- expectation = 0
- for coeff, result in zip(coeff_list, result_list):
- counts = result.get_counts()
- expectation += coeff * expectation_from_counts(counts)
-
- print(expectation)
-
-.. jupyter-output::
-
- (1.2047999999999999-0.0015000000000000013j)
-
-.. note:: Currently, only some devices (e.g. those from IBMQ, Quantinuum and Amazon Braket) support a queue model and benefit from this methodology, though more may adopt this in future. The :py:class:`~pytket.extensions.qiskit.AerBackend` simulator and the :py:class:`~pytket.extensions.quantinuum.QuantinuumBackend` can take advantage of batch submission for parallelisation. In other cases, :py:class:`~pytket.backends.Backend.process_circuits` will just loop through each :py:class:`~pytket.circuit.Circuit` in turn.
-
-Embedding into Qiskit
----------------------
-
-Not only is the goal of tket to be a device-agnostic platform, but also interface-agnostic, so users are not obliged to have to work entirely in tket to benefit from the wide range of devices supported. For example, Qiskit is currently the most widely adopted quantum software development platform, providing its own modules for building and compiling circuits, submitting to backends, applying error mitigation techniques and combining these into higher-level algorithms. Each :py:class:`~pytket.backends.Backend` in ``pytket`` can be wrapped up to imitate a Qiskit backend, allowing the benefits of tket to be felt in existing Qiskit projects with minimal work.
-
-Below we show how the :py:class:`~pytket.extensions.cirq.CirqStateSampleBackend` from the ``pytket-cirq`` extension can be used with its :py:meth:`default_compilation_pass` directly in qiskit.
-
-.. jupyter-execute::
-
- from qiskit.primitives import BackendSampler
- from qiskit_algorithms import Grover, AmplificationProblem
- from qiskit.circuit import QuantumCircuit
-
- from pytket.extensions.cirq import CirqStateSampleBackend
- from pytket.extensions.qiskit.tket_backend import TketBackend
-
- cirq_simulator = CirqStateSampleBackend()
- backend = TketBackend(cirq_simulator, cirq_simulator.default_compilation_pass())
- sampler_options = {"shots":1024}
- qsampler = BackendSampler(backend, options=sampler_options)
-
- oracle = QuantumCircuit(2)
- oracle.cz(0, 1)
-
- def is_good_state(bitstr):
- return sum(map(int, bitstr)) == 2
-
- problem = AmplificationProblem(oracle=oracle, is_good_state=is_good_state)
- grover = Grover(sampler=qsampler)
- result = grover.amplify(problem)
- print("Top measurement:", result.top_measurement)
-
-.. note:: Since Qiskit may not be able to solve all of the constraints of the chosen device/simulator, some compilation may be required after a circuit is passed to the :py:class:`TketBackend`, or it may just be preferable to do so to take advantage of the sophisticated compilation solutions provided in ``pytket``. Upon constructing the :py:class:`TketBackend`, you can provide a ``pytket`` compilation pass to apply to each circuit, e.g. ``TketBackend(backend, backend.default_compilation_pass())``. Some experimentation may be required to find a combination of ``qiskit.transpiler.PassManager`` and ``pytket`` compilation passes that executes successfully.
-
-.. Pytket Assistant
-.. ----------------
-
-.. Goals of the assistant
-.. How to set up and push/pull results
-
-Advanced Backend Topics
------------------------
-
-Simulator Support for Expectation Values
-========================================
-
-.. May be faster to apply simple expectation values within the internal representation of simulator
-.. `supports_expectation`
-.. Examples of Pauli and operator expectations
-
-Some simulators will have dedicated support for fast expectation value calculations. In this special case, they will provide extra methods :py:class:`~pytket.backends.Backend.get_pauli_expectation_value()` and :py:class:`~pytket.backends.Backend.get_operator_expectation_value()`, which take a :py:class:`~pytket.circuit.Circuit` and some operator and directly return the expectation value. Again, we can check whether a :py:class:`~pytket.backends.Backend` has this feature with :py:attr:`~pytket.backends.Backend.supports_expectation`.
-
-.. jupyter-execute::
-
- from pytket import Circuit, Qubit
- from pytket.extensions.qiskit import AerStateBackend
- from pytket.pauli import Pauli, QubitPauliString
- from pytket.utils.operators import QubitPauliOperator
-
- backend = AerStateBackend()
-
- state = Circuit(3)
- state.H(0).CX(0, 1).V(2)
-
- xxy = QubitPauliString({
- Qubit(0) : Pauli.X,
- Qubit(1) : Pauli.X,
- Qubit(2) : Pauli.Y})
- zzi = QubitPauliString({
- Qubit(0) : Pauli.Z,
- Qubit(1) : Pauli.Z})
- iiz = QubitPauliString({
- Qubit(2) : Pauli.Z})
- op = QubitPauliOperator({
- QubitPauliString() : -0.5,
- xxy : 0.7,
- zzi : 1.4,
- iiz : 3.2})
-
- assert backend.supports_expectation
- state = backend.get_compiled_circuit(state)
- print(backend.get_pauli_expectation_value(state, xxy))
- print(backend.get_operator_expectation_value(state, op))
-
-Asynchronous Job Submission
-===========================
-
-.. Checking circuit status
-.. Blocking on retrieval
-
-In the near future, as we look to more sophisticated algorithms and larger problem instances, the quantity and size of :py:class:`~pytket.circuit.Circuit` s to be run per experiment and the number of shots required to obtain satisfactory precision will mean the time taken for the quantum computation could exceed that of the classical computation. At this point, the overall algorithm can be sped up by maintaining maximum throughput on the quantum device and minimising how often the quantum device is left idle whilst the classical system is determining the next :py:class:`~pytket.circuit.Circuit` s to send. This can be achieved by writing your algorithm to operate asynchronously.
-
-The intended semantics of the :py:meth:`~pytket.backends.Backend` methods are designed to enable asynchronous execution of quantum programs whenever admissible from the underlying API provided by the device/simulator. :py:meth:`~pytket.backends.Backend.process_circuit()` and :py:meth:`~pytket.backends.Backend.process_circuits()` will submit the :py:class:`~pytket.circuit.Circuit` (s) and immediately return.
-
-The progress can be checked by querying :py:meth:`~pytket.backends.Backend.circuit_status()`. If this returns a :py:class:`~pytket.backends.status.CircuitStatus` matching ``StatusEnum.COMPLETED``, then :py:meth:`~pytket.backends.Backend.get_X()` will obtain the results and return immediately, otherwise it will block the thread and wait until the results are available.
-
-.. jupyter-input::
-
- import asyncio
- from pytket import Circuit
- from pytket.backends import StatusEnum
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.utils import expectation_from_counts
-
- backend = IBMQBackend("ibmq_quito")
-
- state = Circuit(3)
- state.H(0).CX(0, 1).CX(1, 2).X(0)
-
- # Compute expectation value for -0.3i ZZZ + 0.8 XZZ + 1.2 XXX
- zzz = Circuit(3, 3)
- zzz.measure_all()
- xzz = Circuit(3, 3)
- xzz.H(0).measure_all()
- xxx = Circuit(3, 3)
- xxx.H(0).H(1).H(2).measure_all()
-
- circ_list = []
- for m in [zzz, xzz, xxx]:
- c = state.copy()
- c.append(m)
- circ_list.append(backend.get_compiled_circuit(c))
-
- coeff_list = [
- -0.3j, # ZZZ
- 0.8, # XZZ
- 1.2 # XXX
- ]
-
- handle_list = backend.process_circuits(circ_list, n_shots=2000)
-
- async def check_at_intervals(backend, handle, interval):
- while True:
- await asyncio.sleep(interval)
- status = backend.circuit_status(handle)
- if status.status in (StatusEnum.COMPLETED, StatusEnum.ERROR):
- return status
-
- async def expectation(backend, handle, coeff):
- await check_at_intervals(backend, handle, 5)
- counts = backend.get_result(handle).get_counts()
- return coeff * expectation_from_counts(counts)
-
- async def main():
- task_set = set([asyncio.create_task(expectation(backend, h, c)) for h, c in zip(handle_list, coeff_list)])
- done, pending = await asyncio.wait(task_set, return_when=asyncio.ALL_COMPLETED)
- sum = 0
- for t in done:
- sum += await t
-
- print(sum)
-
- asyncio.run(main())
-
-.. jupyter-output::
-
- (1.2087999999999999-0.002400000000000002j)
-
-In some cases you may want to end execution early, perhaps because it is taking too long or you already have all the data you need. You can use the :py:meth:`~pytket.backends.Backend.cancel()` method to cancel the job for a given :py:class:`~pytket.backends.resulthandle.ResultHandle`. This is recommended to help reduce load on the devices if you no longer need to run the submitted jobs.
-
-.. note:: Asynchronous submission is currently available with the :py:class:`~pytket.extensions.qiskit.IBMQBackend`, :py:class:`~pytket.extensions.quantinuum.QuantinuumBackend`, :py:class:`BraketBackend` and :py:class:`~pytket.extensions.qiskit.AerBackend`. It will be extended to others in future updates.
-
-Persistent Handles
-==================
-
-Being able to split your processing into distinct procedures for :py:class:`~pytket.circuit.Circuit` generation and result interpretation can help improve throughput on the quantum device, but it can also provide a way to split the processing between different Python sessions. This may be desirable when the classical computation to interpret the results and determine the next experiment parameters is sufficiently intensive that we would prefer to perform it offline and only reserve a quantum device once we are ready to run more. Furthermore, resuming with previously-generated results could benefit repeatability of experiments and better error-safety since the logged results can be saved and reused.
-
-Some :py:class:`~pytket.backends.Backend` s support persistent handles, in that the :py:class:`~pytket.backends.resulthandle.ResultHandle` object can be stored and the associated results obtained from another instance of the same :py:class:`~pytket.backends.Backend` in a different session. This is indicated by the boolean ``persistent_handles`` property of the :py:class:`~pytket.backends.Backend`. Use of persistent handles can greatly reduce the amount of logging you would need to do to take advantage of this workflow.
-
-.. jupyter-input::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import IBMQBackend
-
- backend = IBMQBackend("ibmq_quito")
-
- circ = Circuit(3, 3)
- circ.X(1).CZ(0, 1).CX(1, 2).measure_all()
- circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(circ, n_shots=1000)
-
- # assert backend.persistent_handles
- print(str(handle))
- counts = backend.get_result(handle).get_counts()
- print(counts)
-
-.. jupyter-output::
-
- ('5e8f3dcbbb7d8500119cfbf6', 0)
- {(0, 1, 1): 1000}
-
-.. jupyter-input::
-
- from pytket.backends import ResultHandle
- from pytket.extensions.qiskit import IBMQBackend
-
- backend = IBMQBackend("ibmq_quito")
-
- handle = ResultHandle.from_str("('5e8f3dcbbb7d8500119cfbf6', 0)")
- counts = backend.get_result(handle).get_counts()
- print(counts)
-
-.. jupyter-output::
-
- {(0, 1, 1): 1000}
-
-
-Result Serialization
-====================
-
-When performing experiments using :py:class:`~pytket.backends.Backend` s, it is often useful to be able to easily store and retrieve the results for later analysis or backup.
-This can be achieved using native serialiaztion and deserialization of :py:class:`~pytket.backends.backendresult.BackendResult` objects from JSON compatible dictionaries, using the :py:meth:`~pytket.backends.backendresult.BackendResult.to_dict()` and :py:meth:`~pytket.backends.backendresult.BackendResult.from_dict()` methods.
-
-.. jupyter-execute::
-
- import tempfile
- import json
- from pytket import Circuit
- from pytket.backends.backendresult import BackendResult
- from pytket.extensions.qiskit import AerBackend
-
- circ = Circuit(2, 2)
- circ.H(0).CX(0, 1).measure_all()
-
- backend = AerBackend()
- handle = backend.process_circuit(circ, 10)
- res = backend.get_result(handle)
-
- with tempfile.TemporaryFile('w+') as fp:
- json.dump(res.to_dict(), fp)
- fp.seek(0)
- new_res = BackendResult.from_dict(json.load(fp))
-
- print(new_res.get_counts())
-
-
-.. [Niel2001] Nielsen, M.A. and Chuang, I.L., 2001. Quantum computation and quantum information. Phys. Today, 54(2), p.60.
-.. [Wils2020] Wilson, E., Singh, S. and Mueller, F., 2020. Just-in-time Quantum Circuit Transpilation Reduces Noise. arXiv preprint arXiv:2005.12820.
diff --git a/docs/manual/manual_circuit.rst b/docs/manual/manual_circuit.md
similarity index 54%
rename from docs/manual/manual_circuit.rst
rename to docs/manual/manual_circuit.md
index 9c3a720b..6c0bd19c 100644
--- a/docs/manual/manual_circuit.rst
+++ b/docs/manual/manual_circuit.md
@@ -1,25 +1,29 @@
-********************
-Circuit Construction
-********************
+---
+file_format: mystnb
+---
-.. Open DAG; equivalence up to trivial commutations/topological orderings
+# Circuit Construction
-The :py:class:`~pytket.circuit.Circuit` class forms the unit of computation that we can send off to a quantum co-processor. Each instruction is to be performed in order, potentially parallelising when they use disjoint sets of (qu)bits. To capture this freedom of parallelisation, we treat the circuit as a Directed Acyclic Graph with a vertex for each instruction and directed edges following the paths of resources (e.g. qubits and bits) between them. This DAG representation describes the abstract circuit ignoring these trivial commutations/parallel instructions.
+% Open DAG; equivalence up to trivial commutations/topological orderings
-.. Abstract computational model and semantics - map on combined quantum/classical state space
+The {py:class}`~pytket.circuit.Circuit` class forms the unit of computation that we can send off to a quantum co-processor. Each instruction is to be performed in order, potentially parallelising when they use disjoint sets of (qu)bits. To capture this freedom of parallelisation, we treat the circuit as a Directed Acyclic Graph with a vertex for each instruction and directed edges following the paths of resources (e.g. qubits and bits) between them. This DAG representation describes the abstract circuit ignoring these trivial commutations/parallel instructions.
-In general, we consider :py:class:`~pytket.circuit.Circuit` instances to represent open circuits; that is, they can be used within arbitrary contexts, so any input state can be supplied and there is no assumption on how the output state should be used. In practice, when we send a :py:class:`~pytket.circuit.Circuit` off to be executed, it will be run with all qubits in the initial state :math:`|0\rangle^{\otimes n}` and all bits set to :math:`0`, then the classical outputs returned and the quantum state discarded.
+% Abstract computational model and semantics - map on combined quantum/classical state space
-Each circuit can be represented as a POVM on the combined quantum/classical state space by composing the representations assigned to each basic instruction. However, many use cases will live predominantly in the pure quantum space where the operations are simply unitaries acting on the quantum state. One practical distinction between these cases is the relevance of global phase: something that cannot be identified at the POVM level but has importance for pure states as it affects how we interpret the system and has an observable difference when the system is then coherently controlled. For example, an Rz gate and a U1 gate give equivalent effects on the quantum state but have a different global phase, meaning their unitaries *look* different, and a controlled-Rz is different from a controlled-U1. A :py:class:`~pytket.circuit.Circuit` will track global phase to make working with pure quantum processes easier, though this becomes meaningless once measurements and other classical interaction are applied and has no impact on the instructions sent to a quantum device when we eventually run it.
+In general, we consider {py:class}`~pytket.circuit.Circuit` instances to represent open circuits; that is, they can be used within arbitrary contexts, so any input state can be supplied and there is no assumption on how the output state should be used. In practice, when we send a {py:class}`~pytket.circuit.Circuit` off to be executed, it will be run with all qubits in the initial state $|0\rangle^{\otimes n}$ and all bits set to $0$, then the classical outputs returned and the quantum state discarded.
-.. There is no strict notion of control-flow or branching computation within a :py:class:`~pytket.circuit.Circuit`, meaning there is no facility to consider looping or arbitrary computation trees. This is likely to be an engineering limitation of all quantum devices produced in the near future, but this does not sacrifice the ability to do meaningful and interesting computation.
+Each circuit can be represented as a POVM on the combined quantum/classical state space by composing the representations assigned to each basic instruction. However, many use cases will live predominantly in the pure quantum space where the operations are simply unitaries acting on the quantum state. One practical distinction between these cases is the relevance of global phase: something that cannot be identified at the POVM level but has importance for pure states as it affects how we interpret the system and has an observable difference when the system is then coherently controlled. For example, an Rz gate and a U1 gate give equivalent effects on the quantum state but have a different global phase, meaning their unitaries *look* different, and a controlled-Rz is different from a controlled-U1. A {py:class}`~pytket.circuit.Circuit` will track global phase to make working with pure quantum processes easier, though this becomes meaningless once measurements and other classical interaction are applied and has no impact on the instructions sent to a quantum device when we eventually run it.
-.. Resource linearity - no intermediate allocation/disposal of (qu)bits
-.. Constructors (for integer-indexing)
+% There is no strict notion of control-flow or branching computation within a :py:class:`~pytket.circuit.Circuit`, meaning there is no facility to consider looping or arbitrary computation trees. This is likely to be an engineering limitation of all quantum devices produced in the near future, but this does not sacrifice the ability to do meaningful and interesting computation.
-Given the small scale and lack of dynamic quantum memories for both devices and simulations, we assume each qubit and bit is statically registered and hence each :py:class:`~pytket.circuit.Circuit` has the same number of inputs as outputs. The set of data units (qubits and bits) used by the :py:class:`~pytket.circuit.Circuit` is hence going to be constant, so we can define it up-front when we construct one. We can also optionally give it a name for easy identification.
+% Resource linearity - no intermediate allocation/disposal of (qu)bits
-.. jupyter-execute::
+% Constructors (for integer-indexing)
+
+Given the small scale and lack of dynamic quantum memories for both devices and simulations, we assume each qubit and bit is statically registered and hence each {py:class}`~pytket.circuit.Circuit` has the same number of inputs as outputs. The set of data units (qubits and bits) used by the {py:class}`~pytket.circuit.Circuit` is hence going to be constant, so we can define it up-front when we construct one. We can also optionally give it a name for easy identification.
+
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -27,19 +31,20 @@ Given the small scale and lack of dynamic quantum memories for both devices and
quantum_circ = Circuit(4) # 4 qubits and no bits
mixed_circ = Circuit(4, 2) # 4 qubits and 2 bits
named_circ = Circuit(2, 2, "my_circ")
+```
+
+## Basic Gates
-Basic Gates
------------
+% Build up by appending to the end of the circuit
-.. Build up by appending to the end of the circuit
+The bulk of the interaction with a {py:class}`~pytket.circuit.Circuit` object will be in building up the sequence of instructions to be run. The simplest way to do this is by adding each instruction in execution order to the end of the circuit.
-The bulk of the interaction with a :py:class:`~pytket.circuit.Circuit` object will be in building up the sequence of instructions to be run. The simplest way to do this is by adding each instruction in execution order to the end of the circuit.
+% Constant gates
-.. Constant gates
+Basic quantum gates represent some unitary operation applied to some qubits. Adding them to a {py:class}`~pytket.circuit.Circuit` just requires specifying which qubits you want to apply them to. For controlled-gates, the convention is to give the control qubit(s) first, followed by the target qubit(s).
-Basic quantum gates represent some unitary operation applied to some qubits. Adding them to a :py:class:`~pytket.circuit.Circuit` just requires specifying which qubits you want to apply them to. For controlled-gates, the convention is to give the control qubit(s) first, followed by the target qubit(s).
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
@@ -48,12 +53,14 @@ Basic quantum gates represent some unitary operation applied to some qubits. Add
circ.CX(1, 3) # and apply a CX gate with control qubit 1 and target qubit 3
circ.Z(3) # then apply a Z gate to qubit 3
circ.get_commands() # show the commands of the built circuit
+```
-.. parameterised gates; parameter first, always in half-turns
+% parameterised gates; parameter first, always in half-turns
-For parameterised gates, such as rotations, the parameter is always given first. Because of the prevalence of rotations with angles given by fractions of :math:`\pi` in practical quantum computing, the unit for all angular parameters is the half-turn (1 half-turn is equal to :math:`\pi` radians).
+For parameterised gates, such as rotations, the parameter is always given first. Because of the prevalence of rotations with angles given by fractions of $\pi$ in practical quantum computing, the unit for all angular parameters is the half-turn (1 half-turn is equal to $\pi$ radians).
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -61,15 +68,18 @@ For parameterised gates, such as rotations, the parameter is always given first.
circ.Rx(0.5, 0) # Rx of angle pi/2 radians on qubit 0
circ.CRz(0.3, 1, 0) # Controlled-Rz of angle 0.3pi radians with
# control qubit 1 and target qubit 0
+```
+
+% Table of common gates, with circuit notation, unitary, and python command
-.. Table of common gates, with circuit notation, unitary, and python command
-.. Wider variety of gates available via OpType
+% Wider variety of gates available via OpType
-A large selection of common gates are available in this way, as listed in the API reference for the :py:class:`~pytket.circuit.Circuit` class. However, for less commonly used gates, a wider variety is available using the :py:class:`~pytket.circuit.OpType` enum, which can be added using the :py:meth:`~pytket.circuit.Circuit.add_gate` method.
+A large selection of common gates are available in this way, as listed in the API reference for the {py:class}`~pytket.circuit.Circuit` class. However, for less commonly used gates, a wider variety is available using the {py:class}`~pytket.circuit.OpType` enum, which can be added using the {py:meth}`~pytket.circuit.Circuit.add_gate` method.
-.. Example of adding gates using `add_gate`
+% Example of adding gates using `add_gate`
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit, OpType
@@ -80,25 +90,26 @@ A large selection of common gates are available in this way, as listed in the AP
# add e^{-i (0.7 pi / 2) XX} on qubits 0 and 2
circ.add_gate(OpType.PhasedX, [-0.1, 0.5], [3])
# adds Rz(-0.5 pi); Rx(-0.1 pi); Rz(0.5 pi) on qubit 3
+```
-The API reference for the :py:class:`~pytket.OpType` class details all available operations that can exist in a circuit.
+The API reference for the {py:class}`~pytket.OpType` class details all available operations that can exist in a circuit.
-In the above example, we asked for a ``PhasedX`` with angles ``[-0.1, 0.5]``, but received ``PhasedX(3.9, 0.5)``. ``pytket`` will freely map angles into the range :math:`\left[0, r\right)` for some range parameter :math:`r` that depends on the :py:class:`~pytket.circuit.OpType`, preserving the unitary matrix (including global phase).
+In the above example, we asked for a `PhasedX` with angles `[-0.1, 0.5]`, but received `PhasedX(3.9, 0.5)`. `pytket` will freely map angles into the range $\left[0, r\right)$ for some range parameter $r$ that depends on the {py:class}`~pytket.circuit.OpType`, preserving the unitary matrix (including global phase).
-.. The vast majority of gates will also have the same number of inputs as outputs (following resource-linearity), with the exceptions being instructions that are read-only on some classical data.
+% The vast majority of gates will also have the same number of inputs as outputs (following resource-linearity), with the exceptions being instructions that are read-only on some classical data.
-Measurements
-------------
+## Measurements
-.. Non-destructive, single-qubit Z-measurements
+% Non-destructive, single-qubit Z-measurements
-Measurements go a step further by interacting with both the quantum and classical data. The convention used in ``pytket`` is that all measurements are non-destructive, single-qubit measurements in the :math:`Z` basis; other forms of measurements can be constructed by combining these with other operations.
+Measurements go a step further by interacting with both the quantum and classical data. The convention used in `pytket` is that all measurements are non-destructive, single-qubit measurements in the $Z$ basis; other forms of measurements can be constructed by combining these with other operations.
-.. Adding measure gates
+% Adding measure gates
Adding a measurement works just like adding any other gate, where the first argument is the qubit to be measured and the second specifies the classical bit to store the result in.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -108,12 +119,14 @@ Adding a measurement works just like adding any other gate, where the first argu
circ.CX(1, 3)
circ.H(1)
circ.Measure(1, 1) # Measurement of IXXX, saving result in bit 1
+```
-.. Overwriting data in classical bits
+% Overwriting data in classical bits
Because the classical bits are treated as statically assigned locations, writing to the same bit multiple times will overwrite the previous value.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -121,12 +134,14 @@ Because the classical bits are treated as statically assigned locations, writing
circ.Measure(0, 0) # measure the first measurement
circ.CX(0, 1)
circ.Measure(1, 0) # overwrites the first result with a new measurement
+```
-.. Measurement on real devices could require a single layer at end, or sufficiently noisy that they appear destructive so require resets
+% Measurement on real devices could require a single layer at end, or sufficiently noisy that they appear destructive so require resets
Depending on where we plan on running our circuits, the backend or simulator might have different requirements on the structure of measurements in the circuits. For example, statevector simulators will only work deterministically for pure-quantum circuits, so will fail if any measures are present at all. More crucially, near-term quantum hardware almost always requires all measurements to occur in a single parallel layer at the end of the circuit (i.e. we cannot measure a qubit in the middle of the circuit).
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -151,12 +166,14 @@ Depending on where we plan on running our circuits, the backend or simulator mig
# before qubit 1; they won't occur simultaneously so this may be rejected
circ3.Measure(0, 0)
circ3.Measure(1, 0)
+```
-.. `measure_all`
+% `measure_all`
-The simplest way to guarantee this is to finish the circuit by measuring all qubits. There is a short-hand function :py:meth:`~pytket.circuit.Circuit.measure_all` to make this easier.
+The simplest way to guarantee this is to finish the circuit by measuring all qubits. There is a short-hand function {py:meth}`~pytket.circuit.Circuit.measure_all` to make this easier.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -169,10 +186,12 @@ The simplest way to guarantee this is to finish the circuit by measuring all qub
circ = Circuit(2)
circ.H(1)
circ.measure_all()
+```
On devices where mid-circuit measurements are available, they may be highly noisy and not apply just a basic projector on the quantum state. We can view these as "effectively destructive" measurements, where the qubit still exists but is in a noisy state. In this case, it is recommended to actively reset a qubit after measurement if it is intended to be reused.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit, OpType
@@ -184,25 +203,27 @@ On devices where mid-circuit measurements are available, they may be highly nois
circ.X(0, condition_bits=[0], condition_value=1)
# Use the qubit as if the measurement was non-destructive
circ.CX(0, 1)
+```
-Barriers
---------
+## Barriers
-.. Prevent compilation from rearranging gates around the barrier
-.. Some devices may use to provide timing information (no gate after the barrier will be started until all gates before the barrier have completed)
+% Prevent compilation from rearranging gates around the barrier
+
+% Some devices may use to provide timing information (no gate after the barrier will be started until all gates before the barrier have completed)
The concept of barriers comes from low-level classical programming. They exist as instructions but perform no active operation. Instead, their function is twofold:
- At compile-time, prevent the compiler from reordering operations around the barrier.
- At runtime, ensure that all operations before the barrier must have finished before any operations after the barrier start.
-The intention is the same for :py:class:`~pytket.circuit.Circuit` s. Inserting barriers can be used to segment the program to easily spot how it is modified during compilation, and some quantum hardware uses barriers as the primary method of embedding timing information.
+The intention is the same for {py:class}`~pytket.circuit.Circuit` s. Inserting barriers can be used to segment the program to easily spot how it is modified during compilation, and some quantum hardware uses barriers as the primary method of embedding timing information.
+
+% `add_barrier`
-.. `add_barrier`
+Adding a barrier to a {py:class}`~pytket.circuit.Circuit` is done using the {py:meth}`~pytket.circuit.Circuit.add_barrier` method. In general, a barrier is placed on some subset of the (qu)bits to impose these ordering restrictions on those (qu)bits specifically (i.e. we don't care about reorders on the other (qu)bits).
-Adding a barrier to a :py:class:`~pytket.circuit.Circuit` is done using the :py:meth:`~pytket.circuit.Circuit.add_barrier` method. In general, a barrier is placed on some subset of the (qu)bits to impose these ordering restrictions on those (qu)bits specifically (i.e. we don't care about reorders on the other (qu)bits).
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
@@ -212,22 +233,24 @@ Adding a barrier to a :py:class:`~pytket.circuit.Circuit` is done using the :py:
circ.add_barrier([0, 1, 2, 3], [0, 1]) # add a barrier on all qubits and bits
circ.Measure(0, 0)
circ.Measure(2, 1)
+```
+
+## Registers and IDs
-Registers and IDs
------------------
+% When scaling up, want to attach semantic meaning to the names of resources and group them sensibly into related collections; IDs give names and registers allow grouping via indexed arrays; each id is a name and (n-dimensional) index
-.. When scaling up, want to attach semantic meaning to the names of resources and group them sensibly into related collections; IDs give names and registers allow grouping via indexed arrays; each id is a name and (n-dimensional) index
+Using integer values to refer to each of our qubits and bits works fine for small-scale experiments, but when building up larger and more complicated programs, it is much easier to manage if we are able to name the resources to attach semantic meaning to them and group them into related collections. `pytket` enables this by supporting registers and named IDs.
-Using integer values to refer to each of our qubits and bits works fine for small-scale experiments, but when building up larger and more complicated programs, it is much easier to manage if we are able to name the resources to attach semantic meaning to them and group them into related collections. ``pytket`` enables this by supporting registers and named IDs.
+Each unit resource is associated with a {py:class}`~pytket.unit_id.UnitID` (typically the subclasses {py:class}`~pytket.circuit.Qubit` or {py:class}`~pytket.circuit.Bit`), which gives a name and some ($n$-dimensional) index. A (quantum/classical) register is hence some collection of {py:class}`UnitID` s with the same name, dimension of index, and type of associated resource. These identifiers are not necessarily tied to a specific {py:class}`~pytket.circuit.Circuit` and can be reused between many of them.
-Each unit resource is associated with a :py:class:`~pytket.unit_id.UnitID` (typically the subclasses :py:class:`~pytket.circuit.Qubit` or :py:class:`~pytket.circuit.Bit`), which gives a name and some (:math:`n`-dimensional) index. A (quantum/classical) register is hence some collection of :py:class:`UnitID` s with the same name, dimension of index, and type of associated resource. These identifiers are not necessarily tied to a specific :py:class:`~pytket.circuit.Circuit` and can be reused between many of them.
+% Can add to circuits individually or declare a 1-dimensional register (map from unsigned to id)
-.. Can add to circuits individually or declare a 1-dimensional register (map from unsigned to id)
-.. Using ids to add gates
+% Using ids to add gates
-Named resources can be added to :py:class:`~pytket.circuit.Circuit` s individually, or by declaring a 1-dimensional register. Any of the methods for adding gates can then use these IDs.
+Named resources can be added to {py:class}`~pytket.circuit.Circuit` s individually, or by declaring a 1-dimensional register. Any of the methods for adding gates can then use these IDs.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit, Qubit, Bit
@@ -243,12 +266,14 @@ Named resources can be added to :py:class:`~pytket.circuit.Circuit` s individu
circ.CX(qreg[0], anc) # add gates in terms of IDs
circ.CX(qreg[1], anc)
circ.Measure(anc, par)
+```
+
+% Query circuits to identify what qubits and bits it contains
-.. Query circuits to identify what qubits and bits it contains
+A {py:class}`~pytket.circuit.Circuit` can be inspected to identify what qubits and bits it contains.
-A :py:class:`~pytket.circuit.Circuit` can be inspected to identify what qubits and bits it contains.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit, Qubit
@@ -259,53 +284,63 @@ A :py:class:`~pytket.circuit.Circuit` can be inspected to identify what qubits a
print(circ.qubits)
print(circ.bits)
+```
-.. Restrictions on registers (circuit will reject ids if they are already in use or the index dimension/resource type is inconsistent with existing ids of that name)
+% Restrictions on registers (circuit will reject ids if they are already in use or the index dimension/resource type is inconsistent with existing ids of that name)
-To help encourage consistency of identifiers, a :py:class:`~pytket.circuit.Circuit` will reject a new (qu)bit or register if it disagrees with existing IDs with the same name; that is, it refers to a different resource type (qubit vs bit), the index has a different dimension, or some resource already exists with the exact same ID in the :py:class:`~pytket.circuit.Circuit`. Identifiers with the same register name do not have to have contiguous indices (many devices require non-contiguous indices because qubits may be taken offline over the lifetime of the device).
+To help encourage consistency of identifiers, a {py:class}`~pytket.circuit.Circuit` will reject a new (qu)bit or register if it disagrees with existing IDs with the same name; that is, it refers to a different resource type (qubit vs bit), the index has a different dimension, or some resource already exists with the exact same ID in the {py:class}`~pytket.circuit.Circuit`. Identifiers with the same register name do not have to have contiguous indices (many devices require non-contiguous indices because qubits may be taken offline over the lifetime of the device).
-.. jupyter-execute::
+
+```{code-cell} ipython3
:raises: RuntimeError
from pytket import Circuit, Qubit, Bit
-
+
circ = Circuit()
# set up a circuit with qubit a[0]
circ.add_qubit(Qubit("a", 0))
# rejected because "a" is already a qubit register
circ.add_bit(Bit("a", 1))
+```
-.. jupyter-execute::
+
+```{code-cell} ipython3
:raises: RuntimeError
# rejected because "a" is already a 1D register
circ.add_qubit(Qubit("a", [1, 2]))
circ.add_qubit(Qubit("a"))
+```
+
-.. jupyter-execute::
+```{code-cell} ipython3
:raises: RuntimeError
# rejected because a[0] is already in the circuit
circ.add_qubit(Qubit("a", 0))
+```
-.. Integer labels correspond to default registers (example of using explicit labels from `Circuit(n)`)
+% Integer labels correspond to default registers (example of using explicit labels from `Circuit(n)`)
-The basic integer identifiers are actually a special case, referring to the default qubit (``q[i]``) and bit (``c[i]``) registers. We can create the :py:class:`~pytket.unit_id.UnitID` using the nameless :py:class:`~pytket.unit_id.Qubit` and :py:class:`~pytket.unit_id.Bit` constructors.
+The basic integer identifiers are actually a special case, referring to the default qubit (`q[i]`) and bit (`c[i]`) registers. We can create the {py:class}`~pytket.unit_id.UnitID` using the nameless {py:class}`~pytket.unit_id.Qubit` and {py:class}`~pytket.unit_id.Bit` constructors.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit, Qubit, Bit
circ = Circuit(4, 2)
circ.CX(Qubit(0), Qubit("q", 1)) # same as circ.CX(0, 1)
circ.Measure(Qubit(2), Bit("c", 0)) # same as circ.Measure(2, 0)
+```
+
+% Rename with `rename_units` as long as the names after renaming would be unique and have consistent register typings
-.. Rename with `rename_units` as long as the names after renaming would be unique and have consistent register typings
+In some circumstances, it may be useful to rename the resources in the {py:class}`~pytket.circuit.Circuit`. Given a partial map on {py:class}`UnitID` s, {py:meth}`~pytket.circuit.Circuit.rename_units` will change the association of IDs to resources (as long as the final labelling would still have consistent types for all registers). Any unspecified IDs will be preserved.
-In some circumstances, it may be useful to rename the resources in the :py:class:`~pytket.circuit.Circuit`. Given a partial map on :py:class:`UnitID` s, :py:meth:`~pytket.circuit.Circuit.rename_units` will change the association of IDs to resources (as long as the final labelling would still have consistent types for all registers). Any unspecified IDs will be preserved.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit, Qubit, Bit
@@ -320,17 +355,20 @@ In some circumstances, it may be useful to rename the resources in the :py:class
circ.rename_units(qubit_map)
print(circ.qubits)
print(circ.bits)
+```
-Composing Circuits
-------------------
+## Composing Circuits
+
+% Appending matches units of the same id
-.. Appending matches units of the same id
.. currentmodule:: pytket.circuit
+```
+
+Because {py:class}`Circuit` s are defined to have open inputs and outputs, it is perfectly natural to compose them by unifying the outputs of one with the inputs of another. Appending one {py:class}`Circuit` to the end of another matches the inputs and outputs with the same {py:class}`UnitID`.
-Because :py:class:`Circuit` s are defined to have open inputs and outputs, it is perfectly natural to compose them by unifying the outputs of one with the inputs of another. Appending one :py:class:`Circuit` to the end of another matches the inputs and outputs with the same :py:class:`UnitID`.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit, Qubit, Bit
@@ -345,16 +383,18 @@ Because :py:class:`Circuit` s are defined to have open inputs and outputs, it
circ.append(measures)
circ
+```
-.. If a unit does not exist in the other circuit, treated as composing with identity
+% If a unit does not exist in the other circuit, treated as composing with identity
+:::{note}
+If one {py:class}`Circuit` lacks some unit present in the other, then we treat it as if it is an identity on that unit. In the extreme case where the {py:class}`Circuit` s are defined with disjoint sets of {py:class}`UnitID` s, the {py:meth}`Circuit.append` method will compose them in parallel.
+:::
-.. note:: If one :py:class:`Circuit` lacks some unit present in the other, then we treat it as if it is an identity on that unit. In the extreme case where the :py:class:`Circuit` s are defined with disjoint sets of :py:class:`UnitID` s, the :py:meth:`Circuit.append` method will compose them in parallel.
+To compose two circuits in parallel we can take tensor product using the * operator. This requires that the qubits in the circuits have distinct names.
-To compose two circuits in parallel we can take tensor product using the * operator. This requires that the qubits in the circuits have distinct names.
-
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
from pytket.circuit.display import render_circuit_jupyter as draw
@@ -371,10 +411,12 @@ To compose two circuits in parallel we can take tensor product using the * opera
circ3 = circ1 * circ2 # Take the tensor product
draw(circ3)
+```
+
+If we attempt to form the tensor product of two circuits without distinct qubit names then we will get a {py:class}`RuntimeError` as the composition is not defined.
-If we attempt to form the tensor product of two circuits without distinct qubit names then we will get a :py:class:`RuntimeError` as the composition is not defined.
-.. jupyter-execute::
+```{code-cell} ipython3
:raises: RuntimeError
from pytket import Circuit
@@ -387,35 +429,46 @@ If we attempt to form the tensor product of two circuits without distinct qubit
circ_x * circ_y # Error as both circuits have l[0]
+```
+
+% Append onto different qubits with `append_with_map` (equivalent under `rename_units`)
+
+% To change which units get unified, :py:meth:`~pytket.circuit.Circuit.append_with_map` accepts a dictionary of :py:class:`UnitID` s, mapping the units of the argument to units of the main :py:class:`~pytket.circuit.Circuit`.
+
+% ```{code-cell} ipython3
-.. Append onto different qubits with `append_with_map` (equivalent under `rename_units`)
+% from pytket import Circuit, Qubit
-.. To change which units get unified, :py:meth:`~pytket.circuit.Circuit.append_with_map` accepts a dictionary of :py:class:`UnitID` s, mapping the units of the argument to units of the main :py:class:`~pytket.circuit.Circuit`.
+% circ = Circuit()
-.. .. jupyter-execute::
+% a = circ.add_q_register("a", 2)
-.. from pytket import Circuit, Qubit
-.. circ = Circuit()
-.. a = circ.add_q_register("a", 2)
-.. circ.Rx(0.2, a[0])
-.. circ.CX(a[0], a[1])
+% circ.Rx(0.2, a[0])
-.. next = Circuit()
-.. b = next.add_q_register("b", 2)
-.. next.Z(b[0])
-.. next.CZ(b[1], b[0])
+% circ.CX(a[0], a[1])
-.. circ.append_with_map(next, {b[1] : a[0]})
+% next = Circuit()
-.. # This is equivalent to:
-.. # temp = next.copy()
-.. # temp.rename_units({b[1] : a[0]})
-.. # circ.append(temp)
+% b = next.add_q_register("b", 2)
+% next.Z(b[0])
-To change which units get unified, we could use :py:meth:`Circuit.rename_units` as seen before. In the case where we just want to append a subcircuit like a gate, we can do this with :py:meth:`Circuit.add_circuit`.
+% next.CZ(b[1], b[0])
-.. jupyter-execute::
+% circ.append_with_map(next, {b[1] : a[0]})
+
+% # This is equivalent to:
+
+% # temp = next.copy()
+
+% # temp.rename_units({b[1] : a[0]})
+
+% # circ.append(temp)
+
+To change which units get unified, we could use {py:meth}`Circuit.rename_units` as seen before. In the case where we just want to append a subcircuit like a gate, we can do this with {py:meth}`Circuit.add_circuit`.
+
+
+```{code-cell} ipython3
from pytket import Circuit, Qubit
@@ -436,44 +489,52 @@ To change which units get unified, we could use :py:meth:`Circuit.rename_units`
# circ.append(temp)
draw(circ)
+```
+
+:::{note}
+This requires the subcircuit to be defined only over the default registers so that the list of arguments given to {py:meth}`Circuit.add_circuit` can easily be mapped.
+:::
-.. note:: This requires the subcircuit to be defined only over the default registers so that the list of arguments given to :py:meth:`Circuit.add_circuit` can easily be mapped.
+## Statevectors and Unitaries
-Statevectors and Unitaries
---------------------------
+When working with quantum circuits we may want access to the quantum state prepared by our circuit. This can be helpful if we want to check whether our circuit construction is correct. The {py:meth}`Circuit.get_statevector` method will produce the statevector of our system after the circuit is applied. Here it is assumed that all the qubits are initialised in the $|0\rangle^{\otimes n}$ state.
-When working with quantum circuits we may want access to the quantum state prepared by our circuit. This can be helpful if we want to check whether our circuit construction is correct. The :py:meth:`Circuit.get_statevector` method will produce the statevector of our system after the circuit is applied. Here it is assumed that all the qubits are initialised in the :math:`|0\rangle^{\otimes n}` state.
-
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
circ = Circuit(2)
circ.H(0).CX(0, 1)
circ.get_statevector()
+```
+
+In addition {py:meth}`Circuit.get_unitary` can be used to numerically calculate the unitary matrix that will be applied by the circuit.
-In addition :py:meth:`Circuit.get_unitary` can be used to numerically calculate the unitary matrix that will be applied by the circuit.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
circ = Circuit(2)
circ.H(0).CZ(0, 1).H(1)
circ.get_unitary()
+```
-.. warning:: The unitary matrix of a quantum circuit is of dimension :math:`(2^n \times 2^n)` where :math:`n` is the number of qubits. The statevector will be a column vector with :math:`2^n` entries . Due to this exponential scaling it will in general be very inefficient to compute the unitary (or statevector) of a circuit. These functions are intended to be used for sanity checks and spotting mistakes in small circuits.
+:::{warning}
+The unitary matrix of a quantum circuit is of dimension $(2^n \times 2^n)$ where $n$ is the number of qubits. The statevector will be a column vector with $2^n$ entries . Due to this exponential scaling it will in general be very inefficient to compute the unitary (or statevector) of a circuit. These functions are intended to be used for sanity checks and spotting mistakes in small circuits.
+:::
-Analysing Circuits
-------------------
+## Analysing Circuits
-.. Most basic form is to ask for the sequence of operations in the circuit; iteration produces `Command`s, containing an `Op` acting on `args`
+% Most basic form is to ask for the sequence of operations in the circuit; iteration produces `Command`s, containing an `Op` acting on `args`
-After creating a :py:class:`~pytket.circuit.Circuit`, we will typically want to inspect what we have constructed to ensure that it agrees with the design we planned. The most basic form of this is to just get the object to return the sequence of operations back to us. Iterating through the :py:class:`~pytket.circuit.Circuit` object will give back the operations as :py:class:`~pytket.circuit.Command` s (specifying the operations performed and what (qu)bits they are performed on).
+After creating a {py:class}`~pytket.circuit.Circuit`, we will typically want to inspect what we have constructed to ensure that it agrees with the design we planned. The most basic form of this is to just get the object to return the sequence of operations back to us. Iterating through the {py:class}`~pytket.circuit.Circuit` object will give back the operations as {py:class}`~pytket.circuit.Command` s (specifying the operations performed and what (qu)bits they are performed on).
-Because the :py:class:`~pytket.circuit.Circuit` class identifies circuits up to DAG equivalence, the sequence will be some topological sort of the DAG, but not necessarily identical to the order the operations were added to the :py:class:`~pytket.circuit.Circuit`.
+Because the {py:class}`~pytket.circuit.Circuit` class identifies circuits up to DAG equivalence, the sequence will be some topological sort of the DAG, but not necessarily identical to the order the operations were added to the {py:class}`~pytket.circuit.Circuit`.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -483,12 +544,14 @@ Because the :py:class:`~pytket.circuit.Circuit` class identifies circuits up to
for com in circ: # equivalently, circ.get_commands()
print(com.op, com.op.type, com.args)
# NOTE: com is not a reference to something inside circ; this cannot be used to modify the circuit
+```
+
+% To see more succinctly, can visualise in circuit form or the underlying DAG
-.. To see more succinctly, can visualise in circuit form or the underlying DAG
+If you are working in a Jupyter environment, a {py:class}`~pytket.circuit.Circuit` can be rendered using html for inline display.
-If you are working in a Jupyter environment, a :py:class:`~pytket.circuit.Circuit` can be rendered using html for inline display.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
from pytket.circuit.display import render_circuit_jupyter as draw
@@ -496,12 +559,16 @@ If you are working in a Jupyter environment, a :py:class:`~pytket.circuit.Circui
circ = Circuit(3)
circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
draw(circ) # Render interactive circuit diagram
+```
-.. note:: The pytket circuit renderer can represent circuits in the standard circuit model or in the ZX representation. Other interactive features include adjustable zoom, circuit wrapping and image export.
+:::{note}
+The pytket circuit renderer can represent circuits in the standard circuit model or in the ZX representation. Other interactive features include adjustable zoom, circuit wrapping and image export.
+:::
-``pytket`` also features methods to visualise the underlying circuit DAG graphically for easier visual inspection.
+`pytket` also features methods to visualise the underlying circuit DAG graphically for easier visual inspection.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
from pytket.utils import Graph
@@ -509,14 +576,18 @@ If you are working in a Jupyter environment, a :py:class:`~pytket.circuit.Circui
circ = Circuit(3)
circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
Graph(circ).get_DAG() # Displays in interactive python notebooks
+```
+
+The visualisation tool can also describe the interaction graph of a {py:class}`~pytket.circuit.Circuit` consisting of only one- and two-qubit gates -- that is, the graph of which qubits will share a two-qubit gate at some point during execution.
-The visualisation tool can also describe the interaction graph of a :py:class:`~pytket.circuit.Circuit` consisting of only one- and two-qubit gates -- that is, the graph of which qubits will share a two-qubit gate at some point during execution.
+:::{note}
+The visualisations above are shown in ipython notebook cells. When working with a normal python script one can view rendered circuits in the browser with the {py:meth}`~pytket.circuit.display.view_browser` function from the display module.
-.. note:: The visualisations above are shown in ipython notebook cells. When working with a normal python script one can view rendered circuits in the browser with the :py:meth:`~pytket.circuit.display.view_browser` function from the display module.
+There are also the methods {py:meth}`~pytket.utils.Graph.save_DAG` and {py:meth}`~pytket.utils.Graph.view_DAG` for saving and visualising the circuit DAG.
+:::
- There are also the methods :py:meth:`~pytket.utils.Graph.save_DAG` and :py:meth:`~pytket.utils.Graph.view_DAG` for saving and visualising the circuit DAG.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
from pytket.utils import Graph
@@ -524,12 +595,14 @@ The visualisation tool can also describe the interaction graph of a :py:class:`~
circ = Circuit(4)
circ.CX(0, 1).CZ(1, 2).ZZPhase(0.63, 2, 3).CX(1, 3).CY(0, 1)
Graph(circ).get_qubit_graph()
+```
-.. Won't always want this much detail, so can also query for common metrics (gate count, specific ops, depth, T-depth and 2q-depth)
+% Won't always want this much detail, so can also query for common metrics (gate count, specific ops, depth, T-depth and 2q-depth)
The full instruction sequence may often be too much detail for a lot of needs, especially for large circuits. Common circuit metrics like gate count and depth are used to approximate the difficulty of running it on a device, providing some basic tools to help distinguish different implementations of a given algorithm.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -538,14 +611,16 @@ The full instruction sequence may often be too much detail for a lot of needs, e
print("Total gate count =", circ.n_gates)
print("Circuit depth =", circ.depth())
+```
-As characteristics of a :py:class:`~pytket.circuit.Circuit` go, these are pretty basic. In terms of approximating the noise level, they fail heavily from weighting all gates evenly when, in fact, some will be much harder to implement than others. For example, in the NISQ era, we find that most technologies provide good single-qubit gate times and fidelities, with two-qubit gates being much slower and noisier [Arut2019]_. On the other hand, looking forward to the fault-tolerant regime we will expect Clifford gates to be very cheap but the magic :math:`T` gates to require expensive distillation procedures [Brav2005]_ [Brav2012]_.
+As characteristics of a {py:class}`~pytket.circuit.Circuit` go, these are pretty basic. In terms of approximating the noise level, they fail heavily from weighting all gates evenly when, in fact, some will be much harder to implement than others. For example, in the NISQ era, we find that most technologies provide good single-qubit gate times and fidelities, with two-qubit gates being much slower and noisier [^cite_arut2019]. On the other hand, looking forward to the fault-tolerant regime we will expect Clifford gates to be very cheap but the magic $T$ gates to require expensive distillation procedures [^cite_brav2005] [^cite_brav2012].
-We can use the :py:class:`~pytket.circuit.OpType` enum class to look for the number of gates of a particular type. Additionally, the methods :py:meth:`~pytket.circuit.Circuit.n_1qb_gates`, :py:meth:`~pytket.circuit.Circuit.n_2qb_gates` and :py:meth:`~pytket.circuit.Circuit.n_nqb_gates` can be used to count the number of gates in terms of how many qubits they act upon irrespective of type.
+We can use the {py:class}`~pytket.circuit.OpType` enum class to look for the number of gates of a particular type. Additionally, the methods {py:meth}`~pytket.circuit.Circuit.n_1qb_gates`, {py:meth}`~pytket.circuit.Circuit.n_2qb_gates` and {py:meth}`~pytket.circuit.Circuit.n_nqb_gates` can be used to count the number of gates in terms of how many qubits they act upon irrespective of type.
-We also define :math:`G`-depth (for a subset of gate types :math:`G`) as the minimum number of layers of gates in :math:`G` required to run the :py:class:`~pytket.circuit.Circuit`, allowing for topological reorderings. Specific cases of this like :math:`T`-depth and :math:`CX`-depth are common to the literature on circuit simplification [Amy2014]_ [Meij2020]_.
+We also define $G$-depth (for a subset of gate types $G$) as the minimum number of layers of gates in $G$ required to run the {py:class}`~pytket.circuit.Circuit`, allowing for topological reorderings. Specific cases of this like $T$-depth and $CX$-depth are common to the literature on circuit simplification [^cite_amy2014] [^cite_meij2020].
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit, OpType
from pytket.circuit.display import render_circuit_jupyter as draw
@@ -568,39 +643,43 @@ We also define :math:`G`-depth (for a subset of gate types :math:`G`) as the min
print("#3qb gates =", circ.n_nqb_gates(3)) # count the single CnRy gate (n=3)
print("T gate depth =", circ.depth_by_type(OpType.T))
print("2qb gate depth =", circ.depth_by_type({OpType.CX, OpType.CZ}))
+```
+
+:::{note}
+Each of these metrics will analyse the {py:class}`~pytket.circuit.Circuit` "as is", so they will consider each Box as a single unit rather than breaking it down into basic gates, nor will they perform any non-trivial gate commutations (those that don't just follow by deformation of the DAG) or gate decompositions (e.g. recognising that a $CZ$ gate would contribute 1 to $CX$-count in practice).
+:::
-.. note:: Each of these metrics will analyse the :py:class:`~pytket.circuit.Circuit` "as is", so they will consider each Box as a single unit rather than breaking it down into basic gates, nor will they perform any non-trivial gate commutations (those that don't just follow by deformation of the DAG) or gate decompositions (e.g. recognising that a :math:`CZ` gate would contribute 1 to :math:`CX`-count in practice).
+Its also possible to count all the occurrences of each {py:class}`~pytket.circuit.OpType` using the {py:func}`~pytket.utils.stats.gate_counts` function from the {py:mod}`pytket.utils` module.
-Its also possible to count all the occurrences of each :py:class:`~pytket.circuit.OpType` using the :py:func:`~pytket.utils.stats.gate_counts` function from the :py:mod:`pytket.utils` module.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.utils.stats import gate_counts
gate_counts(circ)
+```
-We obtain a :py:class:`collections.Counter` object where the keys are the various :py:class:`~pytket.circuit.OpType` s and the values represent how frequently each :py:class:`~pytket.circuit.OpType` appears in our :py:class:`~pytket.circuit.Circuit`. This method summarises the gate counts obtained for the circuit shown above.
+We obtain a {py:class}`collections.Counter` object where the keys are the various {py:class}`~pytket.circuit.OpType` s and the values represent how frequently each {py:class}`~pytket.circuit.OpType` appears in our {py:class}`~pytket.circuit.Circuit`. This method summarises the gate counts obtained for the circuit shown above.
-Boxes
------
+## Boxes
-Working with individual basic gates is sufficient for implementing arbitrary circuits, but that doesn't mean it is the most convenient option. It is generally far easier to argue the correctness of a circuit's design when it is constructed using higher-level constructions. In ``pytket``, the concept of a "Box" is to abstract away such complex structures as black-boxes within larger circuits.
+Working with individual basic gates is sufficient for implementing arbitrary circuits, but that doesn't mean it is the most convenient option. It is generally far easier to argue the correctness of a circuit's design when it is constructed using higher-level constructions. In `pytket`, the concept of a "Box" is to abstract away such complex structures as black-boxes within larger circuits.
-Defining higher level subroutines as boxes is also beneficial from a circuit optimisation point of view. If the compiler can identify higher level structure in the circuit, this can be exploited to reduce the number of elementary gates in the compiled circuit. Examples of such optimisations can be seen in :py:class:`~pytket.circuit.ToffoliBox` which permutes the computational basis states and :py:class:`~pytket.circuit.ConjugationBox` which allows for more efficient controlled gates by exploiting circuit symmetry.
+Defining higher level subroutines as boxes is also beneficial from a circuit optimisation point of view. If the compiler can identify higher level structure in the circuit, this can be exploited to reduce the number of elementary gates in the compiled circuit. Examples of such optimisations can be seen in {py:class}`~pytket.circuit.ToffoliBox` which permutes the computational basis states and {py:class}`~pytket.circuit.ConjugationBox` which allows for more efficient controlled gates by exploiting circuit symmetry.
-Circuit Boxes
-=============
+### Circuit Boxes
-.. Boxes abstract away complex structures as black-box units within larger circuits
+% Boxes abstract away complex structures as black-box units within larger circuits
-.. Simplest case is the `CircBox`
+% Simplest case is the `CircBox`
-The simplest example of this is a :py:class:`~pytket.circuit.CircBox`, which wraps up another :py:class:`~pytket.circuit.Circuit` defined elsewhere into a single black-box. The difference between adding a :py:class:`~pytket.circuit.CircBox` and just appending the :py:class:`~pytket.circuit.Circuit` is that the :py:class:`~pytket.circuit.CircBox` allows us to wrap up and abstract away the internal structure of the subcircuit we are adding so it appears as if it were a single gate when we view the main :py:class:`~pytket.circuit.Circuit`.
+The simplest example of this is a {py:class}`~pytket.circuit.CircBox`, which wraps up another {py:class}`~pytket.circuit.Circuit` defined elsewhere into a single black-box. The difference between adding a {py:class}`~pytket.circuit.CircBox` and just appending the {py:class}`~pytket.circuit.Circuit` is that the {py:class}`~pytket.circuit.CircBox` allows us to wrap up and abstract away the internal structure of the subcircuit we are adding so it appears as if it were a single gate when we view the main {py:class}`~pytket.circuit.Circuit`.
Let's first build a basic quantum circuit which implements a simplified version of a Grover oracle and then add
it to another circuit as part of a larger algorithm.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import Circuit, OpType
from pytket.circuit.display import render_circuit_jupyter as draw
@@ -615,10 +694,12 @@ it to another circuit as part of a larger algorithm.
oracle_circ.X(2)
draw(oracle_circ)
+```
+
+Now that we've built our circuit we can wrap it up in a {py:class}`~pytket.circuit.CircBox` and add it to a another circuit as a subroutine.
-Now that we've built our circuit we can wrap it up in a :py:class:`~pytket.circuit.CircBox` and add it to a another circuit as a subroutine.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import CircBox
@@ -629,22 +710,25 @@ Now that we've built our circuit we can wrap it up in a :py:class:`~pytket.circu
draw(circ)
+```
See how the name of the circuit appears in the rendered circuit diagram. Clicking on the box will show the underlying circuit.
-.. Note:: Despite the :py:class:`~pytket.circuit.Circuit` class having methods for adding each type of box, the :py:meth:`Circuit.add_gate` is sufficiently general to append any pytket OpType to a :py:class:`~pytket.circuit.Circuit`.
-
+:::{Note}
+Despite the {py:class}`~pytket.circuit.Circuit` class having methods for adding each type of box, the {py:meth}`Circuit.add_gate` is sufficiently general to append any pytket OpType to a {py:class}`~pytket.circuit.Circuit`.
+:::
When constructing subroutines to implement quantum algorithms it is natural to distinguish different groups of qubits. For instance, in the quantum phase estimation algorithm (QPE) we would want to distinguish between state preparation qubits and ancillary qubits which are measured to yield an approximation of the phase.
-The QPE can then be used as a subroutine in other algorithms: for example, integer factoring or estimating the ground state energy of some molecule. For more on the phase estimation algorithm see the `QPE example notebook `_.
+The QPE can then be used as a subroutine in other algorithms: for example, integer factoring or estimating the ground state energy of some molecule. For more on the phase estimation algorithm see the [QPE example notebook](https://tket.quantinuum.com/examples/phase_estimation.html).
+
+For such algorithms we may wish to create a {py:class}`~pytket.circuit.CircBox` containing qubit registers with distinct names. Below we will show construction of a simplified quantum phase estimation circuit which we will then turn into a subroutine.
-For such algorithms we may wish to create a :py:class:`~pytket.circuit.CircBox` containing qubit registers with distinct names. Below we will show construction of a simplified quantum phase estimation circuit which we will then turn into a subroutine.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import Circuit
- # Set up circuit registers
+ # Set up circuit registers
qpe_circ = Circuit(name="QPE")
a = qpe_circ.add_q_register("a", 2)
s = qpe_circ.add_q_register("s", 1)
@@ -667,24 +751,29 @@ For such algorithms we may wish to create a :py:class:`~pytket.circuit.CircBox`
# Measure qubits writing to the classical register
qpe_circ.measure_register(a, "c")
-
+
draw(qpe_circ)
+```
+
.. currentmodule:: pytket.circuit
+```
-Now that we have defined our phase estimation circuit we can use a :py:class:`CircBox` to define a reusable subroutine. This :py:class:`CircBox` will contain the state preparation and ancilla registers.
+Now that we have defined our phase estimation circuit we can use a {py:class}`CircBox` to define a reusable subroutine. This {py:class}`CircBox` will contain the state preparation and ancilla registers.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import CircBox
# Construct QPE subroutine
qpe_box = CircBox(qpe_circ)
+```
-Let's now create a circuit to implement the QPE algorithm where we prepare the :math:`|1\rangle` state in the state prep register with a single X gate.
+Let's now create a circuit to implement the QPE algorithm where we prepare the $|1\rangle$ state in the state prep register with a single X gate.
-.. jupyter-execute::
+```{code-cell} ipython3
# Construct simplified state preparation circuit
algorithm_circ = Circuit()
@@ -694,13 +783,15 @@ Let's now create a circuit to implement the QPE algorithm where we prepare the :
algorithm_circ.X(state[0])
draw(algorithm_circ)
+```
+
+We can then compose our subroutine registerwise by using {py:meth}`Circuit.add_circbox_with_regmap` method.
-We can then compose our subroutine registerwise by using :py:meth:`Circuit.add_circbox_with_regmap` method.
+To use the method, we pass in a python dictionary which maps the registers inside the box to those outside. The keys are the register names inside the {py:class}`CircBox` and the values are the register names of the external {py:class}`Circuit`.
+Note that the sizes of the registers used as keys and values must be equal.
-To use the method, we pass in a python dictionary which maps the registers inside the box to those outside. The keys are the register names inside the :py:class:`CircBox` and the values are the register names of the external :py:class:`Circuit`.
-Note that the sizes of the registers used as keys and values must be equal.
-.. jupyter-execute::
+```{code-cell} ipython3
# Append QPE subroutine to algorithm_circ registerwise
algorithm_circ.add_circbox_with_regmap(
@@ -708,14 +799,16 @@ Note that the sizes of the registers used as keys and values must be equal.
)
draw(algorithm_circ)
+```
Click on the QPE box in the diagram above to view the underlying circuit.
-If we want add a :py:class:`~pytket.circuit.CircBox` across multiple registers we can do this with the :py:meth:`Circuit.add_circbox_regwise` method.
+If we want add a {py:class}`~pytket.circuit.CircBox` across multiple registers we can do this with the {py:meth}`Circuit.add_circbox_regwise` method.
-Lets first define a circuit with the register names ``a``, ``b`` and ``c``.
+Lets first define a circuit with the register names `a`, `b` and `c`.
-.. jupyter-execute::
+
+```{code-cell} ipython3
# Set up Circuit with registers a_reg, b_reg and c_reg
abc_circuit = Circuit()
@@ -728,12 +821,14 @@ Lets first define a circuit with the register names ``a``, ``b`` and ``c``.
abc_circuit.Ry(0.46, a_reg[1])
abc_circuit.CCX(a_reg[0], a_reg[1], c_reg[0])
draw(abc_circuit)
+```
+
+Now lets create a {py:class}`CircBox` containing some elementary gates and append it across the `b` and `c` registers with {py:meth}`Circuit.add_circbox_regwise`.
-Now lets create a :py:class:`CircBox` containing some elementary gates and append it across the ``b`` and ``c`` registers with :py:meth:`Circuit.add_circbox_regwise`.
-.. jupyter-execute::
+```{code-cell} ipython3
- # Create subroutine
+ # Create subroutine
sub_circuit = Circuit(4, name="BC")
sub_circuit.CX(3, 2).CX(3, 1).CX(3, 0)
sub_circuit.H(3)
@@ -744,13 +839,14 @@ Now lets create a :py:class:`CircBox` containing some elementary gates and appen
draw(abc_circuit)
+```
-Boxes for Unitary Synthesis
-===========================
+### Boxes for Unitary Synthesis
-It is possible to specify small unitaries from ``numpy`` arrays and embed them directly into circuits as boxes, which can then be synthesised into gate sequences during compilation.
+It is possible to specify small unitaries from `numpy` arrays and embed them directly into circuits as boxes, which can then be synthesised into gate sequences during compilation.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import Circuit, Unitary1qBox, Unitary2qBox
import numpy as np
@@ -772,19 +868,23 @@ It is possible to specify small unitaries from ``numpy`` arrays and embed them d
circ.add_unitary2qbox(u2box, 1, 0)
draw(circ)
+```
+
+:::{note}
+For performance reasons pytket currently only supports unitary synthesis up to three qubits. Three-qubit synthesis can be accomplished with {py:class}`~pytket.circuit.Unitary3qBox` using a similar syntax.
+:::
+
+% `PauliExpBox` for simulations and general interactions
-.. note:: For performance reasons pytket currently only supports unitary synthesis up to three qubits. Three-qubit synthesis can be accomplished with :py:class:`~pytket.circuit.Unitary3qBox` using a similar syntax.
+Also in this category of synthesis boxes is {py:class}`~pytket.circuit.DiagonalBox`. This allows synthesis of circuits for diagonal unitaries.
+This box can be constructed by passing in a $(1 \times 2^n)$ numpy array representing the diagonal entries of the desired unitary matrix.
-.. `PauliExpBox` for simulations and general interactions
+### Controlled Box Operations
-Also in this category of synthesis boxes is :py:class:`~pytket.circuit.DiagonalBox`. This allows synthesis of circuits for diagonal unitaries.
-This box can be constructed by passing in a :math:`(1 \times 2^n)` numpy array representing the diagonal entries of the desired unitary matrix.
+If our subcircuit is a pure quantum circuit (i.e. it corresponds to a unitary operation), we can construct the controlled version that is applied coherently according to some set of control qubits. If all control qubits are in the $|1\rangle$ state, then the unitary is applied to the target system, otherwise it acts as an identity.
-Controlled Box Operations
-=========================
-If our subcircuit is a pure quantum circuit (i.e. it corresponds to a unitary operation), we can construct the controlled version that is applied coherently according to some set of control qubits. If all control qubits are in the :math:`|1\rangle` state, then the unitary is applied to the target system, otherwise it acts as an identity.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import Circuit, CircBox, QControlBox
@@ -803,24 +903,28 @@ If our subcircuit is a pure quantum circuit (i.e. it corresponds to a unitary op
circ.add_gate(cont, [0, 1, 2, 3])
draw(circ)
+```
-As well as creating controlled boxes, we can create a controlled version of an arbitrary :py:class:`~pytket.circuit.Op` as follows.
+As well as creating controlled boxes, we can create a controlled version of an arbitrary {py:class}`~pytket.circuit.Op` as follows.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import Op, OpType, QControlBox
op = Op.create(OpType.S)
ccs = QControlBox(op, 2)
+```
-.. note:: Whilst adding a control qubit is asymptotically efficient, the gate overhead is significant and can be hard to synthesise optimally, so using these constructions in a NISQ context should be done with caution.
-
-In addition, we can construct a :py:class:`~pytket.circuit.QControlBox` from any other pure quantum box type in pytket.
-For example, we can construct a multicontrolled :math:`\sqrt{Y}` operation as by first synthesising the base unitary with :py:class:`~pytket.circuit.Unitary1qBox` and then constructing a :py:class:`~pytket.circuit.QControlBox` from the box implementing :math:`\sqrt{Y}`.
+:::{note}
+Whilst adding a control qubit is asymptotically efficient, the gate overhead is significant and can be hard to synthesise optimally, so using these constructions in a NISQ context should be done with caution.
+:::
+In addition, we can construct a {py:class}`~pytket.circuit.QControlBox` from any other pure quantum box type in pytket.
+For example, we can construct a multicontrolled $\sqrt{Y}$ operation as by first synthesising the base unitary with {py:class}`~pytket.circuit.Unitary1qBox` and then constructing a {py:class}`~pytket.circuit.QControlBox` from the box implementing $\sqrt{Y}$.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import Unitary1qBox, QControlBox
import numpy as np
@@ -831,16 +935,18 @@ For example, we can construct a multicontrolled :math:`\sqrt{Y}` operation as by
sqrt_y_box = Unitary1qBox(sqrt_y)
c2_root_y = QControlBox(sqrt_y_box, 2)
+```
-Normally when we deal with controlled gates we implicitly assume that the control state is the "all :math:`|1\rangle`" state. So that the base gate is applied when all of the control qubits are all set to :math:`|1\rangle`.
+Normally when we deal with controlled gates we implicitly assume that the control state is the "all $|1\rangle$" state. So that the base gate is applied when all of the control qubits are all set to $|1\rangle$.
However its often useful to the flexibility to define the control state as some string of zeros and ones. Certain approaches to quantum algorithms with linear combination of unitaries (LCU) frequently make use of such gates.
-A :py:class:`~pytket.circuit.QControlBox` accepts an optional ``control_state`` argument in the constructor. This is either a list of binary values or a single (big-endian) integer representing the binary string.
+A {py:class}`~pytket.circuit.QControlBox` accepts an optional `control_state` argument in the constructor. This is either a list of binary values or a single (big-endian) integer representing the binary string.
-Lets now construct a multi-controlled Rz gate with the control state :math:`|0010\rangle`. This means that the base operation will only be applied if the control qubits are in the state :math:`|0010\rangle`.
+Lets now construct a multi-controlled Rz gate with the control state $|0010\rangle$. This means that the base operation will only be applied if the control qubits are in the state $|0010\rangle$.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import Circuit, Op, OpType, QControlBox
@@ -851,24 +957,24 @@ Lets now construct a multi-controlled Rz gate with the control state :math:`|001
test_circ.add_gate(multi_controlled_rz, test_circ.qubits)
draw(test_circ)
+```
-Notice how the circuit renderer shows both filled and unfilled circles on the control qubits. Filled circles correspond to :math:`|1\rangle` controls whereas empty circles represent :math:`|0\rangle`. As pytket uses the big-endian ordering convention we read off the control state from the top to the bottom of the circuit.
-
-Pauli Exponential Boxes
-=======================
+Notice how the circuit renderer shows both filled and unfilled circles on the control qubits. Filled circles correspond to $|1\rangle$ controls whereas empty circles represent $|0\rangle$. As pytket uses the big-endian ordering convention we read off the control state from the top to the bottom of the circuit.
-Another notable construct that is common to many algorithms and high-level circuit descriptions is the exponential of a Pauli tensor:
+### Pauli Exponential Boxes
-.. math::
-
- \begin{equation}
- e^{-i \frac{\pi}{2} \theta P}\,, \quad P \in \{I, X, Y, Z\}^{\otimes n}
- \end{equation}
+Another notable construct that is common to many algorithms and high-level circuit descriptions is the exponential of a Pauli tensor:
+$$
+\begin{equation}
+e^{-i \frac{\pi}{2} \theta P}\,, \quad P \in \{I, X, Y, Z\}^{\otimes n}
+\end{equation}
+$$
These occur very naturally in Trotterising evolution operators and native device operations.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import PauliExpBox
from pytket.pauli import Pauli
@@ -880,54 +986,56 @@ These occur very naturally in Trotterising evolution operators and native device
pauli_circ.add_gate(xyyz, [0, 1, 2, 3])
draw(pauli_circ)
+```
+
+To understand what happens inside a {py:class}`~pytket.circuit.PauliExpBox` let's take a look at the underlying circuit for $e^{-i \frac{\pi}{2}\theta XYYZ}$
-To understand what happens inside a :py:class:`~pytket.circuit.PauliExpBox` let's take a look at the underlying circuit for :math:`e^{-i \frac{\pi}{2}\theta XYYZ}`
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.passes import DecomposeBoxes
DecomposeBoxes().apply(pauli_circ)
draw(pauli_circ)
+```
-All Pauli exponentials of the form above can be implemented in terms of a single Rz(:math:`\theta`) rotation and a symmetric chain of CX gates on either side together with some single qubit basis rotations. This class of circuit is called a Pauli gadget. The subset of these circuits corresponding to "Z only" Pauli strings are referred to as phase gadgets.
+All Pauli exponentials of the form above can be implemented in terms of a single Rz($\theta$) rotation and a symmetric chain of CX gates on either side together with some single qubit basis rotations. This class of circuit is called a Pauli gadget. The subset of these circuits corresponding to "Z only" Pauli strings are referred to as phase gadgets.
-We see that the Pauli exponential :math:`e^{i\frac{\pi}{2} \theta \text{XYYZ}}` has basis rotations on the first three qubits. The V and Vdg gates rotate from the default Z basis to the Y basis and the Hadamard gate serves to change to the X basis.
+We see that the Pauli exponential $e^{i\frac{\pi}{2} \theta \text{XYYZ}}$ has basis rotations on the first three qubits. The V and Vdg gates rotate from the default Z basis to the Y basis and the Hadamard gate serves to change to the X basis.
-These Pauli gadget circuits have interesting algebraic properties which are useful for circuit optimisation.
+These Pauli gadget circuits have interesting algebraic properties which are useful for circuit optimisation.
-A Pauli gadget can be expressed as :math:`V \, A \, V^\dagger` where :math:`V` is the the circuit composed of CX gates and single qubit basis rotations on the right hand side of the Rz gate and :math:`A` is the Rz gate itself. This observation allows one to construct controlled Pauli gadgets much more efficently. See the `blog post `_ on the :py:class:`~pytket.circuit.ConjugationBox` construct for more details.
+A Pauli gadget can be expressed as $V \, A \, V^\dagger$ where $V$ is the the circuit composed of CX gates and single qubit basis rotations on the right hand side of the Rz gate and $A$ is the Rz gate itself. This observation allows one to construct controlled Pauli gadgets much more efficently. See the [blog post](https://tket.quantinuum.com/blog/posts/controlled_gates/) on the {py:class}`~pytket.circuit.ConjugationBox` construct for more details.
-For further discussion see the research publication on phase gadget synthesis [Cowt2020]_. Ideas from this paper are implemented in TKET as the `OptimisePhaseGadgets `_ and `PauliSimp `_ optimisation passes.
+For further discussion see the research publication on phase gadget synthesis [^cite_cowt2020]. Ideas from this paper are implemented in TKET as the [OptimisePhaseGadgets](https://tket.quantinuum.com/api-docs/passes.html#pytket.passes.OptimisePhaseGadgets) and [PauliSimp](https://tket.quantinuum.com/api-docs/passes.html#pytket.passes.PauliSimp) optimisation passes.
-Phase Polynomials
-=================
+### Phase Polynomials
Now we move on to discuss another class of quantum circuits known as phase polynomials. Phase polynomial circuits are a special type of circuits that use the {CX, Rz} gateset.
-A phase polynomial :math:`p(x)` is defined as a weighted sum of Boolean linear functions :math:`f_i(x)`:
-
-.. math::
-
- \begin{equation}
- p(x) = \sum_{i=1}^{2^n} \theta_i f_i(x)
- \end{equation}
+A phase polynomial $p(x)$ is defined as a weighted sum of Boolean linear functions $f_i(x)$:
-A phase polynomial circuit :math:`C` has the following action on computational basis states :math:`|x\rangle`:
+$$
+\begin{equation}
+p(x) = \sum_{i=1}^{2^n} \theta_i f_i(x)
+\end{equation}
+$$
-.. math::
+A phase polynomial circuit $C$ has the following action on computational basis states $|x\rangle$:
- \begin{equation}
- C: |x\rangle \longmapsto e^{2\pi i p(x)}|g(x)\rangle
- \end{equation}
+$$
+\begin{equation}
+C: |x\rangle \longmapsto e^{2\pi i p(x)}|g(x)\rangle
+\end{equation}
+$$
+Such a phase polynomial circuit can be synthesisied in pytket using the {py:class}`~pytket.circuit.PhasePolyBox`. A {py:class}`~pytket.circuit.PhasePolyBox` is constructed using the number of qubits, the qubit indices and a dictionary indicating whether or not a phase should be applied to specific qubits.
-Such a phase polynomial circuit can be synthesisied in pytket using the :py:class:`~pytket.circuit.PhasePolyBox`. A :py:class:`~pytket.circuit.PhasePolyBox` is constructed using the number of qubits, the qubit indices and a dictionary indicating whether or not a phase should be applied to specific qubits.
+Finally a `linear_transfromation` parameter needs to be specified: this is a matrix encoding the linear permutation between the bitstrings $|x\rangle$ and $|g(x)\rangle$ in the equation above.
-Finally a ``linear_transfromation`` parameter needs to be specified: this is a matrix encoding the linear permutation between the bitstrings :math:`|x\rangle` and :math:`|g(x)\rangle` in the equation above.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import PhasePolyBox
@@ -950,26 +1058,29 @@ Finally a ``linear_transfromation`` parameter needs to be specified: this is a
phase_poly_circ.add_gate(p_box, [0, 1, 2])
draw(p_box.get_circuit())
+```
-Multiplexors, Arbitrary State Preparation and :py:class:`~pytket.circuit.ToffoliBox`
-====================================================================================
+### Multiplexors, Arbitrary State Preparation and {py:class}`~pytket.circuit.ToffoliBox`
In the context of quantum circuits a multiplexor is type of generalised multicontrolled gate. Multiplexors grant us the flexibility to specify different operations on target qubits for different control states.
To create a multiplexor we simply construct a dictionary where the keys are the state of the control qubits and the values represent the operation performed on the target.
Lets implement a multiplexor with the following logic. Here we treat the first two qubits as controls and the third qubit as the target.
+> if control qubits in $|00\rangle$:
+>
+> : do Rz(0.3) on the third qubit
+>
+> else if control qubits in $|11\rangle$:
+>
+> : do H on the third qubit
+>
+> else:
+>
+> : do identity (i.e. do nothing)
- if control qubits in :math:`|00\rangle`:
- do Rz(0.3) on the third qubit
- else if control qubits in :math:`|11\rangle`:
- do H on the third qubit
- else:
- do identity (i.e. do nothing)
-
-
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import Op, OpType, MultiplexorBox
@@ -986,52 +1097,44 @@ Lets implement a multiplexor with the following logic. Here we treat the first t
draw(multi_circ)
+```
-Notice how in the example above the control qubits are both in the :math:`|1\rangle` state and so the multiplexor applies the Hadamard operation to the third qubit. If we calculate our statevector we see that the third qubit is in the
-:math:`|+\rangle = H|0\rangle` state.
+Notice how in the example above the control qubits are both in the $|1\rangle$ state and so the multiplexor applies the Hadamard operation to the third qubit. If we calculate our statevector we see that the third qubit is in the
+$|+\rangle = H|0\rangle$ state.
-.. jupyter-execute::
+```{code-cell} ipython3
# Assume all qubits initialised to |0> here
# Amplitudes of |+> approx 0.707...
print("Statevector =", np.round(multi_circ.get_statevector().real, 4))
+```
-In addition to the general :py:class:`~pytket.circuit.MultiplexorBox` pytket has several other type of multiplexor box operations available.
-
-==================================================== =====================================================
-Multiplexor Description
-==================================================== =====================================================
-:py:class:`~pytket.circuit.MultiplexorBox` The most general type of multiplexor (see above).
-
-:py:class:`~pytket.circuit.MultiplexedRotationBox` Multiplexor where the operation applied to the
- target is a rotation gate about a single axis.
-
-:py:class:`~pytket.circuit.MultiplexedU2Box` Multiplexor for uniformly controlled single
- qubit gates ( :math:`U(2)` operations).
-
-:py:class:`~pytket.circuit.MultiplexedTensoredU2Box` Multiplexor where the operation applied to the
- target is a tensor product of single qubit gates.
-
-==================================================== =====================================================
+In addition to the general {py:class}`~pytket.circuit.MultiplexorBox` pytket has several other type of multiplexor box operations available.
+| Multiplexor | Description |
+| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
+| {py:class}`~pytket.circuit.MultiplexorBox` | The most general type of multiplexor (see above). |
+| {py:class}`~pytket.circuit.MultiplexedRotationBox` | Multiplexor where the operation applied to the target is a rotation gate about a single axis. |
+| {py:class}`~pytket.circuit.MultiplexedU2Box` | Multiplexor for uniformly controlled single qubit gates ( $U(2)$ operations). |
+| {py:class}`~pytket.circuit.MultiplexedTensoredU2Box` | Multiplexor where the operation applied to the target is a tensor product of single qubit gates. |
One place where multiplexor operations are useful is in state preparation algorithms.
-TKET supports the preparation of arbitrary quantum states via the :py:class:`~pytket.circuit.StatePreparationBox`. This box takes a :math:`(1\times 2^n)` numpy array representing the :math:`n` qubit statevector where the entries represent the amplitudes of the quantum state.
+TKET supports the preparation of arbitrary quantum states via the {py:class}`~pytket.circuit.StatePreparationBox`. This box takes a $(1\times 2^n)$ numpy array representing the $n$ qubit statevector where the entries represent the amplitudes of the quantum state.
-Given the vector of amplitudes TKET will construct a box containing a sequence of multiplexors using the method outlined in [Shen2004]_.
+Given the vector of amplitudes TKET will construct a box containing a sequence of multiplexors using the method outlined in [^cite_shen2004].
-To demonstrate :py:class:`~pytket.circuit.StatePreparationBox` let's use it to prepare the W state.
+To demonstrate {py:class}`~pytket.circuit.StatePreparationBox` let's use it to prepare the W state.
-.. math::
+$$
+\begin{equation}
+|W\rangle = \frac{1}{\sqrt{3}} \big(|001\rangle + |010\rangle + |100\rangle \big)
+\end{equation}
+$$
- \begin{equation}
- |W\rangle = \frac{1}{\sqrt{3}} \big(|001\rangle + |010\rangle + |100\rangle \big)
- \end{equation}
-
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import StatePreparationBox
@@ -1042,39 +1145,47 @@ To demonstrate :py:class:`~pytket.circuit.StatePreparationBox` let's use it to p
state_circ = Circuit(3)
state_circ.add_gate(w_state_box, [0, 1, 2])
+```
+
-.. jupyter-execute::
+```{code-cell} ipython3
# Verify state preperation
np.round(state_circ.get_statevector().real, 3) # 1/sqrt(3) approx 0.577
+```
+
+:::{Note}
+Generic state preperation circuits can be very complex with the gatecount and depth increasing rapidly with the size of the state. In the special case where the desired state has only real-valued amplitudes, only multiplexed Ry operations are needed to accomplish the state preparation.
+:::
-.. Note:: Generic state preperation circuits can be very complex with the gatecount and depth increasing rapidly with the size of the state. In the special case where the desired state has only real-valued amplitudes, only multiplexed Ry operations are needed to accomplish the state preparation.
+For some use cases it may be desirable to reset all qubits to the $|0\rangle$ state prior to state preparation. This can be done using the `with_initial_reset` flag.
-For some use cases it may be desirable to reset all qubits to the :math:`|0\rangle` state prior to state preparation. This can be done using the ``with_initial_reset`` flag.
-.. jupyter-execute::
+```{code-cell} ipython3
# Ensure all qubits initialised to |0>
w_state_box_reset = StatePreparationBox(w_state, with_initial_reset=True)
-
-Finally let's consider another box type, namely the :py:class:`~pytket.circuit.ToffoliBox`. This box can be used to prepare an arbitrary permutation of the computational basis states.
-To construct the box we need to specify the permutation as a key-value pair where the key is the input basis state and the value is output.
-Let's construct a :py:class:`~pytket.circuit.ToffoliBox` to perform the following mapping:
-
-.. math::
+```
- \begin{gather}
- |001\rangle \longmapsto |111\rangle \\
- |111\rangle \longmapsto |001\rangle \\
- |100\rangle \longmapsto |000\rangle \\
- |000\rangle \longmapsto |100\rangle
- \end{gather}
-
-We can construct a :py:class:`~pytket.circuit.ToffoliBox` with a python dictionary where the basis states above are entered as key-value pairs.
+Finally let's consider another box type, namely the {py:class}`~pytket.circuit.ToffoliBox`. This box can be used to prepare an arbitrary permutation of the computational basis states.
+To construct the box we need to specify the permutation as a key-value pair where the key is the input basis state and the value is output.
+Let's construct a {py:class}`~pytket.circuit.ToffoliBox` to perform the following mapping:
+
+$$
+\begin{gather}
+|001\rangle \longmapsto |111\rangle \\
+|111\rangle \longmapsto |001\rangle \\
+|100\rangle \longmapsto |000\rangle \\
+|000\rangle \longmapsto |100\rangle
+\end{gather}
+$$
+
+We can construct a {py:class}`~pytket.circuit.ToffoliBox` with a python dictionary where the basis states above are entered as key-value pairs.
For correctness if a basis state appears as key in the permutation dictionary then it must also appear and a value.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import ToffoliBox
@@ -1088,41 +1199,47 @@ For correctness if a basis state appears as key in the permutation dictionary th
# Define box to perform the permutation
perm_box = ToffoliBox(permutation=mapping)
+```
-This permutation of basis states can be achieved with purely classical operations {X, CCX}, hence the name :py:class:`~pytket.circuit.ToffoliBox`.
-In pytket however, the permutation is implemented efficently using a sequence of multiplexed rotations followed by a :py:class:`~pytket.circuit.DiagonalBox`.
+This permutation of basis states can be achieved with purely classical operations {X, CCX}, hence the name {py:class}`~pytket.circuit.ToffoliBox`.
+In pytket however, the permutation is implemented efficently using a sequence of multiplexed rotations followed by a {py:class}`~pytket.circuit.DiagonalBox`.
-.. jupyter-execute::
+```{code-cell} ipython3
draw(perm_box.get_circuit())
+```
-Finally let's append the :py:class:`~pytket.circuit.ToffoliBox` onto our circuit preparing our w state to perform the permutation of basis states specified above.
+Finally let's append the {py:class}`~pytket.circuit.ToffoliBox` onto our circuit preparing our w state to perform the permutation of basis states specified above.
-.. jupyter-execute::
+```{code-cell} ipython3
state_circ.add_gate(perm_box, [0, 1, 2])
draw(state_circ)
+```
+
-.. jupyter-execute::
+```{code-cell} ipython3
np.round(state_circ.get_statevector().real, 3)
+```
-Looking at the statevector calculation we see that our :py:class:`~pytket.circuit.ToffoliBox` has exchanged the coefficents of our w state so that the non-zero coefficents are now on the :math:`|000\rangle` and :math:`|111\rangle` bitstrings with the coefficent of :math:`|010\rangle` remaining unchanged.
+Looking at the statevector calculation we see that our {py:class}`~pytket.circuit.ToffoliBox` has exchanged the coefficents of our w state so that the non-zero coefficents are now on the $|000\rangle$ and $|111\rangle$ bitstrings with the coefficent of $|010\rangle$ remaining unchanged.
+## Importing/Exporting Circuits
-Importing/Exporting Circuits
-----------------------------
.. currentmodule:: pytket.circuit
+```
-``pytket`` :py:class:`~pytket.circuit.Circuit` s can be natively serialized and deserialized from JSON-compatible dictionaries, using the :py:meth:`Circuit.to_dict` and :py:meth:`Circuit.from_dict` methods. This is the method of serialization which supports the largest class of circuits, and provides the highest fidelity.
+`pytket` {py:class}`~pytket.circuit.Circuit` s can be natively serialized and deserialized from JSON-compatible dictionaries, using the {py:meth}`Circuit.to_dict` and {py:meth}`Circuit.from_dict` methods. This is the method of serialization which supports the largest class of circuits, and provides the highest fidelity.
-.. jupyter-execute::
+
+```{code-cell} ipython3
import tempfile
import json
@@ -1142,17 +1259,19 @@ Importing/Exporting Circuits
new_circ = Circuit.from_dict(json.load(fp))
draw(new_circ)
+```
-.. Support other frameworks for easy conversion of existing code and enable freedom to choose preferred input system and use available high-level packages
+% Support other frameworks for easy conversion of existing code and enable freedom to choose preferred input system and use available high-level packages
-``pytket`` also supports interoperability with a number of other quantum software frameworks and programming languages for easy conversion of existing code and to provide users the freedom to choose their preferred input system and use available high-level packages.
+`pytket` also supports interoperability with a number of other quantum software frameworks and programming languages for easy conversion of existing code and to provide users the freedom to choose their preferred input system and use available high-level packages.
-.. OpenQASM (doubles up as method of serialising circuits)
+% OpenQASM (doubles up as method of serialising circuits)
OpenQASM is one of the current industry standards for low-level circuit description languages, featuring named quantum and classical registers, parameterised subroutines, and a limited form of conditional execution. Having bidirectional conversion support allows this to double up as a method of serializing circuits for later use.
Though less expressive than native dictionary serialization, it is widely supported and so serves as a platform-independent method of storing circuits.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.qasm import circuit_from_qasm, circuit_to_qasm_str
import tempfile, os
@@ -1172,14 +1291,18 @@ Though less expressive than native dictionary serialization, it is widely suppor
os.remove(path)
print(circuit_to_qasm_str(circ)) # print QASM string
+```
-.. Quipper
+% Quipper
-.. note:: The OpenQASM converters do not support circuits with :ref:`implicit qubit permutations `. This means that if a circuit contains such a permutation it will be ignored when exported to OpenQASM format.
+:::{note}
+The OpenQASM converters do not support circuits with {ref}`implicit qubit permutations `. This means that if a circuit contains such a permutation it will be ignored when exported to OpenQASM format.
+:::
-The core ``pytket`` package additionally features a converter from Quipper, another circuit description language.
+The core `pytket` package additionally features a converter from Quipper, another circuit description language.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.quipper import circuit_from_quipper
import tempfile, os
@@ -1195,16 +1318,20 @@ The core ``pytket`` package additionally features a converter from Quipper, anot
circ = circuit_from_quipper(path)
draw(circ)
os.remove(path)
+```
+
+:::{note}
+There are a few features of the Quipper language that are not supported by the converter, which are outlined in the {py:mod}`pytket.quipper` documentation.
+:::
-.. note:: There are a few features of the Quipper language that are not supported by the converter, which are outlined in the :py:mod:`pytket.quipper` documentation.
+% Extension modules; example with qiskit, cirq, pyquil; caution that they may not support all gate sets or features (e.g. conditional gates with qiskit only)
-.. Extension modules; example with qiskit, cirq, pyquil; caution that they may not support all gate sets or features (e.g. conditional gates with qiskit only)
+Converters for other quantum software frameworks can optionally be included by installing the corresponding extension module. These are additional PyPI packages with names `pytket-X`, which extend the `pytket` namespace with additional features to interact with other systems, either using them as a front-end for circuit construction and high-level algorithms or targeting simulators and devices as backends.
-Converters for other quantum software frameworks can optionally be included by installing the corresponding extension module. These are additional PyPI packages with names ``pytket-X``, which extend the ``pytket`` namespace with additional features to interact with other systems, either using them as a front-end for circuit construction and high-level algorithms or targeting simulators and devices as backends.
+For example, installing the `pytket-qiskit` package will add the {py:func}`~pytket.extensions.qiskit.tk_to_qiskit` and {py:func}`~pytket.extensions.qiskit.qiskit_to_tk` methods which convert between the {py:class}`~pytket.circuit.Circuit` class from `pytket` and {py:class}`~qiskit.circuit.QuantumCircuit`.
-For example, installing the ``pytket-qiskit`` package will add the :py:func:`~pytket.extensions.qiskit.tk_to_qiskit` and :py:func:`~pytket.extensions.qiskit.qiskit_to_tk` methods which convert between the :py:class:`~pytket.circuit.Circuit` class from ``pytket`` and :py:class:`~qiskit.circuit.QuantumCircuit`.
-.. jupyter-execute::
+```{code-cell} ipython3
from qiskit import QuantumCircuit
from math import pi
@@ -1214,10 +1341,12 @@ For example, installing the ``pytket-qiskit`` package will add the :py:func:`~py
qc.cx(0, 1)
qc.rz(pi/2, 1)
print(qc)
+```
-We can convert this :py:class:`~qiskit.circuit.QuantumCircuit` to a pytket :py:class:`Circuit`, append some gates and then convert back.
+We can convert this {py:class}`~qiskit.circuit.QuantumCircuit` to a pytket {py:class}`Circuit`, append some gates and then convert back.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit
@@ -1227,21 +1356,24 @@ We can convert this :py:class:`~qiskit.circuit.QuantumCircuit` to a pytket :py:c
qc2 = tk_to_qiskit(circ)
print(qc2)
+```
+
+## Symbolic Circuits
-Symbolic Circuits
------------------
+% Common pattern to construct many circuits with a similar shape and different gate parameters
-.. Common pattern to construct many circuits with a similar shape and different gate parameters
-.. Main example of ansatze for variational algorithms
+% Main example of ansatze for variational algorithms
In practice, it is very common for an experiment to use many circuits with similar structure but with varying gate parameters. In variational algorithms like VQE and QAOA, we are trying to explore the energy landscape with respect to the circuit parameters, realised as the angles of rotation gates. The only differences between iterations of the optimisation procedure are the specific angles of rotations in the circuits. Because the procedures of generating and compiling the circuits typically won't care what the exact angles are, we can define the circuits abstractly, treating each parameter as an algebraic symbol. The circuit generation and compilation can then be pulled outside of the optimisation loop, being performed once and for all rather than once for each set of parameter values.
-.. Symbolic parameters of circuits defined as sympy symbols
-.. Gate parameters can use arbitrary symbolic expressions
+% Symbolic parameters of circuits defined as sympy symbols
-`sympy `_ is a widely-used python package for symbolic expressions and algebraic manipulation, defining a sympy :py:class:`~sympy.core.symbol.Symbol` objects to represent algebraic variables and using them in sympy `Expression `_ s to build mathematical statements and arithmetic expressions. Symbolic circuits are managed in ``pytket`` by defining the circuit parameters as :py:class:`sympy.Symbol` s, which can be passed in as arguments to the gates and later substituted for concrete values.
+% Gate parameters can use arbitrary symbolic expressions
-.. jupyter-execute::
+[sympy](https://docs.sympy.org/latest/index.html) is a widely-used python package for symbolic expressions and algebraic manipulation, defining a sympy {py:class}`~sympy.core.symbol.Symbol` objects to represent algebraic variables and using them in sympy [Expression](https://docs.sympy.org/latest/explanation/glossary.html#term-Expression) s to build mathematical statements and arithmetic expressions. Symbolic circuits are managed in `pytket` by defining the circuit parameters as {py:class}`sympy.Symbol` s, which can be passed in as arguments to the gates and later substituted for concrete values.
+
+
+```{code-cell} ipython3
from pytket import Circuit, OpType
from sympy import Symbol
@@ -1260,12 +1392,14 @@ In practice, it is very common for an experiment to use many circuits with simil
s_map = {a:0.3, b:1.25}
circ.symbol_substitution(s_map)
print(circ.free_symbols())
+```
+
+% Instantiate by mapping symbols to values (in half-turns)
-.. Instantiate by mapping symbols to values (in half-turns)
+It is important to note that the units of the parameter values will still be in half-turns, and so may need conversion to/from radians if there is important semantic meaning to the parameter values. This can either be done at the point of interpreting the values, or by embedding the conversion into the {py:class}`~pytket.circuit.Circuit`.
-It is important to note that the units of the parameter values will still be in half-turns, and so may need conversion to/from radians if there is important semantic meaning to the parameter values. This can either be done at the point of interpreting the values, or by embedding the conversion into the :py:class:`~pytket.circuit.Circuit`.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
from sympy import Symbol, pi
@@ -1277,12 +1411,14 @@ It is important to note that the units of the parameter values will still be in
s_map = {a: pi/4}
circ.symbol_substitution(s_map)
draw(circ)
+```
-.. Can use substitution to replace by arbitrary expressions, including renaming alpha-conversion
+% Can use substitution to replace by arbitrary expressions, including renaming alpha-conversion
Substitution need not be for concrete values, but is defined more generally to allow symbols to be replaced by arbitrary expressions, including other symbols. This allows for alpha-conversion or to look at special cases with redundant parameters.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
from sympy import symbols
@@ -1294,13 +1430,16 @@ Substitution need not be for concrete values, but is defined more generally to a
s_map = {a: 2*a, c: a} # replacement happens simultaneously, and not recursively
circ.symbol_substitution(s_map)
draw(circ)
+```
+
+% Can query circuit for its free symbols
-.. Can query circuit for its free symbols
-.. Warning about devices and some optimisations will not function with symbolic gates
+% Warning about devices and some optimisations will not function with symbolic gates
-There are currently no simulators or devices that can run symbolic circuits algebraically, so every symbol must be instantiated before running. At any time, you can query the :py:class:`~pytket.circuit.Circuit` object for the set of free symbols it contains to check what would need to be instantiated before it can be run.
+There are currently no simulators or devices that can run symbolic circuits algebraically, so every symbol must be instantiated before running. At any time, you can query the {py:class}`~pytket.circuit.Circuit` object for the set of free symbols it contains to check what would need to be instantiated before it can be run.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit
from sympy import symbols
@@ -1313,18 +1452,22 @@ There are currently no simulators or devices that can run symbolic circuits alge
print(circ.free_symbols())
print(circ.is_symbolic()) # returns True when free_symbols() is non-empty
+```
-.. note:: There are some minor drawbacks associated with symbolic compilation. When using `Euler-angle equations `_ or quaternions for merging adjacent rotation gates, the resulting angles are given by some lengthy trigonometric expressions which cannot be evaluated down to just a number when one of the original angles was parameterised; this can lead to unhelpfully long expressions for the angles of some gates in the compiled circuit. It is also not possible to apply the :py:class:`pytket.passes.KAKDecomposition` pass to simplify a parameterised circuit, so that pass will only apply to non-parameterised subcircuits, potentially missing some valid opportunities for optimisation.
+:::{note}
+There are some minor drawbacks associated with symbolic compilation. When using [Euler-angle equations](https://tket.quantinuum.com/api-docs/passes.html#pytket.passes.EulerAngleReduction) or quaternions for merging adjacent rotation gates, the resulting angles are given by some lengthy trigonometric expressions which cannot be evaluated down to just a number when one of the original angles was parameterised; this can lead to unhelpfully long expressions for the angles of some gates in the compiled circuit. It is also not possible to apply the {py:class}`pytket.passes.KAKDecomposition` pass to simplify a parameterised circuit, so that pass will only apply to non-parameterised subcircuits, potentially missing some valid opportunities for optimisation.
+:::
-.. seealso:: To see how to use symbolic compilation in a variational experiment, have a look at our `VQE (UCCSD) example `_.
+:::{seealso}
+To see how to use symbolic compilation in a variational experiment, have a look at our [VQE (UCCSD) example](https://tket.quantinuum.com/examples/ucc_vqe.html).
+:::
+### Symbolic unitaries and states
-Symbolic unitaries and states
-=============================
+In {py:mod}`pytket.utils.symbolic` we provide functions {py:func}`~pytket.utils.symbolic.circuit_to_symbolic_unitary`, which can calculate the unitary representation of a possibly symbolic circuit, and {py:func}`~pytket.utils.symbolic.circuit_apply_symbolic_statevector`, which can apply a symbolic circuit to an input statevector and return the output state (effectively simulating it).
-In :py:mod:`pytket.utils.symbolic` we provide functions :py:func:`~pytket.utils.symbolic.circuit_to_symbolic_unitary`, which can calculate the unitary representation of a possibly symbolic circuit, and :py:func:`~pytket.utils.symbolic.circuit_apply_symbolic_statevector`, which can apply a symbolic circuit to an input statevector and return the output state (effectively simulating it).
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
from pytket.utils.symbolic import circuit_apply_symbolic_statevector, circuit_to_symbolic_unitary
@@ -1335,31 +1478,35 @@ In :py:mod:`pytket.utils.symbolic` we provide functions :py:func:`~pytket.utils.
circ.Rx(a/pi, 0).CX(0, 1)
# All zero input state is assumed if no initial state is provided
- display(circuit_apply_symbolic_statevector(circ))
+ display(circuit_apply_symbolic_statevector(circ))
circuit_to_symbolic_unitary(circ)
+```
+
+The unitaries are calculated using the unitary representation of each [OpType](https://tket.quantinuum.com/api-docs/optype.html) , and according to the default ILO BasisOrder convention used in backends [ILO BasisOrder convention used in backends](https://tket.quantinuum.com/user-manual/manual_backend.html#interpreting-results).
+The outputs are sympy [ImmutableMatrix](https://docs.sympy.org/latest/modules/matrices/immutablematrices.html) objects, and use the same symbols as in the circuit, so can be further substituted and manipulated.
+The conversion functions use the [sympy Quantum Mechanics module](https://docs.sympy.org/latest/modules/physics/quantum/index.html), see also the {py:func}`~pytket.utils.symbolic.circuit_to_symbolic_gates` and {py:func}`~pytket.utils.symbolic.circuit_apply_symbolic_qubit` functions to see how to work with those objects directly.
-The unitaries are calculated using the unitary representation of each `OpType `_ , and according to the default ILO BasisOrder convention used in backends `ILO BasisOrder convention used in backends `_.
-The outputs are sympy `ImmutableMatrix `_ objects, and use the same symbols as in the circuit, so can be further substituted and manipulated.
-The conversion functions use the `sympy Quantum Mechanics module `_, see also the :py:func:`~pytket.utils.symbolic.circuit_to_symbolic_gates` and :py:func:`~pytket.utils.symbolic.circuit_apply_symbolic_qubit` functions to see how to work with those objects directly.
+:::{warning}
+Unitaries corresponding to circuits with $n$ qubits have dimensions $2^n \times 2^n$, so are computationally very expensive to calculate. Symbolic calculation is also computationally costly, meaning calculation of symbolic unitaries is only really feasible for very small circuits (of up to a few qubits in size). These utilities are provided as way to test the design of small subcircuits to check they are performing the intended unitary. Note also that as mentioned above, compilation of a symbolic circuit can generate long symbolic expressions; converting these circuits to a symbolic unitary could then result in a matrix object that is very hard to work with or interpret.
+:::
-.. warning::
- Unitaries corresponding to circuits with :math:`n` qubits have dimensions :math:`2^n \times 2^n`, so are computationally very expensive to calculate. Symbolic calculation is also computationally costly, meaning calculation of symbolic unitaries is only really feasible for very small circuits (of up to a few qubits in size). These utilities are provided as way to test the design of small subcircuits to check they are performing the intended unitary. Note also that as mentioned above, compilation of a symbolic circuit can generate long symbolic expressions; converting these circuits to a symbolic unitary could then result in a matrix object that is very hard to work with or interpret.
+## Advanced Circuit Construction Topics
-Advanced Circuit Construction Topics
-------------------------------------
+### Custom parameterised Gates
-Custom parameterised Gates
-==========================
+% Custom gates can also be defined with custom parameters
-.. Custom gates can also be defined with custom parameters
-.. Define by giving a symbolic circuit and list of symbols to bind
-.. Instantiate upon inserting into circuit by providing concrete parameters
-.. Any symbols that are not bound are treated as free symbols in the global scope
+% Define by giving a symbolic circuit and list of symbols to bind
-The :py:class:`~pytket.circuit.CircBox` construction is good for subroutines where the instruction sequence is fixed. The :py:class:`~pytket.circuit.CustomGateDef` construction generalises this to construct parameterised subroutines by binding symbols in the definition circuit and instantiating them at each instance. Any symbolic :py:class:`~pytket.circuit.Circuit` can be provided as the subroutine definition. Remaining symbols that are not bound are treated as free symbols in the global scope.
+% Instantiate upon inserting into circuit by providing concrete parameters
-.. jupyter-execute::
+% Any symbols that are not bound are treated as free symbols in the global scope
+
+The {py:class}`~pytket.circuit.CircBox` construction is good for subroutines where the instruction sequence is fixed. The {py:class}`~pytket.circuit.CustomGateDef` construction generalises this to construct parameterised subroutines by binding symbols in the definition circuit and instantiating them at each instance. Any symbolic {py:class}`~pytket.circuit.Circuit` can be provided as the subroutine definition. Remaining symbols that are not bound are treated as free symbols in the global scope.
+
+
+```{code-cell} ipython3
from pytket.circuit import Circuit, CustomGateDef
from sympy import symbols
@@ -1379,15 +1526,16 @@ The :py:class:`~pytket.circuit.CircBox` construction is good for subroutines whe
draw(circ)
print(circ.free_symbols()) # Print remaining free symbols
+```
+
+### Clifford Tableaux
-Clifford Tableaux
-=================
+The Clifford (a.k.a. stabilizer) fragment of quantum theory is known to exhibit efficient classical representations of states and unitaries. This allows for short descriptions that can fully characterise a state/unitary and efficient circuit simulation. Whilst the Clifford group can be characterised as the operations generated by `CX`, `H`, and `S` gates with qubit initialisation in the $|0\rangle$ state, it is also the largest group of operations under which the Pauli group is closed, i.e. for any tensor of Paulis $P$ and Clifford operation $C$, $CPC^\dagger$ is also a tensor of Paulis.
-The Clifford (a.k.a. stabilizer) fragment of quantum theory is known to exhibit efficient classical representations of states and unitaries. This allows for short descriptions that can fully characterise a state/unitary and efficient circuit simulation. Whilst the Clifford group can be characterised as the operations generated by `CX`, `H`, and `S` gates with qubit initialisation in the :math:`|0\rangle` state, it is also the largest group of operations under which the Pauli group is closed, i.e. for any tensor of Paulis :math:`P` and Clifford operation :math:`C`, :math:`CPC^\dagger` is also a tensor of Paulis.
+Any state $|\psi\rangle$ in the Clifford fragment is uniquely identified by those tensors of Pauli operators that stabilize it (those $P$ such that $P|\psi\rangle = |\psi\rangle$). These stabilizers form a group of size $2^n$ for an $n$ qubit state, but it is sufficient to identify $n$ independent generators to specify the group. If a Clifford gate $C$ is applied to the state, each generator $P$ can be updated to $P' = CPC^\dagger$ since $C|\psi\rangle = CP|\psi\rangle = (CPC^\dagger)C|\psi\rangle$. We can therefore characterise each Clifford operation by its actions on generators of the Pauli group, giving us the Clifford tableau form. In `pytket`, the {py:class}`~pytket.tableau.UnitaryTableau` class uses the binary symplectic representation from Aaronson and Gottesman [^cite_aaro2004].
-Any state :math:`|\psi\rangle` in the Clifford fragment is uniquely identified by those tensors of Pauli operators that stabilize it (those :math:`P` such that :math:`P|\psi\rangle = |\psi\rangle`). These stabilizers form a group of size :math:`2^n` for an :math:`n` qubit state, but it is sufficient to identify :math:`n` independent generators to specify the group. If a Clifford gate :math:`C` is applied to the state, each generator :math:`P` can be updated to :math:`P' = CPC^\dagger` since :math:`C|\psi\rangle = CP|\psi\rangle = (CPC^\dagger)C|\psi\rangle`. We can therefore characterise each Clifford operation by its actions on generators of the Pauli group, giving us the Clifford tableau form. In ``pytket``, the :py:class:`~pytket.tableau.UnitaryTableau` class uses the binary symplectic representation from Aaronson and Gottesman [Aaro2004]_.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import OpType, Qubit
from pytket.tableau import UnitaryTableau
@@ -1396,12 +1544,14 @@ Any state :math:`|\psi\rangle` in the Clifford fragment is uniquely identified b
tab.apply_gate_at_end(OpType.S, [Qubit(0)])
tab.apply_gate_at_end(OpType.CX, [Qubit(1), Qubit(2)])
print(tab)
+```
-The way to interpret this format is that, for example, the top rows state that the unitary transforms :math:`X_0 I_1 I_2` at its input to :math:`-Y_0 I_1 I_2` at its output, and it transforms :math:`I_0 X_1 I_2` to :math:`I_0 X_1 X_2`.
+The way to interpret this format is that, for example, the top rows state that the unitary transforms $X_0 I_1 I_2$ at its input to $-Y_0 I_1 I_2$ at its output, and it transforms $I_0 X_1 I_2$ to $I_0 X_1 X_2$.
-The primary use for tableaux in ``pytket`` is as a scalable means of specifying a Clifford unitary for insertion into a circuit as a Box. This can then be decomposed into basic gates during compilation.
+The primary use for tableaux in `pytket` is as a scalable means of specifying a Clifford unitary for insertion into a circuit as a Box. This can then be decomposed into basic gates during compilation.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import Circuit
from pytket.tableau import UnitaryTableauBox
@@ -1420,10 +1570,12 @@ The primary use for tableaux in ``pytket`` is as a scalable means of specifying
draw(circ)
+```
After the tableau is added to a circuit, it can be readily decomposed to Clifford gates.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.passes import DecomposeBoxes, RemoveRedundancies
@@ -1431,14 +1583,15 @@ After the tableau is added to a circuit, it can be readily decomposed to Cliffor
RemoveRedundancies().apply(circ) # Eliminate some redundant gates
draw(circ)
+```
-.. note:: The current decomposition method for tableaux is not particularly efficient in terms of gate count, so consider using higher optimisation levels when compiling to help reduce the gate cost.
+:::{note}
+The current decomposition method for tableaux is not particularly efficient in terms of gate count, so consider using higher optimisation levels when compiling to help reduce the gate cost.
+:::
-The data structure used here for tableaux is intended for compilation use. For fast simulation of Clifford circuits, we recommend using the :py:class:`StimBackend` from ``pytket-stim``, the :py:class:`SimplexBackend` from ``pytket-pysimplex`` (optimized for large sparse circuits), or the :py:class:`~pytket.extensions.qiskit.AerBackend` from ``pytket-qiskit``. Future versions of ``pytket`` may include improved decompositions from tableaux, as well as more flexible tableaux to represent stabilizer states, isometries, and diagonalisation circuits.
-
-Classical and conditional operations
-====================================
+The data structure used here for tableaux is intended for compilation use. For fast simulation of Clifford circuits, we recommend using the {py:class}`StimBackend` from `pytket-stim`, the {py:class}`SimplexBackend` from `pytket-pysimplex` (optimized for large sparse circuits), or the {py:class}`~pytket.extensions.qiskit.AerBackend` from `pytket-qiskit`. Future versions of `pytket` may include improved decompositions from tableaux, as well as more flexible tableaux to represent stabilizer states, isometries, and diagonalisation circuits.
+### Classical and conditional operations
Moving beyond toy circuit examples, many applications of quantum computing
require looking at circuits as POVMs for extra expressivity, or introducing
@@ -1447,30 +1600,28 @@ performing measurements mid-circuit and then performing subsequent gates
conditional on the classical value of the measurement result, or on the results
of calculations on the results.
-
-Any ``pytket`` operation can be made conditional at the point of adding it to
-the :py:class:`~pytket.circuit.Circuit` by providing the ``condition`` kwarg. The interpretation
-of ``circ.G(q, condition=reg[0])`` is: "if the bit ``reg[0]`` is set to 1, then
-perform ``G(q)``".
-Conditions on more complicated expressions over the values of :py:class:`~pytket.unit_id.Bit` and :py:class:`~pytket.unit_id.BitRegister` are also
+Any `pytket` operation can be made conditional at the point of adding it to
+the {py:class}`~pytket.circuit.Circuit` by providing the `condition` kwarg. The interpretation
+of `circ.G(q, condition=reg[0])` is: "if the bit `reg[0]` is set to 1, then
+perform `G(q)`".
+Conditions on more complicated expressions over the values of {py:class}`~pytket.unit_id.Bit` and {py:class}`~pytket.unit_id.BitRegister` are also
possible, expressed as conditions on the results of expressions involving
bitwise AND (&), OR (|) and XOR (^) operations. In the case of registers, you
-can also express arithmetic operations: add (+), subtract (-), multiply (*),
-floor/integer division (//), left shift (<<) and right shift (>>).
+can also express arithmetic operations: add (+), subtract (-), multiply (\*),
+floor/integer division (//), left shift (\<\<) and right shift (>>).
For example a gate can be made conditional on the result of a bitwise XOR of
-registers ``a``, ``b``, and ``c`` being larger than 4 by writing ``circ.G(q,
-condition=reg_gt(a ^ b ^ c, 4))``.
+registers `a`, `b`, and `c` being larger than 4 by writing `circ.G(q,
+condition=reg_gt(a ^ b ^ c, 4))`.
When such a condition is added, the result of the expression is written to a
scratch bit or register, and the gate is made conditional on the value of the
scratch variable.
For comparison of registers, a special `RangePredicate` type is used to encode
the result of the comparison onto a scratch bit.
-See the :py:mod:`pytket.circuit.logic_exp` documentation for more on the
+See the {py:mod}`pytket.circuit.logic_exp` documentation for more on the
possible expressions and predicates.
-
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import (
Circuit,
@@ -1525,18 +1676,20 @@ possible expressions and predicates.
# compound register expressions
big_reg_exp = (reg_a & reg_b) | reg_c
circ.CX(qreg[3], qreg[4], condition=reg_eq(big_reg_exp, 3))
+```
So far we've looked at conditioning the application of a gate on bits,
registers, or expressions over those. We can also write some more standard
classical computations by assigning the result of some computation to output
bits or registers. We can also set the value or copy the contents of one resource
in to another. Note in the examples below to express something like ` =
-` we use circuit methods (like :py:meth:`~pytket.circuit.Circuit.add_c_setreg`, or
-:py:meth:`~pytket.circuit.Circuit.add_classicalexpbox_register`) that take `` as the first input and ``
+` we use circuit methods (like {py:meth}`~pytket.circuit.Circuit.add_c_setreg`, or
+{py:meth}`~pytket.circuit.Circuit.add_classicalexpbox_register`) that take `` as the first input and ``
as the second. Note that these classical operations can be conditional on other
classical operations, just like quantum operations.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import Circuit, reg_gt
@@ -1548,12 +1701,12 @@ classical operations, just like quantum operations.
# Write to classical registers
-
+
# a = 3
circ.add_c_setreg(3, reg_a)
# a[0] = 1
circ.add_c_setbits([1], [reg_a[0]])
- # Copy: b = a
+ # Copy: b = a
# b is smaller than a so the first 3 bits of a will be copied
circ.add_c_copyreg(reg_a, reg_b)
# b[1] = a[2]
@@ -1582,29 +1735,35 @@ classical operations, just like quantum operations.
# c = b >> 1
circ.add_classicalexpbox_register(reg_b >> 1, reg_c)
+```
-.. warning:: Unlike most uses of readouts in ``pytket``, register comparisons expect a little-endian value, e.g. in the above example ``condition=reg_eq(reg_a, 3)`` (representing the little-endian binary string ``110000...``) is triggered when ``reg_a[0]`` and ``reg_a[1]`` are in state ``1`` and the remainder of the register is in state ``0``.
+:::{warning}
+Unlike most uses of readouts in `pytket`, register comparisons expect a little-endian value, e.g. in the above example `condition=reg_eq(reg_a, 3)` (representing the little-endian binary string `110000...`) is triggered when `reg_a[0]` and `reg_a[1]` are in state `1` and the remainder of the register is in state `0`.
+:::
-.. note:: This feature is only usable on a limited selection of devices and simulators which support conditional gates or classical operations.
+:::{note}
+This feature is only usable on a limited selection of devices and simulators which support conditional gates or classical operations.
- The :py:class:`~pytket.extensions.qiskit.AerBackend` (from `pytket-qiskit `_) can support the OpenQasm model,
- where gates can only be conditional on an entire classical register being an
- exact integer value. Bitwise logical operations and register arithmetic are not supported.
- Therefore only conditions of the form
- ``condition=reg_eq(reg, val)`` are valid.
+The {py:class}`~pytket.extensions.qiskit.AerBackend` (from [pytket-qiskit](https://tket.quantinuum.com/extensions/pytket-qiskit/)) can support the OpenQasm model,
+where gates can only be conditional on an entire classical register being an
+exact integer value. Bitwise logical operations and register arithmetic are not supported.
+Therefore only conditions of the form
+`condition=reg_eq(reg, val)` are valid.
- The :py:class:`~pytket.extensions.quantinuum.QuantinuumBackend` (from `pytket-quantinuum `_)
- can support the full range of expressions and comparisons shown above.
+The {py:class}`~pytket.extensions.quantinuum.QuantinuumBackend` (from [pytket-quantinuum](https://tket.quantinuum.com/extensions/pytket-quantinuum/))
+can support the full range of expressions and comparisons shown above.
+:::
-Circuit-Level Operations
-========================
+### Circuit-Level Operations
-.. Produce a new circuit, related by some construction
-.. Dagger and transpose of unitary circuits
+% Produce a new circuit, related by some construction
-Systematic modifications to a :py:class:`~pytket.circuit.Circuit` object can go beyond simply adding gates one at a time. For example, given a unitary :py:class:`~pytket.circuit.Circuit`, we may wish to generate its inverse for the purposes of uncomputation of ancillae or creating conjugation circuits to diagonalise an operator as in the sample below.
+% Dagger and transpose of unitary circuits
-.. jupyter-execute::
+Systematic modifications to a {py:class}`~pytket.circuit.Circuit` object can go beyond simply adding gates one at a time. For example, given a unitary {py:class}`~pytket.circuit.Circuit`, we may wish to generate its inverse for the purposes of uncomputation of ancillae or creating conjugation circuits to diagonalise an operator as in the sample below.
+
+
+```{code-cell} ipython3
from pytket import Circuit
@@ -1620,22 +1779,26 @@ Systematic modifications to a :py:class:`~pytket.circuit.Circuit` object can go
circ.append(conj)
circ.Rx(0.6, 0).Rz(0.2, 1)
circ.append(conj_dag)
+```
+
+Generating the transpose of a unitary works similarly using {py:meth}`~pytket.circuit.Circuit.transpose`.
+
+:::{note}
+Since it is not possible to construct the inverse of an arbitrary POVM, the {py:meth}`~pytket.circuit.Circuit.dagger` and {py:meth}`~pytket.circuit.Circuit.transpose` methods will fail if there are any measurements, resets, or other operations that they cannot directly invert.
+:::
-Generating the transpose of a unitary works similarly using :py:meth:`~pytket.circuit.Circuit.transpose`.
+% Gradients wrt symbolic parameters
-.. note:: Since it is not possible to construct the inverse of an arbitrary POVM, the :py:meth:`~pytket.circuit.Circuit.dagger` and :py:meth:`~pytket.circuit.Circuit.transpose` methods will fail if there are any measurements, resets, or other operations that they cannot directly invert.
+### Implicit Qubit Permutations
-.. Gradients wrt symbolic parameters
+% DAG is used to help follow paths of resources and represent circuit up to trivial commutations
-Implicit Qubit Permutations
-===========================
+% SWAPs (and general permutations) can be treated as having the same effect as physically swapping the wires, so can be reduced to edges connecting predecessors and successors; makes it possible to spot more commutations and interacting gates for optimisations
-.. DAG is used to help follow paths of resources and represent circuit up to trivial commutations
-.. SWAPs (and general permutations) can be treated as having the same effect as physically swapping the wires, so can be reduced to edges connecting predecessors and successors; makes it possible to spot more commutations and interacting gates for optimisations
+The {py:class}`~pytket.circuit.Circuit` class is built as a DAG to help follow the paths of resources and represent the circuit canonically up to trivial commutations. Each of the edges represents a resource passing from one instruction to the next, so we could represent SWAPs (and general permutations) by connecting the predecessors of the SWAP instruction to the opposite successors. This eliminates the SWAP instruction from the graph (meaning we would no longer perform the operation at runtime) and could enable the compiler to spot additional opportunities for simplification. One example of this in practice is the ability to convert a pair of CXs in opposite directions to just a single CX (along with an implicit SWAP that isn't actually performed).
-The :py:class:`~pytket.circuit.Circuit` class is built as a DAG to help follow the paths of resources and represent the circuit canonically up to trivial commutations. Each of the edges represents a resource passing from one instruction to the next, so we could represent SWAPs (and general permutations) by connecting the predecessors of the SWAP instruction to the opposite successors. This eliminates the SWAP instruction from the graph (meaning we would no longer perform the operation at runtime) and could enable the compiler to spot additional opportunities for simplification. One example of this in practice is the ability to convert a pair of CXs in opposite directions to just a single CX (along with an implicit SWAP that isn't actually performed).
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import Circuit
from pytket.utils import Graph
@@ -1648,8 +1811,10 @@ The :py:class:`~pytket.circuit.Circuit` class is built as a DAG to help follow t
print(circ.get_commands())
Graph(circ).get_DAG()
+```
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.passes import CliffordSimp
@@ -1657,30 +1822,32 @@ The :py:class:`~pytket.circuit.Circuit` class is built as a DAG to help follow t
print(circ.get_commands())
print(circ.implicit_qubit_permutation())
Graph(circ).get_DAG()
+```
+
+% This encapsulates naturality of the symmetry in the resource theory, effectively shifting the swap to the end of the circuit
-.. This encapsulates naturality of the symmetry in the resource theory, effectively shifting the swap to the end of the circuit
+This procedure essentially exploits the naturality of the symmetry operator in the resource theory to push it to the end of the circuit: the `Rx` gate has moved from qubit `q[1]` to `q[0]` and can be commuted through to the start. This is automatically considered when composing two {py:class}`~pytket.circuit.Circuit` s together.
-This procedure essentially exploits the naturality of the symmetry operator in the resource theory to push it to the end of the circuit: the ``Rx`` gate has moved from qubit ``q[1]`` to ``q[0]`` and can be commuted through to the start. This is automatically considered when composing two :py:class:`~pytket.circuit.Circuit` s together.
+% Means that tracing the path from an input might reach an output labelled by a different resource
-.. Means that tracing the path from an input might reach an output labelled by a different resource
-.. Can inspect the implicit permutation at the end of the circuit
-.. Two circuits can have the same sequence of gates but different unitaries (and behave differently under composition) because of implicit permutations
+% Can inspect the implicit permutation at the end of the circuit
-The permutation has been reduced to something implicit in the graph, and we now find that tracing a path from an input can reach an output with a different :py:class:`UnitID`. Since this permutation is missing in the command sequence, simulating the circuit would only give the correct state up to a permutation of the qubits. This does not matter when running on real devices where the final quantum system is discarded after use, but is detectable when using a statevector simulator. This is handled automatically by ``pytket`` backends, but care should be taken when reading from the :py:class:`~pytket.circuit.Circuit` directly - two quantum :py:class:`~pytket.circuit.Circuit` s can have the same sequence of instructions but different unitaries because of implicit permutations. This permutation information is typically dropped when exporting to another software framework. The :py:meth:`~pytket.circuit.Circuit.implicit_qubit_permutation` method can be used to inspect such a permutation.
+% Two circuits can have the same sequence of gates but different unitaries (and behave differently under composition) because of implicit permutations
+The permutation has been reduced to something implicit in the graph, and we now find that tracing a path from an input can reach an output with a different {py:class}`UnitID`. Since this permutation is missing in the command sequence, simulating the circuit would only give the correct state up to a permutation of the qubits. This does not matter when running on real devices where the final quantum system is discarded after use, but is detectable when using a statevector simulator. This is handled automatically by `pytket` backends, but care should be taken when reading from the {py:class}`~pytket.circuit.Circuit` directly - two quantum {py:class}`~pytket.circuit.Circuit` s can have the same sequence of instructions but different unitaries because of implicit permutations. This permutation information is typically dropped when exporting to another software framework. The {py:meth}`~pytket.circuit.Circuit.implicit_qubit_permutation` method can be used to inspect such a permutation.
-Modifying Operations Within Circuits
-====================================
+### Modifying Operations Within Circuits
Symbolic parameters allow one to construct a circuit with some not-yet-assigned parameters, and later (perhaps after some optimization), to instantiate them with different values. Occasionally, however, one may desire more flexibility in substituting operations within a circuit. For example, one may wish to apply controls from a certain qubit to certain operations, or to insert or remove certain operations.
-This can be achieved with ``pytket``, provided the mutable operations are tagged during circuit construction with identifying names (which can be arbitrary strings). If two operations are given the same name then they belong to the same "operation group"; they can (and must) then be substituted simultaneously.
+This can be achieved with `pytket`, provided the mutable operations are tagged during circuit construction with identifying names (which can be arbitrary strings). If two operations are given the same name then they belong to the same "operation group"; they can (and must) then be substituted simultaneously.
Both primitive gates and boxes can be tagged and substituted in this way. The only constraint is that the signature (number and order of quantum and classical wires) of the substituted operation must match that of the original operation in the circuit. (It follows that all operations in the same group must have the same signature. An attempt to add an operation with an existing name with a mismatching signature will fail.)
-To add gates or boxes to a circuit with specified op group names, simply pass the name as a keyword argument ``opgroup`` to the method that adds the gate or box. To substitute all operations in a group, use the :py:meth:`~pytket.circuit.Circuit.substitute_named` method. This can be used to substitute a circuit, an operation or a box into the existing circuit.
+To add gates or boxes to a circuit with specified op group names, simply pass the name as a keyword argument `opgroup` to the method that adds the gate or box. To substitute all operations in a group, use the {py:meth}`~pytket.circuit.Circuit.substitute_named` method. This can be used to substitute a circuit, an operation or a box into the existing circuit.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import Circuit, CircBox
@@ -1695,8 +1862,10 @@ To add gates or boxes to a circuit with specified op group names, simply pass th
circ.CX(1, 2, opgroup="Fred")
draw(circ)
+```
+
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.circuit import Op
@@ -1713,14 +1882,16 @@ To add gates or boxes to a circuit with specified op group names, simply pass th
circ.substitute_named(newcbox, "Fred")
draw(circ)
+```
Note that when an operation or box is substituted in, the op group name is retained (and further substitutions can be made). When a circuit is substituted in, the op group name disappears.
To remove an operation, one can replace it with an empty circuit.
-To add a control to an operation, one can add the original operation as a :py:class:`~pytket.circuit.CircBox` with one unused qubit, and subtitute it with a :py:class:`~pytket.circuit.QControlBox`.
+To add a control to an operation, one can add the original operation as a {py:class}`~pytket.circuit.CircBox` with one unused qubit, and subtitute it with a {py:class}`~pytket.circuit.QControlBox`.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.circuit import QControlBox
@@ -1749,11 +1920,18 @@ To add a control to an operation, one can add the original operation as a :py:cl
c.substitute_named(cx_q_qbox, "cxgroup")
draw(c)
+```
+
+[^cite_cowt2020]: Cowtan, A. and Dilkes, S. and Duncan and R., Simmons, W and Sivarajah, S., 2020. Phase Gadget Synthesis for Shallow Circuits. Electronic Proceedings in Theoretical Computer Science
+
+[^cite_shen2004]: V.V. Shende and S.S. Bullock and I.L. Markov, 2004. Synthesis of quantum-logic circuits. \{IEEE} Transactions on Computer-Aided Design of Integrated Circuits and Systems
+
+[^cite_aaro2004]: Aaronson, S. and Gottesman, D., 2004. Improved Simulation of Stabilizer Circuits. Physical Review A, 70(5), p.052328.
+
+[^cite_brav2005]: Bravyi, S. and Kitaev, A., 2005. Universal quantum computation with ideal Clifford gates and noisy ancillas. Physical Review A, 71(2), p.022316.
+
+[^cite_brav2012]: Bravyi, S. and Haah, J., 2012. Magic-state distillation with low overhead. Physical Review A, 86(5), p.052329.
+
+[^cite_amy2014]: Amy, M., Maslov, D. and Mosca, M., 2014. Polynomial-time T-depth optimization of Clifford+ T circuits via matroid partitioning. IEEE Transactions on Computer-Aided Design of Integrated Circuits and Systems, 33(10), pp.1476-1489.
-.. [Cowt2020] Cowtan, A. and Dilkes, S. and Duncan and R., Simmons, W and Sivarajah, S., 2020. Phase Gadget Synthesis for Shallow Circuits. Electronic Proceedings in Theoretical Computer Science
-.. [Shen2004] V.V. Shende and S.S. Bullock and I.L. Markov, 2004. Synthesis of quantum-logic circuits. {IEEE} Transactions on Computer-Aided Design of Integrated Circuits and Systems
-.. [Aaro2004] Aaronson, S. and Gottesman, D., 2004. Improved Simulation of Stabilizer Circuits. Physical Review A, 70(5), p.052328.
-.. [Brav2005] Bravyi, S. and Kitaev, A., 2005. Universal quantum computation with ideal Clifford gates and noisy ancillas. Physical Review A, 71(2), p.022316.
-.. [Brav2012] Bravyi, S. and Haah, J., 2012. Magic-state distillation with low overhead. Physical Review A, 86(5), p.052329.
-.. [Amy2014] Amy, M., Maslov, D. and Mosca, M., 2014. Polynomial-time T-depth optimization of Clifford+ T circuits via matroid partitioning. IEEE Transactions on Computer-Aided Design of Integrated Circuits and Systems, 33(10), pp.1476-1489.
-.. [Meij2020] de Griend, A.M.V. and Duncan, R., 2020. Architecture-aware synthesis of phase polynomials for NISQ devices. arXiv preprint arXiv:2004.06052.
+[^cite_meij2020]: de Griend, A.M.V. and Duncan, R., 2020. Architecture-aware synthesis of phase polynomials for NISQ devices. arXiv preprint arXiv:2004.06052.
diff --git a/docs/manual/manual_compiler.md b/docs/manual/manual_compiler.md
new file mode 100644
index 00000000..3022d964
--- /dev/null
+++ b/docs/manual/manual_compiler.md
@@ -0,0 +1,1266 @@
+---
+file_format: mystnb
+---
+
+# Compilation
+
+So far, we have already covered enough to be able to design the {py:class}`~pytket.circuit.Circuit` s we want to run, submit them to a {py:class}`~pytket.backends.Backend`, and interpret the results in a meaningful way. This is all you need if you want to just try out a quantum computer, run some toy examples and observe some basic results. We actually glossed over a key step in this process by using the {py:meth}`~pytket.backends.Backend.get_compiled_circuit()` method. The compilation step maps from the universal computer abstraction presented at {py:class}`~pytket.circuit.Circuit` construction to the restricted fragment supported by the target {py:class}`~pytket.backends.Backend`, and knowing what a compiler can do to your program can help reduce the burden of design and improve performance on real devices.
+
+The necessity of compilation maps over from the world of classical computation: it is much easier to design correct programs when working with higher-level constructions that aren't natively supported, and it shouldn't require a programmer to be an expert in the exact device architecture to achieve good performance. There are many possible low-level implementations on the device for each high-level program, which vary in the time and resources taken to execute. However, because QPUs are analog devices, the implementation can have a massive impact on the quality of the final outcomes as a result of changing how susceptible the system is to noise. Using a good compiler and choosing the methods appropriately can automatically find a better low-level implementation. Each aspect of the compilation procedure is exposed through `pytket` to provide users with a way to have full control over what is applied and how.
+
+% Optimisation/simplification and constraint solving
+
+The primary goals of compilation are two-fold: solving the constraints of the {py:class}`~pytket.backends.Backend` to get from the abstract model to something runnable, and optimising/simplifying the {py:class}`~pytket.circuit.Circuit` to make it faster, smaller, and less prone to noise. Every step in compilation can generally be split up into one of these two categories (though even the constraint solving steps could have multiple solutions over which we could optimise for noise).
+
+% Passes capture methods of transforming the circuit, acting in place
+
+Each compiler pass inherits from the {py:class}`~pytket.passes.BasePass` class, capturing a method of transforming a {py:class}`~pytket.circuit.Circuit`. The main functionality is built into the {py:meth}`BasePass.apply()` method, which applies the transformation to a {py:class}`~pytket.circuit.Circuit` in-place. The {py:meth}`~pytket.backends.Backend.get_compiled_circuit()` method is a wrapper around the {py:meth}`~pytket.passes.BasePass.apply()` from the {py:class}`~pytket.backends.Backend` 's recommended pass sequence. This chapter will explore these compiler passes, the different kinds of constraints they are used to solve and optimisations they apply, to help you identify which ones are appropriate for a given task.
+
+## Compilation Predicates
+
+% Predicates capture properties a circuit could satisfy
+
+% Primarily used to describe requirements of the backends
+
+Solving the constraints of the target {py:class}`~pytket.backends.Backend` is the essential goal of compilation, so our choice of passes is mostly driven by this set of constraints. We already saw in the last chapter that the {py:attr}`~pytket.backends.Backend.required_predicates` property gives a collection of {py:class}`~pytket.predicates.Predicate` s, describing the necessary properties a {py:class}`~pytket.circuit.Circuit` must satisfy in order to be run.
+
+Each {py:class}`~pytket.predicates.Predicate` can be constructed on its own to impose tests on {py:class}`~pytket.circuit.Circuit` s during construction.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+ from pytket.predicates import GateSetPredicate, NoMidMeasurePredicate
+
+ circ = Circuit(2, 2)
+ circ.Rx(0.2, 0).CX(0, 1).Rz(-0.7, 1).measure_all()
+
+ gateset = GateSetPredicate({OpType.Rx, OpType.CX, OpType.Rz, OpType.Measure})
+ midmeasure = NoMidMeasurePredicate()
+
+ print(gateset.verify(circ))
+ print(midmeasure.verify(circ))
+
+ circ.S(0)
+
+ print(gateset.verify(circ))
+ print(midmeasure.verify(circ))
+```
+
+% Common predicates
+
+| Common {py:class}`~pytket.predicates.Predicate` | Constraint |
+| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| {py:class}`~pytket.predicates.GateSetPredicate` | Every gate is within a set of allowed {py:class}`~pytket.circuit.OpType` s |
+| {py:class}`~pytket.predicates.ConnectivityPredicate` | Every multi-qubit gate acts on adjacent qubits according to some connectivity graph |
+| {py:class}`~pytket.predicates.DirectednessPredicate` | Extends {py:class}`~pytket.predicates.ConnectivityPredicate` where `OpType.CX` gates are only supported in a specific orientation between adjacent qubits |
+| {py:class}`~pytket.predicates.NoClassicalControlPredicate` | The {py:class}`~pytket.circuit.Circuit` does not contain any gates that act conditionally on classical data |
+| {py:class}`~pytket.predicates.NoMidMeasurePredicate` | All `OpType.Measure` gates act at the end of the {py:class}`~pytket.circuit.Circuit` (there are no subsequent gates on either the {py:class}`~pytket.unit_id.Qubit` measured or the {py:class}`~pytket.unit_id.Bit` written to) |
+
+% Pre/post-conditions of passes
+
+When applying passes, you may find that you apply some constraint-solving pass to satisfy a particular {py:class}`~pytket.predicates.Predicate`, but then a subsequent pass will invalidate it by, for example, introducing gates of different gate types or changing which qubits interact via multi-qubit gates. To help understand and manage this, each pass has a set of pre-conditions that specify the requirements assumed on the {py:class}`~pytket.circuit.Circuit` in order for the pass to successfully be applied, and a set of post-conditions that specify which {py:class}`~pytket.predicates.Predicate` s are guaranteed to hold for the outputs and which are invalidated or preserved by the pass. These can be viewed in the API reference for each pass.
+
+Users may find it desirable to enforce their own constraints upon circuits they are working with. It is possible to construct a {py:class}`UserDefinedPredicate` in pytket based on a function that returns a True/False value.
+
+Below is a minimal example where we construct a predicate which checks if our {py:class}`~pytket.circuit.Circuit` contains fewer than 3 CX gates.
+
+
+```{code-cell} ipython3
+
+ from pytket.circuit import Circuit, OpType
+ from pytket.predicates import UserDefinedPredicate
+
+ def max_cx_count(circ: Circuit) -> bool:
+ return circ.n_gates_of_type(OpType.CX) < 3
+
+ # Now construct our predicate using the function defined above
+ my_predicate = UserDefinedPredicate(max_cx_count)
+
+ test_circ = Circuit(2).CX(0, 1).Rz(0.25, 1).CX(0, 1) # Define a test Circuit
+
+ my_predicate.verify(test_circ)
+ # test_circ satisfies predicate as it contains only 2 CX gates
+```
+
+## Rebases
+
+% Description
+
+One of the simplest constraints to solve for is the {py:class}`~pytket.predicates.GateSetPredicate`, since we can just substitute each gate in a {py:class}`~pytket.circuit.Circuit` with an equivalent sequence of gates in the target gateset according to some known gate decompositions. In `pytket`, such passes are referred to as "rebases". The intention here is to perform this translation naively, leaving the optimisation of gate sequences to other passes. Rebases can be applied to any {py:class}`~pytket.circuit.Circuit` and will preserve every structural {py:class}`~pytket.predicates.Predicate`, only changing the types of gates used.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.passes import RebaseTket
+
+ circ = Circuit(2, 2)
+ circ.Rx(0.3, 0).Ry(-0.9, 1).CZ(0, 1).S(0).CX(1, 0).measure_all()
+
+ RebaseTket().apply(circ)
+
+ print(circ.get_commands())
+```
+
+{py:class}`~pytket.passes.RebaseTket` is a standard rebase pass that converts to CX and TK1 gates. This is the preferred internal gateset for many `pytket` compiler passes. However, it is possible to define a rebase for an arbitrary gateset. Using {py:class}`RebaseCustom`, we can provide an arbitrary set of one- and two-qubit gates. Rather than requiring custom decompositions to be provided for every gate type, it is sufficient to just give them for `OpType.CX` and `OpType.TK1`. For any gate in a given {py:class}`~pytket.circuit.Circuit`, it is either already in the target gateset, or we can use known decompositions to obtain a `OpType.CX` and `OpType.TK1` representation and then map this to the target gateset.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+ from pytket.passes import RebaseCustom
+
+ gates = {OpType.Rz, OpType.Ry, OpType.CY, OpType.ZZPhase}
+ cx_in_cy = Circuit(2)
+ cx_in_cy.Rz(0.5, 1).CY(0, 1).Rz(-0.5, 1)
+
+ def tk1_to_rzry(a, b, c):
+ circ = Circuit(1)
+ circ.Rz(c + 0.5, 0).Ry(b, 0).Rz(a - 0.5, 0)
+ return circ
+
+ custom = RebaseCustom(gates, cx_in_cy, tk1_to_rzry)
+
+ circ = Circuit(3)
+ circ.X(0).CX(0, 1).Ry(0.2, 1)
+ circ.ZZPhase(-0.83, 2, 1).Rx(0.6, 2)
+
+ custom.apply(circ)
+
+ print(circ.get_commands())
+```
+
+For some gatesets, it is not even necessary to specify the CX and TK1 decompositions: there is a useful function {py:meth}`auto_rebase_pass` which can take care of this for you. The pass returned is constructed from the gateset alone. (It relies on some known decompositions, and will raise an exception if no suitable known decompositions exist.) An example is given in the "Combinators" section below.
+
+A similar pair of methods, {py:meth}`SquashCustom` and {py:meth}`auto_squash_pass`, may be used to construct a pass that squashes sequences of single-qubit gates from a given set of single-qubit gates to as short a sequence as possible. Both take a gateset as an argument. {py:meth}`SquashCustom` also takes a function for converting the parameters of a TK1 gate to the target gate set. (Internally, the compiler squashes all gates to TK1 and then applies the supplied function.) {py:meth}`auto_squash_pass` attempts to do the squash using known internal decompositions (but may fail for some gatesets). For example:
+
+
+```{code-cell} ipython3
+
+ from pytket.circuit import Circuit, OpType
+ from pytket.passes import auto_squash_pass
+
+ gates = {OpType.PhasedX, OpType.Rz, OpType.Rx, OpType.Ry}
+ custom = auto_squash_pass(gates)
+
+ circ = Circuit(1).H(0).Ry(0.5, 0).Rx(-0.5, 0).Rz(1.5, 0).Ry(0.5, 0).H(0)
+ custom.apply(circ)
+ print(circ.get_commands())
+```
+
+Note that the H gates (which are not in the specified gateset) are left alone.
+
+(compiler-placement)=
+
+## Placement
+
+% Task of selecting appropriate physical qubits to use; better use of connectivity and better noise characteristics
+
+Initially, a {py:class}`~pytket.circuit.Circuit` designed without a target device in mind will be expressed in terms of actions on a set of "logical qubits" - those with semantic meaning to the computation. A `placement` (or `initial mapping`) is a map from these logical qubits to the physical qubits of the device that will be used to carry them. A given placement may be preferred over another if the connectivity of the physical qubits better matches the interactions between the logical qubits caused by multi-qubit gates, or if the selection of physical qubits has better noise characteristics. All of the information for connectivity and noise characteristics of a given {py:class}`~pytket.backends.Backend` is wrapped up in a {py:class}`~pytket.backends.backendinfo.BackendInfo` object by the {py:attr}`Backend.backend_info` property.
+
+% Affects where the logical qubits start initially, but it not necessarily where they will end up being measured at the end
+
+The placement only specifies where the logical qubits will be at the start of execution, which is not necessarily where they will end up on termination. Other compiler passes may choose to permute the qubits in the middle of a {py:class}`~pytket.circuit.Circuit` to either exploit further optimisations or enable interactions between logical qubits that were not assigned to adjacent physical qubits.
+
+% Placement acts in place by renaming qubits to their physical addresses (classical data is never renamed)
+
+A placement pass will act in place on a {py:class}`~pytket.circuit.Circuit` by renaming the qubits from their logical names (the {py:class}`UnitID` s used at circuit construction) to their physical addresses (the {py:class}`UnitID` s recognised by the {py:class}`~pytket.backends.Backend`). Classical data is never renamed.
+
+% Basic example
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.passes import PlacementPass
+from pytket.predicates import ConnectivityPredicate
+from pytket.placement import GraphPlacement
+
+circ = Circuit(4, 4)
+circ.H(0).H(1).H(2).V(3)
+circ.CX(0, 1).CX(1, 2).CX(2, 3)
+circ.Rz(-0.37, 3)
+circ.CX(2, 3).CX(1, 2).CX(0, 1)
+circ.H(0).H(1).H(2).Vdg(3)
+circ.measure_all()
+
+backend = IBMQBackend("ibmq_quito")
+place = PlacementPass(GraphPlacement(backend.backend_info.architecture))
+place.apply(circ)
+
+print(circ.get_commands())
+print(ConnectivityPredicate(backend.backend_info.architecture).verify(circ))
+```
+
+
+```
+[H node[0];, H node[1];, H node[3];, V node[4];, CX node[0], node[1];, CX node[1], node[3];, CX node[3], node[4];, Rz(3.63*PI) node[4];, CX node[3], node[4];, CX node[1], node[3];, Vdg node[4];, Measure node[4] --> c[3];, CX node[0], node[1];, H node[3];, Measure node[3] --> c[2];, H node[0];, H node[1];, Measure node[0] --> c[0];, Measure node[1] --> c[1];]
+True
+```
+
+In this example, the placement was able to find an exact match for the connectivity onto the device.
+
+% Sometimes best location is not determined and left to later compilation, leaving partial placement; indicated by "unplaced" register
+
+In some circumstances, the best location is not fully determined immediately and is deferred until later in compilation. This gives rise to a partial placement (the map from logical qubits to physical qubits is a partial function, where undefined qubits are renamed into an `unplaced` register).
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.passes import PlacementPass
+from pytket.placement import LinePlacement
+
+circ = Circuit(4)
+circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
+
+backend = IBMQBackend("ibmq_quito")
+place = PlacementPass(LinePlacement(backend.backend_info.architecture))
+place.apply(circ)
+
+print(circ.get_commands())
+```
+
+
+```
+[CX node[2], node[1];, CX node[2], node[3];, CX node[1], node[3];, CX unplaced[0], node[3];, CX node[2], unplaced[0];]
+```
+
+% Define custom placement by providing qubit map
+
+A custom (partial) placement can be applied by providing the appropriate qubit map.
+
+
+```{code-cell} ipython3
+
+from pytket.circuit import Circuit, Qubit, Node
+from pytket.placement import Placements
+
+circ = Circuit(4)
+circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
+
+q_map = {Qubit(0) : Node(3), Qubit(2) : Node(1)}
+Placement.place_with_map(circ, q_map)
+
+print(circ.get_commands())
+```
+
+A custom placement may also be defined as a pass (which can then be combined with others to construct a more complex pass).
+
+
+```{code-cell} ipython3
+
+from pytket.circuit import Circuit, Qubit, Node
+from pytket.passes import RenameQubitsPass
+
+circ = Circuit(4)
+circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
+
+q_map = {Qubit(0) : Qubit("z", 0), Qubit(2) : Qubit("z", 1)}
+rename = RenameQubitsPass(q_map)
+rename.apply(circ)
+
+print(circ.get_commands())
+```
+
+% Existing heuristics: trivial (all "unplaced"), line, graph, noise
+
+Several heuristics have been implemented for identifying candidate placements. For example, {py:class}`~pytket.placement.LinePlacement` will try to identify long paths on the connectivity graph which could be treated as a linear nearest-neighbour system. {py:class}`~pytket.placement.GraphPlacement` will try to identify a subgraph isomorphism between the graph of interacting logical qubits (up to some depth into the {py:class}`~pytket.circuit.Circuit`) and the connectivity graph of the physical qubits. Then {py:class}`~pytket.placement.NoiseAwarePlacement` extends this to break ties in equivalently good graph maps by looking at the error rates of the physical qubits and their couplers. The latter two can be configured using e.g. {py:meth}`~pytket.utils.GraphPlacement.modify_config()` to change parameters like how far into the {py:class}`~pytket.circuit.Circuit` it will look for interacting qubits (trading off time spent searching for the chance to find a better placement).
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.passes import PlacementPass
+from pytket.predicates import ConnectivityPredicate
+from pytket.placement import GraphPlacement
+
+circ = Circuit(5)
+circ.CX(0, 1).CX(1, 2).CX(3, 4)
+circ.CX(0, 1).CX(1, 2).CX(3, 4)
+circ.CX(0, 1).CX(1, 2).CX(3, 4)
+circ.CX(0, 1).CX(1, 2).CX(3, 4)
+circ.CX(0, 1).CX(1, 2).CX(3, 4)
+circ.CX(1, 4) # Extra interaction hidden at higher depth than cutoff
+
+backend = IBMQBackend("ibmq_quito")
+g_pl = GraphPlacement(backend.backend_info.architecture)
+connected = ConnectivityPredicate(backend.backend_info.architecture)
+
+PlacementPass(g_pl).apply(circ)
+print(connected.verify(circ)) # Imperfect placement because the final CX was not considered
+
+# Default depth limit is 5, but there is a new interaction at depth 11
+g_pl.modify_config(depth_limit=11)
+
+PlacementPass(g_pl).apply(circ)
+print(connected.verify(circ)) # Now have an exact placement
+```
+
+
+```
+
+False
+True
+```
+
+
+## Mapping
+
+% Heterogeneous architectures and limited connectivity
+
+% Far easier to program correctly when assuming full connectivity
+
+The heterogeneity of quantum architectures and limited connectivity of their qubits impose the strict restriction that multi-qubit gates are only allowed between specific pairs of qubits. Given it is far easier to program a high-level operation which is semantically correct and meaningful when assuming full connectivity, a compiler will have to solve this constraint. In general, there won't be an exact subgraph isomorphism between the graph of interacting logical qubits and the connected physical qubits, so this cannot be solved with placement alone.
+
+% Invalid interactions between non-local qubits can be sovled by moving qubits to adjacent positions or by performing a distributed operation using the intervening qubits
+
+% Routing takes a placed circuit and finds non-local operations, inserting operations to fix them
+
+One solution here, is to scan through the {py:class}`~pytket.circuit.Circuit` looking for invalid interactions. Each of these can be solved by either moving the qubits around on the architecture by adding `OpType.SWAP` gates until they are in adjacent locations, or performing a distributed entangling operation using the intervening qubits (such as the "bridged-CX" `OpType.BRIDGE` which uses 4 CX gates and a single shared neighbour). The `routing` procedure used in the `pytket` `RoutingPass` takes a placed {py:class}`~pytket.circuit.Circuit` and inserts gates to reduce non-local operations to sequences of valid local ones.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.passes import PlacementPass, RoutingPass
+from pytket.placement import GraphPlacement
+
+circ = Circuit(4)
+circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
+backend = IBMQBackend("ibmq_quito")
+PlacementPass(GraphPlacement(backend.backend_info.architecture)).apply(circ)
+print(circ.get_commands()) # One qubit still unplaced
+ # node[0] and node[2] are not adjacent
+
+RoutingPass(backend.backend_info.architecture).apply(circ)
+print(circ.get_commands())
+```
+
+
+```
+[CX node[1], node[0];, CX node[1], node[2];, CX node[0], node[2];, CX unplaced[0], node[2];, CX node[1], unplaced[0];]
+[CX node[1], node[0];, CX node[1], node[2];, SWAP node[0], node[1];, CX node[1], node[2];, SWAP node[1], node[3];, CX node[1], node[2];, CX node[0], node[1];]
+```
+
+% Given partial placements, selects physical qubits on the fly
+
+% Due to swap insertion, logical qubits may be mapped to different physical qubits at the start and end of the circuit
+
+As shown here, if a partial placement is used, the routing procedure will allocate the remaining qubits dynamically. We also see that the logical qubits are mapped to different physical qubits at the start and end because of the inserted `OpType.SWAP` gates, such as `q[1]` starting at `node[0]` and ending at `node[3]`.
+
+% Other Routing options
+
+`RoutingPass` only provides the default option for mapping to physical circuits. The decision making used in Routing is defined in the `RoutgMethod` class and the choice of `RoutingMethod` used can be defined in the `FullMappingPass` compiler pass for producing physical circuits.
+
+## Decomposing Structures
+
+% Box structures for high-level operations need to be mapped to low-level gates
+
+% Unwraps `CircuitBox`es, decomposes others into known, efficient patterns
+
+The numerous Box structures in `pytket` provide practical abstractions for high-level operations to assist in {py:class}`~pytket.circuit.Circuit` construction, but need to be mapped to low-level gates before we can run the {py:class}`~pytket.circuit.Circuit`. The {py:class}`~pytket.passes.DecomposeBoxes` pass will unwrap any {py:class}`~pytket.circuit.CircBox`, substituting it for the corresponding {py:class}`~pytket.circuit.Circuit`, and decompose others like the {py:class}`~pytket.circuit.Unitary1qBox` and {py:class}`~pytket.circuit.PauliExpBox` into efficient templated patterns of gates.
+
+
+```{code-cell} ipython3
+
+from pytket.circuit import Circuit, CircBox, PauliExpBox
+from pytket.passes import DecomposeBoxes
+from pytket.pauli import Pauli
+
+sub = Circuit(2)
+sub.CZ(0, 1).T(0).Tdg(1)
+sub_box = CircBox(sub)
+circ = Circuit(4)
+circ.Rx(0.42, 2).CX(2, 0)
+circ.add_gate(sub_box, [0, 1])
+circ.add_gate(sub_box, [2, 3])
+circ.add_gate(PauliExpBox([Pauli.X, Pauli.Y, Pauli.Y, Pauli.Y], 0.2), [0, 1, 2, 3])
+
+DecomposeBoxes().apply(circ)
+print(circ.get_commands())
+```
+
+% This could introduce undetermined structures to the circuit, invalidating gate set, connectivity, and other crucial requirements of the backend, so recommended to be performed early in the compilation procedure, allowing for these requirements to be solved again
+
+Unwrapping Boxes could introduce arbitrarily complex structures into a {py:class}`~pytket.circuit.Circuit` which could possibly invalidate almost all {py:class}`~pytket.predicates.Predicate` s, including {py:class}`GateSetPredicate`, {py:class}`~pytket.predicates.ConnectivityPredicate`, and {py:class}`NoMidMeasurePredicate`. It is hence recommended to apply this early in the compilation procedure, prior to any pass that solves for these constraints.
+
+## Optimisations
+
+Having covered the primary goal of compilation and reduced our {py:class}`~pytket.circuit.Circuit` s to a form where they can be run, we find that there are additional techniques we can use to obtain more reliable results by reducing the noise and probability of error. Most {py:class}`~pytket.circuit.Circuit` optimisations follow the mantra of "fewer expensive resources gives less opportunity for noise to creep in", whereby if we find an alternative {py:class}`~pytket.circuit.Circuit` that is observationally equivalent in a perfect noiseless setting but uses fewer resources (gates, time, ancilla qubits) then it is likely to perform better in a noisy context (though not always guaranteed).
+
+% Generic peephole - "looking for specific patterns of gates"; may take into account local commutations
+
+% Examples describing `RemoveRedundancies`, `EulerAngleReduction`, `KAKDecomposition`, and `CliffordSimp`
+
+If we have two {py:class}`~pytket.circuit.Circuit` s that are observationally equivalent, we know that replacing one for the other in any context also gives something that is observationally equivalent. The simplest optimisations will take an inefficient pattern, find all matches in the given {py:class}`~pytket.circuit.Circuit` and replace them by the efficient alternative. A good example from this class of `peephole` optimisations is the {py:class}`~pytket.passes.RemoveRedundancies` pass, which looks for a number of easy-to-spot redundant gates, such as zero-parameter rotation gates, gate-inverse pairs, adjacent rotation gates in the same basis, and diagonal rotation gates followed by measurements.
+
+
+```{code-cell} ipython3
+
+from pytket import Circuit, OpType
+from pytket.passes import RemoveRedundancies
+
+circ = Circuit(3, 3)
+circ.Rx(0.92, 0).CX(1, 2).Rx(-0.18, 0) # Adjacent Rx gates can be merged
+circ.CZ(0, 1).Ry(0.11, 2).CZ(0, 1) # CZ is self-inverse
+circ.XXPhase(0.6, 0, 1)
+circ.YYPhase(0, 0, 1) # 0-angle rotation does nothing
+circ.ZZPhase(-0.84, 0, 1)
+circ.Rx(0.03, 0).Rz(-0.9, 1).measure_all() # Effect of Rz is eliminated by measurement
+
+RemoveRedundancies().apply(circ)
+print(circ.get_commands())
+```
+
+It is understandable to question the relevance of such an optimisation, since a sensible programmer would not intentionally write a {py:class}`~pytket.circuit.Circuit` with such redundant gates. These are still largely useful because other compiler passes might introduce them, such as routing adding a `OpType.SWAP` gate immediately following a `OpType.SWAP` gate made by the user, or commuting a Z-rotation through the control of a CX which allows it to merge with another Z-rotation on the other side.
+
+Previous iterations of the {py:class}`~pytket.passes.CliffordSimp` pass would work in this way as well, looking for specific sequences of Clifford gates where we could reduce the number of two-qubit gates. This has since been generalised to spot these patterns up to gate commutations and changes of basis from single-qubit Clifford rotations.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+ from pytket.passes import CliffordSimp
+
+ # A basic inefficient pattern can be reduced by 1 CX
+ simple_circ = Circuit(2)
+ simple_circ.CX(0, 1).S(1).CX(1, 0)
+
+ CliffordSimp().apply(simple_circ)
+ print(simple_circ.get_commands())
+
+ # The same pattern, up to commutation and local Clifford algebra
+ complex_circ = Circuit(3)
+ complex_circ.CX(0, 1)
+ complex_circ.Rx(0.42, 1)
+ complex_circ.S(1)
+ complex_circ.YYPhase(0.96, 1, 2) # Requires 2 CXs to implement
+ complex_circ.CX(0, 1)
+
+ CliffordSimp().apply(complex_circ)
+ print(complex_circ.get_commands())
+```
+
+The next step up in scale has optimisations based on optimal decompositions of subcircuits over $n$-qubits, including {py:class}`~pytket.passes.EulerAngleReduction` for single-qubit unitary chains (producing three rotations in a choice of axes), and {py:class}`~pytket.passes.KAKDecomposition` for two-qubit unitaries (using at most three CXs and some single-qubit gates).
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+ from pytket.passes import EulerAngleReduction, KAKDecomposition
+
+ circ = Circuit(2)
+ circ.CZ(0, 1)
+ circ.Rx(0.4, 0).Ry(0.289, 0).Rx(-0.34, 0).Ry(0.12, 0).Rx(-0.81, 0)
+ circ.CX(1, 0)
+
+ # Reduce long chain to a triple of Ry, Rx, Ry
+ EulerAngleReduction(OpType.Rx, OpType.Ry).apply(circ)
+ print(circ.get_commands())
+
+ circ = Circuit(3)
+ circ.CX(0, 1)
+ circ.CX(1, 2).Rx(0.3, 1).CX(1, 2).Rz(1.5, 2).CX(1, 2).Ry(-0.94, 1).Ry(0.37, 2).CX(1, 2)
+ circ.CX(1, 0)
+
+ # Reduce long 2-qubit subcircuit to at most 3 CXs
+ KAKDecomposition().apply(circ)
+ print(circ.get_commands())
+```
+
+% Situational macroscopic - identifies large structures in circuit or converts circuit to alternative algebraic representation; use properties of the structures to find simplifications; resynthesise into basic gates
+
+% Examples describing `PauliSimp`
+
+All of these so far are generic optimisations that work for any application, but only identify local redundancies since they are limited to working up to individual gate commutations. Other techniques instead focus on identifying macroscopic structures in a {py:class}`~pytket.circuit.Circuit` or convert it entirely into an alternative algebraic representation, and then using the properties of the structures/algebra to find simplifications and resynthesise into basic gates. For example, the {py:class}`~pytket.passes.PauliSimp` pass will represent the entire {py:class}`~pytket.circuit.Circuit` as a sequence of exponentials of Pauli operators, capturing the effects of non-Clifford gates as rotations in a basis determined by the Clifford gates. This abstracts away any redundant information in the Clifford gates entirely, and can be used to merge non-Clifford gates that cannot be brought together from any sequence of commutations, as well as finding efficient Clifford constructions for the basis changes.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.passes import PauliSimp
+ from pytket.utils import Graph
+
+ circ = Circuit(3)
+ circ.Rz(0.2, 0)
+ circ.Rx(0.35, 1)
+ circ.V(0).H(1).CX(0, 1).CX(1, 2).Rz(-0.6, 2).CX(1, 2).CX(0, 1).Vdg(0).H(1)
+ circ.H(1).H(2).CX(0, 1).CX(1, 2).Rz(0.8, 2).CX(1, 2).CX(0, 1).H(1).H(2)
+ circ.Rx(0.1, 1)
+
+ PauliSimp().apply(circ)
+ Graph(circ).get_DAG()
+```
+
+% May not always improve the circuit if it doesn't match the structures it was designed to exploit, and the large structural changes from resynthesis could make routing harder
+
+This can give great benefits for {py:class}`~pytket.circuit.Circuit` s where non-Clifford gates are sparse and there is hence a lot of redundancy in the Clifford change-of-basis sections. But if the {py:class}`~pytket.circuit.Circuit` already has a very efficient usage of Clifford gates, this will be lost when converting to the abstract representation, and so the resynthesis is likely to give less efficient sequences. The large structural changes from abstraction and resynthesis can also make routing harder to perform as the interaction graph of the logical qubits can drastically change. The effectiveness of such optimisations depends on the situation, but can be transformative under the right circumstances.
+
+Some of these optimisation passes have optional parameters to customise the routine slightly. A good example is adapting the {py:class}`~pytket.passes.PauliSimp` pass to have a preference for different forms of `OpType.CX` decompositions. Setting the `cx_config` option to `CXConfigType.Snake` (default) will prefer chains of gates where the target of one becomes the control of the next, whereas `CXConfigType.Star` prefers using a single qubit as the control for many gates, and `CXConfigType.Tree` introduces entanglement in a balanced tree form. Each of these has its own benefits and drawbacks that could make it more effective for a particular routine, like `CXConfigType.Snake` giving circuits that are easier to route on linear nearest-neighbour architectures, `CXConfigType.Star` allowing any of the gates to commute through to cancel out with others at the start or end of the sequence, and `CXConfigType.Tree` giving optimal depth on a fully-connected device.
+
+
+```{code-cell} ipython3
+
+ from pytket.circuit import Circuit, PauliExpBox
+ from pytket.passes import PauliSimp
+ from pytket.pauli import Pauli
+ from pytket.transform import CXConfigType
+ from pytket.utils import Graph
+
+ pauli_XYXZYXZZ = PauliExpBox([Pauli.X, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Z], 0.42)
+
+ circ = Circuit(8)
+ circ.add_gate(pauli_XYXZYXZZ, [0, 1, 2, 3, 4, 5, 6, 7])
+
+ PauliSimp(cx_config=CXConfigType.Snake).apply(circ)
+ print(circ.get_commands())
+ Graph(circ).get_qubit_graph()
+```
+
+
+```{code-cell} ipython3
+
+ PauliSimp(cx_config=CXConfigType.Star).apply(circ)
+ print(circ.get_commands())
+ Graph(circ).get_qubit_graph()
+```
+
+
+```{code-cell} ipython3
+
+ PauliSimp(cx_config=CXConfigType.Tree).apply(circ)
+ print(circ.get_commands())
+ Graph(circ).get_qubit_graph()
+```
+
+## Combinators
+
+% Passes are building blocks that can be composed into more sophisticated strategies encapsulating the full compilation flow
+
+% Basic sequencing
+
+The passes encountered so far represent elementary, self-contained transformations on {py:class}`~pytket.circuit.Circuit` s. In practice, we will almost always want to apply sequences of these to combine optimisations with solving for many constraints. The passes in `pytket` have a rudimentary compositional structure to describe generic compilation strategies, with the most basic example being just applying a list of passes in order.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+ from pytket.passes import auto_rebase_pass, EulerAngleReduction, SequencePass
+
+ rebase_quil = auto_rebase_pass({OpType.CZ, OpType.Rz, OpType.Rx})
+ circ = Circuit(3)
+ circ.CX(0, 1).Rx(0.3, 1).CX(2, 1).Rz(0.8, 1)
+ comp = SequencePass([rebase_quil, EulerAngleReduction(OpType.Rz, OpType.Rx)])
+ comp.apply(circ)
+ print(circ.get_commands())
+```
+
+% Repeat passes until no further change - useful when one pass can enable further matches for another type of optimisation
+
+When composing optimisation passes, we may find that applying one type of optimisation could open up opportunities for others by, for example, rearranging gates to match the desired template. To make the most of this, it may be beneficial to apply some pass combination repeatedly until no further changes are made, i.e. until we have found and exploited every simplification that we can.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatPass, SequencePass
+
+ circ = Circuit(4)
+ circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
+ comp = RepeatPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]))
+ comp.apply(circ)
+ print(circ.get_commands())
+```
+
+:::{warning}
+This looping mechanism does not directly compare the {py:class}`~pytket.circuit.Circuit` to its old state from the previous iteration, instead checking if any of the passes within the loop body claimed they performed any rewrite. Some sequences of passes will do and undo some changes to the {py:class}`~pytket.circuit.Circuit`, giving no net effect but nonetheless causing the loop to repeat. This can lead to infinite loops if used in such a way. Some passes where the {py:class}`~pytket.circuit.Circuit` is converted to another form and back again (e.g. {py:class}`~pytket.passes.PauliSimp`) will always report that a change took place. We recommend testing any looping passes thoroughly to check for termination.
+:::
+
+% Repeat with metric - useful when hard to tell when a change is being made or you only care about specific changes
+
+Increased termination safety can be given by only repeating whilst some easy-to-check metric (such as number of gates or depth) decreases. For example, we may want to try to minimise the number of `OpType.CX` gates since these will tend to be very slow and noisy on a lot of devices.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+ from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatWithMetricPass, SequencePass
+
+ circ = Circuit(4)
+ circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
+ cost = lambda c : c.n_gates_of_type(OpType.CX)
+ comp = RepeatWithMetricPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]), cost)
+ comp.apply(circ) # Stops earlier than before, since removing CYs doesn't change the number of CXs
+ print(circ.get_commands())
+```
+
+% May reject compositions if pre/post-conditions don't match up; some passes will fail to complete or fail to achieve their objective if a circuit does not match their pre-conditions, so we prevent compositions where the latter's pre-conditions cannot be guaranteed
+
+We mentioned earlier that each pass has a set of pre-conditions and post-conditions expressed via {py:class}`~pytket.predicates.Predicate` s. We may find that applying one pass invalidates the pre-conditions of a later pass, meaning it may hit an error when applied to a {py:class}`~pytket.circuit.Circuit`. For example, the {py:class}`~pytket.passes.KAKDecomposition` optimisation method can only operate on {py:class}`~pytket.circuit.Circuit` s with a specific gate set which doesn't allow for any gates on more than 2 qubits, so when {py:class}`~pytket.passes.RoutingPass` can introduce `OpType.BRIDGE` gates over 3 qubits, this could cause an error when trying to apply {py:class}`~pytket.passes.KAKDecomposition`. When using combinators like {py:class}`~pytket.passes.SequencePass` and {py:class}`~pytket.passes.RepeatPass`, `pytket` checks that the passes are safe to compose, in the sense that former passes do not invalidate pre-conditions of the latter passes. This procedure uses a basic form of Hoare logic to identify new pre- and post-conditions for the combined pass and identify whether it is still satisfiable.
+
+% Warning about composing with `DecomposeBoxes`
+
+A special mention here goes to the {py:class}`~pytket.passes.DecomposeBoxes` pass. Because the Box structures could potentially contain arbitrary sequences of gates, there is no guarantee that expanding them will yield a {py:class}`~pytket.circuit.Circuit` that satisfies `any` {py:class}`~pytket.predicates.Predicate`. Since it has potential to invalidate the pre-conditions of any subsequent pass, composing it with anything else `will` generate such an error.
+
+
+```{code-cell} ipython3
+ :raises: RuntimeError
+
+ from pytket.passes import DecomposeBoxes, PauliSimp, SequencePass
+ # PauliSimp requires a specific gateset and no conditional gates
+ # or mid-circuit measurement, so this will raise an exception
+ comp = SequencePass([DecomposeBoxes(), PauliSimp()])
+```
+
+## Predefined Sequences
+
+Knowing what sequences of compiler passes to apply for maximal performance is often a very hard problem and can require a lot of experimentation and intuition to predict reliably. Fortunately, there are often common patterns that are applicable to virtually any scenario, for which `pytket` provides some predefined sequences.
+
+% `FullPeepholeOptimise` kitchen-sink, but assumes a universal quantum computer
+
+In practice, peephole and structure-preserving optimisations are almost always strictly beneficial to apply, or at least will never increase the size of the {py:class}`~pytket.circuit.Circuit`. The {py:class}`~pytket.passes.FullPeepholeOptimise` pass applies Clifford simplifications, commutes single-qubit gates to the front of the circuit and applies passes to squash subcircuits of up to three qubits. This provides a one-size-approximately-fits-all "kitchen sink" solution to {py:class}`~pytket.circuit.Circuit` optimisation. This assumes no device constraints by default, so will not generally preserve gateset, connectivity, etc.
+
+When targeting a heterogeneous device architecture, solving this constraint in its entirety will generally require both placement and subsequent routing. {py:class}`DefaultMappingPass` simply combines these to apply the {py:class}`GraphPlacement` strategy and solve any remaining invalid multi-qubit operations. This is taken a step further with {py:class}`~pytket.passes.CXMappingPass` which also decomposes the introduced `OpType.SWAP` and `OpType.BRIDGE` gates into elementary `OpType.CX` gates.
+
+% `Synthesise<>` passes combine light optimisations that preserve qubit connectivity and target a specific gate set
+
+After solving for the device connectivity, we then need to restrict what optimisations we can apply to those that won't invalidate this. The set of {py:class}`SynthesiseX` passes combine light optimisations that preserve the qubit connectivity and target a specific final gate set (e.g. {py:class}`SynthesiseTket` guarantees the output is in the gateset of `OpType.CX`, `OpType.TK1`, and `OpType.Measure`). In general, this will not reduce the size of a {py:class}`~pytket.circuit.Circuit` as much as {py:class}`~pytket.passes.FullPeepholeOptimise`, but has the benefit of removing some redundancies introduced by routing without invalidating it.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit, OpType
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.passes import FullPeepholeOptimise, DefaultMappingPass, SynthesiseTket, RebaseTket
+
+circ = Circuit(5)
+circ.CX(0, 1).CX(0, 2).CX(0, 3)
+circ.CZ(0, 1).CZ(0, 2).CZ(0, 3)
+circ.CX(3, 4).CX(0, 3).CX(4, 0)
+
+RebaseTket().apply(circ) # Get number of 2qb gates by converting all to CX
+print(circ.n_gates_of_type(OpType.CX))
+
+FullPeepholeOptimise().apply(circ) # Freely rewrite circuit
+print(circ.n_gates_of_type(OpType.CX))
+
+backend = IBMQBackend("ibmq_quito")
+DefaultMappingPass(backend.backend_info.architecture).apply(circ)
+RebaseTket().apply(circ)
+print(circ.n_gates_of_type(OpType.CX)) # Routing adds gates
+print(circ.get_commands())
+
+SynthesiseTket().apply(circ) # Some added gates may be redundant
+print(circ.n_gates_of_type(OpType.CX)) # But not in this case
+```
+
+
+```
+9
+6
+9
+[tk1(0, 0, 1.5) node[0];, tk1(0, 0, 1.5) node[1];, tk1(0, 0, 1.5) node[2];, tk1(0, 0, 1.5) node[3];, CX node[1], node[0];, tk1(0, 0, 0.5) node[0];, CX node[1], node[2];, CX node[1], node[3];, tk1(0, 0, 0.5) node[2];, tk1(0, 0, 0.5) node[3];, CX node[3], node[4];, CX node[1], node[3];, CX node[3], node[4];, CX node[4], node[3];, CX node[3], node[4];, CX node[3], node[1];]
+9
+```
+
+:::{Note}
+{py:class}`~pytket.passes.FullPeepholeOptimise` takes an optional `allow_swaps` argument. This is a boolean flag to indicate whether {py:class}`~pytket.passes.FullPeepholeOptimise` should preserve the circuit connectivity or not. If set to `False` the pass will presrve circuit connectivity but the circuit will generally be less optimised than if connectivity was ignored.
+
+{py:class}`~pytket.passes.FullPeepholeOptimise` also takes an optional `target_2qb_gate` argument to specify whether to target the {{py:class}`OpType.TK1`, {py:class}`OpType.CX`} or {{py:class}`OpType.TK1`, {py:class}`OpType.TK2`} gateset.
+:::
+
+:::{Note}
+Prevous versions of {py:class}`~pytket.passes.FullPeepholeOptimise` did not apply the {py:class}`~pytket.passes.ThreeQubitSquash` pass. There is a {py:class}`~pytket.passes.PeepholeOptimise2Q` pass which applies the old pass sequence with the {py:class}`~pytket.passes.ThreeQubitSquash` pass excluded.
+:::
+
+% `Backend.default_compilation_pass` gives a recommended compiler pass to solve the backend's constraints with little or light optimisation
+
+Also in this category of pre-defined sequences, we have the {py:meth}`~pytket.backends.Backend.default_compilation_pass()` which is run by {py:meth}`~pytket.backends.Backend.get_compiled_circuit`. These give a recommended compiler pass to solve the {py:class}`~pytket.backends.Backend` s constraints with a choice of optimisation levels.
+
+| Optimisation level | Description |
+| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
+| 0 | Just solves the constraints as simply as possible. No optimisation. |
+| 1 | Adds basic optimisations (those covered by the {py:class}`SynthesiseX` passes) for efficient compilation. |
+| 2 | Extends to more intensive optimisations (those covered by the {py:class}`~pytket.passes.FullPeepholeOptimise` pass). |
+
+We will now demonstrate the {py:meth}`~pytket.backends.Backend.default_compilation_pass` with the different levels of optimisation using the `ibmq_quito` device.
+
+As more intensive optimisations are applied by level 2 the pass may take a long to run for large circuits. In this case it may be preferable to apply the lighter optimisations of level 1.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+ from pytket import Circuit, OpType
+ from pytket.extensions.qiskit import IBMQBackend
+
+ circ = Circuit(3) # Define a circuit to be compiled to the backend
+ circ.CX(0, 1)
+ circ.H(1)
+ circ.Rx(0.42, 1)
+ circ.S(1)
+ circ.CX(0, 2)
+ circ.CX(2, 1)
+ circ.Z(2)
+ circ.Y(1)
+ circ.CX(0, 1)
+ circ.CX(2, 0)
+ circ.measure_all()
+
+ backend = IBMQBackend("ibmq_quito") # Initialise Backend
+
+ print("Total gate count before compilation =", circ.n_gates)
+ print("CX count before compilation =", circ.n_gates_of_type(OpType.CX))
+
+ # Now apply the default_compilation_pass at different levels of optimisation.
+
+ for ol in range(3):
+ test_circ = circ.copy()
+ backend.default_compilation_pass(optimisation_level=ol).apply(test_circ)
+ assert backend.valid_circuit(test_circ)
+ print("Optimisation level", ol)
+ print("Gates", test_circ.n_gates)
+ print("CXs", test_circ.n_gates_of_type(OpType.CX))
+```
+
+
+```
+Total gate count before compilation = 13
+CX count before compilation = 5
+Optimisation level 0
+Gates 22
+CXs 8
+Optimisation level 1
+Gates 12
+CXs 5
+Optimisation level 2
+Gates 6
+CXs 1
+```
+
+**Explanation**
+
+We see that compiling the circuit to `ibmq_quito` at optimisation level 0 actually increases the gate count. This is because `ibmq_quito` has connectivity constraints which require additional CX gates to be added to validate the circuit.
+The single-qubit gates in our circuit also need to be decomposed into the IBM gatset.
+
+We see that compiling at optimisation level 1 manages to reduce the CX count to 5. Our connectivity constraints are satisfied without increasing the CX gate count. Single-qubit gates are also combined to reduce the overall gate count further.
+
+Finally we see that the default pass for optimisation level 2 manages to reduce the overall gate count to just 6 with only one CX gate. This is because more intensive optimisations are applied at this level including squashing passes that enable optimal two and three-qubit circuits to be synthesised. Applying these more powerful passes comes with a runtime overhead that may be noticeable for larger circuits.
+
+## Guidance for Combining Passes
+
+% More powerful optimisations tend to have fewer guarantees on the structure of the output, so advisable to perform before trying to satisfy device constraints
+
+We find that the most powerful optimisation techniques (those that have the potential to reduce {py:class}`~pytket.circuit.Circuit` size the most for some class of {py:class}`~pytket.circuit.Circuit` s) tend to have fewer guarantees on the structure of the output, requiring a universal quantum computer with the ability to perform any gates on any qubits. It is recommended to apply these early on in compilation.
+
+% Solving some device constraints might invalidate others, such as routing invalidating `NoMidMeasurePredicate` and `GateSetPredicate`
+
+The passes to solve some device constraints might invalidate others: for example, the {py:class}`RoutingPass` generally invalidates {py:class}`NoMidMeasurePredicate` and {py:class}`GateSetPredicate`. Therefore, the order in which these are solved should be chosen with care.
+
+% Recommended order of decompose boxes, strong optimisations, placement, routing, delay measures, rebase; could insert minor optimisations between each step to tidy up any redundancies introduced as long as they preserve solved constraints
+
+For most standard use cases, we recommend starting with {py:class}`~pytket.passes.DecomposeBoxes` to reduce the {py:class}`~pytket.circuit.Circuit` down to primitive gates, followed by strong optimisation passes like {py:class}`PauliSimp` (when appropriate for the types of {py:class}`~pytket.circuit.Circuit` s being considered) and {py:class}`~pytket.passes.FullPeepholeOptimise` to eliminate a large number of redundant operations. Then start to solve some more device constraints with some choice of placement and routing strategy, followed by {py:class}`DelayMeasures` to push measurements back through any introduced `OpType.SWAP` or `OpType.BRIDGE` gates, and then finally rebase to the desired gate set. The {py:meth}`~pytket.backends.Backend.default_compilation_pass()` definitions can replace this sequence from placement onwards for simplicity. Minor optimisations could also be inserted between successive steps to tidy up any redundancies introduced, as long as they preserve the solved constraints.
+
+## Initial and Final Maps
+
+% Placement, routing, and other passes can change the names of qubits; the map from logical to physical qubits can be different at the start and end of the circuit; define initial and final maps
+
+% Can use this to identify what placement was selected or how to interpret the final state
+
+{py:class}`PlacementPass` modifies the set of qubits used in the {py:class}`~pytket.circuit.Circuit` from the logical names used during construction to the names of the physical addresses on the {py:class}`~pytket.backends.Backend`, so the logical qubit names wiil no longer exist within the {py:class}`~pytket.circuit.Circuit` by design. Knowing the map between the logical qubits and the chosen physical qubits is necessary for understanding the choice of placement, interpreting the final state from a naive simulator, identifying which physical qubits each measurement was made on for error mitigation, and appending additional gates to the logical qubits after applying the pass.
+
+Other passes like {py:class}`RoutingPass` and {py:class}`~pytket.passes.CliffordSimp` can introduce (explicit or implicit) permutations of the logical qubits in the middle of a {py:class}`~pytket.circuit.Circuit`, meaning a logical qubit may exist on a different physical qubit at the start of the {py:class}`~pytket.circuit.Circuit` compared to the end.
+
+% Encapsulating a circuit in a `CompilationUnit` allows the initial and final maps to be tracked when a pass is applied
+
+We can wrap up a {py:class}`~pytket.circuit.Circuit` in a {py:class}`~pytket.predicates.CompilationUnit` to allow us to track any changes to the locations of the logical qubits when passes are applied. The {py:attr}`CompilationUnit.initial_map` is a dictionary mapping the original {py:class}`UnitID` s to the corresponding {py:class}`UnitID` used in {py:attr}`CompilationUnit.circuit`, and similarly {py:attr}`CompilationUnit.final_map` for outputs. Applying {py:meth}`BasePass.apply()` to a {py:class}`~pytket.predicates.CompilationUnit` will apply the transformation to the underlying {py:class}`~pytket.circuit.Circuit` and track the changes to the initial and final maps.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.passes import DefaultMappingPass
+from pytket.predicates import CompilationUnit
+
+circ = Circuit(5, 5)
+circ.CX(0, 1).CX(0, 2).CX(0, 3).CX(0, 4).measure_all()
+backend = IBMQBackend("ibmq_quito")
+cu = CompilationUnit(circ)
+DefaultMappingPass(backend.backend_info.architecture).apply(cu)
+print(cu.circuit.get_commands())
+print(cu.initial_map)
+print(cu.final_map)
+```
+
+
+```
+[CX node[1], node[0];, Measure node[0] --> c[1];, CX node[1], node[2];, Measure node[2] --> c[2];, CX node[1], node[3];, Measure node[3] --> c[3];, SWAP node[1], node[3];, CX node[3], node[4];, Measure node[3] --> c[0];, Measure node[4] --> c[4];]
+{c[0]: c[0], c[1]: c[1], c[2]: c[2], c[3]: c[3], c[4]: c[4], q[0]: node[1], q[1]: node[0], q[2]: node[2], q[3]: node[3], q[4]: node[4]}
+{c[0]: c[0], c[1]: c[1], c[2]: c[2], c[3]: c[3], c[4]: c[4], q[0]: node[3], q[1]: node[0], q[2]: node[2], q[3]: node[1], q[4]: node[4]}
+```
+
+:::{note}
+No passes currently rename or swap classical data, but the classical bits are included in these maps for completeness.
+:::
+
+## Advanced Compilation Topics
+
+### Compiling Symbolic Circuits
+
+% Defining a single symbolic circuit and instantiating it multiple times saves effort in circuit construction, and means the circuit only has to be compiled once, saving time or allowing more expensive optimisations to be considered
+
+For variational algorithms, the prominent benefit of defining a {py:class}`~pytket.circuit.Circuit` symbolically and only instantiating it with concrete values when needed is that the compilation procedure would only need to be performed once. By saving time here we can cut down the overall time for an experiment; we could invest the time saved into applying more expensive optimisations on the {py:class}`~pytket.circuit.Circuit` to reduce the impact of noise further.
+
+% Example with variational optimisation using statevector simulator
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, Qubit
+ from pytket.extensions.qiskit import AerStateBackend
+ from pytket.pauli import Pauli, QubitPauliString
+ from pytket.utils.operators import QubitPauliOperator
+ from sympy import symbols
+
+ a, b = symbols("a b")
+ circ = Circuit(2)
+ circ.Ry(a, 0)
+ circ.Ry(a, 1)
+ circ.CX(0, 1)
+ circ.Rz(b, 1)
+ circ.CX(0, 1)
+ xx = QubitPauliString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
+ op = QubitPauliOperator({xx : 1.5})
+
+ backend = AerStateBackend()
+
+ # Compile once outside of the objective function
+ circ = backend.get_compiled_circuit(circ)
+
+ def objective(params):
+ state = circ.copy()
+ state.symbol_substitution({a : params[0], b : params[1]})
+ handle = backend.process_circuit(state) # No need to compile again
+ vec = backend.get_result(handle).get_state()
+ return op.state_expectation(vec)
+
+ print(objective([0.25, 0.5]))
+ print(objective([0.5, 0]))
+```
+
+% Warning about `NoSymbolsPredicate` and necessity of instantiation before running on backends
+
+:::{note}
+Every {py:class}`~pytket.backends.Backend` requires {py:class}`NoSymbolsPredicate`, so it is necessary to instantiate all symbols before running a {py:class}`~pytket.circuit.Circuit`.
+:::
+
+### User-defined Passes
+
+We have already seen that pytket allows users to combine passes in a desired order using {py:class}`~pytket.passes.SequencePass`. An addtional feature is the {py:class}`~pytket.passes.CustomPass` which allows users to define their own custom circuit transformation using pytket.
+The {py:class}`~pytket.passes.CustomPass` class accepts a `transform` parameter, a python function that takes a {py:class}`~pytket.circuit.Circuit` as input and returns a {py:class}`~pytket.circuit.Circuit` as output.
+
+We will show how to use {py:class}`~pytket.passes.CustomPass` by defining a simple transformation that replaces any Pauli Z gate in the {py:class}`~pytket.circuit.Circuit` with a Hadamard gate, Pauli X gate, Hadamard gate chain.
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit, OpType
+
+ def z_transform(circ: Circuit) -> Circuit:
+ n_qubits = circ.n_qubits
+ circ_prime = Circuit(n_qubits) # Define a replacement circuit
+
+ for cmd in circ.get_commands():
+ qubit_list = cmd.qubits # Qubit(s) our gate is applied on (as a list)
+ if cmd.op.type == OpType.Z:
+ # If cmd is a Z gate, decompose to a H, X, H sequence.
+ circ_prime.add_gate(OpType.H, qubit_list)
+ circ_prime.add_gate(OpType.X, qubit_list)
+ circ_prime.add_gate(OpType.H, qubit_list)
+ else:
+ # Otherwise, apply the gate as usual.
+ circ_prime.add_gate(cmd.op.type, cmd.op.params, qubit_list)
+
+ return circ_prime
+```
+
+After we've defined our `transform` we can construct a {py:class}`~pytket.passes.CustomPass`. This pass can then be applied to a {py:class}`~pytket.circuit.Circuit`.
+
+
+```{code-cell} ipython3
+
+ from pytket.passes import CustomPass
+
+ DecompseZPass = CustomPass(z_transform) # Define our pass
+
+ test_circ = Circuit(2) # Define a test Circuit for our pass
+ test_circ.Z(0)
+ test_circ.Z(1)
+ test_circ.CX(0, 1)
+ test_circ.Z(1)
+ test_circ.CRy(0.5, 0, 1)
+
+ DecompseZPass.apply(test_circ) # Apply our pass to the test Circuit
+
+ test_circ.get_commands() # Commands of our transformed Circuit
+```
+
+We see from the output above that our newly defined {py:class}`DecompseZPass` has successfully decomposed the Pauli Z gates to Hadamard, Pauli X, Hadamard chains and left other gates unchanged.
+
+:::{warning}
+pytket does not require that {py:class}`~pytket.passes.CustomPass` preserves the unitary of the {py:class}`~pytket.circuit.Circuit` . This is for the user to ensure.
+:::
+
+### Partial Compilation
+
+% Commonly want to run many circuits that have large identical regions; by splitting circuits into regions, can often compile individually and compose to speed up compilation time
+
+A common pattern across expectation value and tomography experiments is to run many {py:class}`~pytket.circuit.Circuit` s that have large identical regions, such as a single state preparation with many different measurements. We can further speed up the overall compilation time by splitting up the state preparation from the measurements, compiling each subcircuit only once, and composing together at the end.
+
+% Only have freedom to identify good placements for the first subcircuit to be run, the rest are determined by final maps in order to compose well
+
+The main technical consideration here is that the compiler will only have the freedom to identify good placements for the first subcircuit to be run. This means that the state preparation should be compiled first, and the placement for the measurements is given by the final map in order to compose well.
+
+Once compiled, we can use {py:meth}`~pytket.backends.Backend.process_circuits` to submit several circuits at once for execution on the backend. The circuits to be executed are passed as list. If the backend is shot-based, the number of shots can be passed using the `n_shots` parameter, which can be a single integer or a list of integers of the same length as the list of circuits to be executed. In the following example, 4000 shots are measured for the first circuit and 2000 for the second.
+
+% Example of state prep with many measurements; compile state prep once, inspect final map, use this as placement for measurement circuits and compile them, then compose
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket import Circuit, OpType
+from pytket.extensions.qiskit import IBMQBackend
+from pytket.predicates import CompilationUnit
+from pytket.placement import Placement
+
+state_prep = Circuit(4)
+state_prep.H(0)
+state_prep.add_gate(OpType.CnRy, 0.1, [0, 1])
+state_prep.add_gate(OpType.CnRy, 0.2, [0, 2])
+state_prep.add_gate(OpType.CnRy, 0.3, [0, 3])
+
+measure0 = Circuit(4, 4)
+measure0.H(1).H(3).measure_all()
+measure1 = Circuit(4, 4)
+measure1.CX(1, 2).CX(3, 2).measure_all()
+
+backend = IBMQBackend("ibmq_quito")
+cu = CompilationUnit(state_prep)
+backend.default_compilation_pass().apply(cu)
+Placement.place_with_map(measure0, cu.final_map)
+Placement.place_with_map(measure1, cu.final_map)
+backend.default_compilation_pass().apply(measure0)
+backend.default_compilation_pass().apply(measure1)
+
+circ0 = cu.circuit
+circ1 = circ0.copy()
+circ0.append(measure0)
+circ1.append(measure1)
+handles = backend.process_circuits([circ0, circ1], n_shots=[4000, 2000])
+r0, r1 = backend.get_results(handles)
+print(r0.get_counts())
+print(r1.get_counts())
+```
+
+
+```
+
+{(0, 0, 0, 0): 503, (0, 0, 0, 1): 488, (0, 1, 0, 0): 533, (0, 1, 0, 1): 493, (1, 0, 0, 0): 1041, (1, 0, 0, 1): 107, (1, 0, 1, 0): 115, (1, 0, 1, 1): 14, (1, 1, 0, 0): 576, (1, 1, 0, 1): 69, (1, 1, 1, 0): 54, (1, 1, 1, 1): 7}
+{(0, 0, 0, 0): 2047, (0, 1, 0, 0): 169, (0, 1, 1, 0): 1729, (1, 1, 0, 0): 7, (1, 1, 1, 0): 48}
+```
+
+### Measurement Reduction
+
+% Measurement scenario has a single state generation circuit but many measurements we want to make; suppose each measurements is Pauli
+
+% Naively, need one measurement circuit per measurement term
+
+% Commuting observables can be measured simultaneously
+
+Suppose we have one of these measurement scenarios (i.e. a single state preparation, but many measurements to make on it) and that each of the measurements is a Pauli observable, such as when calculating the expectation value of the state with respect to some {py:class}`QubitPauliOperator`. Naively, we would need a different measurement {py:class}`~pytket.circuit.Circuit` per term in the operator, but we can reduce this by exploiting the fact that commuting observables can be measured simultaneously.
+
+% Given a set of observables, partition into sets that are easy to measure simultaneously and generate circuits performing this by diagonalising them (reducing each to a combination of Z-measurements)
+
+Given a set of observables, we can partition them into subsets that are easy to measure simultaneously. A {py:class}`~pytket.circuit.Circuit` is generated for each subset by diagonalising the observables (reducing all of them to a combination of $Z$-measurements).
+
+% Commuting sets vs non-conflicting sets
+
+Diagonalising a mutually commuting set of Pauli observables could require an arbitrary Clifford circuit in general. If we are considering the near-term regime where "every gate counts", the diagonalisation of the observables could introduce more of the (relatively) expensive two-qubit gates, giving us the speedup at the cost of some extra noise. `pytket` can partition the Pauli observables into either general commuting sets for improved reduction in the number of measurement {py:class}`~pytket.circuit.Circuit` s, or into smaller sets which can be diagonalised without introducing any multi-qubit gates - this is possible when all observables are substrings of some measured Pauli string (e.g. `XYI` and `IYZ` is fine, but `ZZZ` and `XZX` is not).
+
+% Could have multiple circuits producing the same observable, so can get extra shots/precision for free
+
+This measurement partitioning is built into the {py:meth}`~pytket.backends.Backend.get_operator_expectation_value` utility method, or can be used directly using {py:meth}`pytket.partition.measurement_reduction()` which builds a {py:class}`~pytket.partition.MeasurementSetup` object. A {py:class}`~pytket.partition.MeasurementSetup` contains a list of measurement {py:class}`~pytket.circuit.Circuit` s and a map from the {py:class}`QubitPauliString` of each observable to the information required to extract the expectation value (which bits to consider from which {py:class}`~pytket.circuit.Circuit`).
+
+
+```{code-cell} ipython3
+
+from pytket import Qubit
+from pytket.pauli import Pauli, QubitPauliString
+from pytket.partition import measurement_reduction, PauliPartitionStrat
+
+zi = QubitPauliString({Qubit(0):Pauli.Z})
+iz = QubitPauliString({Qubit(1):Pauli.Z})
+zz = QubitPauliString({Qubit(0):Pauli.Z, Qubit(1):Pauli.Z})
+xx = QubitPauliString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
+yy = QubitPauliString({Qubit(0):Pauli.Y, Qubit(1):Pauli.Y})
+
+setup = measurement_reduction([zi, iz, zz, xx, yy], strat=PauliPartitionStrat.CommutingSets)
+print("Via Commuting Sets:")
+for i, c in enumerate(setup.measurement_circs):
+ print(i, c.get_commands())
+print(setup.results[yy])
+
+setup = measurement_reduction([zi, iz, zz, xx, yy], strat=PauliPartitionStrat.NonConflictingSets)
+print("Via Non-Conflicting Sets:")
+for i, c in enumerate(setup.measurement_circs):
+ print(i, c.get_commands())
+print(setup.results[yy])
+```
+
+:::{note}
+Since there could be multiple measurement {py:class}`~pytket.circuit.Circuit` s generating the same observable, we could theoretically use this to extract extra shots (and hence extra precision) for that observable for free; automatically doing this as part of {py:meth}`measurement_reduction()` is planned for a future release of `pytket`.
+:::
+
+### Contextual Optimisations
+
+By default, tket makes no assumptions about a circuit's input state, nor about
+the destiny of its output state. We can therefore compose circuits freely,
+construct boxes from them that we can then place inside other circuits, and so
+on. However, when we come to run a circuit on a real device we can almost always
+assume that it will be initialised in the all-zero state, and that the final
+state of the qubits will be discarded (after measurement).
+
+This is where `contextual optimisations` can come into play. These are
+optimisations that depend on knowledge of the context of the circuit being run.
+They do not generally preserve the full unitary, but they generate circuits that
+are observationally indistinguishable (on an ideal device), and reduce noise by
+eliminating unnecessary operations from the beginning or end of the circuit.
+
+First of all, tket provides methods to `annotate` a qubit (or all qubits) as
+being initialized to zero, or discarded at the end of the circuit, or both.
+
+
+```{code-cell} ipython3
+
+from pytket import Circuit
+
+c = Circuit(2)
+c.Y(0)
+c.CX(0,1)
+c.H(0)
+c.H(1)
+c.Rz(0.125, 1)
+c.measure_all()
+c.qubit_create_all()
+c.qubit_discard_all()
+```
+
+The last two lines tell the compiler that all qubits are to be initialized to
+zero and discarded at the end. The methods {py:meth}`Circuit.qubit_create` and
+{py:meth}`Circuit.qubit_discard` can be used to achieve the same on individual
+qubits.
+
+:::{warning}
+Note that we are now restricted in how we can compose our circuit with other circuits. When composing after another circuit, a "created" qubit becomes a Reset operation. Whem composing before another circuit, a "discarded" qubit may not be joined to another qubit unless that qubit has itself been "created" (so that the discarded state gets reset to zero).
+:::
+
+#### Initial simplification
+
+When the above circuit is run from an all-zero state, the Y and CX gates at the
+beginning just have the effect of putting both qubits in the $\lvert 1
+\rangle$ state (ignoring unobservable global phase), so they could be replaced
+with two X gates. This is exactly what the {py:class}`~pytket.passes.SimplifyInitial` pass does.
+
+
+```{code-cell} ipython3
+
+from pytket.passes import SimplifyInitial
+
+SimplifyInitial().apply(c)
+print(c.get_commands())
+```
+
+This pass tracks the state of qubits known to be initialised to zero (or reset
+mid-circuit) forward through the circuit, for as long as the qubits remain in a
+computational basis state, either removing gates (when they don't change the
+state) or replacing them with X gates (when they invert the state).
+
+By default, this pass also replaces Measure operations acting on qubits with a
+known state by classical set-bits operations on the target bits:
+
+
+```{code-cell} ipython3
+
+ c = Circuit(1).X(0).measure_all()
+ c.qubit_create_all()
+ SimplifyInitial().apply(c)
+ print(c.get_commands())
+```
+
+The measurement has disappeared, replaced with a classical operation on its
+target bit. To disable this behaviour, pass the `allow_classical=False`
+argument to {py:class}`~pytket.passes.SimplifyInitial` when constructing the pass.
+
+:::{warning}
+Most backends currently do not support set-bit operations, so these could cause errors when using this pass with mid-circuit measurements. In such cases you should set `allow_classical=False`.
+:::
+
+Note that {py:class}`~pytket.passes.SimplifyInitial` does not automatically cancel successive
+pairs of X gates introduced by the simplification. It is a good idea to follow
+it with a {py:class}`~pytket.passes.RemoveRedundancies` pass in order to perform these
+cancellations.
+
+#### Removal of discarded operations
+
+An operation that has no quantum or classical output in its causal future has no
+effect (or rather, no observable effect on an ideal system), and can be removed
+from the circuit. By marking a qubit as discarded, we tell the compiler that it
+has no quantum output, potentially enabling this simplification.
+
+Note that if the qubit is measured, even if it is then discarded, the Measure
+operation has a classical output in its causal future so will not be removed.
+
+
+```{code-cell} ipython3
+
+ from pytket.circuit import Qubit
+ from pytket.passes import RemoveDiscarded
+
+ c = Circuit(3, 2)
+ c.H(0).H(1).H(2).CX(0, 1).Measure(0, 0).Measure(1, 1).H(0).H(1)
+ c.qubit_discard(Qubit(0))
+ c.qubit_discard(Qubit(2))
+ RemoveDiscarded().apply(c)
+ print(c.get_commands())
+```
+
+The Hadamard gate following the measurement on qubit 0, as well as the Hadamard
+on qubit 2, have disappeared, because those qubits were discarded. The Hadamard
+following the measurement on qubit 1 remains, because that qubit was not
+discarded.
+
+#### Commutation of measured classical maps
+
+The last type of contextual optimization is a little more subtle. Let's call a
+quantum unitary operation a `classical map` if it sends every computational
+basis state to a computational basis state, possibly composed with a diagonal
+operator. For example, X, Y, Z, Rz, CX, CY, CZ and Sycamore are classical maps,
+but Rx, Ry and H are not. Check the
+[documentation of gate types](https://tket.quantinuum.com/api-docs/optype.html)
+to see which gates have unitaries that make them amenable to optimisation.
+
+When a classical map is followed by a measurement of all its qubits, and those
+qubits are then discarded, it can be replaced by a purely classical operation
+acting on the classical outputs of the measurement.
+
+For example, if we apply a CX gate and then measure the two qubits, the result
+is (ideally) the same as if we measured the two qubits first and then applied a
+classical controlled-NOT on the measurement bits. If the gate were a CY instead
+of a CX the effect would be identical: the only difference is the insertion of a
+diagonal operator, whose effect is unmeasurable.
+
+This simplification is effected by the {py:class}`~pytket.passes.SimplifyMeasured` pass.
+
+Let's illustrate this with a Bell circuit:
+
+
+```{code-cell} ipython3
+
+ from pytket.passes import SimplifyMeasured
+
+ c = Circuit(2).H(0).CX(0, 1).measure_all()
+ c.qubit_discard_all()
+ SimplifyMeasured().apply(c)
+ print(c.get_commands())
+```
+
+The CX gate has disappeared, replaced with a classical transform acting on the
+bits after the measurement.
+
+#### Contextual optimisation in practice
+
+The above three passes are combined in the {py:class}`~pytket.passes.ContextSimp` pass, which
+also performs a final {py:class}`~pytket.passes.RemoveRedundancies`. Normally, before running a
+circuit on a device you will want to apply this pass (after using
+{py:meth}`Circuit.qubit_create_all` and {py:meth}`Circuit.qubit_discard_all` to
+enable the simplifications).
+
+However, most backends cannot process the classical operations that may be
+introduced by {py:class}`~pytket.passes.SimplifyMeasured` or (possibly)
+{py:class}`~pytket.passes.SimplifyInitial`. So pytket provides a method
+{py:meth}`separate_classical` to separate the classical postprocessing circuit
+from the main circuit to be run on the device. This postprocessing circuit is
+then passed as the `ppcirc` argument to {py:meth}`~pytket.backends.BackendResult.get_counts` or
+{py:meth}`~pytket.backends.BackendResult.get_shots`, in order to obtain the postprocessed
+results.
+
+Much of the above is wrapped up in the utility method
+{py:meth}`prepare_circuit`. This takes a circuit, applies
+{py:meth}`Circuit.qubit_create_all` and {py:meth}`Circuit.qubit_discard_all`,
+runs the full {py:class}`~pytket.passes.ContextSimp` pass, and then separates the result into
+the main circuit and the postprocessing circuit, returning both.
+
+Thus a typical usage would look something like this:
+
+
+```{code-cell} ipython3
+
+ from pytket.utils import prepare_circuit
+ from pytket.extensions.qiskit import AerBackend
+
+ b = AerBackend()
+ c = Circuit(2).H(0).CX(0, 1)
+ c.measure_all()
+ c0, ppcirc = prepare_circuit(c)
+ c0 = b.get_compiled_circuit(c0)
+ h = b.process_circuit(c0, n_shots=10)
+ r = b.get_result(h)
+ shots = r.get_shots(ppcirc=ppcirc)
+ print(shots)
+```
+
+This is a toy example, but illustrates the principle. The actual circuit sent to
+the backend consisted only of a Hadamard gate on qubit 0 and a single
+measurement to bit 0. The classical postprocessing circuit set bit 1 to zero and
+then executed a controlled-NOT from bit 0 to bit 1. These details are hidden
+from us (unless we inspect the circuits), and what we end up with is a shots
+table that is indistinguishable from running the original circuit but with less
+noise.
diff --git a/docs/manual/manual_compiler.rst b/docs/manual/manual_compiler.rst
deleted file mode 100644
index 6b84136a..00000000
--- a/docs/manual/manual_compiler.rst
+++ /dev/null
@@ -1,1168 +0,0 @@
-***********
-Compilation
-***********
-
-So far, we have already covered enough to be able to design the :py:class:`~pytket.circuit.Circuit` s we want to run, submit them to a :py:class:`~pytket.backends.Backend`, and interpret the results in a meaningful way. This is all you need if you want to just try out a quantum computer, run some toy examples and observe some basic results. We actually glossed over a key step in this process by using the :py:meth:`~pytket.backends.Backend.get_compiled_circuit()` method. The compilation step maps from the universal computer abstraction presented at :py:class:`~pytket.circuit.Circuit` construction to the restricted fragment supported by the target :py:class:`~pytket.backends.Backend`, and knowing what a compiler can do to your program can help reduce the burden of design and improve performance on real devices.
-
-The necessity of compilation maps over from the world of classical computation: it is much easier to design correct programs when working with higher-level constructions that aren't natively supported, and it shouldn't require a programmer to be an expert in the exact device architecture to achieve good performance. There are many possible low-level implementations on the device for each high-level program, which vary in the time and resources taken to execute. However, because QPUs are analog devices, the implementation can have a massive impact on the quality of the final outcomes as a result of changing how susceptible the system is to noise. Using a good compiler and choosing the methods appropriately can automatically find a better low-level implementation. Each aspect of the compilation procedure is exposed through ``pytket`` to provide users with a way to have full control over what is applied and how.
-
-.. Optimisation/simplification and constraint solving
-
-The primary goals of compilation are two-fold: solving the constraints of the :py:class:`~pytket.backends.Backend` to get from the abstract model to something runnable, and optimising/simplifying the :py:class:`~pytket.circuit.Circuit` to make it faster, smaller, and less prone to noise. Every step in compilation can generally be split up into one of these two categories (though even the constraint solving steps could have multiple solutions over which we could optimise for noise).
-
-.. Passes capture methods of transforming the circuit, acting in place
-
-Each compiler pass inherits from the :py:class:`~pytket.passes.BasePass` class, capturing a method of transforming a :py:class:`~pytket.circuit.Circuit`. The main functionality is built into the :py:meth:`BasePass.apply()` method, which applies the transformation to a :py:class:`~pytket.circuit.Circuit` in-place. The :py:meth:`~pytket.backends.Backend.get_compiled_circuit()` method is a wrapper around the :py:meth:`~pytket.passes.BasePass.apply()` from the :py:class:`~pytket.backends.Backend` 's recommended pass sequence. This chapter will explore these compiler passes, the different kinds of constraints they are used to solve and optimisations they apply, to help you identify which ones are appropriate for a given task.
-
-Compilation Predicates
-----------------------
-
-.. Predicates capture properties a circuit could satisfy
-.. Primarily used to describe requirements of the backends
-
-Solving the constraints of the target :py:class:`~pytket.backends.Backend` is the essential goal of compilation, so our choice of passes is mostly driven by this set of constraints. We already saw in the last chapter that the :py:attr:`~pytket.backends.Backend.required_predicates` property gives a collection of :py:class:`~pytket.predicates.Predicate` s, describing the necessary properties a :py:class:`~pytket.circuit.Circuit` must satisfy in order to be run.
-
-Each :py:class:`~pytket.predicates.Predicate` can be constructed on its own to impose tests on :py:class:`~pytket.circuit.Circuit` s during construction.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.predicates import GateSetPredicate, NoMidMeasurePredicate
-
- circ = Circuit(2, 2)
- circ.Rx(0.2, 0).CX(0, 1).Rz(-0.7, 1).measure_all()
-
- gateset = GateSetPredicate({OpType.Rx, OpType.CX, OpType.Rz, OpType.Measure})
- midmeasure = NoMidMeasurePredicate()
-
- print(gateset.verify(circ))
- print(midmeasure.verify(circ))
-
- circ.S(0)
-
- print(gateset.verify(circ))
- print(midmeasure.verify(circ))
-
-.. Common predicates
-
-========================================================== =====================================================
-Common :py:class:`~pytket.predicates.Predicate` Constraint
-========================================================== =====================================================
-:py:class:`~pytket.predicates.GateSetPredicate`
- Every gate is within a set of allowed
- :py:class:`~pytket.circuit.OpType` s
-:py:class:`~pytket.predicates.ConnectivityPredicate`
- Every multi-qubit gate acts on
- adjacent qubits according to some
- connectivity graph
-:py:class:`~pytket.predicates.DirectednessPredicate`
- Extends
- :py:class:`~pytket.predicates.ConnectivityPredicate`
- where ``OpType.CX`` gates are only
- supported in a specific orientation
- between adjacent qubits
-:py:class:`~pytket.predicates.NoClassicalControlPredicate`
- The :py:class:`~pytket.circuit.Circuit` does not
- contain any gates that act
- conditionally on classical data
-:py:class:`~pytket.predicates.NoMidMeasurePredicate`
- All ``OpType.Measure`` gates act at
- the end of the :py:class:`~pytket.circuit.Circuit`
- (there are no subsequent gates on
- either the :py:class:`~pytket.unit_id.Qubit` measured
- or the :py:class:`~pytket.unit_id.Bit` written to)
-========================================================== =====================================================
-
-.. Pre/post-conditions of passes
-
-When applying passes, you may find that you apply some constraint-solving pass to satisfy a particular :py:class:`~pytket.predicates.Predicate`, but then a subsequent pass will invalidate it by, for example, introducing gates of different gate types or changing which qubits interact via multi-qubit gates. To help understand and manage this, each pass has a set of pre-conditions that specify the requirements assumed on the :py:class:`~pytket.circuit.Circuit` in order for the pass to successfully be applied, and a set of post-conditions that specify which :py:class:`~pytket.predicates.Predicate` s are guaranteed to hold for the outputs and which are invalidated or preserved by the pass. These can be viewed in the API reference for each pass.
-
-Users may find it desirable to enforce their own constraints upon circuits they are working with. It is possible to construct a :py:class:`UserDefinedPredicate` in pytket based on a function that returns a True/False value.
-
-Below is a minimal example where we construct a predicate which checks if our :py:class:`~pytket.circuit.Circuit` contains fewer than 3 CX gates.
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, OpType
- from pytket.predicates import UserDefinedPredicate
-
- def max_cx_count(circ: Circuit) -> bool:
- return circ.n_gates_of_type(OpType.CX) < 3
-
- # Now construct our predicate using the function defined above
- my_predicate = UserDefinedPredicate(max_cx_count)
-
- test_circ = Circuit(2).CX(0, 1).Rz(0.25, 1).CX(0, 1) # Define a test Circuit
-
- my_predicate.verify(test_circ)
- # test_circ satisfies predicate as it contains only 2 CX gates
-
-Rebases
--------
-
-.. Description
-
-One of the simplest constraints to solve for is the :py:class:`~pytket.predicates.GateSetPredicate`, since we can just substitute each gate in a :py:class:`~pytket.circuit.Circuit` with an equivalent sequence of gates in the target gateset according to some known gate decompositions. In ``pytket``, such passes are referred to as "rebases". The intention here is to perform this translation naively, leaving the optimisation of gate sequences to other passes. Rebases can be applied to any :py:class:`~pytket.circuit.Circuit` and will preserve every structural :py:class:`~pytket.predicates.Predicate`, only changing the types of gates used.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.passes import RebaseTket
-
- circ = Circuit(2, 2)
- circ.Rx(0.3, 0).Ry(-0.9, 1).CZ(0, 1).S(0).CX(1, 0).measure_all()
-
- RebaseTket().apply(circ)
-
- print(circ.get_commands())
-
-:py:class:`~pytket.passes.RebaseTket` is a standard rebase pass that converts to CX and TK1 gates. This is the preferred internal gateset for many ``pytket`` compiler passes. However, it is possible to define a rebase for an arbitrary gateset. Using :py:class:`RebaseCustom`, we can provide an arbitrary set of one- and two-qubit gates. Rather than requiring custom decompositions to be provided for every gate type, it is sufficient to just give them for ``OpType.CX`` and ``OpType.TK1``. For any gate in a given :py:class:`~pytket.circuit.Circuit`, it is either already in the target gateset, or we can use known decompositions to obtain a ``OpType.CX`` and ``OpType.TK1`` representation and then map this to the target gateset.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.passes import RebaseCustom
-
- gates = {OpType.Rz, OpType.Ry, OpType.CY, OpType.ZZPhase}
- cx_in_cy = Circuit(2)
- cx_in_cy.Rz(0.5, 1).CY(0, 1).Rz(-0.5, 1)
-
- def tk1_to_rzry(a, b, c):
- circ = Circuit(1)
- circ.Rz(c + 0.5, 0).Ry(b, 0).Rz(a - 0.5, 0)
- return circ
-
- custom = RebaseCustom(gates, cx_in_cy, tk1_to_rzry)
-
- circ = Circuit(3)
- circ.X(0).CX(0, 1).Ry(0.2, 1)
- circ.ZZPhase(-0.83, 2, 1).Rx(0.6, 2)
-
- custom.apply(circ)
-
- print(circ.get_commands())
-
-For some gatesets, it is not even necessary to specify the CX and TK1 decompositions: there is a useful function :py:meth:`auto_rebase_pass` which can take care of this for you. The pass returned is constructed from the gateset alone. (It relies on some known decompositions, and will raise an exception if no suitable known decompositions exist.) An example is given in the "Combinators" section below.
-
-A similar pair of methods, :py:meth:`SquashCustom` and :py:meth:`auto_squash_pass`, may be used to construct a pass that squashes sequences of single-qubit gates from a given set of single-qubit gates to as short a sequence as possible. Both take a gateset as an argument. :py:meth:`SquashCustom` also takes a function for converting the parameters of a TK1 gate to the target gate set. (Internally, the compiler squashes all gates to TK1 and then applies the supplied function.) :py:meth:`auto_squash_pass` attempts to do the squash using known internal decompositions (but may fail for some gatesets). For example:
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, OpType
- from pytket.passes import auto_squash_pass
-
- gates = {OpType.PhasedX, OpType.Rz, OpType.Rx, OpType.Ry}
- custom = auto_squash_pass(gates)
-
- circ = Circuit(1).H(0).Ry(0.5, 0).Rx(-0.5, 0).Rz(1.5, 0).Ry(0.5, 0).H(0)
- custom.apply(circ)
- print(circ.get_commands())
-
-Note that the H gates (which are not in the specified gateset) are left alone.
-
-
-.. _compiler-placement:
-
-Placement
----------
-
-.. Task of selecting appropriate physical qubits to use; better use of connectivity and better noise characteristics
-
-Initially, a :py:class:`~pytket.circuit.Circuit` designed without a target device in mind will be expressed in terms of actions on a set of "logical qubits" - those with semantic meaning to the computation. A `placement` (or `initial mapping`) is a map from these logical qubits to the physical qubits of the device that will be used to carry them. A given placement may be preferred over another if the connectivity of the physical qubits better matches the interactions between the logical qubits caused by multi-qubit gates, or if the selection of physical qubits has better noise characteristics. All of the information for connectivity and noise characteristics of a given :py:class:`~pytket.backends.Backend` is wrapped up in a :py:class:`~pytket.backends.backendinfo.BackendInfo` object by the :py:attr:`Backend.backend_info` property.
-
-.. Affects where the logical qubits start initially, but it not necessarily where they will end up being measured at the end
-
-The placement only specifies where the logical qubits will be at the start of execution, which is not necessarily where they will end up on termination. Other compiler passes may choose to permute the qubits in the middle of a :py:class:`~pytket.circuit.Circuit` to either exploit further optimisations or enable interactions between logical qubits that were not assigned to adjacent physical qubits.
-
-.. Placement acts in place by renaming qubits to their physical addresses (classical data is never renamed)
-
-A placement pass will act in place on a :py:class:`~pytket.circuit.Circuit` by renaming the qubits from their logical names (the :py:class:`UnitID` s used at circuit construction) to their physical addresses (the :py:class:`UnitID` s recognised by the :py:class:`~pytket.backends.Backend`). Classical data is never renamed.
-
-.. Basic example
-
-.. jupyter-input::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.passes import PlacementPass
- from pytket.predicates import ConnectivityPredicate
- from pytket.placement import GraphPlacement
-
- circ = Circuit(4, 4)
- circ.H(0).H(1).H(2).V(3)
- circ.CX(0, 1).CX(1, 2).CX(2, 3)
- circ.Rz(-0.37, 3)
- circ.CX(2, 3).CX(1, 2).CX(0, 1)
- circ.H(0).H(1).H(2).Vdg(3)
- circ.measure_all()
-
- backend = IBMQBackend("ibmq_quito")
- place = PlacementPass(GraphPlacement(backend.backend_info.architecture))
- place.apply(circ)
-
- print(circ.get_commands())
- print(ConnectivityPredicate(backend.backend_info.architecture).verify(circ))
-
-.. jupyter-output::
-
- [H node[0];, H node[1];, H node[3];, V node[4];, CX node[0], node[1];, CX node[1], node[3];, CX node[3], node[4];, Rz(3.63*PI) node[4];, CX node[3], node[4];, CX node[1], node[3];, Vdg node[4];, Measure node[4] --> c[3];, CX node[0], node[1];, H node[3];, Measure node[3] --> c[2];, H node[0];, H node[1];, Measure node[0] --> c[0];, Measure node[1] --> c[1];]
- True
-
-In this example, the placement was able to find an exact match for the connectivity onto the device.
-
-.. Sometimes best location is not determined and left to later compilation, leaving partial placement; indicated by "unplaced" register
-
-In some circumstances, the best location is not fully determined immediately and is deferred until later in compilation. This gives rise to a partial placement (the map from logical qubits to physical qubits is a partial function, where undefined qubits are renamed into an ``unplaced`` register).
-
-.. jupyter-input::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.passes import PlacementPass
- from pytket.placement import LinePlacement
-
- circ = Circuit(4)
- circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
-
- backend = IBMQBackend("ibmq_quito")
- place = PlacementPass(LinePlacement(backend.backend_info.architecture))
- place.apply(circ)
-
- print(circ.get_commands())
-
-.. jupyter-output::
-
- [CX node[2], node[1];, CX node[2], node[3];, CX node[1], node[3];, CX unplaced[0], node[3];, CX node[2], unplaced[0];]
-
-.. Define custom placement by providing qubit map
-
-A custom (partial) placement can be applied by providing the appropriate qubit map.
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, Qubit, Node
- from pytket.placement import Placement
-
- circ = Circuit(4)
- circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
-
- q_map = {Qubit(0) : Node(3), Qubit(2) : Node(1)}
- Placement.place_with_map(circ, q_map)
-
- print(circ.get_commands())
-
-A custom placement may also be defined as a pass (which can then be combined with others to construct a more complex pass).
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, Qubit, Node
- from pytket.passes import RenameQubitsPass
-
- circ = Circuit(4)
- circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
-
- q_map = {Qubit(0) : Qubit("z", 0), Qubit(2) : Qubit("z", 1)}
- rename = RenameQubitsPass(q_map)
- rename.apply(circ)
-
- print(circ.get_commands())
-
-.. Existing heuristics: trivial (all "unplaced"), line, graph, noise
-
-Several heuristics have been implemented for identifying candidate placements. For example, :py:class:`~pytket.placement.LinePlacement` will try to identify long paths on the connectivity graph which could be treated as a linear nearest-neighbour system. :py:class:`~pytket.placement.GraphPlacement` will try to identify a subgraph isomorphism between the graph of interacting logical qubits (up to some depth into the :py:class:`~pytket.circuit.Circuit`) and the connectivity graph of the physical qubits. Then :py:class:`~pytket.placement.NoiseAwarePlacement` extends this to break ties in equivalently good graph maps by looking at the error rates of the physical qubits and their couplers. The latter two can be configured using e.g. :py:meth:`~pytket.utils.GraphPlacement.modify_config()` to change parameters like how far into the :py:class:`~pytket.circuit.Circuit` it will look for interacting qubits (trading off time spent searching for the chance to find a better placement).
-
-.. jupyter-input::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.passes import PlacementPass
- from pytket.predicates import ConnectivityPredicate
- from pytket.placement import GraphPlacement
-
- circ = Circuit(5)
- circ.CX(0, 1).CX(1, 2).CX(3, 4)
- circ.CX(0, 1).CX(1, 2).CX(3, 4)
- circ.CX(0, 1).CX(1, 2).CX(3, 4)
- circ.CX(0, 1).CX(1, 2).CX(3, 4)
- circ.CX(0, 1).CX(1, 2).CX(3, 4)
- circ.CX(1, 4) # Extra interaction hidden at higher depth than cutoff
-
- backend = IBMQBackend("ibmq_quito")
- g_pl = GraphPlacement(backend.backend_info.architecture)
- connected = ConnectivityPredicate(backend.backend_info.architecture)
-
- PlacementPass(g_pl).apply(circ)
- print(connected.verify(circ)) # Imperfect placement because the final CX was not considered
-
- # Default depth limit is 5, but there is a new interaction at depth 11
- g_pl.modify_config(depth_limit=11)
-
- PlacementPass(g_pl).apply(circ)
- print(connected.verify(circ)) # Now have an exact placement
-
-.. jupyter-output::
-
- False
- True
-
-.. _compiler-routing:
-
-Mapping
--------
-
-.. Heterogeneous architectures and limited connectivity
-.. Far easier to program correctly when assuming full connectivity
-
-The heterogeneity of quantum architectures and limited connectivity of their qubits impose the strict restriction that multi-qubit gates are only allowed between specific pairs of qubits. Given it is far easier to program a high-level operation which is semantically correct and meaningful when assuming full connectivity, a compiler will have to solve this constraint. In general, there won't be an exact subgraph isomorphism between the graph of interacting logical qubits and the connected physical qubits, so this cannot be solved with placement alone.
-
-.. Invalid interactions between non-local qubits can be sovled by moving qubits to adjacent positions or by performing a distributed operation using the intervening qubits
-.. Routing takes a placed circuit and finds non-local operations, inserting operations to fix them
-
-One solution here, is to scan through the :py:class:`~pytket.circuit.Circuit` looking for invalid interactions. Each of these can be solved by either moving the qubits around on the architecture by adding ``OpType.SWAP`` gates until they are in adjacent locations, or performing a distributed entangling operation using the intervening qubits (such as the "bridged-CX" ``OpType.BRIDGE`` which uses 4 CX gates and a single shared neighbour). The `routing` procedure used in the ``pytket`` ``RoutingPass`` takes a placed :py:class:`~pytket.circuit.Circuit` and inserts gates to reduce non-local operations to sequences of valid local ones.
-
-.. jupyter-input::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.passes import PlacementPass, RoutingPass
- from pytket.placement import GraphPlacement
-
- circ = Circuit(4)
- circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
- backend = IBMQBackend("ibmq_quito")
- PlacementPass(GraphPlacement(backend.backend_info.architecture)).apply(circ)
- print(circ.get_commands()) # One qubit still unplaced
- # node[0] and node[2] are not adjacent
-
- RoutingPass(backend.backend_info.architecture).apply(circ)
- print(circ.get_commands())
-
-.. jupyter-output::
-
- [CX node[1], node[0];, CX node[1], node[2];, CX node[0], node[2];, CX unplaced[0], node[2];, CX node[1], unplaced[0];]
- [CX node[1], node[0];, CX node[1], node[2];, SWAP node[0], node[1];, CX node[1], node[2];, SWAP node[1], node[3];, CX node[1], node[2];, CX node[0], node[1];]
-
-.. Given partial placements, selects physical qubits on the fly
-.. Due to swap insertion, logical qubits may be mapped to different physical qubits at the start and end of the circuit
-
-As shown here, if a partial placement is used, the routing procedure will allocate the remaining qubits dynamically. We also see that the logical qubits are mapped to different physical qubits at the start and end because of the inserted ``OpType.SWAP`` gates, such as ``q[1]`` starting at ``node[0]`` and ending at ``node[3]``.
-
-.. Other Routing options
-
-``RoutingPass`` only provides the default option for mapping to physical circuits. The decision making used in Routing is defined in the ``RoutgMethod`` class and the choice of ``RoutingMethod`` used can be defined in the ``FullMappingPass`` compiler pass for producing physical circuits.
-
-Decomposing Structures
-----------------------
-
-.. Box structures for high-level operations need to be mapped to low-level gates
-.. Unwraps `CircuitBox`es, decomposes others into known, efficient patterns
-
-The numerous Box structures in ``pytket`` provide practical abstractions for high-level operations to assist in :py:class:`~pytket.circuit.Circuit` construction, but need to be mapped to low-level gates before we can run the :py:class:`~pytket.circuit.Circuit`. The :py:class:`~pytket.passes.DecomposeBoxes` pass will unwrap any :py:class:`~pytket.circuit.CircBox`, substituting it for the corresponding :py:class:`~pytket.circuit.Circuit`, and decompose others like the :py:class:`~pytket.circuit.Unitary1qBox` and :py:class:`~pytket.circuit.PauliExpBox` into efficient templated patterns of gates.
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, CircBox, PauliExpBox
- from pytket.passes import DecomposeBoxes
- from pytket.pauli import Pauli
-
- sub = Circuit(2)
- sub.CZ(0, 1).T(0).Tdg(1)
- sub_box = CircBox(sub)
- circ = Circuit(4)
- circ.Rx(0.42, 2).CX(2, 0)
- circ.add_gate(sub_box, [0, 1])
- circ.add_gate(sub_box, [2, 3])
- circ.add_gate(PauliExpBox([Pauli.X, Pauli.Y, Pauli.Y, Pauli.Y], 0.2), [0, 1, 2, 3])
-
- DecomposeBoxes().apply(circ)
- print(circ.get_commands())
-
-.. This could introduce undetermined structures to the circuit, invalidating gate set, connectivity, and other crucial requirements of the backend, so recommended to be performed early in the compilation procedure, allowing for these requirements to be solved again
-
-Unwrapping Boxes could introduce arbitrarily complex structures into a :py:class:`~pytket.circuit.Circuit` which could possibly invalidate almost all :py:class:`~pytket.predicates.Predicate` s, including :py:class:`GateSetPredicate`, :py:class:`~pytket.predicates.ConnectivityPredicate`, and :py:class:`NoMidMeasurePredicate`. It is hence recommended to apply this early in the compilation procedure, prior to any pass that solves for these constraints.
-
-Optimisations
--------------
-
-Having covered the primary goal of compilation and reduced our :py:class:`~pytket.circuit.Circuit` s to a form where they can be run, we find that there are additional techniques we can use to obtain more reliable results by reducing the noise and probability of error. Most :py:class:`~pytket.circuit.Circuit` optimisations follow the mantra of "fewer expensive resources gives less opportunity for noise to creep in", whereby if we find an alternative :py:class:`~pytket.circuit.Circuit` that is observationally equivalent in a perfect noiseless setting but uses fewer resources (gates, time, ancilla qubits) then it is likely to perform better in a noisy context (though not always guaranteed).
-
-.. Generic peephole - "looking for specific patterns of gates"; may take into account local commutations
-.. Examples describing `RemoveRedundancies`, `EulerAngleReduction`, `KAKDecomposition`, and `CliffordSimp`
-
-If we have two :py:class:`~pytket.circuit.Circuit` s that are observationally equivalent, we know that replacing one for the other in any context also gives something that is observationally equivalent. The simplest optimisations will take an inefficient pattern, find all matches in the given :py:class:`~pytket.circuit.Circuit` and replace them by the efficient alternative. A good example from this class of `peephole` optimisations is the :py:class:`~pytket.passes.RemoveRedundancies` pass, which looks for a number of easy-to-spot redundant gates, such as zero-parameter rotation gates, gate-inverse pairs, adjacent rotation gates in the same basis, and diagonal rotation gates followed by measurements.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.passes import RemoveRedundancies
-
- circ = Circuit(3, 3)
- circ.Rx(0.92, 0).CX(1, 2).Rx(-0.18, 0) # Adjacent Rx gates can be merged
- circ.CZ(0, 1).Ry(0.11, 2).CZ(0, 1) # CZ is self-inverse
- circ.XXPhase(0.6, 0, 1)
- circ.YYPhase(0, 0, 1) # 0-angle rotation does nothing
- circ.ZZPhase(-0.84, 0, 1)
- circ.Rx(0.03, 0).Rz(-0.9, 1).measure_all() # Effect of Rz is eliminated by measurement
-
- RemoveRedundancies().apply(circ)
- print(circ.get_commands())
-
-It is understandable to question the relevance of such an optimisation, since a sensible programmer would not intentionally write a :py:class:`~pytket.circuit.Circuit` with such redundant gates. These are still largely useful because other compiler passes might introduce them, such as routing adding a ``OpType.SWAP`` gate immediately following a ``OpType.SWAP`` gate made by the user, or commuting a Z-rotation through the control of a CX which allows it to merge with another Z-rotation on the other side.
-
-Previous iterations of the :py:class:`~pytket.passes.CliffordSimp` pass would work in this way as well, looking for specific sequences of Clifford gates where we could reduce the number of two-qubit gates. This has since been generalised to spot these patterns up to gate commutations and changes of basis from single-qubit Clifford rotations.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.passes import CliffordSimp
-
- # A basic inefficient pattern can be reduced by 1 CX
- simple_circ = Circuit(2)
- simple_circ.CX(0, 1).S(1).CX(1, 0)
-
- CliffordSimp().apply(simple_circ)
- print(simple_circ.get_commands())
-
- # The same pattern, up to commutation and local Clifford algebra
- complex_circ = Circuit(3)
- complex_circ.CX(0, 1)
- complex_circ.Rx(0.42, 1)
- complex_circ.S(1)
- complex_circ.YYPhase(0.96, 1, 2) # Requires 2 CXs to implement
- complex_circ.CX(0, 1)
-
- CliffordSimp().apply(complex_circ)
- print(complex_circ.get_commands())
-
-The next step up in scale has optimisations based on optimal decompositions of subcircuits over :math:`n`-qubits, including :py:class:`~pytket.passes.EulerAngleReduction` for single-qubit unitary chains (producing three rotations in a choice of axes), and :py:class:`~pytket.passes.KAKDecomposition` for two-qubit unitaries (using at most three CXs and some single-qubit gates).
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.passes import EulerAngleReduction, KAKDecomposition
-
- circ = Circuit(2)
- circ.CZ(0, 1)
- circ.Rx(0.4, 0).Ry(0.289, 0).Rx(-0.34, 0).Ry(0.12, 0).Rx(-0.81, 0)
- circ.CX(1, 0)
-
- # Reduce long chain to a triple of Ry, Rx, Ry
- EulerAngleReduction(OpType.Rx, OpType.Ry).apply(circ)
- print(circ.get_commands())
-
- circ = Circuit(3)
- circ.CX(0, 1)
- circ.CX(1, 2).Rx(0.3, 1).CX(1, 2).Rz(1.5, 2).CX(1, 2).Ry(-0.94, 1).Ry(0.37, 2).CX(1, 2)
- circ.CX(1, 0)
-
- # Reduce long 2-qubit subcircuit to at most 3 CXs
- KAKDecomposition().apply(circ)
- print(circ.get_commands())
-
-.. Situational macroscopic - identifies large structures in circuit or converts circuit to alternative algebraic representation; use properties of the structures to find simplifications; resynthesise into basic gates
-.. Examples describing `PauliSimp`
-
-All of these so far are generic optimisations that work for any application, but only identify local redundancies since they are limited to working up to individual gate commutations. Other techniques instead focus on identifying macroscopic structures in a :py:class:`~pytket.circuit.Circuit` or convert it entirely into an alternative algebraic representation, and then using the properties of the structures/algebra to find simplifications and resynthesise into basic gates. For example, the :py:class:`~pytket.passes.PauliSimp` pass will represent the entire :py:class:`~pytket.circuit.Circuit` as a sequence of exponentials of Pauli operators, capturing the effects of non-Clifford gates as rotations in a basis determined by the Clifford gates. This abstracts away any redundant information in the Clifford gates entirely, and can be used to merge non-Clifford gates that cannot be brought together from any sequence of commutations, as well as finding efficient Clifford constructions for the basis changes.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.passes import PauliSimp
- from pytket.utils import Graph
-
- circ = Circuit(3)
- circ.Rz(0.2, 0)
- circ.Rx(0.35, 1)
- circ.V(0).H(1).CX(0, 1).CX(1, 2).Rz(-0.6, 2).CX(1, 2).CX(0, 1).Vdg(0).H(1)
- circ.H(1).H(2).CX(0, 1).CX(1, 2).Rz(0.8, 2).CX(1, 2).CX(0, 1).H(1).H(2)
- circ.Rx(0.1, 1)
-
- PauliSimp().apply(circ)
- Graph(circ).get_DAG()
-
-.. May not always improve the circuit if it doesn't match the structures it was designed to exploit, and the large structural changes from resynthesis could make routing harder
-
-This can give great benefits for :py:class:`~pytket.circuit.Circuit` s where non-Clifford gates are sparse and there is hence a lot of redundancy in the Clifford change-of-basis sections. But if the :py:class:`~pytket.circuit.Circuit` already has a very efficient usage of Clifford gates, this will be lost when converting to the abstract representation, and so the resynthesis is likely to give less efficient sequences. The large structural changes from abstraction and resynthesis can also make routing harder to perform as the interaction graph of the logical qubits can drastically change. The effectiveness of such optimisations depends on the situation, but can be transformative under the right circumstances.
-
-Some of these optimisation passes have optional parameters to customise the routine slightly. A good example is adapting the :py:class:`~pytket.passes.PauliSimp` pass to have a preference for different forms of ``OpType.CX`` decompositions. Setting the ``cx_config`` option to ``CXConfigType.Snake`` (default) will prefer chains of gates where the target of one becomes the control of the next, whereas ``CXConfigType.Star`` prefers using a single qubit as the control for many gates, and ``CXConfigType.Tree`` introduces entanglement in a balanced tree form. Each of these has its own benefits and drawbacks that could make it more effective for a particular routine, like ``CXConfigType.Snake`` giving circuits that are easier to route on linear nearest-neighbour architectures, ``CXConfigType.Star`` allowing any of the gates to commute through to cancel out with others at the start or end of the sequence, and ``CXConfigType.Tree`` giving optimal depth on a fully-connected device.
-
-.. jupyter-execute::
-
- from pytket.circuit import Circuit, PauliExpBox
- from pytket.passes import PauliSimp
- from pytket.pauli import Pauli
- from pytket.transform import CXConfigType
- from pytket.utils import Graph
-
- pauli_XYXZYXZZ = PauliExpBox([Pauli.X, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Z], 0.42)
-
- circ = Circuit(8)
- circ.add_gate(pauli_XYXZYXZZ, [0, 1, 2, 3, 4, 5, 6, 7])
-
- PauliSimp(cx_config=CXConfigType.Snake).apply(circ)
- print(circ.get_commands())
- Graph(circ).get_qubit_graph()
-
-.. jupyter-execute::
-
- PauliSimp(cx_config=CXConfigType.Star).apply(circ)
- print(circ.get_commands())
- Graph(circ).get_qubit_graph()
-
-.. jupyter-execute::
-
- PauliSimp(cx_config=CXConfigType.Tree).apply(circ)
- print(circ.get_commands())
- Graph(circ).get_qubit_graph()
-
-Combinators
------------
-
-.. Passes are building blocks that can be composed into more sophisticated strategies encapsulating the full compilation flow
-.. Basic sequencing
-
-The passes encountered so far represent elementary, self-contained transformations on :py:class:`~pytket.circuit.Circuit` s. In practice, we will almost always want to apply sequences of these to combine optimisations with solving for many constraints. The passes in ``pytket`` have a rudimentary compositional structure to describe generic compilation strategies, with the most basic example being just applying a list of passes in order.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.passes import auto_rebase_pass, EulerAngleReduction, SequencePass
-
- rebase_quil = auto_rebase_pass({OpType.CZ, OpType.Rz, OpType.Rx})
- circ = Circuit(3)
- circ.CX(0, 1).Rx(0.3, 1).CX(2, 1).Rz(0.8, 1)
- comp = SequencePass([rebase_quil, EulerAngleReduction(OpType.Rz, OpType.Rx)])
- comp.apply(circ)
- print(circ.get_commands())
-
-.. Repeat passes until no further change - useful when one pass can enable further matches for another type of optimisation
-
-When composing optimisation passes, we may find that applying one type of optimisation could open up opportunities for others by, for example, rearranging gates to match the desired template. To make the most of this, it may be beneficial to apply some pass combination repeatedly until no further changes are made, i.e. until we have found and exploited every simplification that we can.
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatPass, SequencePass
-
- circ = Circuit(4)
- circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
- comp = RepeatPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]))
- comp.apply(circ)
- print(circ.get_commands())
-
-.. warning:: This looping mechanism does not directly compare the :py:class:`~pytket.circuit.Circuit` to its old state from the previous iteration, instead checking if any of the passes within the loop body claimed they performed any rewrite. Some sequences of passes will do and undo some changes to the :py:class:`~pytket.circuit.Circuit`, giving no net effect but nonetheless causing the loop to repeat. This can lead to infinite loops if used in such a way. Some passes where the :py:class:`~pytket.circuit.Circuit` is converted to another form and back again (e.g. :py:class:`~pytket.passes.PauliSimp`) will always report that a change took place. We recommend testing any looping passes thoroughly to check for termination.
-
-.. Repeat with metric - useful when hard to tell when a change is being made or you only care about specific changes
-
-Increased termination safety can be given by only repeating whilst some easy-to-check metric (such as number of gates or depth) decreases. For example, we may want to try to minimise the number of ``OpType.CX`` gates since these will tend to be very slow and noisy on a lot of devices.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
- from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatWithMetricPass, SequencePass
-
- circ = Circuit(4)
- circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
- cost = lambda c : c.n_gates_of_type(OpType.CX)
- comp = RepeatWithMetricPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]), cost)
- comp.apply(circ) # Stops earlier than before, since removing CYs doesn't change the number of CXs
- print(circ.get_commands())
-
-.. May reject compositions if pre/post-conditions don't match up; some passes will fail to complete or fail to achieve their objective if a circuit does not match their pre-conditions, so we prevent compositions where the latter's pre-conditions cannot be guaranteed
-
-We mentioned earlier that each pass has a set of pre-conditions and post-conditions expressed via :py:class:`~pytket.predicates.Predicate` s. We may find that applying one pass invalidates the pre-conditions of a later pass, meaning it may hit an error when applied to a :py:class:`~pytket.circuit.Circuit`. For example, the :py:class:`~pytket.passes.KAKDecomposition` optimisation method can only operate on :py:class:`~pytket.circuit.Circuit` s with a specific gate set which doesn't allow for any gates on more than 2 qubits, so when :py:class:`~pytket.passes.RoutingPass` can introduce ``OpType.BRIDGE`` gates over 3 qubits, this could cause an error when trying to apply :py:class:`~pytket.passes.KAKDecomposition`. When using combinators like :py:class:`~pytket.passes.SequencePass` and :py:class:`~pytket.passes.RepeatPass`, ``pytket`` checks that the passes are safe to compose, in the sense that former passes do not invalidate pre-conditions of the latter passes. This procedure uses a basic form of Hoare logic to identify new pre- and post-conditions for the combined pass and identify whether it is still satisfiable.
-
-.. Warning about composing with `DecomposeBoxes`
-
-A special mention here goes to the :py:class:`~pytket.passes.DecomposeBoxes` pass. Because the Box structures could potentially contain arbitrary sequences of gates, there is no guarantee that expanding them will yield a :py:class:`~pytket.circuit.Circuit` that satisfies `any` :py:class:`~pytket.predicates.Predicate`. Since it has potential to invalidate the pre-conditions of any subsequent pass, composing it with anything else `will` generate such an error.
-
-.. jupyter-execute::
- :raises: RuntimeError
-
- from pytket.passes import DecomposeBoxes, PauliSimp, SequencePass
- # PauliSimp requires a specific gateset and no conditional gates
- # or mid-circuit measurement, so this will raise an exception
- comp = SequencePass([DecomposeBoxes(), PauliSimp()])
-
-Predefined Sequences
----------------------
-
-Knowing what sequences of compiler passes to apply for maximal performance is often a very hard problem and can require a lot of experimentation and intuition to predict reliably. Fortunately, there are often common patterns that are applicable to virtually any scenario, for which ``pytket`` provides some predefined sequences.
-
-.. `FullPeepholeOptimise` kitchen-sink, but assumes a universal quantum computer
-
-In practice, peephole and structure-preserving optimisations are almost always strictly beneficial to apply, or at least will never increase the size of the :py:class:`~pytket.circuit.Circuit`. The :py:class:`~pytket.passes.FullPeepholeOptimise` pass applies Clifford simplifications, commutes single-qubit gates to the front of the circuit and applies passes to squash subcircuits of up to three qubits. This provides a one-size-approximately-fits-all "kitchen sink" solution to :py:class:`~pytket.circuit.Circuit` optimisation. This assumes no device constraints by default, so will not generally preserve gateset, connectivity, etc.
-
-When targeting a heterogeneous device architecture, solving this constraint in its entirety will generally require both placement and subsequent routing. :py:class:`DefaultMappingPass` simply combines these to apply the :py:class:`GraphPlacement` strategy and solve any remaining invalid multi-qubit operations. This is taken a step further with :py:class:`~pytket.passes.CXMappingPass` which also decomposes the introduced ``OpType.SWAP`` and ``OpType.BRIDGE`` gates into elementary ``OpType.CX`` gates.
-
-.. `Synthesise<>` passes combine light optimisations that preserve qubit connectivity and target a specific gate set
-
-After solving for the device connectivity, we then need to restrict what optimisations we can apply to those that won't invalidate this. The set of :py:class:`SynthesiseX` passes combine light optimisations that preserve the qubit connectivity and target a specific final gate set (e.g. :py:class:`SynthesiseTket` guarantees the output is in the gateset of ``OpType.CX``, ``OpType.TK1``, and ``OpType.Measure``). In general, this will not reduce the size of a :py:class:`~pytket.circuit.Circuit` as much as :py:class:`~pytket.passes.FullPeepholeOptimise`, but has the benefit of removing some redundancies introduced by routing without invalidating it.
-
-.. jupyter-input::
-
- from pytket import Circuit, OpType
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.passes import FullPeepholeOptimise, DefaultMappingPass, SynthesiseTket, RebaseTket
-
- circ = Circuit(5)
- circ.CX(0, 1).CX(0, 2).CX(0, 3)
- circ.CZ(0, 1).CZ(0, 2).CZ(0, 3)
- circ.CX(3, 4).CX(0, 3).CX(4, 0)
-
- RebaseTket().apply(circ) # Get number of 2qb gates by converting all to CX
- print(circ.n_gates_of_type(OpType.CX))
-
- FullPeepholeOptimise().apply(circ) # Freely rewrite circuit
- print(circ.n_gates_of_type(OpType.CX))
-
- backend = IBMQBackend("ibmq_quito")
- DefaultMappingPass(backend.backend_info.architecture).apply(circ)
- RebaseTket().apply(circ)
- print(circ.n_gates_of_type(OpType.CX)) # Routing adds gates
- print(circ.get_commands())
-
- SynthesiseTket().apply(circ) # Some added gates may be redundant
- print(circ.n_gates_of_type(OpType.CX)) # But not in this case
-
-.. jupyter-output::
-
- 9
- 6
- 9
- [tk1(0, 0, 1.5) node[0];, tk1(0, 0, 1.5) node[1];, tk1(0, 0, 1.5) node[2];, tk1(0, 0, 1.5) node[3];, CX node[1], node[0];, tk1(0, 0, 0.5) node[0];, CX node[1], node[2];, CX node[1], node[3];, tk1(0, 0, 0.5) node[2];, tk1(0, 0, 0.5) node[3];, CX node[3], node[4];, CX node[1], node[3];, CX node[3], node[4];, CX node[4], node[3];, CX node[3], node[4];, CX node[3], node[1];]
- 9
-
-
-.. Note::
- :py:class:`~pytket.passes.FullPeepholeOptimise` takes an optional ``allow_swaps`` argument. This is a boolean flag to indicate whether :py:class:`~pytket.passes.FullPeepholeOptimise` should preserve the circuit connectivity or not. If set to ``False`` the pass will presrve circuit connectivity but the circuit will generally be less optimised than if connectivity was ignored.
-
- :py:class:`~pytket.passes.FullPeepholeOptimise` also takes an optional ``target_2qb_gate`` argument to specify whether to target the {:py:class:`OpType.TK1`, :py:class:`OpType.CX`} or {:py:class:`OpType.TK1`, :py:class:`OpType.TK2`} gateset.
-
-.. Note::
- Prevous versions of :py:class:`~pytket.passes.FullPeepholeOptimise` did not apply the :py:class:`~pytket.passes.ThreeQubitSquash` pass. There is a :py:class:`~pytket.passes.PeepholeOptimise2Q` pass which applies the old pass sequence with the :py:class:`~pytket.passes.ThreeQubitSquash` pass excluded.
-
-.. `Backend.default_compilation_pass` gives a recommended compiler pass to solve the backend's constraints with little or light optimisation
-
-Also in this category of pre-defined sequences, we have the :py:meth:`~pytket.backends.Backend.default_compilation_pass()` which is run by :py:meth:`~pytket.backends.Backend.get_compiled_circuit`. These give a recommended compiler pass to solve the :py:class:`~pytket.backends.Backend` s constraints with a choice of optimisation levels.
-
-================== ========================================================================================================
-Optimisation level Description
-================== ========================================================================================================
-0 Just solves the constraints as simply as possible. No optimisation.
-1 Adds basic optimisations (those covered by the :py:class:`SynthesiseX` passes) for efficient compilation.
-2 Extends to more intensive optimisations (those covered by the :py:class:`~pytket.passes.FullPeepholeOptimise` pass).
-================== ========================================================================================================
-
-We will now demonstrate the :py:meth:`~pytket.backends.Backend.default_compilation_pass` with the different levels of optimisation using the ``ibmq_quito`` device.
-
-As more intensive optimisations are applied by level 2 the pass may take a long to run for large circuits. In this case it may be preferable to apply the lighter optimisations of level 1.
-
-.. jupyter-input::
-
- from pytket import Circuit, OpType
- from pytket.extensions.qiskit import IBMQBackend
-
- circ = Circuit(3) # Define a circuit to be compiled to the backend
- circ.CX(0, 1)
- circ.H(1)
- circ.Rx(0.42, 1)
- circ.S(1)
- circ.CX(0, 2)
- circ.CX(2, 1)
- circ.Z(2)
- circ.Y(1)
- circ.CX(0, 1)
- circ.CX(2, 0)
- circ.measure_all()
-
- backend = IBMQBackend("ibmq_quito") # Initialise Backend
-
- print("Total gate count before compilation =", circ.n_gates)
- print("CX count before compilation =", circ.n_gates_of_type(OpType.CX))
-
- # Now apply the default_compilation_pass at different levels of optimisation.
-
- for ol in range(3):
- test_circ = circ.copy()
- backend.default_compilation_pass(optimisation_level=ol).apply(test_circ)
- assert backend.valid_circuit(test_circ)
- print("Optimisation level", ol)
- print("Gates", test_circ.n_gates)
- print("CXs", test_circ.n_gates_of_type(OpType.CX))
-
-.. jupyter-output::
-
- Total gate count before compilation = 13
- CX count before compilation = 5
- Optimisation level 0
- Gates 22
- CXs 8
- Optimisation level 1
- Gates 12
- CXs 5
- Optimisation level 2
- Gates 6
- CXs 1
-
-**Explanation**
-
-We see that compiling the circuit to ``ibmq_quito`` at optimisation level 0 actually increases the gate count. This is because ``ibmq_quito`` has connectivity constraints which require additional CX gates to be added to validate the circuit.
-The single-qubit gates in our circuit also need to be decomposed into the IBM gatset.
-
-We see that compiling at optimisation level 1 manages to reduce the CX count to 5. Our connectivity constraints are satisfied without increasing the CX gate count. Single-qubit gates are also combined to reduce the overall gate count further.
-
-Finally we see that the default pass for optimisation level 2 manages to reduce the overall gate count to just 6 with only one CX gate. This is because more intensive optimisations are applied at this level including squashing passes that enable optimal two and three-qubit circuits to be synthesised. Applying these more powerful passes comes with a runtime overhead that may be noticeable for larger circuits.
-
-Guidance for Combining Passes
------------------------------
-
-.. More powerful optimisations tend to have fewer guarantees on the structure of the output, so advisable to perform before trying to satisfy device constraints
-
-We find that the most powerful optimisation techniques (those that have the potential to reduce :py:class:`~pytket.circuit.Circuit` size the most for some class of :py:class:`~pytket.circuit.Circuit` s) tend to have fewer guarantees on the structure of the output, requiring a universal quantum computer with the ability to perform any gates on any qubits. It is recommended to apply these early on in compilation.
-
-.. Solving some device constraints might invalidate others, such as routing invalidating `NoMidMeasurePredicate` and `GateSetPredicate`
-
-The passes to solve some device constraints might invalidate others: for example, the :py:class:`RoutingPass` generally invalidates :py:class:`NoMidMeasurePredicate` and :py:class:`GateSetPredicate`. Therefore, the order in which these are solved should be chosen with care.
-
-.. Recommended order of decompose boxes, strong optimisations, placement, routing, delay measures, rebase; could insert minor optimisations between each step to tidy up any redundancies introduced as long as they preserve solved constraints
-
-For most standard use cases, we recommend starting with :py:class:`~pytket.passes.DecomposeBoxes` to reduce the :py:class:`~pytket.circuit.Circuit` down to primitive gates, followed by strong optimisation passes like :py:class:`PauliSimp` (when appropriate for the types of :py:class:`~pytket.circuit.Circuit` s being considered) and :py:class:`~pytket.passes.FullPeepholeOptimise` to eliminate a large number of redundant operations. Then start to solve some more device constraints with some choice of placement and routing strategy, followed by :py:class:`DelayMeasures` to push measurements back through any introduced ``OpType.SWAP`` or ``OpType.BRIDGE`` gates, and then finally rebase to the desired gate set. The :py:meth:`~pytket.backends.Backend.default_compilation_pass()` definitions can replace this sequence from placement onwards for simplicity. Minor optimisations could also be inserted between successive steps to tidy up any redundancies introduced, as long as they preserve the solved constraints.
-
-Initial and Final Maps
-----------------------
-
-.. Placement, routing, and other passes can change the names of qubits; the map from logical to physical qubits can be different at the start and end of the circuit; define initial and final maps
-.. Can use this to identify what placement was selected or how to interpret the final state
-
-:py:class:`PlacementPass` modifies the set of qubits used in the :py:class:`~pytket.circuit.Circuit` from the logical names used during construction to the names of the physical addresses on the :py:class:`~pytket.backends.Backend`, so the logical qubit names wiil no longer exist within the :py:class:`~pytket.circuit.Circuit` by design. Knowing the map between the logical qubits and the chosen physical qubits is necessary for understanding the choice of placement, interpreting the final state from a naive simulator, identifying which physical qubits each measurement was made on for error mitigation, and appending additional gates to the logical qubits after applying the pass.
-
-Other passes like :py:class:`RoutingPass` and :py:class:`~pytket.passes.CliffordSimp` can introduce (explicit or implicit) permutations of the logical qubits in the middle of a :py:class:`~pytket.circuit.Circuit`, meaning a logical qubit may exist on a different physical qubit at the start of the :py:class:`~pytket.circuit.Circuit` compared to the end.
-
-.. Encapsulating a circuit in a `CompilationUnit` allows the initial and final maps to be tracked when a pass is applied
-
-We can wrap up a :py:class:`~pytket.circuit.Circuit` in a :py:class:`~pytket.predicates.CompilationUnit` to allow us to track any changes to the locations of the logical qubits when passes are applied. The :py:attr:`CompilationUnit.initial_map` is a dictionary mapping the original :py:class:`UnitID` s to the corresponding :py:class:`UnitID` used in :py:attr:`CompilationUnit.circuit`, and similarly :py:attr:`CompilationUnit.final_map` for outputs. Applying :py:meth:`BasePass.apply()` to a :py:class:`~pytket.predicates.CompilationUnit` will apply the transformation to the underlying :py:class:`~pytket.circuit.Circuit` and track the changes to the initial and final maps.
-
-.. jupyter-input::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.passes import DefaultMappingPass
- from pytket.predicates import CompilationUnit
-
- circ = Circuit(5, 5)
- circ.CX(0, 1).CX(0, 2).CX(0, 3).CX(0, 4).measure_all()
- backend = IBMQBackend("ibmq_quito")
- cu = CompilationUnit(circ)
- DefaultMappingPass(backend.backend_info.architecture).apply(cu)
- print(cu.circuit.get_commands())
- print(cu.initial_map)
- print(cu.final_map)
-
-.. jupyter-output::
-
- [CX node[1], node[0];, Measure node[0] --> c[1];, CX node[1], node[2];, Measure node[2] --> c[2];, CX node[1], node[3];, Measure node[3] --> c[3];, SWAP node[1], node[3];, CX node[3], node[4];, Measure node[3] --> c[0];, Measure node[4] --> c[4];]
- {c[0]: c[0], c[1]: c[1], c[2]: c[2], c[3]: c[3], c[4]: c[4], q[0]: node[1], q[1]: node[0], q[2]: node[2], q[3]: node[3], q[4]: node[4]}
- {c[0]: c[0], c[1]: c[1], c[2]: c[2], c[3]: c[3], c[4]: c[4], q[0]: node[3], q[1]: node[0], q[2]: node[2], q[3]: node[1], q[4]: node[4]}
-
-.. note:: No passes currently rename or swap classical data, but the classical bits are included in these maps for completeness.
-
-Advanced Compilation Topics
----------------------------
-
-Compiling Symbolic Circuits
-===========================
-
-.. Defining a single symbolic circuit and instantiating it multiple times saves effort in circuit construction, and means the circuit only has to be compiled once, saving time or allowing more expensive optimisations to be considered
-
-For variational algorithms, the prominent benefit of defining a :py:class:`~pytket.circuit.Circuit` symbolically and only instantiating it with concrete values when needed is that the compilation procedure would only need to be performed once. By saving time here we can cut down the overall time for an experiment; we could invest the time saved into applying more expensive optimisations on the :py:class:`~pytket.circuit.Circuit` to reduce the impact of noise further.
-
-.. Example with variational optimisation using statevector simulator
-
-.. jupyter-execute::
-
- from pytket import Circuit, Qubit
- from pytket.extensions.qiskit import AerStateBackend
- from pytket.pauli import Pauli, QubitPauliString
- from pytket.utils.operators import QubitPauliOperator
- from sympy import symbols
-
- a, b = symbols("a b")
- circ = Circuit(2)
- circ.Ry(a, 0)
- circ.Ry(a, 1)
- circ.CX(0, 1)
- circ.Rz(b, 1)
- circ.CX(0, 1)
- xx = QubitPauliString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
- op = QubitPauliOperator({xx : 1.5})
-
- backend = AerStateBackend()
-
- # Compile once outside of the objective function
- circ = backend.get_compiled_circuit(circ)
-
- def objective(params):
- state = circ.copy()
- state.symbol_substitution({a : params[0], b : params[1]})
- handle = backend.process_circuit(state) # No need to compile again
- vec = backend.get_result(handle).get_state()
- return op.state_expectation(vec)
-
- print(objective([0.25, 0.5]))
- print(objective([0.5, 0]))
-
-.. Warning about `NoSymbolsPredicate` and necessity of instantiation before running on backends
-
-.. note:: Every :py:class:`~pytket.backends.Backend` requires :py:class:`NoSymbolsPredicate`, so it is necessary to instantiate all symbols before running a :py:class:`~pytket.circuit.Circuit`.
-
-User-defined Passes
-===================
-
-We have already seen that pytket allows users to combine passes in a desired order using :py:class:`~pytket.passes.SequencePass`. An addtional feature is the :py:class:`~pytket.passes.CustomPass` which allows users to define their own custom circuit transformation using pytket.
-The :py:class:`~pytket.passes.CustomPass` class accepts a ``transform`` parameter, a python function that takes a :py:class:`~pytket.circuit.Circuit` as input and returns a :py:class:`~pytket.circuit.Circuit` as output.
-
-We will show how to use :py:class:`~pytket.passes.CustomPass` by defining a simple transformation that replaces any Pauli Z gate in the :py:class:`~pytket.circuit.Circuit` with a Hadamard gate, Pauli X gate, Hadamard gate chain.
-
-.. jupyter-execute::
-
- from pytket import Circuit, OpType
-
- def z_transform(circ: Circuit) -> Circuit:
- n_qubits = circ.n_qubits
- circ_prime = Circuit(n_qubits) # Define a replacement circuit
-
- for cmd in circ.get_commands():
- qubit_list = cmd.qubits # Qubit(s) our gate is applied on (as a list)
- if cmd.op.type == OpType.Z:
- # If cmd is a Z gate, decompose to a H, X, H sequence.
- circ_prime.add_gate(OpType.H, qubit_list)
- circ_prime.add_gate(OpType.X, qubit_list)
- circ_prime.add_gate(OpType.H, qubit_list)
- else:
- # Otherwise, apply the gate as usual.
- circ_prime.add_gate(cmd.op.type, cmd.op.params, qubit_list)
-
- return circ_prime
-
-After we've defined our ``transform`` we can construct a :py:class:`~pytket.passes.CustomPass`. This pass can then be applied to a :py:class:`~pytket.circuit.Circuit`.
-
-.. jupyter-execute::
-
- from pytket.passes import CustomPass
-
- DecompseZPass = CustomPass(z_transform) # Define our pass
-
- test_circ = Circuit(2) # Define a test Circuit for our pass
- test_circ.Z(0)
- test_circ.Z(1)
- test_circ.CX(0, 1)
- test_circ.Z(1)
- test_circ.CRy(0.5, 0, 1)
-
- DecompseZPass.apply(test_circ) # Apply our pass to the test Circuit
-
- test_circ.get_commands() # Commands of our transformed Circuit
-
-We see from the output above that our newly defined :py:class:`DecompseZPass` has successfully decomposed the Pauli Z gates to Hadamard, Pauli X, Hadamard chains and left other gates unchanged.
-
-.. warning::
- pytket does not require that :py:class:`~pytket.passes.CustomPass` preserves the unitary of the :py:class:`~pytket.circuit.Circuit` . This is for the user to ensure.
-
-
-Partial Compilation
-===================
-
-.. Commonly want to run many circuits that have large identical regions; by splitting circuits into regions, can often compile individually and compose to speed up compilation time
-
-A common pattern across expectation value and tomography experiments is to run many :py:class:`~pytket.circuit.Circuit` s that have large identical regions, such as a single state preparation with many different measurements. We can further speed up the overall compilation time by splitting up the state preparation from the measurements, compiling each subcircuit only once, and composing together at the end.
-
-.. Only have freedom to identify good placements for the first subcircuit to be run, the rest are determined by final maps in order to compose well
-
-The main technical consideration here is that the compiler will only have the freedom to identify good placements for the first subcircuit to be run. This means that the state preparation should be compiled first, and the placement for the measurements is given by the final map in order to compose well.
-
-Once compiled, we can use :py:meth:`~pytket.backends.Backend.process_circuits` to submit several circuits at once for execution on the backend. The circuits to be executed are passed as list. If the backend is shot-based, the number of shots can be passed using the `n_shots` parameter, which can be a single integer or a list of integers of the same length as the list of circuits to be executed. In the following example, 4000 shots are measured for the first circuit and 2000 for the second.
-
-.. Example of state prep with many measurements; compile state prep once, inspect final map, use this as placement for measurement circuits and compile them, then compose
-
-.. jupyter-input::
-
- from pytket import Circuit, OpType
- from pytket.extensions.qiskit import IBMQBackend
- from pytket.predicates import CompilationUnit
- from pytket.placement import Placement
-
- state_prep = Circuit(4)
- state_prep.H(0)
- state_prep.add_gate(OpType.CnRy, 0.1, [0, 1])
- state_prep.add_gate(OpType.CnRy, 0.2, [0, 2])
- state_prep.add_gate(OpType.CnRy, 0.3, [0, 3])
-
- measure0 = Circuit(4, 4)
- measure0.H(1).H(3).measure_all()
- measure1 = Circuit(4, 4)
- measure1.CX(1, 2).CX(3, 2).measure_all()
-
- backend = IBMQBackend("ibmq_quito")
- cu = CompilationUnit(state_prep)
- backend.default_compilation_pass().apply(cu)
- Placement.place_with_map(measure0, cu.final_map)
- Placement.place_with_map(measure1, cu.final_map)
- backend.default_compilation_pass().apply(measure0)
- backend.default_compilation_pass().apply(measure1)
-
- circ0 = cu.circuit
- circ1 = circ0.copy()
- circ0.append(measure0)
- circ1.append(measure1)
- handles = backend.process_circuits([circ0, circ1], n_shots=[4000, 2000])
- r0, r1 = backend.get_results(handles)
- print(r0.get_counts())
- print(r1.get_counts())
-
-.. jupyter-output::
-
- {(0, 0, 0, 0): 503, (0, 0, 0, 1): 488, (0, 1, 0, 0): 533, (0, 1, 0, 1): 493, (1, 0, 0, 0): 1041, (1, 0, 0, 1): 107, (1, 0, 1, 0): 115, (1, 0, 1, 1): 14, (1, 1, 0, 0): 576, (1, 1, 0, 1): 69, (1, 1, 1, 0): 54, (1, 1, 1, 1): 7}
- {(0, 0, 0, 0): 2047, (0, 1, 0, 0): 169, (0, 1, 1, 0): 1729, (1, 1, 0, 0): 7, (1, 1, 1, 0): 48}
-
-Measurement Reduction
-=====================
-
-.. Measurement scenario has a single state generation circuit but many measurements we want to make; suppose each measurements is Pauli
-.. Naively, need one measurement circuit per measurement term
-.. Commuting observables can be measured simultaneously
-
-Suppose we have one of these measurement scenarios (i.e. a single state preparation, but many measurements to make on it) and that each of the measurements is a Pauli observable, such as when calculating the expectation value of the state with respect to some :py:class:`QubitPauliOperator`. Naively, we would need a different measurement :py:class:`~pytket.circuit.Circuit` per term in the operator, but we can reduce this by exploiting the fact that commuting observables can be measured simultaneously.
-
-.. Given a set of observables, partition into sets that are easy to measure simultaneously and generate circuits performing this by diagonalising them (reducing each to a combination of Z-measurements)
-
-Given a set of observables, we can partition them into subsets that are easy to measure simultaneously. A :py:class:`~pytket.circuit.Circuit` is generated for each subset by diagonalising the observables (reducing all of them to a combination of :math:`Z`-measurements).
-
-.. Commuting sets vs non-conflicting sets
-
-Diagonalising a mutually commuting set of Pauli observables could require an arbitrary Clifford circuit in general. If we are considering the near-term regime where "every gate counts", the diagonalisation of the observables could introduce more of the (relatively) expensive two-qubit gates, giving us the speedup at the cost of some extra noise. ``pytket`` can partition the Pauli observables into either general commuting sets for improved reduction in the number of measurement :py:class:`~pytket.circuit.Circuit` s, or into smaller sets which can be diagonalised without introducing any multi-qubit gates - this is possible when all observables are substrings of some measured Pauli string (e.g. `XYI` and `IYZ` is fine, but `ZZZ` and `XZX` is not).
-
-.. Could have multiple circuits producing the same observable, so can get extra shots/precision for free
-
-This measurement partitioning is built into the :py:meth:`~pytket.backends.Backend.get_operator_expectation_value` utility method, or can be used directly using :py:meth:`pytket.partition.measurement_reduction()` which builds a :py:class:`~pytket.partition.MeasurementSetup` object. A :py:class:`~pytket.partition.MeasurementSetup` contains a list of measurement :py:class:`~pytket.circuit.Circuit` s and a map from the :py:class:`QubitPauliString` of each observable to the information required to extract the expectation value (which bits to consider from which :py:class:`~pytket.circuit.Circuit`).
-
-.. jupyter-execute::
-
- from pytket import Qubit
- from pytket.pauli import Pauli, QubitPauliString
- from pytket.partition import measurement_reduction, PauliPartitionStrat
-
- zi = QubitPauliString({Qubit(0):Pauli.Z})
- iz = QubitPauliString({Qubit(1):Pauli.Z})
- zz = QubitPauliString({Qubit(0):Pauli.Z, Qubit(1):Pauli.Z})
- xx = QubitPauliString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
- yy = QubitPauliString({Qubit(0):Pauli.Y, Qubit(1):Pauli.Y})
-
- setup = measurement_reduction([zi, iz, zz, xx, yy], strat=PauliPartitionStrat.CommutingSets)
- print("Via Commuting Sets:")
- for i, c in enumerate(setup.measurement_circs):
- print(i, c.get_commands())
- print(setup.results[yy])
-
- setup = measurement_reduction([zi, iz, zz, xx, yy], strat=PauliPartitionStrat.NonConflictingSets)
- print("Via Non-Conflicting Sets:")
- for i, c in enumerate(setup.measurement_circs):
- print(i, c.get_commands())
- print(setup.results[yy])
-
-.. note:: Since there could be multiple measurement :py:class:`~pytket.circuit.Circuit` s generating the same observable, we could theoretically use this to extract extra shots (and hence extra precision) for that observable for free; automatically doing this as part of :py:meth:`measurement_reduction()` is planned for a future release of ``pytket``.
-
-Contextual Optimisations
-========================
-
-By default, tket makes no assumptions about a circuit's input state, nor about
-the destiny of its output state. We can therefore compose circuits freely,
-construct boxes from them that we can then place inside other circuits, and so
-on. However, when we come to run a circuit on a real device we can almost always
-assume that it will be initialised in the all-zero state, and that the final
-state of the qubits will be discarded (after measurement).
-
-This is where `contextual optimisations` can come into play. These are
-optimisations that depend on knowledge of the context of the circuit being run.
-They do not generally preserve the full unitary, but they generate circuits that
-are observationally indistinguishable (on an ideal device), and reduce noise by
-eliminating unnecessary operations from the beginning or end of the circuit.
-
-First of all, tket provides methods to `annotate` a qubit (or all qubits) as
-being initialized to zero, or discarded at the end of the circuit, or both.
-
-.. jupyter-execute::
-
- from pytket import Circuit
-
- c = Circuit(2)
- c.Y(0)
- c.CX(0,1)
- c.H(0)
- c.H(1)
- c.Rz(0.125, 1)
- c.measure_all()
- c.qubit_create_all()
- c.qubit_discard_all()
-
-The last two lines tell the compiler that all qubits are to be initialized to
-zero and discarded at the end. The methods :py:meth:`Circuit.qubit_create` and
-:py:meth:`Circuit.qubit_discard` can be used to achieve the same on individual
-qubits.
-
-.. warning:: Note that we are now restricted in how we can compose our circuit with other circuits. When composing after another circuit, a "created" qubit becomes a Reset operation. Whem composing before another circuit, a "discarded" qubit may not be joined to another qubit unless that qubit has itself been "created" (so that the discarded state gets reset to zero).
-
-Initial simplification
-~~~~~~~~~~~~~~~~~~~~~~
-
-When the above circuit is run from an all-zero state, the Y and CX gates at the
-beginning just have the effect of putting both qubits in the :math:`\lvert 1
-\rangle` state (ignoring unobservable global phase), so they could be replaced
-with two X gates. This is exactly what the :py:class:`~pytket.passes.SimplifyInitial` pass does.
-
-.. jupyter-execute::
-
- from pytket.passes import SimplifyInitial
-
- SimplifyInitial().apply(c)
- print(c.get_commands())
-
-This pass tracks the state of qubits known to be initialised to zero (or reset
-mid-circuit) forward through the circuit, for as long as the qubits remain in a
-computational basis state, either removing gates (when they don't change the
-state) or replacing them with X gates (when they invert the state).
-
-By default, this pass also replaces Measure operations acting on qubits with a
-known state by classical set-bits operations on the target bits:
-
-.. jupyter-execute::
-
- c = Circuit(1).X(0).measure_all()
- c.qubit_create_all()
- SimplifyInitial().apply(c)
- print(c.get_commands())
-
-The measurement has disappeared, replaced with a classical operation on its
-target bit. To disable this behaviour, pass the ``allow_classical=False``
-argument to :py:class:`~pytket.passes.SimplifyInitial` when constructing the pass.
-
-.. warning:: Most backends currently do not support set-bit operations, so these could cause errors when using this pass with mid-circuit measurements. In such cases you should set ``allow_classical=False``.
-
-Note that :py:class:`~pytket.passes.SimplifyInitial` does not automatically cancel successive
-pairs of X gates introduced by the simplification. It is a good idea to follow
-it with a :py:class:`~pytket.passes.RemoveRedundancies` pass in order to perform these
-cancellations.
-
-Removal of discarded operations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-An operation that has no quantum or classical output in its causal future has no
-effect (or rather, no observable effect on an ideal system), and can be removed
-from the circuit. By marking a qubit as discarded, we tell the compiler that it
-has no quantum output, potentially enabling this simplification.
-
-Note that if the qubit is measured, even if it is then discarded, the Measure
-operation has a classical output in its causal future so will not be removed.
-
-.. jupyter-execute::
-
- from pytket.circuit import Qubit
- from pytket.passes import RemoveDiscarded
-
- c = Circuit(3, 2)
- c.H(0).H(1).H(2).CX(0, 1).Measure(0, 0).Measure(1, 1).H(0).H(1)
- c.qubit_discard(Qubit(0))
- c.qubit_discard(Qubit(2))
- RemoveDiscarded().apply(c)
- print(c.get_commands())
-
-The Hadamard gate following the measurement on qubit 0, as well as the Hadamard
-on qubit 2, have disappeared, because those qubits were discarded. The Hadamard
-following the measurement on qubit 1 remains, because that qubit was not
-discarded.
-
-Commutation of measured classical maps
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The last type of contextual optimization is a little more subtle. Let's call a
-quantum unitary operation a `classical map` if it sends every computational
-basis state to a computational basis state, possibly composed with a diagonal
-operator. For example, X, Y, Z, Rz, CX, CY, CZ and Sycamore are classical maps,
-but Rx, Ry and H are not. Check the
-`documentation of gate types `_
-to see which gates have unitaries that make them amenable to optimisation.
-
-When a classical map is followed by a measurement of all its qubits, and those
-qubits are then discarded, it can be replaced by a purely classical operation
-acting on the classical outputs of the measurement.
-
-For example, if we apply a CX gate and then measure the two qubits, the result
-is (ideally) the same as if we measured the two qubits first and then applied a
-classical controlled-NOT on the measurement bits. If the gate were a CY instead
-of a CX the effect would be identical: the only difference is the insertion of a
-diagonal operator, whose effect is unmeasurable.
-
-This simplification is effected by the :py:class:`~pytket.passes.SimplifyMeasured` pass.
-
-Let's illustrate this with a Bell circuit:
-
-.. jupyter-execute::
-
- from pytket.passes import SimplifyMeasured
-
- c = Circuit(2).H(0).CX(0, 1).measure_all()
- c.qubit_discard_all()
- SimplifyMeasured().apply(c)
- print(c.get_commands())
-
-The CX gate has disappeared, replaced with a classical transform acting on the
-bits after the measurement.
-
-Contextual optimisation in practice
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-The above three passes are combined in the :py:class:`~pytket.passes.ContextSimp` pass, which
-also performs a final :py:class:`~pytket.passes.RemoveRedundancies`. Normally, before running a
-circuit on a device you will want to apply this pass (after using
-:py:meth:`Circuit.qubit_create_all` and :py:meth:`Circuit.qubit_discard_all` to
-enable the simplifications).
-
-However, most backends cannot process the classical operations that may be
-introduced by :py:class:`~pytket.passes.SimplifyMeasured` or (possibly)
-:py:class:`~pytket.passes.SimplifyInitial`. So pytket provides a method
-:py:meth:`separate_classical` to separate the classical postprocessing circuit
-from the main circuit to be run on the device. This postprocessing circuit is
-then passed as the ``ppcirc`` argument to :py:meth:`~pytket.backends.BackendResult.get_counts` or
-:py:meth:`~pytket.backends.BackendResult.get_shots`, in order to obtain the postprocessed
-results.
-
-Much of the above is wrapped up in the utility method
-:py:meth:`prepare_circuit`. This takes a circuit, applies
-:py:meth:`Circuit.qubit_create_all` and :py:meth:`Circuit.qubit_discard_all`,
-runs the full :py:class:`~pytket.passes.ContextSimp` pass, and then separates the result into
-the main circuit and the postprocessing circuit, returning both.
-
-Thus a typical usage would look something like this:
-
-.. jupyter-execute::
-
- from pytket.utils import prepare_circuit
- from pytket.extensions.qiskit import AerBackend
-
- b = AerBackend()
- c = Circuit(2).H(0).CX(0, 1)
- c.measure_all()
- c0, ppcirc = prepare_circuit(c)
- c0 = b.get_compiled_circuit(c0)
- h = b.process_circuit(c0, n_shots=10)
- r = b.get_result(h)
- shots = r.get_shots(ppcirc=ppcirc)
- print(shots)
-
-This is a toy example, but illustrates the principle. The actual circuit sent to
-the backend consisted only of a Hadamard gate on qubit 0 and a single
-measurement to bit 0. The classical postprocessing circuit set bit 1 to zero and
-then executed a controlled-NOT from bit 0 to bit 1. These details are hidden
-from us (unless we inspect the circuits), and what we end up with is a shots
-table that is indistinguishable from running the original circuit but with less
-noise.
diff --git a/docs/manual/manual_intro.rst b/docs/manual/manual_intro.md
similarity index 50%
rename from docs/manual/manual_intro.rst
rename to docs/manual/manual_intro.md
index 0652427f..248e12e7 100644
--- a/docs/manual/manual_intro.rst
+++ b/docs/manual/manual_intro.md
@@ -1,111 +1,111 @@
-*************
-What is tket?
-*************
+---
+file_format: mystnb
+---
-.. Two-sentence overview
+# What is tket?
+
+% Two-sentence overview
The tket framework is a software platform for the development and execution of gate-level quantum computation, providing state-of-the-art performance in circuit compilation. The toolset is designed to aid platform-agnostic software and extract the most out of the available NISQ devices of today.
-There is currently an implementation of tket available in the form of the ``pytket`` package for python 3.10+, which can be installed for free using the ``pip`` package manager. Additional extension modules are available for interfacing ``pytket`` with several popular quantum software packages, including `Qiskit `_, `Cirq `_, and `pyQuil `_, and for adding more devices and simulators to target.
+There is currently an implementation of tket available in the form of the `pytket` package for python 3.10+, which can be installed for free using the `pip` package manager. Additional extension modules are available for interfacing `pytket` with several popular quantum software packages, including [Qiskit](https://www.ibm.com/quantum/qiskit), [Cirq](https://quantumai.google/cirq/), and [pyQuil](https://pyquil-docs.rigetti.com/en/stable/), and for adding more devices and simulators to target.
-.. Introduction to manual and link to other resource
+% Introduction to manual and link to other resource
This user manual is targeted at readers who are already familiar with the basics of quantum computing via the circuit model and want to explore the tools available in tket. It provides a comprehensive, feature-focussed tour of the platform to take you from "Hello world" to playing with advanced techniques for speeding up and improving the accuracy of your quantum experiments.
-In addition to this manual, there is also a selection of `example notebooks `_ to see ``pytket`` in the context of the algorithms and applications that can be built on top of it. These are supported by the API reference for in-depth overviews of each method, class, and interface.
+In addition to this manual, there is also a selection of [example notebooks](https://tket.quantinuum.com/examples) to see `pytket` in the context of the algorithms and applications that can be built on top of it. These are supported by the API reference for in-depth overviews of each method, class, and interface.
-NISQ Considerations
--------------------
+## NISQ Considerations
-.. Hardware limitations
+% Hardware limitations
Looking at any quantum computing textbook would give hope that quantum computers could achieve monumental goals in computation like factoring large numbers for breaking RSA-encryption, simulating molecular energies for material discovery, or speeding up unstructured searches and optimisation problems. So you would be forgiven for being excited by the availability of quantum devices and wanting to run your first instance of Grover's algorithm on real quantum hardware. The truth is that the devices of today are still some way off from having the capability to run these kinds of algorithms at an interesting scale.
-.. NISQ considerations
+% NISQ considerations
-The NISQ [Pres2018]_ (Noisy Intermediate-Scale Quantum) era is characterised by having low numbers of qubits and high error rates. Running meaningful examples of the classic quantum algorithms would require circuits far larger than could be executed before the quantum state is dominated by the noise. Whilst quantum error correction is intended to reduce the effect of noise channels, we are not yet at the point of being able to exploit this since we lack the number of qubits for good code distances and the error rates are far beyond the code thresholds. With noise this high, every gate counts, so being able to simplify and reduce each circuit as much as possible is crucial to getting the best results.
+The NISQ [^cite_pres2018] (Noisy Intermediate-Scale Quantum) era is characterised by having low numbers of qubits and high error rates. Running meaningful examples of the classic quantum algorithms would require circuits far larger than could be executed before the quantum state is dominated by the noise. Whilst quantum error correction is intended to reduce the effect of noise channels, we are not yet at the point of being able to exploit this since we lack the number of qubits for good code distances and the error rates are far beyond the code thresholds. With noise this high, every gate counts, so being able to simplify and reduce each circuit as much as possible is crucial to getting the best results.
The devices will often only support a small (but universal) fragment of quantum circuits, either constrained by the engineering difficulty of building the necessary control systems or imposed artificially because the extreme noise in some operations would render the state unusable. Good examples of these are the heterogeneous architectures of most hardware where it may only be possible to perform multi-qubit operations between specific pairs of qubits, and the delayed-measurement model where all measurements must occur in a single layer at the end of the circuit (the measurement operations are effectively destructive and the remaining quantum state would not survive the measurement and reinitialisation of a qubit). In theory, it is still possible to perform arbitrary quantum computation with these restrictions, but there are additional resource costs that are introduced by adapting an experiment to fit and they can outright eliminate the possibility of some techniques, like error correction, that rely on mid-circuit measurement and fast feedback.
-.. Differences between textbook and practical QC
+% Differences between textbook and practical QC
Practical quantum computing in the near term differs significantly from the textbook ideals, as great attention must be paid to the fine details of each circuit, eliminating redundancy and mitigating noise wherever possible to obtain the most accurate results. Some constructions, like synthesising an arbitrary unitary or even just adding a control qubit onto a unitary, which are fine in the asymptotic limit are no longer viable in general as the scaling constant is still too high to generate efficient circuits in the device's primitives.
Even the classical processing around the quantum computation would require adjustment to account for noise, such as working with distributions over outcomes even when the algorithm expects a single definite measurement outcome, or using optimisation and learning methods that are robust to noise.
-Many of the intricacies of NISQ devices can be handled automatically by tools like ``pytket``, lowering the barrier to getting competitive results from quantum hardware and freeing the user to focus on the technique or project they are working on.
+Many of the intricacies of NISQ devices can be handled automatically by tools like `pytket`, lowering the barrier to getting competitive results from quantum hardware and freeing the user to focus on the technique or project they are working on.
-Quantum Compilation
--------------------
+## Quantum Compilation
-.. Classify both circuit compilation and more generally for experiments, employing error mitigation and transformation techniques in advance of runtime
+% Classify both circuit compilation and more generally for experiments, employing error mitigation and transformation techniques in advance of runtime
There are numerous definitions of quantum compilation used in the literature, from constructing a unitary from a problem description, to decomposing a unitary matrix into elementary gates, or the generation of a pulse schedule from a sequence of gates. These all fall under the broad idea that we are converting from a generic description of the computation we wish to perform to a description that is actually possible to run on a quantum device. The compiler in tket focuses on circuit compilation: taking a circuit representation of the procedure (possibly with small high-level components like embedded unitaries or evolution operators) and solving the gate-level constraints of the target device such as the restricted gateset, heterogeneous couplings, and measurement model.
-.. Benefits of good compilation
+% Benefits of good compilation
-Compilation of circuits does not have a unique solution, and whilst each solution would perform the same procedure on a perfect simulation they will have distinct noise characteristics, largely driven by the quantity and scheduling of the more expensive gates on devices. For example, the fidelities of two-qubit gates are typically an order of magnitude worse than for single-qubit gates on most current hardware [Arut2019]_, so solutions that require fewer swap gates to conform to the connectivity are likely to yield more reliable results. Making good use of the state-of-the-art in compilation techniques and codesign of the compilation strategy with the circuit structure can give significant reductions in the circuit size, which not only speeds up the computation but also increases the quality of the output and allows for larger problem instances to be considered.
+Compilation of circuits does not have a unique solution, and whilst each solution would perform the same procedure on a perfect simulation they will have distinct noise characteristics, largely driven by the quantity and scheduling of the more expensive gates on devices. For example, the fidelities of two-qubit gates are typically an order of magnitude worse than for single-qubit gates on most current hardware [^cite_arut2019], so solutions that require fewer swap gates to conform to the connectivity are likely to yield more reliable results. Making good use of the state-of-the-art in compilation techniques and codesign of the compilation strategy with the circuit structure can give significant reductions in the circuit size, which not only speeds up the computation but also increases the quality of the output and allows for larger problem instances to be considered.
-Platform-Agnosticism
---------------------
+## Platform-Agnosticism
Platform agnosticism is the principle that the tools and software built can be made independent of the target hardware that it will run on, and conversely that the utilisation of desirable devices is not locked behind specific interface software.
-.. Freedom of choice for input language
+% Freedom of choice for input language
Providing the freedom of choice for input language allows active developers and researchers to reuse existing code solutions or use software that presents the right level of abstraction and useful high-level constructions relevant to their domain without sacrificing the ability to target backends from other providers.
-.. Hardware independence
-.. Reusability of code and hot-swapping
-.. Futureproof tools
+% Hardware independence
+
+% Reusability of code and hot-swapping
+
+% Futureproof tools
The ability to abstract away the nuances of the hardware and work independently of the device improves the reusability of code, allowing experiments to be quickly rerun on different backends with minimal code changes (often simply hot-swapping the single line of code for connecting to the backend). In turn, this makes the high-level tools you develop to be more futureproof, as they can react to the availability of newer and better quantum hardware or changes to the capabilities and characteristics of a particular device over the course of its lifetime.
-.. Modular extensions
+% Modular extensions
-Tket enables this by providing an intermediate language for conversion of circuits between a large number of other quantum software frameworks. The uniform interface given to supported backends allows them to be seamlessly inserted into high-level code and the transfer of control from the user's input platform of choice to the system that handles the device connection is performed automatically upon submitting circuits to be executed. The modular packaging of tket into the core ``pytket`` package and a collection of extensions allows this greater software flexibility with minimal redundant package dependencies. An extension module is provided for each compatible framework or device provider, which adds in the methods for converting primitives to and from the representation used in ``pytket`` and wrappers for presenting the backends through the standard interface.
+Tket enables this by providing an intermediate language for conversion of circuits between a large number of other quantum software frameworks. The uniform interface given to supported backends allows them to be seamlessly inserted into high-level code and the transfer of control from the user's input platform of choice to the system that handles the device connection is performed automatically upon submitting circuits to be executed. The modular packaging of tket into the core `pytket` package and a collection of extensions allows this greater software flexibility with minimal redundant package dependencies. An extension module is provided for each compatible framework or device provider, which adds in the methods for converting primitives to and from the representation used in `pytket` and wrappers for presenting the backends through the standard interface.
-Installation
-------------
+## Installation
-.. license
-.. pip install pytket
+% license
-Tket is currently available through its pythonic realisation ``pytket``, which is freely available under the Apache 2 license. To install using the ``pip`` package manager, just run ``pip install pytket`` from your terminal. Each extension module can also be installed similarly as ``pip install pytket-X``, e.g. ``pip install pytket-qiskit``.
+% pip install pytket
-.. Link to troubleshooting
+Tket is currently available through its pythonic realisation `pytket`, which is freely available under the Apache 2 license. To install using the `pip` package manager, just run `pip install pytket` from your terminal. Each extension module can also be installed similarly as `pip install pytket-X`, e.g. `pip install pytket-qiskit`.
-``pytket`` is available on Linux, MacOS, and Windows. For any difficulties with installation, please consult our `troubleshooting `_ page.
+% Link to troubleshooting
-How To Cite
------------
+`pytket` is available on Linux, MacOS, and Windows. For any difficulties with installation, please consult our [troubleshooting](https://tket.quantinuum.com/api-docs/install.html) page.
-.. Instructions and link to paper
+## How To Cite
-If you wish to cite tket in any academic publications, we generally recommend citing our `software overview paper `_ for most cases.
+% Instructions and link to paper
+
+If you wish to cite tket in any academic publications, we generally recommend citing our [software overview paper](https://doi.org/10.1088/2058-9565/ab8e92) for most cases.
If your work is on the topic of specific compilation tasks, it may be more appropriate to cite one of our other papers:
-- `"On the qubit routing problem" `_ for qubit placement (aka allocation, mapping) and routing (aka swap network insertion, connectivity solving).
-- `"Phase Gadget Synthesis for Shallow Circuits" `_ for representing exponentiated Pauli operators in the ZX calculus and their circuit decompositions.
-- `"A Generic Compilation Strategy for the Unitary Coupled Cluster Ansatz" `_ for sequencing of terms in Trotterisation and Pauli diagonalisation.
+- ["On the qubit routing problem"](https://doi.org/10.4230/LIPIcs.TQC.2019.5) for qubit placement (aka allocation, mapping) and routing (aka swap network insertion, connectivity solving).
+- ["Phase Gadget Synthesis for Shallow Circuits"](https://doi.org/10.4204/EPTCS.318.13) for representing exponentiated Pauli operators in the ZX calculus and their circuit decompositions.
+- ["A Generic Compilation Strategy for the Unitary Coupled Cluster Ansatz"](https://doi.org/10.48550/arXiv.2007.10515) for sequencing of terms in Trotterisation and Pauli diagonalisation.
+
+We are also keen for others to benchmark their compilation techniques against us. We recommend checking our [benchmark repository](https://github.com/CQCL/tket_benchmarking) for examples on how to run basic benchmarks with the latest version of `pytket`. Please list the release version of `pytket` with any benchmarks you give, and feel free to get in touch for any assistance needed in setting up fair and representative tests.
-We are also keen for others to benchmark their compilation techniques against us. We recommend checking our `benchmark repository `_ for examples on how to run basic benchmarks with the latest version of ``pytket``. Please list the release version of ``pytket`` with any benchmarks you give, and feel free to get in touch for any assistance needed in setting up fair and representative tests.
+## Support
-Support
--------
+% Github issues
-.. Github issues
+If you spot any bugs or have any feature suggestions, feel free to add to the issues board on our [Github repository](https://github.com/CQCL/tket). We appreciate exact error messages and reproduction steps where possible for bug reports to help us address them quickly.
-If you spot any bugs or have any feature suggestions, feel free to add to the issues board on our `Github repository `_. We appreciate exact error messages and reproduction steps where possible for bug reports to help us address them quickly.
+% For more specific assistance, e-mail tket-support
-.. For more specific assistance, e-mail tket-support
-.. To open up direct support channels or collaboration with teams, e-mail Denise?
+% To open up direct support channels or collaboration with teams, e-mail Denise?
-There is a public slack channel for community discussion and support. Click `here `_ to join.
+There is a public slack channel for community discussion and support. Click [here](https://join.slack.com/t/tketusers/shared_invite/zt-18qmsamj9-UqQFVdkRzxnXCcKtcarLRA) to join.
-You can also join our `mailing list `_ for updates on new ``pytket`` releases and features. If you would like to open up direct support channels for your team, engage in research collaborations, or inquire about commercial licenses, please get in touch with us (info@cambridgequantum.com). If you have support questions please send them to tket-support@cambridgequantum.com.
+You can also join our [mailing list](https://list.cambridgequantum.com/cgi-bin/mailman/listinfo/tket-users) for updates on new `pytket` releases and features. If you would like to open up direct support channels for your team, engage in research collaborations, or inquire about commercial licenses, please get in touch with us (). If you have support questions please send them to .
+[^cite_pres2018]: Preskill, J., 2018. Quantum Computing in the NISQ era and beyond. Quantum, 2, p.79.
-.. [Pres2018] Preskill, J., 2018. Quantum Computing in the NISQ era and beyond. Quantum, 2, p.79.
-.. [Arut2019] Arute, F., Arya, K., Babbush, R., Bacon, D., Bardin, J.C., Barends, R., Biswas, R., Boixo, S., Brandao, F.G., Buell, D.A. and Burkett, B., 2019. Quantum supremacy using a programmable superconducting processor. Nature, 574(7779), pp.505-510.
+[^cite_arut2019]: Arute, F., Arya, K., Babbush, R., Bacon, D., Bardin, J.C., Barends, R., Biswas, R., Boixo, S., Brandao, F.G., Buell, D.A. and Burkett, B., 2019. Quantum supremacy using a programmable superconducting processor. Nature, 574(7779), pp.505-510.
diff --git a/docs/manual/manual_noise.md b/docs/manual/manual_noise.md
new file mode 100644
index 00000000..fc731a7d
--- /dev/null
+++ b/docs/manual/manual_noise.md
@@ -0,0 +1,543 @@
+---
+file_format: mystnb
+---
+
+# Noise and the Quantum Circuit Model
+
+% Overview
+
+% NISQ Devices are noisy
+
+NISQ era devices are characterised as having high error rates, meaning the effect of running quantum circuits on these devices is that states are commonly dominated by noise. This is usually to the extent that even for circuits with very few gates, significant errors are accrued quickly enough that the returned results are unusable.
+
+% Compilation prioritise different mterics to minimise devic enoise
+
+Compilation of quantum circuits does not have a unique solution. Typically compilation strategies are designed to produce quantum circuits that implement the same logical circuit with fewer total gates and so have less opportunity to accrue errors. Understanding which quantum operations will lead to excessive noise accumulation in devices can help design compilation strategies to reduce this noise. Examples of this are circuit optimistation strategies that target the removal of multi-qubit gates as they typically have worse error rates than single-qubit gates, or designing circuit routing methods that introduce fewer total swap gates when conforming circuits to some device connectivity.
+
+% What else can be done
+
+Given the range of types of quantum devices available and their individual noise profiles, more precise characterisations of where noise accumulates in devices can aid in designing techniques for suppressing specific or general noise - `pytket` has several such techniques available.
+
+Noise in the quantum circuit model can be viewed as the distance between the expected distribution of measurement outcomes from a quantum state and the distribution returned by the process of repeatedly sampling shots. While some compilation methods aim to reduce this distance by the optimisation of some other metric, such as the number of occurrences of a given device's multi-qubit primitives, the discussed techniques are explicitly designed to modify the returned distribution favourably by dealing directly with noise levels.
+
+% Noise Aware Placement, via Device and reported backend information
+
+## Noise Aware Mapping
+
+% Why this is originally a problem
+
+Many quantum devices place limits on which qubits can
+interact, with these limitations being determined by the device architecture.
+When compiling a circuit to run on one of these devices, the circuit
+must be modified to fit the architecture, a process described in the
+previous chapter under {ref}`compiler-placement` and
+{ref}`compiler-routing`.
+
+In addition, the noise present in NISQ devices typically varies across
+the architecture, with different qubits and couplings experiencing
+different error rates, which may also vary depending on the operation
+being performed. To complicate matters further, these characteristics
+vary over time, a phenomenon commonly referred to as device drift
+[^cite_white2019].
+
+Some devices expose error characterisation information through
+their programming interface. When available, {py:class}`~pytket.backends.Backend`
+objects will populate a {py:class}`~pytket.backends.backendinfo.BackendInfo` object with this information.
+
+A {py:class}`~pytket.backends.backendinfo.BackendInfo` object contains a variety of characterisation information supplied by hardware providers.
+Some information, including gate error rates, is stored in attributes with specific names.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket.extensions.qiskit import IBMQBackend
+
+backend = IBMQBackend("ibmq_manila")
+print(backend.backend_info.averaged_node_gate_errors)
+```
+
+
+```
+{node[0]: 0.0002186159622502225,
+node[1]: 0.0002839221599849252,
+node[2]: 0.00014610243862697218,
+node[3]: 0.00015814094160059136,
+node[4]: 0.00013411930305754117}
+```
+
+Other miscellaneous information, varying between backends, is stored in the `misc` attribute, also accessible through the {py:meth}`~pytket.backends.BackendInfo.get_misc` method.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+print(backend.backend_info.get_misc())
+```
+
+
+```
+dict_keys(['t1times', 't2times', 'Frequencies', 'GateTimes'])
+```
+
+There is typically a large variation in device noise characteristics.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket.circuit import Node
+
+print(backend.backend_info.all_node_gate_errors[Node(0)])
+print(backend.backend_info.all_node_gate_errors[Node(1)])
+```
+
+
+```
+
+{: 0.00036435993708370417,
+: 0.0,
+: 0.00036435993708370417,
+: 0.00036435993708370417,
+: 0.0}
+{: 0.0004732035999748754,
+: 0.0,
+: 0.0004732035999748754,
+: 0.0004732035999748754,
+: 0.0}
+```
+
+
+```
+
+print(backend.backend_info.all_edge_gate_errors)
+```
+
+
+```
+
+{(node[4], node[3]): {: 0.01175674116384029},
+(node[3], node[4]): {: 0.005878370581920145},
+(node[2], node[3]): {: 0.013302220876095505},
+(node[3], node[2]): {: 0.006651110438047753},
+(node[2], node[1]): {: 0.022572084465386333},
+(node[1], node[2]): {: 0.011286042232693166},
+(node[0], node[1]): {: 0.026409836177538337},
+(node[1], node[0]): {: 0.013204918088769169}}
+
+```
+
+Recall that mapping in `pytket` works in two phases --
+first assigning logical circuit qubits to physical device qubits
+(placement) and then permuting these qubits via `OpType.SWAP`
+networks (routing). Device characteristics can inform the choices
+made in both phases, by prioritising edges with lower error rates.
+
+% Noise-Aware placement is effective
+
+The class {py:class}`NoiseAwarePlacement` uses characteristics stored in
+{py:class}`~pytket.backends.backendinfo.BackendInfo` to find an initial placement of logical qubits on
+physical qubits which minimises the error accrued during a circuit's
+execution. It achieves this by minimising the additional
+`OpType.SWAP` overhead to route circuits, as in conventional
+placement, and at the same time avoiding qubits with worse error
+rates. Further information on this method is available in section 7.1
+of our [software overview paper](https://iopscience.iop.org/article/10.1088/2058-9565/ab8e92).
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket.placement import NoiseAwarePlacement, GraphPlacement
+
+noise_placer = NoiseAwarePlacement(backend.backend_info.architecture,
+ backend.backend_info.averaged_readout_errors,
+ backend.backend_info.averaged_node_gate_errors,
+ backend.backend_info.averaged_edge_gate_errors)
+
+graph_placer = GraphPlacement(backend.backend_info.architecture)
+
+circ = Circuit(3).CX(0,1).CX(0,2)
+
+print(backend.backend_info.architecture.coupling, '\n')
+
+noise_placement = noise_placer.get_placement_map(circ)
+graph_placement = graph_placer.get_placement_map(circ)
+
+print('NoiseAwarePlacement mapping:')
+for k, v in noise_placement.items():
+ print(k, v)
+
+print('\nGraphPlacement mapping:')
+for k, v in graph_placement.items():
+ print(k, v)
+
+```
+
+
+```
+
+[(node[0], node[1]), (node[1], node[0]), (node[1], node[2]), (node[1], node[3]), (node[2], node[1]), (node[3], node[1]), (node[3], node[4]), (node[4], node[3])]
+
+NoiseAwarePlacement mapping:
+q[0] node[3]
+q[1] node[1]
+q[2] node[4]
+
+GraphPlacement mapping:
+q[0] node[1]
+q[1] node[0]
+q[2] node[2]
+```
+
+Both placement methods will satisfy the device's connectivity
+constraints, however looking at the device characteristics for
+`ibmq_manila` above, we see that the placement provided by
+{py:class}`NoiseAwarePlacement` is over a set of qubits with generally
+better error rates. This will produce a circuit whose output
+statistics are closer to the ideal, noiseless, distribution.
+
+% Frame Randomisation and friends
+
+## Noise Tailoring Methods
+
+% Why Noise tailoring might be helpful
+
+While it is not possible to efficiently characterise and suppress all device noise, it can be advantageous to transform some adverse type of noise into a less damaging type.
+
+Coherent errors are additional unwanted unitary rotations that may appear throughout a quantum computation. Their effect can be damaging due to a possible faster rate of error accumulation than in the case of probabilistic (incoherent) errors.
+
+Randomisation protocols can be used to tailor the form of the noise profile. By averaging the n-qubit noise channel over all elements from a group (specifically some subgroup of the full unitary group on n qubits), the resulting noise is invariant under the action of any element from this group.
+
+For example, averaging a noise channel over the n-qubit Pauli group has the effect of producing an n-qubit stochastic Pauli channel -- this is a probabilistic linear combination of n-qubit Pauli unitary errors.
+
+In this manner, an n-qubit coherent noise channel can be tailored into an n-qubit stochastic Pauli noise channel. For Pauli channels, the worst case error rate is similar to the average error rate, whilst for coherent noise the worst case error rate scales as a square root of the average error rate.
+
+The `pytket` {py:class}`FrameRandomisation` class available in the tailoring module provides methods for using randomised protocols on generic quantum circuits. At a high level, {py:class}`FrameRandomisation` provides methods for identifying n-qubit subcircuits (or cycles) comprised of gates chosen for tailoring in some circuit of choice, and then constructing new circuits for averaging these subcircuits over some ensemble of n-qubit operators (constructed from the Kronecker product of single qubit gates referred to as 'Frame' gates). Tailored counts for a circuit of choice are then produced by running each of the new circuits through a backend with the same number of shots and then combining the returned counts.
+
+For each cycle in the circuit, each of the ensemble's operators is prepended to the cycle and a new operator is derived to append to the cycle such that the whole unitary operation is unchanged. When constructing a {py:class}`FrameRandomisation` object the information required to derive the correct operator to prepend must be provided through a dictionary. An example of this procedure is *randomised compilation* [^cite_wallman2015].
+
+
+```{code-cell} ipython3
+
+ from pytket.tailoring import FrameRandomisation
+ from pytket import OpType, Circuit
+ from pytket.extensions.qiskit import AerBackend
+
+ circ = Circuit(2).X(0).CX(0,1).S(1).measure_all()
+ frame_randomisation = FrameRandomisation(
+ {OpType.CX}, # Set of OpType that cycles are comprised of. For a randomised circuit, the minimum number of cycles is found such that every gate with a cycle OpType is in exactly one cycle.
+ {OpType.Y}, # Set of OpType frames are constructed from
+ {
+ OpType.CX: {(OpType.Y, OpType.Y): (OpType.X, OpType.Z)}, # Operations to prepend and append to CX respectively such that unitary is preserved i.e. Y(0).Y(1).CX(0,1).X(0).Z(1) == CX(0,1)
+ },
+ )
+
+ averaging_circuits = frame_randomisation.get_all_circuits(circ)
+ print('For a single gate in the averaging ensemble we return a single circuit:')
+ for com in averaging_circuits[0]:
+ print(com)
+
+ print('\nWe can check that the unitary of the circuit is preserved by comparing output counts:')
+ backend = AerBackend()
+ print(backend.run_circuit(circ, 100).get_counts())
+ print(backend.run_circuit(averaging_circuits[0], 100).get_counts())
+```
+
+% preset cycle and frame gates to tailor meaningful noise
+
+Note that the {py:class}`FrameRandomisation` procedure sandwiches each cycle between `OpType.Barrier` operations. This is because frame gates can be combined with adjacent rotation gates to reduce gate overhead, but can not be commuted through their associated cycle as this will undo the framing process. As FrameRandomisation will lead to a blow up in the number of circuits compiled, it is recommended to run FrameRandomisation procedures after circuit optimisation techniques.
+
+Running a randomised protocol to achieve meaningful results requires a careful choice of cycle gates and frame gates, which the above example does not make. However, the {py:class}`PauliFrameRandomisation` class is preset with cycle gates {`OpType.CX`, `OpType.H`, `OpType.S`} and frame gates {`OpType.X`, `OpType.Y`, `OpType.Z`, `OpType.noop`} that should.
+
+The {py:meth}`PauliFrameRandomisation.get_all_circuits` method returns circuits that tailor the noise of subcircuits comprised of cycle gates into a stochastic Pauli noise when run on a device (given some assumptions, such as additional frame gates not providing additional incoherent noise).
+
+
+```{code-cell} ipython3
+
+ from pytket import Circuit
+ from pytket.extensions.qiskit import AerBackend
+ from pytket.tailoring import PauliFrameRandomisation
+
+ circ = Circuit(2).X(0).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
+
+ pauli_frame_randomisation = PauliFrameRandomisation()
+ averaging_circuits = pauli_frame_randomisation.get_all_circuits(circ)
+
+ print('Number of PauliFrameRandomisation averaging circuits: ', len(averaging_circuits))
+
+ print('\nAn example averaging circuit with frames applied to two cycles: ')
+ for com in averaging_circuits[3].get_commands():
+ print(com)
+ print('\n')
+
+ backend = AerBackend()
+
+ averaging_circuits = backend.get_compiled_circuits(averaging_circuits)
+ circ = backend.get_compiled_circuit(circ)
+
+ pfr_counts_list = [
+ res.get_counts() for res in backend.run_circuits(averaging_circuits, 50)
+ ]
+ # combine each averaging circuits counts into a single counts object for comparison
+ pfr_counts = {}
+ for counts in pfr_counts_list:
+ pfr_counts = {key: pfr_counts.get(key,0) + counts.get(key,0) for key in set(pfr_counts)|set(counts)}
+
+ print(pfr_counts)
+ print(backend.run_circuit(circ, 50*len(averaging_circuits)).get_counts())
+
+```
+
+For a noise free backend, we can see that the same counts distribution is returned as expected. We can use a basic noise model based on a real device to see how a realistic noise channel can change when applying {py:class}`PauliFrameRandomisation`.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from qiskit_aer.noise import NoiseModel
+from qiskit import IBMQ
+IBMQ.load_account()
+
+circ = Circuit(2).X(0).H(1).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
+
+noisy_backend = AerBackend(NoiseModel.from_backend(IBMQ.providers()[0].get_backend('ibmq_manila')))
+
+averaging_circuits = pauli_frame_randomisation.get_all_circuits(circ)
+
+averaging_circuits = noisy_backend.get_compiled_circuits(averaging_circuits)
+circ = noisy_backend.get_compiled_circuit(circ)
+
+pfr_counts_list = [res.get_counts() for res in noisy_backend.run_circuits(averaging_circuits, 50)]
+pfr_counts = {}
+for counts in pfr_counts_list:
+ pfr_counts = {key: pfr_counts.get(key,0) + counts.get(key,0) for key in set(pfr_counts)|set(counts)}
+
+
+print('Noiseless Counts:', AerBackend().run_circuit(circ, 50*len(averaging_circuits).get_counts()))
+print('Base Noisy Counts:', noisy_backend.run_circuit(circ, 50*len(averaging_circuits).get_counts()))
+print('Recombined Noisy Counts using PauliFrameRandomisation:', pfr_counts)
+
+```
+
+
+```
+
+Noiseless Counts: Counter({(1, 1): 6415, (1, 0): 6385})
+Base Noisy Counts: Counter({(1, 0): 6368, (1, 1): 5951, (0, 1): 253, (0, 0): 228})
+Recombined Noisy Counts using PauliFrameRandomisation: {(0, 1): 203, (0, 0): 215, (1, 0): 6194, (1, 1): 6188}
+
+```
+
+For this simple case we observe that more shots are returning basis states not in the expected state (though it would be unwise to declare the methods efficacy from this alone).
+
+Given that cycle gates for {py:class}`PauliFrameRandomisation` do not form a universal gate set for the quantum circuit model, randomised protocols using {py:class}`PauliFrameRandomisation` will usually need to individually tailor many cycle instances for a given circuit. This can lead to large circuit overhead required for complete averaging, or a loss of guarantee that the resulting channel is a stochastic Pauli noise when not every frame is used.
+
+An alternative class, {py:class}`UniversalFrameRandomisation`, is set with cycle gates {`OpType.CX`, `OpType.H`, `OpType.Rz`} and frame gates {`OpType.X`, `OpType.Y`, `OpType.Z`, `OpType.noop`} and so can treat a whole circuit as a single cycle if rebased appropriately. It providers averaging circuits while preserving the unitary of the circuit by changing the rotation angle of cycle `OpType.Rz` gates when prepending and appending frame gates, meaning that the stochastic Pauli noise property is additionally dependent on incoherent noise not being dependent on the rotation angle.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket.tailoring import UniversalFrameRandomisation
+
+universal_frame_randomisation = UniversalFrameRandomisation()
+
+circ = Circuit(2).X(0).H(1).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
+
+averaging_circuits = universal_frame_randomisation.get_all_circuits(circ)
+
+averaging_circuits = noisy_backend.get_compiled_circuits(averaging_circuits)
+circ = noisy_backend.get_compiled_circuit(circ)
+
+ufr_noisy_counts_list = [res.get_counts() for res in noisy_backend.run_circuits(averaging_circuits, 800)]
+ufr_noisy_counts = {}
+for counts in ufr_noisy_counts_list:
+ ufr_noisy_counts = {key: ufr_noisy_counts.get(key,0) + counts.get(key,0) for key in set(ufr_noisy_counts)|set(counts)}
+
+
+ufr_noiseless_counts_list = [res.get_counts() for res in AerBackend().run_circuits(averaging_circuits, 800)]
+ufr_noiseless_counts = {}
+for counts in ufr_noiseless_counts_list:
+ ufr_noiseless_counts = {key: ufr_noiseless_counts.get(key,0) + counts.get(key,0) for key in set(ufr_noiseless_counts)|set(counts)}
+
+
+print('Noiseless Counts:', noiseless_counts)
+print('Recombined Noiseless Counts using UniversalFrameRandomisation:', ufr_noiseless_counts)
+print('Base Noisy Counts:', noisy_counts)
+print('Recombined Noisy Counts using PauliFrameRandomisation:', pfr_counts)
+print('Recombined Noisy Counts using UniversalFrameRandomisation:', ufr_noisy_counts)
+
+```
+
+
+```
+
+Noiseless Counts: Counter({(1, 0): 6490, (1, 1): 6310})
+Recombined Noiseless Counts using UniversalFrameRandomisation: {(1, 0): 6440, (1, 1): 6360}
+Base Noisy Counts: Counter({(1, 0): 6298, (1, 1): 6022, (0, 1): 261, (0, 0): 219})
+Recombined Noisy Counts using PauliFrameRandomisation: {(0, 1): 240, (0, 0): 212, (1, 0): 6253, (1, 1): 6095}
+Recombined Noisy Counts using UniversalFrameRandomisation: {(0, 1): 208, (0, 0): 208, (1, 0): 6277, (1, 1): 6107}
+```
+
+Similarly as to the previous case, more shots are returning basis states in the expected state.
+
+We can use {py:meth}`auto_rebase_pass` to create a pass that can be applied to a circuit to rebase its gates to {`OpType.CX`, `OpType.H`, `OpType.Rz`}, the cycle gate primitives for Universal Frame Randomisation.
+
+
+```{code-cell} ipython3
+
+from pytket.circuit import PauliExpBox, Pauli, Circuit, OpType
+from pytket.transform import Transform
+from pytket.passes import auto_rebase_pass
+from pytket.tailoring import UniversalFrameRandomisation
+
+rebase_ufr = auto_rebase_pass({OpType.CX, OpType.H, OpType.Rz})
+
+universal_frame_randomisation = UniversalFrameRandomisation()
+
+circ = Circuit(4)
+circ.X(0)
+circ.X(1)
+circ.add_gate(
+ PauliExpBox([Pauli.X, Pauli.Z, Pauli.Y, Pauli.I], 0.034), [0, 1, 2, 3]
+)
+circ.add_gate(
+ PauliExpBox([Pauli.Y, Pauli.Z, Pauli.X, Pauli.I], -0.2), [0, 1, 2, 3]
+)
+circ.add_gate(
+ PauliExpBox([Pauli.I, Pauli.X, Pauli.Z, Pauli.Y], 0.45), [0, 1, 2, 3]
+)
+
+Transform.DecomposeBoxes().apply(circ)
+ufr_averaging_circuits = universal_frame_randomisation.get_all_circuits(circ)
+print('Number of Universal Frame Randomisation averaging circuits without rebase: ', len(ufr_averaging_circuits))
+
+rebase_ufr.apply(circ)
+ufr_averaging_circuits = universal_frame_randomisation.get_all_circuits(circ)
+print('Number of Universal Frame Randomisation averaging circuits with rebase: ', len(ufr_averaging_circuits))
+
+ufr_averaging_circuits = universal_frame_randomisation.sample_circuits(circ, 200)
+print('Number of sampled Universal Frame Randomisation averaging circuits with rebase: ', len(ufr_averaging_circuits))
+
+```
+
+By rebasing the circuit Universal Frame Randomisation is being applied to, we can see a significant reduction in the number of averaging circuits required. For large circuits with many cycles {py:meth}`FrameRandomisation.sample_circuits`
+can be used to sample from the full set of averaging circuits. It is recommended to use {py:meth}`FrameRandomisation.sample_circuit` over {py:meth}`FrameRandomisation.get_all_circuits` for larger circuits with many cycles as the overhead in finding frame permutations becomes significant.
+
+% SPAM Mitigation module and how to use
+
+## SPAM Mitigation
+
+A prominent source of noise is that occurring during State Preparation and Measurement (SPAM) in the hardware.
+
+SPAM error mitigation methods can correct for such noise through a post-processing step that modifies the output distribution measured from repeatedly sampling shots. This is possible given the assumption that SPAM noise is not dependent on the quantum computation run.
+
+By repeatedly preparing and measuring a basis state of the device, a distribution over basis states is procured. While for a perfect device the distribution would be the prepared basis state with probability 1, for devices prone to SPAM noise this distribution is perturbed and other basis states may be returned with (expected) small probability.
+
+If this process is repeated for all (or a suitable subset given many qubits won't experience correlated SPAM errors) basis states of a device, a transition matrix can be derived that describes the noisy SPAM process.
+Simply applying the inverse of this transition matrix to the distribution of a quantum state from some desired quantum computation can effectively uncompute the errors caused by SPAM noise.
+
+The {py:class}`SpamCorrecter` provides the required tools for characterising and correcting SPAM noise in this manner. A {py:class}`SpamCorrecter` object is initialised from a partition of a subset of the quantum device's qubits. Qubits are assumed to have SPAM errors which are correlated with that of other qubits in their set, but uncorrelated with the other sets.
+
+As an n-qubit device has $2^n$ basis states, finding the exact noisy SPAM process becomes infeasible for larger devices. However, as correlated errors are typically spatially dependent though, one can usually characterise SPAM noise well by only assuming correlated SPAM noise between nearest-neighbour qubits.
+
+The {py:class}`SpamCorrecter` object uses these subsets of qubits to produce calibration circuits.
+
+
+
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
+
+from pytket.utils.spam import SpamCorrecter
+from pytket.extensions.qiskit import IBMQBackend
+
+backend = IBMQBackend("ibmq_quito")
+nodes = backend.backend_info.architecture.nodes
+
+spam_correcter = SpamCorrecter([nodes])
+
+calibration_circuits = spam_correcter.calibration_circuits()
+print('Number of calibration circuits: ' , len(calibration_circuits))
+print(calibration_circuits[1].get_commands())
+
+```
+
+
+```
+Number of calibration circuits: 32
+
+[X node[4];, Barrier node[0], node[1], node[2], node[3], node[4];, Measure node[0] --> c[0];, Measure node[1] --> c[1];, Measure node[2] --> c[2];, Measure node[3] --> c[3];, Measure node[4] --> c[4];]
+```
+
+Assuming SPAM correlation between all 5 qubits of the "ibmq_quito" device, there are a total of 32 calibration circuits total for constructing each basis state. Printing the commands of the second basis state preparation circuit, we see that the circuits simply apply X gates to the states of qubits initialised in the 0 state as appropriate.
+
+To display the performance of SPAM correction in a controlled environment, we can construct a noise model with measurement errors from `qiskit-aer` and use it to define a simulator backend with known measurement noise.
+
+First the {py:class}`SpamCorrecter` is characterised using counts results for calibration circuits executed through the noisy backend of choice using {py:meth}`SpamCorrecter.calculate_matrices`. Once characterised, noisy counts for a circuit can be corrected using {py:meth}`SpamCorrecter.correct_counts`.
+
+
+```{code-cell} ipython3
+
+ from pytket.extensions.qiskit import AerBackend
+ from pytket import Circuit
+ from pytket.utils.spam import SpamCorrecter
+
+ from qiskit_aer.noise import NoiseModel
+ from qiskit_aer.noise.errors import depolarizing_error
+
+ noise_model = NoiseModel()
+ noise_model.add_readout_error([[0.9, 0.1],[0.1, 0.9]], [0])
+ noise_model.add_readout_error([[0.95, 0.05],[0.05, 0.95]], [1])
+ noise_model.add_quantum_error(depolarizing_error(0.1, 2), ["cx"], [0, 1])
+
+ noisy_backend = AerBackend(noise_model)
+ noiseless_backend = AerBackend()
+ spam_correcter = SpamCorrecter([noisy_backend.backend_info.architecture.nodes], noisy_backend)
+ calibration_circuits = spam_correcter.calibration_circuits()
+
+ char_handles = noisy_backend.process_circuits(calibration_circuits, 1000)
+ char_results = noisy_backend.get_results(char_handles)
+
+ spam_correcter.calculate_matrices(char_results)
+
+ circ = Circuit(2).H(0).CX(0,1).measure_all()
+ circ = noisy_backend.get_compiled_circuit(circ)
+ noisy_handle = noisy_backend.process_circuit(circ, 1000)
+ noisy_result = noisy_backend.get_result(noisy_handle)
+ noiseless_handle = noiseless_backend.process_circuit(circ, 1000)
+ noiseless_result = noiseless_backend.get_result(noiseless_handle)
+
+ circ_parallel_measure = spam_correcter.get_parallel_measure(circ)
+ corrected_counts = spam_correcter.correct_counts(noisy_result, circ_parallel_measure)
+
+ print('Noisy Counts:', noisy_result.get_counts())
+ print('Corrected Counts:', corrected_counts.get_counts())
+ print('Noiseless Counts:', noiseless_result.get_counts())
+
+```
+
+Despite the presence of additional noise, it is straightforward to see that the corrected counts results are closer to the expected noiseless counts than the original noisy counts. All that is required to use {py:class}`SpamCorrecter` with a real device is the interchange of {py:class}`~pytket.extensions.qiskit.AerBackend` with a real device backend, such as {py:class}`~pytket.extensions.qiskit.IBMQBackend`.
+
+[^cite_wallman2015]: Wallman, J., Emerson, J., 2015. Noise tailoring for scalable quantum computation via randomized compiling. Phys. Rev. A 94, 052325 (2016).
+
+[^cite_white2019]: White, G., Hill, C., Hollenberg, L., 2019. Performance optimisation for drift-robust fidelity improvement of two-qubit gates. arXiv:1911.12096.
diff --git a/docs/manual/manual_noise.rst b/docs/manual/manual_noise.rst
deleted file mode 100644
index 020d9128..00000000
--- a/docs/manual/manual_noise.rst
+++ /dev/null
@@ -1,511 +0,0 @@
-***********************************
-Noise and the Quantum Circuit Model
-***********************************
-
-.. Overview
-
-.. NISQ Devices are noisy
-
-NISQ era devices are characterised as having high error rates, meaning the effect of running quantum circuits on these devices is that states are commonly dominated by noise. This is usually to the extent that even for circuits with very few gates, significant errors are accrued quickly enough that the returned results are unusable.
-
-
-.. Compilation prioritise different mterics to minimise devic enoise
-
-Compilation of quantum circuits does not have a unique solution. Typically compilation strategies are designed to produce quantum circuits that implement the same logical circuit with fewer total gates and so have less opportunity to accrue errors. Understanding which quantum operations will lead to excessive noise accumulation in devices can help design compilation strategies to reduce this noise. Examples of this are circuit optimistation strategies that target the removal of multi-qubit gates as they typically have worse error rates than single-qubit gates, or designing circuit routing methods that introduce fewer total swap gates when conforming circuits to some device connectivity.
-
-.. What else can be done
-
-Given the range of types of quantum devices available and their individual noise profiles, more precise characterisations of where noise accumulates in devices can aid in designing techniques for suppressing specific or general noise - ``pytket`` has several such techniques available.
-
-
-Noise in the quantum circuit model can be viewed as the distance between the expected distribution of measurement outcomes from a quantum state and the distribution returned by the process of repeatedly sampling shots. While some compilation methods aim to reduce this distance by the optimisation of some other metric, such as the number of occurrences of a given device's multi-qubit primitives, the discussed techniques are explicitly designed to modify the returned distribution favourably by dealing directly with noise levels.
-
-
-.. Noise Aware Placement, via Device and reported backend information
-
-Noise Aware Mapping
--------------------
-
-.. Why this is originally a problem
-
-Many quantum devices place limits on which qubits can
-interact, with these limitations being determined by the device architecture.
-When compiling a circuit to run on one of these devices, the circuit
-must be modified to fit the architecture, a process described in the
-previous chapter under :ref:`compiler-placement` and
-:ref:`compiler-routing`.
-
-In addition, the noise present in NISQ devices typically varies across
-the architecture, with different qubits and couplings experiencing
-different error rates, which may also vary depending on the operation
-being performed. To complicate matters further, these characteristics
-vary over time, a phenomenon commonly referred to as device drift
-[White2019]_.
-
-Some devices expose error characterisation information through
-their programming interface. When available, :py:class:`~pytket.backends.Backend`
-objects will populate a :py:class:`~pytket.backends.backendinfo.BackendInfo` object with this information.
-
-A :py:class:`~pytket.backends.backendinfo.BackendInfo` object contains a variety of characterisation information supplied by hardware providers.
-Some information, including gate error rates, is stored in attributes with specific names.
-
-
-.. jupyter-input::
-
- from pytket.extensions.qiskit import IBMQBackend
-
- backend = IBMQBackend("ibmq_manila")
- print(backend.backend_info.averaged_node_gate_errors)
-
-.. jupyter-output::
-
- {node[0]: 0.0002186159622502225,
- node[1]: 0.0002839221599849252,
- node[2]: 0.00014610243862697218,
- node[3]: 0.00015814094160059136,
- node[4]: 0.00013411930305754117}
-
-Other miscellaneous information, varying between backends, is stored in the `misc` attribute, also accessible through the :py:meth:`~pytket.backends.BackendInfo.get_misc` method.
-
-.. jupyter-input::
-
- print(backend.backend_info.get_misc())
-
-.. jupyter-output::
-
- dict_keys(['t1times', 't2times', 'Frequencies', 'GateTimes'])
-
-There is typically a large variation in device noise characteristics.
-
-.. jupyter-input::
-
- from pytket.circuit import Node
-
- print(backend.backend_info.all_node_gate_errors[Node(0)])
- print(backend.backend_info.all_node_gate_errors[Node(1)])
-
-.. jupyter-output::
-
- {: 0.00036435993708370417,
- : 0.0,
- : 0.00036435993708370417,
- : 0.00036435993708370417,
- : 0.0}
- {: 0.0004732035999748754,
- : 0.0,
- : 0.0004732035999748754,
- : 0.0004732035999748754,
- : 0.0}
-
-.. jupyter-input::
-
- print(backend.backend_info.all_edge_gate_errors)
-
-.. jupyter-output::
-
- {(node[4], node[3]): {: 0.01175674116384029},
- (node[3], node[4]): {: 0.005878370581920145},
- (node[2], node[3]): {: 0.013302220876095505},
- (node[3], node[2]): {: 0.006651110438047753},
- (node[2], node[1]): {: 0.022572084465386333},
- (node[1], node[2]): {: 0.011286042232693166},
- (node[0], node[1]): {: 0.026409836177538337},
- (node[1], node[0]): {: 0.013204918088769169}}
-
-
-Recall that mapping in ``pytket`` works in two phases --
-first assigning logical circuit qubits to physical device qubits
-(placement) and then permuting these qubits via ``OpType.SWAP``
-networks (routing). Device characteristics can inform the choices
-made in both phases, by prioritising edges with lower error rates.
-
-.. Noise-Aware placement is effective
-
-The class :py:class:`NoiseAwarePlacement` uses characteristics stored in
-:py:class:`~pytket.backends.backendinfo.BackendInfo` to find an initial placement of logical qubits on
-physical qubits which minimises the error accrued during a circuit's
-execution. It achieves this by minimising the additional
-``OpType.SWAP`` overhead to route circuits, as in conventional
-placement, and at the same time avoiding qubits with worse error
-rates. Further information on this method is available in section 7.1
-of our `software overview paper
-`_.
-
-.. jupyter-input::
-
- from pytket.placement import NoiseAwarePlacement, GraphPlacement
-
- noise_placer = NoiseAwarePlacement(backend.backend_info.architecture,
- backend.backend_info.averaged_readout_errors,
- backend.backend_info.averaged_node_gate_errors,
- backend.backend_info.averaged_edge_gate_errors)
-
- graph_placer = GraphPlacement(backend.backend_info.architecture)
-
- circ = Circuit(3).CX(0,1).CX(0,2)
-
- print(backend.backend_info.architecture.coupling, '\n')
-
- noise_placement = noise_placer.get_placement_map(circ)
- graph_placement = graph_placer.get_placement_map(circ)
-
- print('NoiseAwarePlacement mapping:')
- for k, v in noise_placement.items():
- print(k, v)
-
- print('\nGraphPlacement mapping:')
- for k, v in graph_placement.items():
- print(k, v)
-
-
-.. jupyter-output::
-
- [(node[0], node[1]), (node[1], node[0]), (node[1], node[2]), (node[1], node[3]), (node[2], node[1]), (node[3], node[1]), (node[3], node[4]), (node[4], node[3])]
-
- NoiseAwarePlacement mapping:
- q[0] node[3]
- q[1] node[1]
- q[2] node[4]
-
- GraphPlacement mapping:
- q[0] node[1]
- q[1] node[0]
- q[2] node[2]
-
-Both placement methods will satisfy the device's connectivity
-constraints, however looking at the device characteristics for
-``ibmq_manila`` above, we see that the placement provided by
-:py:class:`NoiseAwarePlacement` is over a set of qubits with generally
-better error rates. This will produce a circuit whose output
-statistics are closer to the ideal, noiseless, distribution.
-
-.. Frame Randomisation and friends
-
-Noise Tailoring Methods
------------------------
-
-.. Why Noise tailoring might be helpful
-
-While it is not possible to efficiently characterise and suppress all device noise, it can be advantageous to transform some adverse type of noise into a less damaging type.
-
-
-Coherent errors are additional unwanted unitary rotations that may appear throughout a quantum computation. Their effect can be damaging due to a possible faster rate of error accumulation than in the case of probabilistic (incoherent) errors.
-
-
-Randomisation protocols can be used to tailor the form of the noise profile. By averaging the n-qubit noise channel over all elements from a group (specifically some subgroup of the full unitary group on n qubits), the resulting noise is invariant under the action of any element from this group.
-
-
-For example, averaging a noise channel over the n-qubit Pauli group has the effect of producing an n-qubit stochastic Pauli channel -- this is a probabilistic linear combination of n-qubit Pauli unitary errors.
-
-
-In this manner, an n-qubit coherent noise channel can be tailored into an n-qubit stochastic Pauli noise channel. For Pauli channels, the worst case error rate is similar to the average error rate, whilst for coherent noise the worst case error rate scales as a square root of the average error rate.
-
-
-The ``pytket`` :py:class:`FrameRandomisation` class available in the tailoring module provides methods for using randomised protocols on generic quantum circuits. At a high level, :py:class:`FrameRandomisation` provides methods for identifying n-qubit subcircuits (or cycles) comprised of gates chosen for tailoring in some circuit of choice, and then constructing new circuits for averaging these subcircuits over some ensemble of n-qubit operators (constructed from the Kronecker product of single qubit gates referred to as 'Frame' gates). Tailored counts for a circuit of choice are then produced by running each of the new circuits through a backend with the same number of shots and then combining the returned counts.
-
-
-For each cycle in the circuit, each of the ensemble's operators is prepended to the cycle and a new operator is derived to append to the cycle such that the whole unitary operation is unchanged. When constructing a :py:class:`FrameRandomisation` object the information required to derive the correct operator to prepend must be provided through a dictionary. An example of this procedure is *randomised compilation* [Wallman2015]_.
-
-
-
-.. jupyter-execute::
-
- from pytket.tailoring import FrameRandomisation
- from pytket import OpType, Circuit
- from pytket.extensions.qiskit import AerBackend
-
- circ = Circuit(2).X(0).CX(0,1).S(1).measure_all()
- frame_randomisation = FrameRandomisation(
- {OpType.CX}, # Set of OpType that cycles are comprised of. For a randomised circuit, the minimum number of cycles is found such that every gate with a cycle OpType is in exactly one cycle.
- {OpType.Y}, # Set of OpType frames are constructed from
- {
- OpType.CX: {(OpType.Y, OpType.Y): (OpType.X, OpType.Z)}, # Operations to prepend and append to CX respectively such that unitary is preserved i.e. Y(0).Y(1).CX(0,1).X(0).Z(1) == CX(0,1)
- },
- )
-
- averaging_circuits = frame_randomisation.get_all_circuits(circ)
- print('For a single gate in the averaging ensemble we return a single circuit:')
- for com in averaging_circuits[0]:
- print(com)
-
- print('\nWe can check that the unitary of the circuit is preserved by comparing output counts:')
- backend = AerBackend()
- print(backend.run_circuit(circ, 100).get_counts())
- print(backend.run_circuit(averaging_circuits[0], 100).get_counts())
-
-.. preset cycle and frame gates to tailor meaningful noise
-
-Note that the :py:class:`FrameRandomisation` procedure sandwiches each cycle between ``OpType.Barrier`` operations. This is because frame gates can be combined with adjacent rotation gates to reduce gate overhead, but can not be commuted through their associated cycle as this will undo the framing process. As FrameRandomisation will lead to a blow up in the number of circuits compiled, it is recommended to run FrameRandomisation procedures after circuit optimisation techniques.
-
-
-Running a randomised protocol to achieve meaningful results requires a careful choice of cycle gates and frame gates, which the above example does not make. However, the :py:class:`PauliFrameRandomisation` class is preset with cycle gates {``OpType.CX``, ``OpType.H``, ``OpType.S``} and frame gates {``OpType.X``, ``OpType.Y``, ``OpType.Z``, ``OpType.noop``} that should.
-
-The :py:meth:`PauliFrameRandomisation.get_all_circuits` method returns circuits that tailor the noise of subcircuits comprised of cycle gates into a stochastic Pauli noise when run on a device (given some assumptions, such as additional frame gates not providing additional incoherent noise).
-
-.. jupyter-execute::
-
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
- from pytket.tailoring import PauliFrameRandomisation
-
- circ = Circuit(2).X(0).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
-
- pauli_frame_randomisation = PauliFrameRandomisation()
- averaging_circuits = pauli_frame_randomisation.get_all_circuits(circ)
-
- print('Number of PauliFrameRandomisation averaging circuits: ', len(averaging_circuits))
-
- print('\nAn example averaging circuit with frames applied to two cycles: ')
- for com in averaging_circuits[3].get_commands():
- print(com)
- print('\n')
-
- backend = AerBackend()
-
- averaging_circuits = backend.get_compiled_circuits(averaging_circuits)
- circ = backend.get_compiled_circuit(circ)
-
- pfr_counts_list = [
- res.get_counts() for res in backend.run_circuits(averaging_circuits, 50)
- ]
- # combine each averaging circuits counts into a single counts object for comparison
- pfr_counts = {}
- for counts in pfr_counts_list:
- pfr_counts = {key: pfr_counts.get(key,0) + counts.get(key,0) for key in set(pfr_counts)|set(counts)}
-
- print(pfr_counts)
- print(backend.run_circuit(circ, 50*len(averaging_circuits)).get_counts())
-
-
-For a noise free backend, we can see that the same counts distribution is returned as expected. We can use a basic noise model based on a real device to see how a realistic noise channel can change when applying :py:class:`PauliFrameRandomisation`.
-
-.. jupyter-input::
-
- from qiskit_aer.noise import NoiseModel
- from qiskit import IBMQ
- IBMQ.load_account()
-
- circ = Circuit(2).X(0).H(1).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
-
- noisy_backend = AerBackend(NoiseModel.from_backend(IBMQ.providers()[0].get_backend('ibmq_manila')))
-
- averaging_circuits = pauli_frame_randomisation.get_all_circuits(circ)
-
- averaging_circuits = noisy_backend.get_compiled_circuits(averaging_circuits)
- circ = noisy_backend.get_compiled_circuit(circ)
-
- pfr_counts_list = [res.get_counts() for res in noisy_backend.run_circuits(averaging_circuits, 50)]
- pfr_counts = {}
- for counts in pfr_counts_list:
- pfr_counts = {key: pfr_counts.get(key,0) + counts.get(key,0) for key in set(pfr_counts)|set(counts)}
-
-
- print('Noiseless Counts:', AerBackend().run_circuit(circ, 50*len(averaging_circuits).get_counts()))
- print('Base Noisy Counts:', noisy_backend.run_circuit(circ, 50*len(averaging_circuits).get_counts()))
- print('Recombined Noisy Counts using PauliFrameRandomisation:', pfr_counts)
-
-
-.. jupyter-output::
-
- Noiseless Counts: Counter({(1, 1): 6415, (1, 0): 6385})
- Base Noisy Counts: Counter({(1, 0): 6368, (1, 1): 5951, (0, 1): 253, (0, 0): 228})
- Recombined Noisy Counts using PauliFrameRandomisation: {(0, 1): 203, (0, 0): 215, (1, 0): 6194, (1, 1): 6188}
-
-
-For this simple case we observe that more shots are returning basis states not in the expected state (though it would be unwise to declare the methods efficacy from this alone).
-
-
-Given that cycle gates for :py:class:`PauliFrameRandomisation` do not form a universal gate set for the quantum circuit model, randomised protocols using :py:class:`PauliFrameRandomisation` will usually need to individually tailor many cycle instances for a given circuit. This can lead to large circuit overhead required for complete averaging, or a loss of guarantee that the resulting channel is a stochastic Pauli noise when not every frame is used.
-
-
-An alternative class, :py:class:`UniversalFrameRandomisation`, is set with cycle gates {``OpType.CX``, ``OpType.H``, ``OpType.Rz``} and frame gates {``OpType.X``, ``OpType.Y``, ``OpType.Z``, ``OpType.noop``} and so can treat a whole circuit as a single cycle if rebased appropriately. It providers averaging circuits while preserving the unitary of the circuit by changing the rotation angle of cycle ``OpType.Rz`` gates when prepending and appending frame gates, meaning that the stochastic Pauli noise property is additionally dependent on incoherent noise not being dependent on the rotation angle.
-
-.. jupyter-input::
-
- from pytket.tailoring import UniversalFrameRandomisation
-
- universal_frame_randomisation = UniversalFrameRandomisation()
-
- circ = Circuit(2).X(0).H(1).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
-
- averaging_circuits = universal_frame_randomisation.get_all_circuits(circ)
- print()
-
- averaging_circuits = noisy_backend.get_compiled_circuits(averaging_circuits)
- circ = noisy_backend.get_compiled_circuit(circ)
-
- ufr_noisy_counts_list = [res.get_counts() for res in noisy_backend.run_circuits(averaging_circuits, 800)]
- ufr_noisy_counts = {}
- for counts in ufr_noisy_counts_list:
- ufr_noisy_counts = {key: ufr_noisy_counts.get(key,0) + counts.get(key,0) for key in set(ufr_noisy_counts)|set(counts)}
-
-
- ufr_noiseless_counts_list = [res.get_counts() for res in AerBackend().run_circuits(averaging_circuits, 800)]
- ufr_noiseless_counts = {}
- for counts in ufr_noiseless_counts_list:
- ufr_noiseless_counts = {key: ufr_noiseless_counts.get(key,0) + counts.get(key,0) for key in set(ufr_noiseless_counts)|set(counts)}
-
-
- print('Noiseless Counts:', noiseless_counts)
- print('Recombined Noiseless Counts using UniversalFrameRandomisation:', ufr_noiseless_counts)
- print('Base Noisy Counts:', noisy_counts)
- print('Recombined Noisy Counts using PauliFrameRandomisation:', pfr_counts)
- print('Recombined Noisy Counts using UniversalFrameRandomisation:', ufr_noisy_counts)
-
-
-.. jupyter-output::
-
- Noiseless Counts: Counter({(1, 0): 6490, (1, 1): 6310})
- Recombined Noiseless Counts using UniversalFrameRandomisation: {(1, 0): 6440, (1, 1): 6360}
- Base Noisy Counts: Counter({(1, 0): 6298, (1, 1): 6022, (0, 1): 261, (0, 0): 219})
- Recombined Noisy Counts using PauliFrameRandomisation: {(0, 1): 240, (0, 0): 212, (1, 0): 6253, (1, 1): 6095}
- Recombined Noisy Counts using UniversalFrameRandomisation: {(0, 1): 208, (0, 0): 208, (1, 0): 6277, (1, 1): 6107}
-
-Similarly as to the previous case, more shots are returning basis states in the expected state.
-
-We can use :py:meth:`auto_rebase_pass` to create a pass that can be applied to a circuit to rebase its gates to {``OpType.CX``, ``OpType.H``, ``OpType.Rz``}, the cycle gate primitives for Universal Frame Randomisation.
-
-.. jupyter-execute::
-
- from pytket.circuit import PauliExpBox, Pauli, Circuit, OpType
- from pytket.transform import Transform
- from pytket.passes import auto_rebase_pass
- from pytket.tailoring import UniversalFrameRandomisation
-
- rebase_ufr = auto_rebase_pass({OpType.CX, OpType.H, OpType.Rz})
-
- universal_frame_randomisation = UniversalFrameRandomisation()
-
- circ = Circuit(4)
- circ.X(0)
- circ.X(1)
- circ.add_gate(
- PauliExpBox([Pauli.X, Pauli.Z, Pauli.Y, Pauli.I], 0.034), [0, 1, 2, 3]
- )
- circ.add_gate(
- PauliExpBox([Pauli.Y, Pauli.Z, Pauli.X, Pauli.I], -0.2), [0, 1, 2, 3]
- )
- circ.add_gate(
- PauliExpBox([Pauli.I, Pauli.X, Pauli.Z, Pauli.Y], 0.45), [0, 1, 2, 3]
- )
-
- Transform.DecomposeBoxes().apply(circ)
- ufr_averaging_circuits = universal_frame_randomisation.get_all_circuits(circ)
- print('Number of Universal Frame Randomisation averaging circuits without rebase: ', len(ufr_averaging_circuits))
-
- rebase_ufr.apply(circ)
- ufr_averaging_circuits = universal_frame_randomisation.get_all_circuits(circ)
- print('Number of Universal Frame Randomisation averaging circuits with rebase: ', len(ufr_averaging_circuits))
-
- ufr_averaging_circuits = universal_frame_randomisation.sample_circuits(circ, 200)
- print('Number of sampled Universal Frame Randomisation averaging circuits with rebase: ', len(ufr_averaging_circuits))
-
-
-By rebasing the circuit Universal Frame Randomisation is being applied to, we can see a significant reduction in the number of averaging circuits required. For large circuits with many cycles :py:meth:`FrameRandomisation.sample_circuits`
-can be used to sample from the full set of averaging circuits. It is recommended to use :py:meth:`FrameRandomisation.sample_circuit` over :py:meth:`FrameRandomisation.get_all_circuits` for larger circuits with many cycles as the overhead in finding frame permutations becomes significant.
-
-.. SPAM Mitigation module and how to use
-
-SPAM Mitigation
----------------
-
-
-A prominent source of noise is that occurring during State Preparation and Measurement (SPAM) in the hardware.
-
-SPAM error mitigation methods can correct for such noise through a post-processing step that modifies the output distribution measured from repeatedly sampling shots. This is possible given the assumption that SPAM noise is not dependent on the quantum computation run.
-
-By repeatedly preparing and measuring a basis state of the device, a distribution over basis states is procured. While for a perfect device the distribution would be the prepared basis state with probability 1, for devices prone to SPAM noise this distribution is perturbed and other basis states may be returned with (expected) small probability.
-
-If this process is repeated for all (or a suitable subset given many qubits won't experience correlated SPAM errors) basis states of a device, a transition matrix can be derived that describes the noisy SPAM process.
-Simply applying the inverse of this transition matrix to the distribution of a quantum state from some desired quantum computation can effectively uncompute the errors caused by SPAM noise.
-
-The :py:class:`SpamCorrecter` provides the required tools for characterising and correcting SPAM noise in this manner. A :py:class:`SpamCorrecter` object is initialised from a partition of a subset of the quantum device's qubits. Qubits are assumed to have SPAM errors which are correlated with that of other qubits in their set, but uncorrelated with the other sets.
-
-As an n-qubit device has :math:`2^n` basis states, finding the exact noisy SPAM process becomes infeasible for larger devices. However, as correlated errors are typically spatially dependent though, one can usually characterise SPAM noise well by only assuming correlated SPAM noise between nearest-neighbour qubits.
-
-The :py:class:`SpamCorrecter` object uses these subsets of qubits to produce calibration circuits.
-
-
-.. jupyter-input::
-
- from pytket.utils.spam import SpamCorrecter
- from pytket.extensions.qiskit import IBMQBackend
-
- backend = IBMQBackend("ibmq_quito")
- nodes = backend.backend_info.architecture.nodes
-
- spam_correcter = SpamCorrecter([nodes])
-
- calibration_circuits = spam_correcter.calibration_circuits()
- print('Number of calibration circuits: ' , len(calibration_circuits))
- print(calibration_circuits[1].get_commands())
-
-
-.. jupyter-output::
-
- Number of calibration circuits: 32
-
- [X node[4];, Barrier node[0], node[1], node[2], node[3], node[4];, Measure node[0] --> c[0];, Measure node[1] --> c[1];, Measure node[2] --> c[2];, Measure node[3] --> c[3];, Measure node[4] --> c[4];]
-
-
-
-Assuming SPAM correlation between all 5 qubits of the "ibmq_quito" device, there are a total of 32 calibration circuits total for constructing each basis state. Printing the commands of the second basis state preparation circuit, we see that the circuits simply apply X gates to the states of qubits initialised in the 0 state as appropriate.
-
-To display the performance of SPAM correction in a controlled environment, we can construct a noise model with measurement errors from ``qiskit-aer`` and use it to define a simulator backend with known measurement noise.
-
-First the :py:class:`SpamCorrecter` is characterised using counts results for calibration circuits executed through the noisy backend of choice using :py:meth:`SpamCorrecter.calculate_matrices`. Once characterised, noisy counts for a circuit can be corrected using :py:meth:`SpamCorrecter.correct_counts`.
-
-.. jupyter-execute::
-
- from pytket.extensions.qiskit import AerBackend
- from pytket import Circuit
- from pytket.utils.spam import SpamCorrecter
-
- from qiskit_aer.noise import NoiseModel
- from qiskit_aer.noise.errors import depolarizing_error
-
- noise_model = NoiseModel()
- noise_model.add_readout_error([[0.9, 0.1],[0.1, 0.9]], [0])
- noise_model.add_readout_error([[0.95, 0.05],[0.05, 0.95]], [1])
- noise_model.add_quantum_error(depolarizing_error(0.1, 2), ["cx"], [0, 1])
-
- noisy_backend = AerBackend(noise_model)
- noiseless_backend = AerBackend()
- spam_correcter = SpamCorrecter([noisy_backend.backend_info.architecture.nodes], noisy_backend)
- calibration_circuits = spam_correcter.calibration_circuits()
-
- char_handles = noisy_backend.process_circuits(calibration_circuits, 1000)
- char_results = noisy_backend.get_results(char_handles)
-
- spam_correcter.calculate_matrices(char_results)
-
- circ = Circuit(2).H(0).CX(0,1).measure_all()
- circ = noisy_backend.get_compiled_circuit(circ)
- noisy_handle = noisy_backend.process_circuit(circ, 1000)
- noisy_result = noisy_backend.get_result(noisy_handle)
- noiseless_handle = noiseless_backend.process_circuit(circ, 1000)
- noiseless_result = noiseless_backend.get_result(noiseless_handle)
-
- circ_parallel_measure = spam_correcter.get_parallel_measure(circ)
- corrected_counts = spam_correcter.correct_counts(noisy_result, circ_parallel_measure)
-
- print('Noisy Counts:', noisy_result.get_counts())
- print('Corrected Counts:', corrected_counts.get_counts())
- print('Noiseless Counts:', noiseless_result.get_counts())
-
-
-Despite the presence of additional noise, it is straightforward to see that the corrected counts results are closer to the expected noiseless counts than the original noisy counts. All that is required to use :py:class:`SpamCorrecter` with a real device is the interchange of :py:class:`~pytket.extensions.qiskit.AerBackend` with a real device backend, such as :py:class:`~pytket.extensions.qiskit.IBMQBackend`.
-
-
-
-
-
-
-
-.. [Wallman2015] Wallman, J., Emerson, J., 2015. Noise tailoring for scalable quantum computation via randomized compiling. Phys. Rev. A 94, 052325 (2016).
-
-.. [White2019] White, G., Hill, C., Hollenberg, L., 2019. Performance optimisation for drift-robust fidelity improvement of two-qubit gates. arXiv:1911.12096.
-
-
-
-
diff --git a/docs/manual/manual_zx.rst b/docs/manual/manual_zx.md
similarity index 61%
rename from docs/manual/manual_zx.rst
rename to docs/manual/manual_zx.md
index 725fd6da..b129fbe1 100644
--- a/docs/manual/manual_zx.rst
+++ b/docs/manual/manual_zx.md
@@ -1,40 +1,39 @@
-***********
-ZX Diagrams
-***********
+---
+file_format: mystnb
+---
-Aside from optimisation methods focussed on localised sequences of gates, the ZX-calculus has shown itself to be a useful representation for quantum operations that can highlight and exploit some specific kinds of higher-level redundancy in the structure of the circuit. In this section, we will assume the reader is familiar with the theory of ZX-calculus in terms of how to construct diagrams, apply rewrites to them, and interpret them as linear maps. We will focus on how to make use of the ZX module in ``pytket`` to help automate and scale your ideas. For a comprehensive introduction to the theory, we recommend reading van de Wetering's overview paper [vdWet2020]_ or taking a look at the resources available at `zxcalculus.com `_.
-The graph representation used in the ZX module is intended to be sufficiently generalised to support experimentation with other graphical calculi like ZH, algebraic ZX, and MBQC patterns. This includes:
+# ZX Diagrams
-* Port annotations on edges to differentiate between multiple incident edges on a vertex for asymmetric generators such as subdiagram abstractions (:py:class:`ZXBox`) or the triangle generator of algebraic ZX.
+Aside from optimisation methods focussed on localised sequences of gates, the ZX-calculus has shown itself to be a useful representation for quantum operations that can highlight and exploit some specific kinds of higher-level redundancy in the structure of the circuit. In this section, we will assume the reader is familiar with the theory of ZX-calculus in terms of how to construct diagrams, apply rewrites to them, and interpret them as linear maps. We will focus on how to make use of the ZX module in `pytket` to help automate and scale your ideas. For a comprehensive introduction to the theory, we recommend reading van de Wetering's overview paper [^cite_vdwet2020] or taking a look at the resources available at [zxcalculus.com](https://zxcalculus.com).
-* Structured generators for varied parameterisations, such as continuous real parameters of ZX spiders and discrete (Boolean) parameters of specialised Clifford generators.
+The graph representation used in the ZX module is intended to be sufficiently generalised to support experimentation with other graphical calculi like ZH, algebraic ZX, and MBQC patterns. This includes:
-* Mixed quantum-classical diagram support via annotating edges and some generators with :py:class:`QuantumType.Quantum` for doubled diagrams (shorthand notation for a pair of adjoint edges/generators) or :py:class:`QuantumType.Classical` for the singular variants (sometimes referred to as decoherent/bastard generators).
+- Port annotations on edges to differentiate between multiple incident edges on a vertex for asymmetric generators such as subdiagram abstractions ({py:class}`ZXBox`) or the triangle generator of algebraic ZX.
+- Structured generators for varied parameterisations, such as continuous real parameters of ZX spiders and discrete (Boolean) parameters of specialised Clifford generators.
+- Mixed quantum-classical diagram support via annotating edges and some generators with {py:class}`QuantumType.Quantum` for doubled diagrams (shorthand notation for a pair of adjoint edges/generators) or {py:class}`QuantumType.Classical` for the singular variants (sometimes referred to as decoherent/bastard generators).
-.. note:: Providing this flexibility comes at the expense of some efficiency in both memory and speed of operations. For data structures more focussed on the core ZX-calculus and its well-developed simplification strategies, we recommend checking out ``pyzx`` (https://github.com/Quantomatic/pyzx) and its Rust port ``quizx`` (https://github.com/Quantomatic/quizx). Some functionality for interoperation between ``pytket`` and ``pyzx`` circuits is provided in the ``pytket-pyzx`` extension package. There is no intention to support non-qubit calculi or SZX scalable notation in the near future as the additional complexity required by the data structure would introduce excessive bureaucracy to maintain during every rewrite.
+:::{note}
+Providing this flexibility comes at the expense of some efficiency in both memory and speed of operations. For data structures more focussed on the core ZX-calculus and its well-developed simplification strategies, we recommend checking out `pyzx` () and its Rust port `quizx` (). Some functionality for interoperation between `pytket` and `pyzx` circuits is provided in the `pytket-pyzx` extension package. There is no intention to support non-qubit calculi or SZX scalable notation in the near future as the additional complexity required by the data structure would introduce excessive bureaucracy to maintain during every rewrite.
+:::
-Generator Types
----------------
+## Generator Types
Before we start building diagrams, it is useful to cover the kinds of generators we can populate them with. A full list and details can be found in the API reference.
-* Boundary generators: :py:class:`ZXType.Input`, :py:class:`ZXType.Output`, :py:class:`ZXType.Open`. These are used to turn some edges of the diagram into half-edges, indicating the input, output, or unspecified boundaries of the diagram. These will be maintained in an ordered list in a :py:class:`ZXDiagram` to specify the intended order of indices when interpreting the diagram as a tensor.
-
-* Symmetric ZXH generators: :py:class:`ZXType.ZSpider`, :py:class:`ZXType.XSpider`, :py:class:`ZXType.HBox`. These are the familiar generators from standard literature. All incident edges are exchangeable, so no port information is used (all edges attach at port ``None``). The degenerate :py:class:`QuantumType.Classical` variants support both :py:class:`QuantumType.Classical` and :py:class:`QuantumType.Quantum` incident edges, with the latter being treated as two distinct edges.
+- Boundary generators: {py:class}`ZXType.Input`, {py:class}`ZXType.Output`, {py:class}`ZXType.Open`. These are used to turn some edges of the diagram into half-edges, indicating the input, output, or unspecified boundaries of the diagram. These will be maintained in an ordered list in a {py:class}`ZXDiagram` to specify the intended order of indices when interpreting the diagram as a tensor.
+- Symmetric ZXH generators: {py:class}`ZXType.ZSpider`, {py:class}`ZXType.XSpider`, {py:class}`ZXType.HBox`. These are the familiar generators from standard literature. All incident edges are exchangeable, so no port information is used (all edges attach at port `None`). The degenerate {py:class}`QuantumType.Classical` variants support both {py:class}`QuantumType.Classical` and {py:class}`QuantumType.Quantum` incident edges, with the latter being treated as two distinct edges.
+- MBQC generators: {py:class}`ZXType.XY`, {py:class}`ZXType.XZ`, {py:class}`ZXType.YZ`, {py:class}`ZXType.PX`, {py:class}`ZXType.PY`, {py:class}`ZXType.PZ`. These represent qubits in a measurement pattern that are postselected into the correct outcome (i.e. we do not consider errors and corrections as these can be inferred by flow detection). Each of them can be thought of as shorthand for a {py:class}`ZXType.ZSpider` with an adjacent spider indicating the postselection projector. The different types indicate either planar measurements with a continuous-parameter angle or a Pauli measurement with a Boolean angle selecting which outcome is the intended. Entanglement between qubits can be established with a {py:class}`ZXWireType.H` edge between vertices, with {py:class}`ZXWireType.Basic` edges connecting to a {py:class}`ZXType.Input` to indicate input qubits. Unmeasured output qubits can be indicated using a {py:class}`ZXType.PX` vertex (essentially a zero phase {py:class}`ZXType.ZSpider`) attached to a {py:class}`ZXType.Output`.
+- {py:class}`ZXType.ZXBox`. Similar to the concept of a {py:class}`~pytket.circuit.CircBox` for circuits, a {py:class}`ZXBox` contains another {py:class}`ZXDiagram` abstracted away which can later be expanded in-place. The ports and {py:class}`QuantumType` of incident edges will align with the indices and types of the boundaries on the inner diagram.
-* MBQC generators: :py:class:`ZXType.XY`, :py:class:`ZXType.XZ`, :py:class:`ZXType.YZ`, :py:class:`ZXType.PX`, :py:class:`ZXType.PY`, :py:class:`ZXType.PZ`. These represent qubits in a measurement pattern that are postselected into the correct outcome (i.e. we do not consider errors and corrections as these can be inferred by flow detection). Each of them can be thought of as shorthand for a :py:class:`ZXType.ZSpider` with an adjacent spider indicating the postselection projector. The different types indicate either planar measurements with a continuous-parameter angle or a Pauli measurement with a Boolean angle selecting which outcome is the intended. Entanglement between qubits can be established with a :py:class:`ZXWireType.H` edge between vertices, with :py:class:`ZXWireType.Basic` edges connecting to a :py:class:`ZXType.Input` to indicate input qubits. Unmeasured output qubits can be indicated using a :py:class:`ZXType.PX` vertex (essentially a zero phase :py:class:`ZXType.ZSpider`) attached to a :py:class:`ZXType.Output`.
+Each generator in a diagram is described by a {py:class}`ZXGen` object, or rather an object of one of its concrete subtypes depending on the data needed to describe the generator.
-* :py:class:`ZXType.ZXBox`. Similar to the concept of a :py:class:`~pytket.circuit.CircBox` for circuits, a :py:class:`ZXBox` contains another :py:class:`ZXDiagram` abstracted away which can later be expanded in-place. The ports and :py:class:`QuantumType` of incident edges will align with the indices and types of the boundaries on the inner diagram.
-
-Each generator in a diagram is described by a :py:class:`ZXGen` object, or rather an object of one of its concrete subtypes depending on the data needed to describe the generator.
-
-Creating Diagrams
------------------
+## Creating Diagrams
Let's start by making the standard diagram for the qubit teleportation algorithm to showcase the capacity for mixed quantum-classical diagrams. Assuming that the Bell pair will be written in as initialised ancillae rather than open inputs, we just need to start with a diagram with just one quantum input and one quantum output.
-.. jupyter-execute::
+
+```{code-cell} ipython3
import pytket
from pytket.zx import ZXDiagram, ZXType, QuantumType, ZXWireType
@@ -42,10 +41,12 @@ Let's start by making the standard diagram for the qubit teleportation algorithm
tele = ZXDiagram(1, 1, 0, 0)
gv.Source(tele.to_graphviz_str())
+```
-We will choose to represent the Bell state as a cup (i.e. an edge connecting one side of the CX to the first correction). In terms of vertices, we need two for the CX gate, two for the measurements, and four for the encoding and application of corrections. The CX and corrections need to be coherent operations so will be :py:class:`QuantumType.Quantum` as opposed to the measurements and encodings. We can then link them up by adding edges of the appropriate :py:class:`QuantumType`. The visualisations will show :py:class:`QuantumType.Quantum` generators and edges with thick lines and :py:class:`QuantumType.Classical` with thinner lines as per standard notation conventions.
+We will choose to represent the Bell state as a cup (i.e. an edge connecting one side of the CX to the first correction). In terms of vertices, we need two for the CX gate, two for the measurements, and four for the encoding and application of corrections. The CX and corrections need to be coherent operations so will be {py:class}`QuantumType.Quantum` as opposed to the measurements and encodings. We can then link them up by adding edges of the appropriate {py:class}`QuantumType`. The visualisations will show {py:class}`QuantumType.Quantum` generators and edges with thick lines and {py:class}`QuantumType.Classical` with thinner lines as per standard notation conventions.
-.. jupyter-execute::
+
+```{code-cell} ipython3
(in_v, out_v) = tele.get_boundary()
cx_c = tele.add_vertex(ZXType.ZSpider)
@@ -79,10 +80,12 @@ We will choose to represent the Bell state as a cup (i.e. an edge connecting one
tele.add_wire(z_correct, out_v)
gv.Source(tele.to_graphviz_str())
+```
+
+We can use this teleportation algorithm as a component in a larger diagram using a {py:class}`ZXBox`. Here, we insert it in the middle of a two qubit circuit.
-We can use this teleportation algorithm as a component in a larger diagram using a :py:class:`ZXBox`. Here, we insert it in the middle of a two qubit circuit.
-.. jupyter-execute::
+```{code-cell} ipython3
circ_diag = ZXDiagram(2, 1, 0, 1)
qin0 = circ_diag.get_boundary(ZXType.Input)[0]
@@ -116,39 +119,39 @@ We can use this teleportation algorithm as a component in a larger diagram using
circ_diag.add_wire(x_meas, cout, type=ZXWireType.H, qtype=QuantumType.Classical)
gv.Source(circ_diag.to_graphviz_str())
+```
-.. Validity conditions of a diagram
-
-As the entire graph data structure is exposed, it is very easy to construct objects that cannot be interpreted as a valid diagram. This is to be expected from intermediate states during the construction of a diagram or in the middle of applying a rewrite, before the state is returned to something sensible. The :py:meth:`ZXDiagram.check_validity()` method will perform a number of sanity checks on a given diagram object and it will raise an exception if any of them fail. We recommend using this during debugging to check that the diagram is not left in an invalid state. A diagram is deemed valid if it satisfies each of the following:
+% Validity conditions of a diagram
-* Any vertex with of a boundary type (:py:class:`ZXType.Input`, :py:class:`ZXType.Output`, or :py:class:`ZXType.Open`) must have degree 1 (they uniquely identify a single edge as open) and exist in the boundary list.
+As the entire graph data structure is exposed, it is very easy to construct objects that cannot be interpreted as a valid diagram. This is to be expected from intermediate states during the construction of a diagram or in the middle of applying a rewrite, before the state is returned to something sensible. The {py:meth}`ZXDiagram.check_validity()` method will perform a number of sanity checks on a given diagram object and it will raise an exception if any of them fail. We recommend using this during debugging to check that the diagram is not left in an invalid state. A diagram is deemed valid if it satisfies each of the following:
-* Undirected vertices (those without port information, such as :py:class:`ZXType.ZSpider`, or :py:class:`ZXType.HBox`) have no port annotations on incident edges.
+- Any vertex with of a boundary type ({py:class}`ZXType.Input`, {py:class}`ZXType.Output`, or {py:class}`ZXType.Open`) must have degree 1 (they uniquely identify a single edge as open) and exist in the boundary list.
+- Undirected vertices (those without port information, such as {py:class}`ZXType.ZSpider`, or {py:class}`ZXType.HBox`) have no port annotations on incident edges.
+- Directed vertices (such as {py:class}`ZXType.Triangle` or {py:class}`ZXType.ZXBox`) have exactly one incident edge at each port.
+- The {py:class}`QuantumType` of each edge is compatible with the vertices and ports they attach to. For example, a {py:class}`ZXType.ZSpider` with {py:class}`QuantumType.Quantum` requires all incident edges to also have {py:class}`QuantumType.Quantum`, whereas a {py:class}`QuantumType.Classical` vertex accepts any edge, and for a {py:class}`ZXType.ZXBox` the {py:class}`QuantumType` of an edge must match the signature at the corresponding port.
-* Directed vertices (such as :py:class:`ZXType.Triangle` or :py:class:`ZXType.ZXBox`) have exactly one incident edge at each port.
-
-* The :py:class:`QuantumType` of each edge is compatible with the vertices and ports they attach to. For example, a :py:class:`ZXType.ZSpider` with :py:class:`QuantumType.Quantum` requires all incident edges to also have :py:class:`QuantumType.Quantum`, whereas a :py:class:`QuantumType.Classical` vertex accepts any edge, and for a :py:class:`ZXType.ZXBox` the :py:class:`QuantumType` of an edge must match the signature at the corresponding port.
-
-Tensor Evaluation
------------------
+## Tensor Evaluation
Evaluating a diagram as a tensor is beneficial for practical use cases in scalar diagram evaluation (e.g. as part of expectation value calculations or simulation tasks), or for verification of correctness of diagram designs or rewrites. Evaluation is performed by building a tensor network out of the definitions of the generators and using a contraction strategy to reduce it down to a single tensor. Each diagram carries a global scalar which is multiplied into the tensor.
-.. Mixed diagrams and different evaluation methods (global phase/scalar); reasons to use Quantum or Classical
+% Mixed diagrams and different evaluation methods (global phase/scalar); reasons to use Quantum or Classical
+
+As the pytket ZX diagrams represent mixed diagrams, this impacts the interpretation of the tensors. Traditionally, we expect each edge of a ZX diagram to have dimension 2. This is the case for {py:class}`QuantumType.Classical` edges, but since {py:class}`QuantumType.Quantum` edges represent a pair via doubling, they instead have dimension 4. The convention set by density matrix notation is to split this into two different indices, so {py:meth}`tensor_from_mixed_diagram()` will first expand the doubling notation in the diagram explicitly to give a diagram with only {py:class}`QuantumType.Classical` edges and then evaluate it, meaning there will be an index for each original {py:class}`QuantumType.Quantum` edge and a new one for its conjugate. In particular, this will increase the number of boundary edges and therefore the expected rank of the overall tensor. The ordering of the indices will primarily follow the boundary order in the original diagram, subordered by doubling index for each {py:class}`QuantumType.Quantum` boundary as in the following example.
-As the pytket ZX diagrams represent mixed diagrams, this impacts the interpretation of the tensors. Traditionally, we expect each edge of a ZX diagram to have dimension 2. This is the case for :py:class:`QuantumType.Classical` edges, but since :py:class:`QuantumType.Quantum` edges represent a pair via doubling, they instead have dimension 4. The convention set by density matrix notation is to split this into two different indices, so :py:meth:`tensor_from_mixed_diagram()` will first expand the doubling notation in the diagram explicitly to give a diagram with only :py:class:`QuantumType.Classical` edges and then evaluate it, meaning there will be an index for each original :py:class:`QuantumType.Quantum` edge and a new one for its conjugate. In particular, this will increase the number of boundary edges and therefore the expected rank of the overall tensor. The ordering of the indices will primarily follow the boundary order in the original diagram, subordered by doubling index for each :py:class:`QuantumType.Quantum` boundary as in the following example.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.zx.tensor_eval import tensor_from_mixed_diagram
ten = tensor_from_mixed_diagram(circ_diag)
# Indices are (qin0, qin0_conj, qin1, qin1_conj, qout, qout_conj, cout)
print(ten.shape)
print(ten[:, :, 1, 1, 0, 0, :].round(4))
+```
-In many cases, we work with pure quantum diagrams. This doubling would cause substantial blowup in time and memory for evaluation, as well as making the tensor difficult to navigate for large diagrams. :py:meth:`tensor_from_quantum_diagram()` achieves the same as converting all :py:class:`QuantumType.Quantum` components to :py:class:`QuantumType.Classical`, meaning every edge is reduced down to dimension 2. Since the global scalar is maintained with respect to a doubled diagram, its square root is incorporated into the tensor, though we do not maintain the coherent global phase of a pure quantum diagram in this way. For diagrams like this, :py:meth:`unitary_from_quantum_diagram()` reformats the tensor into the conventional unitary (with big-endian indexing).
+In many cases, we work with pure quantum diagrams. This doubling would cause substantial blowup in time and memory for evaluation, as well as making the tensor difficult to navigate for large diagrams. {py:meth}`tensor_from_quantum_diagram()` achieves the same as converting all {py:class}`QuantumType.Quantum` components to {py:class}`QuantumType.Classical`, meaning every edge is reduced down to dimension 2. Since the global scalar is maintained with respect to a doubled diagram, its square root is incorporated into the tensor, though we do not maintain the coherent global phase of a pure quantum diagram in this way. For diagrams like this, {py:meth}`unitary_from_quantum_diagram()` reformats the tensor into the conventional unitary (with big-endian indexing).
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.zx.tensor_eval import tensor_from_quantum_diagram, unitary_from_quantum_diagram
u_diag = ZXDiagram(2, 2, 0, 0)
@@ -167,72 +170,83 @@ In many cases, we work with pure quantum diagrams. This doubling would cause sub
print(tensor_from_quantum_diagram(u_diag).round(4))
print(unitary_from_quantum_diagram(u_diag).round(4))
+```
+
+Similarly, one may use {py:meth}`density_matrix_from_cptp_diagram()` to obtain a density matrix when all boundaries are {py:class}`QuantumType.Quantum` but the diagram itself contains mixed components. When input boundaries exist, this gives the density matrix under the Choi-Jamiołkovski isomorphism. For example, we can verify that our teleportation diagram from earlier really does reduce to the identity (recall that the Choi-Jamiołkovski isomorphism maps the identity channel to a Bell state).
-Similarly, one may use :py:meth:`density_matrix_from_cptp_diagram()` to obtain a density matrix when all boundaries are :py:class:`QuantumType.Quantum` but the diagram itself contains mixed components. When input boundaries exist, this gives the density matrix under the Choi-Jamiołkovski isomorphism. For example, we can verify that our teleportation diagram from earlier really does reduce to the identity (recall that the Choi-Jamiołkovski isomorphism maps the identity channel to a Bell state).
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.zx.tensor_eval import density_matrix_from_cptp_diagram
print(density_matrix_from_cptp_diagram(tele))
+```
+
+% Tensor indices, unitaries and states; initialisation and post-selection
-.. Tensor indices, unitaries and states; initialisation and post-selection
+Another way to potentially reduce the computational load for tensor evaluation is to fix basis states at the boundary vertices, corresponding to initialising inputs or post-selecting on outputs. There are utility methods for setting all inputs/outputs or specific boundary vertices to Z-basis states. For example, we can recover statevector simulation of a quantum circuit by setting all inputs to the zero state and calling {py:meth}`unitary_from_quantum_diagram()`.
-Another way to potentially reduce the computational load for tensor evaluation is to fix basis states at the boundary vertices, corresponding to initialising inputs or post-selecting on outputs. There are utility methods for setting all inputs/outputs or specific boundary vertices to Z-basis states. For example, we can recover statevector simulation of a quantum circuit by setting all inputs to the zero state and calling :py:meth:`unitary_from_quantum_diagram()`.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.zx.tensor_eval import fix_inputs_to_binary_state
state_diag = fix_inputs_to_binary_state(u_diag, [1, 0])
print(unitary_from_quantum_diagram(state_diag).round(4))
+```
-.. Note on location in test folder
+% Note on location in test folder
-Graph Traversal, Inspection, and Manual Rewriting
--------------------------------------------------
+## Graph Traversal, Inspection, and Manual Rewriting
The ability to build static diagrams is fine for visualisation and simulation needs, but the bulk of interest in graphical calculi is in rewriting for simplification. For this, it is enough to traverse the graph to search for relevant subgraphs and manipulate the graph in place. We will illustrate this by gradually rewriting the teleportation diagram to be the identity.
-.. jupyter-execute::
+
+```{code-cell} ipython3
gv.Source(tele.to_graphviz_str())
+```
+
+% Boundaries (ordering, types and incident edges, not associated to UnitIDs)
-.. Boundaries (ordering, types and incident edges, not associated to UnitIDs)
+The boundary vertices offer a useful starting point for traversals. Each {py:class}`ZXDiagram` maintains an ordered list of its boundaries to help distinguish them (note that this is different from the {py:class}`UnitID` system used by {py:class}`~pytket.circuit.Circuit` objects), which we can retrieve with {py:meth}`ZXDiagram.get_boundary()`. Each boundary vertex should have a unique incident edge which we can access through {py:meth}`ZXDiagram.adj_wires()`.
-The boundary vertices offer a useful starting point for traversals. Each :py:class:`ZXDiagram` maintains an ordered list of its boundaries to help distinguish them (note that this is different from the :py:class:`UnitID` system used by :py:class:`~pytket.circuit.Circuit` objects), which we can retrieve with :py:meth:`ZXDiagram.get_boundary()`. Each boundary vertex should have a unique incident edge which we can access through :py:meth:`ZXDiagram.adj_wires()`.
+% Semi-ordered edges, incident edge order and traversal, edge properties and editing
-.. Semi-ordered edges, incident edge order and traversal, edge properties and editing
+Once we have an edge, we can inspect and modify its properties, specifically its {py:class}`QuantumType` with {py:meth}`ZXDiagram.get/set_wire_qtype()` (whether it represents a single wire or a pair of wires under the doubling construction) and {py:class}`ZXWireType` with {py:meth}`ZXDiagram.get/set_wire_type()` (whether it is equivalent to an identity process or a Hadamard gate). To change the end points of a wire (even just moving it to another port on the same vertex), it is conventional to remove it and create a new wire.
-Once we have an edge, we can inspect and modify its properties, specifically its :py:class:`QuantumType` with :py:meth:`ZXDiagram.get/set_wire_qtype()` (whether it represents a single wire or a pair of wires under the doubling construction) and :py:class:`ZXWireType` with :py:meth:`ZXDiagram.get/set_wire_type()` (whether it is equivalent to an identity process or a Hadamard gate). To change the end points of a wire (even just moving it to another port on the same vertex), it is conventional to remove it and create a new wire.
-.. jupyter-execute::
+```{code-cell} ipython3
(in_v, out_v) = tele.get_boundary()
in_edge = tele.adj_wires(in_v)[0]
print(tele.get_wire_qtype(in_edge))
print(tele.get_wire_type(in_edge))
+```
+
+The diagram is presented as an undirected graph. We can inspect the end points of an edge with {py:meth}`ZXDiagram.get_wire_ends()`, which returns pairs of vertex and port. If we simply wish to traverse the edge to the next vertex, we use {py:meth}`ZXDiagram.other_end()`. Or we can skip wire traversal altogether using {py:meth}`ZXDiagram.neighbours()` to enumerate the neighbours of a given vertex. This is mostly useful when the wires in a diagram have a consistent form, such as in a graphlike or MBQC diagram (every wire is a Hadamard except for boundary wires).
-The diagram is presented as an undirected graph. We can inspect the end points of an edge with :py:meth:`ZXDiagram.get_wire_ends()`, which returns pairs of vertex and port. If we simply wish to traverse the edge to the next vertex, we use :py:meth:`ZXDiagram.other_end()`. Or we can skip wire traversal altogether using :py:meth:`ZXDiagram.neighbours()` to enumerate the neighbours of a given vertex. This is mostly useful when the wires in a diagram have a consistent form, such as in a graphlike or MBQC diagram (every wire is a Hadamard except for boundary wires).
+If you are searching the diagram for a pattern that is simple enough that a full traversal would be excessive, {py:class}`ZXDiagram.vertices` and {py:class}`ZXDiagram.wires` return lists of all vertices or edges in the diagram at that moment (in a deterministic but not semantically relevant order) which you can iterate over to search the graph quickly. Be aware that inserting or removing components of the diagram during iteration will not update these lists.
-If you are searching the diagram for a pattern that is simple enough that a full traversal would be excessive, :py:class:`ZXDiagram.vertices` and :py:class:`ZXDiagram.wires` return lists of all vertices or edges in the diagram at that moment (in a deterministic but not semantically relevant order) which you can iterate over to search the graph quickly. Be aware that inserting or removing components of the diagram during iteration will not update these lists.
-.. jupyter-execute::
+```{code-cell} ipython3
cx_c = tele.other_end(in_edge, in_v)
assert tele.get_wire_ends(in_edge) == ((in_v, None), (cx_c, None))
for v in tele.vertices:
print(tele.get_zxtype(v))
+```
-Using this, we can scan our diagram for adjacent spiders of the same colour connected by a basic edge to apply spider fusion. In general, this will require us to also inspect the generators of the vertex to be able to add the phases and update the :py:class:`QuantumType` in case of merging with a :py:class:`QuantumType.Classical` spider.
+Using this, we can scan our diagram for adjacent spiders of the same colour connected by a basic edge to apply spider fusion. In general, this will require us to also inspect the generators of the vertex to be able to add the phases and update the {py:class}`QuantumType` in case of merging with a {py:class}`QuantumType.Classical` spider.
-.. Vertex contents, generators, and editing vertex
+% Vertex contents, generators, and editing vertex
-Similar to edges, each vertex contains a :py:class:`ZXGen` object describing the particular generator it represents which we can retrieve using :py:meth:`ZXDiagram.get_vertex_ZXGen()`. As each kind of generator has different data, when using a diagram with many kinds of generators it is useful to inspect the :py:class:`ZXType` or the subclass of :py:class:`ZXGen` first. For example, if :py:meth:`ZXDiagram.get_zxtype()` returns :py:class:`ZXType.ZSpider`, we know the generator is a :py:class:`PhasedGen` and hence has the :py:class:`PhasedGen.param` field describing the phase of the spider.
+Similar to edges, each vertex contains a {py:class}`ZXGen` object describing the particular generator it represents which we can retrieve using {py:meth}`ZXDiagram.get_vertex_ZXGen()`. As each kind of generator has different data, when using a diagram with many kinds of generators it is useful to inspect the {py:class}`ZXType` or the subclass of {py:class}`ZXGen` first. For example, if {py:meth}`ZXDiagram.get_zxtype()` returns {py:class}`ZXType.ZSpider`, we know the generator is a {py:class}`PhasedGen` and hence has the {py:class}`PhasedGen.param` field describing the phase of the spider.
-Each generator object is immutable, so updating a vertex requires creating a new :py:class:`ZXGen` object with the desired properties and passing it to :py:meth:`ZXDiagram.set_vertex_ZXGen()`.
+Each generator object is immutable, so updating a vertex requires creating a new {py:class}`ZXGen` object with the desired properties and passing it to {py:meth}`ZXDiagram.set_vertex_ZXGen()`.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.zx import PhasedGen
@@ -244,15 +258,15 @@ Each generator object is immutable, so updating a vertex requires creating a new
for w in tele.adj_wires(v):
if tele.get_wire_type(w) != ZXWireType.Basic:
continue
-
+
n = tele.other_end(w, v)
if tele.get_zxtype(n) != tele.get_zxtype(v):
continue
-
+
# Match found, copy n's edges onto v
for nw in tele.adj_wires(n):
if nw != w:
- # We know all vertices here are symmetric generators so we
+ # We know all vertices here are symmetric generators so we
# don't need to care about port information
nn = tele.other_end(nw, n)
wtype = tele.get_wire_type(nw)
@@ -266,14 +280,16 @@ Each generator object is immutable, so updating a vertex requires creating a new
# Remove n
tele.remove_vertex(n)
removed.append(n)
-
+
fuse()
-
+
gv.Source(tele.to_graphviz_str())
+```
Similarly, we can scan for a pair of adjacent basic edges between a green and a red spider for the strong complementarity rule.
-.. jupyter-execute::
+
+```{code-cell} ipython3
def strong_comp():
gr_edges = dict()
@@ -286,7 +302,7 @@ Similarly, we can scan for a pair of adjacent basic edges between a green and a
gr_match = (u, v)
elif tele.get_zxtype(u) == ZXType.XSpider and tele.get_zxtype(v) == ZXType.ZSpider:
gr_match = (v, u)
-
+
if gr_match:
if gr_match in gr_edges:
# Found a matching pair, remove them
@@ -297,14 +313,16 @@ Similarly, we can scan for a pair of adjacent basic edges between a green and a
else:
# Record the edge for later
gr_edges[gr_match] = w
-
+
strong_comp()
gv.Source(tele.to_graphviz_str())
+```
-Finally, we write a procedure that finds spiders of degree 2 which act like an identity. We need to check that the phase on the spider is zero, and that the :py:class:`QuantumType` of the generator matches those of the incident edges (so we don't accidentally remove decoherence spiders).
+Finally, we write a procedure that finds spiders of degree 2 which act like an identity. We need to check that the phase on the spider is zero, and that the {py:class}`QuantumType` of the generator matches those of the incident edges (so we don't accidentally remove decoherence spiders).
-.. jupyter-execute::
+
+```{code-cell} ipython3
def id_remove():
for v in tele.vertices:
@@ -318,38 +336,45 @@ Finally, we write a procedure that finds spiders of degree 2 which act like an i
wtype = ZXWireType.H if (tele.get_wire_type(ws[0]) == ZXWireType.H) != (tele.get_wire_type(ws[1]) == ZXWireType.H) else ZXWireType.Basic
tele.add_wire(n0, n1, wtype, spid.qtype)
tele.remove_vertex(v)
-
+
id_remove()
gv.Source(tele.to_graphviz_str())
+```
+
-.. jupyter-execute::
+```{code-cell} ipython3
fuse()
gv.Source(tele.to_graphviz_str())
+```
-.. jupyter-execute::
+
+```{code-cell} ipython3
strong_comp()
gv.Source(tele.to_graphviz_str())
+```
+
-.. jupyter-execute::
+```{code-cell} ipython3
id_remove()
gv.Source(tele.to_graphviz_str())
+```
-.. Removing vertices and edges versus editing in-place
+% Removing vertices and edges versus editing in-place
A number of other methods for inspecting and traversing a diagram are available and can be found in the API reference.
-Built-in Rewrite Passes
------------------------
+## Built-in Rewrite Passes
-.. Not just individual rewrites but maximal (not necessarily exhaustive) applications
+% Not just individual rewrites but maximal (not necessarily exhaustive) applications
The pytket ZX module comes with a handful of common rewrite procedures built-in to prevent the need to write manual traversals in many cases. These procedures work in a similar way to the pytket compilation passes in applying a particular strategy across the entire diagram, saving computational time by potentially applying many rewrites in a single traversal. In the cases where there are overlapping patterns or rewrites that introduce new target patterns in the output diagram, these rewrites may not always be applied exhaustively to save time backtracking.
-.. jupyter-execute::
+
+```{code-cell} ipython3
# This diagram follows from section A of https://arxiv.org/pdf/1902.03178.pdf
diag = ZXDiagram(4, 4, 0, 0)
@@ -413,90 +438,103 @@ The pytket ZX module comes with a handful of common rewrite procedures built-in
diag.check_validity()
gv.Source(diag.to_graphviz_str())
+```
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket.zx import Rewrite
-
+
Rewrite.red_to_green().apply(diag)
Rewrite.spider_fusion().apply(diag)
Rewrite.io_extension().apply(diag)
gv.Source(diag.to_graphviz_str())
+```
+
-.. jupyter-execute::
+```{code-cell} ipython3
Rewrite.reduce_graphlike_form().apply(diag)
gv.Source(diag.to_graphviz_str())
+```
-.. Intended to support common optimisation strategies; focussed on reducing to specific forms and work in graphlike form
+% Intended to support common optimisation strategies; focussed on reducing to specific forms and work in graphlike form
The particular rewrites available are intended to support common optimisation strategies. In particular, they mostly focus on converting a diagram to graphlike form and working on graphlike diagrams to reduce the number of vertices as much as possible. These have close correspondences with MBQC patterns, and the rewrites preserve the existence of flow, which helps guarantee an efficient extraction procedure.
-.. May not work as intended if diagram is not in intended form, especially for classical or mixed diagrams
+% May not work as intended if diagram is not in intended form, especially for classical or mixed diagrams
-.. warning:: Because of the focus on strategies using graphlike diagrams, many of the rewrites expect the inputs to be of a particular form. This may cause some issues if you attempt to apply them to diagrams that aren't in the intended form, especially when working with classical or mixed diagrams.
+:::{warning}
+Because of the focus on strategies using graphlike diagrams, many of the rewrites expect the inputs to be of a particular form. This may cause some issues if you attempt to apply them to diagrams that aren't in the intended form, especially when working with classical or mixed diagrams.
+:::
-.. Types (decompositions into generating sets, graphlike form, graphlike reduction, MBQC)
+% Types (decompositions into generating sets, graphlike form, graphlike reduction, MBQC)
The rewrite passes can be broken down into a few categories depending on the form of the diagrams expected and the function of the passes. Full descriptions of each pass are given in the API reference.
+
=================================== ===========================================
-Decompositions into generating sets
- :py:meth:`Rewrite.decompose_boxes()`,
- :py:meth:`Rewrite.basic_wires()`,
- :py:meth:`Rewrite.rebase_to_zx()`,
+Decompositions into generating sets
+ :py:meth:`Rewrite.decompose_boxes()`,
+ :py:meth:`Rewrite.basic_wires()`,
+ :py:meth:`Rewrite.rebase_to_zx()`,
:py:meth:`Rewrite.rebase_to_mbqc()`
-Rewriting into graphlike form
+Rewriting into graphlike form
:py:meth:`Rewrite.red_to_green()`,
:py:meth:`Rewrite.spider_fusion()`,
:py:meth:`Rewrite.self_loop_removal()`,
:py:meth:`Rewrite.parallel_h_removal()`,
:py:meth:`Rewrite.separate_boundaries()`,
:py:meth:`Rewrite.io_extension()`
-Reduction within graphlike form
+Reduction within graphlike form
:py:meth:`Rewrite.remove_interior_cliffords()`,
- :py:meth:`Rewrite.remove_interior_paulis()`,
- :py:meth:`Rewrite.gadgetise_interior_paulis()`,
- :py:meth:`Rewrite.merge_gadgets()`,
+ :py:meth:`Rewrite.remove_interior_paulis()`,
+ :py:meth:`Rewrite.gadgetise_interior_paulis()`,
+ :py:meth:`Rewrite.merge_gadgets()`,
:py:meth:`Rewrite.extend_at_boundary_paulis()`
-MBQC
- :py:meth:`Rewrite.extend_for_PX_outputs()`,
+MBQC
+ :py:meth:`Rewrite.extend_for_PX_outputs()`,
:py:meth:`Rewrite.internalise_gadgets()`
-Composite sequences
+Composite sequences
:py:meth:`Rewrite.to_graphlike_form()`,
:py:meth:`Rewrite.reduce_graphlike_form()`,
:py:meth:`Rewrite.to_MBQC_diag()`
=================================== ===========================================
+```
-.. Current implementations may not track global scalar; semantics is only preserved up to scalar; warning if attempting to use for scalar diagram evaluation
+% Current implementations may not track global scalar; semantics is only preserved up to scalar; warning if attempting to use for scalar diagram evaluation
-.. warning:: Current implementations of rewrite passes may not track the global scalar. Semantics of diagrams is only preserved up to scalar. This is fine for simplification of states or unitaries as they can be renormalised but this may cause issues if attempting to use rewrites for scalar diagram evaluation.
+:::{warning}
+Current implementations of rewrite passes may not track the global scalar. Semantics of diagrams is only preserved up to scalar. This is fine for simplification of states or unitaries as they can be renormalised but this may cause issues if attempting to use rewrites for scalar diagram evaluation.
+:::
-MBQC Flow Detection
--------------------
+## MBQC Flow Detection
-.. MBQC form of diagrams
+% MBQC form of diagrams
So far, we have focussed mostly on the circuit model of quantum computing, but the ZX module is also geared towards assisting for MBQC. The most practical measurement patterns are those with uniform, stepwise, strong determinism - that is, performing an individual measurement and its associated corrections will yield exactly the same residual state, and furthermore this is the case for any choice of angle parameter the qubit is measured in (within a particular plane of the Bloch sphere or choice of polarity of a Pauli measurement, according to the label of the measurement). In this case, the order of measurements and corrections can be described by a Flow over the entanglement graph.
-.. MBQC diagrams only show intended branch, order and corrections handled by flow
+% MBQC diagrams only show intended branch, order and corrections handled by flow
+
+When using the ZX module to represent measurement patterns, we care about representing the semantics and so it is sufficient to consider post-selecting the intended branch outcome at each qubit. This simplifies the diagram by eliminating the corrections and any need to track the order of measurements internally to the diagram. Instead, we may track these externally using a {py:class}`Flow` object.
-When using the ZX module to represent measurement patterns, we care about representing the semantics and so it is sufficient to consider post-selecting the intended branch outcome at each qubit. This simplifies the diagram by eliminating the corrections and any need to track the order of measurements internally to the diagram. Instead, we may track these externally using a :py:class:`Flow` object.
+Each of the MBQC {py:class}`ZXType` options represent a qubit that is initialised and post-selected into the plane/Pauli specified by the type, at the angle/polarity given by the parameter of the {py:class}`ZXGen`. Entanglement between these qubits is given by {py:class}`ZXWireType.H` edges, representing CZ gates. We identify input and output qubits using {py:class}`ZXWireType.Basic` edges connecting them to {py:class}`ZXType.Input` or {py:class}`ZXType.Output` vertices (since output qubits are unmeasured, their semantics as tensors are equivalent to {py:class}`ZXType.PX` vertices with `False` polarity). The {py:meth}`Rewrite.to_MBQC_diag()` rewrite will transform any ZX diagram into one of this form.
-Each of the MBQC :py:class:`ZXType` options represent a qubit that is initialised and post-selected into the plane/Pauli specified by the type, at the angle/polarity given by the parameter of the :py:class:`ZXGen`. Entanglement between these qubits is given by :py:class:`ZXWireType.H` edges, representing CZ gates. We identify input and output qubits using :py:class:`ZXWireType.Basic` edges connecting them to :py:class:`ZXType.Input` or :py:class:`ZXType.Output` vertices (since output qubits are unmeasured, their semantics as tensors are equivalent to :py:class:`ZXType.PX` vertices with ``False`` polarity). The :py:meth:`Rewrite.to_MBQC_diag()` rewrite will transform any ZX diagram into one of this form.
-.. jupyter-execute::
+```{code-cell} ipython3
Rewrite.to_MBQC_diag().apply(diag)
gv.Source(diag.to_graphviz_str())
+```
+
+% Causal flow, gflow, Pauli flow (completeness of extended Pauli flow and hence Pauli flow)
-.. Causal flow, gflow, Pauli flow (completeness of extended Pauli flow and hence Pauli flow)
+Given a ZX diagram in MBQC form, there are algorithms that can find a suitable {py:class}`Flow` if one exists. Since there are several classifications of flow (e.g. causal flow, gflow, Pauli flow, extended Pauli flow) with varying levels of generality, we offer multiple algorithms for identifying them. For example, any diagram supporting a uniform, stepwise, strongly deterministic measurement and correction scheme will have a Pauli flow, but identification of this is $O(n^4)$ in the number of qubits (vertices) in the pattern. On the other hand, causal flow is a particular special case that may not always exist but can be identified in $O(n^2 \log n)$ time.
-Given a ZX diagram in MBQC form, there are algorithms that can find a suitable :py:class:`Flow` if one exists. Since there are several classifications of flow (e.g. causal flow, gflow, Pauli flow, extended Pauli flow) with varying levels of generality, we offer multiple algorithms for identifying them. For example, any diagram supporting a uniform, stepwise, strongly deterministic measurement and correction scheme will have a Pauli flow, but identification of this is :math:`O(n^4)` in the number of qubits (vertices) in the pattern. On the other hand, causal flow is a particular special case that may not always exist but can be identified in :math:`O(n^2 \log n)` time.
+The {py:class}`Flow` object that is returned abstracts away the partial ordering of the measured qubits of the diagram by just giving the depth from the outputs, i.e. all output qubits and those with no corrections have depth $0$, all qubits with depth $n$ can be measured simultaneously and only require corrections on qubits at depth strictly less than $n$. The measurement corrections can also be inferred from the flow, where {py:meth}`Flow.c()` gives the correction set for a given measured qubit (the qubits which require an $X$ correction if a measurement error occurs) and {py:meth}`Flow.odd()` gives its odd neighbourhood (the qubits which require a $Z$ correction).
-The :py:class:`Flow` object that is returned abstracts away the partial ordering of the measured qubits of the diagram by just giving the depth from the outputs, i.e. all output qubits and those with no corrections have depth :math:`0`, all qubits with depth :math:`n` can be measured simultaneously and only require corrections on qubits at depth strictly less than :math:`n`. The measurement corrections can also be inferred from the flow, where :py:meth:`Flow.c()` gives the correction set for a given measured qubit (the qubits which require an :math:`X` correction if a measurement error occurs) and :py:meth:`Flow.odd()` gives its odd neighbourhood (the qubits which require a :math:`Z` correction).
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.zx import Flow
@@ -515,29 +553,34 @@ The :py:class:`Flow` object that is returned abstracts away the partial ordering
# Or we can obtain the entire flow as maps for easy iteration
print({ vertex_ids[v] : d for (v, d) in fl.dmap.items() })
print({ vertex_ids[v] : [vertex_ids[c] for c in cs] for (v, cs) in fl.cmap.items() })
+```
-.. note:: In accordance with the Pauli flow criteria, :py:meth:`Flow.c()` and :py:meth:`Flow.odd()` may return qubits that have already been measured, but this may only happen in cases where the required correction would not have affected the past measurement such as a :math:`Z` on a :py:class:`ZXType.PZ` qubit.
+:::{note}
+In accordance with the Pauli flow criteria, {py:meth}`Flow.c()` and {py:meth}`Flow.odd()` may return qubits that have already been measured, but this may only happen in cases where the required correction would not have affected the past measurement such as a $Z$ on a {py:class}`ZXType.PZ` qubit.
+:::
-.. Verification and focussing
+% Verification and focussing
-In general, multiple valid flows may exist for a given diagram, but a pattern with equal numbers of inputs and outputs will always have a unique focussed flow (where the corrections permitted on each qubit are restricted to be a single Pauli based on its label, e.g. if qubit :math:`q` is labelled as :py:class:`ZXType.XY`, then we may only apply :math:`X` corrections to :math:`q`). Given any flow, we may transform it to a focussed flow using :py:meth:`Flow.focus()`.
+In general, multiple valid flows may exist for a given diagram, but a pattern with equal numbers of inputs and outputs will always have a unique focussed flow (where the corrections permitted on each qubit are restricted to be a single Pauli based on its label, e.g. if qubit $q$ is labelled as {py:class}`ZXType.XY`, then we may only apply $X$ corrections to $q$). Given any flow, we may transform it to a focussed flow using {py:meth}`Flow.focus()`.
-.. Warning that does not update on rewriting
+% Warning that does not update on rewriting
-.. warning:: A :py:class:`Flow` object is always with respect to a particular :py:class:`ZXDiagram` in a particular state. It cannot be applied to other diagrams and does not automatically update on rewriting the diagram.
+:::{warning}
+A {py:class}`Flow` object is always with respect to a particular {py:class}`ZXDiagram` in a particular state. It cannot be applied to other diagrams and does not automatically update on rewriting the diagram.
+:::
-Conversions & Extraction
-------------------------
+## Conversions & Extraction
-.. Circuits to ZX diagram by gate definitions
+% Circuits to ZX diagram by gate definitions
-Up to this point, we have only examined the ZX module in a vacuum, so now we will look at integrating it with the rest of tket's functionality by converting between :py:class:`ZXDiagram` and :py:class:`~pytket.circuit.Circuit` objects. The :py:meth:`circuit_to_zx()` function will reconstruct a :py:class:`~pytket.circuit.Circuit` as a :py:class:`ZXDiagram` by replacing each gate with a choice of representation in the ZX-calculus.
+Up to this point, we have only examined the ZX module in a vacuum, so now we will look at integrating it with the rest of tket's functionality by converting between {py:class}`ZXDiagram` and {py:class}`~pytket.circuit.Circuit` objects. The {py:meth}`circuit_to_zx()` function will reconstruct a {py:class}`~pytket.circuit.Circuit` as a {py:class}`ZXDiagram` by replacing each gate with a choice of representation in the ZX-calculus.
-.. Created and discarded qubits are not open boundaries; indexing of boundaries made by qubit and bit order; conversion returns a map between boundaries and UnitIDs
+% Created and discarded qubits are not open boundaries; indexing of boundaries made by qubit and bit order; conversion returns a map between boundaries and UnitIDs
-The boundaries of the resulting :py:class:`ZXDiagram` will match up with the open boundaries of the :py:class:`~pytket.circuit.Circuit`. However, :py:class:`OpType.Create` and :py:class:`OpType.Discard` operations will be replaced with an initialisation and a discard map respectively, meaning the number of boundary vertices in the resulting diagram may not match up with the number of qubits and bits in the original :py:class:`~pytket.circuit.Circuit`. This makes it difficult to have a sensible policy for knowing where in the linear boundary of the :py:class:`ZXDiagram` is the input/output of a particular qubit. The second return value of :py:meth:`circuit_to_zx()` is a map sending a :py:class:`UnitID` to the pair of :py:class:`ZXVert` objects for the corresponding input and output.
+The boundaries of the resulting {py:class}`ZXDiagram` will match up with the open boundaries of the {py:class}`~pytket.circuit.Circuit`. However, {py:class}`OpType.Create` and {py:class}`OpType.Discard` operations will be replaced with an initialisation and a discard map respectively, meaning the number of boundary vertices in the resulting diagram may not match up with the number of qubits and bits in the original {py:class}`~pytket.circuit.Circuit`. This makes it difficult to have a sensible policy for knowing where in the linear boundary of the {py:class}`ZXDiagram` is the input/output of a particular qubit. The second return value of {py:meth}`circuit_to_zx()` is a map sending a {py:class}`UnitID` to the pair of {py:class}`ZXVert` objects for the corresponding input and output.
-.. jupyter-execute::
+
+```{code-cell} ipython3
from pytket import Circuit, Qubit
from pytket.zx import circuit_to_zx
@@ -560,16 +603,18 @@ The boundaries of the resulting :py:class:`ZXDiagram` will match up with the ope
# Look at the neighbour of the input to check the first operation is the X
n = diag.neighbours(in3)[0]
print(diag.get_vertex_ZXGen(n))
+```
-.. Extraction is not computationally feasible for general diagrams; known to be efficient for MBQC diagrams with flow; current method permits unitary diagrams with gflow, based on Backens et al.; more methods will be written in future for different extraction methods, e.g. causal flow, MBQC, pauli flow, mixed diagram extraction
+% Extraction is not computationally feasible for general diagrams; known to be efficient for MBQC diagrams with flow; current method permits unitary diagrams with gflow, based on Backens et al.; more methods will be written in future for different extraction methods, e.g. causal flow, MBQC, pauli flow, mixed diagram extraction
From here, we are able to rewrite our circuit as a ZX diagram, and even though we may aim to preserve the semantics, there is often little guarantee that the diagram will resemble the structure of a circuit after rewriting. The extraction problem concerns taking a ZX diagram and attempting to identify an equivalent circuit, and this is known to be #P-Hard for arbitrary diagrams equivalent to a unitary circuit which is not computationally feasible. However, if we can guarantee that our rewriting leaves us with a diagram in MBQC form which admits a flow of some kind, then there exist efficient methods for extracting an equivalent circuit.
-The current method implemented in :py:meth:`ZXDiagram.to_circuit()` permits extraction of a circuit from a unitary ZX diagram with gflow, based on the method of Backens et al. [Back2021]_. More methods may be added in the future for different extraction methods, such as fast extraction with causal flow, MBQC (i.e. a :py:class:`~pytket.circuit.Circuit` with explicit measurement and correction operations), extraction from Pauli flow, and mixed diagram extraction.
+The current method implemented in {py:meth}`ZXDiagram.to_circuit()` permits extraction of a circuit from a unitary ZX diagram with gflow, based on the method of Backens et al. [^cite_back2021]. More methods may be added in the future for different extraction methods, such as fast extraction with causal flow, MBQC (i.e. a {py:class}`~pytket.circuit.Circuit` with explicit measurement and correction operations), extraction from Pauli flow, and mixed diagram extraction.
+
+Since the {py:class}`ZXDiagram` class does not associate a {py:class}`UnitID` to each boundary vertex, {py:meth}`ZXDiagram.to_circuit()` also returns a map sending each boundary {py:class}`ZXVert` to the corresponding {py:class}`UnitID` in the resulting {py:class}`~pytket.circuit.Circuit`.
-Since the :py:class:`ZXDiagram` class does not associate a :py:class:`UnitID` to each boundary vertex, :py:meth:`ZXDiagram.to_circuit()` also returns a map sending each boundary :py:class:`ZXVert` to the corresponding :py:class:`UnitID` in the resulting :py:class:`~pytket.circuit.Circuit`.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket import OpType
from pytket.circuit.display import render_circuit_jupyter
@@ -590,67 +635,62 @@ Since the :py:class:`ZXDiagram` class does not associate a :py:class:`UnitID` to
Rewrite.to_MBQC_diag().apply(diag)
circ, _ = diag.to_circuit()
render_circuit_jupyter(circ)
+```
+
+## Compiler Passes Using ZX
-Compiler Passes Using ZX
-------------------------
+% Prepackaged into ZXGraphlikeOptimisation pass for convenience to try out
-.. Prepackaged into ZXGraphlikeOptimisation pass for convenience to try out
+The known methods for circuit rewriting and optimisation lend themselves to a single common routine of mapping to graphlike form, reducing within that form, and extracting back out. {py:class}`ZXGraphlikeOptimisation` is a standard pytket compiler pass that packages this routine up for convenience to save the user from manually digging into the ZX module before they can test out using the compilation routine on their circuits.
-The known methods for circuit rewriting and optimisation lend themselves to a single common routine of mapping to graphlike form, reducing within that form, and extracting back out. :py:class:`ZXGraphlikeOptimisation` is a standard pytket compiler pass that packages this routine up for convenience to save the user from manually digging into the ZX module before they can test out using the compilation routine on their circuits.
-.. jupyter-execute::
+```{code-cell} ipython3
from pytket.passes import ZXGraphlikeOptimisation
# Use the same CCX example from above
ZXGraphlikeOptimisation().apply(c)
render_circuit_jupyter(c)
+```
-The specific nature of optimising circuits via ZX diagrams gives rise to some general advice regarding how to use :py:class:`ZXGraphlikeOptimisation` in compilation sequences and what to expect from its performance:
+The specific nature of optimising circuits via ZX diagrams gives rise to some general advice regarding how to use {py:class}`ZXGraphlikeOptimisation` in compilation sequences and what to expect from its performance:
-.. Extraction techniques are not optimal so starting with a well-structured circuit, abstracting away that structure and starting from scratch is likely to increase gate counts; since graphlike form abstracts away Cliffords to focus on non-Cliffords, most likely to give good results on Clifford-dense circuits
+% Extraction techniques are not optimal so starting with a well-structured circuit, abstracting away that structure and starting from scratch is likely to increase gate counts; since graphlike form abstracts away Cliffords to focus on non-Cliffords, most likely to give good results on Clifford-dense circuits
-* The routine can broadly be thought of as a resynthesis pass: converting to a graphlike ZX diagram completely abstracts away most of the circuit structure and attempts to extract a new circuit from scratch. Coupling this with the difficulty of optimal extraction means that if the original circuit is already well-structured or close to optimal, it is likely that the process of forgetting that structure and trying to extract something new will increase gate counts. Since the graphlike form abstracts away the structure from Clifford gates to focus on the non-Cliffords, it is most likely going to give its best results on very Clifford-dense circuits. Even in cases where this improves on gate counts, it may be the case that the new circuit structure is harder to efficiently route on a device with restricted qubit connectivity, so it is important to consider the context of a full compilation sequence when analysing the benefits of using this routine.
+- The routine can broadly be thought of as a resynthesis pass: converting to a graphlike ZX diagram completely abstracts away most of the circuit structure and attempts to extract a new circuit from scratch. Coupling this with the difficulty of optimal extraction means that if the original circuit is already well-structured or close to optimal, it is likely that the process of forgetting that structure and trying to extract something new will increase gate counts. Since the graphlike form abstracts away the structure from Clifford gates to focus on the non-Cliffords, it is most likely going to give its best results on very Clifford-dense circuits. Even in cases where this improves on gate counts, it may be the case that the new circuit structure is harder to efficiently route on a device with restricted qubit connectivity, so it is important to consider the context of a full compilation sequence when analysing the benefits of using this routine.
-.. Since ZX does resynthesis and completely abstracts away circuit structure, there is little point in running optimisations before ZX
+% Since ZX does resynthesis and completely abstracts away circuit structure, there is little point in running optimisations before ZX
-* Similarly, because the conversion to a graphlike ZX diagram completely abstracts away the Clifford gates, there is often little-to-no benefit in running most simple optimisations before applying :py:class:`ZXGraphlikeOptimisation` since it will largely ignore them and achieve the same graphlike form regardless.
+- Similarly, because the conversion to a graphlike ZX diagram completely abstracts away the Clifford gates, there is often little-to-no benefit in running most simple optimisations before applying {py:class}`ZXGraphlikeOptimisation` since it will largely ignore them and achieve the same graphlike form regardless.
-.. Extraction is not optimised so best to run other passes afterwards
+% Extraction is not optimised so best to run other passes afterwards
-* The implementation of the extraction routine in pytket follows the steps from Backens et al. [Back2021]_ very closely without optimising the gate sequences as they are produced. It is recommended to run additional peephole optimisation passes afterwards to account for redundancies introduced by the extraction procedure. For example, we can see in the above example that there are many sequences of successive Hadamard gates that could be removed using a pass like :py:class:`RemoveRedundancies`. :py:class:`~pytket.passes.FullPeepholeOptimise` is a good catch-all that incorporates many peephole optimisations and could further reduce the extracted circuit.
+- The implementation of the extraction routine in pytket follows the steps from Backens et al. [^cite_back2021] very closely without optimising the gate sequences as they are produced. It is recommended to run additional peephole optimisation passes afterwards to account for redundancies introduced by the extraction procedure. For example, we can see in the above example that there are many sequences of successive Hadamard gates that could be removed using a pass like {py:class}`RemoveRedundancies`. {py:class}`~pytket.passes.FullPeepholeOptimise` is a good catch-all that incorporates many peephole optimisations and could further reduce the extracted circuit.
-Advanced Topics
----------------
+## Advanced Topics
-C++ Implementation
-==================
+### C++ Implementation
-.. Use for speed and more control
+% Use for speed and more control
-As with the rest of pytket, the ZX module features a python interface that has enough flexibility to realise any diagram a user would wish to construct or a rewrite they would like to apply, but the data structure itself is defined in the core C++ library for greater speed for longer rewrite passes and analysis tasks. This comes with the downside that interacting via the python interface is slowed down by the need to convert data through the bindings. After experimenting with the python interface and devising new rewrite strategies, we recommend users use the C++ library directly for speed and greater control over the data structure when attempting to write heavy-duty implementations that require the use of this module's unique features (for simpler rewriting tasks, it may be faster to use ``quizx`` [https://github.com/Quantomatic/quizx] which sacrifices some flexibility for even more performance).
+As with the rest of pytket, the ZX module features a python interface that has enough flexibility to realise any diagram a user would wish to construct or a rewrite they would like to apply, but the data structure itself is defined in the core C++ library for greater speed for longer rewrite passes and analysis tasks. This comes with the downside that interacting via the python interface is slowed down by the need to convert data through the bindings. After experimenting with the python interface and devising new rewrite strategies, we recommend users use the C++ library directly for speed and greater control over the data structure when attempting to write heavy-duty implementations that require the use of this module's unique features (for simpler rewriting tasks, it may be faster to use `quizx` \[\] which sacrifices some flexibility for even more performance).
-.. Underlying graph structure is directed to distinguish between ends of an edge for port data
+% Underlying graph structure is directed to distinguish between ends of an edge for port data
-The interface to the ``ZXDiagram`` C++ class is extremely similar to the python interface. The main difference is that, whilst the edges of a ZX diagram are semantically undirected, the underlying data structure for the graph itself uses directed edges. This allows us to attach the port data for an edge to the edge metadata and distinguish between its two end-points by referring to the source and target of the edge - for example, an edge between :math:`(u,1)` and :math:`(v,-)` (where :math:`v` is a symmetric generator without port information) can be represented as an edge from :math:`u` to :math:`v` whose metadata carries ``(source_port = 1, target_port = std::nullopt)``.
+The interface to the `ZXDiagram` C++ class is extremely similar to the python interface. The main difference is that, whilst the edges of a ZX diagram are semantically undirected, the underlying data structure for the graph itself uses directed edges. This allows us to attach the port data for an edge to the edge metadata and distinguish between its two end-points by referring to the source and target of the edge - for example, an edge between $(u,1)$ and $(v,-)$ (where $v$ is a symmetric generator without port information) can be represented as an edge from $u$ to $v$ whose metadata carries `(source_port = 1, target_port = std::nullopt)`.
-.. Tensor evaluation only available in python, so easiest to expose in pybind for testing
+% Tensor evaluation only available in python, so easiest to expose in pybind for testing
-When implementing a rewrite in C++, we recommend exposing your method via the pybind interface and testing it using pytket when possible. The primary reason for this is that the tensor evaluation available uses the ``quimb`` python package to scale to large numbers of nodes in the tensor network, which is particularly useful for testing that your rewrite preserves the diagram semantics.
+When implementing a rewrite in C++, we recommend exposing your method via the pybind interface and testing it using pytket when possible. The primary reason for this is that the tensor evaluation available uses the `quimb` python package to scale to large numbers of nodes in the tensor network, which is particularly useful for testing that your rewrite preserves the diagram semantics.
In place of API reference and code examples, we recommend looking at the following parts of the tket source code to see how the ZX module is already used:
-* ZXDiagram.hpp gives inline summaries for the interface to the core diagram data structure.
-
-* ``Rewrite::spider_fusion_fun()`` in ZXRWAxioms.cpp is an example of a simple rewrite that is applied across the entire graph by iterating over each vertex and looking for patterns in its immediate neighbourhood. It demonstrates the relevance of checking edge data for its :py:class:`ZXWireType` and :py:class:`QuantumType` and maintaining track of these throughout a rewrite.
-
-* ``Rewrite::remove_interior_paulis_fun()`` in ZXRWGraphLikeSimplification.cpp demonstrates how the checks and management of the format of vertices and edges can be simplified a little once it is established that the diagram is of a particular form (e.g. graphlike).
-
-* ``ZXGraphlikeOptimisation()`` in PassLibrary.cpp uses a sequence of rewrites along with the converters to build a compilation pass for circuits. Most of the method contents is just there to define the expectations of the form of the circuit using the tket :py:class:`~pytket.predicates.Predicate` system, which saves the need for the pass to be fully generic and be constantly maintained to accept arbitrary circuits.
-
-* ``zx_to_circuit()`` in ZXConverters.cpp implements the extraction procedure. It is advised to read this alongside the algorithm description in Backens et al. for more detail on the intent and intuition around each step.
-
+- ZXDiagram.hpp gives inline summaries for the interface to the core diagram data structure.
+- `Rewrite::spider_fusion_fun()` in ZXRWAxioms.cpp is an example of a simple rewrite that is applied across the entire graph by iterating over each vertex and looking for patterns in its immediate neighbourhood. It demonstrates the relevance of checking edge data for its {py:class}`ZXWireType` and {py:class}`QuantumType` and maintaining track of these throughout a rewrite.
+- `Rewrite::remove_interior_paulis_fun()` in ZXRWGraphLikeSimplification.cpp demonstrates how the checks and management of the format of vertices and edges can be simplified a little once it is established that the diagram is of a particular form (e.g. graphlike).
+- `ZXGraphlikeOptimisation()` in PassLibrary.cpp uses a sequence of rewrites along with the converters to build a compilation pass for circuits. Most of the method contents is just there to define the expectations of the form of the circuit using the tket {py:class}`~pytket.predicates.Predicate` system, which saves the need for the pass to be fully generic and be constantly maintained to accept arbitrary circuits.
+- `zx_to_circuit()` in ZXConverters.cpp implements the extraction procedure. It is advised to read this alongside the algorithm description in Backens et al. for more detail on the intent and intuition around each step.
-.. [Back2021] Backens, M. et al., 2021. There and back again: A circuit extraction tale. Quantum, 5, p.451.
+[^cite_back2021]: Backens, M. et al., 2021. There and back again: A circuit extraction tale. Quantum, 5, p.451.
-.. [vdWet2020] van de Wetering, J., 2020. ZX-calculus for the working quantum computer scientist. https://arxiv.org/abs/2012.13966
+[^cite_vdwet2020]: van de Wetering, J., 2020. ZX-calculus for the working quantum computer scientist.
From c4846dff7804cdf271a7649b2e5866bdfdf5224f Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 13:05:04 +0100
Subject: [PATCH 02/16] fix indentation issues in backends page
---
docs/manual/manual_backend.md | 478 +++++++++++++++++-----------------
1 file changed, 239 insertions(+), 239 deletions(-)
diff --git a/docs/manual/manual_backend.md b/docs/manual/manual_backend.md
index f5ec6662..c6a6e985 100644
--- a/docs/manual/manual_backend.md
+++ b/docs/manual/manual_backend.md
@@ -63,19 +63,19 @@ Knowing the requirements of each {py:class}`~pytket.backends.Backend` is handy i
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.extensions.qiskit import AerBackend
+from pytket import Circuit, OpType
+from pytket.extensions.qiskit import AerBackend
- circ = Circuit(3, 2)
- circ.H(0).Ry(0.25, 1)
- circ.add_gate(OpType.CnRy, [0.74], [0, 1, 2]) # CnRy not in AerBackend gate set
- circ.measure_all()
+circ = Circuit(3, 2)
+circ.H(0).Ry(0.25, 1)
+circ.add_gate(OpType.CnRy, [0.74], [0, 1, 2]) # CnRy not in AerBackend gate set
+circ.measure_all()
- backend = AerBackend()
- print("Circuit valid for AerBackend?", backend.valid_circuit(circ))
- compiled_circ = backend.get_compiled_circuit(circ) # Compile circuit to AerBackend
+backend = AerBackend()
+print("Circuit valid for AerBackend?", backend.valid_circuit(circ))
+compiled_circ = backend.get_compiled_circuit(circ) # Compile circuit to AerBackend
- print("Compiled circuit valid for AerBackend?", backend.valid_circuit(compiled_circ))
+print("Compiled circuit valid for AerBackend?", backend.valid_circuit(compiled_circ))
```
Now that we can prepare our {py:class}`~pytket.circuit.Circuit` s to be suitable for a given {py:class}`~pytket.backends.Backend`, we can send them off to be run and examine the results. This is always done by calling {py:meth}`~pytket.backends.Backend.process_circuit()` which sends a {py:class}`~pytket.circuit.Circuit` for execution and returns a {py:class}`~pytket.backends.resulthandle.ResultHandle` as an identifier for the job which can later be used to retrieve the actual results once the job has finished.
@@ -83,14 +83,14 @@ Now that we can prepare our {py:class}`~pytket.circuit.Circuit` s to be suitab
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerStateBackend
+from pytket import Circuit
+from pytket.extensions.qiskit import AerStateBackend
- circ = Circuit(2, 2)
- circ.Rx(0.3, 0).Ry(0.5, 1).CRz(-0.6, 1, 0)
- backend = AerStateBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ)
+circ = Circuit(2, 2)
+circ.Rx(0.3, 0).Ry(0.5, 1).CRz(-0.6, 1, 0)
+backend = AerStateBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
+handle = backend.process_circuit(compiled_circ)
```
The exact arguments to {py:meth}`~pytket.backends.Backend.process_circuit` and the means of retrieving results back are dependent on the type of data the {py:class}`~pytket.backends.Backend` can produce and whether it samples measurements or calculates the internal state of the quantum system.
@@ -110,17 +110,17 @@ The interaction with a QPU (or a simulator that tries to imitate a device by sam
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
+from pytket import Circuit
+from pytket.extensions.qiskit import AerBackend
- circ = Circuit(2, 2)
- circ.H(0).X(1).measure_all()
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
+circ = Circuit(2, 2)
+circ.H(0).X(1).measure_all()
+backend = AerBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, n_shots=20)
- shots = backend.get_result(handle).get_shots()
- print(shots)
+handle = backend.process_circuit(compiled_circ, n_shots=20)
+shots = backend.get_result(handle).get_shots()
+print(shots)
```
% Often interested in probabilities of each measurement outcome, so need many shots for high precision
@@ -136,20 +136,20 @@ If we don't care about the temporal order of the shots, we can instead retrieve
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
- from pytket.utils import probs_from_counts
+from pytket import Circuit
+from pytket.extensions.qiskit import AerBackend
+from pytket.utils import probs_from_counts
- circ = Circuit(2, 2)
- circ.H(0).X(1).measure_all()
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
+circ = Circuit(2, 2)
+circ.H(0).X(1).measure_all()
+backend = AerBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, n_shots=2000)
- counts = backend.get_result(handle).get_counts()
- print(counts)
+handle = backend.process_circuit(compiled_circ, n_shots=2000)
+counts = backend.get_result(handle).get_counts()
+print(counts)
- print(probs_from_counts(counts))
+print(probs_from_counts(counts))
```
:::{note}
@@ -167,16 +167,16 @@ Any form of sampling from a distribution will introduce sampling error and (unle
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerStateBackend
+from pytket import Circuit
+from pytket.extensions.qiskit import AerStateBackend
- circ = Circuit(3)
- circ.H(0).CX(0, 1).S(1).X(2)
- backend = AerStateBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
+circ = Circuit(3)
+circ.H(0).CX(0, 1).S(1).X(2)
+backend = AerStateBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
- state = backend.run_circuit(compiled_circ).get_state()
- print(state.round(5))
+state = backend.run_circuit(compiled_circ).get_state()
+print(state.round(5))
```
:::{note}
@@ -190,16 +190,16 @@ The majority of {py:class}`~pytket.backends.Backend` s will run the {py:class}
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerUnitaryBackend
+from pytket import Circuit
+from pytket.extensions.qiskit import AerUnitaryBackend
- circ = Circuit(2)
- circ.H(0).CX(0, 1)
- backend = AerUnitaryBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
+circ = Circuit(2)
+circ.H(0).CX(0, 1)
+backend = AerUnitaryBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
- unitary = backend.run_circuit(compiled_circ).get_unitary()
- print(unitary.round(5))
+unitary = backend.run_circuit(compiled_circ).get_unitary()
+print(unitary.round(5))
```
% Useful for obtaining high-precision results as well as verifying correctness of circuits
@@ -211,15 +211,15 @@ Whilst the drive for quantum hardware is driven by the limited scalability of si
```{code-cell} ipython3
- from pytket.utils.results import compare_statevectors
- import numpy as np
+from pytket.utils.results import compare_statevectors
+import numpy as np
- ref_state = np.asarray([1, 0, 1, 0]) / np.sqrt(2.) # |+0>
- gph_state = np.asarray([1, 0, 1, 0]) * 1j / np.sqrt(2.) # i|+0>
- prm_state = np.asarray([1, 1, 0, 0]) / np.sqrt(2.) # |0+>
+ref_state = np.asarray([1, 0, 1, 0]) / np.sqrt(2.) # |+0>
+gph_state = np.asarray([1, 0, 1, 0]) * 1j / np.sqrt(2.) # i|+0>
+prm_state = np.asarray([1, 1, 0, 0]) / np.sqrt(2.) # |0+>
- print(compare_statevectors(ref_state, gph_state)) # Differ by global phase
- print(compare_statevectors(ref_state, prm_state)) # Differ by qubit permutation
+print(compare_statevectors(ref_state, gph_state)) # Differ by global phase
+print(compare_statevectors(ref_state, prm_state)) # Differ by qubit permutation
```
% Warning that interactions with classical data (conditional gates and measurements) or deliberately collapsing the state (Collapse and Reset) do not yield a deterministic result in this Hilbert space, so will be rejected
@@ -237,18 +237,18 @@ By default, the bits in readouts (shots and counts) are ordered in Increasing Le
```{code-cell} ipython3
- from pytket.circuit import Circuit, BasisOrder
- from pytket.extensions.qiskit import AerBackend
+from pytket.circuit import Circuit, BasisOrder
+from pytket.extensions.qiskit import AerBackend
- circ = Circuit(2, 2)
- circ.X(1).measure_all() # write 0 to c[0] and 1 to c[1]
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, n_shots=10)
- result = backend.get_result(handle)
+circ = Circuit(2, 2)
+circ.X(1).measure_all() # write 0 to c[0] and 1 to c[1]
+backend = AerBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
+handle = backend.process_circuit(compiled_circ, n_shots=10)
+result = backend.get_result(handle)
- print(result.get_counts()) # ILO gives (c[0], c[1]) == (0, 1)
- print(result.get_counts(basis=BasisOrder.dlo)) # DLO gives (c[1], c[0]) == (1, 0)
+print(result.get_counts()) # ILO gives (c[0], c[1]) == (0, 1)
+print(result.get_counts(basis=BasisOrder.dlo)) # DLO gives (c[1], c[0]) == (1, 0)
```
The choice of ILO or DLO defines the ordering of a bit sequence, but this can still be interpreted into the index of a statevector in two ways: by mapping the bits to a big-endian (BE) or little-endian (LE) integer. Every statevector and unitary in `pytket` uses a BE encoding (if LE is preferred, note that the ILO-LE interpretation gives the same result as DLO-BE for statevectors and unitaries, so just change the `basis` argument accordingly). The ILO-BE convention gives unitaries of individual gates as they typically appear in common textbooks [^cite_niel2001].
@@ -256,18 +256,18 @@ The choice of ILO or DLO defines the ordering of a bit sequence, but this can st
```{code-cell} ipython3
- from pytket.circuit import Circuit, BasisOrder
- from pytket.extensions.qiskit import AerUnitaryBackend
+from pytket.circuit import Circuit, BasisOrder
+from pytket.extensions.qiskit import AerUnitaryBackend
- circ = Circuit(2)
- circ.CX(0, 1)
- backend = AerUnitaryBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ)
- result = backend.get_result(handle)
+circ = Circuit(2)
+circ.CX(0, 1)
+backend = AerUnitaryBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
+handle = backend.process_circuit(compiled_circ)
+result = backend.get_result(handle)
- print(result.get_unitary())
- print(result.get_unitary(basis=BasisOrder.dlo))
+print(result.get_unitary())
+print(result.get_unitary(basis=BasisOrder.dlo))
```
Suppose that we only care about a subset of the measurements used in a {py:class}`~pytket.circuit.Circuit`. A shot table is a `numpy.ndarray`, so it can be filtered by column selections. To identify which columns need to be retained/removed, we are able to predict their column indices from the {py:class}`~pytket.circuit.Circuit` object. {py:attr}`pytket.Circuit.bit_readout` maps {py:class}`~pytket.unit_id.Bit` s to their column index (assuming the ILO convention).
@@ -275,26 +275,26 @@ Suppose that we only care about a subset of the measurements used in a {py:class
```{code-cell} ipython3
- from pytket import Circuit, Bit
- from pytket.extensions.qiskit import AerBackend
- from pytket.utils import expectation_from_shots
+from pytket import Circuit, Bit
+from pytket.extensions.qiskit import AerBackend
+from pytket.utils import expectation_from_shots
- circ = Circuit(3, 3)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
+circ = Circuit(3, 3)
+circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
- circ.H(1) # Measure ZXY operator qubit-wise
- circ.Rx(0.5, 2)
- circ.measure_all()
+circ.H(1) # Measure ZXY operator qubit-wise
+circ.Rx(0.5, 2)
+circ.measure_all()
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, 2000)
- shots = backend.get_result(handle).get_shots()
+backend = AerBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
+handle = backend.process_circuit(compiled_circ, 2000)
+shots = backend.get_result(handle).get_shots()
- # To extract the expectation value for ZIY, we only want to consider bits c[0] and c[2]
- bitmap = compiled_circ.bit_readout
- shots = shots[:, [bitmap[Bit(0)], bitmap[Bit(2)]]]
- print(expectation_from_shots(shots))
+# To extract the expectation value for ZIY, we only want to consider bits c[0] and c[2]
+bitmap = compiled_circ.bit_readout
+shots = shots[:, [bitmap[Bit(0)], bitmap[Bit(2)]]]
+print(expectation_from_shots(shots))
```
If measurements occur at the end of the {py:class}`~pytket.circuit.Circuit`, then we can associate each measurement to the qubit that was measured. {py:attr}`~pytket.circuit.Circuit.qubit_readout` gives the equivalent map to column indices for {py:class}`~pytket.unit_id.Qubit` s, and {py:attr}`~pytket.circuit.Circuit.qubit_to_bit_map` relates each measured {py:class}`~pytket.unit_id.Qubit` to the {py:class}`~pytket.unit_id.Bit` that holds the corresponding measurement result.
@@ -302,15 +302,15 @@ If measurements occur at the end of the {py:class}`~pytket.circuit.Circuit`, the
```{code-cell} ipython3
- from pytket import Circuit, Qubit, Bit
- circ = Circuit(3, 2)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2)
- circ.Measure(0, 0)
- circ.Measure(2, 1)
+from pytket import Circuit, Qubit, Bit
+circ = Circuit(3, 2)
+circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2)
+circ.Measure(0, 0)
+circ.Measure(2, 1)
- print(circ.bit_readout)
- print(circ.qubit_readout)
- print(circ.qubit_to_bit_map)
+print(circ.bit_readout)
+print(circ.qubit_readout)
+print(circ.qubit_to_bit_map)
```
For more control over the bits extracted from the results, we can instead call {py:class}`~pytket.backends.Backend.get_result()`. The {py:class}`~pytket.backends.backendresult.BackendResult` object returned wraps up all the information returned from the experiment and allows it to be projected into any preferred way of viewing it. In particular, we can provide the list of {py:class}`~pytket.unit_id.Bit` s we want to look at in the shot table/counts dictionary, and given the exact permutation we want (and similarly for the permutation of {py:class}`~pytket.unit_id.Qubit` s for statevectors/unitaries).
@@ -318,28 +318,28 @@ For more control over the bits extracted from the results, we can instead call {
```{code-cell} ipython3
- from pytket import Circuit, Bit, Qubit
- from pytket.extensions.qiskit import AerBackend, AerStateBackend
+from pytket import Circuit, Bit, Qubit
+from pytket.extensions.qiskit import AerBackend, AerStateBackend
- circ = Circuit(3)
- circ.H(0).Ry(-0.3, 2)
- state_b = AerStateBackend()
- circ = state_b.get_compiled_circuit(circ)
- handle = state_b.process_circuit(circ)
+circ = Circuit(3)
+circ.H(0).Ry(-0.3, 2)
+state_b = AerStateBackend()
+circ = state_b.get_compiled_circuit(circ)
+handle = state_b.process_circuit(circ)
- # Make q[1] the most-significant qubit, so interesting state uses consecutive coefficients
- result = state_b.get_result(handle)
- print(result.get_state([Qubit(1), Qubit(0), Qubit(2)]))
+# Make q[1] the most-significant qubit, so interesting state uses consecutive coefficients
+result = state_b.get_result(handle)
+print(result.get_state([Qubit(1), Qubit(0), Qubit(2)]))
- circ.measure_all()
- shot_b = AerBackend()
- circ = shot_b.get_compiled_circuit(circ)
- handle = shot_b.process_circuit(circ, n_shots=2000)
- result = shot_b.get_result(handle)
+circ.measure_all()
+shot_b = AerBackend()
+circ = shot_b.get_compiled_circuit(circ)
+handle = shot_b.process_circuit(circ, n_shots=2000)
+result = shot_b.get_result(handle)
- # Marginalise out q[0] from counts
- print(result.get_counts())
- print(result.get_counts([Bit(1), Bit(2)]))
+# Marginalise out q[0] from counts
+print(result.get_counts())
+print(result.get_counts([Bit(1), Bit(2)]))
```
## Expectation Value Calculations
@@ -349,39 +349,39 @@ One of the most common calculations performed with a quantum state $\left| \psi
```{code-cell} ipython3
- from pytket import Circuit, Qubit
- from pytket.extensions.qiskit import AerBackend
- from pytket.partition import PauliPartitionStrat
- from pytket.pauli import Pauli, QubitPauliString
- from pytket.utils import get_pauli_expectation_value, get_operator_expectation_value
- from pytket.utils.operators import QubitPauliOperator
-
- circ = Circuit(3)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
- backend = AerBackend()
-
- zxy = QubitPauliString({
- Qubit(0) : Pauli.Z,
- Qubit(1) : Pauli.X,
- Qubit(2) : Pauli.Y})
- xzi = QubitPauliString({
- Qubit(0) : Pauli.X,
- Qubit(1) : Pauli.Z})
- op = QubitPauliOperator({
- QubitPauliString() : 0.3,
- zxy : -1,
- xzi : 1})
- print(get_pauli_expectation_value(
- circ,
- zxy,
- backend,
- n_shots=2000))
- print(get_operator_expectation_value(
- circ,
- op,
- backend,
- n_shots=2000,
- partition_strat=PauliPartitionStrat.CommutingSets))
+from pytket import Circuit, Qubit
+from pytket.extensions.qiskit import AerBackend
+from pytket.partition import PauliPartitionStrat
+from pytket.pauli import Pauli, QubitPauliString
+from pytket.utils import get_pauli_expectation_value, get_operator_expectation_value
+from pytket.utils.operators import QubitPauliOperator
+
+circ = Circuit(3)
+circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
+backend = AerBackend()
+
+zxy = QubitPauliString({
+ Qubit(0) : Pauli.Z,
+ Qubit(1) : Pauli.X,
+ Qubit(2) : Pauli.Y})
+xzi = QubitPauliString({
+ Qubit(0) : Pauli.X,
+ Qubit(1) : Pauli.Z})
+op = QubitPauliOperator({
+ QubitPauliString() : 0.3,
+ zxy : -1,
+ xzi : 1})
+print(get_pauli_expectation_value(
+ circ,
+ zxy,
+ backend,
+ n_shots=2000))
+print(get_operator_expectation_value(
+ circ,
+ op,
+ backend,
+ n_shots=2000,
+ partition_strat=PauliPartitionStrat.CommutingSets))
```
If you want a greater level of control over the procedure, then you may wish to write your own method for calculating $\langle \psi | H | \psi \rangle$. This is simple multiplication if we are given the statevector $| \psi \rangle$, but is slightly more complicated for measured systems. Since each measurement projects into either the subspace of +1 or -1 eigenvectors, we can assign +1 to each `0` readout and -1 to each `1` readout and take the average across all shots. When the desired operator is given by the product of multiple measurements, the contribution of +1 or -1 is dependent on the parity (XOR) of each measurement result in that shot. `pytket` provides some utility functions to wrap up this calculation and apply it to either a shot table ({py:meth}`~pytket.utils.expectation_from_shots()`) or a counts dictionary ({py:meth}`~pytket.utils.expectation_from_counts()`).
@@ -389,23 +389,23 @@ If you want a greater level of control over the procedure, then you may wish to
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
- from pytket.utils import expectation_from_counts
+from pytket import Circuit
+from pytket.extensions.qiskit import AerBackend
+from pytket.utils import expectation_from_counts
- circ = Circuit(3, 3)
- circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
+circ = Circuit(3, 3)
+circ.Rx(0.3, 0).CX(0, 1).CZ(1, 2) # Generate the state we want to consider
- circ.H(1) # Want to measure expectation for Pauli ZXY
- circ.Rx(0.5, 2) # Measure ZII, IXI, IIY separately
- circ.measure_all()
+circ.H(1) # Want to measure expectation for Pauli ZXY
+circ.Rx(0.5, 2) # Measure ZII, IXI, IIY separately
+circ.measure_all()
- backend = AerBackend()
- compiled_circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(compiled_circ, 2000)
- counts = backend.get_result(handle).get_counts()
- print(counts)
- print(expectation_from_counts(counts))
+backend = AerBackend()
+compiled_circ = backend.get_compiled_circuit(circ)
+handle = backend.process_circuit(compiled_circ, 2000)
+counts = backend.get_result(handle).get_counts()
+print(counts)
+print(expectation_from_counts(counts))
```
% Obtaining indices of specific bits/qubits of interest using `bit_readout` and `qubit_readout` or `qubit_to_bit_map`, and filtering results
@@ -429,24 +429,24 @@ The first point in an experiment where you might have to act differently between
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend #, AerStateBackend
- from pytket.predicates import NoMidMeasurePredicate
+from pytket import Circuit
+from pytket.extensions.qiskit import AerBackend #, AerStateBackend
+from pytket.predicates import NoMidMeasurePredicate
- backend = AerBackend() # Choose backend in one place
- # backend = AerStateBackend() # A backend that is incompatible with the experiment
+backend = AerBackend() # Choose backend in one place
+# backend = AerStateBackend() # A backend that is incompatible with the experiment
- # For algorithms using mid-circuit measurement, we can assert this is valid
- qkd = Circuit(1, 3)
- qkd.H(0).Measure(0, 0) # Prepare a random bit in the Z basis
- qkd.H(0).Measure(0, 1).H(0) # Eavesdropper measures in the X basis
- qkd.Measure(0, 2) # Recipient measures in the Z basis
+# For algorithms using mid-circuit measurement, we can assert this is valid
+qkd = Circuit(1, 3)
+qkd.H(0).Measure(0, 0) # Prepare a random bit in the Z basis
+qkd.H(0).Measure(0, 1).H(0) # Eavesdropper measures in the X basis
+qkd.Measure(0, 2) # Recipient measures in the Z basis
- assert backend.supports_counts # Using AerStateBackend would fail at this check
- assert NoMidMeasurePredicate() not in backend.required_predicates
- compiled_qkd = backend.get_compiled_circuit(qkd)
- handle = backend.process_circuit(compiled_qkd, n_shots=1000)
- print(backend.get_result(handle).get_counts())
+assert backend.supports_counts # Using AerStateBackend would fail at this check
+assert NoMidMeasurePredicate() not in backend.required_predicates
+compiled_qkd = backend.get_compiled_circuit(qkd)
+handle = backend.process_circuit(compiled_qkd, n_shots=1000)
+print(backend.get_result(handle).get_counts())
```
:::{note}
@@ -474,40 +474,40 @@ For the final steps of retrieving and interpreting the results, it suffices to j
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend #, AerStateBackend
- from pytket.utils import expectation_from_counts
- import numpy as np
+from pytket import Circuit
+from pytket.extensions.qiskit import AerBackend #, AerStateBackend
+from pytket.utils import expectation_from_counts
+import numpy as np
- backend = AerBackend() # Choose backend in one place
- # backend = AerStateBackend() # Alternative backend with different requirements and result type
+backend = AerBackend() # Choose backend in one place
+# backend = AerStateBackend() # Alternative backend with different requirements and result type
- # For many algorithms, we can separate the state preparation from measurements
- circ = Circuit(2) # Apply e^{0.135 i pi XY} to the initial state
- circ.H(0).V(1).CX(0, 1).Rz(-0.27, 1).CX(0, 1).H(0).Vdg(1)
- measure = Circuit(2, 2) # Measure the YZ operator via YI and IZ
- measure.V(0).measure_all()
+# For many algorithms, we can separate the state preparation from measurements
+circ = Circuit(2) # Apply e^{0.135 i pi XY} to the initial state
+circ.H(0).V(1).CX(0, 1).Rz(-0.27, 1).CX(0, 1).H(0).Vdg(1)
+measure = Circuit(2, 2) # Measure the YZ operator via YI and IZ
+measure.V(0).measure_all()
- if backend.supports_counts:
- circ.append(measure)
+if backend.supports_counts:
+ circ.append(measure)
- circ = backend.get_compiled_circuit(circ)
- handle = backend.process_circuit(circ, n_shots=2000)
+circ = backend.get_compiled_circuit(circ)
+handle = backend.process_circuit(circ, n_shots=2000)
- expectation = 0
- if backend.supports_state:
- yz = np.asarray([
- [0, 0, -1j, 0],
- [0, 0, 0, 1j],
- [1j, 0, 0, 0],
- [0, -1j, 0, 0]])
- svec = backend.get_result(handle).get_state()
- expectation = np.vdot(svec, yz.dot(svec))
- else:
- counts = backend.get_result(handle).get_counts()
- expectation = expectation_from_counts(counts)
+expectation = 0
+if backend.supports_state:
+ yz = np.asarray([
+ [0, 0, -1j, 0],
+ [0, 0, 0, 1j],
+ [1j, 0, 0, 0],
+ [0, -1j, 0, 0]])
+ svec = backend.get_result(handle).get_state()
+ expectation = np.vdot(svec, yz.dot(svec))
+else:
+ counts = backend.get_result(handle).get_counts()
+ expectation = expectation_from_counts(counts)
- print(expectation)
+print(expectation)
```
@@ -641,35 +641,35 @@ Some simulators will have dedicated support for fast expectation value calculati
```{code-cell} ipython3
- from pytket import Circuit, Qubit
- from pytket.extensions.qiskit import AerStateBackend
- from pytket.pauli import Pauli, QubitPauliString
- from pytket.utils.operators import QubitPauliOperator
-
- backend = AerStateBackend()
+from pytket import Circuit, Qubit
+from pytket.extensions.qiskit import AerStateBackend
+from pytket.pauli import Pauli, QubitPauliString
+from pytket.utils.operators import QubitPauliOperator
- state = Circuit(3)
- state.H(0).CX(0, 1).V(2)
+backend = AerStateBackend()
- xxy = QubitPauliString({
- Qubit(0) : Pauli.X,
- Qubit(1) : Pauli.X,
- Qubit(2) : Pauli.Y})
- zzi = QubitPauliString({
- Qubit(0) : Pauli.Z,
- Qubit(1) : Pauli.Z})
- iiz = QubitPauliString({
- Qubit(2) : Pauli.Z})
- op = QubitPauliOperator({
- QubitPauliString() : -0.5,
- xxy : 0.7,
- zzi : 1.4,
- iiz : 3.2})
-
- assert backend.supports_expectation
- state = backend.get_compiled_circuit(state)
- print(backend.get_pauli_expectation_value(state, xxy))
- print(backend.get_operator_expectation_value(state, op))
+state = Circuit(3)
+state.H(0).CX(0, 1).V(2)
+
+xxy = QubitPauliString({
+ Qubit(0) : Pauli.X,
+ Qubit(1) : Pauli.X,
+ Qubit(2) : Pauli.Y})
+zzi = QubitPauliString({
+ Qubit(0) : Pauli.Z,
+ Qubit(1) : Pauli.Z})
+iiz = QubitPauliString({
+ Qubit(2) : Pauli.Z})
+op = QubitPauliOperator({
+ QubitPauliString() : -0.5,
+ xxy : 0.7,
+ zzi : 1.4,
+ iiz : 3.2})
+
+assert backend.supports_expectation
+state = backend.get_compiled_circuit(state)
+print(backend.get_pauli_expectation_value(state, xxy))
+print(backend.get_operator_expectation_value(state, op))
```
### Asynchronous Job Submission
From 510f1d50a98b18fbd33a5d038faa75637f464027 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 13:18:04 +0100
Subject: [PATCH 03/16] fix indentation issues in circuit page
---
docs/manual/manual_circuit.md | 986 +++++++++++++++++-----------------
1 file changed, 488 insertions(+), 498 deletions(-)
diff --git a/docs/manual/manual_circuit.md b/docs/manual/manual_circuit.md
index 6c0bd19c..af752555 100644
--- a/docs/manual/manual_circuit.md
+++ b/docs/manual/manual_circuit.md
@@ -46,13 +46,13 @@ Basic quantum gates represent some unitary operation applied to some qubits. Add
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(4) # qubits are numbered 0-3
- circ.X(0) # first apply an X gate to qubit 0
- circ.CX(1, 3) # and apply a CX gate with control qubit 1 and target qubit 3
- circ.Z(3) # then apply a Z gate to qubit 3
- circ.get_commands() # show the commands of the built circuit
+circ = Circuit(4) # qubits are numbered 0-3
+circ.X(0) # first apply an X gate to qubit 0
+circ.CX(1, 3) # and apply a CX gate with control qubit 1 and target qubit 3
+circ.Z(3) # then apply a Z gate to qubit 3
+circ.get_commands() # show the commands of the built circuit
```
% parameterised gates; parameter first, always in half-turns
@@ -62,12 +62,12 @@ For parameterised gates, such as rotations, the parameter is always given first.
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(2)
- circ.Rx(0.5, 0) # Rx of angle pi/2 radians on qubit 0
- circ.CRz(0.3, 1, 0) # Controlled-Rz of angle 0.3pi radians with
- # control qubit 1 and target qubit 0
+circ = Circuit(2)
+circ.Rx(0.5, 0) # Rx of angle pi/2 radians on qubit 0
+circ.CRz(0.3, 1, 0) # Controlled-Rz of angle 0.3pi radians with
+ # control qubit 1 and target qubit 0
```
% Table of common gates, with circuit notation, unitary, and python command
@@ -81,15 +81,15 @@ A large selection of common gates are available in this way, as listed in the AP
```{code-cell} ipython3
- from pytket import Circuit, OpType
+from pytket import Circuit, OpType
- circ = Circuit(5)
- circ.add_gate(OpType.CnX, [0, 1, 4, 3])
- # add controlled-X with control qubits 0, 1, 4 and target qubit 3
- circ.add_gate(OpType.XXPhase, 0.7, [0, 2])
- # add e^{-i (0.7 pi / 2) XX} on qubits 0 and 2
- circ.add_gate(OpType.PhasedX, [-0.1, 0.5], [3])
- # adds Rz(-0.5 pi); Rx(-0.1 pi); Rz(0.5 pi) on qubit 3
+circ = Circuit(5)
+circ.add_gate(OpType.CnX, [0, 1, 4, 3])
+ # add controlled-X with control qubits 0, 1, 4 and target qubit 3
+circ.add_gate(OpType.XXPhase, 0.7, [0, 2])
+ # add e^{-i (0.7 pi / 2) XX} on qubits 0 and 2
+circ.add_gate(OpType.PhasedX, [-0.1, 0.5], [3])
+ # adds Rz(-0.5 pi); Rx(-0.1 pi); Rz(0.5 pi) on qubit 3
```
The API reference for the {py:class}`~pytket.OpType` class details all available operations that can exist in a circuit.
@@ -111,14 +111,14 @@ Adding a measurement works just like adding any other gate, where the first argu
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(4, 2)
- circ.Measure(0, 0) # Z-basis measurement on qubit 0, saving result in bit 0
- circ.CX(1, 2)
- circ.CX(1, 3)
- circ.H(1)
- circ.Measure(1, 1) # Measurement of IXXX, saving result in bit 1
+circ = Circuit(4, 2)
+circ.Measure(0, 0) # Z-basis measurement on qubit 0, saving result in bit 0
+circ.CX(1, 2)
+circ.CX(1, 3)
+circ.H(1)
+circ.Measure(1, 1) # Measurement of IXXX, saving result in bit 1
```
% Overwriting data in classical bits
@@ -128,12 +128,12 @@ Because the classical bits are treated as statically assigned locations, writing
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(2, 1)
- circ.Measure(0, 0) # measure the first measurement
- circ.CX(0, 1)
- circ.Measure(1, 0) # overwrites the first result with a new measurement
+circ = Circuit(2, 1)
+circ.Measure(0, 0) # measure the first measurement
+circ.CX(0, 1)
+circ.Measure(1, 0) # overwrites the first result with a new measurement
```
% Measurement on real devices could require a single layer at end, or sufficiently noisy that they appear destructive so require resets
@@ -143,29 +143,29 @@ Depending on where we plan on running our circuits, the backend or simulator mig
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ0 = Circuit(2, 2) # all measurements at end
- circ0.H(1)
- circ0.Measure(0, 0)
- circ0.Measure(1, 1)
+circ0 = Circuit(2, 2) # all measurements at end
+circ0.H(1)
+circ0.Measure(0, 0)
+circ0.Measure(1, 1)
- circ1 = Circuit(2, 2) # this is DAG-equivalent to circ1, so is still ok
- circ1.Measure(0, 0)
- circ1.H(1)
- circ1.Measure(1, 1)
+circ1 = Circuit(2, 2) # this is DAG-equivalent to circ1, so is still ok
+circ1.Measure(0, 0)
+circ1.H(1)
+circ1.Measure(1, 1)
- circ2 = Circuit(2, 2)
- # reuses qubit 0 after measuring, so this may be rejected by a device
- circ2.Measure(0, 0)
- circ2.CX(0, 1)
- circ2.Measure(1, 1)
+circ2 = Circuit(2, 2)
+ # reuses qubit 0 after measuring, so this may be rejected by a device
+circ2.Measure(0, 0)
+circ2.CX(0, 1)
+circ2.Measure(1, 1)
- circ3 = Circuit(2, 1)
- # overwriting the classical value means we have to measure qubit 0
- # before qubit 1; they won't occur simultaneously so this may be rejected
- circ3.Measure(0, 0)
- circ3.Measure(1, 0)
+circ3 = Circuit(2, 1)
+ # overwriting the classical value means we have to measure qubit 0
+ # before qubit 1; they won't occur simultaneously so this may be rejected
+circ3.Measure(0, 0)
+circ3.Measure(1, 0)
```
% `measure_all`
@@ -175,17 +175,17 @@ The simplest way to guarantee this is to finish the circuit by measuring all qub
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- # measure qubit 0 in Z basis and 1 in X basis
- circ = Circuit(2, 2)
- circ.H(1)
- circ.measure_all()
+# measure qubit 0 in Z basis and 1 in X basis
+circ = Circuit(2, 2)
+circ.H(1)
+circ.measure_all()
- # measure_all() adds bits if they are not already defined, so equivalently
- circ = Circuit(2)
- circ.H(1)
- circ.measure_all()
+# measure_all() adds bits if they are not already defined, so equivalently
+circ = Circuit(2)
+circ.H(1)
+circ.measure_all()
```
On devices where mid-circuit measurements are available, they may be highly noisy and not apply just a basic projector on the quantum state. We can view these as "effectively destructive" measurements, where the qubit still exists but is in a noisy state. In this case, it is recommended to actively reset a qubit after measurement if it is intended to be reused.
@@ -193,16 +193,16 @@ On devices where mid-circuit measurements are available, they may be highly nois
```{code-cell} ipython3
- from pytket import Circuit, OpType
+from pytket import Circuit, OpType
- circ = Circuit(2, 2)
- circ.Measure(0, 0)
- # Actively reset state to |0>
- circ.Reset(0)
- # Conditionally flip state to |1> to reflect measurement result
- circ.X(0, condition_bits=[0], condition_value=1)
- # Use the qubit as if the measurement was non-destructive
- circ.CX(0, 1)
+circ = Circuit(2, 2)
+circ.Measure(0, 0)
+# Actively reset state to |0>
+circ.Reset(0)
+# Conditionally flip state to |1> to reflect measurement result
+circ.X(0, condition_bits=[0], condition_value=1)
+# Use the qubit as if the measurement was non-destructive
+circ.CX(0, 1)
```
## Barriers
@@ -225,14 +225,14 @@ Adding a barrier to a {py:class}`~pytket.circuit.Circuit` is done using the {py:
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(4, 2)
- circ.H(0)
- circ.CX(1, 2)
- circ.add_barrier([0, 1, 2, 3], [0, 1]) # add a barrier on all qubits and bits
- circ.Measure(0, 0)
- circ.Measure(2, 1)
+circ = Circuit(4, 2)
+circ.H(0)
+circ.CX(1, 2)
+circ.add_barrier([0, 1, 2, 3], [0, 1]) # add a barrier on all qubits and bits
+circ.Measure(0, 0)
+circ.Measure(2, 1)
```
## Registers and IDs
@@ -252,20 +252,20 @@ Named resources can be added to {py:class}`~pytket.circuit.Circuit` s individu
```{code-cell} ipython3
- from pytket import Circuit, Qubit, Bit
+from pytket import Circuit, Qubit, Bit
- circ = Circuit()
- qreg = circ.add_q_register("reg", 2) # add a qubit register
+circ = Circuit()
+qreg = circ.add_q_register("reg", 2) # add a qubit register
- anc = Qubit("ancilla") # add a named qubit
- circ.add_qubit(anc)
+anc = Qubit("ancilla") # add a named qubit
+circ.add_qubit(anc)
- par = Bit("parity", [0, 0]) # add a named bit with a 2D index
- circ.add_bit(par)
+par = Bit("parity", [0, 0]) # add a named bit with a 2D index
+circ.add_bit(par)
- circ.CX(qreg[0], anc) # add gates in terms of IDs
- circ.CX(qreg[1], anc)
- circ.Measure(anc, par)
+circ.CX(qreg[0], anc) # add gates in terms of IDs
+circ.CX(qreg[1], anc)
+circ.Measure(anc, par)
```
% Query circuits to identify what qubits and bits it contains
@@ -275,15 +275,15 @@ A {py:class}`~pytket.circuit.Circuit` can be inspected to identify what qubits a
```{code-cell} ipython3
- from pytket import Circuit, Qubit
+from pytket import Circuit, Qubit
- circ = Circuit()
- circ.add_q_register("a", 4)
- circ.add_qubit(Qubit("b"))
- circ.add_c_register("z", 3)
+circ = Circuit()
+circ.add_q_register("a", 4)
+circ.add_qubit(Qubit("b"))
+circ.add_c_register("z", 3)
- print(circ.qubits)
- print(circ.bits)
+print(circ.qubits)
+print(circ.bits)
```
% Restrictions on registers (circuit will reject ids if they are already in use or the index dimension/resource type is inconsistent with existing ids of that name)
@@ -294,31 +294,31 @@ To help encourage consistency of identifiers, a {py:class}`~pytket.circuit.Circu
```{code-cell} ipython3
:raises: RuntimeError
- from pytket import Circuit, Qubit, Bit
+from pytket import Circuit, Qubit, Bit
- circ = Circuit()
- # set up a circuit with qubit a[0]
- circ.add_qubit(Qubit("a", 0))
+circ = Circuit()
+# set up a circuit with qubit a[0]
+circ.add_qubit(Qubit("a", 0))
- # rejected because "a" is already a qubit register
- circ.add_bit(Bit("a", 1))
+# rejected because "a" is already a qubit register
+circ.add_bit(Bit("a", 1))
```
```{code-cell} ipython3
:raises: RuntimeError
- # rejected because "a" is already a 1D register
- circ.add_qubit(Qubit("a", [1, 2]))
- circ.add_qubit(Qubit("a"))
+# rejected because "a" is already a 1D register
+circ.add_qubit(Qubit("a", [1, 2]))
+circ.add_qubit(Qubit("a"))
```
```{code-cell} ipython3
:raises: RuntimeError
- # rejected because a[0] is already in the circuit
- circ.add_qubit(Qubit("a", 0))
+# rejected because a[0] is already in the circuit
+circ.add_qubit(Qubit("a", 0))
```
% Integer labels correspond to default registers (example of using explicit labels from `Circuit(n)`)
@@ -328,11 +328,11 @@ The basic integer identifiers are actually a special case, referring to the defa
```{code-cell} ipython3
- from pytket import Circuit, Qubit, Bit
+from pytket import Circuit, Qubit, Bit
- circ = Circuit(4, 2)
- circ.CX(Qubit(0), Qubit("q", 1)) # same as circ.CX(0, 1)
- circ.Measure(Qubit(2), Bit("c", 0)) # same as circ.Measure(2, 0)
+circ = Circuit(4, 2)
+circ.CX(Qubit(0), Qubit("q", 1)) # same as circ.CX(0, 1)
+circ.Measure(Qubit(2), Bit("c", 0)) # same as circ.Measure(2, 0)
```
% Rename with `rename_units` as long as the names after renaming would be unique and have consistent register typings
@@ -342,19 +342,19 @@ In some circumstances, it may be useful to rename the resources in the {py:class
```{code-cell} ipython3
- from pytket import Circuit, Qubit, Bit
+from pytket import Circuit, Qubit, Bit
- circ = Circuit(2, 2)
- circ.add_qubit(Qubit("a", 0))
+circ = Circuit(2, 2)
+circ.add_qubit(Qubit("a", 0))
- qubit_map = {
- Qubit("a", 0) : Qubit(3),
- Qubit(1) : Qubit("a", 0),
- Bit(0) : Bit("z", [0, 1]),
- }
- circ.rename_units(qubit_map)
- print(circ.qubits)
- print(circ.bits)
+qubit_map = {
+ Qubit("a", 0) : Qubit(3),
+ Qubit(1) : Qubit("a", 0),
+ Bit(0) : Bit("z", [0, 1]),
+}
+circ.rename_units(qubit_map)
+print(circ.qubits)
+print(circ.bits)
```
## Composing Circuits
@@ -362,27 +362,26 @@ In some circumstances, it may be useful to rename the resources in the {py:class
% Appending matches units of the same id
-.. currentmodule:: pytket.circuit
-```
+
Because {py:class}`Circuit` s are defined to have open inputs and outputs, it is perfectly natural to compose them by unifying the outputs of one with the inputs of another. Appending one {py:class}`Circuit` to the end of another matches the inputs and outputs with the same {py:class}`UnitID`.
```{code-cell} ipython3
- from pytket import Circuit, Qubit, Bit
+from pytket import Circuit, Qubit, Bit
- circ = Circuit(2, 2)
- circ.CX(0, 1)
- circ.Rz(0.3, 1)
- circ.CX(0, 1)
+circ = Circuit(2, 2)
+circ.CX(0, 1)
+circ.Rz(0.3, 1)
+circ.CX(0, 1)
- measures = Circuit(2, 2)
- measures.H(1)
- measures.measure_all()
+measures = Circuit(2, 2)
+measures.H(1)
+measures.measure_all()
- circ.append(measures)
- circ
+circ.append(measures)
+circ
```
% If a unit does not exist in the other circuit, treated as composing with identity
@@ -396,21 +395,21 @@ To compose two circuits in parallel we can take tensor product using the * opera
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.circuit.display import render_circuit_jupyter as draw
+from pytket import Circuit
+from pytket.circuit.display import render_circuit_jupyter as draw
- circ1 = Circuit()
- j = circ1.add_q_register("j", 1)
- circ1.Y(j[0])
+circ1 = Circuit()
+j = circ1.add_q_register("j", 1)
+circ1.Y(j[0])
- circ2 = Circuit()
- k = circ2.add_q_register("k", 2)
- circ2.X(k[1])
- circ2.CRz(0.64, k[1], k[0])
+circ2 = Circuit()
+k = circ2.add_q_register("k", 2)
+circ2.X(k[1])
+circ2.CRz(0.64, k[1], k[0])
- circ3 = circ1 * circ2 # Take the tensor product
+circ3 = circ1 * circ2 # Take the tensor product
- draw(circ3)
+draw(circ3)
```
If we attempt to form the tensor product of two circuits without distinct qubit names then we will get a {py:class}`RuntimeError` as the composition is not defined.
@@ -419,15 +418,15 @@ If we attempt to form the tensor product of two circuits without distinct qubit
```{code-cell} ipython3
:raises: RuntimeError
- from pytket import Circuit
+from pytket import Circuit
- circ_x = Circuit()
- l_reg1 = circ_x.add_q_register("l", 1)
+circ_x = Circuit()
+l_reg1 = circ_x.add_q_register("l", 1)
- circ_y = Circuit()
- l_reg2 = circ_y.add_q_register("l", 1)
+circ_y = Circuit()
+l_reg2 = circ_y.add_q_register("l", 1)
- circ_x * circ_y # Error as both circuits have l[0]
+circ_x * circ_y # Error as both circuits have l[0]
```
@@ -470,25 +469,25 @@ To change which units get unified, we could use {py:meth}`Circuit.rename_units`
```{code-cell} ipython3
- from pytket import Circuit, Qubit
+from pytket import Circuit, Qubit
- circ = Circuit()
- a = circ.add_q_register("a", 2)
- circ.Rx(0.2, a[0])
- circ.CX(a[0], a[1])
+circ = Circuit()
+a = circ.add_q_register("a", 2)
+circ.Rx(0.2, a[0])
+circ.CX(a[0], a[1])
- next_circ = Circuit(2)
- next_circ.Z(0)
- next_circ.CZ(1, 0)
+next_circ = Circuit(2)
+next_circ.Z(0)
+next_circ.CZ(1, 0)
- circ.add_circuit(next_circ, [a[1], a[0]])
+circ.add_circuit(next_circ, [a[1], a[0]])
- # This is equivalent to:
- # temp = next_circ.copy()
- # temp.rename_units({Qubit(0) : a[1], Qubit(1) : a[0]})
- # circ.append(temp)
+# This is equivalent to:
+# temp = next_circ.copy()
+# temp.rename_units({Qubit(0) : a[1], Qubit(1) : a[0]})
+# circ.append(temp)
- draw(circ)
+draw(circ)
```
:::{note}
@@ -502,11 +501,11 @@ When working with quantum circuits we may want access to the quantum state prepa
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(2)
- circ.H(0).CX(0, 1)
- circ.get_statevector()
+circ = Circuit(2)
+circ.H(0).CX(0, 1)
+circ.get_statevector()
```
In addition {py:meth}`Circuit.get_unitary` can be used to numerically calculate the unitary matrix that will be applied by the circuit.
@@ -514,11 +513,11 @@ In addition {py:meth}`Circuit.get_unitary` can be used to numerically calculate
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(2)
- circ.H(0).CZ(0, 1).H(1)
- circ.get_unitary()
+circ = Circuit(2)
+circ.H(0).CZ(0, 1).H(1)
+circ.get_unitary()
```
:::{warning}
@@ -536,14 +535,14 @@ Because the {py:class}`~pytket.circuit.Circuit` class identifies circuits up to
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(3)
- circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
+circ = Circuit(3)
+circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
- for com in circ: # equivalently, circ.get_commands()
- print(com.op, com.op.type, com.args)
- # NOTE: com is not a reference to something inside circ; this cannot be used to modify the circuit
+for com in circ: # equivalently, circ.get_commands()
+ print(com.op, com.op.type, com.args)
+ # NOTE: com is not a reference to something inside circ; this cannot be used to modify the circuit
```
% To see more succinctly, can visualise in circuit form or the underlying DAG
@@ -553,12 +552,12 @@ If you are working in a Jupyter environment, a {py:class}`~pytket.circuit.Circui
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.circuit.display import render_circuit_jupyter as draw
+from pytket import Circuit
+from pytket.circuit.display import render_circuit_jupyter as draw
- circ = Circuit(3)
- circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
- draw(circ) # Render interactive circuit diagram
+circ = Circuit(3)
+circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
+draw(circ) # Render interactive circuit diagram
```
:::{note}
@@ -570,12 +569,12 @@ The pytket circuit renderer can represent circuits in the standard circuit model
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.utils import Graph
+from pytket import Circuit
+from pytket.utils import Graph
- circ = Circuit(3)
- circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
- Graph(circ).get_DAG() # Displays in interactive python notebooks
+circ = Circuit(3)
+circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
+Graph(circ).get_DAG() # Displays in interactive python notebooks
```
The visualisation tool can also describe the interaction graph of a {py:class}`~pytket.circuit.Circuit` consisting of only one- and two-qubit gates -- that is, the graph of which qubits will share a two-qubit gate at some point during execution.
@@ -589,12 +588,12 @@ There are also the methods {py:meth}`~pytket.utils.Graph.save_DAG` and {py:meth}
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.utils import Graph
+from pytket import Circuit
+from pytket.utils import Graph
- circ = Circuit(4)
- circ.CX(0, 1).CZ(1, 2).ZZPhase(0.63, 2, 3).CX(1, 3).CY(0, 1)
- Graph(circ).get_qubit_graph()
+circ = Circuit(4)
+circ.CX(0, 1).CZ(1, 2).ZZPhase(0.63, 2, 3).CX(1, 3).CY(0, 1)
+Graph(circ).get_qubit_graph()
```
% Won't always want this much detail, so can also query for common metrics (gate count, specific ops, depth, T-depth and 2q-depth)
@@ -604,13 +603,13 @@ The full instruction sequence may often be too much detail for a lot of needs, e
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- circ = Circuit(3)
- circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
+circ = Circuit(3)
+circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
- print("Total gate count =", circ.n_gates)
- print("Circuit depth =", circ.depth())
+print("Total gate count =", circ.n_gates)
+print("Circuit depth =", circ.depth())
```
As characteristics of a {py:class}`~pytket.circuit.Circuit` go, these are pretty basic. In terms of approximating the noise level, they fail heavily from weighting all gates evenly when, in fact, some will be much harder to implement than others. For example, in the NISQ era, we find that most technologies provide good single-qubit gate times and fidelities, with two-qubit gates being much slower and noisier [^cite_arut2019]. On the other hand, looking forward to the fault-tolerant regime we will expect Clifford gates to be very cheap but the magic $T$ gates to require expensive distillation procedures [^cite_brav2005] [^cite_brav2012].
@@ -622,27 +621,27 @@ We also define $G$-depth (for a subset of gate types $G$) as the minimum number
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.circuit.display import render_circuit_jupyter as draw
+from pytket import Circuit, OpType
+from pytket.circuit.display import render_circuit_jupyter as draw
- circ = Circuit(3)
- circ.T(0)
- circ.CX(0, 1)
- circ.CX(2, 0)
- circ.add_gate(OpType.CnRy, [0.6], [0, 1, 2])
- circ.T(2)
- circ.CZ(0, 1)
- circ.CZ(1, 2)
- circ.T(1)
+circ = Circuit(3)
+circ.T(0)
+circ.CX(0, 1)
+circ.CX(2, 0)
+circ.add_gate(OpType.CnRy, [0.6], [0, 1, 2])
+circ.T(2)
+circ.CZ(0, 1)
+circ.CZ(1, 2)
+circ.T(1)
- draw(circ) # draw circuit diagram
+draw(circ) # draw circuit diagram
- print("T gate count =", circ.n_gates_of_type(OpType.T))
- print("#1qb gates =", circ.n_1qb_gates())
- print("#2qb gates =", circ.n_2qb_gates())
- print("#3qb gates =", circ.n_nqb_gates(3)) # count the single CnRy gate (n=3)
- print("T gate depth =", circ.depth_by_type(OpType.T))
- print("2qb gate depth =", circ.depth_by_type({OpType.CX, OpType.CZ}))
+print("T gate count =", circ.n_gates_of_type(OpType.T))
+print("#1qb gates =", circ.n_1qb_gates())
+print("#2qb gates =", circ.n_2qb_gates())
+print("#3qb gates =", circ.n_nqb_gates(3)) # count the single CnRy gate (n=3)
+print("T gate depth =", circ.depth_by_type(OpType.T))
+print("2qb gate depth =", circ.depth_by_type({OpType.CX, OpType.CZ}))
```
:::{note}
@@ -681,19 +680,19 @@ it to another circuit as part of a larger algorithm.
```{code-cell} ipython3
- from pytket.circuit import Circuit, OpType
- from pytket.circuit.display import render_circuit_jupyter as draw
+from pytket.circuit import Circuit, OpType
+from pytket.circuit.display import render_circuit_jupyter as draw
- oracle_circ = Circuit(3, name="Oracle")
- oracle_circ.X(0)
- oracle_circ.X(1)
- oracle_circ.X(2)
- oracle_circ.add_gate(OpType.CnZ, [0, 1, 2])
- oracle_circ.X(0)
- oracle_circ.X(1)
- oracle_circ.X(2)
+oracle_circ = Circuit(3, name="Oracle")
+oracle_circ.X(0)
+oracle_circ.X(1)
+oracle_circ.X(2)
+oracle_circ.add_gate(OpType.CnZ, [0, 1, 2])
+oracle_circ.X(0)
+oracle_circ.X(1)
+oracle_circ.X(2)
- draw(oracle_circ)
+draw(oracle_circ)
```
Now that we've built our circuit we can wrap it up in a {py:class}`~pytket.circuit.CircBox` and add it to a another circuit as a subroutine.
@@ -701,15 +700,14 @@ Now that we've built our circuit we can wrap it up in a {py:class}`~pytket.circu
```{code-cell} ipython3
- from pytket.circuit import CircBox
-
- oracle_box = CircBox(oracle_circ)
- circ = Circuit(3)
- circ.H(0).H(1).H(2)
- circ.add_gate(oracle_box, [0, 1, 2])
+from pytket.circuit import CircBox
- draw(circ)
+oracle_box = CircBox(oracle_circ)
+circ = Circuit(3)
+circ.H(0).H(1).H(2)
+circ.add_gate(oracle_box, [0, 1, 2])
+draw(circ)
```
See how the name of the circuit appears in the rendered circuit diagram. Clicking on the box will show the underlying circuit.
@@ -726,37 +724,33 @@ For such algorithms we may wish to create a {py:class}`~pytket.circuit.CircBox`
```{code-cell} ipython3
- from pytket.circuit import Circuit
-
- # Set up circuit registers
- qpe_circ = Circuit(name="QPE")
- a = qpe_circ.add_q_register("a", 2)
- s = qpe_circ.add_q_register("s", 1)
- c = qpe_circ.add_c_register("c", 2)
+from pytket.circuit import Circuit
- # Initial superposition
- qpe_circ.H(a[0])
- qpe_circ.H(a[1])
+# Set up circuit registers
+qpe_circ = Circuit(name="QPE")
+a = qpe_circ.add_q_register("a", 2)
+s = qpe_circ.add_q_register("s", 1)
+c = qpe_circ.add_c_register("c", 2)
- # Sequence of controlled unitaries
- qpe_circ.CU1(0.94, a[1], s[0])
- qpe_circ.CU1(0.94, a[0], s[0])
- qpe_circ.CU1(0.94, a[0], s[0])
+# Initial superposition
+qpe_circ.H(a[0])
+qpe_circ.H(a[1])
- # 2-qubit QFT (simplified)
- qpe_circ.H(a[0])
- qpe_circ.CU1(0.5, a[1], a[0])
- qpe_circ.H(a[1])
- qpe_circ.SWAP(a[0], a[1])
+# Sequence of controlled unitaries
+qpe_circ.CU1(0.94, a[1], s[0])
+qpe_circ.CU1(0.94, a[0], s[0])
+qpe_circ.CU1(0.94, a[0], s[0])
- # Measure qubits writing to the classical register
- qpe_circ.measure_register(a, "c")
-
- draw(qpe_circ)
-```
+# 2-qubit QFT (simplified)
+qpe_circ.H(a[0])
+qpe_circ.CU1(0.5, a[1], a[0])
+qpe_circ.H(a[1])
+qpe_circ.SWAP(a[0], a[1])
+# Measure qubits writing to the classical register
+qpe_circ.measure_register(a, "c")
-.. currentmodule:: pytket.circuit
+draw(qpe_circ)
```
Now that we have defined our phase estimation circuit we can use a {py:class}`CircBox` to define a reusable subroutine. This {py:class}`CircBox` will contain the state preparation and ancilla registers.
@@ -764,10 +758,10 @@ Now that we have defined our phase estimation circuit we can use a {py:class}`Ci
```{code-cell} ipython3
- from pytket.circuit import CircBox
+from pytket.circuit import CircBox
- # Construct QPE subroutine
- qpe_box = CircBox(qpe_circ)
+# Construct QPE subroutine
+qpe_box = CircBox(qpe_circ)
```
Let's now create a circuit to implement the QPE algorithm where we prepare the $|1\rangle$ state in the state prep register with a single X gate.
@@ -775,14 +769,14 @@ Let's now create a circuit to implement the QPE algorithm where we prepare the $
```{code-cell} ipython3
- # Construct simplified state preparation circuit
- algorithm_circ = Circuit()
- ancillas = algorithm_circ.add_q_register("ancillas", 2)
- state = algorithm_circ.add_q_register("state", 1)
- c = algorithm_circ.add_c_register("c", 2)
- algorithm_circ.X(state[0])
+# Construct simplified state preparation circuit
+algorithm_circ = Circuit()
+ancillas = algorithm_circ.add_q_register("ancillas", 2)
+state = algorithm_circ.add_q_register("state", 1)
+c = algorithm_circ.add_c_register("c", 2)
+algorithm_circ.X(state[0])
- draw(algorithm_circ)
+draw(algorithm_circ)
```
We can then compose our subroutine registerwise by using {py:meth}`Circuit.add_circbox_with_regmap` method.
@@ -793,12 +787,12 @@ Note that the sizes of the registers used as keys and values must be equal.
```{code-cell} ipython3
- # Append QPE subroutine to algorithm_circ registerwise
- algorithm_circ.add_circbox_with_regmap(
- qpe_box, qregmap={"a": "ancillas", "s": "state"}, cregmap={"c": "c"}
- )
+# Append QPE subroutine to algorithm_circ registerwise
+algorithm_circ.add_circbox_with_regmap(
+ qpe_box, qregmap={"a": "ancillas", "s": "state"}, cregmap={"c": "c"}
+)
- draw(algorithm_circ)
+draw(algorithm_circ)
```
Click on the QPE box in the diagram above to view the underlying circuit.
@@ -810,17 +804,17 @@ Lets first define a circuit with the register names `a`, `b` and `c`.
```{code-cell} ipython3
- # Set up Circuit with registers a_reg, b_reg and c_reg
- abc_circuit = Circuit()
- a_reg = abc_circuit.add_q_register("a", 2)
- b_reg = abc_circuit.add_q_register("b", 2)
- c_reg = abc_circuit.add_q_register("c", 2)
+# Set up Circuit with registers a_reg, b_reg and c_reg
+abc_circuit = Circuit()
+a_reg = abc_circuit.add_q_register("a", 2)
+b_reg = abc_circuit.add_q_register("b", 2)
+c_reg = abc_circuit.add_q_register("c", 2)
- # Add some gates
- abc_circuit.H(a_reg[0])
- abc_circuit.Ry(0.46, a_reg[1])
- abc_circuit.CCX(a_reg[0], a_reg[1], c_reg[0])
- draw(abc_circuit)
+# Add some gates
+abc_circuit.H(a_reg[0])
+abc_circuit.Ry(0.46, a_reg[1])
+abc_circuit.CCX(a_reg[0], a_reg[1], c_reg[0])
+draw(abc_circuit)
```
Now lets create a {py:class}`CircBox` containing some elementary gates and append it across the `b` and `c` registers with {py:meth}`Circuit.add_circbox_regwise`.
@@ -828,16 +822,16 @@ Now lets create a {py:class}`CircBox` containing some elementary gates and appen
```{code-cell} ipython3
- # Create subroutine
- sub_circuit = Circuit(4, name="BC")
- sub_circuit.CX(3, 2).CX(3, 1).CX(3, 0)
- sub_circuit.H(3)
- bc_subroutine = CircBox(sub_circuit)
+# Create subroutine
+sub_circuit = Circuit(4, name="BC")
+sub_circuit.CX(3, 2).CX(3, 1).CX(3, 0)
+sub_circuit.H(3)
+bc_subroutine = CircBox(sub_circuit)
- # Append CircBox to the b_reg and c_reg registers (note empty list for classical registers)
- abc_circuit.add_circbox_regwise(bc_subroutine, [b_reg, c_reg], [])
+# Append CircBox to the b_reg and c_reg registers (note empty list for classical registers)
+abc_circuit.add_circbox_regwise(bc_subroutine, [b_reg, c_reg], [])
- draw(abc_circuit)
+draw(abc_circuit)
```
@@ -848,26 +842,26 @@ It is possible to specify small unitaries from `numpy` arrays and embed them dir
```{code-cell} ipython3
- from pytket.circuit import Circuit, Unitary1qBox, Unitary2qBox
- import numpy as np
+from pytket.circuit import Circuit, Unitary1qBox, Unitary2qBox
+import numpy as np
- u1 = np.asarray([[2/3, (-2+1j)/3],
- [(2+1j)/3, 2/3]])
- u1box = Unitary1qBox(u1)
+u1 = np.asarray([[2/3, (-2+1j)/3],
+ [(2+1j)/3, 2/3]])
+u1box = Unitary1qBox(u1)
- u2 = np.asarray([[0, 1, 0, 0],
- [0, 0, 0, -1],
- [1, 0, 0, 0],
- [0, 0, -1j, 0]])
- u2box = Unitary2qBox(u2)
+u2 = np.asarray([[0, 1, 0, 0],
+ [0, 0, 0, -1],
+ [1, 0, 0, 0],
+ [0, 0, -1j, 0]])
+u2box = Unitary2qBox(u2)
- circ = Circuit(3)
- circ.add_unitary1qbox(u1box, 0)
- circ.add_unitary2qbox(u2box, 1, 2)
- circ.add_unitary1qbox(u1box, 2)
- circ.add_unitary2qbox(u2box, 1, 0)
+circ = Circuit(3)
+circ.add_unitary1qbox(u1box, 0)
+circ.add_unitary2qbox(u2box, 1, 2)
+circ.add_unitary1qbox(u1box, 2)
+circ.add_unitary2qbox(u2box, 1, 0)
- draw(circ)
+draw(circ)
```
:::{note}
@@ -886,23 +880,23 @@ If our subcircuit is a pure quantum circuit (i.e. it corresponds to a unitary op
```{code-cell} ipython3
- from pytket.circuit import Circuit, CircBox, QControlBox
+from pytket.circuit import Circuit, CircBox, QControlBox
- sub = Circuit(2, name="V")
- sub.CX(0, 1).Rz(0.2, 1).CX(0, 1)
- sub_box = CircBox(sub)
+sub = Circuit(2, name="V")
+sub.CX(0, 1).Rz(0.2, 1).CX(0, 1)
+sub_box = CircBox(sub)
- # Define the controlled operation with 2 control qubits
- cont = QControlBox(sub_box, 2)
+# Define the controlled operation with 2 control qubits
+cont = QControlBox(sub_box, 2)
- circ = Circuit(4)
- circ.add_gate(sub_box, [2, 3])
- circ.Ry(0.3, 0).Ry(0.8, 1)
+circ = Circuit(4)
+circ.add_gate(sub_box, [2, 3])
+circ.Ry(0.3, 0).Ry(0.8, 1)
- # Add to circuit with controls q[0], q[1], and targets q[2], q[3]
- circ.add_gate(cont, [0, 1, 2, 3])
+# Add to circuit with controls q[0], q[1], and targets q[2], q[3]
+circ.add_gate(cont, [0, 1, 2, 3])
- draw(circ)
+draw(circ)
```
As well as creating controlled boxes, we can create a controlled version of an arbitrary {py:class}`~pytket.circuit.Op` as follows.
@@ -910,10 +904,10 @@ As well as creating controlled boxes, we can create a controlled version of an a
```{code-cell} ipython3
- from pytket.circuit import Op, OpType, QControlBox
+from pytket.circuit import Op, OpType, QControlBox
- op = Op.create(OpType.S)
- ccs = QControlBox(op, 2)
+op = Op.create(OpType.S)
+ccs = QControlBox(op, 2)
```
:::{note}
@@ -926,15 +920,15 @@ For example, we can construct a multicontrolled $\sqrt{Y}$ operation as by first
```{code-cell} ipython3
- from pytket.circuit import Unitary1qBox, QControlBox
- import numpy as np
+from pytket.circuit import Unitary1qBox, QControlBox
+import numpy as np
- # Unitary for sqrt(Y)
- sqrt_y = np.asarray([[1/2+1j/2, -1/2-1j/2],
- [1/2+1j/2, 1/2+1j/2]])
+# Unitary for sqrt(Y)
+sqrt_y = np.asarray([[1/2+1j/2, -1/2-1j/2],
+ [1/2+1j/2, 1/2+1j/2]])
- sqrt_y_box = Unitary1qBox(sqrt_y)
- c2_root_y = QControlBox(sqrt_y_box, 2)
+sqrt_y_box = Unitary1qBox(sqrt_y)
+c2_root_y = QControlBox(sqrt_y_box, 2)
```
Normally when we deal with controlled gates we implicitly assume that the control state is the "all $|1\rangle$" state. So that the base gate is applied when all of the control qubits are all set to $|1\rangle$.
@@ -948,15 +942,15 @@ Lets now construct a multi-controlled Rz gate with the control state $|0010\rang
```{code-cell} ipython3
- from pytket.circuit import Circuit, Op, OpType, QControlBox
+from pytket.circuit import Circuit, Op, OpType, QControlBox
- rz_op = Op.create(OpType.Rz, 0.61)
- multi_controlled_rz = QControlBox(rz_op, n_controls=4, control_state=[0, 0, 1, 0])
+rz_op = Op.create(OpType.Rz, 0.61)
+multi_controlled_rz = QControlBox(rz_op, n_controls=4, control_state=[0, 0, 1, 0])
- test_circ = Circuit(5)
- test_circ.add_gate(multi_controlled_rz, test_circ.qubits)
+test_circ = Circuit(5)
+test_circ.add_gate(multi_controlled_rz, test_circ.qubits)
- draw(test_circ)
+draw(test_circ)
```
Notice how the circuit renderer shows both filled and unfilled circles on the control qubits. Filled circles correspond to $|1\rangle$ controls whereas empty circles represent $|0\rangle$. As pytket uses the big-endian ordering convention we read off the control state from the top to the bottom of the circuit.
@@ -976,16 +970,16 @@ These occur very naturally in Trotterising evolution operators and native device
```{code-cell} ipython3
- from pytket.circuit import PauliExpBox
- from pytket.pauli import Pauli
+from pytket.circuit import PauliExpBox
+from pytket.pauli import Pauli
- # Construct a PauliExpBox with a list of Paulis followed by the phase theta
- xyyz = PauliExpBox([Pauli.X, Pauli.Y, Pauli.Y, Pauli.Z], -0.2)
+# Construct a PauliExpBox with a list of Paulis followed by the phase theta
+xyyz = PauliExpBox([Pauli.X, Pauli.Y, Pauli.Y, Pauli.Z], -0.2)
- pauli_circ = Circuit(4)
+pauli_circ = Circuit(4)
- pauli_circ.add_gate(xyyz, [0, 1, 2, 3])
- draw(pauli_circ)
+pauli_circ.add_gate(xyyz, [0, 1, 2, 3])
+draw(pauli_circ)
```
To understand what happens inside a {py:class}`~pytket.circuit.PauliExpBox` let's take a look at the underlying circuit for $e^{-i \frac{\pi}{2}\theta XYYZ}$
@@ -993,11 +987,11 @@ To understand what happens inside a {py:class}`~pytket.circuit.PauliExpBox` let'
```{code-cell} ipython3
- from pytket.passes import DecomposeBoxes
+from pytket.passes import DecomposeBoxes
- DecomposeBoxes().apply(pauli_circ)
+DecomposeBoxes().apply(pauli_circ)
- draw(pauli_circ)
+draw(pauli_circ)
```
All Pauli exponentials of the form above can be implemented in terms of a single Rz($\theta$) rotation and a symmetric chain of CX gates on either side together with some single qubit basis rotations. This class of circuit is called a Pauli gadget. The subset of these circuits corresponding to "Z only" Pauli strings are referred to as phase gadgets.
@@ -1037,27 +1031,27 @@ Finally a `linear_transfromation` parameter needs to be specified: this is a ma
```{code-cell} ipython3
- from pytket.circuit import PhasePolyBox
+from pytket.circuit import PhasePolyBox
- phase_poly_circ = Circuit(3)
+phase_poly_circ = Circuit(3)
- qubit_indices = {Qubit(0): 0, Qubit(1): 1, Qubit(2): 2}
+qubit_indices = {Qubit(0): 0, Qubit(1): 1, Qubit(2): 2}
- phase_polynomial = {
- (True, False, True): 0.333,
- (False, False, True): 0.05,
- (False, True, False): 1.05,
- }
+phase_polynomial = {
+ (True, False, True): 0.333,
+ (False, False, True): 0.05,
+ (False, True, False): 1.05,
+}
- n_qb = 3
+n_qb = 3
- linear_transformation = np.array([[1, 1, 0], [0, 1, 0], [0, 0, 1]])
+linear_transformation = np.array([[1, 1, 0], [0, 1, 0], [0, 0, 1]])
- p_box = PhasePolyBox(n_qb, qubit_indices, phase_polynomial, linear_transformation)
+p_box = PhasePolyBox(n_qb, qubit_indices, phase_polynomial, linear_transformation)
- phase_poly_circ.add_gate(p_box, [0, 1, 2])
+phase_poly_circ.add_gate(p_box, [0, 1, 2])
- draw(p_box.get_circuit())
+draw(p_box.get_circuit())
```
### Multiplexors, Arbitrary State Preparation and {py:class}`~pytket.circuit.ToffoliBox`
@@ -1082,20 +1076,20 @@ Lets implement a multiplexor with the following logic. Here we treat the first t
```{code-cell} ipython3
- from pytket.circuit import Op, OpType, MultiplexorBox
+from pytket.circuit import Op, OpType, MultiplexorBox
- # Define both gates as an Op
- rz_op = Op.create(OpType.Rz, 0.3)
- h_op = Op.create(OpType.H)
+# Define both gates as an Op
+rz_op = Op.create(OpType.Rz, 0.3)
+h_op = Op.create(OpType.H)
- op_map = {(0, 0): rz_op, (1, 1): h_op}
- multiplexor = MultiplexorBox(op_map)
+op_map = {(0, 0): rz_op, (1, 1): h_op}
+multiplexor = MultiplexorBox(op_map)
- multi_circ = Circuit(3)
- multi_circ.X(0).X(1) # Put both control qubits in the state |1>
- multi_circ.add_gate(multiplexor, [0, 1, 2])
+multi_circ = Circuit(3)
+multi_circ.X(0).X(1) # Put both control qubits in the state |1>
+multi_circ.add_gate(multiplexor, [0, 1, 2])
- draw(multi_circ)
+draw(multi_circ)
```
@@ -1105,9 +1099,9 @@ $|+\rangle = H|0\rangle$ state.
```{code-cell} ipython3
- # Assume all qubits initialised to |0> here
- # Amplitudes of |+> approx 0.707...
- print("Statevector =", np.round(multi_circ.get_statevector().real, 4))
+# Assume all qubits initialised to |0> here
+# Amplitudes of |+> approx 0.707...
+print("Statevector =", np.round(multi_circ.get_statevector().real, 4))
```
In addition to the general {py:class}`~pytket.circuit.MultiplexorBox` pytket has several other type of multiplexor box operations available.
@@ -1136,22 +1130,21 @@ $$
```{code-cell} ipython3
- from pytket.circuit import StatePreparationBox
-
- w_state = 1 / np.sqrt(3) * np.array([0, 1, 1, 0, 1, 0, 0, 0])
+from pytket.circuit import StatePreparationBox
- w_state_box = StatePreparationBox(w_state)
+w_state = 1 / np.sqrt(3) * np.array([0, 1, 1, 0, 1, 0, 0, 0])
- state_circ = Circuit(3)
- state_circ.add_gate(w_state_box, [0, 1, 2])
+w_state_box = StatePreparationBox(w_state)
+state_circ = Circuit(3)
+state_circ.add_gate(w_state_box, [0, 1, 2])
```
```{code-cell} ipython3
- # Verify state preperation
- np.round(state_circ.get_statevector().real, 3) # 1/sqrt(3) approx 0.577
+# Verify state preperation
+np.round(state_circ.get_statevector().real, 3) # 1/sqrt(3) approx 0.577
```
:::{Note}
@@ -1163,8 +1156,8 @@ For some use cases it may be desirable to reset all qubits to the $|0\rangle$ st
```{code-cell} ipython3
- # Ensure all qubits initialised to |0>
- w_state_box_reset = StatePreparationBox(w_state, with_initial_reset=True)
+# Ensure all qubits initialised to |0>
+w_state_box_reset = StatePreparationBox(w_state, with_initial_reset=True)
```
@@ -1187,18 +1180,18 @@ For correctness if a basis state appears as key in the permutation dictionary th
```{code-cell} ipython3
- from pytket.circuit import ToffoliBox
+from pytket.circuit import ToffoliBox
- # Specify the desired permutation of the basis states
- mapping = {
- (0, 0, 1): (1, 1, 1),
- (1, 1, 1): (0, 0, 1),
- (1, 0, 0): (0, 0, 0),
- (0, 0, 0): (1, 0, 0),
- }
+# Specify the desired permutation of the basis states
+mapping = {
+ (0, 0, 1): (1, 1, 1),
+ (1, 1, 1): (0, 0, 1),
+ (1, 0, 0): (0, 0, 0),
+ (0, 0, 0): (1, 0, 0),
+}
- # Define box to perform the permutation
- perm_box = ToffoliBox(permutation=mapping)
+# Define box to perform the permutation
+perm_box = ToffoliBox(permutation=mapping)
```
This permutation of basis states can be achieved with purely classical operations {X, CCX}, hence the name {py:class}`~pytket.circuit.ToffoliBox`.
@@ -1207,7 +1200,7 @@ In pytket however, the permutation is implemented efficently using a sequence of
```{code-cell} ipython3
- draw(perm_box.get_circuit())
+draw(perm_box.get_circuit())
```
@@ -1216,15 +1209,15 @@ Finally let's append the {py:class}`~pytket.circuit.ToffoliBox` onto our circuit
```{code-cell} ipython3
- state_circ.add_gate(perm_box, [0, 1, 2])
+state_circ.add_gate(perm_box, [0, 1, 2])
- draw(state_circ)
+draw(state_circ)
```
```{code-cell} ipython3
- np.round(state_circ.get_statevector().real, 3)
+np.round(state_circ.get_statevector().real, 3)
```
@@ -1233,32 +1226,31 @@ Looking at the statevector calculation we see that our {py:class}`~pytket.circui
## Importing/Exporting Circuits
-.. currentmodule:: pytket.circuit
-```
+
`pytket` {py:class}`~pytket.circuit.Circuit` s can be natively serialized and deserialized from JSON-compatible dictionaries, using the {py:meth}`Circuit.to_dict` and {py:meth}`Circuit.from_dict` methods. This is the method of serialization which supports the largest class of circuits, and provides the highest fidelity.
```{code-cell} ipython3
- import tempfile
- import json
- from pytket import Circuit, OpType
+import tempfile
+import json
+from pytket import Circuit, OpType
- circ = Circuit(2)
- circ.Rx(0.1, 0)
- circ.CX(0, 1)
- circ.YYPhase(0.2, 0, 1)
+circ = Circuit(2)
+circ.Rx(0.1, 0)
+circ.CX(0, 1)
+circ.YYPhase(0.2, 0, 1)
- circ_dict = circ.to_dict()
- print(circ_dict)
+circ_dict = circ.to_dict()
+print(circ_dict)
- with tempfile.TemporaryFile('w+') as fp:
- json.dump(circ_dict, fp)
- fp.seek(0)
- new_circ = Circuit.from_dict(json.load(fp))
+with tempfile.TemporaryFile('w+') as fp:
+ json.dump(circ_dict, fp)
+ fp.seek(0)
+ new_circ = Circuit.from_dict(json.load(fp))
- draw(new_circ)
+draw(new_circ)
```
% Support other frameworks for easy conversion of existing code and enable freedom to choose preferred input system and use available high-level packages
@@ -1273,24 +1265,24 @@ Though less expressive than native dictionary serialization, it is widely suppor
```{code-cell} ipython3
- from pytket.qasm import circuit_from_qasm, circuit_to_qasm_str
- import tempfile, os
+from pytket.qasm import circuit_from_qasm, circuit_to_qasm_str
+import tempfile, os
- fd, path = tempfile.mkstemp(".qasm")
- os.write(fd, """OPENQASM 2.0;
- include "qelib1.inc";
- qreg q[2];
- creg c[2];
- h q[0];
- cx q[0], q[1];
- cz q[1], q[0];
- measure q -> c;
- """.encode())
- os.close(fd)
- circ = circuit_from_qasm(path)
- os.remove(path)
+fd, path = tempfile.mkstemp(".qasm")
+os.write(fd, """OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[2];
+creg c[2];
+h q[0];
+cx q[0], q[1];
+cz q[1], q[0];
+measure q -> c;
+""".encode())
+os.close(fd)
+circ = circuit_from_qasm(path)
+os.remove(path)
- print(circuit_to_qasm_str(circ)) # print QASM string
+print(circuit_to_qasm_str(circ)) # print QASM string
```
% Quipper
@@ -1304,20 +1296,20 @@ The core `pytket` package additionally features a converter from Quipper, anothe
```{code-cell} ipython3
- from pytket.quipper import circuit_from_quipper
- import tempfile, os
+from pytket.quipper import circuit_from_quipper
+import tempfile, os
- fd, path = tempfile.mkstemp(".quip")
- os.write(fd, """Inputs: 0:Qbit, 1:Qbit, 2:Qbit
- QGate["X"](0)
- QGate["Y"](1)
- QGate["Z"](2)
- Outputs: 0:Qbit, 1:Qbit, 2:Qbit
- """.encode())
- os.close(fd)
- circ = circuit_from_quipper(path)
- draw(circ)
- os.remove(path)
+fd, path = tempfile.mkstemp(".quip")
+os.write(fd, """Inputs: 0:Qbit, 1:Qbit, 2:Qbit
+QGate["X"](0)
+QGate["Y"](1)
+QGate["Z"](2)
+Outputs: 0:Qbit, 1:Qbit, 2:Qbit
+""".encode())
+os.close(fd)
+circ = circuit_from_quipper(path)
+draw(circ)
+os.remove(path)
```
:::{note}
@@ -1333,14 +1325,14 @@ For example, installing the `pytket-qiskit` package will add the {py:func}`~pytk
```{code-cell} ipython3
- from qiskit import QuantumCircuit
- from math import pi
+from qiskit import QuantumCircuit
+from math import pi
- qc = QuantumCircuit(3)
- qc.h(0)
- qc.cx(0, 1)
- qc.rz(pi/2, 1)
- print(qc)
+qc = QuantumCircuit(3)
+qc.h(0)
+qc.cx(0, 1)
+qc.rz(pi/2, 1)
+print(qc)
```
We can convert this {py:class}`~qiskit.circuit.QuantumCircuit` to a pytket {py:class}`Circuit`, append some gates and then convert back.
@@ -1348,14 +1340,14 @@ We can convert this {py:class}`~qiskit.circuit.QuantumCircuit` to a pytket {py:c
```{code-cell} ipython3
- from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit
+from pytket.extensions.qiskit import qiskit_to_tk, tk_to_qiskit
- circ = qiskit_to_tk(qc)
- circ.CX(1, 2)
- circ.measure_all()
+circ = qiskit_to_tk(qc)
+circ.CX(1, 2)
+circ.measure_all()
- qc2 = tk_to_qiskit(circ)
- print(qc2)
+qc2 = tk_to_qiskit(circ)
+print(qc2)
```
## Symbolic Circuits
@@ -1375,23 +1367,23 @@ In practice, it is very common for an experiment to use many circuits with simil
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from sympy import Symbol
+from pytket import Circuit, OpType
+from sympy import Symbol
- a = Symbol("alpha")
- b = Symbol("beta")
+a = Symbol("alpha")
+b = Symbol("beta")
- circ = Circuit(2)
- circ.Rx(a, 0)
- circ.Rx(-2*a, 1)
- circ.CX(0, 1)
- circ.YYPhase(b, 0, 1)
+circ = Circuit(2)
+circ.Rx(a, 0)
+circ.Rx(-2*a, 1)
+circ.CX(0, 1)
+circ.YYPhase(b, 0, 1)
- draw(circ)
+draw(circ)
- s_map = {a:0.3, b:1.25}
- circ.symbol_substitution(s_map)
- print(circ.free_symbols())
+s_map = {a:0.3, b:1.25}
+circ.symbol_substitution(s_map)
+print(circ.free_symbols())
```
% Instantiate by mapping symbols to values (in half-turns)
@@ -1401,16 +1393,16 @@ It is important to note that the units of the parameter values will still be in
```{code-cell} ipython3
- from pytket import Circuit
- from sympy import Symbol, pi
+from pytket import Circuit
+from sympy import Symbol, pi
- a = Symbol("alpha") # suppose that alpha is given in radians
- circ = Circuit(2) # convert alpha to half-turns when adding gates
- circ.Rx(a/pi, 0).CX(0, 1).Ry(-a/pi, 0)
+a = Symbol("alpha") # suppose that alpha is given in radians
+circ = Circuit(2) # convert alpha to half-turns when adding gates
+circ.Rx(a/pi, 0).CX(0, 1).Ry(-a/pi, 0)
- s_map = {a: pi/4}
- circ.symbol_substitution(s_map)
- draw(circ)
+s_map = {a: pi/4}
+circ.symbol_substitution(s_map)
+draw(circ)
```
% Can use substitution to replace by arbitrary expressions, including renaming alpha-conversion
@@ -1420,16 +1412,16 @@ Substitution need not be for concrete values, but is defined more generally to a
```{code-cell} ipython3
- from pytket import Circuit
- from sympy import symbols
+from pytket import Circuit
+from sympy import symbols
- a, b, c = symbols("a b c")
- circ = Circuit(2)
- circ.Rx(a, 0).Rx(b, 1).CX(0, 1).Ry(c, 0).Ry(c, 1)
+a, b, c = symbols("a b c")
+circ = Circuit(2)
+circ.Rx(a, 0).Rx(b, 1).CX(0, 1).Ry(c, 0).Ry(c, 1)
- s_map = {a: 2*a, c: a} # replacement happens simultaneously, and not recursively
- circ.symbol_substitution(s_map)
- draw(circ)
+s_map = {a: 2*a, c: a} # replacement happens simultaneously, and not recursively
+circ.symbol_substitution(s_map)
+draw(circ)
```
% Can query circuit for its free symbols
@@ -1441,17 +1433,16 @@ There are currently no simulators or devices that can run symbolic circuits alge
```{code-cell} ipython3
- from pytket import Circuit
- from sympy import symbols
+from pytket import Circuit
+from sympy import symbols
- a, b = symbols("a, b")
- circ = Circuit(2)
- circ.Rx(a, 0).Rx(b, 1).CZ(0, 1)
- circ.symbol_substitution({a:0.2})
-
- print(circ.free_symbols())
- print(circ.is_symbolic()) # returns True when free_symbols() is non-empty
+a, b = symbols("a, b")
+circ = Circuit(2)
+circ.Rx(a, 0).Rx(b, 1).CZ(0, 1)
+circ.symbol_substitution({a:0.2})
+print(circ.free_symbols())
+print(circ.is_symbolic()) # returns True when free_symbols() is non-empty
```
:::{note}
@@ -1469,17 +1460,16 @@ In {py:mod}`pytket.utils.symbolic` we provide functions {py:func}`~pytket.utils.
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.utils.symbolic import circuit_apply_symbolic_statevector, circuit_to_symbolic_unitary
- from sympy import Symbol, pi
+from pytket.utils.symbolic import circuit_apply_symbolic_statevector, circuit_to_symbolic_unitary
+from sympy import Symbol, pi
- a = Symbol("alpha")
- circ = Circuit(2)
- circ.Rx(a/pi, 0).CX(0, 1)
+a = Symbol("alpha")
+circ = Circuit(2)
+circ.Rx(a/pi, 0).CX(0, 1)
- # All zero input state is assumed if no initial state is provided
- display(circuit_apply_symbolic_statevector(circ))
- circuit_to_symbolic_unitary(circ)
+# All zero input state is assumed if no initial state is provided
+display(circuit_apply_symbolic_statevector(circ))
+circuit_to_symbolic_unitary(circ)
```
From a1aeaa5cae435c776a46945c7eef9dec1b090beb Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 13:38:21 +0100
Subject: [PATCH 04/16] fix broken sphinx admonitions
---
docs/manual/manual_backend.md | 28 +++----
docs/manual/manual_circuit.md | 149 +++++++++++++++++----------------
docs/manual/manual_compiler.md | 36 ++++----
docs/manual/manual_zx.md | 20 ++---
4 files changed, 119 insertions(+), 114 deletions(-)
diff --git a/docs/manual/manual_backend.md b/docs/manual/manual_backend.md
index c6a6e985..040c62bf 100644
--- a/docs/manual/manual_backend.md
+++ b/docs/manual/manual_backend.md
@@ -152,9 +152,9 @@ print(counts)
print(probs_from_counts(counts))
```
-:::{note}
+```{note}
{py:class}`~pytket.backends.Backend.process_circuit` returns a handle to the computation to perform the quantum computation asynchronously. Non-blocking operations are essential when running circuits on remote devices, as submission and queuing times can be long. The handle may then be used to retrieve the results with {py:class}`~pytket.backends.Backend.get_result`. If asynchronous computation is not needed, for example when running on a local simulator, pytket provides the shortcut {py:meth}`pytket.backends.Backend.run_circuit` that will immediately execute the circuit and return a {py:class}`~pytket.backends.backendresult.BackendResult`.
-:::
+```
## Statevector and Unitary Simulation with TKET Backends
@@ -179,9 +179,9 @@ state = backend.run_circuit(compiled_circ).get_state()
print(state.round(5))
```
-:::{note}
+```{note}
We have rounded the results here because simulators typically introduce a small amount of floating-point error, so killing near-zero entries gives a much more readable representation.
-:::
+```
% `get_unitary` treats circuit with open inputs and gives map on 2^n-dimensional complex Hilbert space
@@ -410,9 +410,9 @@ print(expectation_from_counts(counts))
% Obtaining indices of specific bits/qubits of interest using `bit_readout` and `qubit_readout` or `qubit_to_bit_map`, and filtering results
-:::{note}
+```{note}
{py:meth}`~pytket.utils.expectation_from_shots()` and {py:meth}`~pytket.utils.expectation_from_counts()` take into account every classical bit in the results object. If the expectation value of interest is a product of only a subset of the measurements in the {py:class}`~pytket.circuit.Circuit` (as is the case when simultaneously measuring several commuting operators), then you will want to filter/marginalise out the ignored bits when performing this calculation.
-:::
+```
## Guidance for Writing Hardware-Agnostic Code
@@ -449,9 +449,9 @@ handle = backend.process_circuit(compiled_qkd, n_shots=1000)
print(backend.get_result(handle).get_counts())
```
-:::{note}
+```{note}
The same effect can be achieved by `assert backend.valid_circuit(qkd)` after compilation. However, when designing the compilation procedure manually, it is unclear whether a failure for this assertion would come from the incompatibility of the {py:class}`~pytket.backends.Backend` for the experiment or from the compilation failing.
-:::
+```
Otherwise, a practical solution around different measurement requirements is to separate the design into "state circuits" and "measurement circuits". At the point of running on the {py:class}`~pytket.backends.Backend`, we can then choose to either just send the state circuit for statevector calculations or compose it with the measurement circuits to run on sampling {py:class}`~pytket.backends.Backend` s.
@@ -577,9 +577,9 @@ print(expectation)
(1.2047999999999999-0.0015000000000000013j)
```
-:::{note}
+```{note}
Currently, only some devices (e.g. those from IBMQ, Quantinuum and Amazon Braket) support a queue model and benefit from this methodology, though more may adopt this in future. The {py:class}`~pytket.extensions.qiskit.AerBackend` simulator and the {py:class}`~pytket.extensions.quantinuum.QuantinuumBackend` can take advantage of batch submission for parallelisation. In other cases, {py:class}`~pytket.backends.Backend.process_circuits` will just loop through each {py:class}`~pytket.circuit.Circuit` in turn.
-:::
+```
## Embedding into Qiskit
@@ -614,9 +614,9 @@ result = grover.amplify(problem)
print("Top measurement:", result.top_measurement)
```
-:::{note}
+```{note}
Since Qiskit may not be able to solve all of the constraints of the chosen device/simulator, some compilation may be required after a circuit is passed to the {py:class}`TketBackend`, or it may just be preferable to do so to take advantage of the sophisticated compilation solutions provided in `pytket`. Upon constructing the {py:class}`TketBackend`, you can provide a `pytket` compilation pass to apply to each circuit, e.g. `TketBackend(backend, backend.default_compilation_pass())`. Some experimentation may be required to find a combination of `qiskit.transpiler.PassManager` and `pytket` compilation passes that executes successfully.
-:::
+```
% Pytket Assistant
@@ -754,9 +754,9 @@ asyncio.run(main())
In some cases you may want to end execution early, perhaps because it is taking too long or you already have all the data you need. You can use the {py:meth}`~pytket.backends.Backend.cancel()` method to cancel the job for a given {py:class}`~pytket.backends.resulthandle.ResultHandle`. This is recommended to help reduce load on the devices if you no longer need to run the submitted jobs.
-:::{note}
+```{note}
Asynchronous submission is currently available with the {py:class}`~pytket.extensions.qiskit.IBMQBackend`, {py:class}`~pytket.extensions.quantinuum.QuantinuumBackend`, {py:class}`BraketBackend` and {py:class}`~pytket.extensions.qiskit.AerBackend`. It will be extended to others in future updates.
-:::
+```
### Persistent Handles
diff --git a/docs/manual/manual_circuit.md b/docs/manual/manual_circuit.md
index af752555..820c7a94 100644
--- a/docs/manual/manual_circuit.md
+++ b/docs/manual/manual_circuit.md
@@ -1,5 +1,7 @@
---
file_format: mystnb
+kernelspec:
+ name: python3
---
# Circuit Construction
@@ -25,12 +27,12 @@ Given the small scale and lack of dynamic quantum memories for both devices and
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- trivial_circ = Circuit() # no qubits or bits
- quantum_circ = Circuit(4) # 4 qubits and no bits
- mixed_circ = Circuit(4, 2) # 4 qubits and 2 bits
- named_circ = Circuit(2, 2, "my_circ")
+trivial_circ = Circuit() # no qubits or bits
+quantum_circ = Circuit(4) # 4 qubits and no bits
+mixed_circ = Circuit(4, 2) # 4 qubits and 2 bits
+named_circ = Circuit(2, 2, "my_circ")
```
## Basic Gates
@@ -292,34 +294,37 @@ To help encourage consistency of identifiers, a {py:class}`~pytket.circuit.Circu
```{code-cell} ipython3
- :raises: RuntimeError
+---
+raises:
+ RuntimeError
+---
from pytket import Circuit, Qubit, Bit
-circ = Circuit()
-# set up a circuit with qubit a[0]
-circ.add_qubit(Qubit("a", 0))
-
-# rejected because "a" is already a qubit register
-circ.add_bit(Bit("a", 1))
+#circ = Circuit()
+## set up a circuit with qubit a[0]
+#circ.add_qubit(Qubit("a", 0))
+#
+## rejected because "a" is already a qubit register
+#circ.add_bit(Bit("a", 1))
```
-```{code-cell} ipython3
- :raises: RuntimeError
-
-# rejected because "a" is already a 1D register
-circ.add_qubit(Qubit("a", [1, 2]))
-circ.add_qubit(Qubit("a"))
-```
-
+# ```{code-cell} ipython3
+# :raises: RuntimeError
+#
+# # rejected because "a" is already a 1D register
+# circ.add_qubit(Qubit("a", [1, 2]))
+# circ.add_qubit(Qubit("a"))
+# ```
-```{code-cell} ipython3
- :raises: RuntimeError
-# rejected because a[0] is already in the circuit
-circ.add_qubit(Qubit("a", 0))
-```
+## ```{code-cell} ipython3
+## :raises: RuntimeError
+##
+## # rejected because a[0] is already in the circuit
+## circ.add_qubit(Qubit("a", 0))
+## ```
% Integer labels correspond to default registers (example of using explicit labels from `Circuit(n)`)
@@ -386,9 +391,9 @@ circ
% If a unit does not exist in the other circuit, treated as composing with identity
-:::{note}
+```{note}
If one {py:class}`Circuit` lacks some unit present in the other, then we treat it as if it is an identity on that unit. In the extreme case where the {py:class}`Circuit` s are defined with disjoint sets of {py:class}`UnitID` s, the {py:meth}`Circuit.append` method will compose them in parallel.
-:::
+```
To compose two circuits in parallel we can take tensor product using the * operator. This requires that the qubits in the circuits have distinct names.
@@ -418,15 +423,15 @@ If we attempt to form the tensor product of two circuits without distinct qubit
```{code-cell} ipython3
:raises: RuntimeError
-from pytket import Circuit
-
-circ_x = Circuit()
-l_reg1 = circ_x.add_q_register("l", 1)
-
-circ_y = Circuit()
-l_reg2 = circ_y.add_q_register("l", 1)
-
-circ_x * circ_y # Error as both circuits have l[0]
+#from pytket import Circuit
+#
+#circ_x = Circuit()
+#l_reg1 = circ_x.add_q_register("l", 1)
+#
+#circ_y = Circuit()
+#l_reg2 = circ_y.add_q_register("l", 1)
+#
+#circ_x * circ_y # Error as both circuits have l[0]
```
@@ -490,9 +495,9 @@ circ.add_circuit(next_circ, [a[1], a[0]])
draw(circ)
```
-:::{note}
+```{note}
This requires the subcircuit to be defined only over the default registers so that the list of arguments given to {py:meth}`Circuit.add_circuit` can easily be mapped.
-:::
+```
## Statevectors and Unitaries
@@ -520,9 +525,9 @@ circ.H(0).CZ(0, 1).H(1)
circ.get_unitary()
```
-:::{warning}
+```{warning}
The unitary matrix of a quantum circuit is of dimension $(2^n \times 2^n)$ where $n$ is the number of qubits. The statevector will be a column vector with $2^n$ entries . Due to this exponential scaling it will in general be very inefficient to compute the unitary (or statevector) of a circuit. These functions are intended to be used for sanity checks and spotting mistakes in small circuits.
-:::
+```
## Analysing Circuits
@@ -560,9 +565,9 @@ circ.CX(0, 1).CZ(1, 2).X(1).Rx(0.3, 0)
draw(circ) # Render interactive circuit diagram
```
-:::{note}
+```{note}
The pytket circuit renderer can represent circuits in the standard circuit model or in the ZX representation. Other interactive features include adjustable zoom, circuit wrapping and image export.
-:::
+```
`pytket` also features methods to visualise the underlying circuit DAG graphically for easier visual inspection.
@@ -579,11 +584,11 @@ Graph(circ).get_DAG() # Displays in interactive python notebooks
The visualisation tool can also describe the interaction graph of a {py:class}`~pytket.circuit.Circuit` consisting of only one- and two-qubit gates -- that is, the graph of which qubits will share a two-qubit gate at some point during execution.
-:::{note}
+```{note}
The visualisations above are shown in ipython notebook cells. When working with a normal python script one can view rendered circuits in the browser with the {py:meth}`~pytket.circuit.display.view_browser` function from the display module.
There are also the methods {py:meth}`~pytket.utils.Graph.save_DAG` and {py:meth}`~pytket.utils.Graph.view_DAG` for saving and visualising the circuit DAG.
-:::
+```
```{code-cell} ipython3
@@ -644,9 +649,9 @@ print("T gate depth =", circ.depth_by_type(OpType.T))
print("2qb gate depth =", circ.depth_by_type({OpType.CX, OpType.CZ}))
```
-:::{note}
+```{note}
Each of these metrics will analyse the {py:class}`~pytket.circuit.Circuit` "as is", so they will consider each Box as a single unit rather than breaking it down into basic gates, nor will they perform any non-trivial gate commutations (those that don't just follow by deformation of the DAG) or gate decompositions (e.g. recognising that a $CZ$ gate would contribute 1 to $CX$-count in practice).
-:::
+```
Its also possible to count all the occurrences of each {py:class}`~pytket.circuit.OpType` using the {py:func}`~pytket.utils.stats.gate_counts` function from the {py:mod}`pytket.utils` module.
@@ -712,9 +717,9 @@ draw(circ)
See how the name of the circuit appears in the rendered circuit diagram. Clicking on the box will show the underlying circuit.
-:::{Note}
+```{Note}
Despite the {py:class}`~pytket.circuit.Circuit` class having methods for adding each type of box, the {py:meth}`Circuit.add_gate` is sufficiently general to append any pytket OpType to a {py:class}`~pytket.circuit.Circuit`.
-:::
+```
When constructing subroutines to implement quantum algorithms it is natural to distinguish different groups of qubits. For instance, in the quantum phase estimation algorithm (QPE) we would want to distinguish between state preparation qubits and ancillary qubits which are measured to yield an approximation of the phase.
The QPE can then be used as a subroutine in other algorithms: for example, integer factoring or estimating the ground state energy of some molecule. For more on the phase estimation algorithm see the [QPE example notebook](https://tket.quantinuum.com/examples/phase_estimation.html).
@@ -864,9 +869,9 @@ circ.add_unitary2qbox(u2box, 1, 0)
draw(circ)
```
-:::{note}
+```{note}
For performance reasons pytket currently only supports unitary synthesis up to three qubits. Three-qubit synthesis can be accomplished with {py:class}`~pytket.circuit.Unitary3qBox` using a similar syntax.
-:::
+```
% `PauliExpBox` for simulations and general interactions
@@ -910,9 +915,9 @@ op = Op.create(OpType.S)
ccs = QControlBox(op, 2)
```
-:::{note}
+```{note}
Whilst adding a control qubit is asymptotically efficient, the gate overhead is significant and can be hard to synthesise optimally, so using these constructions in a NISQ context should be done with caution.
-:::
+```
In addition, we can construct a {py:class}`~pytket.circuit.QControlBox` from any other pure quantum box type in pytket.
For example, we can construct a multicontrolled $\sqrt{Y}$ operation as by first synthesising the base unitary with {py:class}`~pytket.circuit.Unitary1qBox` and then constructing a {py:class}`~pytket.circuit.QControlBox` from the box implementing $\sqrt{Y}$.
@@ -1147,9 +1152,9 @@ state_circ.add_gate(w_state_box, [0, 1, 2])
np.round(state_circ.get_statevector().real, 3) # 1/sqrt(3) approx 0.577
```
-:::{Note}
+```{Note}
Generic state preperation circuits can be very complex with the gatecount and depth increasing rapidly with the size of the state. In the special case where the desired state has only real-valued amplitudes, only multiplexed Ry operations are needed to accomplish the state preparation.
-:::
+```
For some use cases it may be desirable to reset all qubits to the $|0\rangle$ state prior to state preparation. This can be done using the `with_initial_reset` flag.
@@ -1287,9 +1292,9 @@ print(circuit_to_qasm_str(circ)) # print QASM string
% Quipper
-:::{note}
+```{note}
The OpenQASM converters do not support circuits with {ref}`implicit qubit permutations `. This means that if a circuit contains such a permutation it will be ignored when exported to OpenQASM format.
-:::
+```
The core `pytket` package additionally features a converter from Quipper, another circuit description language.
@@ -1312,9 +1317,9 @@ draw(circ)
os.remove(path)
```
-:::{note}
+```{note}
There are a few features of the Quipper language that are not supported by the converter, which are outlined in the {py:mod}`pytket.quipper` documentation.
-:::
+```
% Extension modules; example with qiskit, cirq, pyquil; caution that they may not support all gate sets or features (e.g. conditional gates with qiskit only)
@@ -1445,13 +1450,13 @@ print(circ.free_symbols())
print(circ.is_symbolic()) # returns True when free_symbols() is non-empty
```
-:::{note}
+```{note}
There are some minor drawbacks associated with symbolic compilation. When using [Euler-angle equations](https://tket.quantinuum.com/api-docs/passes.html#pytket.passes.EulerAngleReduction) or quaternions for merging adjacent rotation gates, the resulting angles are given by some lengthy trigonometric expressions which cannot be evaluated down to just a number when one of the original angles was parameterised; this can lead to unhelpfully long expressions for the angles of some gates in the compiled circuit. It is also not possible to apply the {py:class}`pytket.passes.KAKDecomposition` pass to simplify a parameterised circuit, so that pass will only apply to non-parameterised subcircuits, potentially missing some valid opportunities for optimisation.
-:::
+```
-:::{seealso}
+```{seealso}
To see how to use symbolic compilation in a variational experiment, have a look at our [VQE (UCCSD) example](https://tket.quantinuum.com/examples/ucc_vqe.html).
-:::
+```
### Symbolic unitaries and states
@@ -1477,9 +1482,9 @@ The unitaries are calculated using the unitary representation of each [OpType](h
The outputs are sympy [ImmutableMatrix](https://docs.sympy.org/latest/modules/matrices/immutablematrices.html) objects, and use the same symbols as in the circuit, so can be further substituted and manipulated.
The conversion functions use the [sympy Quantum Mechanics module](https://docs.sympy.org/latest/modules/physics/quantum/index.html), see also the {py:func}`~pytket.utils.symbolic.circuit_to_symbolic_gates` and {py:func}`~pytket.utils.symbolic.circuit_apply_symbolic_qubit` functions to see how to work with those objects directly.
-:::{warning}
+```{warning}
Unitaries corresponding to circuits with $n$ qubits have dimensions $2^n \times 2^n$, so are computationally very expensive to calculate. Symbolic calculation is also computationally costly, meaning calculation of symbolic unitaries is only really feasible for very small circuits (of up to a few qubits in size). These utilities are provided as way to test the design of small subcircuits to check they are performing the intended unitary. Note also that as mentioned above, compilation of a symbolic circuit can generate long symbolic expressions; converting these circuits to a symbolic unitary could then result in a matrix object that is very hard to work with or interpret.
-:::
+```
## Advanced Circuit Construction Topics
@@ -1575,9 +1580,9 @@ After the tableau is added to a circuit, it can be readily decomposed to Cliffor
draw(circ)
```
-:::{note}
+```{note}
The current decomposition method for tableaux is not particularly efficient in terms of gate count, so consider using higher optimisation levels when compiling to help reduce the gate cost.
-:::
+```
The data structure used here for tableaux is intended for compilation use. For fast simulation of Clifford circuits, we recommend using the {py:class}`StimBackend` from `pytket-stim`, the {py:class}`SimplexBackend` from `pytket-pysimplex` (optimized for large sparse circuits), or the {py:class}`~pytket.extensions.qiskit.AerBackend` from `pytket-qiskit`. Future versions of `pytket` may include improved decompositions from tableaux, as well as more flexible tableaux to represent stabilizer states, isometries, and diagonalisation circuits.
@@ -1727,11 +1732,11 @@ classical operations, just like quantum operations.
```
-:::{warning}
+```{warning}
Unlike most uses of readouts in `pytket`, register comparisons expect a little-endian value, e.g. in the above example `condition=reg_eq(reg_a, 3)` (representing the little-endian binary string `110000...`) is triggered when `reg_a[0]` and `reg_a[1]` are in state `1` and the remainder of the register is in state `0`.
-:::
+```
-:::{note}
+```{note}
This feature is only usable on a limited selection of devices and simulators which support conditional gates or classical operations.
The {py:class}`~pytket.extensions.qiskit.AerBackend` (from [pytket-qiskit](https://tket.quantinuum.com/extensions/pytket-qiskit/)) can support the OpenQasm model,
@@ -1742,7 +1747,7 @@ Therefore only conditions of the form
The {py:class}`~pytket.extensions.quantinuum.QuantinuumBackend` (from [pytket-quantinuum](https://tket.quantinuum.com/extensions/pytket-quantinuum/))
can support the full range of expressions and comparisons shown above.
-:::
+```
### Circuit-Level Operations
@@ -1773,9 +1778,9 @@ Systematic modifications to a {py:class}`~pytket.circuit.Circuit` object can go
Generating the transpose of a unitary works similarly using {py:meth}`~pytket.circuit.Circuit.transpose`.
-:::{note}
+```{note}
Since it is not possible to construct the inverse of an arbitrary POVM, the {py:meth}`~pytket.circuit.Circuit.dagger` and {py:meth}`~pytket.circuit.Circuit.transpose` methods will fail if there are any measurements, resets, or other operations that they cannot directly invert.
-:::
+```
% Gradients wrt symbolic parameters
diff --git a/docs/manual/manual_compiler.md b/docs/manual/manual_compiler.md
index 3022d964..e646d1a4 100644
--- a/docs/manual/manual_compiler.md
+++ b/docs/manual/manual_compiler.md
@@ -594,9 +594,9 @@ When composing optimisation passes, we may find that applying one type of optimi
print(circ.get_commands())
```
-:::{warning}
+```{warning}
This looping mechanism does not directly compare the {py:class}`~pytket.circuit.Circuit` to its old state from the previous iteration, instead checking if any of the passes within the loop body claimed they performed any rewrite. Some sequences of passes will do and undo some changes to the {py:class}`~pytket.circuit.Circuit`, giving no net effect but nonetheless causing the loop to repeat. This can lead to infinite loops if used in such a way. Some passes where the {py:class}`~pytket.circuit.Circuit` is converted to another form and back again (e.g. {py:class}`~pytket.passes.PauliSimp`) will always report that a change took place. We recommend testing any looping passes thoroughly to check for termination.
-:::
+```
% Repeat with metric - useful when hard to tell when a change is being made or you only care about specific changes
@@ -689,15 +689,15 @@ print(circ.n_gates_of_type(OpType.CX)) # But not in this case
9
```
-:::{Note}
+```{Note}
{py:class}`~pytket.passes.FullPeepholeOptimise` takes an optional `allow_swaps` argument. This is a boolean flag to indicate whether {py:class}`~pytket.passes.FullPeepholeOptimise` should preserve the circuit connectivity or not. If set to `False` the pass will presrve circuit connectivity but the circuit will generally be less optimised than if connectivity was ignored.
{py:class}`~pytket.passes.FullPeepholeOptimise` also takes an optional `target_2qb_gate` argument to specify whether to target the {{py:class}`OpType.TK1`, {py:class}`OpType.CX`} or {{py:class}`OpType.TK1`, {py:class}`OpType.TK2`} gateset.
-:::
+```
-:::{Note}
+```{Note}
Prevous versions of {py:class}`~pytket.passes.FullPeepholeOptimise` did not apply the {py:class}`~pytket.passes.ThreeQubitSquash` pass. There is a {py:class}`~pytket.passes.PeepholeOptimise2Q` pass which applies the old pass sequence with the {py:class}`~pytket.passes.ThreeQubitSquash` pass excluded.
-:::
+```
% `Backend.default_compilation_pass` gives a recommended compiler pass to solve the backend's constraints with little or light optimisation
@@ -833,9 +833,9 @@ print(cu.final_map)
{c[0]: c[0], c[1]: c[1], c[2]: c[2], c[3]: c[3], c[4]: c[4], q[0]: node[3], q[1]: node[0], q[2]: node[2], q[3]: node[1], q[4]: node[4]}
```
-:::{note}
+```{note}
No passes currently rename or swap classical data, but the classical bits are included in these maps for completeness.
-:::
+```
## Advanced Compilation Topics
@@ -884,9 +884,9 @@ For variational algorithms, the prominent benefit of defining a {py:class}`~pytk
% Warning about `NoSymbolsPredicate` and necessity of instantiation before running on backends
-:::{note}
+```{note}
Every {py:class}`~pytket.backends.Backend` requires {py:class}`NoSymbolsPredicate`, so it is necessary to instantiate all symbols before running a {py:class}`~pytket.circuit.Circuit`.
-:::
+```
### User-defined Passes
@@ -941,9 +941,9 @@ After we've defined our `transform` we can construct a {py:class}`~pytket.passes
We see from the output above that our newly defined {py:class}`DecompseZPass` has successfully decomposed the Pauli Z gates to Hadamard, Pauli X, Hadamard chains and left other gates unchanged.
-:::{warning}
+```{warning}
pytket does not require that {py:class}`~pytket.passes.CustomPass` preserves the unitary of the {py:class}`~pytket.circuit.Circuit` . This is for the user to ensure.
-:::
+```
### Partial Compilation
@@ -1055,9 +1055,9 @@ for i, c in enumerate(setup.measurement_circs):
print(setup.results[yy])
```
-:::{note}
+```{note}
Since there could be multiple measurement {py:class}`~pytket.circuit.Circuit` s generating the same observable, we could theoretically use this to extract extra shots (and hence extra precision) for that observable for free; automatically doing this as part of {py:meth}`measurement_reduction()` is planned for a future release of `pytket`.
-:::
+```
### Contextual Optimisations
@@ -1098,9 +1098,9 @@ zero and discarded at the end. The methods {py:meth}`Circuit.qubit_create` and
{py:meth}`Circuit.qubit_discard` can be used to achieve the same on individual
qubits.
-:::{warning}
+```{warning}
Note that we are now restricted in how we can compose our circuit with other circuits. When composing after another circuit, a "created" qubit becomes a Reset operation. Whem composing before another circuit, a "discarded" qubit may not be joined to another qubit unless that qubit has itself been "created" (so that the discarded state gets reset to zero).
-:::
+```
#### Initial simplification
@@ -1139,9 +1139,9 @@ The measurement has disappeared, replaced with a classical operation on its
target bit. To disable this behaviour, pass the `allow_classical=False`
argument to {py:class}`~pytket.passes.SimplifyInitial` when constructing the pass.
-:::{warning}
+```{warning}
Most backends currently do not support set-bit operations, so these could cause errors when using this pass with mid-circuit measurements. In such cases you should set `allow_classical=False`.
-:::
+```
Note that {py:class}`~pytket.passes.SimplifyInitial` does not automatically cancel successive
pairs of X gates introduced by the simplification. It is a good idea to follow
diff --git a/docs/manual/manual_zx.md b/docs/manual/manual_zx.md
index b129fbe1..546d6bab 100644
--- a/docs/manual/manual_zx.md
+++ b/docs/manual/manual_zx.md
@@ -13,9 +13,9 @@ The graph representation used in the ZX module is intended to be sufficiently ge
- Structured generators for varied parameterisations, such as continuous real parameters of ZX spiders and discrete (Boolean) parameters of specialised Clifford generators.
- Mixed quantum-classical diagram support via annotating edges and some generators with {py:class}`QuantumType.Quantum` for doubled diagrams (shorthand notation for a pair of adjoint edges/generators) or {py:class}`QuantumType.Classical` for the singular variants (sometimes referred to as decoherent/bastard generators).
-:::{note}
+```{note}
Providing this flexibility comes at the expense of some efficiency in both memory and speed of operations. For data structures more focussed on the core ZX-calculus and its well-developed simplification strategies, we recommend checking out `pyzx` () and its Rust port `quizx` (). Some functionality for interoperation between `pytket` and `pyzx` circuits is provided in the `pytket-pyzx` extension package. There is no intention to support non-qubit calculi or SZX scalable notation in the near future as the additional complexity required by the data structure would introduce excessive bureaucracy to maintain during every rewrite.
-:::
+```
## Generator Types
@@ -464,9 +464,9 @@ The particular rewrites available are intended to support common optimisation st
% May not work as intended if diagram is not in intended form, especially for classical or mixed diagrams
-:::{warning}
+```{warning}
Because of the focus on strategies using graphlike diagrams, many of the rewrites expect the inputs to be of a particular form. This may cause some issues if you attempt to apply them to diagrams that aren't in the intended form, especially when working with classical or mixed diagrams.
-:::
+```
% Types (decompositions into generating sets, graphlike form, graphlike reduction, MBQC)
@@ -504,9 +504,9 @@ Composite sequences
% Current implementations may not track global scalar; semantics is only preserved up to scalar; warning if attempting to use for scalar diagram evaluation
-:::{warning}
+```{warning}
Current implementations of rewrite passes may not track the global scalar. Semantics of diagrams is only preserved up to scalar. This is fine for simplification of states or unitaries as they can be renormalised but this may cause issues if attempting to use rewrites for scalar diagram evaluation.
-:::
+```
## MBQC Flow Detection
@@ -555,9 +555,9 @@ The {py:class}`Flow` object that is returned abstracts away the partial ordering
print({ vertex_ids[v] : [vertex_ids[c] for c in cs] for (v, cs) in fl.cmap.items() })
```
-:::{note}
+```{note}
In accordance with the Pauli flow criteria, {py:meth}`Flow.c()` and {py:meth}`Flow.odd()` may return qubits that have already been measured, but this may only happen in cases where the required correction would not have affected the past measurement such as a $Z$ on a {py:class}`ZXType.PZ` qubit.
-:::
+```
% Verification and focussing
@@ -565,9 +565,9 @@ In general, multiple valid flows may exist for a given diagram, but a pattern wi
% Warning that does not update on rewriting
-:::{warning}
+```{warning}
A {py:class}`Flow` object is always with respect to a particular {py:class}`ZXDiagram` in a particular state. It cannot be applied to other diagrams and does not automatically update on rewriting the diagram.
-:::
+```
## Conversions & Extraction
From 7a84ac213c7edbc84be3ba072d986e0ec732162d Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 13:40:52 +0100
Subject: [PATCH 05/16] fix remaining indentation issues in Circuit
construction page
---
docs/manual/manual_circuit.md | 394 +++++++++++++++++-----------------
1 file changed, 196 insertions(+), 198 deletions(-)
diff --git a/docs/manual/manual_circuit.md b/docs/manual/manual_circuit.md
index 820c7a94..64bc6b07 100644
--- a/docs/manual/manual_circuit.md
+++ b/docs/manual/manual_circuit.md
@@ -1503,24 +1503,24 @@ The {py:class}`~pytket.circuit.CircBox` construction is good for subroutines whe
```{code-cell} ipython3
- from pytket.circuit import Circuit, CustomGateDef
- from sympy import symbols
+from pytket.circuit import Circuit, CustomGateDef
+from sympy import symbols
- a, b = symbols("a b")
- def_circ = Circuit(2)
- def_circ.CZ(0, 1)
- def_circ.Rx(a, 1)
- def_circ.CZ(0, 1)
- def_circ.Rx(-a, 1)
- def_circ.Rz(b, 0)
+a, b = symbols("a b")
+def_circ = Circuit(2)
+def_circ.CZ(0, 1)
+def_circ.Rx(a, 1)
+def_circ.CZ(0, 1)
+def_circ.Rx(-a, 1)
+def_circ.Rz(b, 0)
- gate_def = CustomGateDef.define("MyCRx", def_circ, [a])
- circ = Circuit(3)
- circ.add_custom_gate(gate_def, [0.2], [0, 1])
- circ.add_custom_gate(gate_def, [0.3], [0, 2])
+gate_def = CustomGateDef.define("MyCRx", def_circ, [a])
+circ = Circuit(3)
+circ.add_custom_gate(gate_def, [0.2], [0, 1])
+circ.add_custom_gate(gate_def, [0.3], [0, 2])
- draw(circ)
- print(circ.free_symbols()) # Print remaining free symbols
+draw(circ)
+print(circ.free_symbols()) # Print remaining free symbols
```
### Clifford Tableaux
@@ -1532,13 +1532,13 @@ Any state $|\psi\rangle$ in the Clifford fragment is uniquely identified by thos
```{code-cell} ipython3
- from pytket.circuit import OpType, Qubit
- from pytket.tableau import UnitaryTableau
+from pytket.circuit import OpType, Qubit
+from pytket.tableau import UnitaryTableau
- tab = UnitaryTableau(3)
- tab.apply_gate_at_end(OpType.S, [Qubit(0)])
- tab.apply_gate_at_end(OpType.CX, [Qubit(1), Qubit(2)])
- print(tab)
+tab = UnitaryTableau(3)
+tab.apply_gate_at_end(OpType.S, [Qubit(0)])
+tab.apply_gate_at_end(OpType.CX, [Qubit(1), Qubit(2)])
+print(tab)
```
The way to interpret this format is that, for example, the top rows state that the unitary transforms $X_0 I_1 I_2$ at its input to $-Y_0 I_1 I_2$ at its output, and it transforms $I_0 X_1 I_2$ to $I_0 X_1 X_2$.
@@ -1548,23 +1548,21 @@ The primary use for tableaux in `pytket` is as a scalable means of specifying a
```{code-cell} ipython3
- from pytket.circuit import Circuit
- from pytket.tableau import UnitaryTableauBox
-
- box = UnitaryTableauBox(
- np.asarray([[1, 1, 0], [0, 1, 0], [0, 0, 1]], dtype=bool),
- np.asarray([[0, 0, 0], [0, 0, 0], [0, 0, 1]], dtype=bool),
- np.asarray([0, 0, 1], dtype=bool),
- np.asarray([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=bool),
- np.asarray([[1, 0, 0], [1, 1, 0], [0, 0, 1]], dtype=bool),
- np.asarray([1, 0, 1], dtype=bool)
- )
-
- circ = Circuit(3)
- circ.add_gate(box, [0, 1, 2])
- draw(circ)
-
+from pytket.circuit import Circuit
+from pytket.tableau import UnitaryTableauBox
+
+box = UnitaryTableauBox(
+ np.asarray([[1, 1, 0], [0, 1, 0], [0, 0, 1]], dtype=bool),
+ np.asarray([[0, 0, 0], [0, 0, 0], [0, 0, 1]], dtype=bool),
+ np.asarray([0, 0, 1], dtype=bool),
+ np.asarray([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=bool),
+ np.asarray([[1, 0, 0], [1, 1, 0], [0, 0, 1]], dtype=bool),
+ np.asarray([1, 0, 1], dtype=bool)
+)
+circ = Circuit(3)
+circ.add_gate(box, [0, 1, 2])
+draw(circ)
```
After the tableau is added to a circuit, it can be readily decomposed to Clifford gates.
@@ -1572,12 +1570,12 @@ After the tableau is added to a circuit, it can be readily decomposed to Cliffor
```{code-cell} ipython3
- from pytket.passes import DecomposeBoxes, RemoveRedundancies
+from pytket.passes import DecomposeBoxes, RemoveRedundancies
- DecomposeBoxes().apply(circ)
- RemoveRedundancies().apply(circ) # Eliminate some redundant gates
+DecomposeBoxes().apply(circ)
+RemoveRedundancies().apply(circ) # Eliminate some redundant gates
- draw(circ)
+draw(circ)
```
```{note}
@@ -1618,59 +1616,59 @@ possible expressions and predicates.
```{code-cell} ipython3
- from pytket.circuit import (
- Circuit,
- BitRegister,
- if_bit,
- if_not_bit,
- reg_eq,
- reg_geq,
- reg_gt,
- reg_leq,
- reg_lt,
- reg_neq,
- )
-
- # create a circuit and add quantum and classical registers
- circ = Circuit()
- qreg = circ.add_q_register("q", 10)
- reg_a = circ.add_c_register("a", 4)
- # another way of adding a register to the Circuit
- reg_b = BitRegister("b", 3)
- circ.add_c_register(reg_b)
- reg_c = circ.add_c_register("c", 3)
-
- # if (reg_a[0] == 1)
- circ.H(qreg[0], condition=reg_a[0])
- circ.X(qreg[0], condition=if_bit(reg_a[0]))
-
- # if (reg_a[2] == 0)
- circ.T(qreg[1], condition=if_not_bit(reg_a[2]))
-
- # compound logical expressions
- circ.Z(qreg[0], condition=(reg_a[2] & reg_a[3]))
- circ.Z(qreg[1], condition=if_not_bit(reg_a[2] & reg_a[3]))
- big_exp = reg_a[0] | reg_a[1] ^ reg_a[2] & reg_a[3]
- # syntactic sugar for big_exp = BitOr(reg_a[0], BitXor(reg_a[1], BitAnd(reg_a[2], reg_a[3])))
- circ.CX(qreg[1], qreg[2], condition=big_exp)
-
- # Register comparisons
-
- # if (reg_a == 3)
- circ.H(qreg[2], condition=reg_eq(reg_a, 3))
- # if (reg_c != 6)
- circ.Y(qreg[4], condition=reg_neq(reg_c, 5))
- # if (reg_b < 6)
- circ.X(qreg[3], condition=reg_lt(reg_b, 6))
- # if (reg_b > 3)
- circ.Z(qreg[5], condition=reg_gt(reg_b, 3))
- # if (reg_c <= 6)
- circ.S(qreg[6], condition=reg_leq(reg_c, 6))
- # if (reg_a >= 3)
- circ.T(qreg[7], condition=reg_geq(reg_a, 3))
- # compound register expressions
- big_reg_exp = (reg_a & reg_b) | reg_c
- circ.CX(qreg[3], qreg[4], condition=reg_eq(big_reg_exp, 3))
+from pytket.circuit import (
+ Circuit,
+ BitRegister,
+ if_bit,
+ if_not_bit,
+ reg_eq,
+ reg_geq,
+ reg_gt,
+ reg_leq,
+ reg_lt,
+ reg_neq,
+)
+
+# create a circuit and add quantum and classical registers
+circ = Circuit()
+qreg = circ.add_q_register("q", 10)
+reg_a = circ.add_c_register("a", 4)
+# another way of adding a register to the Circuit
+reg_b = BitRegister("b", 3)
+circ.add_c_register(reg_b)
+reg_c = circ.add_c_register("c", 3)
+
+# if (reg_a[0] == 1)
+circ.H(qreg[0], condition=reg_a[0])
+circ.X(qreg[0], condition=if_bit(reg_a[0]))
+
+# if (reg_a[2] == 0)
+circ.T(qreg[1], condition=if_not_bit(reg_a[2]))
+
+# compound logical expressions
+circ.Z(qreg[0], condition=(reg_a[2] & reg_a[3]))
+circ.Z(qreg[1], condition=if_not_bit(reg_a[2] & reg_a[3]))
+big_exp = reg_a[0] | reg_a[1] ^ reg_a[2] & reg_a[3]
+# syntactic sugar for big_exp = BitOr(reg_a[0], BitXor(reg_a[1], BitAnd(reg_a[2], reg_a[3])))
+circ.CX(qreg[1], qreg[2], condition=big_exp)
+
+# Register comparisons
+
+# if (reg_a == 3)
+circ.H(qreg[2], condition=reg_eq(reg_a, 3))
+# if (reg_c != 6)
+circ.Y(qreg[4], condition=reg_neq(reg_c, 5))
+# if (reg_b < 6)
+circ.X(qreg[3], condition=reg_lt(reg_b, 6))
+# if (reg_b > 3)
+circ.Z(qreg[5], condition=reg_gt(reg_b, 3))
+# if (reg_c <= 6)
+circ.S(qreg[6], condition=reg_leq(reg_c, 6))
+# if (reg_a >= 3)
+circ.T(qreg[7], condition=reg_geq(reg_a, 3))
+# compound register expressions
+big_reg_exp = (reg_a & reg_b) | reg_c
+circ.CX(qreg[3], qreg[4], condition=reg_eq(big_reg_exp, 3))
```
So far we've looked at conditioning the application of a gate on bits,
@@ -1686,49 +1684,49 @@ classical operations, just like quantum operations.
```{code-cell} ipython3
- from pytket.circuit import Circuit, reg_gt
+from pytket.circuit import Circuit, reg_gt
- # create a circuit and add some classical registers
- circ = Circuit()
- reg_a = circ.add_c_register("a", 4)
- reg_b = circ.add_c_register("b", 3)
- reg_c = circ.add_c_register("c", 3)
+# create a circuit and add some classical registers
+circ = Circuit()
+reg_a = circ.add_c_register("a", 4)
+reg_b = circ.add_c_register("b", 3)
+reg_c = circ.add_c_register("c", 3)
- # Write to classical registers
+# Write to classical registers
- # a = 3
- circ.add_c_setreg(3, reg_a)
- # a[0] = 1
- circ.add_c_setbits([1], [reg_a[0]])
- # Copy: b = a
- # b is smaller than a so the first 3 bits of a will be copied
- circ.add_c_copyreg(reg_a, reg_b)
- # b[1] = a[2]
- circ.add_c_copybits([reg_a[2]], [reg_b[1]])
+# a = 3
+circ.add_c_setreg(3, reg_a)
+# a[0] = 1
+circ.add_c_setbits([1], [reg_a[0]])
+# Copy: b = a
+# b is smaller than a so the first 3 bits of a will be copied
+circ.add_c_copyreg(reg_a, reg_b)
+# b[1] = a[2]
+circ.add_c_copybits([reg_a[2]], [reg_b[1]])
- # Conditional classical operation
+# Conditional classical operation
- # if (a > 1) b = 3
- circ.add_c_setreg(3, reg_b, condition=reg_gt(reg_a, 1))
+# if (a > 1) b = 3
+circ.add_c_setreg(3, reg_b, condition=reg_gt(reg_a, 1))
- # Write out the results of logical expressions
+# Write out the results of logical expressions
- # c = a ^ b
- circ.add_classicalexpbox_register(reg_a ^ reg_b, reg_c)
- # c[0] = a[1] & b[2]
- circ.add_classicalexpbox_bit(reg_a[1] & reg_b[2], [reg_c[0]])
+# c = a ^ b
+circ.add_classicalexpbox_register(reg_a ^ reg_b, reg_c)
+# c[0] = a[1] & b[2]
+circ.add_classicalexpbox_bit(reg_a[1] & reg_b[2], [reg_c[0]])
- # Register arithmetic
+# Register arithmetic
- # c = a + b // c (note the use of the floor divide symbol)
- circ.add_classicalexpbox_register(reg_a + reg_b // reg_c, reg_c)
- # a = a - b * c
- circ.add_classicalexpbox_register(reg_a - reg_b * reg_c, reg_a)
- # a = a << 2
- circ.add_classicalexpbox_register(reg_a << 2, reg_a)
- # c = b >> 1
- circ.add_classicalexpbox_register(reg_b >> 1, reg_c)
+# c = a + b // c (note the use of the floor divide symbol)
+circ.add_classicalexpbox_register(reg_a + reg_b // reg_c, reg_c)
+# a = a - b * c
+circ.add_classicalexpbox_register(reg_a - reg_b * reg_c, reg_a)
+# a = a << 2
+circ.add_classicalexpbox_register(reg_a << 2, reg_a)
+# c = b >> 1
+circ.add_classicalexpbox_register(reg_b >> 1, reg_c)
```
@@ -1760,20 +1758,20 @@ Systematic modifications to a {py:class}`~pytket.circuit.Circuit` object can go
```{code-cell} ipython3
- from pytket import Circuit
+from pytket import Circuit
- # we want a circuit for E = exp(-i pi (0.3 XX + 0.1 YY))
- circ = Circuit(2)
+# we want a circuit for E = exp(-i pi (0.3 XX + 0.1 YY))
+circ = Circuit(2)
- # find C such that C; Rx(a, 0); C^dagger performs exp(-i a pi XX/2)
- # and C; Rz(b, 1); C^dagger performs exp(-i b pi YY/2)
- conj = Circuit(2)
- conj.V(0).V(1).CX(0, 1)
- conj_dag = conj.dagger()
+# find C such that C; Rx(a, 0); C^dagger performs exp(-i a pi XX/2)
+# and C; Rz(b, 1); C^dagger performs exp(-i b pi YY/2)
+conj = Circuit(2)
+conj.V(0).V(1).CX(0, 1)
+conj_dag = conj.dagger()
- circ.append(conj)
- circ.Rx(0.6, 0).Rz(0.2, 1)
- circ.append(conj_dag)
+circ.append(conj)
+circ.Rx(0.6, 0).Rz(0.2, 1)
+circ.append(conj_dag)
```
Generating the transpose of a unitary works similarly using {py:meth}`~pytket.circuit.Circuit.transpose`.
@@ -1795,28 +1793,28 @@ The {py:class}`~pytket.circuit.Circuit` class is built as a DAG to help follow t
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.utils import Graph
+from pytket import Circuit
+from pytket.utils import Graph
- circ = Circuit(4)
- circ.CX(0, 1)
- circ.CX(1, 0)
- circ.Rx(0.2, 1)
- circ.CZ(0, 1)
+circ = Circuit(4)
+circ.CX(0, 1)
+circ.CX(1, 0)
+circ.Rx(0.2, 1)
+circ.CZ(0, 1)
- print(circ.get_commands())
- Graph(circ).get_DAG()
+print(circ.get_commands())
+Graph(circ).get_DAG()
```
```{code-cell} ipython3
- from pytket.passes import CliffordSimp
+from pytket.passes import CliffordSimp
- CliffordSimp().apply(circ)
- print(circ.get_commands())
- print(circ.implicit_qubit_permutation())
- Graph(circ).get_DAG()
+CliffordSimp().apply(circ)
+print(circ.get_commands())
+print(circ.implicit_qubit_permutation())
+Graph(circ).get_DAG()
```
% This encapsulates naturality of the symmetry in the resource theory, effectively shifting the swap to the end of the circuit
@@ -1844,39 +1842,39 @@ To add gates or boxes to a circuit with specified op group names, simply pass th
```{code-cell} ipython3
- from pytket.circuit import Circuit, CircBox
+from pytket.circuit import Circuit, CircBox
- circ = Circuit(3)
- circ.Rz(0.25, 0, opgroup="rotations")
- circ.CX(0, 1)
- circ.Ry(0.75, 1, opgroup="rotations")
- circ.H(2, opgroup="special one")
- circ.CX(2, 1)
- cbox = CircBox(Circuit(2, name="P").S(0).CY(0, 1))
- circ.add_gate(cbox, [0, 1], opgroup="Fred")
- circ.CX(1, 2, opgroup="Fred")
+circ = Circuit(3)
+circ.Rz(0.25, 0, opgroup="rotations")
+circ.CX(0, 1)
+circ.Ry(0.75, 1, opgroup="rotations")
+circ.H(2, opgroup="special one")
+circ.CX(2, 1)
+cbox = CircBox(Circuit(2, name="P").S(0).CY(0, 1))
+circ.add_gate(cbox, [0, 1], opgroup="Fred")
+circ.CX(1, 2, opgroup="Fred")
- draw(circ)
+draw(circ)
```
```{code-cell} ipython3
- from pytket.circuit import Op
+from pytket.circuit import Op
- # Substitute a new 1-qubit circuit for all ops in the "rotations" group:
- newcirc = Circuit(1).Rx(0.125, 0).Ry(0.875, 0)
- circ.substitute_named(newcirc, "rotations")
+# Substitute a new 1-qubit circuit for all ops in the "rotations" group:
+newcirc = Circuit(1).Rx(0.125, 0).Ry(0.875, 0)
+circ.substitute_named(newcirc, "rotations")
- # Replace the "special one" with a different op:
- newop = Op.create(OpType.T)
- circ.substitute_named(newop, "special one")
+# Replace the "special one" with a different op:
+newop = Op.create(OpType.T)
+circ.substitute_named(newop, "special one")
- # Substitute a box for the "Fred" group:
- newcbox = CircBox(Circuit(2, name="Q").H(1).CX(1, 0))
- circ.substitute_named(newcbox, "Fred")
+# Substitute a box for the "Fred" group:
+newcbox = CircBox(Circuit(2, name="Q").H(1).CX(1, 0))
+circ.substitute_named(newcbox, "Fred")
- draw(circ)
+draw(circ)
```
Note that when an operation or box is substituted in, the op group name is retained (and further substitutions can be made). When a circuit is substituted in, the op group name disappears.
@@ -1888,33 +1886,33 @@ To add a control to an operation, one can add the original operation as a {py:cl
```{code-cell} ipython3
- from pytket.circuit import QControlBox
-
- def with_empty_qubit(op: Op) -> CircBox:
- n_qb = op.n_qubits
- return CircBox(Circuit(n_qb + 1).add_gate(op, list(range(1, n_qb + 1))))
+from pytket.circuit import QControlBox
- def with_control_qubit(op: Op) -> QControlBox:
- return QControlBox(op, 1)
+def with_empty_qubit(op: Op) -> CircBox:
+ n_qb = op.n_qubits
+ return CircBox(Circuit(n_qb + 1).add_gate(op, list(range(1, n_qb + 1))))
- c = Circuit(3)
- h_op = Op.create(OpType.H)
- cx_op = Op.create(OpType.CX)
- h_0_cbox = with_empty_qubit(h_op)
- h_q_qbox = with_control_qubit(h_op)
- cx_0_cbox = with_empty_qubit(cx_op)
- cx_q_qbox = with_control_qubit(cx_op)
- c.X(0).Y(1)
- c.add_gate(h_0_cbox, [2, 0], opgroup="hgroup")
- c.add_gate(cx_0_cbox, [2, 0, 1], opgroup="cxgroup")
- c.Y(0).X(1)
- c.add_gate(h_0_cbox, [2, 1], opgroup="hgroup")
- c.add_gate(cx_0_cbox, [2, 1, 0], opgroup="cxgroup")
- c.X(0).Y(1)
- c.substitute_named(h_q_qbox, "hgroup")
- c.substitute_named(cx_q_qbox, "cxgroup")
+def with_control_qubit(op: Op) -> QControlBox:
+ return QControlBox(op, 1)
- draw(c)
+c = Circuit(3)
+h_op = Op.create(OpType.H)
+cx_op = Op.create(OpType.CX)
+h_0_cbox = with_empty_qubit(h_op)
+h_q_qbox = with_control_qubit(h_op)
+cx_0_cbox = with_empty_qubit(cx_op)
+cx_q_qbox = with_control_qubit(cx_op)
+c.X(0).Y(1)
+c.add_gate(h_0_cbox, [2, 0], opgroup="hgroup")
+c.add_gate(cx_0_cbox, [2, 0, 1], opgroup="cxgroup")
+c.Y(0).X(1)
+c.add_gate(h_0_cbox, [2, 1], opgroup="hgroup")
+c.add_gate(cx_0_cbox, [2, 1, 0], opgroup="cxgroup")
+c.X(0).Y(1)
+c.substitute_named(h_q_qbox, "hgroup")
+c.substitute_named(cx_q_qbox, "cxgroup")
+
+draw(c)
```
[^cite_cowt2020]: Cowtan, A. and Dilkes, S. and Duncan and R., Simmons, W and Sivarajah, S., 2020. Phase Gadget Synthesis for Shallow Circuits. Electronic Proceedings in Theoretical Computer Science
From cce3b32f50357b99ca6ec1d8ffc7b864b4aaad4e Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 13:49:49 +0100
Subject: [PATCH 06/16] port index page to Myst Markdown
---
docs/index.md | 160 +++++++++++++++++++++++++++++++++++++++++++++++++
docs/index.rst | 146 --------------------------------------------
2 files changed, 160 insertions(+), 146 deletions(-)
create mode 100644 docs/index.md
delete mode 100644 docs/index.rst
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..8afeebb3
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,160 @@
+---
+file_format: mystnb
+kernelspec:
+ name: python3
+---
+
+# Getting Started
+
+## Building a circuit with the `Circuit` class
+
+You can create a circuit by creating an instance of the `Circuit`
+class and adding gates manually.
+
+
+```{code-cell} ipython3
+
+from pytket import Circuit
+
+ghz_circ = Circuit(3)
+ghz_circ.H(0)
+ghz_circ.CX(0, 1)
+ghz_circ.CX(1, 2)
+ghz_circ.add_barrier(ghz_circ.qubits)
+ghz_circ.measure_all()
+```
+
+Now let’s draw a nice picture of the circuit with the circuit renderer
+
+
+```{code-cell} ipython3
+
+from pytket.circuit.display import render_circuit_jupyter
+
+render_circuit_jupyter(ghz_circ)
+```
+
+See also the [Circuit
+construction](https://tket.quantinuum.com/user-manual/manual_circuit.html)
+section of the user manual.
+
+## Build a `Circuit` from a QASM file
+
+Alternatively we can import a circuit from a QASM file using
+[pytket.qasm](https://tket.quantinuum.com/api-docs/qasm.html). There
+are also functions for generating a circuit from a QASM string or
+exporting to a qasm file.
+
+Note that its also possible to import a circuit from quipper using
+[pytket.quipper](https://tket.quantinuum.com/api-docs/quipper.html)
+module.
+
+
+```{code-cell} ipython3
+
+from pytket.qasm import circuit_from_qasm
+
+w_state_circ = circuit_from_qasm("examples/qasm/W-state.qasm")
+render_circuit_jupyter(w_state_circ)
+```
+
+## Import a circuit from qiskit (or other SDK)
+
+Its possible to generate a circuit directly from a qiskit
+`QuantumCircuit` using the
+[qiskit_to_tk](https://tket.quantinuum.com/extensions/pytket-qiskit/api.html#pytket.extensions.qiskit.tk_to_qiskit)
+function.
+
+
+```{code-cell} ipython3
+
+from qiskit import QuantumCircuit
+
+qiskit_circ = QuantumCircuit(3)
+qiskit_circ.h(range(3))
+qiskit_circ.ccx(2, 1 ,0)
+qiskit_circ.cx(0, 1)
+print(qiskit_circ)
+```
+
+
+```{code-cell} ipython3
+
+from pytket.extensions.qiskit import qiskit_to_tk
+
+tket_circ = qiskit_to_tk(qiskit_circ)
+
+render_circuit_jupyter(tket_circ)
+```
+
+Note that pytket and qiskit use opposite qubit ordering conventions. So
+circuits which look identical may correspond to different unitary
+operations.
+
+Circuit conversion functions are also available for
+[pytket-cirq](https://tket.quantinuum.com/extensions/pytket-cirq/),
+[pytket-pennylane](https://tket.quantinuum.com/extensions/pytket-pennylane/),
+[pytket-braket](https://tket.quantinuum.com/extensions/pytket-braket/)
+and more.
+
+## Using Backends
+
+In pytket a `Backend` represents an interface to a quantum device or
+simulator.
+
+We will show a simple example of running the `ghz_circ` defined above
+on the `AerBackend` simulator.
+
+
+```{code-cell} ipython3
+
+render_circuit_jupyter(ghz_circ)
+```
+
+
+```{code-cell} ipython3
+
+from pytket.extensions.qiskit import AerBackend
+
+backend = AerBackend()
+result = backend.run_circuit(ghz_circ)
+print(result.get_counts())
+```
+
+The `AerBackend` simulator is highly idealised having a broad gateset,
+and no restrictive connectivity or device noise.
+
+The Hadamard and CX gate are supported operations of the simulator so we
+can run the GHZ circuit without changing any of the operations. For more
+realistic cases a compiler will have to solve for the limited gateset of
+the target backend as well as other backend requirements.
+
+See the [Running on
+Backends](https://tket.quantinuum.com/user-manual/manual_backend.html)
+section of the user manual and the [backends example
+notebook](https://tket.quantinuum.com/examples/backends_example.html)
+for more.
+
+```{toctree}
+:caption: Manual
+:maxdepth: 2
+
+manual/manual_intro.rst
+manual/manual_circuit.rst
+manual/manual_backend.rst
+manual/manual_compiler.rst
+manual/manual_noise.rst
+manual/manual_assertion.rst
+manual/manual_zx.rst
+```
+
+```{toctree}
+:caption: Example Notebooks
+:glob: true
+:maxdepth: 2
+
+examples/circuit_construction/*
+examples/backends/*
+examples/circuit_compilation/*
+examples/algorithms_and_protocols/*
+```
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index a8597742..00000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,146 +0,0 @@
-Getting Started
-===============
-
-Building a circuit with the ``Circuit`` class
----------------------------------------------
-
-You can create a circuit by creating an instance of the ``Circuit``
-class and adding gates manually.
-
-.. jupyter-execute::
-
- from pytket import Circuit
-
- ghz_circ = Circuit(3)
- ghz_circ.H(0)
- ghz_circ.CX(0, 1)
- ghz_circ.CX(1, 2)
- ghz_circ.add_barrier(ghz_circ.qubits)
- ghz_circ.measure_all()
-
-Now let’s draw a nice picture of the circuit with the circuit renderer
-
-.. jupyter-execute::
-
- from pytket.circuit.display import render_circuit_jupyter
-
- render_circuit_jupyter(ghz_circ)
-
-See also the `Circuit
-construction `__
-section of the user manual.
-
-Build a ``Circuit`` from a QASM file
-------------------------------------
-
-Alternatively we can import a circuit from a QASM file using
-`pytket.qasm `__. There
-are also functions for generating a circuit from a QASM string or
-exporting to a qasm file.
-
-Note that its also possible to import a circuit from quipper using
-`pytket.quipper `__
-module.
-
-.. jupyter-execute::
-
- from pytket.qasm import circuit_from_qasm
-
- w_state_circ = circuit_from_qasm("examples/qasm/W-state.qasm")
- render_circuit_jupyter(w_state_circ)
-
-Import a circuit from qiskit (or other SDK)
--------------------------------------------
-
-Its possible to generate a circuit directly from a qiskit
-``QuantumCircuit`` using the
-`qiskit_to_tk `__
-function.
-
-.. jupyter-execute::
-
- from qiskit import QuantumCircuit
-
- qiskit_circ = QuantumCircuit(3)
- qiskit_circ.h(range(3))
- qiskit_circ.ccx(2, 1 ,0)
- qiskit_circ.cx(0, 1)
- print(qiskit_circ)
-
-.. jupyter-execute::
-
- from pytket.extensions.qiskit import qiskit_to_tk
-
- tket_circ = qiskit_to_tk(qiskit_circ)
-
- render_circuit_jupyter(tket_circ)
-
-Note that pytket and qiskit use opposite qubit ordering conventions. So
-circuits which look identical may correspond to different unitary
-operations.
-
-Circuit conversion functions are also available for
-`pytket-cirq `_,
-`pytket-pennylane `_,
-`pytket-braket `_
-and more.
-
-Using Backends
---------------
-
-In pytket a ``Backend`` represents an interface to a quantum device or
-simulator.
-
-We will show a simple example of running the ``ghz_circ`` defined above
-on the ``AerBackend`` simulator.
-
-.. jupyter-execute::
-
- render_circuit_jupyter(ghz_circ)
-
-.. jupyter-execute::
-
- from pytket.extensions.qiskit import AerBackend
-
- backend = AerBackend()
- result = backend.run_circuit(ghz_circ)
- print(result.get_counts())
-
-The ``AerBackend`` simulator is highly idealised having a broad gateset,
-and no restrictive connectivity or device noise.
-
-The Hadamard and CX gate are supported operations of the simulator so we
-can run the GHZ circuit without changing any of the operations. For more
-realistic cases a compiler will have to solve for the limited gateset of
-the target backend as well as other backend requirements.
-
-See the `Running on
-Backends `__
-section of the user manual and the `backends example
-notebook `__
-for more.
-
-
-
-
-.. toctree::
- :caption: Manual
- :maxdepth: 2
-
- manual/manual_intro.rst
- manual/manual_circuit.rst
- manual/manual_backend.rst
- manual/manual_compiler.rst
- manual/manual_noise.rst
- manual/manual_assertion.rst
- manual/manual_zx.rst
-
-.. toctree::
- :glob:
- :caption: Example Notebooks
- :maxdepth: 2
-
- examples/circuit_construction/*
- examples/backends/*
- examples/circuit_compilation/*
- examples/algorithms_and_protocols/*
From 64b15fc724f8bd8f7d5ab2563452ca0b0c92e540 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 13:59:19 +0100
Subject: [PATCH 07/16] fix cells with expected errors
---
docs/manual/manual_circuit.md | 67 +++++++++++++++++++----------------
1 file changed, 36 insertions(+), 31 deletions(-)
diff --git a/docs/manual/manual_circuit.md b/docs/manual/manual_circuit.md
index 64bc6b07..7de75917 100644
--- a/docs/manual/manual_circuit.md
+++ b/docs/manual/manual_circuit.md
@@ -295,36 +295,39 @@ To help encourage consistency of identifiers, a {py:class}`~pytket.circuit.Circu
```{code-cell} ipython3
---
-raises:
- RuntimeError
+tags: [raises-exception]
---
from pytket import Circuit, Qubit, Bit
-#circ = Circuit()
-## set up a circuit with qubit a[0]
-#circ.add_qubit(Qubit("a", 0))
-#
-## rejected because "a" is already a qubit register
-#circ.add_bit(Bit("a", 1))
+circ = Circuit()
+# set up a circuit with qubit a[0]
+circ.add_qubit(Qubit("a", 0))
+
+# rejected because "a" is already a qubit register
+circ.add_bit(Bit("a", 1))
```
-# ```{code-cell} ipython3
-# :raises: RuntimeError
-#
-# # rejected because "a" is already a 1D register
-# circ.add_qubit(Qubit("a", [1, 2]))
-# circ.add_qubit(Qubit("a"))
-# ```
+```{code-cell} ipython3
+---
+tags: [raises-exception]
+---
+
+# rejected because "a" is already a 1D register
+circ.add_qubit(Qubit("a", [1, 2]))
+circ.add_qubit(Qubit("a"))
+```
+
+```{code-cell} ipython3
+---
+tags: [raises-exception]
+---
-## ```{code-cell} ipython3
-## :raises: RuntimeError
-##
-## # rejected because a[0] is already in the circuit
-## circ.add_qubit(Qubit("a", 0))
-## ```
+# rejected because a[0] is already in the circuit
+circ.add_qubit(Qubit("a", 0))
+```
% Integer labels correspond to default registers (example of using explicit labels from `Circuit(n)`)
@@ -421,17 +424,19 @@ If we attempt to form the tensor product of two circuits without distinct qubit
```{code-cell} ipython3
- :raises: RuntimeError
+---
+tags: [raises-exception]
+---
+
+from pytket import Circuit
+
+circ_x = Circuit()
+l_reg1 = circ_x.add_q_register("l", 1)
+
+circ_y = Circuit()
+l_reg2 = circ_y.add_q_register("l", 1)
-#from pytket import Circuit
-#
-#circ_x = Circuit()
-#l_reg1 = circ_x.add_q_register("l", 1)
-#
-#circ_y = Circuit()
-#l_reg2 = circ_y.add_q_register("l", 1)
-#
-#circ_x * circ_y # Error as both circuits have l[0]
+circ_x * circ_y # Error as both circuits have l[0]
```
From 7282befd3ef62a9b69e5544e347e0f595b28da1b Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 14:26:17 +0100
Subject: [PATCH 08/16] clean up compilation chapter
---
docs/manual/manual_compiler.md | 489 ++++++++++++++++-----------------
docs/manual/manual_noise.md | 2 +-
2 files changed, 245 insertions(+), 246 deletions(-)
diff --git a/docs/manual/manual_compiler.md b/docs/manual/manual_compiler.md
index e646d1a4..18527c70 100644
--- a/docs/manual/manual_compiler.md
+++ b/docs/manual/manual_compiler.md
@@ -68,19 +68,19 @@ Below is a minimal example where we construct a predicate which checks if our {p
```{code-cell} ipython3
- from pytket.circuit import Circuit, OpType
- from pytket.predicates import UserDefinedPredicate
+from pytket.circuit import Circuit, OpType
+from pytket.predicates import UserDefinedPredicate
- def max_cx_count(circ: Circuit) -> bool:
- return circ.n_gates_of_type(OpType.CX) < 3
+def max_cx_count(circ: Circuit) -> bool:
+ return circ.n_gates_of_type(OpType.CX) < 3
- # Now construct our predicate using the function defined above
- my_predicate = UserDefinedPredicate(max_cx_count)
+# Now construct our predicate using the function defined above
+my_predicate = UserDefinedPredicate(max_cx_count)
- test_circ = Circuit(2).CX(0, 1).Rz(0.25, 1).CX(0, 1) # Define a test Circuit
+test_circ = Circuit(2).CX(0, 1).Rz(0.25, 1).CX(0, 1) # Define a test Circuit
- my_predicate.verify(test_circ)
- # test_circ satisfies predicate as it contains only 2 CX gates
+my_predicate.verify(test_circ)
+# test_circ satisfies predicate as it contains only 2 CX gates
```
## Rebases
@@ -92,15 +92,15 @@ One of the simplest constraints to solve for is the {py:class}`~pytket.predicate
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.passes import RebaseTket
+from pytket import Circuit
+from pytket.passes import RebaseTket
- circ = Circuit(2, 2)
- circ.Rx(0.3, 0).Ry(-0.9, 1).CZ(0, 1).S(0).CX(1, 0).measure_all()
+circ = Circuit(2, 2)
+circ.Rx(0.3, 0).Ry(-0.9, 1).CZ(0, 1).S(0).CX(1, 0).measure_all()
- RebaseTket().apply(circ)
+RebaseTket().apply(circ)
- print(circ.get_commands())
+print(circ.get_commands())
```
{py:class}`~pytket.passes.RebaseTket` is a standard rebase pass that converts to CX and TK1 gates. This is the preferred internal gateset for many `pytket` compiler passes. However, it is possible to define a rebase for an arbitrary gateset. Using {py:class}`RebaseCustom`, we can provide an arbitrary set of one- and two-qubit gates. Rather than requiring custom decompositions to be provided for every gate type, it is sufficient to just give them for `OpType.CX` and `OpType.TK1`. For any gate in a given {py:class}`~pytket.circuit.Circuit`, it is either already in the target gateset, or we can use known decompositions to obtain a `OpType.CX` and `OpType.TK1` representation and then map this to the target gateset.
@@ -108,27 +108,27 @@ One of the simplest constraints to solve for is the {py:class}`~pytket.predicate
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.passes import RebaseCustom
+from pytket import Circuit, OpType
+from pytket.passes import RebaseCustom
- gates = {OpType.Rz, OpType.Ry, OpType.CY, OpType.ZZPhase}
- cx_in_cy = Circuit(2)
- cx_in_cy.Rz(0.5, 1).CY(0, 1).Rz(-0.5, 1)
+gates = {OpType.Rz, OpType.Ry, OpType.CY, OpType.ZZPhase}
+cx_in_cy = Circuit(2)
+cx_in_cy.Rz(0.5, 1).CY(0, 1).Rz(-0.5, 1)
- def tk1_to_rzry(a, b, c):
- circ = Circuit(1)
- circ.Rz(c + 0.5, 0).Ry(b, 0).Rz(a - 0.5, 0)
- return circ
+def tk1_to_rzry(a, b, c):
+ circ = Circuit(1)
+ circ.Rz(c + 0.5, 0).Ry(b, 0).Rz(a - 0.5, 0)
+ return circ
- custom = RebaseCustom(gates, cx_in_cy, tk1_to_rzry)
+custom = RebaseCustom(gates, cx_in_cy, tk1_to_rzry)
- circ = Circuit(3)
- circ.X(0).CX(0, 1).Ry(0.2, 1)
- circ.ZZPhase(-0.83, 2, 1).Rx(0.6, 2)
+circ = Circuit(3)
+circ.X(0).CX(0, 1).Ry(0.2, 1)
+circ.ZZPhase(-0.83, 2, 1).Rx(0.6, 2)
- custom.apply(circ)
+custom.apply(circ)
- print(circ.get_commands())
+print(circ.get_commands())
```
For some gatesets, it is not even necessary to specify the CX and TK1 decompositions: there is a useful function {py:meth}`auto_rebase_pass` which can take care of this for you. The pass returned is constructed from the gateset alone. (It relies on some known decompositions, and will raise an exception if no suitable known decompositions exist.) An example is given in the "Combinators" section below.
@@ -138,21 +138,20 @@ A similar pair of methods, {py:meth}`SquashCustom` and {py:meth}`auto_squash_pas
```{code-cell} ipython3
- from pytket.circuit import Circuit, OpType
- from pytket.passes import auto_squash_pass
+from pytket.circuit import Circuit, OpType
+from pytket.passes import auto_squash_pass
- gates = {OpType.PhasedX, OpType.Rz, OpType.Rx, OpType.Ry}
- custom = auto_squash_pass(gates)
+gates = {OpType.PhasedX, OpType.Rz, OpType.Rx, OpType.Ry}
+custom = auto_squash_pass(gates)
- circ = Circuit(1).H(0).Ry(0.5, 0).Rx(-0.5, 0).Rz(1.5, 0).Ry(0.5, 0).H(0)
- custom.apply(circ)
- print(circ.get_commands())
+circ = Circuit(1).H(0).Ry(0.5, 0).Rx(-0.5, 0).Rz(1.5, 0).Ry(0.5, 0).H(0)
+custom.apply(circ)
+print(circ.get_commands())
```
Note that the H gates (which are not in the specified gateset) are left alone.
(compiler-placement)=
-
## Placement
% Task of selecting appropriate physical qubits to use; better use of connectivity and better noise characteristics
@@ -315,12 +314,11 @@ print(connected.verify(circ)) # Now have an exact placement
```
-
False
True
```
-
+(compiler-mapping)=
## Mapping
% Heterogeneous architectures and limited connectivity
@@ -441,26 +439,26 @@ Previous iterations of the {py:class}`~pytket.passes.CliffordSimp` pass would wo
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.passes import CliffordSimp
+from pytket import Circuit, OpType
+from pytket.passes import CliffordSimp
- # A basic inefficient pattern can be reduced by 1 CX
- simple_circ = Circuit(2)
- simple_circ.CX(0, 1).S(1).CX(1, 0)
+# A basic inefficient pattern can be reduced by 1 CX
+simple_circ = Circuit(2)
+simple_circ.CX(0, 1).S(1).CX(1, 0)
- CliffordSimp().apply(simple_circ)
- print(simple_circ.get_commands())
+CliffordSimp().apply(simple_circ)
+print(simple_circ.get_commands())
- # The same pattern, up to commutation and local Clifford algebra
- complex_circ = Circuit(3)
- complex_circ.CX(0, 1)
- complex_circ.Rx(0.42, 1)
- complex_circ.S(1)
- complex_circ.YYPhase(0.96, 1, 2) # Requires 2 CXs to implement
- complex_circ.CX(0, 1)
+# The same pattern, up to commutation and local Clifford algebra
+complex_circ = Circuit(3)
+complex_circ.CX(0, 1)
+complex_circ.Rx(0.42, 1)
+complex_circ.S(1)
+complex_circ.YYPhase(0.96, 1, 2) # Requires 2 CXs to implement
+complex_circ.CX(0, 1)
- CliffordSimp().apply(complex_circ)
- print(complex_circ.get_commands())
+CliffordSimp().apply(complex_circ)
+print(complex_circ.get_commands())
```
The next step up in scale has optimisations based on optimal decompositions of subcircuits over $n$-qubits, including {py:class}`~pytket.passes.EulerAngleReduction` for single-qubit unitary chains (producing three rotations in a choice of axes), and {py:class}`~pytket.passes.KAKDecomposition` for two-qubit unitaries (using at most three CXs and some single-qubit gates).
@@ -468,26 +466,26 @@ The next step up in scale has optimisations based on optimal decompositions of s
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.passes import EulerAngleReduction, KAKDecomposition
+from pytket import Circuit, OpType
+from pytket.passes import EulerAngleReduction, KAKDecomposition
- circ = Circuit(2)
- circ.CZ(0, 1)
- circ.Rx(0.4, 0).Ry(0.289, 0).Rx(-0.34, 0).Ry(0.12, 0).Rx(-0.81, 0)
- circ.CX(1, 0)
+circ = Circuit(2)
+circ.CZ(0, 1)
+circ.Rx(0.4, 0).Ry(0.289, 0).Rx(-0.34, 0).Ry(0.12, 0).Rx(-0.81, 0)
+circ.CX(1, 0)
- # Reduce long chain to a triple of Ry, Rx, Ry
- EulerAngleReduction(OpType.Rx, OpType.Ry).apply(circ)
- print(circ.get_commands())
+# Reduce long chain to a triple of Ry, Rx, Ry
+EulerAngleReduction(OpType.Rx, OpType.Ry).apply(circ)
+print(circ.get_commands())
- circ = Circuit(3)
- circ.CX(0, 1)
- circ.CX(1, 2).Rx(0.3, 1).CX(1, 2).Rz(1.5, 2).CX(1, 2).Ry(-0.94, 1).Ry(0.37, 2).CX(1, 2)
- circ.CX(1, 0)
+circ = Circuit(3)
+circ.CX(0, 1)
+circ.CX(1, 2).Rx(0.3, 1).CX(1, 2).Rz(1.5, 2).CX(1, 2).Ry(-0.94, 1).Ry(0.37, 2).CX(1, 2)
+circ.CX(1, 0)
- # Reduce long 2-qubit subcircuit to at most 3 CXs
- KAKDecomposition().apply(circ)
- print(circ.get_commands())
+# Reduce long 2-qubit subcircuit to at most 3 CXs
+KAKDecomposition().apply(circ)
+print(circ.get_commands())
```
% Situational macroscopic - identifies large structures in circuit or converts circuit to alternative algebraic representation; use properties of the structures to find simplifications; resynthesise into basic gates
@@ -499,19 +497,19 @@ All of these so far are generic optimisations that work for any application, but
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.passes import PauliSimp
- from pytket.utils import Graph
-
- circ = Circuit(3)
- circ.Rz(0.2, 0)
- circ.Rx(0.35, 1)
- circ.V(0).H(1).CX(0, 1).CX(1, 2).Rz(-0.6, 2).CX(1, 2).CX(0, 1).Vdg(0).H(1)
- circ.H(1).H(2).CX(0, 1).CX(1, 2).Rz(0.8, 2).CX(1, 2).CX(0, 1).H(1).H(2)
- circ.Rx(0.1, 1)
-
- PauliSimp().apply(circ)
- Graph(circ).get_DAG()
+from pytket import Circuit
+from pytket.passes import PauliSimp
+from pytket.utils import Graph
+
+circ = Circuit(3)
+circ.Rz(0.2, 0)
+circ.Rx(0.35, 1)
+circ.V(0).H(1).CX(0, 1).CX(1, 2).Rz(-0.6, 2).CX(1, 2).CX(0, 1).Vdg(0).H(1)
+circ.H(1).H(2).CX(0, 1).CX(1, 2).Rz(0.8, 2).CX(1, 2).CX(0, 1).H(1).H(2)
+circ.Rx(0.1, 1)
+
+PauliSimp().apply(circ)
+Graph(circ).get_DAG()
```
% May not always improve the circuit if it doesn't match the structures it was designed to exploit, and the large structural changes from resynthesis could make routing harder
@@ -523,36 +521,36 @@ Some of these optimisation passes have optional parameters to customise the rout
```{code-cell} ipython3
- from pytket.circuit import Circuit, PauliExpBox
- from pytket.passes import PauliSimp
- from pytket.pauli import Pauli
- from pytket.transform import CXConfigType
- from pytket.utils import Graph
+from pytket.circuit import Circuit, PauliExpBox
+from pytket.passes import PauliSimp
+from pytket.pauli import Pauli
+from pytket.transform import CXConfigType
+from pytket.utils import Graph
- pauli_XYXZYXZZ = PauliExpBox([Pauli.X, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Z], 0.42)
+pauli_XYXZYXZZ = PauliExpBox([Pauli.X, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Y, Pauli.X, Pauli.Z, Pauli.Z], 0.42)
- circ = Circuit(8)
- circ.add_gate(pauli_XYXZYXZZ, [0, 1, 2, 3, 4, 5, 6, 7])
+circ = Circuit(8)
+circ.add_gate(pauli_XYXZYXZZ, [0, 1, 2, 3, 4, 5, 6, 7])
- PauliSimp(cx_config=CXConfigType.Snake).apply(circ)
- print(circ.get_commands())
- Graph(circ).get_qubit_graph()
+PauliSimp(cx_config=CXConfigType.Snake).apply(circ)
+print(circ.get_commands())
+Graph(circ).get_qubit_graph()
```
```{code-cell} ipython3
- PauliSimp(cx_config=CXConfigType.Star).apply(circ)
- print(circ.get_commands())
- Graph(circ).get_qubit_graph()
+PauliSimp(cx_config=CXConfigType.Star).apply(circ)
+print(circ.get_commands())
+Graph(circ).get_qubit_graph()
```
```{code-cell} ipython3
- PauliSimp(cx_config=CXConfigType.Tree).apply(circ)
- print(circ.get_commands())
- Graph(circ).get_qubit_graph()
+PauliSimp(cx_config=CXConfigType.Tree).apply(circ)
+print(circ.get_commands())
+Graph(circ).get_qubit_graph()
```
## Combinators
@@ -566,15 +564,15 @@ The passes encountered so far represent elementary, self-contained transformatio
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.passes import auto_rebase_pass, EulerAngleReduction, SequencePass
-
- rebase_quil = auto_rebase_pass({OpType.CZ, OpType.Rz, OpType.Rx})
- circ = Circuit(3)
- circ.CX(0, 1).Rx(0.3, 1).CX(2, 1).Rz(0.8, 1)
- comp = SequencePass([rebase_quil, EulerAngleReduction(OpType.Rz, OpType.Rx)])
- comp.apply(circ)
- print(circ.get_commands())
+from pytket import Circuit, OpType
+from pytket.passes import auto_rebase_pass, EulerAngleReduction, SequencePass
+
+rebase_quil = auto_rebase_pass({OpType.CZ, OpType.Rz, OpType.Rx})
+circ = Circuit(3)
+circ.CX(0, 1).Rx(0.3, 1).CX(2, 1).Rz(0.8, 1)
+comp = SequencePass([rebase_quil, EulerAngleReduction(OpType.Rz, OpType.Rx)])
+comp.apply(circ)
+print(circ.get_commands())
```
% Repeat passes until no further change - useful when one pass can enable further matches for another type of optimisation
@@ -584,14 +582,14 @@ When composing optimisation passes, we may find that applying one type of optimi
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatPass, SequencePass
+from pytket import Circuit
+from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatPass, SequencePass
- circ = Circuit(4)
- circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
- comp = RepeatPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]))
- comp.apply(circ)
- print(circ.get_commands())
+circ = Circuit(4)
+circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
+comp = RepeatPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]))
+comp.apply(circ)
+print(circ.get_commands())
```
```{warning}
@@ -605,15 +603,15 @@ Increased termination safety can be given by only repeating whilst some easy-to-
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatWithMetricPass, SequencePass
-
- circ = Circuit(4)
- circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
- cost = lambda c : c.n_gates_of_type(OpType.CX)
- comp = RepeatWithMetricPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]), cost)
- comp.apply(circ) # Stops earlier than before, since removing CYs doesn't change the number of CXs
- print(circ.get_commands())
+from pytket import Circuit, OpType
+from pytket.passes import RemoveRedundancies, CommuteThroughMultis, RepeatWithMetricPass, SequencePass
+
+circ = Circuit(4)
+circ.CX(2, 3).CY(1, 2).CX(0, 1).Rz(0.24, 0).CX(0, 1).Rz(0.89, 1).CY(1, 2).Rz(-0.3, 2).CX(2, 3)
+cost = lambda c : c.n_gates_of_type(OpType.CX)
+comp = RepeatWithMetricPass(SequencePass([CommuteThroughMultis(), RemoveRedundancies()]), cost)
+comp.apply(circ) # Stops earlier than before, since removing CYs doesn't change the number of CXs
+print(circ.get_commands())
```
% May reject compositions if pre/post-conditions don't match up; some passes will fail to complete or fail to achieve their objective if a circuit does not match their pre-conditions, so we prevent compositions where the latter's pre-conditions cannot be guaranteed
@@ -626,12 +624,14 @@ A special mention here goes to the {py:class}`~pytket.passes.DecomposeBoxes` pas
```{code-cell} ipython3
- :raises: RuntimeError
+---
+tags: [raises-exception]
+---
- from pytket.passes import DecomposeBoxes, PauliSimp, SequencePass
- # PauliSimp requires a specific gateset and no conditional gates
- # or mid-circuit measurement, so this will raise an exception
- comp = SequencePass([DecomposeBoxes(), PauliSimp()])
+from pytket.passes import DecomposeBoxes, PauliSimp, SequencePass
+# PauliSimp requires a specific gateset and no conditional gates
+# or mid-circuit measurement, so this will raise an exception
+comp = SequencePass([DecomposeBoxes(), PauliSimp()])
```
## Predefined Sequences
@@ -680,7 +680,6 @@ SynthesiseTket().apply(circ) # Some added gates may be redundant
print(circ.n_gates_of_type(OpType.CX)) # But not in this case
```
-
```
9
6
@@ -720,36 +719,36 @@ As more intensive optimisations are applied by level 2 the pass may take a long
tags: [skip-execution]
---
- from pytket import Circuit, OpType
- from pytket.extensions.qiskit import IBMQBackend
-
- circ = Circuit(3) # Define a circuit to be compiled to the backend
- circ.CX(0, 1)
- circ.H(1)
- circ.Rx(0.42, 1)
- circ.S(1)
- circ.CX(0, 2)
- circ.CX(2, 1)
- circ.Z(2)
- circ.Y(1)
- circ.CX(0, 1)
- circ.CX(2, 0)
- circ.measure_all()
-
- backend = IBMQBackend("ibmq_quito") # Initialise Backend
-
- print("Total gate count before compilation =", circ.n_gates)
- print("CX count before compilation =", circ.n_gates_of_type(OpType.CX))
-
- # Now apply the default_compilation_pass at different levels of optimisation.
-
- for ol in range(3):
- test_circ = circ.copy()
- backend.default_compilation_pass(optimisation_level=ol).apply(test_circ)
- assert backend.valid_circuit(test_circ)
- print("Optimisation level", ol)
- print("Gates", test_circ.n_gates)
- print("CXs", test_circ.n_gates_of_type(OpType.CX))
+from pytket import Circuit, OpType
+from pytket.extensions.qiskit import IBMQBackend
+
+circ = Circuit(3) # Define a circuit to be compiled to the backend
+circ.CX(0, 1)
+circ.H(1)
+circ.Rx(0.42, 1)
+circ.S(1)
+circ.CX(0, 2)
+circ.CX(2, 1)
+circ.Z(2)
+circ.Y(1)
+circ.CX(0, 1)
+circ.CX(2, 0)
+circ.measure_all()
+
+backend = IBMQBackend("ibmq_quito") # Initialise Backend
+
+print("Total gate count before compilation =", circ.n_gates)
+print("CX count before compilation =", circ.n_gates_of_type(OpType.CX))
+
+ # Now apply the default_compilation_pass at different levels of optimisation.
+
+for ol in range(3):
+ test_circ = circ.copy()
+ backend.default_compilation_pass(optimisation_level=ol).apply(test_circ)
+ assert backend.valid_circuit(test_circ)
+ print("Optimisation level", ol)
+ print("Gates", test_circ.n_gates)
+ print("CXs", test_circ.n_gates_of_type(OpType.CX))
```
@@ -850,36 +849,36 @@ For variational algorithms, the prominent benefit of defining a {py:class}`~pytk
```{code-cell} ipython3
- from pytket import Circuit, Qubit
- from pytket.extensions.qiskit import AerStateBackend
- from pytket.pauli import Pauli, QubitPauliString
- from pytket.utils.operators import QubitPauliOperator
- from sympy import symbols
-
- a, b = symbols("a b")
- circ = Circuit(2)
- circ.Ry(a, 0)
- circ.Ry(a, 1)
- circ.CX(0, 1)
- circ.Rz(b, 1)
- circ.CX(0, 1)
- xx = QubitPauliString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
- op = QubitPauliOperator({xx : 1.5})
-
- backend = AerStateBackend()
-
- # Compile once outside of the objective function
- circ = backend.get_compiled_circuit(circ)
-
- def objective(params):
- state = circ.copy()
- state.symbol_substitution({a : params[0], b : params[1]})
- handle = backend.process_circuit(state) # No need to compile again
- vec = backend.get_result(handle).get_state()
- return op.state_expectation(vec)
-
- print(objective([0.25, 0.5]))
- print(objective([0.5, 0]))
+from pytket import Circuit, Qubit
+from pytket.extensions.qiskit import AerStateBackend
+from pytket.pauli import Pauli, QubitPauliString
+from pytket.utils.operators import QubitPauliOperator
+from sympy import symbols
+
+a, b = symbols("a b")
+circ = Circuit(2)
+circ.Ry(a, 0)
+circ.Ry(a, 1)
+circ.CX(0, 1)
+circ.Rz(b, 1)
+circ.CX(0, 1)
+xx = QubitPauliString({Qubit(0):Pauli.X, Qubit(1):Pauli.X})
+op = QubitPauliOperator({xx : 1.5})
+
+backend = AerStateBackend()
+
+# Compile once outside of the objective function
+circ = backend.get_compiled_circuit(circ)
+
+def objective(params):
+ state = circ.copy()
+ state.symbol_substitution({a : params[0], b : params[1]})
+ handle = backend.process_circuit(state) # No need to compile again
+ vec = backend.get_result(handle).get_state()
+ return op.state_expectation(vec)
+
+print(objective([0.25, 0.5]))
+print(objective([0.5, 0]))
```
% Warning about `NoSymbolsPredicate` and necessity of instantiation before running on backends
@@ -898,24 +897,24 @@ We will show how to use {py:class}`~pytket.passes.CustomPass` by defining a simp
```{code-cell} ipython3
- from pytket import Circuit, OpType
+from pytket import Circuit, OpType
- def z_transform(circ: Circuit) -> Circuit:
- n_qubits = circ.n_qubits
- circ_prime = Circuit(n_qubits) # Define a replacement circuit
-
- for cmd in circ.get_commands():
- qubit_list = cmd.qubits # Qubit(s) our gate is applied on (as a list)
- if cmd.op.type == OpType.Z:
- # If cmd is a Z gate, decompose to a H, X, H sequence.
- circ_prime.add_gate(OpType.H, qubit_list)
- circ_prime.add_gate(OpType.X, qubit_list)
- circ_prime.add_gate(OpType.H, qubit_list)
- else:
- # Otherwise, apply the gate as usual.
- circ_prime.add_gate(cmd.op.type, cmd.op.params, qubit_list)
-
- return circ_prime
+def z_transform(circ: Circuit) -> Circuit:
+ n_qubits = circ.n_qubits
+ circ_prime = Circuit(n_qubits) # Define a replacement circuit
+
+ for cmd in circ.get_commands():
+ qubit_list = cmd.qubits # Qubit(s) our gate is applied on (as a list)
+ if cmd.op.type == OpType.Z:
+ # If cmd is a Z gate, decompose to a H, X, H sequence.
+ circ_prime.add_gate(OpType.H, qubit_list)
+ circ_prime.add_gate(OpType.X, qubit_list)
+ circ_prime.add_gate(OpType.H, qubit_list)
+ else:
+ # Otherwise, apply the gate as usual.
+ circ_prime.add_gate(cmd.op.type, cmd.op.params, qubit_list)
+
+ return circ_prime
```
After we've defined our `transform` we can construct a {py:class}`~pytket.passes.CustomPass`. This pass can then be applied to a {py:class}`~pytket.circuit.Circuit`.
@@ -923,20 +922,20 @@ After we've defined our `transform` we can construct a {py:class}`~pytket.passes
```{code-cell} ipython3
- from pytket.passes import CustomPass
+from pytket.passes import CustomPass
- DecompseZPass = CustomPass(z_transform) # Define our pass
+DecompseZPass = CustomPass(z_transform) # Define our pass
- test_circ = Circuit(2) # Define a test Circuit for our pass
- test_circ.Z(0)
- test_circ.Z(1)
- test_circ.CX(0, 1)
- test_circ.Z(1)
- test_circ.CRy(0.5, 0, 1)
+test_circ = Circuit(2) # Define a test Circuit for our pass
+test_circ.Z(0)
+test_circ.Z(1)
+test_circ.CX(0, 1)
+test_circ.Z(1)
+test_circ.CRy(0.5, 0, 1)
- DecompseZPass.apply(test_circ) # Apply our pass to the test Circuit
+DecompseZPass.apply(test_circ) # Apply our pass to the test Circuit
- test_circ.get_commands() # Commands of our transformed Circuit
+test_circ.get_commands() # Commands of our transformed Circuit
```
We see from the output above that our newly defined {py:class}`DecompseZPass` has successfully decomposed the Pauli Z gates to Hadamard, Pauli X, Hadamard chains and left other gates unchanged.
@@ -1161,15 +1160,15 @@ operation has a classical output in its causal future so will not be removed.
```{code-cell} ipython3
- from pytket.circuit import Qubit
- from pytket.passes import RemoveDiscarded
+from pytket.circuit import Qubit
+from pytket.passes import RemoveDiscarded
- c = Circuit(3, 2)
- c.H(0).H(1).H(2).CX(0, 1).Measure(0, 0).Measure(1, 1).H(0).H(1)
- c.qubit_discard(Qubit(0))
- c.qubit_discard(Qubit(2))
- RemoveDiscarded().apply(c)
- print(c.get_commands())
+c = Circuit(3, 2)
+c.H(0).H(1).H(2).CX(0, 1).Measure(0, 0).Measure(1, 1).H(0).H(1)
+c.qubit_discard(Qubit(0))
+c.qubit_discard(Qubit(2))
+RemoveDiscarded().apply(c)
+print(c.get_commands())
```
The Hadamard gate following the measurement on qubit 0, as well as the Hadamard
@@ -1204,12 +1203,12 @@ Let's illustrate this with a Bell circuit:
```{code-cell} ipython3
- from pytket.passes import SimplifyMeasured
+from pytket.passes import SimplifyMeasured
- c = Circuit(2).H(0).CX(0, 1).measure_all()
- c.qubit_discard_all()
- SimplifyMeasured().apply(c)
- print(c.get_commands())
+c = Circuit(2).H(0).CX(0, 1).measure_all()
+c.qubit_discard_all()
+SimplifyMeasured().apply(c)
+print(c.get_commands())
```
The CX gate has disappeared, replaced with a classical transform acting on the
@@ -1243,18 +1242,18 @@ Thus a typical usage would look something like this:
```{code-cell} ipython3
- from pytket.utils import prepare_circuit
- from pytket.extensions.qiskit import AerBackend
-
- b = AerBackend()
- c = Circuit(2).H(0).CX(0, 1)
- c.measure_all()
- c0, ppcirc = prepare_circuit(c)
- c0 = b.get_compiled_circuit(c0)
- h = b.process_circuit(c0, n_shots=10)
- r = b.get_result(h)
- shots = r.get_shots(ppcirc=ppcirc)
- print(shots)
+from pytket.utils import prepare_circuit
+from pytket.extensions.qiskit import AerBackend
+
+b = AerBackend()
+c = Circuit(2).H(0).CX(0, 1)
+c.measure_all()
+c0, ppcirc = prepare_circuit(c)
+c0 = b.get_compiled_circuit(c0)
+h = b.process_circuit(c0, n_shots=10)
+r = b.get_result(h)
+shots = r.get_shots(ppcirc=ppcirc)
+print(shots)
```
This is a toy example, but illustrates the principle. The actual circuit sent to
diff --git a/docs/manual/manual_noise.md b/docs/manual/manual_noise.md
index fc731a7d..28374c4a 100644
--- a/docs/manual/manual_noise.md
+++ b/docs/manual/manual_noise.md
@@ -31,7 +31,7 @@ interact, with these limitations being determined by the device architecture.
When compiling a circuit to run on one of these devices, the circuit
must be modified to fit the architecture, a process described in the
previous chapter under {ref}`compiler-placement` and
-{ref}`compiler-routing`.
+{ref}`compiler-mapping`.
In addition, the noise present in NISQ devices typically varies across
the architecture, with different qubits and couplings experiencing
From f4569bdb6773b5337b1d54f2d3c3620f7157c689 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 14:34:29 +0100
Subject: [PATCH 09/16] clean up ZX diagrams chapter
---
docs/manual/manual_zx.md | 674 ++++++++++++++++++++-------------------
1 file changed, 338 insertions(+), 336 deletions(-)
diff --git a/docs/manual/manual_zx.md b/docs/manual/manual_zx.md
index 546d6bab..48b12ecb 100644
--- a/docs/manual/manual_zx.md
+++ b/docs/manual/manual_zx.md
@@ -35,12 +35,12 @@ Let's start by making the standard diagram for the qubit teleportation algorithm
```{code-cell} ipython3
- import pytket
- from pytket.zx import ZXDiagram, ZXType, QuantumType, ZXWireType
- import graphviz as gv
+import pytket
+from pytket.zx import ZXDiagram, ZXType, QuantumType, ZXWireType
+import graphviz as gv
- tele = ZXDiagram(1, 1, 0, 0)
- gv.Source(tele.to_graphviz_str())
+tele = ZXDiagram(1, 1, 0, 0)
+gv.Source(tele.to_graphviz_str())
```
We will choose to represent the Bell state as a cup (i.e. an edge connecting one side of the CX to the first correction). In terms of vertices, we need two for the CX gate, two for the measurements, and four for the encoding and application of corrections. The CX and corrections need to be coherent operations so will be {py:class}`QuantumType.Quantum` as opposed to the measurements and encodings. We can then link them up by adding edges of the appropriate {py:class}`QuantumType`. The visualisations will show {py:class}`QuantumType.Quantum` generators and edges with thick lines and {py:class}`QuantumType.Classical` with thinner lines as per standard notation conventions.
@@ -48,38 +48,38 @@ We will choose to represent the Bell state as a cup (i.e. an edge connecting one
```{code-cell} ipython3
- (in_v, out_v) = tele.get_boundary()
- cx_c = tele.add_vertex(ZXType.ZSpider)
- cx_t = tele.add_vertex(ZXType.XSpider)
- z_meas = tele.add_vertex(ZXType.ZSpider, qtype=QuantumType.Classical)
- x_meas = tele.add_vertex(ZXType.XSpider, qtype=QuantumType.Classical)
- z_enc = tele.add_vertex(ZXType.ZSpider, qtype=QuantumType.Classical)
- x_enc = tele.add_vertex(ZXType.XSpider, qtype=QuantumType.Classical)
- z_correct = tele.add_vertex(ZXType.ZSpider)
- x_correct = tele.add_vertex(ZXType.XSpider)
-
- # Bell pair between CX and first correction
- tele.add_wire(cx_t, x_correct)
-
- # Apply CX between input and first ancilla
- tele.add_wire(in_v, cx_c)
- tele.add_wire(cx_c, cx_t)
-
- # Measure first two qubits
- tele.add_wire(cx_c, x_meas)
- tele.add_wire(cx_t, z_meas)
-
- # Feed measurement outcomes to corrections
- tele.add_wire(x_meas, x_enc, qtype=QuantumType.Classical)
- tele.add_wire(x_enc, z_correct)
- tele.add_wire(z_meas, z_enc, qtype=QuantumType.Classical)
- tele.add_wire(z_enc, x_correct)
-
- # Apply corrections to second ancilla
- tele.add_wire(x_correct, z_correct)
- tele.add_wire(z_correct, out_v)
-
- gv.Source(tele.to_graphviz_str())
+(in_v, out_v) = tele.get_boundary()
+cx_c = tele.add_vertex(ZXType.ZSpider)
+cx_t = tele.add_vertex(ZXType.XSpider)
+z_meas = tele.add_vertex(ZXType.ZSpider, qtype=QuantumType.Classical)
+x_meas = tele.add_vertex(ZXType.XSpider, qtype=QuantumType.Classical)
+z_enc = tele.add_vertex(ZXType.ZSpider, qtype=QuantumType.Classical)
+x_enc = tele.add_vertex(ZXType.XSpider, qtype=QuantumType.Classical)
+z_correct = tele.add_vertex(ZXType.ZSpider)
+x_correct = tele.add_vertex(ZXType.XSpider)
+
+# Bell pair between CX and first correction
+tele.add_wire(cx_t, x_correct)
+
+# Apply CX between input and first ancilla
+tele.add_wire(in_v, cx_c)
+tele.add_wire(cx_c, cx_t)
+
+# Measure first two qubits
+tele.add_wire(cx_c, x_meas)
+tele.add_wire(cx_t, z_meas)
+
+# Feed measurement outcomes to corrections
+tele.add_wire(x_meas, x_enc, qtype=QuantumType.Classical)
+tele.add_wire(x_enc, z_correct)
+tele.add_wire(z_meas, z_enc, qtype=QuantumType.Classical)
+tele.add_wire(z_enc, x_correct)
+
+# Apply corrections to second ancilla
+tele.add_wire(x_correct, z_correct)
+tele.add_wire(z_correct, out_v)
+
+gv.Source(tele.to_graphviz_str())
```
We can use this teleportation algorithm as a component in a larger diagram using a {py:class}`ZXBox`. Here, we insert it in the middle of a two qubit circuit.
@@ -87,38 +87,38 @@ We can use this teleportation algorithm as a component in a larger diagram using
```{code-cell} ipython3
- circ_diag = ZXDiagram(2, 1, 0, 1)
- qin0 = circ_diag.get_boundary(ZXType.Input)[0]
- qin1 = circ_diag.get_boundary(ZXType.Input)[1]
- qout = circ_diag.get_boundary(ZXType.Output)[0]
- cout = circ_diag.get_boundary(ZXType.Output)[1]
-
- cz_c = circ_diag.add_vertex(ZXType.ZSpider)
- cz_t = circ_diag.add_vertex(ZXType.ZSpider)
- # Phases of spiders are given in half-turns, so this is a pi/4 rotation
- rx = circ_diag.add_vertex(ZXType.XSpider, 0.25)
- x_meas = circ_diag.add_vertex(ZXType.XSpider, qtype=QuantumType.Classical)
- box = circ_diag.add_zxbox(tele)
-
- # CZ between inputs
- circ_diag.add_wire(qin0, cz_c)
- circ_diag.add_wire(qin1, cz_t)
- circ_diag.add_wire(cz_c, cz_t, type=ZXWireType.H)
-
- # Rx on first qubit
- circ_diag.add_wire(cz_c, rx)
-
- # Teleport first qubit
- # The inputs appear first in the boundary of tele, so port 0 is the input
- circ_diag.add_wire(u=rx, v=box, v_port=0)
- # Port 1 for the output
- circ_diag.add_wire(u=box, v=qout, u_port=1)
-
- # Measure second qubit destructively and output result
- circ_diag.add_wire(cz_t, x_meas)
- circ_diag.add_wire(x_meas, cout, type=ZXWireType.H, qtype=QuantumType.Classical)
-
- gv.Source(circ_diag.to_graphviz_str())
+circ_diag = ZXDiagram(2, 1, 0, 1)
+qin0 = circ_diag.get_boundary(ZXType.Input)[0]
+qin1 = circ_diag.get_boundary(ZXType.Input)[1]
+qout = circ_diag.get_boundary(ZXType.Output)[0]
+cout = circ_diag.get_boundary(ZXType.Output)[1]
+
+cz_c = circ_diag.add_vertex(ZXType.ZSpider)
+cz_t = circ_diag.add_vertex(ZXType.ZSpider)
+# Phases of spiders are given in half-turns, so this is a pi/4 rotation
+rx = circ_diag.add_vertex(ZXType.XSpider, 0.25)
+x_meas = circ_diag.add_vertex(ZXType.XSpider, qtype=QuantumType.Classical)
+box = circ_diag.add_zxbox(tele)
+
+# CZ between inputs
+circ_diag.add_wire(qin0, cz_c)
+circ_diag.add_wire(qin1, cz_t)
+circ_diag.add_wire(cz_c, cz_t, type=ZXWireType.H)
+
+# Rx on first qubit
+circ_diag.add_wire(cz_c, rx)
+
+# Teleport first qubit
+# The inputs appear first in the boundary of tele, so port 0 is the input
+circ_diag.add_wire(u=rx, v=box, v_port=0)
+# Port 1 for the output
+circ_diag.add_wire(u=box, v=qout, u_port=1)
+
+# Measure second qubit destructively and output result
+circ_diag.add_wire(cz_t, x_meas)
+circ_diag.add_wire(x_meas, cout, type=ZXWireType.H, qtype=QuantumType.Classical)
+
+gv.Source(circ_diag.to_graphviz_str())
```
% Validity conditions of a diagram
@@ -141,11 +141,12 @@ As the pytket ZX diagrams represent mixed diagrams, this impacts the interpretat
```{code-cell} ipython3
- from pytket.zx.tensor_eval import tensor_from_mixed_diagram
- ten = tensor_from_mixed_diagram(circ_diag)
- # Indices are (qin0, qin0_conj, qin1, qin1_conj, qout, qout_conj, cout)
- print(ten.shape)
- print(ten[:, :, 1, 1, 0, 0, :].round(4))
+from pytket.zx.tensor_eval import tensor_from_mixed_diagram
+
+ten = tensor_from_mixed_diagram(circ_diag)
+# Indices are (qin0, qin0_conj, qin1, qin1_conj, qout, qout_conj, cout)
+print(ten.shape)
+print(ten[:, :, 1, 1, 0, 0, :].round(4))
```
In many cases, we work with pure quantum diagrams. This doubling would cause substantial blowup in time and memory for evaluation, as well as making the tensor difficult to navigate for large diagrams. {py:meth}`tensor_from_quantum_diagram()` achieves the same as converting all {py:class}`QuantumType.Quantum` components to {py:class}`QuantumType.Classical`, meaning every edge is reduced down to dimension 2. Since the global scalar is maintained with respect to a doubled diagram, its square root is incorporated into the tensor, though we do not maintain the coherent global phase of a pure quantum diagram in this way. For diagrams like this, {py:meth}`unitary_from_quantum_diagram()` reformats the tensor into the conventional unitary (with big-endian indexing).
@@ -153,23 +154,24 @@ In many cases, we work with pure quantum diagrams. This doubling would cause sub
```{code-cell} ipython3
- from pytket.zx.tensor_eval import tensor_from_quantum_diagram, unitary_from_quantum_diagram
- u_diag = ZXDiagram(2, 2, 0, 0)
- ins = u_diag.get_boundary(ZXType.Input)
- outs = u_diag.get_boundary(ZXType.Output)
- cx_c = u_diag.add_vertex(ZXType.ZSpider)
- cx_t = u_diag.add_vertex(ZXType.XSpider)
- rz = u_diag.add_vertex(ZXType.ZSpider, -0.25)
-
- u_diag.add_wire(ins[0], cx_c)
- u_diag.add_wire(ins[1], cx_t)
- u_diag.add_wire(cx_c, cx_t)
- u_diag.add_wire(cx_t, rz)
- u_diag.add_wire(cx_c, outs[0])
- u_diag.add_wire(rz, outs[1])
-
- print(tensor_from_quantum_diagram(u_diag).round(4))
- print(unitary_from_quantum_diagram(u_diag).round(4))
+from pytket.zx.tensor_eval import tensor_from_quantum_diagram, unitary_from_quantum_diagram
+
+u_diag = ZXDiagram(2, 2, 0, 0)
+ins = u_diag.get_boundary(ZXType.Input)
+outs = u_diag.get_boundary(ZXType.Output)
+cx_c = u_diag.add_vertex(ZXType.ZSpider)
+cx_t = u_diag.add_vertex(ZXType.XSpider)
+rz = u_diag.add_vertex(ZXType.ZSpider, -0.25)
+
+u_diag.add_wire(ins[0], cx_c)
+u_diag.add_wire(ins[1], cx_t)
+u_diag.add_wire(cx_c, cx_t)
+u_diag.add_wire(cx_t, rz)
+u_diag.add_wire(cx_c, outs[0])
+u_diag.add_wire(rz, outs[1])
+
+print(tensor_from_quantum_diagram(u_diag).round(4))
+print(unitary_from_quantum_diagram(u_diag).round(4))
```
Similarly, one may use {py:meth}`density_matrix_from_cptp_diagram()` to obtain a density matrix when all boundaries are {py:class}`QuantumType.Quantum` but the diagram itself contains mixed components. When input boundaries exist, this gives the density matrix under the Choi-Jamiołkovski isomorphism. For example, we can verify that our teleportation diagram from earlier really does reduce to the identity (recall that the Choi-Jamiołkovski isomorphism maps the identity channel to a Bell state).
@@ -177,9 +179,9 @@ Similarly, one may use {py:meth}`density_matrix_from_cptp_diagram()` to obtain a
```{code-cell} ipython3
- from pytket.zx.tensor_eval import density_matrix_from_cptp_diagram
+from pytket.zx.tensor_eval import density_matrix_from_cptp_diagram
- print(density_matrix_from_cptp_diagram(tele))
+print(density_matrix_from_cptp_diagram(tele))
```
% Tensor indices, unitaries and states; initialisation and post-selection
@@ -189,9 +191,9 @@ Another way to potentially reduce the computational load for tensor evaluation i
```{code-cell} ipython3
- from pytket.zx.tensor_eval import fix_inputs_to_binary_state
- state_diag = fix_inputs_to_binary_state(u_diag, [1, 0])
- print(unitary_from_quantum_diagram(state_diag).round(4))
+from pytket.zx.tensor_eval import fix_inputs_to_binary_state
+state_diag = fix_inputs_to_binary_state(u_diag, [1, 0])
+print(unitary_from_quantum_diagram(state_diag).round(4))
```
% Note on location in test folder
@@ -203,7 +205,7 @@ The ability to build static diagrams is fine for visualisation and simulation ne
```{code-cell} ipython3
- gv.Source(tele.to_graphviz_str())
+gv.Source(tele.to_graphviz_str())
```
% Boundaries (ordering, types and incident edges, not associated to UnitIDs)
@@ -217,10 +219,10 @@ Once we have an edge, we can inspect and modify its properties, specifically its
```{code-cell} ipython3
- (in_v, out_v) = tele.get_boundary()
- in_edge = tele.adj_wires(in_v)[0]
- print(tele.get_wire_qtype(in_edge))
- print(tele.get_wire_type(in_edge))
+(in_v, out_v) = tele.get_boundary()
+in_edge = tele.adj_wires(in_v)[0]
+print(tele.get_wire_qtype(in_edge))
+print(tele.get_wire_type(in_edge))
```
The diagram is presented as an undirected graph. We can inspect the end points of an edge with {py:meth}`ZXDiagram.get_wire_ends()`, which returns pairs of vertex and port. If we simply wish to traverse the edge to the next vertex, we use {py:meth}`ZXDiagram.other_end()`. Or we can skip wire traversal altogether using {py:meth}`ZXDiagram.neighbours()` to enumerate the neighbours of a given vertex. This is mostly useful when the wires in a diagram have a consistent form, such as in a graphlike or MBQC diagram (every wire is a Hadamard except for boundary wires).
@@ -230,11 +232,11 @@ If you are searching the diagram for a pattern that is simple enough that a full
```{code-cell} ipython3
- cx_c = tele.other_end(in_edge, in_v)
- assert tele.get_wire_ends(in_edge) == ((in_v, None), (cx_c, None))
+cx_c = tele.other_end(in_edge, in_v)
+assert tele.get_wire_ends(in_edge) == ((in_v, None), (cx_c, None))
- for v in tele.vertices:
- print(tele.get_zxtype(v))
+for v in tele.vertices:
+ print(tele.get_zxtype(v))
```
Using this, we can scan our diagram for adjacent spiders of the same colour connected by a basic edge to apply spider fusion. In general, this will require us to also inspect the generators of the vertex to be able to add the phases and update the {py:class}`QuantumType` in case of merging with a {py:class}`QuantumType.Classical` spider.
@@ -248,42 +250,42 @@ Each generator object is immutable, so updating a vertex requires creating a new
```{code-cell} ipython3
- from pytket.zx import PhasedGen
+from pytket.zx import PhasedGen
- def fuse():
- removed = []
- for v in tele.vertices:
- if v in removed or tele.get_zxtype(v) not in (ZXType.ZSpider, ZXType.XSpider):
+def fuse():
+ removed = []
+ for v in tele.vertices:
+ if v in removed or tele.get_zxtype(v) not in (ZXType.ZSpider, ZXType.XSpider):
+ continue
+ for w in tele.adj_wires(v):
+ if tele.get_wire_type(w) != ZXWireType.Basic:
continue
- for w in tele.adj_wires(v):
- if tele.get_wire_type(w) != ZXWireType.Basic:
- continue
-
- n = tele.other_end(w, v)
- if tele.get_zxtype(n) != tele.get_zxtype(v):
- continue
-
- # Match found, copy n's edges onto v
- for nw in tele.adj_wires(n):
- if nw != w:
- # We know all vertices here are symmetric generators so we
- # don't need to care about port information
- nn = tele.other_end(nw, n)
- wtype = tele.get_wire_type(nw)
- qtype = tele.get_wire_qtype(nw)
- tele.add_wire(v, nn, wtype, qtype)
- # Update v to have total phase
- n_spid = tele.get_vertex_ZXGen(n)
- v_spid = tele.get_vertex_ZXGen(v)
- v_qtype = QuantumType.Classical if n_spid.qtype == QuantumType.Classical or v_spid.qtype == QuantumType.Classical else QuantumType.Quantum
- tele.set_vertex_ZXGen(v, PhasedGen(v_spid.type, v_spid.param + n_spid.param, v_qtype))
- # Remove n
- tele.remove_vertex(n)
- removed.append(n)
-
- fuse()
-
- gv.Source(tele.to_graphviz_str())
+
+ n = tele.other_end(w, v)
+ if tele.get_zxtype(n) != tele.get_zxtype(v):
+ continue
+
+ # Match found, copy n's edges onto v
+ for nw in tele.adj_wires(n):
+ if nw != w:
+ # We know all vertices here are symmetric generators so we
+ # don't need to care about port information
+ nn = tele.other_end(nw, n)
+ wtype = tele.get_wire_type(nw)
+ qtype = tele.get_wire_qtype(nw)
+ tele.add_wire(v, nn, wtype, qtype)
+ # Update v to have total phase
+ n_spid = tele.get_vertex_ZXGen(n)
+ v_spid = tele.get_vertex_ZXGen(v)
+ v_qtype = QuantumType.Classical if n_spid.qtype == QuantumType.Classical or v_spid.qtype == QuantumType.Classical else QuantumType.Quantum
+ tele.set_vertex_ZXGen(v, PhasedGen(v_spid.type, v_spid.param + n_spid.param, v_qtype))
+ # Remove n
+ tele.remove_vertex(n)
+ removed.append(n)
+
+fuse()
+
+gv.Source(tele.to_graphviz_str())
```
Similarly, we can scan for a pair of adjacent basic edges between a green and a red spider for the strong complementarity rule.
@@ -291,32 +293,32 @@ Similarly, we can scan for a pair of adjacent basic edges between a green and a
```{code-cell} ipython3
- def strong_comp():
- gr_edges = dict()
- for w in tele.wires:
- if tele.get_wire_type(w) != ZXWireType.Basic:
- continue
- ((u, u_port), (v, v_port)) = tele.get_wire_ends(w)
- gr_match = None
- if tele.get_zxtype(u) == ZXType.ZSpider and tele.get_zxtype(v) == ZXType.XSpider:
- gr_match = (u, v)
- elif tele.get_zxtype(u) == ZXType.XSpider and tele.get_zxtype(v) == ZXType.ZSpider:
- gr_match = (v, u)
-
- if gr_match:
- if gr_match in gr_edges:
- # Found a matching pair, remove them
- other_w = gr_edges[gr_match]
- tele.remove_wire(w)
- tele.remove_wire(other_w)
- del gr_edges[gr_match]
- else:
- # Record the edge for later
- gr_edges[gr_match] = w
-
- strong_comp()
-
- gv.Source(tele.to_graphviz_str())
+def strong_comp():
+ gr_edges = dict()
+ for w in tele.wires:
+ if tele.get_wire_type(w) != ZXWireType.Basic:
+ continue
+ ((u, u_port), (v, v_port)) = tele.get_wire_ends(w)
+ gr_match = None
+ if tele.get_zxtype(u) == ZXType.ZSpider and tele.get_zxtype(v) == ZXType.XSpider:
+ gr_match = (u, v)
+ elif tele.get_zxtype(u) == ZXType.XSpider and tele.get_zxtype(v) == ZXType.ZSpider:
+ gr_match = (v, u)
+
+ if gr_match:
+ if gr_match in gr_edges:
+ # Found a matching pair, remove them
+ other_w = gr_edges[gr_match]
+ tele.remove_wire(w)
+ tele.remove_wire(other_w)
+ del gr_edges[gr_match]
+ else:
+ # Record the edge for later
+ gr_edges[gr_match] = w
+
+strong_comp()
+
+gv.Source(tele.to_graphviz_str())
```
Finally, we write a procedure that finds spiders of degree 2 which act like an identity. We need to check that the phase on the spider is zero, and that the {py:class}`QuantumType` of the generator matches those of the incident edges (so we don't accidentally remove decoherence spiders).
@@ -324,43 +326,43 @@ Finally, we write a procedure that finds spiders of degree 2 which act like an i
```{code-cell} ipython3
- def id_remove():
- for v in tele.vertices:
- if tele.degree(v) == 2 and tele.get_zxtype(v) in (ZXType.ZSpider, ZXType.XSpider):
- spid = tele.get_vertex_ZXGen(v)
- ws = tele.adj_wires(v)
- if spid.param == 0 and tele.get_wire_qtype(ws[0]) == spid.qtype and tele.get_wire_qtype(ws[1]) == spid.qtype:
- # Found an identity
- n0 = tele.other_end(ws[0], v)
- n1 = tele.other_end(ws[1], v)
- wtype = ZXWireType.H if (tele.get_wire_type(ws[0]) == ZXWireType.H) != (tele.get_wire_type(ws[1]) == ZXWireType.H) else ZXWireType.Basic
- tele.add_wire(n0, n1, wtype, spid.qtype)
- tele.remove_vertex(v)
-
- id_remove()
-
- gv.Source(tele.to_graphviz_str())
+def id_remove():
+ for v in tele.vertices:
+ if tele.degree(v) == 2 and tele.get_zxtype(v) in (ZXType.ZSpider, ZXType.XSpider):
+ spid = tele.get_vertex_ZXGen(v)
+ ws = tele.adj_wires(v)
+ if spid.param == 0 and tele.get_wire_qtype(ws[0]) == spid.qtype and tele.get_wire_qtype(ws[1]) == spid.qtype:
+ # Found an identity
+ n0 = tele.other_end(ws[0], v)
+ n1 = tele.other_end(ws[1], v)
+ wtype = ZXWireType.H if (tele.get_wire_type(ws[0]) == ZXWireType.H) != (tele.get_wire_type(ws[1]) == ZXWireType.H) else ZXWireType.Basic
+ tele.add_wire(n0, n1, wtype, spid.qtype)
+ tele.remove_vertex(v)
+
+id_remove()
+
+gv.Source(tele.to_graphviz_str())
```
```{code-cell} ipython3
- fuse()
- gv.Source(tele.to_graphviz_str())
+fuse()
+gv.Source(tele.to_graphviz_str())
```
```{code-cell} ipython3
- strong_comp()
- gv.Source(tele.to_graphviz_str())
+strong_comp()
+gv.Source(tele.to_graphviz_str())
```
```{code-cell} ipython3
- id_remove()
- gv.Source(tele.to_graphviz_str())
+id_remove()
+gv.Source(tele.to_graphviz_str())
```
% Removing vertices and edges versus editing in-place
@@ -376,86 +378,86 @@ The pytket ZX module comes with a handful of common rewrite procedures built-in
```{code-cell} ipython3
- # This diagram follows from section A of https://arxiv.org/pdf/1902.03178.pdf
- diag = ZXDiagram(4, 4, 0, 0)
- ins = diag.get_boundary(ZXType.Input)
- outs = diag.get_boundary(ZXType.Output)
- v11 = diag.add_vertex(ZXType.ZSpider, 1.5)
- v12 = diag.add_vertex(ZXType.ZSpider, 0.5)
- v13 = diag.add_vertex(ZXType.ZSpider)
- v14 = diag.add_vertex(ZXType.XSpider)
- v15 = diag.add_vertex(ZXType.ZSpider, 0.25)
- v21 = diag.add_vertex(ZXType.ZSpider, 0.5)
- v22 = diag.add_vertex(ZXType.ZSpider)
- v23 = diag.add_vertex(ZXType.ZSpider)
- v24 = diag.add_vertex(ZXType.ZSpider, 0.25)
- v25 = diag.add_vertex(ZXType.ZSpider)
- v31 = diag.add_vertex(ZXType.XSpider)
- v32 = diag.add_vertex(ZXType.XSpider)
- v33 = diag.add_vertex(ZXType.ZSpider, 0.5)
- v34 = diag.add_vertex(ZXType.ZSpider, 0.5)
- v35 = diag.add_vertex(ZXType.XSpider)
- v41 = diag.add_vertex(ZXType.ZSpider)
- v42 = diag.add_vertex(ZXType.ZSpider)
- v43 = diag.add_vertex(ZXType.ZSpider, 1.5)
- v44 = diag.add_vertex(ZXType.XSpider, 1.0)
- v45 = diag.add_vertex(ZXType.ZSpider, 0.5)
- v46 = diag.add_vertex(ZXType.XSpider, 1.0)
-
- diag.add_wire(ins[0], v11)
- diag.add_wire(v11, v12, ZXWireType.H)
- diag.add_wire(v12, v13)
- diag.add_wire(v13, v41, ZXWireType.H)
- diag.add_wire(v13, v14)
- diag.add_wire(v14, v42)
- diag.add_wire(v14, v15, ZXWireType.H)
- diag.add_wire(v15, outs[0], ZXWireType.H)
-
- diag.add_wire(ins[1], v21)
- diag.add_wire(v21, v22)
- diag.add_wire(v22, v31)
- diag.add_wire(v22, v23, ZXWireType.H)
- diag.add_wire(v23, v32)
- diag.add_wire(v23, v24)
- diag.add_wire(v24, v25, ZXWireType.H)
- diag.add_wire(v25, v35)
- diag.add_wire(outs[1], v25)
-
- diag.add_wire(ins[2], v31)
- diag.add_wire(v31, v32)
- diag.add_wire(v32, v33)
- diag.add_wire(v33, v34, ZXWireType.H)
- diag.add_wire(v34, v35)
- diag.add_wire(v35, outs[2])
-
- diag.add_wire(ins[3], v41, ZXWireType.H)
- diag.add_wire(v41, v42)
- diag.add_wire(v42, v43, ZXWireType.H)
- diag.add_wire(v43, v44)
- diag.add_wire(v44, v45)
- diag.add_wire(v45, v46)
- diag.add_wire(v46, outs[3])
- diag.check_validity()
-
- gv.Source(diag.to_graphviz_str())
+# This diagram follows from section A of https://arxiv.org/pdf/1902.03178.pdf
+diag = ZXDiagram(4, 4, 0, 0)
+ins = diag.get_boundary(ZXType.Input)
+outs = diag.get_boundary(ZXType.Output)
+v11 = diag.add_vertex(ZXType.ZSpider, 1.5)
+v12 = diag.add_vertex(ZXType.ZSpider, 0.5)
+v13 = diag.add_vertex(ZXType.ZSpider)
+v14 = diag.add_vertex(ZXType.XSpider)
+v15 = diag.add_vertex(ZXType.ZSpider, 0.25)
+v21 = diag.add_vertex(ZXType.ZSpider, 0.5)
+v22 = diag.add_vertex(ZXType.ZSpider)
+v23 = diag.add_vertex(ZXType.ZSpider)
+v24 = diag.add_vertex(ZXType.ZSpider, 0.25)
+v25 = diag.add_vertex(ZXType.ZSpider)
+v31 = diag.add_vertex(ZXType.XSpider)
+v32 = diag.add_vertex(ZXType.XSpider)
+v33 = diag.add_vertex(ZXType.ZSpider, 0.5)
+v34 = diag.add_vertex(ZXType.ZSpider, 0.5)
+v35 = diag.add_vertex(ZXType.XSpider)
+v41 = diag.add_vertex(ZXType.ZSpider)
+v42 = diag.add_vertex(ZXType.ZSpider)
+v43 = diag.add_vertex(ZXType.ZSpider, 1.5)
+v44 = diag.add_vertex(ZXType.XSpider, 1.0)
+v45 = diag.add_vertex(ZXType.ZSpider, 0.5)
+v46 = diag.add_vertex(ZXType.XSpider, 1.0)
+
+diag.add_wire(ins[0], v11)
+diag.add_wire(v11, v12, ZXWireType.H)
+diag.add_wire(v12, v13)
+diag.add_wire(v13, v41, ZXWireType.H)
+diag.add_wire(v13, v14)
+diag.add_wire(v14, v42)
+diag.add_wire(v14, v15, ZXWireType.H)
+diag.add_wire(v15, outs[0], ZXWireType.H)
+
+diag.add_wire(ins[1], v21)
+diag.add_wire(v21, v22)
+diag.add_wire(v22, v31)
+diag.add_wire(v22, v23, ZXWireType.H)
+diag.add_wire(v23, v32)
+diag.add_wire(v23, v24)
+diag.add_wire(v24, v25, ZXWireType.H)
+diag.add_wire(v25, v35)
+diag.add_wire(outs[1], v25)
+
+diag.add_wire(ins[2], v31)
+diag.add_wire(v31, v32)
+diag.add_wire(v32, v33)
+diag.add_wire(v33, v34, ZXWireType.H)
+diag.add_wire(v34, v35)
+diag.add_wire(v35, outs[2])
+
+diag.add_wire(ins[3], v41, ZXWireType.H)
+diag.add_wire(v41, v42)
+diag.add_wire(v42, v43, ZXWireType.H)
+diag.add_wire(v43, v44)
+diag.add_wire(v44, v45)
+diag.add_wire(v45, v46)
+diag.add_wire(v46, outs[3])
+diag.check_validity()
+
+gv.Source(diag.to_graphviz_str())
```
```{code-cell} ipython3
- from pytket.zx import Rewrite
+from pytket.zx import Rewrite
- Rewrite.red_to_green().apply(diag)
- Rewrite.spider_fusion().apply(diag)
- Rewrite.io_extension().apply(diag)
- gv.Source(diag.to_graphviz_str())
+Rewrite.red_to_green().apply(diag)
+Rewrite.spider_fusion().apply(diag)
+Rewrite.io_extension().apply(diag)
+gv.Source(diag.to_graphviz_str())
```
```{code-cell} ipython3
- Rewrite.reduce_graphlike_form().apply(diag)
- gv.Source(diag.to_graphviz_str())
+Rewrite.reduce_graphlike_form().apply(diag)
+gv.Source(diag.to_graphviz_str())
```
% Intended to support common optimisation strategies; focussed on reducing to specific forms and work in graphlike form
@@ -475,30 +477,30 @@ The rewrite passes can be broken down into a few categories depending on the for
=================================== ===========================================
Decompositions into generating sets
- :py:meth:`Rewrite.decompose_boxes()`,
- :py:meth:`Rewrite.basic_wires()`,
- :py:meth:`Rewrite.rebase_to_zx()`,
- :py:meth:`Rewrite.rebase_to_mbqc()`
+ `Rewrite.decompose_boxes()`,
+ `Rewrite.basic_wires()`,
+ `Rewrite.rebase_to_zx()`,
+ `Rewrite.rebase_to_mbqc()`
Rewriting into graphlike form
- :py:meth:`Rewrite.red_to_green()`,
- :py:meth:`Rewrite.spider_fusion()`,
- :py:meth:`Rewrite.self_loop_removal()`,
- :py:meth:`Rewrite.parallel_h_removal()`,
- :py:meth:`Rewrite.separate_boundaries()`,
- :py:meth:`Rewrite.io_extension()`
+ `Rewrite.red_to_green()`,
+ `Rewrite.spider_fusion()`,
+ `Rewrite.self_loop_removal()`,
+ `Rewrite.parallel_h_removal()`,
+ `Rewrite.separate_boundaries()`,
+ `Rewrite.io_extension()`
Reduction within graphlike form
- :py:meth:`Rewrite.remove_interior_cliffords()`,
- :py:meth:`Rewrite.remove_interior_paulis()`,
- :py:meth:`Rewrite.gadgetise_interior_paulis()`,
- :py:meth:`Rewrite.merge_gadgets()`,
- :py:meth:`Rewrite.extend_at_boundary_paulis()`
+ `Rewrite.remove_interior_cliffords()`,
+ `Rewrite.remove_interior_paulis()`,
+ `Rewrite.gadgetise_interior_paulis()`,
+ `Rewrite.merge_gadgets()`,
+ `Rewrite.extend_at_boundary_paulis()`
MBQC
- :py:meth:`Rewrite.extend_for_PX_outputs()`,
- :py:meth:`Rewrite.internalise_gadgets()`
+ `Rewrite.extend_for_PX_outputs()`,
+ `Rewrite.internalise_gadgets()`
Composite sequences
- :py:meth:`Rewrite.to_graphlike_form()`,
- :py:meth:`Rewrite.reduce_graphlike_form()`,
- :py:meth:`Rewrite.to_MBQC_diag()`
+ `Rewrite.to_graphlike_form()`,
+ `Rewrite.reduce_graphlike_form()`,
+ `Rewrite.to_MBQC_diag()`
=================================== ===========================================
```
@@ -523,8 +525,8 @@ Each of the MBQC {py:class}`ZXType` options represent a qubit that is initialise
```{code-cell} ipython3
- Rewrite.to_MBQC_diag().apply(diag)
- gv.Source(diag.to_graphviz_str())
+Rewrite.to_MBQC_diag().apply(diag)
+gv.Source(diag.to_graphviz_str())
```
% Causal flow, gflow, Pauli flow (completeness of extended Pauli flow and hence Pauli flow)
@@ -536,23 +538,23 @@ The {py:class}`Flow` object that is returned abstracts away the partial ordering
```{code-cell} ipython3
- from pytket.zx import Flow
+from pytket.zx import Flow
- fl = Flow.identify_pauli_flow(diag)
+fl = Flow.identify_pauli_flow(diag)
- # We can look up the flow data for a particular vertex
- # For example, let's take the first input qubit
- vertex_ids = { v : i for (i, v) in enumerate(diag.vertices) }
- in0 = diag.get_boundary(ZXType.Input)[0]
- v = diag.neighbours(in0)[0]
- print(vertex_ids[v])
- print(fl.d(v))
- print([vertex_ids[c] for c in fl.c(v)])
- print([vertex_ids[o] for o in fl.odd(v, diag)])
+# We can look up the flow data for a particular vertex
+# For example, let's take the first input qubit
+vertex_ids = { v : i for (i, v) in enumerate(diag.vertices) }
+in0 = diag.get_boundary(ZXType.Input)[0]
+v = diag.neighbours(in0)[0]
+print(vertex_ids[v])
+print(fl.d(v))
+print([vertex_ids[c] for c in fl.c(v)])
+print([vertex_ids[o] for o in fl.odd(v, diag)])
- # Or we can obtain the entire flow as maps for easy iteration
- print({ vertex_ids[v] : d for (v, d) in fl.dmap.items() })
- print({ vertex_ids[v] : [vertex_ids[c] for c in cs] for (v, cs) in fl.cmap.items() })
+# Or we can obtain the entire flow as maps for easy iteration
+print({ vertex_ids[v] : d for (v, d) in fl.dmap.items() })
+print({ vertex_ids[v] : [vertex_ids[c] for c in cs] for (v, cs) in fl.cmap.items() })
```
```{note}
@@ -582,27 +584,27 @@ The boundaries of the resulting {py:class}`ZXDiagram` will match up with the ope
```{code-cell} ipython3
- from pytket import Circuit, Qubit
- from pytket.zx import circuit_to_zx
-
- c = Circuit(4)
- c.CZ(0, 1)
- c.CX(1, 2)
- c.H(1)
- c.X(0)
- c.Rx(0.7, 0)
- c.Rz(0.2, 1)
- c.X(3)
- c.H(2)
- c.qubit_create(Qubit(2))
- c.qubit_discard(Qubit(3))
- diag, bound_map = circuit_to_zx(c)
-
- in3, out3 = bound_map[Qubit(3)]
- # Qubit 3 was discarded, so out3 won't give a vertex
- # Look at the neighbour of the input to check the first operation is the X
- n = diag.neighbours(in3)[0]
- print(diag.get_vertex_ZXGen(n))
+from pytket import Circuit, Qubit
+from pytket.zx import circuit_to_zx
+
+c = Circuit(4)
+c.CZ(0, 1)
+c.CX(1, 2)
+c.H(1)
+c.X(0)
+c.Rx(0.7, 0)
+c.Rz(0.2, 1)
+c.X(3)
+c.H(2)
+c.qubit_create(Qubit(2))
+c.qubit_discard(Qubit(3))
+diag, bound_map = circuit_to_zx(c)
+
+in3, out3 = bound_map[Qubit(3)]
+# Qubit 3 was discarded, so out3 won't give a vertex
+# Look at the neighbour of the input to check the first operation is the X
+n = diag.neighbours(in3)[0]
+print(diag.get_vertex_ZXGen(n))
```
% Extraction is not computationally feasible for general diagrams; known to be efficient for MBQC diagrams with flow; current method permits unitary diagrams with gflow, based on Backens et al.; more methods will be written in future for different extraction methods, e.g. causal flow, MBQC, pauli flow, mixed diagram extraction
@@ -616,25 +618,25 @@ Since the {py:class}`ZXDiagram` class does not associate a {py:class}`UnitID` to
```{code-cell} ipython3
- from pytket import OpType
- from pytket.circuit.display import render_circuit_jupyter
- from pytket.passes import auto_rebase_pass
-
- c = Circuit(5)
- c.CCX(0, 1, 4)
- c.CCX(2, 4, 3)
- c.CCX(0, 1, 4)
- # Conversion is only defined for a subset of gate types - rebase as needed
- auto_rebase_pass({ OpType.Rx, OpType.Rz, OpType.X, OpType.Z, OpType.H, OpType.CZ, OpType.CX }).apply(c)
- diag, _ = circuit_to_zx(c)
-
- Rewrite.to_graphlike_form().apply(diag)
- Rewrite.reduce_graphlike_form().apply(diag)
-
- # Extraction requires the diagram to use MBQC generators
- Rewrite.to_MBQC_diag().apply(diag)
- circ, _ = diag.to_circuit()
- render_circuit_jupyter(circ)
+from pytket import OpType
+from pytket.circuit.display import render_circuit_jupyter
+from pytket.passes import auto_rebase_pass
+
+c = Circuit(5)
+c.CCX(0, 1, 4)
+c.CCX(2, 4, 3)
+c.CCX(0, 1, 4)
+# Conversion is only defined for a subset of gate types - rebase as needed
+auto_rebase_pass({ OpType.Rx, OpType.Rz, OpType.X, OpType.Z, OpType.H, OpType.CZ, OpType.CX }).apply(c)
+diag, _ = circuit_to_zx(c)
+
+Rewrite.to_graphlike_form().apply(diag)
+Rewrite.reduce_graphlike_form().apply(diag)
+
+# Extraction requires the diagram to use MBQC generators
+Rewrite.to_MBQC_diag().apply(diag)
+circ, _ = diag.to_circuit()
+render_circuit_jupyter(circ)
```
## Compiler Passes Using ZX
@@ -646,11 +648,11 @@ The known methods for circuit rewriting and optimisation lend themselves to a si
```{code-cell} ipython3
- from pytket.passes import ZXGraphlikeOptimisation
+from pytket.passes import ZXGraphlikeOptimisation
- # Use the same CCX example from above
- ZXGraphlikeOptimisation().apply(c)
- render_circuit_jupyter(c)
+# Use the same CCX example from above
+ZXGraphlikeOptimisation().apply(c)
+render_circuit_jupyter(c)
```
The specific nature of optimising circuits via ZX diagrams gives rise to some general advice regarding how to use {py:class}`ZXGraphlikeOptimisation` in compilation sequences and what to expect from its performance:
From 3c73452e5696dedf64356b26ad71e30c6dbc581b Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 14:35:39 +0100
Subject: [PATCH 10/16] fixes to compiler page
---
docs/manual/manual_compiler.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/manual/manual_compiler.md b/docs/manual/manual_compiler.md
index 18527c70..a813b4a3 100644
--- a/docs/manual/manual_compiler.md
+++ b/docs/manual/manual_compiler.md
@@ -244,7 +244,7 @@ A custom (partial) placement can be applied by providing the appropriate qubit m
```{code-cell} ipython3
from pytket.circuit import Circuit, Qubit, Node
-from pytket.placement import Placements
+from pytket.placement import Placement
circ = Circuit(4)
circ.CX(0, 1).CX(0, 2).CX(1, 2).CX(3, 2).CX(0, 3)
@@ -1128,10 +1128,10 @@ known state by classical set-bits operations on the target bits:
```{code-cell} ipython3
- c = Circuit(1).X(0).measure_all()
- c.qubit_create_all()
- SimplifyInitial().apply(c)
- print(c.get_commands())
+c = Circuit(1).X(0).measure_all()
+c.qubit_create_all()
+SimplifyInitial().apply(c)
+print(c.get_commands())
```
The measurement has disappeared, replaced with a classical operation on its
From 8855d186116939854fe584380f9e7d918dfa3701 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Thu, 29 Aug 2024 14:39:17 +0100
Subject: [PATCH 11/16] remove jupyter-sphinx dependency
---
docs/conf.py | 1 -
poetry.lock | 198 +------------------------------------------------
pyproject.toml | 1 -
3 files changed, 2 insertions(+), 198 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index efd07a46..77bc02cc 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,7 +12,6 @@
extensions = [
"sphinx.ext.intersphinx",
"sphinx.ext.mathjax",
- "jupyter_sphinx",
"sphinx_copybutton",
"sphinx.ext.autosectionlabel",
"myst_nb",
diff --git a/poetry.lock b/poetry.lock
index 5cd2ac61..e39ebb7b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "alabaster"
@@ -120,24 +120,6 @@ charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
-[[package]]
-name = "bleach"
-version = "6.1.0"
-description = "An easy safelist-based HTML-sanitizing tool."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"},
- {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"},
-]
-
-[package.dependencies]
-six = ">=1.9.0"
-webencodings = "*"
-
-[package.extras]
-css = ["tinycss2 (>=1.1.0,<1.3)"]
-
[[package]]
name = "cachetools"
version = "5.4.0"
@@ -714,17 +696,6 @@ files = [
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
-[[package]]
-name = "defusedxml"
-version = "0.7.1"
-description = "XML bomb protection for Python stdlib modules"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-files = [
- {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
- {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
-]
-
[[package]]
name = "deprecation"
version = "2.1.0"
@@ -1363,27 +1334,6 @@ qtconsole = ["qtconsole"]
test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"]
test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
-[[package]]
-name = "ipywidgets"
-version = "8.1.3"
-description = "Jupyter interactive widgets"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"},
- {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"},
-]
-
-[package.dependencies]
-comm = ">=0.1.3"
-ipython = ">=6.1.0"
-jupyterlab-widgets = ">=3.0.11,<3.1.0"
-traitlets = ">=4.3.1"
-widgetsnbextension = ">=4.0.11,<4.1.0"
-
-[package.extras]
-test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
-
[[package]]
name = "jax"
version = "0.4.30"
@@ -1595,51 +1545,6 @@ traitlets = ">=5.3"
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"]
test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"]
-[[package]]
-name = "jupyter-sphinx"
-version = "0.5.3"
-description = "Jupyter Sphinx Extensions"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "jupyter_sphinx-0.5.3-py3-none-any.whl", hash = "sha256:a67b3208d4da5b3508dbb8260d3b359ae476c36c6c642747b78a2520e5be0b05"},
- {file = "jupyter_sphinx-0.5.3.tar.gz", hash = "sha256:2e23699a3a1cf5db31b10981da5aa32606ee730f6b73a844d1e76d800756af56"},
-]
-
-[package.dependencies]
-ipykernel = ">=4.5.1"
-ipython = "*"
-ipywidgets = ">=7.0.0"
-nbconvert = ">=5.5"
-nbformat = "*"
-sphinx = ">=7"
-
-[package.extras]
-doc = ["matplotlib"]
-test = ["bash-kernel", "pytest"]
-
-[[package]]
-name = "jupyterlab-pygments"
-version = "0.3.0"
-description = "Pygments theme using JupyterLab CSS variables"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"},
- {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"},
-]
-
-[[package]]
-name = "jupyterlab-widgets"
-version = "3.0.11"
-description = "Jupyter interactive widgets for JupyterLab"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"},
- {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"},
-]
-
[[package]]
name = "kahypar"
version = "1.3.5"
@@ -2011,17 +1916,6 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
-[[package]]
-name = "mistune"
-version = "3.0.2"
-description = "A sane and fast Markdown parser with useful plugins and renderers"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"},
- {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"},
-]
-
[[package]]
name = "ml-dtypes"
version = "0.4.0"
@@ -2151,43 +2045,6 @@ dev = ["pre-commit"]
docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"]
test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"]
-[[package]]
-name = "nbconvert"
-version = "7.16.4"
-description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)."
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"},
- {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"},
-]
-
-[package.dependencies]
-beautifulsoup4 = "*"
-bleach = "!=5.0.0"
-defusedxml = "*"
-jinja2 = ">=3.0"
-jupyter-core = ">=4.7"
-jupyterlab-pygments = "*"
-markupsafe = ">=2.0"
-mistune = ">=2.0.3,<4"
-nbclient = ">=0.5.0"
-nbformat = ">=5.7"
-packaging = "*"
-pandocfilters = ">=1.4.1"
-pygments = ">=2.4.1"
-tinycss2 = "*"
-traitlets = ">=5.1"
-
-[package.extras]
-all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"]
-docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"]
-qtpdf = ["pyqtwebengine (>=5.15)"]
-qtpng = ["pyqtwebengine (>=5.15)"]
-serve = ["tornado (>=6.1)"]
-test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"]
-webpdf = ["playwright"]
-
[[package]]
name = "nbformat"
version = "5.10.4"
@@ -2444,17 +2301,6 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.9.2)"]
-[[package]]
-name = "pandocfilters"
-version = "1.5.1"
-description = "Utilities for writing pandoc filters in python"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-files = [
- {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"},
- {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"},
-]
-
[[package]]
name = "parso"
version = "0.8.4"
@@ -4201,24 +4047,6 @@ files = [
[package.extras]
widechars = ["wcwidth"]
-[[package]]
-name = "tinycss2"
-version = "1.3.0"
-description = "A tiny CSS parser"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"},
- {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"},
-]
-
-[package.dependencies]
-webencodings = ">=0.4"
-
-[package.extras]
-doc = ["sphinx", "sphinx_rtd_theme"]
-test = ["pytest", "ruff"]
-
[[package]]
name = "tomli"
version = "2.0.1"
@@ -4346,17 +4174,6 @@ files = [
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
-[[package]]
-name = "webencodings"
-version = "0.5.1"
-description = "Character encoding aliases for legacy web content"
-optional = false
-python-versions = "*"
-files = [
- {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
- {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
-]
-
[[package]]
name = "websocket-client"
version = "1.8.0"
@@ -4454,17 +4271,6 @@ files = [
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
]
-[[package]]
-name = "widgetsnbextension"
-version = "4.0.11"
-description = "Jupyter interactive widgets for Jupyter Notebook"
-optional = false
-python-versions = ">=3.7"
-files = [
- {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"},
- {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"},
-]
-
[[package]]
name = "zipp"
version = "3.19.2"
@@ -4483,4 +4289,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "757edf95621f51b3a5b49d6fcc0161d19d97ff1308ad3b3c9b69abbe7fa2fba9"
+content-hash = "dde2b274876990d5bf54b0369de0e9553e29e98cadc6d642f8ad3713df2a4c52"
diff --git a/pyproject.toml b/pyproject.toml
index 5cc1ca69..beaca2e4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,7 +17,6 @@ qiskit-algorithms = "^0.3.0"
ipykernel = "^6.29.4"
kahypar = "^1.3.5"
sphinx-copybutton = "^0.5.2"
-jupyter-sphinx = "^0.5.3"
quimb = "^1.8.1"
openfermion = "^1.6.1"
ipyparallel = "^8.8.0"
From ad08277e4175c58db1b1eb5409f49998540553e7 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Tue, 3 Sep 2024 10:54:44 +0100
Subject: [PATCH 12/16] fix formatting issues found in code review
---
docs/manual/manual_backend.md | 7 +-
docs/manual/manual_compiler.md | 23 +++--
docs/manual/manual_noise.md | 159 ++++++++++++++++-----------------
3 files changed, 89 insertions(+), 100 deletions(-)
diff --git a/docs/manual/manual_backend.md b/docs/manual/manual_backend.md
index 040c62bf..1da99701 100644
--- a/docs/manual/manual_backend.md
+++ b/docs/manual/manual_backend.md
@@ -49,9 +49,8 @@ print(sim_b.required_predicates)
```
-
- [NoClassicalControlPredicate, NoFastFeedforwardPredicate, NoMidMeasurePredicate, NoSymbolsPredicate, GateSetPredicate:{ U1 noop U2 CX Barrier Measure U3 }, DirectednessPredicate:{ Nodes: 5, Edges: 8 }]
- [NoClassicalControlPredicate, NoFastFeedforwardPredicate, GateSetPredicate:{ CU1 CZ CX Unitary2qBox Sdg U1 Unitary1qBox SWAP S U2 CCX Y U3 Z X T noop Tdg Reset H }]
+[NoClassicalControlPredicate, NoFastFeedforwardPredicate, NoMidMeasurePredicate, NoSymbolsPredicate, GateSetPredicate:{ U1 noop U2 CX Barrier Measure U3 }, DirectednessPredicate:{ Nodes: 5, Edges: 8 }]
+[NoClassicalControlPredicate, NoFastFeedforwardPredicate, GateSetPredicate:{ CU1 CZ CX Unitary2qBox Sdg U1 Unitary1qBox SWAP S U2 CCX Y U3 Z X T noop Tdg Reset H }]
```
% Can check if a circuit satisfies all requirements with `valid_circuit`
@@ -811,9 +810,7 @@ print(counts)
```
-
{(0, 1, 1): 1000}
-
```
### Result Serialization
diff --git a/docs/manual/manual_compiler.md b/docs/manual/manual_compiler.md
index a813b4a3..e32dbeac 100644
--- a/docs/manual/manual_compiler.md
+++ b/docs/manual/manual_compiler.md
@@ -29,22 +29,22 @@ Each {py:class}`~pytket.predicates.Predicate` can be constructed on its own to i
```{code-cell} ipython3
- from pytket import Circuit, OpType
- from pytket.predicates import GateSetPredicate, NoMidMeasurePredicate
+from pytket import Circuit, OpType
+from pytket.predicates import GateSetPredicate, NoMidMeasurePredicate
- circ = Circuit(2, 2)
- circ.Rx(0.2, 0).CX(0, 1).Rz(-0.7, 1).measure_all()
+circ = Circuit(2, 2)
+circ.Rx(0.2, 0).CX(0, 1).Rz(-0.7, 1).measure_all()
- gateset = GateSetPredicate({OpType.Rx, OpType.CX, OpType.Rz, OpType.Measure})
- midmeasure = NoMidMeasurePredicate()
+gateset = GateSetPredicate({OpType.Rx, OpType.CX, OpType.Rz, OpType.Measure})
+midmeasure = NoMidMeasurePredicate()
- print(gateset.verify(circ))
- print(midmeasure.verify(circ))
+print(gateset.verify(circ))
+print(midmeasure.verify(circ))
- circ.S(0)
+circ.S(0)
- print(gateset.verify(circ))
- print(midmeasure.verify(circ))
+print(gateset.verify(circ))
+print(midmeasure.verify(circ))
```
% Common predicates
@@ -1001,7 +1001,6 @@ print(r1.get_counts())
```
-
{(0, 0, 0, 0): 503, (0, 0, 0, 1): 488, (0, 1, 0, 0): 533, (0, 1, 0, 1): 493, (1, 0, 0, 0): 1041, (1, 0, 0, 1): 107, (1, 0, 1, 0): 115, (1, 0, 1, 1): 14, (1, 1, 0, 0): 576, (1, 1, 0, 1): 69, (1, 1, 1, 0): 54, (1, 1, 1, 1): 7}
{(0, 0, 0, 0): 2047, (0, 1, 0, 0): 169, (0, 1, 1, 0): 1729, (1, 1, 0, 0): 7, (1, 1, 1, 0): 48}
```
diff --git a/docs/manual/manual_noise.md b/docs/manual/manual_noise.md
index 28374c4a..ee7f8cd0 100644
--- a/docs/manual/manual_noise.md
+++ b/docs/manual/manual_noise.md
@@ -102,7 +102,6 @@ print(backend.backend_info.all_node_gate_errors[Node(1)])
```
-
{: 0.00036435993708370417,
: 0.0,
: 0.00036435993708370417,
@@ -116,14 +115,15 @@ print(backend.backend_info.all_node_gate_errors[Node(1)])
```
-```
-
+```{code-cell} ipython3
+---
+tags: [skip-execution]
+---
print(backend.backend_info.all_edge_gate_errors)
```
```
-
{(node[4], node[3]): {: 0.01175674116384029},
(node[3], node[4]): {: 0.005878370581920145},
(node[2], node[3]): {: 0.013302220876095505},
@@ -132,7 +132,6 @@ print(backend.backend_info.all_edge_gate_errors)
(node[1], node[2]): {: 0.011286042232693166},
(node[0], node[1]): {: 0.026409836177538337},
(node[1], node[0]): {: 0.013204918088769169}}
-
```
Recall that mapping in `pytket` works in two phases --
@@ -187,7 +186,6 @@ for k, v in graph_placement.items():
```
-
[(node[0], node[1]), (node[1], node[0]), (node[1], node[2]), (node[1], node[3]), (node[2], node[1]), (node[3], node[1]), (node[3], node[4]), (node[4], node[3])]
NoiseAwarePlacement mapping:
@@ -231,28 +229,28 @@ For each cycle in the circuit, each of the ensemble's operators is prepended to
```{code-cell} ipython3
- from pytket.tailoring import FrameRandomisation
- from pytket import OpType, Circuit
- from pytket.extensions.qiskit import AerBackend
-
- circ = Circuit(2).X(0).CX(0,1).S(1).measure_all()
- frame_randomisation = FrameRandomisation(
- {OpType.CX}, # Set of OpType that cycles are comprised of. For a randomised circuit, the minimum number of cycles is found such that every gate with a cycle OpType is in exactly one cycle.
- {OpType.Y}, # Set of OpType frames are constructed from
- {
- OpType.CX: {(OpType.Y, OpType.Y): (OpType.X, OpType.Z)}, # Operations to prepend and append to CX respectively such that unitary is preserved i.e. Y(0).Y(1).CX(0,1).X(0).Z(1) == CX(0,1)
- },
- )
-
- averaging_circuits = frame_randomisation.get_all_circuits(circ)
- print('For a single gate in the averaging ensemble we return a single circuit:')
- for com in averaging_circuits[0]:
- print(com)
-
- print('\nWe can check that the unitary of the circuit is preserved by comparing output counts:')
- backend = AerBackend()
- print(backend.run_circuit(circ, 100).get_counts())
- print(backend.run_circuit(averaging_circuits[0], 100).get_counts())
+from pytket.tailoring import FrameRandomisation
+from pytket import OpType, Circuit
+from pytket.extensions.qiskit import AerBackend
+
+circ = Circuit(2).X(0).CX(0,1).S(1).measure_all()
+frame_randomisation = FrameRandomisation(
+ {OpType.CX}, # Set of OpType that cycles are comprised of. For a randomised circuit, the minimum number of cycles is found such that every gate with a cycle OpType is in exactly one cycle.
+ {OpType.Y}, # Set of OpType frames are constructed from
+ {
+ OpType.CX: {(OpType.Y, OpType.Y): (OpType.X, OpType.Z)}, # Operations to prepend and append to CX respectively such that unitary is preserved i.e. Y(0).Y(1).CX(0,1).X(0).Z(1) == CX(0,1)
+ },
+)
+
+averaging_circuits = frame_randomisation.get_all_circuits(circ)
+print('For a single gate in the averaging ensemble we return a single circuit:')
+for com in averaging_circuits[0]:
+ print(com)
+
+print('\nWe can check that the unitary of the circuit is preserved by comparing output counts:')
+backend = AerBackend()
+print(backend.run_circuit(circ, 100).get_counts())
+print(backend.run_circuit(averaging_circuits[0], 100).get_counts())
```
% preset cycle and frame gates to tailor meaningful noise
@@ -266,38 +264,37 @@ The {py:meth}`PauliFrameRandomisation.get_all_circuits` method returns circuits
```{code-cell} ipython3
- from pytket import Circuit
- from pytket.extensions.qiskit import AerBackend
- from pytket.tailoring import PauliFrameRandomisation
+from pytket import Circuit
+from pytket.extensions.qiskit import AerBackend
+from pytket.tailoring import PauliFrameRandomisation
- circ = Circuit(2).X(0).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
+circ = Circuit(2).X(0).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
- pauli_frame_randomisation = PauliFrameRandomisation()
- averaging_circuits = pauli_frame_randomisation.get_all_circuits(circ)
-
- print('Number of PauliFrameRandomisation averaging circuits: ', len(averaging_circuits))
+pauli_frame_randomisation = PauliFrameRandomisation()
+averaging_circuits = pauli_frame_randomisation.get_all_circuits(circ)
- print('\nAn example averaging circuit with frames applied to two cycles: ')
- for com in averaging_circuits[3].get_commands():
- print(com)
- print('\n')
+print('Number of PauliFrameRandomisation averaging circuits: ', len(averaging_circuits))
- backend = AerBackend()
+print('\nAn example averaging circuit with frames applied to two cycles: ')
+for com in averaging_circuits[3].get_commands():
+ print(com)
+print('\n')
- averaging_circuits = backend.get_compiled_circuits(averaging_circuits)
- circ = backend.get_compiled_circuit(circ)
+backend = AerBackend()
- pfr_counts_list = [
- res.get_counts() for res in backend.run_circuits(averaging_circuits, 50)
- ]
- # combine each averaging circuits counts into a single counts object for comparison
- pfr_counts = {}
- for counts in pfr_counts_list:
- pfr_counts = {key: pfr_counts.get(key,0) + counts.get(key,0) for key in set(pfr_counts)|set(counts)}
+averaging_circuits = backend.get_compiled_circuits(averaging_circuits)
+circ = backend.get_compiled_circuit(circ)
- print(pfr_counts)
- print(backend.run_circuit(circ, 50*len(averaging_circuits)).get_counts())
+pfr_counts_list = [
+ res.get_counts() for res in backend.run_circuits(averaging_circuits, 50)
+]
+# combine each averaging circuits counts into a single counts object for comparison
+pfr_counts = {}
+for counts in pfr_counts_list:
+ pfr_counts = {key: pfr_counts.get(key,0) + counts.get(key,0) for key in set(pfr_counts)|set(counts)}
+print(pfr_counts)
+print(backend.run_circuit(circ, 50*len(averaging_circuits)).get_counts())
```
For a noise free backend, we can see that the same counts distribution is returned as expected. We can use a basic noise model based on a real device to see how a realistic noise channel can change when applying {py:class}`PauliFrameRandomisation`.
@@ -336,11 +333,9 @@ print('Recombined Noisy Counts using PauliFrameRandomisation:', pfr_counts)
```
-
Noiseless Counts: Counter({(1, 1): 6415, (1, 0): 6385})
Base Noisy Counts: Counter({(1, 0): 6368, (1, 1): 5951, (0, 1): 253, (0, 0): 228})
Recombined Noisy Counts using PauliFrameRandomisation: {(0, 1): 203, (0, 0): 215, (1, 0): 6194, (1, 1): 6188}
-
```
For this simple case we observe that more shots are returning basis states not in the expected state (though it would be unwise to declare the methods efficacy from this alone).
@@ -389,7 +384,6 @@ print('Recombined Noisy Counts using UniversalFrameRandomisation:', ufr_noisy_co
```
-
Noiseless Counts: Counter({(1, 0): 6490, (1, 1): 6310})
Recombined Noiseless Counts using UniversalFrameRandomisation: {(1, 0): 6440, (1, 1): 6360}
Base Noisy Counts: Counter({(1, 0): 6298, (1, 1): 6022, (0, 1): 261, (0, 0): 219})
@@ -498,42 +492,41 @@ First the {py:class}`SpamCorrecter` is characterised using counts results for ca
```{code-cell} ipython3
- from pytket.extensions.qiskit import AerBackend
- from pytket import Circuit
- from pytket.utils.spam import SpamCorrecter
-
- from qiskit_aer.noise import NoiseModel
- from qiskit_aer.noise.errors import depolarizing_error
+from pytket.extensions.qiskit import AerBackend
+from pytket import Circuit
+from pytket.utils.spam import SpamCorrecter
- noise_model = NoiseModel()
- noise_model.add_readout_error([[0.9, 0.1],[0.1, 0.9]], [0])
- noise_model.add_readout_error([[0.95, 0.05],[0.05, 0.95]], [1])
- noise_model.add_quantum_error(depolarizing_error(0.1, 2), ["cx"], [0, 1])
+from qiskit_aer.noise import NoiseModel
+from qiskit_aer.noise.errors import depolarizing_error
- noisy_backend = AerBackend(noise_model)
- noiseless_backend = AerBackend()
- spam_correcter = SpamCorrecter([noisy_backend.backend_info.architecture.nodes], noisy_backend)
- calibration_circuits = spam_correcter.calibration_circuits()
+noise_model = NoiseModel()
+noise_model.add_readout_error([[0.9, 0.1],[0.1, 0.9]], [0])
+noise_model.add_readout_error([[0.95, 0.05],[0.05, 0.95]], [1])
+noise_model.add_quantum_error(depolarizing_error(0.1, 2), ["cx"], [0, 1])
- char_handles = noisy_backend.process_circuits(calibration_circuits, 1000)
- char_results = noisy_backend.get_results(char_handles)
+noisy_backend = AerBackend(noise_model)
+noiseless_backend = AerBackend()
+spam_correcter = SpamCorrecter([noisy_backend.backend_info.architecture.nodes], noisy_backend)
+calibration_circuits = spam_correcter.calibration_circuits()
- spam_correcter.calculate_matrices(char_results)
+char_handles = noisy_backend.process_circuits(calibration_circuits, 1000)
+char_results = noisy_backend.get_results(char_handles)
- circ = Circuit(2).H(0).CX(0,1).measure_all()
- circ = noisy_backend.get_compiled_circuit(circ)
- noisy_handle = noisy_backend.process_circuit(circ, 1000)
- noisy_result = noisy_backend.get_result(noisy_handle)
- noiseless_handle = noiseless_backend.process_circuit(circ, 1000)
- noiseless_result = noiseless_backend.get_result(noiseless_handle)
+spam_correcter.calculate_matrices(char_results)
- circ_parallel_measure = spam_correcter.get_parallel_measure(circ)
- corrected_counts = spam_correcter.correct_counts(noisy_result, circ_parallel_measure)
+circ = Circuit(2).H(0).CX(0,1).measure_all()
+circ = noisy_backend.get_compiled_circuit(circ)
+noisy_handle = noisy_backend.process_circuit(circ, 1000)
+noisy_result = noisy_backend.get_result(noisy_handle)
+noiseless_handle = noiseless_backend.process_circuit(circ, 1000)
+noiseless_result = noiseless_backend.get_result(noiseless_handle)
- print('Noisy Counts:', noisy_result.get_counts())
- print('Corrected Counts:', corrected_counts.get_counts())
- print('Noiseless Counts:', noiseless_result.get_counts())
+circ_parallel_measure = spam_correcter.get_parallel_measure(circ)
+corrected_counts = spam_correcter.correct_counts(noisy_result, circ_parallel_measure)
+print('Noisy Counts:', noisy_result.get_counts())
+print('Corrected Counts:', corrected_counts.get_counts())
+print('Noiseless Counts:', noiseless_result.get_counts())
```
Despite the presence of additional noise, it is straightforward to see that the corrected counts results are closer to the expected noiseless counts than the original noisy counts. All that is required to use {py:class}`SpamCorrecter` with a real device is the interchange of {py:class}`~pytket.extensions.qiskit.AerBackend` with a real device backend, such as {py:class}`~pytket.extensions.qiskit.IBMQBackend`.
From c5d6557c9de072511adc0a24345eeefa31ac5842 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Tue, 3 Sep 2024 11:06:25 +0100
Subject: [PATCH 13/16] update README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 029f2f09..64029e82 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Pytket is a python module for interfacing with tket, a quantum computing toolkit
This repository contains the pytket user manual and example notebooks content in the `docs` directory. It does not contain source code for pytket itself. The source code is maintained on the [tket repository](https://github.com/CQCL/tket).
-The manual and examples are built and deployed as html pages using the [jupyter-sphinx](https://jupyter-sphinx.readthedocs.io/en/latest/) and [myst-nb](https://myst-nb.readthedocs.io/en/latest/) libraries. Instructions on building the docs locally can be found [here](https://github.com/CQCL/pytket-docs/blob/main/docs/README.md).
+The manual and examples are built and deployed as html pages using the [myst-nb](https://myst-nb.readthedocs.io/en/latest/) library. Instructions on building the docs locally can be found [here](https://github.com/CQCL/pytket-docs/blob/main/docs/README.md).
Note that the TKET website is not deployed from this repository. This repository just contains the content for the documentation.
From 0bb46c3e9b6ebf34d7eda68b6a9ed5e188724fa1 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Tue, 3 Sep 2024 11:07:33 +0100
Subject: [PATCH 14/16] test that C.I. fails on a dodgy cell
---
docs/manual/manual_circuit.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/manual/manual_circuit.md b/docs/manual/manual_circuit.md
index 7de75917..755a5b29 100644
--- a/docs/manual/manual_circuit.md
+++ b/docs/manual/manual_circuit.md
@@ -33,6 +33,8 @@ trivial_circ = Circuit() # no qubits or bits
quantum_circ = Circuit(4) # 4 qubits and no bits
mixed_circ = Circuit(4, 2) # 4 qubits and 2 bits
named_circ = Circuit(2, 2, "my_circ")
+
+named_circ.CCX(0, 1, 2)
```
## Basic Gates
From cacfa5093987c7bc430ac3f5adf8a028a61361fd Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Tue, 3 Sep 2024 11:15:59 +0100
Subject: [PATCH 15/16] remove error test from code cell
---
docs/manual/manual_circuit.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/docs/manual/manual_circuit.md b/docs/manual/manual_circuit.md
index 755a5b29..7de75917 100644
--- a/docs/manual/manual_circuit.md
+++ b/docs/manual/manual_circuit.md
@@ -33,8 +33,6 @@ trivial_circ = Circuit() # no qubits or bits
quantum_circ = Circuit(4) # 4 qubits and no bits
mixed_circ = Circuit(4, 2) # 4 qubits and 2 bits
named_circ = Circuit(2, 2, "my_circ")
-
-named_circ.CCX(0, 1, 2)
```
## Basic Gates
From 4b7448be05e9078e563f542668fafb25ca241328 Mon Sep 17 00:00:00 2001
From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com>
Date: Wed, 4 Sep 2024 10:40:59 +0100
Subject: [PATCH 16/16] fix gate_counts cell indentation
---
docs/manual/manual_circuit.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/manual/manual_circuit.md b/docs/manual/manual_circuit.md
index 7de75917..38a569f4 100644
--- a/docs/manual/manual_circuit.md
+++ b/docs/manual/manual_circuit.md
@@ -663,9 +663,9 @@ Its also possible to count all the occurrences of each {py:class}`~pytket.circui
```{code-cell} ipython3
- from pytket.utils.stats import gate_counts
+from pytket.utils.stats import gate_counts
- gate_counts(circ)
+gate_counts(circ)
```
We obtain a {py:class}`collections.Counter` object where the keys are the various {py:class}`~pytket.circuit.OpType` s and the values represent how frequently each {py:class}`~pytket.circuit.OpType` appears in our {py:class}`~pytket.circuit.Circuit`. This method summarises the gate counts obtained for the circuit shown above.