Skip to content

Commit 1afb025

Browse files
author
Pablo Le Hénaff
committed
Export to quantify_scheduler's Schedule format
- Add ZYZ and ABC decompositions - Add exporter - Dependency on quantify_scheduler only when the Python version is compatible - Test with mocks
1 parent c20eece commit 1afb025

14 files changed

+4288
-325
lines changed

opensquirrel/circuit.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from typing import Callable, Dict, List
1+
from typing import Callable, Dict
22

33
import numpy as np
44

55
import opensquirrel.parsing.antlr.squirrel_ir_from_string
66
from opensquirrel import circuit_matrix_calculator, mckay_decomposer, merger, replacer, writer
77
from opensquirrel.default_gates import default_gate_aliases, default_gate_set
8+
from opensquirrel.export import quantify_scheduler_exporter
9+
from opensquirrel.export_format import ExportFormat
810
from opensquirrel.parsing.libqasm.libqasm_ir_creator import LibqasmIRCreator
911
from opensquirrel.replacer import Decomposer
1012
from opensquirrel.squirrel_ir import Gate, SquirrelIR
@@ -119,3 +121,9 @@ def __repr__(self) -> str:
119121
"""Write the circuit to a cQasm3 string."""
120122

121123
return writer.squirrel_ir_to_string(self.squirrel_ir)
124+
125+
def export(self, format: ExportFormat):
126+
if format == ExportFormat.QUANTIFY_SCHEDULER:
127+
return quantify_scheduler_exporter.export(self.squirrel_ir)
128+
129+
raise Exception("Unknown export format")

opensquirrel/cnot_decomposer.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import math
2+
3+
from opensquirrel.common import ATOL
4+
from opensquirrel.default_gates import cnot, ry, rz
5+
from opensquirrel.identity_filter import filter_out_identities
6+
from opensquirrel.replacer import Decomposer
7+
from opensquirrel.squirrel_ir import BlochSphereRotation, ControlledGate, Float, Gate
8+
from opensquirrel.zyz_decomposer import ZYZDecomposer, get_zyz_decomposition_angles
9+
10+
11+
class CNOTDecomposer(Decomposer):
12+
"""
13+
Decomposes 2-qubit controlled unitary gates to CNOT + rz/ry.
14+
Applying single-qubit gate fusion after this pass might be beneficial.
15+
16+
Source of the math: https://threeplusone.com/pubs/on_gates.pdf, chapter 7.5 "ABC decomposition"
17+
"""
18+
19+
@staticmethod
20+
def decompose(g: Gate) -> [Gate]:
21+
if not isinstance(g, ControlledGate):
22+
# Do nothing:
23+
# - BlochSphereRotation's are only single-qubit,
24+
# - decomposing MatrixGate is currently not supported.
25+
return [g]
26+
27+
if not isinstance(g.target_gate, BlochSphereRotation):
28+
# Do nothing.
29+
# ControlledGate's with 2+ control qubits are ignored.
30+
return [g]
31+
32+
# Perform ZYZ decomposition on the target gate.
33+
# This gives us an ABC decomposition (U = AXBXC, ABC = I) of the target gate.
34+
# See https://threeplusone.com/pubs/on_gates.pdf
35+
theta0, theta1, theta2 = get_zyz_decomposition_angles(g.target_gate.angle, g.target_gate.axis)
36+
target_qubit = g.target_gate.qubit
37+
38+
# First try to see if we can get away with a single CNOT.
39+
# FIXME: see https://github.com/QuTech-Delft/OpenSquirrel/issues/99 this could be extended, I believe.
40+
if abs(abs(theta0 + theta2) % (2 * math.pi)) < ATOL and abs(abs(theta1 - math.pi) % (2 * math.pi)) < ATOL:
41+
# g == rz(theta0) Y rz(theta2) == rz(theta0 - pi / 2) X rz(theta2 + pi / 2)
42+
# theta0 + theta2 == 0
43+
44+
alpha0 = theta0 - math.pi / 2
45+
alpha2 = theta2 + math.pi / 2
46+
47+
return filter_out_identities(
48+
[
49+
rz(q=target_qubit, theta=Float(alpha2)),
50+
cnot(control=g.control_qubit, target=target_qubit),
51+
rz(q=target_qubit, theta=Float(alpha0)),
52+
rz(q=g.control_qubit, theta=Float(g.target_gate.phase - math.pi / 2)),
53+
]
54+
)
55+
56+
A = [ry(q=target_qubit, theta=Float(theta1 / 2)), rz(q=target_qubit, theta=Float(theta2))]
57+
58+
B = [
59+
rz(q=target_qubit, theta=Float(-(theta0 + theta2) / 2)),
60+
ry(q=target_qubit, theta=Float(-theta1 / 2)),
61+
]
62+
63+
C = [
64+
rz(q=target_qubit, theta=Float((theta0 - theta2) / 2)),
65+
]
66+
67+
return filter_out_identities(
68+
C
69+
+ [cnot(control=g.control_qubit, target=target_qubit)]
70+
+ B
71+
+ [cnot(control=g.control_qubit, target=target_qubit)]
72+
+ A
73+
+ [rz(q=g.control_qubit, theta=Float(g.target_gate.phase))]
74+
)

