Skip to content

Commit 4eb6ff5

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
1 parent d6cf496 commit 4eb6ff5

11 files changed

+453
-228
lines changed

opensquirrel/circuit.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
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, replacer, mckay_decomposer, merger, writer
77
from opensquirrel.default_gates import default_gate_aliases, default_gate_set
88
from opensquirrel.parsing.libqasm.libqasm_ir_creator import LibqasmIRCreator
99
from opensquirrel.squirrel_ir import Gate, SquirrelIR
@@ -21,7 +21,7 @@ class Circuit:
2121
<BLANKLINE>
2222
h q[0]
2323
<BLANKLINE>
24-
>>> c.decompose_mckay()
24+
>>> c.decompose(decomposer=mckay_decomposer.mckay_decomposer)
2525
>>> c
2626
version 3.0
2727
<BLANKLINE>
@@ -84,27 +84,25 @@ def number_of_qubits(self) -> int:
8484
def qubit_register_name(self) -> str:
8585
return self.squirrel_ir.qubit_register_name
8686

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.
87+
def merge_single_qubit_gates(self):
88+
"""Perform gate fusion on all one-qubit gates.
89+
Gates obtained from merging other gates become anonymous gates.
9690
"""
9791

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

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

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
99+
def replace(self, gate_generator: Callable[..., Gate], f):
100+
"""Manually replace occurrences of a given gate with a list of gates.
101+
`f` is a callable that takes the arguments of the replaced gate
102+
and returns the decomposition as a list of gates.
105103
"""
106104

107-
replacer.replace(self.squirrel_ir, gate_name, f)
105+
replacer.replace(self.squirrel_ir, gate_generator, f)
108106

