diff --git a/.pylintrc b/.pylintrc index e0e5f75..6e4a048 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,3 +1,3 @@ [MESSAGE CONTROL] -disable=too-many-locals \ No newline at end of file +disable=fixme \ No newline at end of file diff --git a/ionizer/decompositions.py b/ionizer/decompositions.py index 1f3de53..ad82207 100644 --- a/ionizer/decompositions.py +++ b/ionizer/decompositions.py @@ -153,14 +153,14 @@ def gpi_rz(phi, wires): return [GPI(-phi / 2, wires=wires), GPI(0.0, wires=wires)] -def gpi_single_qubit_unitary(U, wires): +def gpi_single_qubit_unitary(unitary, wires): """Single-qubit unitary matrix decomposition into GPI/GPI2 gates. This function is modeled off of PennyLane's unitary_to_rot transform: https://docs.pennylane.ai/en/stable/code/api/pennylane.transforms.unitary_to_rot.html Args: - U (tensor): A unitary matrix. + unitary (tensor): A unitary matrix. wires (Sequence[int] or pennylane.Wires): The wires this gate is acting on. Returns: @@ -168,26 +168,26 @@ def gpi_single_qubit_unitary(U, wires): the desired unitary up to a global phase. """ # Check in case we have the identity - if math.allclose(U, math.eye(2)): + if math.allclose(unitary, math.eye(2)): return [] # Special case: if we have off-diagonal elements this is a single GPI - if math.isclose(U[0, 0], 0.0): - angle = math.angle(U[1, 0]) + if math.isclose(unitary[0, 0], 0.0): + angle = math.angle(unitary[1, 0]) return [GPI(angle, wires=wires)] # Special case: if we have off-diagonal 0s but it is not the identity, # this is an RZ which is a sequence of two GPIs. - if math.allclose([U[0, 1], U[1, 0]], [0.0, 0.0]): - return gpi_rz(2 * math.angle(U[1, 1]), wires) + if math.allclose([unitary[0, 1], unitary[1, 0]], [0.0, 0.0]): + return gpi_rz(2 * math.angle(unitary[1, 1]), wires) # Special case: if both diagonal elements are 1/sqrt(2), this is a GPI2 - if math.allclose([U[0, 0], U[1, 1]], [1 / np.sqrt(2), 1 / np.sqrt(2)]): - angle = math.angle(U[1, 0]) + np.pi / 2 + if math.allclose([unitary[0, 0], unitary[1, 1]], [1 / np.sqrt(2), 1 / np.sqrt(2)]): + angle = math.angle(unitary[1, 0]) + np.pi / 2 return [GPI2(angle, wires=wires)] # In the general case we must compute and return all three angles. - gamma, beta, alpha = extract_gpi2_gpi_gpi2_angles(U) + gamma, beta, alpha = extract_gpi2_gpi_gpi2_angles(unitary) return [GPI2(gamma, wires=wires), GPI(beta, wires=wires), GPI2(alpha, wires=wires)] diff --git a/ionizer/identity_hunter.py b/ionizer/identity_hunter.py index 7f2b3c6..b8ce32f 100644 --- a/ionizer/identity_hunter.py +++ b/ionizer/identity_hunter.py @@ -18,28 +18,28 @@ TRIPLE_IDENTITY_FILE = files("ionizer.resources").joinpath("triple_gate_identities.pkl") -def generate_gate_identities(): - """Generates all 2- and 3-gate identities involving GPI/GPI2 and special angles. +def generate_double_gate_identities(single_gates, id_angles): + """Generates all 2-gate identities involving GPI/GPI2 and special angles. - Results are stored in pkl files which can be used later on. - """ + Args: + single_gates (Dict[str, List[Tuple(float, tensor)]]): Dictionary containing + which gates to generate identities from, along with list of special + cases of angles/matrices to use in identity generation. + id_angles (List[float]): Special values of angles used in identity generation. - id_angles = [ - -np.pi, - -3 * np.pi / 4, - -np.pi / 2, - -np.pi / 4, - 0.0, - np.pi / 4, - np.pi / 2, - 3 * np.pi / 4, - np.pi, - ] + Returns: + Dict[str, Tuple([float, float], str, float)]: Dictionary of identities + where the key is a concatenated string of gates, and the value contains + the angles involved in the identity, the resultant gate, and its argument. - single_gates = { - "GPI": [([angle], GPI.compute_matrix(angle)) for angle in id_angles], - "GPI2": [([angle], GPI2.compute_matrix(angle)) for angle in id_angles], - } + Example: + An entry in the return dictionary under the key 'GPIGPI2' may have the form + + [0.7853981633974483, -2.356194490192345], 'GPI2', [0.7853981633974483] + + This indicates that the product of GPI(0.785398) GPI2(-2.356194) is a GPI2 + gate with parameter 0.78539. + """ double_gate_identities = {} @@ -58,10 +58,8 @@ def generate_gate_identities(): double_gate_identities[combo_name].append(([angle_1, angle_2], "Identity", 0.0)) continue - for id_gate in single_gates: - angles, matrices = [x[0] for x in single_gates[id_gate]], [ - x[1] for x in single_gates[id_gate] - ] + for id_gate, gate_angle_list in single_gates.items(): + angles, matrices = [x[0] for x in gate_angle_list], [x[1] for x in gate_angle_list] for ref_angle, ref_matrix in zip(angles, matrices): if are_mats_equivalent(matrix, ref_matrix): @@ -78,12 +76,35 @@ def generate_gate_identities(): ([angle_1, angle_2], id_gate, ref_angle) ) - with DOUBLE_IDENTITY_FILE.open("wb") as outfile: - pickle.dump(double_gate_identities, outfile) + return double_gate_identities + + +def generate_triple_gate_identities(single_gates, id_angles): + """Generates all 3-gate identities involving GPI/GPI2 and special angles. + + Args: + single_gates (Dict[str, List[Tuple(float, tensor)]]): Dictionary containing + which gates to generate identities from, along with list of special + cases of angles/matrices to use in identity generation. + id_angles (List[float]): Special values of angles used in identity generation. + + Returns: + Dict[str, Tuple([float, float, float], str, float)]: Dictionary of identities + where the key is a concatenated string of gates, and the value contains + the angles involved in the identity, the resultant gate, and its argument. + + Example: + An entry in the return dictionary under the key 'GPIGPIGPI' may have the form + + [3.141592653589793, 3.141592653589793, 0.0], 'GPI', [-3.141592653589793] + + This indicates that the product GPI(3.14) GPI(3.14) GPI(0) is a GPI + gate with parameter -3.14. + """ triple_gate_identities = {} - # Check which combinations of 2 gates reduces to a single one + # Check which combinations of 3 gates reduces to a single one for gate_1, gate_2, gate_3 in product([GPI, GPI2], repeat=3): combo_name = gate_1.__name__ + gate_2.__name__ + gate_3.__name__ @@ -106,10 +127,8 @@ def generate_gate_identities(): ) continue - for id_gate in single_gates: - angles, matrices = [x[0] for x in single_gates[id_gate]], [ - x[1] for x in single_gates[id_gate] - ] + for id_gate, gate_angle_list in single_gates.items(): + angles, matrices = [x[0] for x in gate_angle_list], [x[1] for x in gate_angle_list] for ref_angle, ref_matrix in zip(angles, matrices): if are_mats_equivalent(matrix, ref_matrix): @@ -126,6 +145,38 @@ def generate_gate_identities(): ([angle_1, angle_2, angle_3], id_gate, ref_angle) ) + return triple_gate_identities + + +def generate_gate_identities(): + """Generates all 2- and 3-gate identities involving GPI/GPI2 and special angles. + + Results are stored in pkl files which can be used later on. + """ + + id_angles = [ + -np.pi, + -3 * np.pi / 4, + -np.pi / 2, + -np.pi / 4, + 0.0, + np.pi / 4, + np.pi / 2, + 3 * np.pi / 4, + np.pi, + ] + + single_gates = { + "GPI": [([angle], GPI.compute_matrix(angle)) for angle in id_angles], + "GPI2": [([angle], GPI2.compute_matrix(angle)) for angle in id_angles], + } + + double_gate_identities = generate_double_gate_identities(single_gates, id_angles) + triple_gate_identities = generate_triple_gate_identities(single_gates, id_angles) + + with DOUBLE_IDENTITY_FILE.open("wb") as outfile: + pickle.dump(double_gate_identities, outfile) + with TRIPLE_IDENTITY_FILE.open("wb") as outfile: pickle.dump(triple_gate_identities, outfile) diff --git a/ionizer/ops.py b/ionizer/ops.py index 24a1eab..a9fb5bb 100644 --- a/ionizer/ops.py +++ b/ionizer/ops.py @@ -44,7 +44,9 @@ class GPI(Operation): num_params = 1 ndim_params = (0,) - def __init__(self, phi, wires, id=None): + # Note: disable pylint complaint about redefined built-in, since the id + # value itself is coming from the class definition of Operators in PennyLane proper. + def __init__(self, phi, wires, id=None): # pylint: disable=redefined-builtin super().__init__(phi, wires=wires, id=id) @staticmethod @@ -91,7 +93,7 @@ class GPI2(Operation): num_params = 1 ndim_params = (0,) - def __init__(self, phi, wires, id=None): + def __init__(self, phi, wires, id=None): # pylint: disable=redefined-builtin super().__init__(phi, wires=wires, id=id) @staticmethod @@ -143,7 +145,7 @@ class MS(Operation): num_wires = 2 num_params = 0 - def __init__(self, wires, id=None): + def __init__(self, wires, id=None): # pylint: disable=redefined-builtin super().__init__(wires=wires, id=id) @staticmethod diff --git a/ionizer/utils.py b/ionizer/utils.py index 0b22604..1eee85f 100644 --- a/ionizer/utils.py +++ b/ionizer/utils.py @@ -21,18 +21,18 @@ from pennylane import math -def are_mats_equivalent(mat1, mat2): +def are_mats_equivalent(unitary1, unitary2): """Checks the equivalence of two unitary matrices. Args: - mat1 (tensor): First unitary matrix. - mat2 (tensor): Second unitary matrix. + unitary1 (tensor): First unitary matrix. + unitary2 (tensor): Second unitary matrix. Returns: bool: True if the two matrices are equivalent up to a global phase, False otherwise. """ - mat_product = math.dot(mat1, math.conj(math.T(mat2))) + mat_product = math.dot(unitary1, math.conj(math.T(unitary2))) # If the top-left entry is not 0, divide everything by it and test against identity if not math.isclose(mat_product[0, 0], 0.0): @@ -65,7 +65,7 @@ def rescale_angles(angles, renormalize_for_json=False): return rescaled_angles -def extract_gpi2_gpi_gpi2_angles(U): +def extract_gpi2_gpi_gpi2_angles(unitary): r"""Given a matrix U, recovers a set of three angles alpha, beta, and gamma such that U = GPI2(alpha) GPI(beta) GPI2(gamma) @@ -76,15 +76,15 @@ def extract_gpi2_gpi_gpi2_angles(U): https://docs.pennylane.ai/en/stable/code/api/pennylane.transforms.zyz_decomposition.html Args: - U (tensor): A unitary matrix. + unitary (tensor): A unitary matrix. Returns: tensor: Rotation angles for the GPI/GPI2 gates. The order of the returned angles corresponds to the order in which they would be implemented in the circuit. """ - det = math.angle(math.linalg.det(U)) - su2_mat = math.exp(-1j * det / 2) * U + det = math.angle(math.linalg.det(unitary)) + su2_mat = math.exp(-1j * det / 2) * unitary phase_00 = math.angle(su2_mat[0, 0]) phase_10 = math.angle(su2_mat[1, 0]) diff --git a/tests/test_decompositions.py b/tests/test_decompositions.py index 11f2ee2..014dee1 100644 --- a/tests/test_decompositions.py +++ b/tests/test_decompositions.py @@ -100,10 +100,10 @@ def test_parametric_decompositions(self, gate, decomp_function, angle): mat_product = mat_product / mat_product[0, 0] assert qml.math.allclose(mat_product, qml.math.eye(mat_product.shape[0])) - @pytest.mark.parametrize("U, decomp_list", single_qubit_unitaries) - def test_single_qubit_unitary_decomposition(self, U, decomp_list): + @pytest.mark.parametrize("unitary, decomp_list", single_qubit_unitaries) + def test_single_qubit_unitary_decomposition(self, unitary, decomp_list): """Test decompositions of single-qubit unitary matrices.""" - obtained_decomp_list = gpi_single_qubit_unitary(U, [0]) + obtained_decomp_list = gpi_single_qubit_unitary(unitary, [0]) assert all( op.name == expected_name for op, expected_name in zip(obtained_decomp_list, decomp_list) @@ -115,6 +115,6 @@ def test_single_qubit_unitary_decomposition(self, U, decomp_list): qml.apply(op) obtained_matrix = qml.matrix(tape, wire_order=tape.wires) - mat_product = math.dot(obtained_matrix, math.conj(math.T(U))) + mat_product = math.dot(obtained_matrix, math.conj(math.T(unitary))) mat_product = mat_product / mat_product[0, 0] assert math.allclose(mat_product, math.eye(2)) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 0d96a59..cbbfcf5 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -2,9 +2,10 @@ Test the suite of transpilation transforms. """ -import pytest from functools import partial +import pytest + import pennylane as qml from pennylane import math import numpy as np diff --git a/tests/test_utils.py b/tests/test_utils.py index 8e29eb9..37543b6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -19,8 +19,8 @@ from test_decompositions import single_qubit_unitaries -class TestMatrixEquivalence: - """Test utility function for matrix equivalence checking.""" +class TestMatrixAngleUtilities: + """Test utility functions for matrix and angle manipulations.""" @pytest.mark.parametrize( "mat1, mat2", @@ -49,10 +49,6 @@ def test_inequivalent_matrices(self, mat1, mat2): up to a global phase.""" assert not are_mats_equivalent(mat1, mat2) - -class TestRescaleAngles: - """Test utility function for rescaling of angles.""" - @pytest.mark.parametrize( "angles,rescaled_angles", [ @@ -85,21 +81,17 @@ def test_rescale_angles_hardware(self, angles, rescaled_angles): print(obtained_angles) assert math.allclose(obtained_angles, rescaled_angles) - -class TestExtractGPIGPI2Angles: - """Test utility function for extraction of GPI/GPI2 angles.""" - - @pytest.mark.parametrize("U", [test_case[0] for test_case in single_qubit_unitaries]) - def test_extract_gpi_gpi2_angles(self, U): + @pytest.mark.parametrize("unitary", [test_case[0] for test_case in single_qubit_unitaries]) + def test_extract_gpi_gpi2_angles(self, unitary): """Test that extracting GPI/GPI2 angles yields the correct operation.""" - gamma, beta, alpha = extract_gpi2_gpi_gpi2_angles(U) + gamma, beta, alpha = extract_gpi2_gpi_gpi2_angles(unitary) with qml.tape.QuantumTape() as tape: GPI2(gamma, wires=0) GPI(beta, wires=0) GPI2(alpha, wires=0) - assert are_mats_equivalent(qml.matrix(tape), U) + assert are_mats_equivalent(qml.matrix(tape), unitary) class TestConvertToJSON: