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-statements, too-many-branches, too-many-return-statements, too-many-arguments

from functools import partial
from typing import Sequence, Callable
from collections import OrderedDict
import numpy as np
import pennylane as qml
from pennylane.tape import QuantumScript, QuantumTape
from pennylane.transforms.op_transforms import OperationTransformError
from pennylane.transforms import transform
from pennylane.wires import Wires


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]def to_zx(tape, expand_measurements=False): # pylint: disable=unused-argument """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. **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.PauliZ(wires=0), qml.RZ(p[3], wires=1), qml.PauliX(wires=1), qml.CNOT(wires=[0, 1]), qml.CNOT(wires=[1, 0]), qml.SWAP(wires=[0, 1]), return qml.expval(qml.PauliZ(0) @ qml.PauliZ(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 800x200 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.PauliZ(wires=0), qml.RZ(0.3, wires=1), qml.PauliX(wires=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.PauliX(wires=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.PauliZ(wires=0)) The circuit contains 63 gates; 28 :func:`qml.T` gates, 28 :func:`qml.CNOT`, 6 :func:`qml.Hadmard` and 1 :func:`qml.PauliX`. 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.PauliX` 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)) tapes_opt_reorder, fn = qml.map_wires(input=tape_opt, wire_map=wires_map)[0][0] tape_opt_reorder = fn(tapes_opt_reorder) @qml.qnode(device=dev) def mod_5_4(): for g in tape_opt_reorder: qml.apply(g) return qml.expval(qml.PauliZ(wires=0)) >>> mod_5_4() tensor(1., requires_grad=True) .. note:: It is a PennyLane adapted and reworked `circuit_to_graph <https://github.com/Quantomatic/pyzx/blob/master/pyzx/circuit/graphparser.py>`_ function. Copyright (C) 2018 - Aleks Kissinger and John van de Wetering """ # If it is a simple operation just transform it to a tape if not isinstance(tape, qml.operation.Operator): if not isinstance(tape, (qml.tape.QuantumScript, qml.QNode)) and not callable(tape): raise OperationTransformError( "Input is not an Operator, tape, QNode, or quantum function" ) return _to_zx_transform(tape, expand_measurements=expand_measurements) return to_zx(QuantumScript([tape]))
@partial(transform, is_informative=True) def _to_zx_transform( tape: QuantumTape, expand_measurements=False ) -> (Sequence[QuantumTape], Callable): """Private function to convert a PennyLane tape to a `PyZX graph <https://pyzx.readthedocs.io/en/latest/>`_ .""" # Avoid to make PyZX a requirement for PennyLane. try: # pylint: disable=import-outside-toplevel import pyzx from pyzx.circuit.gates import TargetMapper from pyzx.graph import Graph except ImportError as Error: raise ImportError( "This feature requires PyZX. It can be installed with: pip install pyzx" ) from Error # Dictionary of gates (PennyLane to PyZX circuit) gate_types = { "PauliX": pyzx.circuit.gates.NOT, "PauliZ": pyzx.circuit.gates.Z, "S": pyzx.circuit.gates.S, "T": pyzx.circuit.gates.T, "Hadamard": pyzx.circuit.gates.HAD, "RX": pyzx.circuit.gates.XPhase, "RZ": pyzx.circuit.gates.ZPhase, "PhaseShift": pyzx.circuit.gates.ZPhase, "SWAP": pyzx.circuit.gates.SWAP, "CNOT": pyzx.circuit.gates.CNOT, "CZ": pyzx.circuit.gates.CZ, "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)) mapped_tapes, fn = qml.map_wires(input=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 stop_crit = qml.BooleanFn(lambda obj: obj.name in gate_types) mapped_tape = qml.tape.tape.expand_tape( mapped_tape, depth=10, stop_at=stop_crit, expand_measurements=expand_measurements ) expanded_operations = [] # Define specific decompositions for op in mapped_tape.operations: if op.name == "RY": theta = op.data[0] decomp = [ qml.RX(np.pi / 2, wires=op.wires), qml.RZ(theta + np.pi, wires=op.wires), qml.RX(np.pi / 2, wires=op.wires), qml.RZ(3 * np.pi, wires=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 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 qml.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, *(p / np.pi 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 import pyzx 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.PauliZ(wires=1), qml.RZ(p[3], wires=0), qml.PauliX(wires=0), qml.CNOT(wires=[1, 0]), qml.CNOT(wires=[0, 1]), qml.SWAP(wires=[1, 0]), return qml.expval(qml.PauliZ(0) @ qml.PauliZ(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. >>> pennylane_tape.operations [PauliZ(wires=[0]), T(wires=[0]), RX(0.1, wires=[1]), PauliZ(wires=[0]), Adjoint(T(wires=[0])), PauliZ(wires=[1]), RZ(0.3, wires=[0]), PauliX(wires=[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 qml.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 qml.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.PauliZ(wires=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.PauliZ(wires=qubit_1) if type_1 == VertexType.Z else qml.PauliX(wires=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 qml.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 qml.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]