Skip to content

Commit

Permalink
Relocate equivalence checker. Add a test suite for it.
Browse files Browse the repository at this point in the history
  • Loading branch information
glassnotes committed Aug 24, 2024
1 parent 0355c07 commit 69792ff
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 53 deletions.
71 changes: 35 additions & 36 deletions ionizer/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
performs end-to-end transpilation and optimization of circuits. It calls a
number of helper transforms which can also be used individually.
While all transforms should preserve the behaviour of the circuit, they contain
a mechanism for under-the-hood equivalence checking (up to a global phase)
through the ``verify_equivalence`` flag. When set, an error will be raised if
the transpiled circuit is not equivalent to the original. This flag is ``False``
by default as equivalence is checked at the unitary matrix level. For more details, see
:func:`ionizer.utils.flag_non_equivalence`.
"""

from typing import Sequence, Callable
Expand All @@ -32,7 +39,7 @@
from pennylane.tape import QuantumTape
from pennylane.transforms.optimization.optimization_utils import find_next_gate

from .utils import are_mats_equivalent, rescale_angles, extract_gpi2_gpi_gpi2_angles
from .utils import flag_non_equivalence, rescale_angles, extract_gpi2_gpi_gpi2_angles
from .decompositions import decomp_map
from .ops import GPI, GPI2
from .identity_hunter import (
Expand All @@ -41,25 +48,6 @@
)


def _check_equivalence(tape1, tape2):
"""Check equivalence of two tapes up to a global phase.
Args:
tape1 (pennylane.QuantumTape): a quantum tape
tape2 (pennylane.QuantumTape): quantum tape to compare with ``tape1``
Raises:
ValueError if the two circuits are not equivalent.
"""
# Compute matrix representation using a consistent wire order
joint_wires = qml.wires.Wires.all_wires([tape1.wires, tape2.wires])
matrix_1 = qml.matrix(tape1, wire_order=joint_wires)
matrix_2 = qml.matrix(tape2, wire_order=joint_wires)

if not are_mats_equivalent(matrix_1, matrix_2):
raise ValueError("Quantum circuits are not equivalent after transform.")


@qml.transform
def commute_through_ms_gates(
tape: QuantumTape, direction="right", verify_equivalence=False
Expand All @@ -82,8 +70,9 @@ def commute_through_ms_gates(
tape (pennylane.QuantumTape): A quantum tape to transform.
direction (str): Either ``"right"`` (default) or ``"left"`` to indicate
the direction gates should move (from a circuit diagram perspective).
verify_equivalence (bool): Whether to perform equivalence checking of
the circuit before and after the transform.
verify_equivalence (bool): Whether to perform background equivalence
checking (up to global phase) of the circuit before and after the
transform.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -174,7 +163,7 @@ def circuit():
new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)
flag_non_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
Expand Down Expand Up @@ -203,9 +192,10 @@ def virtualize_rz_gates(tape: QuantumTape, verify_equivalence=False) -> (Sequenc
.. math:: RZ(\phi) = GPI(0) GPI(-\phi/2).
Args:
tape (pennylane.QuantumTape): A quantum tape to transform.
verify_equivalence (bool): Whether to perform equivalence checking of
the circuit before and after the transform.
tape (pennylane.QuantumTape): A quantum tape to transform
verify_equivalence (bool): Whether to perform background equivalence
checking (up to global phase) of the circuit before and after the
transform.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -321,7 +311,7 @@ def circuit():
new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)
flag_non_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
Expand Down Expand Up @@ -351,8 +341,9 @@ def single_qubit_fusion_gpi(tape: QuantumTape, verify_equivalence=False) -> (Seq
Args:
tape (pennylane.QuantumTape): A quantum tape to transform.
verify_equivalence (bool): Whether to perform equivalence checking of
the circuit before and after the transform.
verify_equivalence (bool): Whether to perform background equivalence
checking (up to global phase) of the circuit before and after the
transform.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -467,7 +458,7 @@ def circuit():
new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)
flag_non_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
Expand All @@ -486,8 +477,9 @@ def convert_to_gpi(tape: QuantumTape, exclude_list=None, verify_equivalence=Fals
tape (pennylane.QuantumTape): A quantum tape to transform.
exclude_list (list[str]): A list of names of gates to exclude from
conversion (see the ionize transform for an example).
verify_equivalence (bool): Whether to perform equivalence checking of
the circuit before and after the transform.
verify_equivalence (bool): Whether to perform background equivalence
checking (up to global phase) of the circuit before and after the
transform.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -555,7 +547,7 @@ def stop_at(op):
new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)
flag_non_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
Expand All @@ -581,10 +573,17 @@ def ionize(tape: QuantumTape, verify_equivalence=False) -> (Sequence[QuantumTape
:math:`MS` gates, and perform simplification based on a database of
circuit identities.
.. note::
When ``verify_equivalence`` is set to ``True``, equivalence checking up
to a global phase is performed with respect to the initial and final
circuit matrices only. It is not checked for intermediate transforms.
Args:
tape (pennylane.QuantumTape): A quantum tape to transform.
verify_equivalence (bool): Whether to perform equivalence checking of
the circuit before and after the transform.
verify_equivalence (bool): Whether to perform background equivalence
checking (up to global phase) of the circuit before and after the
transform.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -642,7 +641,7 @@ def stop_at(op):
new_tape = type(tape)(optimized_tape[0].operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)
flag_non_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
Expand Down
21 changes: 21 additions & 0 deletions ionizer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Utility functions.
"""

