Skip to content

Commit 5a0ffdf

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 00821a8 commit 5a0ffdf

13 files changed

+4165
-322
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from opensquirrel.default_gates import cnot, ry, rz
2+
from opensquirrel.identity_filter import filter_out_identities
3+
from opensquirrel.replacer import Decomposer
4+
from opensquirrel.squirrel_ir import BlochSphereRotation, ControlledGate, Float, Gate
5+
from opensquirrel.zyz_decomposer import ZYZDecomposer, theta123
6+
7+
8+
class CNOTDecomposer(Decomposer):
9+
"""
10+
Decomposes 2-qubit controlled unitary gates to CNOT + rz/ry.
11+
Applying single-qubit gate fusion after this pass might be beneficial.
12+
"""
13+
14+
@staticmethod
15+
def decompose(g: Gate) -> [Gate]:
16+
if not isinstance(g, ControlledGate):
17+
# Do nothing.
18+
# Decomposing MatrixGate is more mathematically involved.
19+
return [g]
20+
21+
if not isinstance(g.target_gate, BlochSphereRotation):
22+
# Do nothing.
23+
# ControlledGate's with 2+ control qubits are ignored.
24+
return [g]
25+
26+
# Perform ZYZ decomposition on the target gate.
27+
# This gives us an ABC decomposition (U = AXBXC, ABC = I) of the target gate.
28+
theta0, theta1, theta2 = theta123(g.target_gate.angle, g.target_gate.axis)
29+
30+
target_qubit = g.target_gate.qubit
31+
32+
A = [ry(q=target_qubit, theta=Float(theta1 / 2)), rz(q=target_qubit, theta=Float(theta2))]
33+
34+
B = [
35+
rz(q=target_qubit, theta=Float(-(theta0 + theta2) / 2)),
36+
ry(q=target_qubit, theta=Float(-theta1 / 2)),
37+
]
38+
39+
C = [
40+
rz(q=target_qubit, theta=Float((theta0 - theta2) / 2)),
41+
]
42+
43+
return filter_out_identities(
44+
C
45+
+ [cnot(control=g.control_qubit, target=target_qubit)]
46+
+ B
47+
+ [cnot(control=g.control_qubit, target=target_qubit)]
48+
+ A
49+
+ [rz(q=g.control_qubit, theta=Float(g.target_gate.phase))]
50+
)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
class _ScheduleCreator(SquirrelIRVisitor):
22+
def _get_qubit_string(self, q: Qubit) -> str:
23+
return f"{self.qubit_register_name}[{q.index}]"
24+
25+
def __init__(self, qubit_register_name: str):
26+
self.qubit_register_name = qubit_register_name
27+
self.schedule = quantify_scheduler.Schedule(f"Exported OpenSquirrel circuit")
28+
29+
def visit_bloch_sphere_rotation(self, g: BlochSphereRotation):
30+
if abs(g.axis[2]) < ATOL:
31+
# Rxy rotation.
32+
theta: float = g.angle
33+
phi: float = math.atan2(g.axis[1], g.axis[0])
34+
self.schedule.add(quantify_scheduler_gates.Rxy(theta=theta, phi=phi, qubit=self._get_qubit_string(g.qubit)))
35+
return
36+
37+
if abs(g.axis[0]) < ATOL and abs(g.axis[1]) < ATOL:
38+
# Rz rotation.
39+
self.schedule.add(quantify_scheduler_gates.Rz(theta=g.angle, qubit=self._get_qubit_string(g.qubit)))
40+
return
41+
42+
raise Exception(
43+
"Cannot export circuit: it contains unsupported gates - decompose them to the "
44+
"Quantify-scheduler gate set first"
45+
)
46+
47+
def visit_matrix_gate(self, g: MatrixGate):
48+
raise Exception("Unimplemented: would require a complicated mathematical decomposition...")
49+
50+
def visit_controlled_gate(self, g: ControlledGate):
51+
if not isinstance(g.target_gate, BlochSphereRotation):
52+
raise Exception(
53+
"Cannot export circuit: it contains unsupported gates - decompose them to the "
54+
"Quantify-scheduler gate set first"
55+
)
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 Exception(
74+
"Cannot export circuit: it contains unsupported gates - decompose them to the "
75+
"Quantify-scheduler gate set first"
76+
)
77+
78+
79+
def export(squirrel_ir: SquirrelIR):
80+
if "quantify_scheduler" not in globals():
81+
82+
class QuantifySchedulerNotInstalled:
83+
def __getattr__(self, attr_name):
84+
raise Exception("quantify-scheduler is not installed, or cannot be installed on your system")
85+
86+
global quantify_scheduler
87+
quantify_scheduler = QuantifySchedulerNotInstalled()
88+
global quantify_scheduler_gates
89+
quantify_scheduler_gates = QuantifySchedulerNotInstalled()
90+
91+
schedule_creator = _ScheduleCreator(squirrel_ir.qubit_register_name)
92+
squirrel_ir.accept(schedule_creator)
93+
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 theta123(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+
caracterized 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+
p = math.pi
34+
theta2 = 2 * math.acos(nz)
35+
36+
if abs(nz - 1) < ATOL or abs(nz + 1) < ATOL:
37+
m = p # This can be anything, but setting m = p means theta3 == 0, which is better for gate count.
38+
else:
39+
m = 2 * math.acos(ny / math.sqrt(1 - nz**2))
40+
41+
else:
42+
p = 2 * math.atan2(nz * math.sin(alpha / 2), math.cos(alpha / 2))
43+
theta2 = 2 * math.acos(math.cos(alpha / 2) * math.sqrt(1 + (nz * math.tan(alpha / 2)) ** 2))
44+
theta2 = math.copysign(theta2, alpha)
45+
46+
if abs(math.sin(theta2 / 2)) < ATOL:
47+
m = p # This can be anything, but setting m = p means theta3 == 0, which is better for gate count.
48+
else:
49+
m = 2 * math.acos(ny * math.sin(alpha / 2) / math.sin(theta2 / 2))
50+
51+
theta1 = (p + m) / 2
52+
53+
theta3 = p - theta1
54+
55+
return theta1, theta2, theta3
56+
57+
58+
class ZYZDecomposer(Decomposer):
59+
@staticmethod
60+
def decompose(g: Gate) -> [Gate]:
61+
if not isinstance(g, BlochSphereRotation):
62+
# Only decompose single-qubit gates.
63+
return [g]
64+
65+
theta1, theta2, theta3 = theta123(g.angle, g.axis)
66+
67+
z1 = rz(g.qubit, Float(theta1))
68+
y = ry(g.qubit, Float(theta2))
69+
z2 = rz(g.qubit, Float(theta3))
70+
71+
# Note: written like this, the decomposition doesn't preserve the global phase, which is fine
72+
# since the global phase is a physically irrelevant artifact of the mathematical
73+
# model we use to describe the quantum system.
74+
75+
# Should we want to preserve it, we would need to use a raw BlochSphereRotation, which would then
76+
# be an anonymous gate in the resulting decomposed circuit:
77+
# z2 = BlochSphereRotation(qubit=g.qubit, angle=theta3, axis=(0, 0, 1), phase = g.phase)
78+
79+
return filter_out_identities([z1, y, z2])

0 commit comments

Comments
 (0)