From 0355c07e512f31038d38eb0f65a475f08eda4310 Mon Sep 17 00:00:00 2001 From: Olivia Di Matteo Date: Sat, 24 Aug 2024 10:24:08 -0700 Subject: [PATCH] Add equivalence checking mechanism to transforms and tests --- ionizer/transforms.py | 56 +++++++++++++++++++++++++++++++++++----- tests/test_transforms.py | 18 ++++++++----- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/ionizer/transforms.py b/ionizer/transforms.py index 2239e35..f37c232 100644 --- a/ionizer/transforms.py +++ b/ionizer/transforms.py @@ -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 ( @@ -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. @@ -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], @@ -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``. @@ -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. @@ -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], @@ -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``. @@ -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. @@ -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], @@ -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``. @@ -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], @@ -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``. @@ -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`. @@ -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], @@ -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``. diff --git a/tests/test_transforms.py b/tests/test_transforms.py index bdb6b14..20e3fa5 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -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(): @@ -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) @@ -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) @@ -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)) @@ -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", [ @@ -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) @@ -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),