import pennylane as qml
import numpy as np
from pennylane import math

Expand Down Expand Up @@ -54,6 +55,26 @@ def are_mats_equivalent(unitary1, unitary2):
return False


def flag_non_equivalence(tape1, tape2):
"""Check equivalence of two circuits up to a global phase.
Args:
tape1 (pennylane.QuantumTape): a quantum tape
tape2 (pennylane.QuantumTape): quantum tape to compare with ``tape1``
Raises:
ValueError if the two circuits are not equivalent.
"""
# Compute matrix representation using a consistent wire order
joint_wires = qml.wires.Wires.all_wires([tape1.wires, tape2.wires])
matrix_1 = qml.matrix(tape1, wire_order=joint_wires)
matrix_2 = qml.matrix(tape2, wire_order=joint_wires)

if not are_mats_equivalent(matrix_1, matrix_2):
raise ValueError("Quantum circuits are not equivalent after transform.")



def rescale_angles(angles, renormalize_for_json=False):
r"""Rescale gate rotation angles into a fixed range.
Expand Down
50 changes: 33 additions & 17 deletions tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ def expected_qfunc():
qml.matrix(expected_tape, wire_order=range(3)),
)

def test_commutation_two_qubits_multiple_ms_left(self):
@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_commutation_two_qubits_multiple_ms_left(self, verify_equivalence):
"""Test case where gates on one of the qubits commutes in leftward direction."""

def qfunc():
Expand All @@ -195,7 +196,9 @@ def expected_qfunc():
GPI2(np.pi, wires=2)
MS(wires=[0, 2])

transformed_qfunc = partial(commute_through_ms_gates, direction="left")(qfunc)
transformed_qfunc = partial(
commute_through_ms_gates, direction="left", verify_equivalence=verify_equivalence
)(qfunc)
transformed_tape = qml.tape.make_qscript(transformed_qfunc)()
expected_tape = qml.tape.make_qscript(expected_qfunc)()

Expand Down Expand Up @@ -267,7 +270,8 @@ def qfunc():
qml.matrix(transformed_tape, wire_order=[0, 1]),
)

def test_rz_gpi_multiqubit(self):
@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_rz_gpi_multiqubit(self, verify_equivalence):
"""Test that RZ gates are virtualized on multiple qubits."""

def qfunc():
Expand All @@ -278,7 +282,9 @@ def qfunc():

tape = qml.tape.make_qscript(qfunc)()

transformed_qfunc = virtualize_rz_gates(qfunc)
transformed_qfunc = partial(virtualize_rz_gates, verify_equivalence=verify_equivalence)(
qfunc
)
transformed_tape = qml.tape.make_qscript(transformed_qfunc)()

assert len(transformed_tape.operations) == 4
Expand All @@ -291,7 +297,8 @@ def qfunc():
qml.matrix(transformed_tape, wire_order=[0, 1]),
)

def test_rz_gpi_multiqubit_multims(self):
@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_rz_gpi_multiqubit_multims(self, verify_equivalence):
"""Test that RZ gates are virtualized on multiple qubits with gates in between."""

def qfunc():
Expand All @@ -304,7 +311,9 @@ def qfunc():

tape = qml.tape.make_qscript(qfunc)()

transformed_qfunc = virtualize_rz_gates(qfunc)
transformed_qfunc = partial(virtualize_rz_gates, verify_equivalence=verify_equivalence)(
qfunc
)
transformed_tape = qml.tape.make_qscript(transformed_qfunc)()

assert len(transformed_tape.operations) == 6
Expand Down Expand Up @@ -396,7 +405,8 @@ def qfunc():
qml.matrix(transformed_tape, wire_order=range(3)),
)

def test_fusion_four_gates(self):
@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_fusion_four_gates(self, verify_equivalence):
"""Test that more than three gates get properly fused."""

def qfunc():
Expand All @@ -416,7 +426,9 @@ def qfunc():

tape = qml.tape.make_qscript(qfunc)()

transformed_qfunc = single_qubit_fusion_gpi(qfunc)
transformed_qfunc = partial(single_qubit_fusion_gpi, verify_equivalence=verify_equivalence)(
qfunc
)
transformed_tape = qml.tape.make_qscript(transformed_qfunc)()

assert len(transformed_tape.operations) == len(tape.operations) - 2
Expand All @@ -429,7 +441,8 @@ def qfunc():
class TestConvertToGPI:
"""Tests that operations on a tape are correctly converted to GPI gates."""

def test_convert_tape_to_gpi_known_gates(self):
@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_convert_tape_to_gpi_known_gates(self, verify_equivalence):
"""Test that known gates are correctly converted to GPI gates."""

def qfunc():
Expand All @@ -440,7 +453,7 @@ def qfunc():

tape = qml.tape.make_qscript(qfunc)()

transformed_qfunc = convert_to_gpi(qfunc)
transformed_qfunc = partial(convert_to_gpi, verify_equivalence=verify_equivalence)(qfunc)
transformed_tape = qml.tape.make_qscript(transformed_qfunc)()

assert all(op.name in ["GPI", "GPI2", "MS"] for op in transformed_tape.operations)
Expand All @@ -449,7 +462,8 @@ def qfunc():
qml.matrix(transformed_tape, wire_order=range(3)),
)

def test_convert_tape_to_gpi_known_gates_exclusion(self):
@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_convert_tape_to_gpi_known_gates_exclusion(self, verify_equivalence):
"""Test that known gates are correctly converted to GPI gates and
excluded gates are kept as-is."""

Expand All @@ -461,7 +475,9 @@ def qfunc():

tape = qml.tape.make_qscript(qfunc)()

transformed_qfunc = partial(convert_to_gpi, exclude_list=["RY"])(qfunc)
transformed_qfunc = partial(
convert_to_gpi, exclude_list=["RY"], verify_equivalence=verify_equivalence
)(qfunc)
transformed_tape = qml.tape.make_qscript(transformed_qfunc)()

assert all(op.name in ["GPI", "GPI2", "MS", "RY"] for op in transformed_tape.operations)
Expand Down Expand Up @@ -502,8 +518,7 @@ def qfunc():
qml.matrix(transformed_tape, wire_order=range(4)),
)

@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_ionize_qnode(self, verify_equivalence):
def test_ionize_qnode(self):
"""Test ionize transform on a QNode."""
dev = qml.device("default.qubit", wires=5)

Expand All @@ -521,7 +536,7 @@ def normal_qnode():
return qml.expval(qml.PauliX(0) @ qml.PauliY(wires=1))

@qml.qnode(dev)
@partial(ionize, verify_equivalence=verify_equivalence)
@ionize
def ionized_qnode():
quantum_function()
return qml.expval(qml.PauliX(0) @ qml.PauliY(wires=1))
Expand All @@ -545,7 +560,8 @@ def ionized_qnode():
],
)
def test_ionize_parametrized_qnode(self, params, verify_equivalence):
"""Test ionize transform on a QNode."""
"""Test ionize transform on a QNode including parametrized gates.
Verify also that equivalence is preserved."""
dev = qml.device("default.qubit", wires=5)

def quantum_function(params):
Expand All @@ -572,7 +588,7 @@ def ionized_qnode(params):

print(normal_qnode.qtape)
print(ionized_qnode.qtape)

assert all(op.name in ["GPI", "GPI2", "MS"] for op in ionized_qnode.qtape.operations)
assert are_mats_equivalent(
qml.matrix(normal_qnode, wire_order=range(4))(params),
Expand Down
Loading

0 comments on commit 69792ff

Please sign in to comment.