Skip to content

Commit 09033af

Browse files
author
Pablo Le Hénaff
committed
Create merge pass
- Circuit.merge_single_qubit_gates - mcKay decomposer is now only the decomposition - Circuit.decompose is introduced, pass it the McKay decomposer - Decomposer abstract base class - The decomposer/replacer now checks that every used decomposition is valid in terms of circuit matrix!
1 parent d6cf496 commit 09033af

12 files changed

+661
-224
lines changed

opensquirrel/circuit.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from typing import Callable, Dict
1+
from typing import Callable, Dict, List
22

33
import numpy as np
44

55
import opensquirrel.parsing.antlr.squirrel_ir_from_string
6-
from opensquirrel import circuit_matrix_calculator, mckay_decomposer, replacer, writer
6+
from opensquirrel import circuit_matrix_calculator, mckay_decomposer, merger, replacer, writer
77
from opensquirrel.default_gates import default_gate_aliases, default_gate_set
88
from opensquirrel.parsing.libqasm.libqasm_ir_creator import LibqasmIRCreator
9+
from opensquirrel.replacer import Decomposer
910
from opensquirrel.squirrel_ir import Gate, SquirrelIR
1011

1112

@@ -21,7 +22,7 @@ class Circuit:
2122
<BLANKLINE>
2223
h q[0]
2324
<BLANKLINE>
24-
>>> c.decompose_mckay()
25+
>>> c.decompose(decomposer=mckay_decomposer.McKayDecomposer())
2526
>>> c
2627
version 3.0
2728
<BLANKLINE>
@@ -84,27 +85,25 @@ def number_of_qubits(self) -> int:
8485
def qubit_register_name(self) -> str:
8586
return self.squirrel_ir.qubit_register_name
8687

87-
def decompose_mckay(self):
88-
"""Perform gate fusion on all one-qubit gates and decompose them in the McKay style.
89-
90-
* all one-qubit gates on same qubit are merged together, without attempting to commute any gate
91-
* two-or-more-qubit gates are left as-is
92-
* merged one-qubit gates are decomposed according to McKay decomposition, that is:
93-
gate ----> Rz.Rx(pi/2).Rz.Rx(pi/2).Rz
94-
* _global phase is deemed irrelevant_, therefore a simulator backend might produce different output
95-
for the input and output circuit - those outputs should be equivalent modulo global phase.
88+
def merge_single_qubit_gates(self):
89+
"""Merge all consecutive 1-qubit gates in the circuit.
90+
Gates obtained from merging other gates become anonymous gates.
9691
"""
9792

98-
self.squirrel_ir = mckay_decomposer.decompose_mckay(self.squirrel_ir) # FIXME: inplace
93+
merger.merge_single_qubit_gates(self.squirrel_ir)
9994

100-
def replace(self, gate_name: str, f):
101-
"""Manually replace occurrences of a given gate with a list of gates.
95+
def decompose(self, decomposer: Decomposer):
96+
"""Generic decomposition pass. It applies the given decomposer function
97+
to every gate in the circuit."""
98+
replacer.decompose(self.squirrel_ir, decomposer)
10299

103-
* this can be called decomposition - but it's the least fancy version of it
104-
* function parameter gives the decomposition based on parameters of original gate
100+
def replace(self, gate_generator: Callable[..., Gate], f):
101+
"""Manually replace occurrences of a given gate with a list of gates.
102+
`f` is a callable that takes the arguments of the gate that is to be replaced
103+
and returns the decomposition as a list of gates.
105104
"""
106105

107-
replacer.replace(self.squirrel_ir, gate_name, f)
106+
replacer.replace(self.squirrel_ir, gate_generator, f)
108107

