This page was generated from notebooks/gettingstarted.ipynb [download].

Getting Started

We start by importing the library and configuring matplotlib to display the figures in a nice way.

[1]:
import sys; sys.path.insert(0, '../..') # So that we import the local copy of pyzx if you have installed from Github
import random
import pyzx as zx

Then we can create a new random Clifford circuit and visualise it.

[3]:
qubit_amount = 4
depth = 15
random.seed(1337)
circ = zx.generate.cliffords(qubit_amount, depth)
zx.draw_matplotlib(circ,labels=True,h_edge_draw='box')
[3]:
../_images/notebooks_gettingstarted_3_0.svg

The green and red nodes represent Z- and X-phase gates respectively, the yellow boxes are Hadamard gates, and the vertical lines going between two different colored nodes are CNOT gates.

Internally this circuit is represented as a graph:

[4]:
print(circ)
print("All edges: ", list(circ.edges()))
print("\nThe neighbors of a particular vertex: ", list(circ.neighbors(14)))
Graph(46 vertices, 49 edges)
All edges:  [(0, 4), (1, 5), (2, 6), (3, 7), (4, 13), (5, 11), (6, 8), (7, 9), (8, 10), (9, 12), (10, 14), (11, 18), (12, 15), (13, 19), (14, 15), (14, 17), (15, 16), (16, 17), (16, 24), (17, 20), (18, 19), (18, 21), (19, 23), (20, 22), (21, 29), (22, 25), (23, 28), (24, 27), (25, 26), (26, 31), (27, 33), (28, 29), (28, 30), (29, 35), (30, 31), (30, 37), (31, 32), (32, 33), (32, 34), (33, 41), (34, 36), (35, 39), (36, 37), (36, 40), (37, 38), (38, 42), (39, 43), (40, 44), (41, 45)]

The neighbors of a particular vertex:  [15, 10, 17]

Using this graph representation we can use the rules of the ZX-calculus to simplify it:

[5]:
g = circ.copy()
zx.clifford_simp(g)
[5]:
1
[6]:
g.normalize() # Reposition nodes horizontally to look nicer
zx.draw(g) # This draw function by default uses the D3 Javascript library to draw the diagram. If it doesn't work, use draw_matplotlib() instead

The blue lines represent edges that have a Hadamard gate on them.

Let’s try to see in a bit more detail what goes into rewriting this circuit.

[7]:
from ipywidgets import widgets
from IPython.display import display, Markdown

from pyzx.simplify import clifford_iter

graph = circ.copy()
graphs = [zx.draw_matplotlib(graph)]
names = ["start"]
for gs, n in clifford_iter(graph):
    graphs.append(zx.draw_matplotlib(gs))
    names.append(n)
zx.drawing.pack_circuit_nf(gs,'grg')
graphs.append(zx.draw_matplotlib(gs))
names.append("reposition")

def plotter(rewrite):
    display(Markdown("Rewrite step: " + names[rewrite]))
    display(graphs[rewrite])

w = widgets.interactive(plotter, rewrite=(0,len(graphs)-1))
slider = w.children[0]
slider.layout.width = "{!s}px".format(min(800,50*len(graphs)))
output = w.children[-1]
output.layout.height = "{!s}px".format(200+qubit_amount*20)
slider.value = 0
w

As you can see, the first few steps are spider fusion, where adjacent red nodes and adjacent green nodes are fused together (and their phases added). The next step is to convert all red nodes to green nodes by changing regular edges to hadamard edges.

Now come the steps that do most of the work. These steps are a round of pivots followed by local complementations and then a round of pivots again. The local complementation rules looks for green nodes with a phase of \(\frac\pi2\) or \(\frac{3\pi}2\). By doing a local complementation on this node we can remove the node from the graph. This process is done until no more suitable candidates are found. A pivot consists of a series of three local complementations that is done on two neighboring vertices that have a zero or \(\pi\) phase. A pivot move allows us to remove these two nodes.

Finally we do a round of identity simplification which removes phaseless nodes that are connected to precisely two neighbors.

Note: Depending on the circuit that was generated, any of these steps might not have been done.

Even though this graph is a lot compacter than the one we started out with, it no longer looks like a circuit. To fix this we need to be clever and extract a circuit from the ZX-diagram:

[8]:
circ2 = g.copy()
circ2.normalize()
circ2 = zx.extract_circuit(circ2)
zx.draw(circ2)

To verify that this circuit is still equal to the original circuit, we can transform them into numpy tensors and compare these tensors for equality:

[9]:
t1 = circ.to_tensor()
t2 = circ2.to_tensor()
# This checks whether t1 and t2 are equal up to some number: t1 == z*t2 for some complex number z
zx.compare_tensors(t1,t2,preserve_scalar=False)
[9]:
True

We can represent the circuit in one of several quantum circuit description languages, such as that of QASM:

[10]:
print(circ2.to_qasm())
OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
cx q[2], q[3];
cx q[3], q[2];
cx q[2], q[3];
cx q[0], q[1];
cx q[1], q[0];
cx q[0], q[1];
h q[2];
h q[0];
cz q[0], q[1];
h q[1];
cz q[1], q[2];
cz q[0], q[2];
rz(0.5*pi) q[2];
h q[2];
cz q[2], q[3];
cz q[1], q[3];
cz q[0], q[3];
rz(1.0*pi) q[0];
h q[3];
h q[0];
cz q[2], q[3];
cz q[0], q[2];
cz q[0], q[1];
rz(1.0*pi) q[3];
h q[3];
rz(1.0*pi) q[2];
rz(1.0*pi) q[1];
h q[1];
rz(0.5*pi) q[0];
h q[0];

Now lets try the same thing with a more complicated Clifford+T circuit.

[11]:
qubit_amount = 6
depth = 70
random.seed(1338)
circ = zx.generate.cliffordT(qubit_amount, depth,p_t=0.2)
zx.draw(circ)
[12]:
g = circ.copy()
zx.clifford_simp(g, quiet=True)
g.normalize()
zx.draw(g)

Again, let us extract a circuit from this diagram:

[13]:
g2 = g.copy()
c = zx.extract_circuit(g2)
print(c.stats())
Circuit  on 6 qubits with 62 gates.
        8 is the T-count
        54 Cliffords among which
        28 2-qubit gates (2 CNOT, 26 other) and
        22 Hadamard gates.

We can convert this back into a PyZX-graph:

[14]:
zx.draw(c.to_graph())

And verify that it is still equal to the original graph:

[15]:
# We can just feed the Circuit objects directly to compare_tensors
zx.compare_tensors(c, circ)
[15]:
True

Now lets represent this circuit in the QASM circuit description language:

[ ]:
print(c.to_basic_gates().to_qasm())