From 2b68d1fc356b98b288b31d4a8d38c217d4be389a Mon Sep 17 00:00:00 2001 From: Juan Boschero Date: Fri, 23 Feb 2024 17:35:54 +0100 Subject: [PATCH] Add check for hybrid Control/Matrix Gate classes + test - Configured equivalence of gates from subclass to base class - Renamed get_matrix to get_matrix_after_qubit_remapping and moved function to matrix_expander - Added tests to check equivalence between MatrixGate and ControlGate objects --- .gitignore | 1 + opensquirrel/replacer.py | 18 +---- opensquirrel/squirrel_ir.py | 43 +++++++---- opensquirrel/utils/__init__.py | 3 + opensquirrel/utils/matrix_expander.py | 15 +++- test/test_squirrel_ir.py | 101 ++++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 31 deletions(-) create mode 100644 test/test_squirrel_ir.py diff --git a/.gitignore b/.gitignore index 68bc17f9..7d1f34c7 100644 --- a/.gitignore +++ b/.gitignore @@ -153,6 +153,7 @@ dmypy.json cython_debug/ # PyCharm +.idea/ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear diff --git a/opensquirrel/replacer.py b/opensquirrel/replacer.py index d2564b93..51537fdf 100644 --- a/opensquirrel/replacer.py +++ b/opensquirrel/replacer.py @@ -1,7 +1,6 @@ from abc import ABC, abstractmethod from typing import Callable, List -from opensquirrel.circuit_matrix_calculator import get_circuit_matrix from opensquirrel.common import are_matrices_equivalent_up_to_global_phase from opensquirrel.squirrel_ir import ( BlochSphereRotation, @@ -43,17 +42,6 @@ def visit_controlled_gate(self, controlled_gate: ControlledGate): return result -def get_replacement_matrix(replacement: List[Gate], qubit_mappings): - replacement_ir = SquirrelIR(number_of_qubits=len(qubit_mappings), qubit_register_name="q_temp") - qubit_remapper = _QubitReIndexer(qubit_mappings) - - for gate in replacement: - gate_with_remapped_qubits = gate.accept(qubit_remapper) - replacement_ir.add_gate(gate_with_remapped_qubits) - - return get_circuit_matrix(replacement_ir) - - def check_valid_replacement(statement, replacement): expected_qubit_operands = statement.get_qubit_operands() replacement_qubit_operands = set() @@ -62,8 +50,10 @@ def check_valid_replacement(statement, replacement): if set(expected_qubit_operands) != replacement_qubit_operands: raise Exception(f"Replacement for gate {statement.name} does not seem to operate on the right qubits") - replacement_matrix = get_replacement_matrix(replacement, expected_qubit_operands) - replaced_matrix = get_replacement_matrix([statement], expected_qubit_operands) + from opensquirrel.utils.matrix_expander import get_matrix_after_qubit_remapping + + replacement_matrix = get_matrix_after_qubit_remapping(replacement, expected_qubit_operands) + replaced_matrix = get_matrix_after_qubit_remapping([statement], expected_qubit_operands) if not are_matrices_equivalent_up_to_global_phase(replacement_matrix, replaced_matrix): raise Exception(f"Replacement for gate {statement.name} does not preserve the quantum state") diff --git a/opensquirrel/squirrel_ir.py b/opensquirrel/squirrel_ir.py index 25ebd4bc..0cf07c9e 100644 --- a/opensquirrel/squirrel_ir.py +++ b/opensquirrel/squirrel_ir.py @@ -4,11 +4,19 @@ from dataclasses import dataclass from functools import wraps from math import cos, sin -from typing import Callable, List, Optional, Tuple +from typing import Callable, List, Optional, Tuple, Union import numpy as np -from opensquirrel.common import ATOL, X, Y, Z, normalize_angle, normalize_axis +from opensquirrel.common import ( + ATOL, + X, + Y, + Z, + are_matrices_equivalent_up_to_global_phase, + normalize_angle, + normalize_axis, +) class SquirrelIRVisitor(ABC): @@ -90,6 +98,11 @@ def __init__(self, generator, arguments): self.generator = generator self.arguments = arguments + def __eq__(self, other): + if not isinstance(other, Gate): + return False + return _compare_gate_classes(self, other) + @property def name(self) -> Optional[str]: return self.generator.__name__ if self.generator else "" @@ -171,12 +184,6 @@ def __init__(self, matrix: np.ndarray, operands: List[Qubit], generator=None, ar self.matrix = matrix self.operands = operands - def __eq__(self, other): - # TODO: Determine whether we shall allow for a global phase difference here. - if not isinstance(other, MatrixGate): - return False # FIXME: a MatrixGate can hide a ControlledGate. https://github.com/QuTech-Delft/OpenSquirrel/issues/88 - return np.allclose(self.matrix, other.matrix) - def __repr__(self): return f"MatrixGate(qubits={self.operands}, matrix={self.matrix})" @@ -199,14 +206,6 @@ def __init__(self, control_qubit: Qubit, target_gate: Gate, generator=None, argu self.control_qubit = control_qubit self.target_gate = target_gate - def __eq__(self, other): - if not isinstance(other, ControlledGate): - return False # FIXME: a MatrixGate can hide a ControlledGate. https://github.com/QuTech-Delft/OpenSquirrel/issues/88 - if self.control_qubit != other.control_qubit: - return False - - return self.target_gate == other.target_gate - def __repr__(self): return f"ControlledGate(control_qubit={self.control_qubit}, {self.target_gate})" @@ -221,6 +220,18 @@ def is_identity(self) -> bool: return self.target_gate.is_identity() +def _compare_gate_classes(g1: Gate, g2: Gate) -> bool: + + union_mapping = list(set(g1.get_qubit_operands()) | set(g2.get_qubit_operands())) + + from opensquirrel.utils.matrix_expander import get_matrix_after_qubit_remapping + + matrix_g1 = get_matrix_after_qubit_remapping([g1], union_mapping) + matrix_g2 = get_matrix_after_qubit_remapping([g2], union_mapping) + + return are_matrices_equivalent_up_to_global_phase(matrix_g1, matrix_g2) + + def named_gate(gate_generator: Callable[..., Gate]) -> Callable[..., Gate]: @wraps(gate_generator) def wrapper(*args, **kwargs): diff --git a/opensquirrel/utils/__init__.py b/opensquirrel/utils/__init__.py index e69de29b..c81ad40d 100644 --- a/opensquirrel/utils/__init__.py +++ b/opensquirrel/utils/__init__.py @@ -0,0 +1,3 @@ +from opensquirrel.utils.matrix_expander import get_matrix, get_matrix_after_qubit_remapping + +__all__ = ["get_matrix", "get_matrix_after_qubit_remapping"] diff --git a/opensquirrel/utils/matrix_expander.py b/opensquirrel/utils/matrix_expander.py index 44f2eb4f..650ced0f 100644 --- a/opensquirrel/utils/matrix_expander.py +++ b/opensquirrel/utils/matrix_expander.py @@ -4,7 +4,8 @@ import numpy as np from opensquirrel.common import can1 -from opensquirrel.squirrel_ir import BlochSphereRotation, ControlledGate, Gate, Qubit, SquirrelIRVisitor +from opensquirrel.replacer import _QubitReIndexer +from opensquirrel.squirrel_ir import BlochSphereRotation, ControlledGate, Gate, Qubit, SquirrelIR, SquirrelIRVisitor def get_reduced_ket(ket: int, qubits: List[Qubit]) -> int: @@ -183,3 +184,15 @@ def get_matrix(gate: Gate, number_of_qubits: int) -> np.ndarray: expander = MatrixExpander(number_of_qubits) return gate.accept(expander) + + +def get_matrix_after_qubit_remapping(replacement: List[Gate], qubit_mappings: List[Qubit]): + from opensquirrel.circuit_matrix_calculator import get_circuit_matrix + + replacement_ir = SquirrelIR(number_of_qubits=len(qubit_mappings), qubit_register_name="q_temp") + qubit_remapper = _QubitReIndexer(qubit_mappings) + for gate in replacement: + gate_with_remapped_qubits = gate.accept(qubit_remapper) + replacement_ir.add_gate(gate_with_remapped_qubits) + + return get_circuit_matrix(replacement_ir) diff --git a/test/test_squirrel_ir.py b/test/test_squirrel_ir.py new file mode 100644 index 00000000..1ad7b64d --- /dev/null +++ b/test/test_squirrel_ir.py @@ -0,0 +1,101 @@ +import unittest + +import numpy as np + +from opensquirrel.default_gates import * + + +class SquirrelIRTest(unittest.TestCase): + def test_cnot_equality(self): + cnot_matrix_gate = MatrixGate( + np.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + ] + ), + operands=[Qubit(4), Qubit(100)], + ) + + cnot_controlled_gate = ControlledGate( + Qubit(4), BlochSphereRotation(qubit=Qubit(100), axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2) + ) + + self.assertTrue(cnot_controlled_gate == cnot_matrix_gate) + + def test_cnot_inequality(self): + swap_matrix_gate = MatrixGate( + np.array( + [ + [1, 0, 0, 0], + [0, 0, 1, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + ] + ), + operands=[Qubit(4), Qubit(100)], + ) + + cnot_controlled_gate = ControlledGate( + Qubit(4), BlochSphereRotation(qubit=Qubit(100), axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2) + ) + + self.assertFalse(cnot_controlled_gate == swap_matrix_gate) + + def test_different_qubits_gate(self): + large_identity_matrix_gate = MatrixGate( + np.array( + [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ] + ), + operands=[Qubit(0), Qubit(2)], + ) + small_identity_control_gate = ControlledGate( + Qubit(4), BlochSphereRotation(qubit=Qubit(2), axis=(1, 0, 0), angle=0, phase=0) + ) + + self.assertTrue(large_identity_matrix_gate == small_identity_control_gate) + + def test_inverse_gate(self): + inverted_matrix_gate = MatrixGate( + np.array( + [ + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + [0, 1, 0, 0], + ] + ), + operands=[Qubit(0), Qubit(1)], + ) + + inverted_cnot_gate = ControlledGate( + Qubit(1), BlochSphereRotation(qubit=Qubit(0), axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2) + ) + + self.assertTrue(inverted_matrix_gate == inverted_cnot_gate) + + def test_global_phase(self): + inverted_matrix_with_phase = MatrixGate( + np.array( + [ + [1j, 0, 0, 0], + [0, 0, 0, 1j], + [0, 0, 1j, 0], + [0, 1j, 0, 0], + ] + ), + operands=[Qubit(0), Qubit(1)], + ) + + inverted_cnot_gate = ControlledGate( + Qubit(1), BlochSphereRotation(qubit=Qubit(0), axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2) + ) + + self.assertTrue(inverted_matrix_with_phase == inverted_cnot_gate)