Circuit analysis#

This notebook will introduce the basic methods of analysis and visualization of circuits available in pytket.

It makes use of the modules pytket_qiskit and pytket_cirq for visualization; these need to be installed (with pip) in addition to pytket.

We’ll start by generating a small circuit to use as an example, and give it a name.

from pytket.circuit import Circuit, OpType
c = Circuit(4, name="example")
c.add_gate(OpType.CU1, 0.5, [0, 1])
c.H(0).X(1).Y(2).Z(3)
c.X(0).CX(1, 2).Y(1).Z(2).H(3)
c.Y(0).Z(1)
c.add_gate(OpType.CU1, 0.5, [2, 3])
c.H(2).X(3)
c.Z(0).H(1).X(2).Y(3).CX(3, 0)
[CU1(0.5) q[0], q[1]; Y q[2]; Z q[3]; H q[0]; X q[1]; H q[3]; X q[0]; CX q[1], q[2]; Y q[0]; Y q[1]; Z q[2]; Z q[0]; Z q[1]; CU1(0.5) q[2], q[3]; H q[1]; H q[2]; X q[3]; X q[2]; Y q[3]; CX q[3], q[0]; ]

Basic statistics#

From the circuit we can easily read off the number of qubits …

c.n_qubits
4

… the name …

c.name
'example'

… the overall depth of the circuit …

c.depth()
8

… and the depth by type of gate:

from pytket.circuit import OpType
c.depth_by_type(OpType.CU1), c.depth_by_type(OpType.H)
(2, 2)

This last method counts the number of instances of the specified gate type that cannot be parallelized. Notice that although there are 4 H gates in the circuit, the H-depth is 2 because pairs of them can be parallelized (as will be clear from the visualizations below).

Visualization#

There are several ways to produce useful visualizations of circuits from pytket: we can use the methods in the pytket.circuit.display class; we can use the built-in Graph class to visualize the circuit as a directed acyclic graph (DAG); we can convert the circuit to either Qiskit or Cirq and use the tools provided by those modules; or we can export to Latex.

Via the render_circuit_jupyter method#

from pytket.circuit.display import render_circuit_jupyter
render_circuit_jupyter(c)

Notice that although the render_circuit_jupyter method is the recommended way to render a circuit as jupyter cell output, one of the other methods should be used when working with scripts or python shells.

Via the Graph class#

from pytket.utils import Graph
G = Graph(c)
G.get_DAG()
_images/cc99d8b3a6a36eb35210d20413c1581cafad434b6c0cb6730cf355c324b3cfbc.svg

The small numbers (0 and 1) shown at the entry to and exit from the two-qubit gates represent “port numbers” on the gates; these allow us to track individual qubits, which may be placed in a different order on entry and exit in order to simplify the layout.

The Graph class also has methods to save this image to a file and to open it in a PDF viewer.

We can also view the qubit connectivity graph of a circuit:

G.get_qubit_graph()
_images/f6ecd20c9ea054645f046b8759e95151a71616e1e44c3aa80dad479ed04a95a1.svg

Via Qiskit#

from pytket.extensions.qiskit import tk_to_qiskit
print(tk_to_qiskit(c))
               ┌───┐┌───┐┌───┐  ┌───┐             ┌───┐
q_0: ─■────────┤ H ├┤ X ├┤ Y ├──┤ Z ├─────────────┤ X ├
      │U1(π/2) ├───┤└───┘├───┤  ├───┤   ┌───┐     └─┬─┘
q_1: ─■────────┤ X ├──■──┤ Y ├──┤ Z ├───┤ H ├───────┼──
       ┌───┐   └───┘┌─┴─┐├───┤  └───┘   ├───┤┌───┐  │  
q_2: ──┤ Y ├────────┤ X ├┤ Z ├─■────────┤ H ├┤ X ├──┼──
       ├───┤   ┌───┐└───┘└───┘ │U1(π/2) ├───┤├───┤  │  
q_3: ──┤ Z ├───┤ H ├───────────■────────┤ X ├┤ Y ├──■──
       └───┘   └───┘                    └───┘└───┘     

Via Cirq#

from pytket.extensions.cirq import tk_to_cirq
print(tk_to_cirq(c))
0: ───@───────H───X───Y───Z───────────────X───
      │                                   │
1: ───@^0.5───X───@───Y───Z───────H───────┼───
                  │                       │
2: ───Y───────────X───Z───@───────H───X───┼───
                          │               │
3: ───Z───────H───────────@^0.5───X───Y───@───

(Note that Cirq cannot represent all gates diagrammatically.)

Via Latex#

We can create a Latex document containing a diagram of the circuit using the to_latex_file() method. This uses the quantikz library. The document can be viewed on its own or the Latex can easily be copied and pasted into a larger document.

c.to_latex_file("c.tex")

Commands#

We can retrieve a list of operations comprising a circuit, each represented as a Command object:

cmds = c.get_commands()
print(cmds)
[CU1(0.5) q[0], q[1];, Y q[2];, Z q[3];, H q[0];, X q[1];, H q[3];, X q[0];, CX q[1], q[2];, Y q[0];, Y q[1];, Z q[2];, Z q[0];, Z q[1];, CU1(0.5) q[2], q[3];, H q[1];, H q[2];, X q[3];, X q[2];, Y q[3];, CX q[3], q[0];]

Each Command is defined by an operation and the qubits it operates on (as well as the classical bits and conditions, if any). For example:

cmd0 = cmds[0]
op0 = cmd0.op
print(op0)
qubits0 = cmd0.args
print(qubits0)
CU1(0.5)
[q[0], q[1]]

From the Op we can read off the string representation (in normal or Latex form), the parameters and the type:

op0.get_name()  # normal form
'CU1(0.5)'
op0.get_name(latex=True)  # Latex form
'CU1(0.5)'
op0.type, op0.params
(<OpType.CU1: 58>, [0.5])

Note that some compilation passes introduce implicit wire swaps at the end of the circuit, which are not represented in the command list. (The internal representation of the circuit as a directed acyclic graph reduces explicit permutations of qubits to implicit features of the graph.)