Source code for pennylane.transforms.zx.converter
# Copyright 2022 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Transforms for interacting with PyZX, framework for ZX calculus."""
# pylint: disable=too-many-return-statements,too-many-arguments
from collections import OrderedDict
from fractions import Fraction
from functools import partial
import numpy as np
import pennylane as qml
from pennylane.decomposition import gate_sets
from pennylane.decomposition.decomposition_rule import null_decomp
from pennylane.exceptions import QuantumFunctionError
from pennylane.operation import Operator
from pennylane.tape import QuantumScript
from pennylane.transforms import transform
from pennylane.wires import Wires
from .helper import _needs_pyzx
def _toffoli_clifford_t_decomp(wires):
"""Return the explicit Clifford+T decomposition of the Toffoli gate,
replacing Adjoint(T) with PhaseShift(-π/4)."""
return [
qml.Hadamard(wires=wires[2]),
qml.CNOT(wires=[wires[1], wires[2]]),
qml.PhaseShift(-np.pi / 4, wires=wires[2]),
qml.CNOT(wires=[wires[0], wires[2]]),
qml.T(wires=wires[2]),
qml.CNOT(wires=[wires[1], wires[2]]),
qml.PhaseShift(-np.pi / 4, wires=wires[2]),
qml.CNOT(wires=[wires[0], wires[2]]),
qml.T(wires=wires[2]),
qml.T(wires=wires[1]),
qml.CNOT(wires=[wires[0], wires[1]]),
qml.Hadamard(wires=wires[2]),
qml.T(wires=wires[0]),
qml.PhaseShift(-np.pi / 4, wires=wires[1]),
qml.CNOT(wires=[wires[0], wires[1]]),
]
def _ccz_clifford_t_decomp(wires):
"""Return the explicit Clifford+T decomposition of the CCZ gate,
replacing Adjoint(T) with PhaseShift(-π/4)."""
return [
qml.CNOT(wires=[wires[1], wires[2]]),
qml.PhaseShift(-np.pi / 4, wires=wires[2]),
qml.CNOT(wires=[wires[0], wires[2]]),
qml.T(wires=wires[2]),
qml.CNOT(wires=[wires[1], wires[2]]),
qml.PhaseShift(-np.pi / 4, wires=wires[2]),
qml.CNOT(wires=[wires[0], wires[2]]),
qml.T(wires=wires[2]),
qml.T(wires=wires[1]),
qml.CNOT(wires=[wires[0], wires[1]]),
qml.Hadamard(wires=wires[2]),
qml.T(wires=wires[0]),
qml.PhaseShift(-np.pi / 4, wires=wires[1]),
qml.CNOT(wires=[wires[0], wires[1]]),
qml.Hadamard(wires=wires[2]),
]
class VertexType: # pylint: disable=too-few-public-methods
"""Type of a vertex in the graph.
This class is copied from PyZX as we do not make PyZX a Pennylane requirement.
Copyright (C) 2018 - Aleks Kissinger and John van de Wetering"""
BOUNDARY = 0
Z = 1
X = 2
H_BOX = 3
class EdgeType: # pylint: disable=too-few-public-methods
"""Type of an edge in the graph.
This class is copied from PyZX as we do not make PyZX a Pennylane requirement.
Copyright (C) 2018 - Aleks Kissinger and John van de Wetering"""
SIMPLE = 1
HADAMARD = 2
[docs]
@partial(transform, is_informative=True)
@_needs_pyzx
def to_zx(tape, expand_measurements=False):
"""This transform converts a PennyLane quantum tape to a ZX-Graph in the `PyZX framework <https://pyzx.readthedocs.io/en/latest/>`_.
The graph can be optimized and transformed by well-known ZX-calculus reductions.
Args:
tape(QNode or QuantumTape or Callable or Operation): The PennyLane quantum circuit.
expand_measurements(bool): The expansion will be applied on measurements that are not in the Z-basis and
rotations will be added to the operations.
Returns:
graph (pyzx.Graph) or qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]:
The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit
will provide the ZX graph in the form of a PyZX graph.
Raises:
ModuleNotFoundError: if the required ``pyzx`` package is not installed.
**Example**
You can use the transform decorator directly on your :class:`~.QNode`, quantum function and executing it will produce a
PyZX graph. You can also use the transform directly on the :class:`~.QuantumTape`.
.. code-block:: python
import pyzx
dev = qml.device('default.qubit', wires=2)
@qml.transforms.to_zx
@qml.qnode(device=dev)
def circuit(p):
qml.RZ(p[0], wires=1),
qml.RZ(p[1], wires=1),
qml.RX(p[2], wires=0),
qml.Z(0),
qml.RZ(p[3], wires=1),
qml.X(1),
qml.CNOT(wires=[0, 1]),
qml.CNOT(wires=[1, 0]),
qml.SWAP(wires=[0, 1]),
return qml.expval(qml.Z(0) @ qml.Z(1))
params = [5 / 4 * np.pi, 3 / 4 * np.pi, 0.1, 0.3]
g = circuit(params)
>>> g
Graph(20 vertices, 23 edges)
It is now a PyZX graph and can apply function from the framework on your Graph, for example you can draw it:
>>> pyzx.draw_matplotlib(g)
<Figure size ... with 1 Axes>
Alternatively you can use the transform directly on a quantum tape and get PyZX graph.
.. code-block:: python
operations = [
qml.RZ(5 / 4 * np.pi, wires=1),
qml.RZ(3 / 4 * np.pi, wires=1),
qml.RX(0.1, wires=0),
qml.Z(0),
qml.RZ(0.3, wires=1),
qml.X(1),
qml.CNOT(wires=[0, 1]),
qml.CNOT(wires=[1, 0]),
qml.SWAP(wires=[0, 1]),
]
tape = qml.tape.QuantumTape(operations)
g = qml.transforms.to_zx(tape)
>>> g
Graph(20 vertices, 23 edges)
.. details::
:title: Usage Details
Here we give an example of how to use optimization techniques from ZX calculus to reduce the T count of a
quantum circuit and get back a PennyLane circuit.
Let's start by starting with the mod 5 4 circuit from a known benchmark `library <https://github.com/njross/optimizer>`_
the expanded circuit before optimization is the following QNode:
.. code-block:: python
dev = qml.device("default.qubit", wires=5)
@qml.transforms.to_zx
@qml.qnode(device=dev)
def mod_5_4():
qml.X(4),
qml.Hadamard(wires=4),
qml.CNOT(wires=[3, 4]),
qml.adjoint(qml.T(wires=[4])),
qml.CNOT(wires=[0, 4]),
qml.T(wires=[4]),
qml.CNOT(wires=[3, 4]),
qml.adjoint(qml.T(wires=[4])),
qml.CNOT(wires=[0, 4]),
qml.T(wires=[3]),
qml.T(wires=[4]),
qml.CNOT(wires=[0, 3]),
qml.T(wires=[0]),
qml.adjoint(qml.T(wires=[3]))
qml.CNOT(wires=[0, 3]),
qml.CNOT(wires=[3, 4]),
qml.adjoint(qml.T(wires=[4])),
qml.CNOT(wires=[2, 4]),
qml.T(wires=[4]),
qml.CNOT(wires=[3, 4]),
qml.adjoint(qml.T(wires=[4])),
qml.CNOT(wires=[2, 4]),
qml.T(wires=[3]),
qml.T(wires=[4]),
qml.CNOT(wires=[2, 3]),
qml.T(wires=[2]),
qml.adjoint(qml.T(wires=[3]))
qml.CNOT(wires=[2, 3]),
qml.Hadamard(wires=[4]),
qml.CNOT(wires=[3, 4]),
qml.Hadamard(wires=4),
qml.CNOT(wires=[2, 4]),
qml.adjoint(qml.T(wires=[4]),)
qml.CNOT(wires=[1, 4]),
qml.T(wires=[4]),
qml.CNOT(wires=[2, 4]),
qml.adjoint(qml.T(wires=[4])),
qml.CNOT(wires=[1, 4]),
qml.T(wires=[4]),
qml.T(wires=[2]),
qml.CNOT(wires=[1, 2]),
qml.T(wires=[1]),
qml.adjoint(qml.T(wires=[2]))
qml.CNOT(wires=[1, 2]),
qml.Hadamard(wires=[4]),
qml.CNOT(wires=[2, 4]),
qml.Hadamard(wires=4),
qml.CNOT(wires=[1, 4]),
qml.adjoint(qml.T(wires=[4])),
qml.CNOT(wires=[0, 4]),
qml.T(wires=[4]),
qml.CNOT(wires=[1, 4]),
qml.adjoint(qml.T(wires=[4])),
qml.CNOT(wires=[0, 4]),
qml.T(wires=[4]),
qml.T(wires=[1]),
qml.CNOT(wires=[0, 1]),
qml.T(wires=[0]),
qml.adjoint(qml.T(wires=[1])),
qml.CNOT(wires=[0, 1]),
qml.Hadamard(wires=[4]),
qml.CNOT(wires=[1, 4]),
qml.CNOT(wires=[0, 4]),
return qml.expval(qml.Z(0))
The circuit contains 63 gates; 28 :func:`qml.T` gates, 28 :func:`qml.CNOT`, 6 :func:`qml.Hadmard` and
1 :func:`qml.X`. We applied the ``qml.transforms.to_zx`` decorator in order to transform our circuit to
a ZX graph.
You can get the PyZX graph by simply calling the QNode:
>>> g = mod_5_4()
>>> pyzx.tcount(g)
28
PyZX gives multiple options for optimizing ZX graphs (:func:`pyzx.full_reduce`, :func:`pyzx.teleport_reduce`, ...).
The :func:`pyzx.full_reduce` applies all optimization passes, but the final result may not be circuit-like.
Converting back to a quantum circuit from a fully reduced graph may be difficult to impossible.
Therefore we instead recommend using :func:`pyzx.teleport_reduce`, as it preserves the circuit structure.
>>> g = pyzx.simplify.teleport_reduce(g)
>>> pyzx.tcount(g)
8
If you give a closer look, the circuit contains now 53 gates; 8 :func:`qml.T` gates, 28 :func:`qml.CNOT`, 6 :func:`qml.Hadmard` and
1 :func:`qml.X` and 10 :func:`qml.S`. We successfully reduced the T-count by 20 and have ten additional
S gates. The number of CNOT gates remained the same.
The :func:`from_zx` transform can now convert the optimized circuit back into PennyLane operations:
.. code-block:: python
tape_opt = qml.transforms.from_zx(g)
wires = qml.wires.Wires([4, 3, 0, 2, 1])
wires_map = dict(zip(tape_opt.wires, wires))
tape_opt_reorder = qml.map_wires(tape_opt, wire_map=wires_map)[0][0]
@qml.qnode(device=dev)
def mod_5_4():
for g in tape_opt_reorder:
qml.apply(g)
return qml.expval(qml.Z(0))
>>> result = mod_5_4()
>>> print(result) # doctest: +SKIP
1.0
.. note::
This function is a PennyLane adaptation to `circuit_to_graph <https://github.com/zxcalc/pyzx/blob/master/pyzx/circuit/graphparser.py#L89>`_.
It requires the `pyzx <https://pyzx.readthedocs.io/en/latest/>`_ external package to be installed.
.. note::
Prior to being added to the graph, Toffoli and CCZ gates are replaced by particular decompositions. These decompositions
are described in detail in: J. Welch, A. Bocharov, and K. Svore, “Efficient Approximation of Diagonal Unitaries over the Clifford+T Basis,”
Quantum information & computation, vol. 16, Dec. 2014, doi: 10.26421/QIC16.1-2-6.
This is necessary because Toffoli and CCZ gates are not directly supported in PyZX.
Copyright (C) 2018 - Aleks Kissinger and John van de Wetering
"""
# pylint: disable=import-outside-toplevel
import pyzx
from pyzx.circuit.gates import TargetMapper
from pyzx.graph import Graph
# Dictionary of gates (PennyLane to PyZX circuit)
# Please keep in mind to keep this in sync with the pennylane.decomposition.gate_sets.PYZX,
# and to update both if the PyZX gate spec changes.
gate_types = {
"PauliX": pyzx.circuit.gates.NOT,
"PauliY": pyzx.circuit.gates.Y,
"PauliZ": pyzx.circuit.gates.Z,
"X": pyzx.circuit.gates.NOT,
"Y": pyzx.circuit.gates.Y,
"Z": pyzx.circuit.gates.Z,
"S": pyzx.circuit.gates.S,
"T": pyzx.circuit.gates.T,
"SX": pyzx.circuit.gates.SX,
"Hadamard": pyzx.circuit.gates.HAD,
"RX": pyzx.circuit.gates.XPhase,
"RY": pyzx.circuit.gates.YPhase,
"RZ": pyzx.circuit.gates.ZPhase,
"U2": pyzx.circuit.gates.U2,
"U3": pyzx.circuit.gates.U3,
"PhaseShift": pyzx.circuit.gates.ZPhase,
"CPhase": pyzx.circuit.gates.CPhase,
"SWAP": pyzx.circuit.gates.SWAP,
"CNOT": pyzx.circuit.gates.CNOT,
"CSWAP": pyzx.circuit.gates.CSWAP,
"CY": pyzx.circuit.gates.CY,
"CZ": pyzx.circuit.gates.CZ,
"CRX": pyzx.circuit.gates.CRX,
"CRY": pyzx.circuit.gates.CRY,
"CRZ": pyzx.circuit.gates.CRZ,
"CH": pyzx.circuit.gates.CHAD,
"CCZ": pyzx.circuit.gates.CCZ,
"Toffoli": pyzx.circuit.gates.Tofolli,
}
def processing_fn(res):
# Create the graph, a qubit mapper, the classical mapper stays empty as PennyLane does not support classical bits.
graph = Graph(None)
q_mapper = TargetMapper()
c_mapper = TargetMapper()
# Map the wires to consecutive wires
consecutive_wires = Wires(range(len(res[0].wires)))
consecutive_wires_map = OrderedDict(zip(res[0].wires, consecutive_wires, strict=True))
mapped_tapes, fn = qml.map_wires(res[0], wire_map=consecutive_wires_map)
mapped_tape = fn(mapped_tapes)
inputs = []
# Create the qubits in the graph and the qubit mapper
for i in range(len(mapped_tape.wires)):
vertex = graph.add_vertex(VertexType.BOUNDARY, i, 0)
inputs.append(vertex)
q_mapper.set_prev_vertex(i, vertex)
q_mapper.set_next_row(i, 1)
q_mapper.set_qubit(i, i)
# Expand the tape to be compatible with PyZX and add rotations first for measurements
kwargs = {"gate_set": gate_sets.PYZX}
if qml.decomposition.enabled_graph():
kwargs["fixed_decomps"] = {qml.GlobalPhase: null_decomp}
[mapped_tape], _ = qml.transforms.decompose(mapped_tape, **kwargs)
if expand_measurements:
[mapped_tape], _ = qml.transforms.diagonalize_measurements(mapped_tape, to_eigvals=True)
expanded_operations = []
for op in mapped_tape.operations:
if op.name == "Toffoli":
decomp = _toffoli_clifford_t_decomp(op.wires)
expanded_operations.extend(decomp)
elif op.name == "CCZ":
decomp = _ccz_clifford_t_decomp(op.wires)
expanded_operations.extend(decomp)
else:
expanded_operations.append(op)
expanded_tape = QuantumScript(expanded_operations, mapped_tape.measurements)
_add_operations_to_graph(expanded_tape, graph, gate_types, q_mapper, c_mapper)
row = max(q_mapper.max_row(), c_mapper.max_row())
outputs = []
for mapper in (q_mapper, c_mapper):
for label in mapper.labels():
qubit = mapper.to_qubit(label)
vertex = graph.add_vertex(VertexType.BOUNDARY, qubit, row)
outputs.append(vertex)
pre_vertex = mapper.prev_vertex(label)
graph.add_edge(graph.edge(pre_vertex, vertex))
graph.set_inputs(tuple(inputs))
graph.set_outputs(tuple(outputs))
return graph
return [tape], processing_fn
@to_zx.register
def _op_to_zx(op: Operator, expand_measurements=False):
return to_zx(QuantumScript([op]), expand_measurements=expand_measurements)
def _add_operations_to_graph(tape, graph, gate_types, q_mapper, c_mapper):
"""Add the tape operation to the PyZX graph."""
# Create graph from circuit in the quantum tape (operations, measurements)
for op in tape.operations:
# Check that the gate is compatible with PyZX
name = op.name
if name not in gate_types:
raise QuantumFunctionError(
"The expansion of the quantum tape failed, PyZX does not support", name
)
# Apply wires and parameters
map_gate = gate_types[name]
args = [*op.wires, *(Fraction(p / np.pi).limit_denominator() for p in op.parameters)]
gate = map_gate(*args)
gate.to_graph(graph, q_mapper, c_mapper)
[docs]
def from_zx(graph, decompose_phases=True):
"""Converts a graph from `PyZX <https://pyzx.readthedocs.io/en/latest/>`_ to a PennyLane tape, if the graph is
diagram-like.
Args:
graph (Graph): ZX graph in PyZX.
decompose_phases (bool): If True the phases are decomposed, meaning that :func:`qml.RZ` and :func:`qml.RX` are
simplified into other gates (e.g. :func:`qml.T`, :func:`qml.S`, ...).
**Example**
From the example for the :func:`~.to_zx` function, one can convert back the PyZX graph to a PennyLane by using the
function :func:`~.from_zx`.
.. code-block:: python
dev = qml.device('default.qubit', wires=2)
@qml.transforms.to_zx
def circuit(p):
qml.RZ(p[0], wires=0),
qml.RZ(p[1], wires=0),
qml.RX(p[2], wires=1),
qml.Z(1),
qml.RZ(p[3], wires=0),
qml.X(0),
qml.CNOT(wires=[1, 0]),
qml.CNOT(wires=[0, 1]),
qml.SWAP(wires=[1, 0]),
return qml.expval(qml.Z(0) @ qml.Z(1))
params = [5 / 4 * np.pi, 3 / 4 * np.pi, 0.1, 0.3]
g = circuit(params)
pennylane_tape = qml.transforms.from_zx(g)
You can check that the operations are similar but some were decomposed in the process.
>>> ops = pennylane_tape.operations
>>> from pprint import pprint
>>> pprint(ops)
[Z(0),
T(0),
RX(0.10..., wires=[1]),
Z(0),
Adjoint(T(0)),
Z(1),
RZ(0.30..., wires=[0]),
X(0),
CNOT(wires=[1, 0]),
CNOT(wires=[0, 1]),
CNOT(wires=[1, 0]),
CNOT(wires=[0, 1]),
CNOT(wires=[1, 0])]
.. warning::
Be careful because not all graphs are circuit-like, so the process might not be successful
after you apply some optimization on your PyZX graph. You can extract a circuit by using the dedicated
PyZX function.
.. note::
It is a PennyLane adapted and reworked `graph_to_circuit <https://github.com/Quantomatic/pyzx/blob/master/pyzx/circuit/graphparser.py>`_
function.
Copyright (C) 2018 - Aleks Kissinger and John van de Wetering
"""
# List of PennyLane operations
operations = []
qubits = graph.qubits()
graph_rows = graph.rows()
types = graph.types()
# Parameters are phases in the ZX framework
params = graph.phases()
rows = {}
inputs = graph.inputs()
# Set up the rows dictionary
for vertex in graph.vertices():
if vertex in inputs:
continue
row_index = graph.row(vertex)
if row_index in rows:
rows[row_index].append(vertex)
else:
rows[row_index] = [vertex]
for row_key in sorted(rows.keys()):
for vertex in rows[row_key]:
qubit_1 = qubits[vertex]
param = params[vertex]
type_1 = types[vertex]
neighbors = [w for w in graph.neighbors(vertex) if graph_rows[w] < row_key]
# The graph is not diagram like.
if len(neighbors) != 1:
raise QuantumFunctionError(
"Graph doesn't seem circuit like: multiple parents. Try to use the PyZX function `extract_circuit`."
)
neighbor_0 = neighbors[0]
if qubits[neighbor_0] != qubit_1:
raise QuantumFunctionError(
"Cross qubit connections, the graph is not circuit-like."
)
# Add Hadamard gate (written in the edge)
if graph.edge_type(graph.edge(neighbor_0, vertex)) == EdgeType.HADAMARD:
operations.append(qml.Hadamard(wires=qubit_1))
# Vertex is a boundary
if type_1 == VertexType.BOUNDARY:
continue
# Add the one qubits gate
operations.extend(_add_one_qubit_gate(param, type_1, qubit_1, decompose_phases))
# Given the neighbors on the same rowadd two qubits gates
neighbors = [
w for w in graph.neighbors(vertex) if graph_rows[w] == row_key and w < vertex
]
for neighbor in neighbors:
type_2 = types[neighbor]
qubit_2 = qubits[neighbor]
operations.extend(
_add_two_qubit_gates(graph, vertex, neighbor, type_1, type_2, qubit_1, qubit_2)
)
return QuantumScript(operations)
def _add_one_qubit_gate(param, type_1, qubit_1, decompose_phases):
"""Return the list of one qubit gates, that will be added to the tape."""
if decompose_phases:
type_z = type_1 == VertexType.Z
if type_z and param.denominator == 2:
op = qml.adjoint(qml.S(wires=qubit_1)) if param.numerator == 3 else qml.S(wires=qubit_1)
return [op]
if type_z and param.denominator == 4:
if param.numerator in (1, 7):
op = (
qml.adjoint(qml.T(wires=qubit_1))
if param.numerator == 7
else qml.T(wires=qubit_1)
)
return [op]
if param.numerator in (3, 5):
op1 = qml.Z(qubit_1)
op2 = (
qml.adjoint(qml.T(wires=qubit_1))
if param.numerator == 3
else qml.T(wires=qubit_1)
)
return [op1, op2]
if param == 1:
op = qml.Z(qubit_1) if type_1 == VertexType.Z else qml.X(qubit_1)
return [op]
if param != 0:
scaled_param = np.pi * float(param)
op_class = qml.RZ if type_1 == VertexType.Z else qml.RX
return [op_class(scaled_param, wires=qubit_1)]
# Phases are not decomposed
if param != 0:
scaled_param = np.pi * float(param)
op_class = qml.RZ if type_1 == VertexType.Z else qml.RX
return [op_class(scaled_param, wires=qubit_1)]
# No gate is added
return []
def _add_two_qubit_gates(graph, vertex, neighbor, type_1, type_2, qubit_1, qubit_2):
"""Return the list of two qubit gates giveeen the vertex and its neighbor."""
if type_1 == type_2:
if graph.edge_type(graph.edge(vertex, neighbor)) != EdgeType.HADAMARD:
raise QuantumFunctionError(
"Two green or respectively two red nodes connected by a simple edge does not have a "
"circuit representation."
)
if type_1 == VertexType.Z:
op = qml.CZ(wires=[qubit_2, qubit_1])
return [op]
op_1 = qml.Hadamard(wires=qubit_2)
op_2 = qml.CNOT(wires=[qubit_2, qubit_1])
op_3 = qml.Hadamard(wires=qubit_2)
return [op_1, op_2, op_3]
if graph.edge_type(graph.edge(vertex, neighbor)) != EdgeType.SIMPLE:
raise QuantumFunctionError(
"A green and red node connected by a Hadamard edge does not have a circuit representation."
)
# Type1 is always of type Z therefore the qubits are already ordered.
op = qml.CNOT(wires=[qubit_1, qubit_2])
return [op]
_modules/pennylane/transforms/zx/converter
Download Python script
Download Notebook
View on GitHub