Skip to content

Commit

Permalink
Merge pull request #14 from QSAR-UBC/linting
Browse files Browse the repository at this point in the history
Linting
  • Loading branch information
glassnotes authored May 10, 2024
2 parents 241ad63 + a65441d commit f35cdd2
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 160 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install black pytest
python -m pip install black pytest pylint
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python -m pip install .
- name: Format with black
Expand All @@ -41,6 +41,9 @@ jobs:
with:
add: "ionizer/*.py tests/*.py"
message: "Auto-format with black"
- name: Lint with pylint
run: |
pylint ionizer/*.py tests/*.py
- name: Test with pytest
run: |
pytest tests/test_*.py
3 changes: 3 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[MESSAGE CONTROL]

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
179 changes: 95 additions & 84 deletions ionizer/identity_hunter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,97 @@
TRIPLE_IDENTITY_FILE = files("ionizer.resources").joinpath("triple_gate_identities.pkl")


def generate_gate_identities():
def _test_inclusion_in_identity_db(db_subset, single_gates, candidate_angles, candidate_matrix):
"""Helper function to test if a candidate gate identity was already found.
Args:
db_subset (Dict[str, Tuple(List[float], str, float)]): Subset of database we
wish to search for presence of idendity.
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.
candidate_angles (List[float]): List of angles used in identity generation.
candidate_matrix (tensor): Unitary matrix for this particular set of angles.
Returns:
Tuple(str or None, float or None): If a valid identity is found, returns the name
of the gate and its parameter. Otherwise, returns None, None.
"""
# Loop over GPI/GPI2 for all the special angles
for id_gate, gate_angle_list in single_gates.items():
# Get their explicit reference angles and matrix reprensentations
angles, matrices = [x[0] for x in gate_angle_list], [x[1] for x in gate_angle_list]

# Test each reference against the candidate to see if any are equivalent
for ref_angle, ref_matrix in zip(angles, matrices):
if are_mats_equivalent(candidate_matrix, ref_matrix):
# If they are equivalent, return the identity if not already in database
if not any(
np.allclose(candidate_angles, database_angles)
for database_angles in [identity[0] for identity in db_subset]
):
return id_gate, ref_angle

return None, None


def generate_gate_identities(single_gates, id_angles, identity_length):
"""Generates all identities involving specified number of 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.
identity_length (int): How long a gate sequence to test. Given gate fusion
rules, it makes sense only to have this as value 2 or 3.
Returns:
Dict[str, Tuple(Tuple(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:
If identity_length=2, 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.
"""

gate_identities = {}

# Generate combinations of gates to test if they are equivalent to a single one
for gate_list in product([GPI, GPI2], repeat=identity_length):
combo_name = "".join([gate.__name__ for gate in gate_list])

if combo_name not in gate_identities:
gate_identities[combo_name] = []

for angle_list in product(id_angles, repeat=identity_length):
matrix = math.linalg.multi_dot(
[gate.compute_matrix(angle) for gate, angle in zip(gate_list, angle_list)]
)

# Test in case we produced the identity
if are_mats_equivalent(matrix, np.eye(2)):
gate_identities[combo_name].append((angle_list, "Identity", 0.0))
continue

# Check if we produced something else instead; if so, add to database
equivalent_gate, equivalent_angle = _test_inclusion_in_identity_db(
gate_identities[combo_name], single_gates, angle_list, matrix
)

if equivalent_gate is not None:
gate_identities[combo_name].append((angle_list, equivalent_gate, equivalent_angle))

return gate_identities


def generate_gate_identity_database():
"""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.
Expand All @@ -41,91 +131,12 @@ def generate_gate_identities():
"GPI2": [([angle], GPI2.compute_matrix(angle)) for angle in id_angles],
}

double_gate_identities = {}

# Check which combinations of 2 gates reduces to a single one
for gate_1, gate_2 in product([GPI, GPI2], repeat=2):
combo_name = gate_1.__name__ + gate_2.__name__

for angle_1, angle_2 in product(id_angles, repeat=2):
matrix = math.dot(gate_1.compute_matrix(angle_1), gate_2.compute_matrix(angle_2))

# Test in case we produced the identity;
if not math.isclose(matrix[0, 0], 0.0):
if math.allclose(matrix / matrix[0, 0], math.eye(2)):
if combo_name not in list(double_gate_identities.keys()):
double_gate_identities[combo_name] = []
double_gate_identities[combo_name].append(([angle_1, angle_2], "Identity", 0.0))
continue

for id_gate in list(single_gates.keys()):
angles, matrices = [x[0] for x in single_gates[id_gate]], [
x[1] for x in single_gates[id_gate]
]

for ref_angle, ref_matrix in zip(angles, matrices):
if are_mats_equivalent(matrix, ref_matrix):
if combo_name not in list(double_gate_identities.keys()):
double_gate_identities[combo_name] = []

if not any(
np.allclose([angle_1, angle_2], database_angles)
for database_angles in [
identity[0] for identity in double_gate_identities[combo_name]
]
):
double_gate_identities[combo_name].append(
([angle_1, angle_2], id_gate, ref_angle)
)
double_gate_identities = generate_gate_identities(single_gates, id_angles, 2)
triple_gate_identities = generate_gate_identities(single_gates, id_angles, 3)

with DOUBLE_IDENTITY_FILE.open("wb") as outfile:
pickle.dump(double_gate_identities, outfile)

triple_gate_identities = {}

# Check which combinations of 2 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__

for angle_1, angle_2, angle_3 in product(id_angles, repeat=3):
matrix = math.linalg.multi_dot(
[
gate_1.compute_matrix(angle_1),
gate_2.compute_matrix(angle_2),
gate_3.compute_matrix(angle_3),
]
)

# Test in case we produced the identity;
if not math.isclose(matrix[0, 0], 0.0):
if math.allclose(matrix / matrix[0, 0], math.eye(2)):
if combo_name not in list(triple_gate_identities.keys()):
triple_gate_identities[combo_name] = []
triple_gate_identities[combo_name].append(
([angle_1, angle_2, angle_3], "Identity", 0.0)
)
continue

for id_gate, _ in single_gates.items():
angles, matrices = [x[0] for x in single_gates[id_gate]], [
x[1] for x in single_gates[id_gate]
]

for ref_angle, ref_matrix in zip(angles, matrices):
if are_mats_equivalent(matrix, ref_matrix):
if combo_name not in list(triple_gate_identities.keys()):
triple_gate_identities[combo_name] = []

if not any(
np.allclose([angle_1, angle_2, angle_3], database_angles)
for database_angles in [
identity[0] for identity in triple_gate_identities[combo_name]
]
):
triple_gate_identities[combo_name].append(
([angle_1, angle_2, angle_3], id_gate, ref_angle)
)

with TRIPLE_IDENTITY_FILE.open("wb") as outfile:
pickle.dump(triple_gate_identities, outfile)

Expand All @@ -150,7 +161,7 @@ def lookup_gate_identity(gates):
gate_identities = pickle.load(infile)
except FileNotFoundError:
# Generate the file first and then load it
generate_gate_identities()
generate_gate_identity_database()
with DOUBLE_IDENTITY_FILE.open("rb") as infile:
gate_identities = pickle.load(infile)
elif len(gates) == 3:
Expand All @@ -159,7 +170,7 @@ def lookup_gate_identity(gates):
gate_identities = pickle.load(infile)
except FileNotFoundError:
# Generate the file first and then load it
generate_gate_identities()
generate_gate_identity_database()
with TRIPLE_IDENTITY_FILE.open("rb") as infile:
gate_identities = pickle.load(infile)

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
Binary file modified ionizer/resources/double_gate_identities.pkl
Binary file not shown.
Binary file modified ionizer/resources/triple_gate_identities.pkl
Binary file not shown.
21 changes: 12 additions & 9 deletions ionizer/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def commute_through_ms_gates(
list_copy = list_copy[::-1]

with qml.QueuingManager.stop_recording():
with qml.tape.QuantumTape() as commuted_tape:
with qml.tape.QuantumTape() as _:
while len(list_copy) > 0:
current_gate = list_copy[0]
list_copy.pop(0)
Expand Down Expand Up @@ -305,11 +305,11 @@ def single_qubit_fusion_gpi(tape: QuantumTape) -> (Sequence[QuantumTape], Callab
continue

# Construct the three new operations to apply
first_gate = GPI2(gamma, wires=current_gate.wires)
second_gate = GPI(beta, wires=current_gate.wires)
third_gate = GPI2(alpha, wires=current_gate.wires)

gates_to_apply = [first_gate, second_gate, third_gate]
gates_to_apply = [
GPI2(gamma, wires=current_gate.wires),
GPI(beta, wires=current_gate.wires),
GPI2(alpha, wires=current_gate.wires),
]
new_operations.extend(search_and_apply_three_gate_identities(gates_to_apply))

new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)
Expand All @@ -324,7 +324,7 @@ def null_postprocessing(results):


@qml.transform
def convert_to_gpi(tape: QuantumTape, exclude_list=[]) -> (Sequence[QuantumTape], Callable):
def convert_to_gpi(tape: QuantumTape, exclude_list=None) -> (Sequence[QuantumTape], Callable):
"""Transpile a tape directly to native trapped ion gates.
Any operation without a decomposition in decompositions.py will remain
Expand All @@ -340,11 +340,14 @@ def convert_to_gpi(tape: QuantumTape, exclude_list=[]) -> (Sequence[QuantumTape]
function]: The transformed circuit as described in :func:`qml.transform
<pennylane.transform>`.
"""
if exclude_list is None:
exclude_list = []

new_operations = []

with qml.QueuingManager.stop_recording():
for op in tape.operations:
if op.name not in exclude_list and op.name in decomp_map.keys():
if op.name not in exclude_list and op.name in decomp_map:
if op.num_params > 0:
new_operations.extend(decomp_map[op.name](*op.data, op.wires))
else:
Expand Down Expand Up @@ -387,7 +390,7 @@ def ionize(tape: QuantumTape) -> (Sequence[QuantumTape], Callable):

# The tape will first be expanded into known operations
def stop_at(op):
return op.name in list(decomp_map.keys())
return op.name in decomp_map

custom_expand_fn = qml.transforms.create_expand_fn(depth=9, stop_at=stop_at)

Expand Down
Loading

0 comments on commit f35cdd2

Please sign in to comment.