opensquirrel/default_gates.py

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

4545

46+
@named_gate
47+
def zm90(q: Qubit) -> Gate:
48+
return BlochSphereRotation(qubit=q, axis=(0, 0, 1), angle=-math.pi / 2, phase=0)
49+
50+
4651
@named_gate
4752
def rx(q: Qubit, theta: Float) -> Gate:
4853
return BlochSphereRotation(qubit=q, axis=(1, 0, 0), angle=theta.value, phase=0)
@@ -111,5 +116,10 @@ def sqrt_swap(q1: Qubit, q2: Qubit) -> Gate:
111116
)
112117

113118

114-
default_gate_set = [h, x, x90, xm90, y, y90, z, z90, cz, cr, crk, cnot, rx, ry, rz, x, swap, sqrt_swap]
119+
@named_gate
120+
def ccz(control1: Qubit, control2: Qubit, target: Qubit) -> Gate:
121+
return ControlledGate(control1, cz(control2, target))
122+
123+
124+
default_gate_set = [h, x, x90, xm90, y, y90, z, z90, zm90, cz, cr, crk, cnot, rx, ry, rz, x, swap, sqrt_swap, ccz]
115125
default_gate_aliases = {"X": x, "RX": rx, "RY": ry, "RZ": rz, "Hadamard": h, "H": h}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import math
2+
3+
from opensquirrel.common import ATOL
4+
from opensquirrel.default_gates import x, z
5+
from opensquirrel.squirrel_ir import (
6+
BlochSphereRotation,
7+
ControlledGate,
8+
MatrixGate,
9+
Qubit,
10+
SquirrelIR,
11+
SquirrelIRVisitor,
12+
)
13+
14+
try:
15+
import quantify_scheduler
16+
import quantify_scheduler.operations.gate_library as quantify_scheduler_gates
17+
except Exception as e:
18+
pass
19+
20+
21+
_unsupported_gates_exception = Exception(
22+
"Cannot export circuit: it contains unsupported gates - decompose them to the "
23+
"Quantify-scheduler gate set first (rxy, rz, cnot, cz)"
24+
)
25+
26+
27+
class _ScheduleCreator(SquirrelIRVisitor):
28+
def _get_qubit_string(self, q: Qubit) -> str:
29+
return f"{self.qubit_register_name}[{q.index}]"
30+
31+
def __init__(self, qubit_register_name: str):
32+
self.qubit_register_name = qubit_register_name
33+
self.schedule = quantify_scheduler.Schedule(f"Exported OpenSquirrel circuit")
34+
35+
def visit_bloch_sphere_rotation(self, g: BlochSphereRotation):
36+
if abs(g.axis[2]) < ATOL:
37+
# Rxy rotation.
38+
theta: float = g.angle
39+
phi: float = math.atan2(g.axis[1], g.axis[0])
40+
self.schedule.add(quantify_scheduler_gates.Rxy(theta=theta, phi=phi, qubit=self._get_qubit_string(g.qubit)))
41+
return
42+
43+
if abs(g.axis[0]) < ATOL and abs(g.axis[1]) < ATOL:
44+
# Rz rotation.
45+
self.schedule.add(quantify_scheduler_gates.Rz(theta=g.angle, qubit=self._get_qubit_string(g.qubit)))
46+
return
47+
48+
raise _unsupported_gates_exception
49+
50+
def visit_matrix_gate(self, g: MatrixGate):
51+
raise _unsupported_gates_exception
52+
53+
def visit_controlled_gate(self, g: ControlledGate):
54+
if not isinstance(g.target_gate, BlochSphereRotation):
55+
raise _unsupported_gates_exception
56+
57+
if g.target_gate == x(g.target_gate.qubit):
58+
self.schedule.add(
59+
quantify_scheduler_gates.CNOT(
60+
qC=self._get_qubit_string(g.control_qubit), qT=self._get_qubit_string(g.target_gate.qubit)
61+
)
62+
)
63+
return
64+
65+
if g.target_gate == z(g.target_gate.qubit):
66+
self.schedule.add(
67+
quantify_scheduler_gates.CZ(
68+
qC=self._get_qubit_string(g.control_qubit), qT=self._get_qubit_string(g.target_gate.qubit)
69+
)
70+
)
71+
return
72+
73+
raise _unsupported_gates_exception
74+
75+
76+
def export(squirrel_ir: SquirrelIR):
77+
if "quantify_scheduler" not in globals():
78+
79+
class QuantifySchedulerNotInstalled:
80+
def __getattr__(self, attr_name):
81+
raise Exception("quantify-scheduler is not installed, or cannot be installed on your system")
82+
83+
global quantify_scheduler
84+
quantify_scheduler = QuantifySchedulerNotInstalled()
85+
global quantify_scheduler_gates
86+
quantify_scheduler_gates = QuantifySchedulerNotInstalled()
87+
88+
schedule_creator = _ScheduleCreator(squirrel_ir.qubit_register_name)
89+
squirrel_ir.accept(schedule_creator)
90+
return schedule_creator.schedule

