Skip to content

Commit

Permalink
Continue linting files. Reorganize identity hunter.
Browse files Browse the repository at this point in the history
  • Loading branch information
Olivia Di Matteo committed May 8, 2024
1 parent 1880e40 commit b7c3826
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[MESSAGE CONTROL]

disable=too-many-locals
disable=fixme
20 changes: 10 additions & 10 deletions ionizer/decompositions.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,41 +153,41 @@ 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:
List[Operation]: The sequence of GPI/GPI2 rotations that implements
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)]

Expand Down
111 changes: 81 additions & 30 deletions ionizer/identity_hunter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand All @@ -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):
Expand All @@ -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__

Expand All @@ -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):
Expand All @@ -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)

Expand Down
8 changes: 5 additions & 3 deletions ionizer/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions ionizer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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])
Expand Down
8 changes: 4 additions & 4 deletions tests/test_decompositions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
3 changes: 2 additions & 1 deletion tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 6 additions & 14 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
[
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit b7c3826

Please sign in to comment.