{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Compilation passes\n",
"\n",
"**Download this notebook - {nb-download}`compilation_example.ipynb`**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There are numerous ways to optimize circuits in `pytket`. In this notebook we will introduce the basics of compilation passes and how to combine and apply them.
\n",
"
\n",
"We assume familiarity with the `pytket` `Circuit` class. The objective is to transform one `Circuit` into another, equivalent, `Circuit`, that:
\n",
"* satisfies the connectivity constraints of a given architecture;
\n",
"* satisfies some further user-defined constraints (such as restricted gate sets);
\n",
"* minimizes some cost function (such as CX count)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Passes"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The basic mechanism of compilation is the 'pass', which is a transform that can be applied to a circuit. There is an extensive library of passes in `pytket`, and several standard ways in which they can be combined to form new passes. For example:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from pytket.passes import DecomposeMultiQubitsCX"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"pass1 = DecomposeMultiQubitsCX()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This pass converts all multi-qubit gates into CX and single-qubit gates. So let's create a circuit containing some non-CX multi-qubit gates:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from pytket.circuit import Circuit"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[CRz(0.5) q[0], q[1]; T q[2]; CSWAP q[2], q[0], q[1]; ]"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"circ = Circuit(3)\n",
"circ.CRz(0.5, 0, 1)\n",
"circ.T(2)\n",
"circ.CSWAP(2, 0, 1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to apply a pass to a circuit, we must first create a `CompilationUnit` from it. We can think of this as a 'bridge' between the circuit and the pass. The `CompilationUnit` is constructed from the circuit; the pass is applied to the `CompilationUnit`; and the transformed circuit is extracted from the `CompilationUnit`:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"from pytket.predicates import CompilationUnit"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"cu = CompilationUnit(circ)\n",
"pass1.apply(cu)\n",
"circ1 = cu.circuit"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's have a look at the result of the transformation:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[Rz(0.25) q[1];, T q[2];, CX q[0], q[1];, Rz(3.75) q[1];, CX q[0], q[1];, CX q[1], q[0];, H q[1];, CX q[0], q[1];, Tdg q[1];, CX q[2], q[1];, T q[1];, CX q[0], q[1];, T q[0];, Tdg q[1];, CX q[2], q[1];, CX q[2], q[0];, T q[1];, Tdg q[0];, H q[1];, T q[2];, CX q[2], q[0];, CX q[1], q[0];]\n"
]
}
],
"source": [
"print(circ1.get_commands())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Predicates"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Every `CompilationUnit` has associated with it a set of 'predicates', which describe target properties that can be checked against the circuit. There are many types of predicates available in `pytket`. For example, the `GateSetPredicate` checks whether all gates in a circuit belong to a particular set:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"from pytket.predicates import GateSetPredicate\n",
"from pytket.circuit import OpType"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"pred1 = GateSetPredicate({OpType.Rz, OpType.T, OpType.Tdg, OpType.H, OpType.CX})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When we construct a `CompilationUnit`, we may pass a list of target predicates as well as the circuit:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"cu = CompilationUnit(circ, [pred1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To check whether the circuit associated to a `CompilationUnit` satisfies its target predicates, we can call the `check_all_predicates()` method:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"False"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"cu.check_all_predicates()"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pass1.apply(cu)\n",
"cu.check_all_predicates()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can also directly check whether a given circuit satisfies a given predicate, using the predicate's `verify()` method:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pred1.verify(circ1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### In-place compilation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The example above produced a new circuit, leaving the original circuit untouched. It is also possible to apply a pass to a circuit in-place:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[Rz(0.25) q[1];, T q[2];, CX q[0], q[1];, Rz(3.75) q[1];, CX q[0], q[1];, CX q[1], q[0];, H q[1];, CX q[0], q[1];, Tdg q[1];, CX q[2], q[1];, T q[1];, CX q[0], q[1];, T q[0];, Tdg q[1];, CX q[2], q[1];, CX q[2], q[0];, T q[1];, Tdg q[0];, H q[1];, T q[2];, CX q[2], q[0];, CX q[1], q[0];]\n"
]
}
],
"source": [
"DecomposeMultiQubitsCX().apply(circ)\n",
"print(circ.get_commands())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Combining passes"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There are various ways to combine the elementary passes into more complex ones.
\n",
"
\n",
"To combine several passes in sequence, we use a `SequencePass`:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"from pytket.passes import SequencePass, OptimisePhaseGadgets"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"seqpass = SequencePass([DecomposeMultiQubitsCX(), OptimisePhaseGadgets()])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This pass will apply the two transforms in succession:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[TK1(0, 0, 0.25) q[1];, TK1(0, 0, 0.5) q[2];, CX q[1], q[0];, TK1(0.5, 0.5, 0.5) q[1];, CX q[0], q[1];, TK1(0, 0, 3.75) q[1];, CX q[2], q[1];, TK1(0, 0, 0.25) q[1];, CX q[0], q[1];, TK1(0, 0, 3.75) q[1];, CX q[2], q[1];, CX q[2], q[0];, TK1(0.5, 0.5, 0.75) q[1];, TK1(0, 0, 3.75) q[0];, CX q[2], q[0];, CX q[1], q[0];]\n"
]
}
],
"source": [
"cu = CompilationUnit(circ)\n",
"seqpass.apply(cu)\n",
"circ1 = cu.circuit\n",
"print(circ1.get_commands())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `apply()` method for an elementary pass returns a boolean indicating whether or not the pass had any effect on the circuit. For a `SequencePass`, the return value indicates whether _any_ of the constituent passes had some effect.
\n",
"
\n",
"A `RepeatPass` repeatedly calls `apply()` on a pass until it returns `False`, indicating that there was no effect:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [],
"source": [
"from pytket.passes import CommuteThroughMultis, RemoveRedundancies, RepeatPass"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [],
"source": [
"seqpass = SequencePass([CommuteThroughMultis(), RemoveRedundancies()])\n",
"reppass = RepeatPass(seqpass)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This pass will repeatedly apply `CommuteThroughMultis` (which commutes single-qubit operations through multi-qubit operations where possible towards the start of the circuit) and `RemoveRedundancies` (which cancels inverse pairs, merges coaxial rotations and removes redundant gates before measurement) until neither pass has any effect on the circuit.
\n",
"
\n",
"Let's use `pytket`'s built-in visualizer to see the effect on a circuit:"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [],
"source": [
"from pytket.circuit.display import render_circuit_jupyter"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[X q[0]; Y q[1]; H q[2]; CX q[0], q[1]; Rx(1.5) q[2]; Z q[0]; Rx(1.3) q[1]; Rx(0.5) q[2]; CX q[0], q[1]; H q[2]; Rz(0.4) q[0]; H q[1]; Ry(0.53) q[0]; ]"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"circ = Circuit(3)\n",
"circ.X(0).Y(1).CX(0, 1).Z(0).Rx(1.3, 1).CX(0, 1).Rz(0.4, 0).Ry(0.53, 0).H(1).H(2).Rx(\n",
" 1.5, 2\n",
").Rx(0.5, 2).H(2)"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"