opensquirrel/export_format.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from enum import Enum
2+
3+
4+
class ExportFormat(Enum):
5+
QUANTIFY_SCHEDULER = 0

opensquirrel/identity_filter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from opensquirrel.squirrel_ir import Gate
2+
3+
4+
def filter_out_identities(l: [Gate]):
5+
return [g for g in l if not g.is_identity()]

opensquirrel/squirrel_ir.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def __init__(self, generator, arguments):
9292

9393
@property
9494
def name(self) -> Optional[str]:
95-
return self.generator.__name__ if self.generator else None
95+
return self.generator.__name__ if self.generator else "<anonymous>"
9696

9797
@property
9898
def is_anonymous(self) -> bool:
@@ -102,6 +102,10 @@ def is_anonymous(self) -> bool:
102102
def get_qubit_operands(self) -> List[Qubit]:
103103
raise NotImplementedError
104104

105+
@abstractmethod
106+
def is_identity(self) -> bool:
107+
raise NotImplementedError
108+
105109

106110
class BlochSphereRotation(Gate):
107111
generator: Optional[Callable[..., "BlochSphereRotation"]] = None
@@ -183,6 +187,9 @@ def accept(self, visitor: SquirrelIRVisitor):
183187
def get_qubit_operands(self) -> List[Qubit]:
184188
return self.operands
185189

190+
def is_identity(self) -> bool:
191+
return np.allclose(self.matrix, np.eye(2 ** len(self.operands)))
192+
186193

187194
class ControlledGate(Gate):
188195
generator: Optional[Callable[..., "ControlledGate"]] = None
@@ -210,17 +217,29 @@ def accept(self, visitor: SquirrelIRVisitor):
210217
def get_qubit_operands(self) -> List[Qubit]:
211218
return [self.control_qubit] + self.target_gate.get_qubit_operands()
212219

220+
def is_identity(self) -> bool:
221+
return self.target_gate.is_identity()
222+
213223

214224
def named_gate(gate_generator: Callable[..., Gate]) -> Callable[..., Gate]:
215225
@wraps(gate_generator)
216226
def wrapper(*args, **kwargs):
217-
for i, par in enumerate(inspect.signature(gate_generator).parameters.values()):
227+
result = gate_generator(*args, **kwargs)
228+
result.generator = wrapper
229+
230+
all_args = []
231+
arg_index = 0
232+
for par in inspect.signature(gate_generator).parameters.values():
218233
if not issubclass(par.annotation, Expression):
219234
raise TypeError("Gate argument types must be expressions")
220235

221-
result = gate_generator(*args, **kwargs)
222-
result.generator = wrapper
223-
result.arguments = args
236+
if par.name in kwargs:
237+
all_args.append(kwargs[par.name])
238+
else:
239+
all_args.append(args[arg_index])
240+
arg_index += 1
241+
242+
result.arguments = tuple(all_args)
224243
return result
225244

226245
return wrapper