109108
def test_get_circuit_matrix(self) -> np.ndarray:
110109
"""Get the (large) unitary matrix corresponding to the circuit.
@@ -117,9 +116,6 @@ def test_get_circuit_matrix(self) -> np.ndarray:
117116
return circuit_matrix_calculator.get_circuit_matrix(self.squirrel_ir)
118117

119118
def __repr__(self) -> str:
120-
"""Write the circuit to a cQasm3 string.
121-
122-
* comments are removed
123-
"""
119+
"""Write the circuit to a cQasm3 string."""
124120

125121
return writer.squirrel_ir_to_string(self.squirrel_ir)

opensquirrel/common.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,16 @@ def can1(axis: Tuple[float, float, float], angle: float, phase: float = 0) -> np
4141
)
4242

4343
return result
44+
45+
46+
def are_matrices_equivalent_up_to_global_phase(matrix_a: np.ndarray, matrix_b: np.ndarray) -> bool:
47+
first_non_zero = next(
48+
(i, j) for i in range(matrix_a.shape[0]) for j in range(matrix_a.shape[1]) if abs(matrix_a[i, j]) > ATOL
49+
)
50+
51+
if abs(matrix_b[first_non_zero]) < ATOL:
52+
return False
53+
54+
phase_difference = matrix_a[first_non_zero] / matrix_b[first_non_zero]
55+
56+
return np.allclose(matrix_a, phase_difference * matrix_b)

opensquirrel/default_gates.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ def x90(q: Qubit) -> Gate:
1818
return BlochSphereRotation(qubit=q, axis=(1, 0, 0), angle=math.pi / 2, phase=0)
1919

2020

21+
@named_gate
22+
def xm90(q: Qubit) -> Gate:
23+
return BlochSphereRotation(qubit=q, axis=(1, 0, 0), angle=-math.pi / 2, phase=0)
24+
25+
2126
@named_gate
2227
def y(q: Qubit) -> Gate:
2328
return BlochSphereRotation(qubit=q, axis=(0, 1, 0), angle=math.pi, phase=math.pi / 2)
@@ -76,5 +81,35 @@ def crk(control: Qubit, target: Qubit, k: Int) -> Gate:
7681
return ControlledGate(control, BlochSphereRotation(qubit=target, axis=(0, 0, 1), angle=theta, phase=theta / 2))
7782

7883

79-
default_gate_set = [h, x, x90, y, y90, z, z90, cz, cr, crk, cnot, rx, ry, rz, x]
84+
@named_gate
85+
def swap(q1: Qubit, q2: Qubit) -> Gate:
86+
return MatrixGate(
87+
np.array(
88+
[
89+
[1, 0, 0, 0],
90+
[0, 0, 1, 0],
91+
[0, 1, 0, 0],
92+
[0, 0, 0, 1],
93+
]
94+
),
95+
[q1, q2],
96+
)
97+
98+
99+
@named_gate
100+
def sqrt_swap(q1: Qubit, q2: Qubit) -> Gate:
101+
return MatrixGate(
102+
np.array(
103+
[
104+
[1, 0, 0, 0],
105+
[0, (1 + 1j) / 2, (1 - 1j) / 2, 0],
106+
[0, (1 - 1j) / 2, (1 + 1j) / 2, 0],
107+
[0, 0, 0, 1],
108+
]
109+
),
110+
[q1, q2],
111+
)
112+
113+
114+
default_gate_set = [h, x, x90, xm90, y, y90, z, z90, cz, cr, crk, cnot, rx, ry, rz, x, swap, sqrt_swap]
80115
default_gate_aliases = {"X": x, "RX": rx, "RY": ry, "RZ": rz, "Hadamard": h, "H": h}

opensquirrel/mckay_decomposer.py

Lines changed: 27 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,35 @@
55

66
from opensquirrel.common import ATOL, normalize_angle
77
from opensquirrel.default_gates import rz, x90
8+
from opensquirrel.replacer import Decomposer
89
from opensquirrel.squirrel_ir import BlochSphereRotation, Float, Gate, Qubit, SquirrelIR
910

1011

11-
class _McKayDecomposerImpl:
12-
def __init__(self, number_of_qubits: int, qubit_register_name: str):
13-
self.output = SquirrelIR(number_of_qubits=number_of_qubits, qubit_register_name=qubit_register_name)
14-
self.accumulated_1q_gates = {}
12+
class McKayDecomposer(Decomposer):
13+
def decompose(self, g: Gate) -> [Gate]:
14+
"""Return the McKay decomposition of a 1-qubit gate as a list of gates.
15+
gate ----> Rz.Rx(pi/2).Rz.Rx(pi/2).Rz
1516
16-
def decompose_and_add(self, qubit: Qubit, angle: float, axis: Tuple[float, float, float]):
17-
if abs(angle) < ATOL:
18-
return
17+
_global phase is deemed irrelevant_, therefore a simulator backend might produce different output
18+
for the input and output - the results should be equivalent modulo global phase.
19+
20+
Relevant literature: https://arxiv.org/abs/1612.00858
21+
"""
22+
if not isinstance(g, BlochSphereRotation):
23+
return [g]
24+
25+
if abs(g.angle) < ATOL:
26+
return []
1927

2028
# McKay decomposition
2129

22-
za_mod = sqrt(cos(angle / 2) ** 2 + (axis[2] * sin(angle / 2)) ** 2)
23-
zb_mod = abs(sin(angle / 2)) * sqrt(axis[0] ** 2 + axis[1] ** 2)
30+
za_mod = sqrt(cos(g.angle / 2) ** 2 + (g.axis[2] * sin(g.angle / 2)) ** 2)
31+
zb_mod = abs(sin(g.angle / 2)) * sqrt(g.axis[0] ** 2 + g.axis[1] ** 2)
2432

2533
theta = pi - 2 * atan2(zb_mod, za_mod)
2634

27-
alpha = atan2(-sin(angle / 2) * axis[2], cos(angle / 2))
28-
beta = atan2(-sin(angle / 2) * axis[0], -sin(angle / 2) * axis[1])
35+
alpha = atan2(-sin(g.angle / 2) * g.axis[2], cos(g.angle / 2))
36+
beta = atan2(-sin(g.angle / 2) * g.axis[0], -sin(g.angle / 2) * g.axis[1])
2937

3038
lam = beta - alpha
3139
phi = -beta - alpha - pi
@@ -34,84 +42,19 @@ def decompose_and_add(self, qubit: Qubit, angle: float, axis: Tuple[float, float
3442
phi = normalize_angle(phi)
3543
theta = normalize_angle(theta)
3644

45+
decomposed_g = []
46+
3747
if abs(lam) > ATOL:
38-
self.output.add_gate(rz(qubit, Float(lam)))
48+
decomposed_g.append(rz(g.qubit, Float(lam)))
3949

40-
self.output.add_gate(x90(qubit))
50+
decomposed_g.append(x90(g.qubit))
4151

4252
if abs(theta) > ATOL:
43-
self.output.add_gate(rz(qubit, Float(theta)))
53+
decomposed_g.append(rz(g.qubit, Float(theta)))
4454

45-
self.output.add_gate(x90(qubit))
55+
decomposed_g.append(x90(g.qubit))
4656

4757
if abs(phi) > ATOL:
48-
self.output.add_gate(rz(qubit, Float(phi)))
49-
50-
def flush(self, q):
51-
if q not in self.accumulated_1q_gates:
52-
return
53-
p = self.accumulated_1q_gates.pop(q)
54-
self.decompose_and_add(q, p["angle"], p["axis"])
55-
56-
def flush_all(self):
57-
while len(self.accumulated_1q_gates) > 0:
58-
self.flush(next(iter(self.accumulated_1q_gates)))
59-
60-
def accumulate(self, qubit, bloch_sphere_rotation: BlochSphereRotation):
61-
axis, angle, phase = bloch_sphere_rotation.axis, bloch_sphere_rotation.angle, bloch_sphere_rotation.phase
62-
63-
if qubit not in self.accumulated_1q_gates:
64-
self.accumulated_1q_gates[qubit] = {"angle": angle, "axis": axis, "phase": phase}
65-
return
66-
67-
existing = self.accumulated_1q_gates[qubit]
68-
combined_phase = phase + existing["phase"]
69-
70-
a = angle
71-
l = axis
72-
b = existing["angle"]
73-
m = existing["axis"]
74-
75-
combined_angle = 2 * acos(cos(a / 2) * cos(b / 2) - sin(a / 2) * sin(b / 2) * np.dot(l, m))
76-
77-
if abs(sin(combined_angle / 2)) < ATOL:
78-
self.accumulated_1q_gates.pop(qubit)
79-
return
80-
81-
combined_axis = (
82-
1
83-
/ sin(combined_angle / 2)
84-
* (sin(a / 2) * cos(b / 2) * l + cos(a / 2) * sin(b / 2) * m + sin(a / 2) * sin(b / 2) * np.cross(l, m))
85-
)
86-
87-
self.accumulated_1q_gates[qubit] = {"angle": combined_angle, "axis": combined_axis, "phase": combined_phase}
88-
89-
def process_gate(self, gate: Gate):
90-
qubit_arguments = [arg for arg in gate.arguments if isinstance(arg, Qubit)]
91-
92-
if len(qubit_arguments) >= 2:
93-
[self.flush(q) for q in qubit_arguments]
94-
self.output.add_gate(gate)
95-
return
96-
97-
if len(qubit_arguments) == 0:
98-
assert False, "Unsupported"
99-
return
100-
101-
assert isinstance(gate, BlochSphereRotation), f"Not supported for single qubit gate `{gate.name}`: {type(gate)}"
102-
103-
self.accumulate(qubit_arguments[0], gate)
104-
105-
106-
def decompose_mckay(squirrel_ir):
107-
impl = _McKayDecomposerImpl(squirrel_ir.number_of_qubits, squirrel_ir.qubit_register_name)
108-
109-
for statement in squirrel_ir.statements:
110-
if not isinstance(statement, Gate):
111-
continue
112-
113-
impl.process_gate(statement)
114-
115-
impl.flush_all()
58+
decomposed_g.append(rz(g.qubit, Float(phi)))
11659

117-
return impl.output # FIXME: instead of returning a new IR, modify existing one
60+
return decomposed_g

opensquirrel/merger.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from math import acos, atan2, cos, pi, sin, sqrt
2+
from typing import List, Optional
3+
4+
import numpy as np
5+
6+
from opensquirrel.common import ATOL
7+
from opensquirrel.squirrel_ir import BlochSphereRotation, Gate, Qubit, SquirrelIR
8+
9+
10+
def compose_bloch_sphere_rotations(a: BlochSphereRotation, b: BlochSphereRotation) -> BlochSphereRotation:
11+
"""Computes the Bloch sphere rotation resulting from the composition of two Block sphere rotations.
12+
The first rotation is applied and then the second.
13+
The resulting gate is anonymous except if `a` is the identity and `b` is not anonymous, or vice versa.
14+
15+
Uses Rodrigues' rotation formula, see for instance https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula.
16+
"""
17+
assert a.qubit == b.qubit, "Cannot merge two BlochSphereRotation's on different qubits"
18+
19+
combined_angle = 2 * acos(
20+
cos(a.angle / 2) * cos(b.angle / 2) - sin(a.angle / 2) * sin(b.angle / 2) * np.dot(a.axis, b.axis)
21+
)
22+
23+
if abs(sin(combined_angle / 2)) < ATOL:
24+
return BlochSphereRotation.identity(a.qubit)
25+
26+
combined_axis = (
27+
1
28+
/ sin(combined_angle / 2)
29+
* (
30+
sin(a.angle / 2) * cos(b.angle / 2) * a.axis
31+
+ cos(a.angle / 2) * sin(b.angle / 2) * b.axis
32+
+ sin(a.angle / 2) * sin(b.angle / 2) * np.cross(a.axis, b.axis)
33+
)
34+
)
35+
36+
combined_phase = a.phase + b.phase
37+
38+
generator = b.generator if a.is_identity() else a.generator if b.is_identity() else None
39+
arguments = b.arguments if a.is_identity() else a.arguments if b.is_identity() else None
40+
41+
return BlochSphereRotation(
42+
qubit=a.qubit,
43+
axis=combined_axis,
44+
angle=combined_angle,
45+
phase=combined_phase,
46+
generator=generator,
47+
arguments=arguments,
48+
)
49+
50+
51+
def merge_single_qubit_gates(squirrel_ir: SquirrelIR):
52+
accumulators_per_qubit: dict[Qubit, BlochSphereRotation] = {
53+
Qubit(q): BlochSphereRotation.identity(Qubit(q)) for q in range(squirrel_ir.number_of_qubits)
54+
}
55+
56+
statement_index = 0
57+
while statement_index < len(squirrel_ir.statements):
58+
statement = squirrel_ir.statements[statement_index]
59+
60+
if not isinstance(statement, Gate):
61+
# Skip
62+
statement_index += 1
63+
continue
64+
65+
if isinstance(statement, BlochSphereRotation):
66+
# Accumulate
67+
already_accumulated = accumulators_per_qubit.get(statement.qubit)
68+
69+
composed = compose_bloch_sphere_rotations(statement, already_accumulated)
70+
accumulators_per_qubit[statement.qubit] = composed
71+
72+
del squirrel_ir.statements[statement_index]
73+
continue
74+
75+
for qubit_operand in statement.get_qubit_operands():
76+
if not accumulators_per_qubit[qubit_operand].is_identity():
77+
squirrel_ir.statements.insert(statement_index, accumulators_per_qubit[qubit_operand])
78+
accumulators_per_qubit[qubit_operand] = BlochSphereRotation.identity(qubit_operand)
79+
statement_index += 1
80+
statement_index += 1
81+
82+
for accumulated_bloch_sphere_rotation in accumulators_per_qubit.values():
83+
if not accumulated_bloch_sphere_rotation.is_identity():
84+
squirrel_ir.statements.append(accumulated_bloch_sphere_rotation)

0 commit comments

Comments
 (0)