Skip to content

Commit

Permalink
Add equivalence checking mechanism to transforms and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
glassnotes committed Aug 24, 2024
1 parent d5fcd56 commit 0355c07
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 12 deletions.
56 changes: 50 additions & 6 deletions ionizer/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from pennylane.tape import QuantumTape
from pennylane.transforms.optimization.optimization_utils import find_next_gate

from .utils import rescale_angles, extract_gpi2_gpi_gpi2_angles
from .utils import are_mats_equivalent, rescale_angles, extract_gpi2_gpi_gpi2_angles
from .decompositions import decomp_map
from .ops import GPI, GPI2
from .identity_hunter import (
Expand All @@ -41,9 +41,28 @@
)


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"
tape: QuantumTape, direction="right", verify_equivalence=False
) -> (Sequence[QuantumTape], Callable):
r"""Commute :math:`GPI` and :math:`GPI2` gates with special angle values
through :class:`~ionizer.ops.MS` gates.
Expand All @@ -63,6 +82,8 @@ 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.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -152,6 +173,9 @@ def circuit():

new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
into a result for a single ``QuantumTape``.
Expand All @@ -162,7 +186,7 @@ def null_postprocessing(results):


@qml.transform
def virtualize_rz_gates(tape: QuantumTape) -> (Sequence[QuantumTape], Callable):
def virtualize_rz_gates(tape: QuantumTape, verify_equivalence=False) -> (Sequence[QuantumTape], Callable):
r"""Apply :math:`RZ` gates virtually by adjusting the phase of adjacent
:math:`GPI` and :math:`GPI2` gates.
Expand All @@ -180,6 +204,8 @@ def virtualize_rz_gates(tape: QuantumTape) -> (Sequence[QuantumTape], Callable):
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.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -294,6 +320,9 @@ def circuit():

new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
into a result for a single ``QuantumTape``.
Expand All @@ -304,7 +333,7 @@ def null_postprocessing(results):


@qml.transform
def single_qubit_fusion_gpi(tape: QuantumTape) -> (Sequence[QuantumTape], Callable):
def single_qubit_fusion_gpi(tape: QuantumTape, verify_equivalence=False) -> (Sequence[QuantumTape], Callable):
r"""Simplify sequences of :math:`GPI` and :math:`GPI2` gates using gate
fusion and circuit identities.
Expand All @@ -322,6 +351,8 @@ def single_qubit_fusion_gpi(tape: QuantumTape) -> (Sequence[QuantumTape], Callab
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.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -435,6 +466,9 @@ def circuit():

new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
into a result for a single ``QuantumTape``.
Expand All @@ -445,13 +479,15 @@ def null_postprocessing(results):


@qml.transform
def convert_to_gpi(tape: QuantumTape, exclude_list=None) -> (Sequence[QuantumTape], Callable):
def convert_to_gpi(tape: QuantumTape, exclude_list=None, verify_equivalence=False) -> (Sequence[QuantumTape], Callable):
r"""Transpile desired gates in a circuit to trapped-ion gates.
Args:
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.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -518,6 +554,9 @@ def stop_at(op):

new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)

if verify_equivalence:
_check_equivalence(tape, new_tape)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
into a result for a single ``QuantumTape``.
Expand All @@ -528,7 +567,7 @@ def null_postprocessing(results):


@qml.transform
def ionize(tape: QuantumTape) -> (Sequence[QuantumTape], Callable):
def ionize(tape: QuantumTape, verify_equivalence=False) -> (Sequence[QuantumTape], Callable):
r"""Apply a sequence of passes to transpile and optimize a circuit
over the trapped-ion gate set :math:`GPI`, :math:`GPI2`, and :math:`MS`.
Expand All @@ -544,6 +583,8 @@ def ionize(tape: QuantumTape) -> (Sequence[QuantumTape], Callable):
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.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape],
Expand Down Expand Up @@ -600,6 +641,9 @@ 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)

def null_postprocessing(results):
"""A postprocessing function returned by a transform that only converts the batch of results
into a result for a single ``QuantumTape``.
Expand Down
18 changes: 12 additions & 6 deletions tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,8 @@ def qfunc():
class TestIonize:
"""Integration test for full ionize transform."""

def test_ionize_tape(self):
@pytest.mark.parametrize("verify_equivalence", [True, False])
def test_ionize_tape(self, verify_equivalence):
"""Test ionize transform on a single tape."""

def qfunc():
Expand All @@ -490,7 +491,7 @@ def qfunc():

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

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

print(transformed_tape.operations)
Expand All @@ -501,7 +502,8 @@ def qfunc():
qml.matrix(transformed_tape, wire_order=range(4)),
)

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

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

@qml.qnode(dev)
@ionize
@partial(ionize, verify_equivalence=verify_equivalence)
def ionized_qnode():
quantum_function()
return qml.expval(qml.PauliX(0) @ qml.PauliY(wires=1))
Expand All @@ -532,6 +534,7 @@ def ionized_qnode():
qml.matrix(ionized_qnode, wire_order=range(4))(),
)

@pytest.mark.parametrize("verify_equivalence", [True, False])
@pytest.mark.parametrize(
"params",
[
Expand All @@ -541,7 +544,7 @@ def ionized_qnode():
np.array([-0.54, 0.68, 0.11]),
],
)
def test_ionize_parametrized_qnode(self, params):
def test_ionize_parametrized_qnode(self, params, verify_equivalence):
"""Test ionize transform on a QNode."""
dev = qml.device("default.qubit", wires=5)

Expand All @@ -560,13 +563,16 @@ def normal_qnode(params):
return qml.expval(qml.PauliZ(0) @ qml.PauliX(wires=2))

@qml.qnode(dev)
@ionize
@partial(ionize, verify_equivalence=verify_equivalence)
def ionized_qnode(params):
quantum_function(params)
return qml.expval(qml.PauliZ(0) @ qml.PauliX(wires=2))

assert math.allclose(normal_qnode(params), 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

0 comments on commit 0355c07

Please sign in to comment.