109107
def test_get_circuit_matrix(self) -> np.ndarray:
110108
"""Get the (large) unitary matrix corresponding to the circuit.
@@ -117,9 +115,6 @@ def test_get_circuit_matrix(self) -> np.ndarray:
117115
return circuit_matrix_calculator.get_circuit_matrix(self.squirrel_ir)
118116

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

125120
return writer.squirrel_ir_to_string(self.squirrel_ir)

opensquirrel/default_gates.py

Lines changed: 6 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,5 @@ 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+
default_gate_set = [h, x, x90, xm90, y, y90, z, z90, cz, cr, crk, cnot, rx, ry, rz, x]
8085
default_gate_aliases = {"X": x, "RX": rx, "RY": ry, "RZ": rz, "Hadamard": h, "H": h}

opensquirrel/mckay_decomposer.py

Lines changed: 31 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -8,110 +8,49 @@
88
from opensquirrel.squirrel_ir import BlochSphereRotation, Float, Gate, Qubit, SquirrelIR
99

1010

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 = {}
11+
def mckay_decomposer(g: Gate):
12+
"""Return the McKay decomposition of a 1-qubit gate as a list of gates.
13+
gate ----> Rz.Rx(pi/2).Rz.Rx(pi/2).Rz
1514
16-
def decompose_and_add(self, qubit: Qubit, angle: float, axis: Tuple[float, float, float]):
17-
if abs(angle) < ATOL:
18-
return
15+
_global phase is deemed irrelevant_, therefore a simulator backend might produce different output
16+
for the input and output - the results should be equivalent modulo global phase.
17+
"""
18+
if not isinstance(g, BlochSphereRotation):
19+
return [g]
1920

20-
# McKay decomposition
21+
if abs(g.angle) < ATOL:
22+
return []
2123

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)
24+
# McKay decomposition
2425

25-
theta = pi - 2 * atan2(zb_mod, za_mod)
26+
za_mod = sqrt(cos(g.angle / 2) ** 2 + (g.axis[2] * sin(g.angle / 2)) ** 2)
27+
zb_mod = abs(sin(g.angle / 2)) * sqrt(g.axis[0] ** 2 + g.axis[1] ** 2)
2628

27-
alpha = atan2(-sin(angle / 2) * axis[2], cos(angle / 2))
28-
beta = atan2(-sin(angle / 2) * axis[0], -sin(angle / 2) * axis[1])
29+
theta = pi - 2 * atan2(zb_mod, za_mod)
2930

30-
lam = beta - alpha
31-
phi = -beta - alpha - pi
31+
alpha = atan2(-sin(g.angle / 2) * g.axis[2], cos(g.angle / 2))
32+
beta = atan2(-sin(g.angle / 2) * g.axis[0], -sin(g.angle / 2) * g.axis[1])
3233

33-
lam = normalize_angle(lam)
34-
phi = normalize_angle(phi)
35-
theta = normalize_angle(theta)
34+
lam = beta - alpha
35+
phi = -beta - alpha - pi
3636

37-
if abs(lam) > ATOL:
38-
self.output.add_gate(rz(qubit, Float(lam)))
37+
lam = normalize_angle(lam)
38+
phi = normalize_angle(phi)
39+
theta = normalize_angle(theta)
3940

40-
self.output.add_gate(x90(qubit))
41+
decomposed_g = []
4142

42-
if abs(theta) > ATOL:
43-
self.output.add_gate(rz(qubit, Float(theta)))
43+
if abs(lam) > ATOL:
44+
decomposed_g.append(rz(g.qubit, Float(lam)))
4445

45-
self.output.add_gate(x90(qubit))
46+
decomposed_g.append(x90(g.qubit))
4647

47-
if abs(phi) > ATOL:
48-
self.output.add_gate(rz(qubit, Float(phi)))
48+
if abs(theta) > ATOL:
49+
decomposed_g.append(rz(g.qubit, Float(theta)))
4950

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"])
51+
decomposed_g.append(x90(g.qubit))
5552

56-
def flush_all(self):
57-
while len(self.accumulated_1q_gates) > 0:
58-
self.flush(next(iter(self.accumulated_1q_gates)))
53+
if abs(phi) > ATOL:
54+
decomposed_g.append(rz(g.qubit, Float(phi)))
5955

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()
116-
117-
return impl.output # FIXME: instead of returning a new IR, modify existing one
56+
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)

opensquirrel/replacer.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
1-
from typing import List
1+
from typing import Callable, List
22

3-
from opensquirrel.squirrel_ir import Comment, Gate, SquirrelIR
3+
from opensquirrel.squirrel_ir import Gate, SquirrelIR
44

55

6-
def replace(squirrel_ir: SquirrelIR, gate_name_to_replace: str, f):
6+
def decompose(squirrel_ir: SquirrelIR, decomposer: Callable[[Gate], List[Gate]]):
7+
"""Applies `decomposer` to every gate in the circuit, replacing each gate by the output of `decomposer`.
8+
When `decomposer` decides to not decompose a gate, it needs to return a list with the intact gate as single element.
9+
"""
710
statement_index = 0
811
while statement_index < len(squirrel_ir.statements):
912
statement = squirrel_ir.statements[statement_index]
1013

11-
if isinstance(statement, Comment):
12-
statement_index += 1
13-
continue
14-
1514
if not isinstance(statement, Gate):
16-
raise Exception("Unsupported")
17-
18-
if statement.name != gate_name_to_replace:
1915
statement_index += 1
2016
continue
2117

22-
# FIXME: handle case where if f is not a function but directly a list.
23-
24-
replacement: List[Gate] = f(*statement.arguments)
18+
replacement: List[Gate] = decomposer(statement)
2519
squirrel_ir.statements[statement_index : statement_index + 1] = replacement
2620
statement_index += len(replacement)
2721

2822
# TODO: Here, check that the semantic of the replacement is the same!
2923
# For this, need to update the simulation capabilities.
3024

31-
# TODO: Do we allow skipping the replacement, based on arguments?
25+
26+
def replace(squirrel_ir: SquirrelIR, gate_generator: Callable[..., Gate], f):
27+
"""Does the same as decomposer, but only applies to a given gate."""
28+
29+
def generic_replacer(g: Gate) -> [Gate]:
30+
if g.is_anonymous or g.generator != gate_generator:
31+
return [g]
32+
return f(*g.arguments)
33+
34+
decompose(squirrel_ir, generic_replacer)

0 commit comments

Comments
 (0)