opensquirrel/zyz_decomposer.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import math
2+
from typing import Tuple
3+
4+
from opensquirrel.common import ATOL
5+
from opensquirrel.default_gates import ry, rz
6+
from opensquirrel.identity_filter import filter_out_identities
7+
from opensquirrel.replacer import Decomposer
8+
from opensquirrel.squirrel_ir import BlochSphereRotation, Float, Gate
9+
10+
11+
def get_zyz_decomposition_angles(alpha: float, axis: Tuple[float, float, float]):
12+
"""
13+
Gives the angles used in the Z-Y-Z decomposition of the Bloch sphere rotation
14+
characterized by a rotation around `axis` of angle `alpha`.
15+
16+
Parameters:
17+
alpha: angle of the Bloch sphere rotation
18+
axis: _normalized_ axis of the Bloch sphere rotation
19+
20+
Returns:
21+
a triple (theta1, theta2, theta3) corresponding to the decomposition of the
22+
arbitrary Bloch sphere rotation into U = rz(theta3) ry(theta2) rz(theta1)
23+
"""
24+
25+
nx, ny, nz = axis
26+
27+
assert abs(nx**2 + ny**2 + nz**2 - 1) < ATOL, "Axis needs to be normalized"
28+
29+
assert -math.pi + ATOL < alpha <= math.pi + ATOL, "Angle needs to be normalized"
30+
31+
if abs(alpha - math.pi) < ATOL:
32+
# alpha == pi, math.tan(alpha / 2) is not defined.
33+
34+
if abs(nz) < ATOL:
35+
theta2 = math.pi
36+
p = 0
37+
m = 2 * math.acos(ny)
38+
39+
else:
40+
p = math.pi
41+
theta2 = 2 * math.acos(nz)
42+
43+
if abs(nz - 1) < ATOL or abs(nz + 1) < ATOL:
44+
m = p # This can be anything, but setting m = p means theta3 == 0, which is better for gate count.
45+
else:
46+
m = 2 * math.acos(ny / math.sqrt(1 - nz**2))
47+
48+
else:
49+
p = 2 * math.atan2(nz * math.sin(alpha / 2), math.cos(alpha / 2))
50+
51+
acos_argument = math.cos(alpha / 2) * math.sqrt(1 + (nz * math.tan(alpha / 2)) ** 2)
52+
53+
# This fixes float approximations like 1.0000000000002 which acos doesn't like.
54+
acos_argument = max(min(acos_argument, 1.0), -1.0)
55+
56+
theta2 = 2 * math.acos(acos_argument)
57+
theta2 = math.copysign(theta2, alpha)
58+
59+
if abs(math.sin(theta2 / 2)) < ATOL:
60+
m = p # This can be anything, but setting m = p means theta3 == 0, which is better for gate count.
61+
else:
62+
acos_argument = ny * math.sin(alpha / 2) / math.sin(theta2 / 2)
63+
64+
# This fixes float approximations like 1.0000000000002 which acos doesn't like.
65+
acos_argument = max(min(acos_argument, 1.0), -1.0)
66+
67+
m = 2 * math.acos(acos_argument)
68+
69+
theta1 = (p + m) / 2
70+
71+
theta3 = p - theta1
72+
73+
return theta1, theta2, theta3
74+
75+
76+
class ZYZDecomposer(Decomposer):
77+
@staticmethod
78+
def decompose(g: Gate) -> [Gate]:
79+
if not isinstance(g, BlochSphereRotation):
80+
# Only decompose single-qubit gates.
81+
return [g]
82+
83+
theta1, theta2, theta3 = get_zyz_decomposition_angles(g.angle, g.axis)
84+
85+
z1 = rz(g.qubit, Float(theta1))
86+
y = ry(g.qubit, Float(theta2))
87+
z2 = rz(g.qubit, Float(theta3))
88+
89+
# Note: written like this, the decomposition doesn't preserve the global phase, which is fine
90+
# since the global phase is a physically irrelevant artifact of the mathematical
91+
# model we use to describe the quantum system.
92+
93+
# Should we want to preserve it, we would need to use a raw BlochSphereRotation, which would then
94+
# be an anonymous gate in the resulting decomposed circuit:
95+
# z2 = BlochSphereRotation(qubit=g.qubit, angle=theta3, axis=(0, 0, 1), phase = g.phase)
96+
97+
return filter_out_identities([z1, y, z2])

0 commit comments

Comments
 (0)