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

Simplify

The main functionality of PyZX is the ability to optimize quantum circuits. The main optimization methods work by converting a circuit into a ZX-diagram, simplifying this diagram, and then converting it back into a quantum circuit. This process is explained in the next section. There are also some basic optimization methods that work directly on the quantum circuit representation. This is detailed in the section Gate-level optimization.

Optimizing circuits using the ZX-calculus

PyZX allows the simplification of quantum circuits via a translation to the ZX-calculus. To demonstrate this functionality, let us generate a random circuit using pyzx.generate.CNOT_HAD_PHASE_circuit:

[30]:
import sys; sys.path.insert(0, '../..') # So that we import the local copy of pyzx if you have installed from Github
import pyzx as zx
[54]:
c = zx.generate.CNOT_HAD_PHASE_circuit(qubits=8, depth=100)
print(c.stats())
Circuit  on 8 qubits with 100 gates.
        24 is the T-count
        76 Cliffords among which
        61 2-qubit gates (61 CNOT, 0 other) and
        15 Hadamard gates.

To use the ZX-diagram simplification routines, the circuit must first be converted to a ZX-diagram:

[55]:
g = c.to_graph()
zx.draw(g)

We can now use any of the built-in simplification strategies for ZX-diagrams. The most powerful of these is pyzx.simplify.full_reduce

[56]:
zx.full_reduce(g, quiet=False) # simplifies the Graph in-place, and show the rewrite steps taken.
g.normalize() # Makes the graph more suitable for displaying
zx.draw(g) # Display the resulting diagram

spider_simp: 27. 19. 11. 9. 3.  5 iterations
id_simp: 9. 3.  2 iterations
spider_simp: 6.  1 iterations
pivot_simp: 12. 4. 2. 1. 2. 1. 1. 1.  8 iterations
lcomp_simp: 3. 1. 2.  3 iterations
id_simp: 1.  1 iterations
pivot_simp: 1.  1 iterations
pivot_gadget_simp: 2. 1.  2 iterations

This rewrite strategy implements a variant of the algorithm described in this paper. The resulting diagram most-likely does not resemble the structure of a circuit. In order to extract an equivalent circuit from the diagram, we use the function pyzx.extract.extract_circuit.

Simply use it like so:

[57]:
c_opt = zx.extract_circuit(g.copy())
zx.draw(c_opt)

For some circuits, pyzx.extract.extract_circuit can result in quite large circuits involving many CNOT gates. If one is only interested in optimizing the T-count of a circuit, the extraction stage can be skipped by using the phase-teleportation method of this paper. This applies full_reduce in such a way that only phases are moved around the circuit, and all other structure remains intact:

[58]:
g = c.to_graph()
zx.teleport_reduce(g)
c_opt = zx.Circuit.from_graph(g) # This function is able to reconstruct a Circuit from a Graph that looks sufficiently like a Circuit
zx.draw(c_opt)

Circuit equality verification

In order to verify that the simplified circuit is equal to the original, PyZX supplies two different methods. For circuits on a small number of qubits (generally less than 10) PyZX allows for the direct calculation of the linear maps that the circuits implement. These can then be checked for equality:

[59]:
zx.compare_tensors(c,c_opt) # Returns True if c and c_opt implement the same circuit (up to global phase)
[59]:
True

You can also inspect the linear map itself by calling c.to_matrix(). For larger circuits calculating the linear map directly is not feasible. For those circuits PyZX allows you to check equality of the circuits using the built-in ZX-diagram rewrite strategy. This is done by composing one circuit with the adjoint of the other and simplifying the resulting circuit. If this is reducable to the identity, this is strong evidence that the circuits indeed implement the same unitary:

[60]:
c.verify_equality(c_opt) # Returns True if full_reduce() is able to reduce the composition of the circuits to the identity.
[60]:
True

Gate-level optimization

Besides the advanced simplification strategies based on the ZX-calculus, PyZX also supplies some optimization methods that work directly on pyzx.circuit.Circuit. The most straightforward of these is pyzx.optimize.basic_optimization.

A more advanced optimization technique involves splitting up the circuit into phase polynomial subcircuits, optimizing each of these, and then resynthesising the circuit, which can be done using pyzx.optimize.phase_block_optimize.

The basic_optimization and phase_block_optimize functions are also combined into a single function full_optimize.

Architecture-aware circuit routing

The pyzx.extract.extract_circuit function does not take into account the architecture of the quantum computer that the circuit will be run on. When targeting a specific architecture it is often beneficial to resynthesize the circuit to that architecture by following the qubit connectivity when deciding which multi-qubit gates to use.

First, we need to define the specific architecture we want to target. The function pyzx.routing.create_architecture can create a number of pre-defined pyzx.routing.Architecture objects:

[61]:
# Create a 9-qubit square grid architecture
grid_arch = zx.routing.architecture.create_architecture(zx.routing.architecture.SQUARE, n_qubits = 9)

# Create a IBM qx5 architecture
ibm_arch = zx.routing.architecture.create_architecture(zx.routing.architecture.IBM_QX5)

PyZX provides a function for routing phase-polynomial circuits. These circuits are composed of CNOT, XCX, and ZPhase gates, which can be directly translated to phase gadgets in ZX. The module pyzx.generate provides a function for creating such circuits to an architecture.

[62]:
c_pp = zx.generate.phase_poly(n_qubits=9, n_phase_layers=5, cnots_per_layer=5)
zx.draw(c_pp) #original circuit with no routing

[63]:
routed_circuit = zx.routing.route_phase_poly(c_pp, grid_arch)
zx.draw(routed_circuit) #circuit using the grid_arch routing