compiler-config 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- compiler_config-0.1.0/LICENSE +28 -0
- compiler_config-0.1.0/PKG-INFO +23 -0
- compiler_config-0.1.0/README.md +9 -0
- compiler_config-0.1.0/compiler_config/__init__.py +0 -0
- compiler_config-0.1.0/compiler_config/config.py +447 -0
- compiler_config-0.1.0/compiler_config/serialiser.py +181 -0
- compiler_config-0.1.0/pyproject.toml +48 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, oqc-community
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: compiler-config
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: jamie
|
|
6
|
+
Author-email: jfriel@oxfordquantumcircuits.com
|
|
7
|
+
Requires-Python: >=3.8.1,<3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# compiler-config
|
|
16
|
+
|
|
17
|
+
OQC's task level compiler configuration. The compiler-config package is an internal dependency of [QAT](https://github.com/oqc-community/qat)
|
|
18
|
+
(OQC's compiler and runtime) and will soon be adopted as a dependency of our QCaaS (Quantum Computing as a Service) client. *End users should not need to interact with this package except via QAT or QCAAS client.*
|
|
19
|
+
|
|
20
|
+
#### Licence
|
|
21
|
+
|
|
22
|
+
This code in this repository is licensed under the BSD 3-Clause Licence.
|
|
23
|
+
Please see [LICENSE](LICENSE) for more information.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# compiler-config
|
|
2
|
+
|
|
3
|
+
OQC's task level compiler configuration. The compiler-config package is an internal dependency of [QAT](https://github.com/oqc-community/qat)
|
|
4
|
+
(OQC's compiler and runtime) and will soon be adopted as a dependency of our QCaaS (Quantum Computing as a Service) client. *End users should not need to interact with this package except via QAT or QCAAS client.*
|
|
5
|
+
|
|
6
|
+
#### Licence
|
|
7
|
+
|
|
8
|
+
This code in this repository is licensed under the BSD 3-Clause Licence.
|
|
9
|
+
Please see [LICENSE](LICENSE) for more information.
|
|
File without changes
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2023 Oxford Quantum Circuits Ltd
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from enum import Enum, Flag, IntEnum, auto
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from compiler_config.serialiser import json_dumps, json_loads
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InlineResultsProcessing(Flag):
|
|
15
|
+
"""
|
|
16
|
+
Results transforms applied directly to the read-out value on the QPU. In most
|
|
17
|
+
situations applied post-execution, but can also be interwoven.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Raw readout from the QPU. Normally Z-axis values for each shot.
|
|
21
|
+
Raw = auto()
|
|
22
|
+
|
|
23
|
+
# Shot results averaged out to get a single 0/1 value for each qubit.
|
|
24
|
+
Binary = auto()
|
|
25
|
+
|
|
26
|
+
# Return the values in numpy arrays not Python lists.
|
|
27
|
+
NumpyArrays = auto()
|
|
28
|
+
|
|
29
|
+
Experiment = Raw | NumpyArrays
|
|
30
|
+
Program = Binary
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
return self.name
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ResultsFormatting(Flag):
|
|
37
|
+
# Transforms each shot into binary then counts the instances.
|
|
38
|
+
# Example for two qubits: { '00': 15, '01': 2524, '10': 250, '11': 730 }
|
|
39
|
+
BinaryCount = auto()
|
|
40
|
+
|
|
41
|
+
# Change results value based on conditions for ease-of-use. Set as a flag because
|
|
42
|
+
# it means that return values from execution may change format unexpectedly, so
|
|
43
|
+
# should have a way to disable it for certain uses.
|
|
44
|
+
DynamicStructureReturn = auto()
|
|
45
|
+
|
|
46
|
+
# If your qubit results are lists of binary, squash to one string representation.
|
|
47
|
+
# Changes 1: [1, 0, 0, 1] to 1: '1001'.
|
|
48
|
+
# Only works when Binary format is returned.
|
|
49
|
+
SquashBinaryResultArrays = auto()
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return self.name
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class QuantumResultsFormat:
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self.format: Optional[InlineResultsProcessing] = None
|
|
58
|
+
self.transforms: Optional[ResultsFormatting] = (
|
|
59
|
+
ResultsFormatting.DynamicStructureReturn
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def raw(self) -> QuantumResultsFormat:
|
|
63
|
+
self.format = InlineResultsProcessing.Raw
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def binary(self) -> QuantumResultsFormat:
|
|
67
|
+
self.format = InlineResultsProcessing.Binary
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def binary_count(self):
|
|
71
|
+
"""
|
|
72
|
+
Returns a count of each instance of measured qubit registers.
|
|
73
|
+
Switches result format to raw.
|
|
74
|
+
"""
|
|
75
|
+
self.transforms = (
|
|
76
|
+
ResultsFormatting.BinaryCount | ResultsFormatting.DynamicStructureReturn
|
|
77
|
+
)
|
|
78
|
+
self.format = InlineResultsProcessing.Raw
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def squash_binary_result_arrays(self):
|
|
82
|
+
"""
|
|
83
|
+
Squashes binary result list into a singular bit string. Switches results to
|
|
84
|
+
binary.
|
|
85
|
+
"""
|
|
86
|
+
self.transforms = (
|
|
87
|
+
ResultsFormatting.SquashBinaryResultArrays
|
|
88
|
+
| ResultsFormatting.DynamicStructureReturn
|
|
89
|
+
)
|
|
90
|
+
self.format = InlineResultsProcessing.Binary
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def __contains__(self, other):
|
|
94
|
+
if isinstance(other, ResultsFormatting):
|
|
95
|
+
return self.transforms.__contains__(other)
|
|
96
|
+
elif isinstance(other, InlineResultsProcessing):
|
|
97
|
+
return self.format.__contains__(other)
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def __or__(self, other):
|
|
101
|
+
if isinstance(other, ResultsFormatting):
|
|
102
|
+
self.transforms = self.transforms.__or__(other)
|
|
103
|
+
elif isinstance(other, InlineResultsProcessing):
|
|
104
|
+
self.format = self.format.__or__(other)
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __and__(self, other):
|
|
108
|
+
if isinstance(other, ResultsFormatting):
|
|
109
|
+
self.transforms = self.transforms.__and__(other)
|
|
110
|
+
elif isinstance(other, InlineResultsProcessing):
|
|
111
|
+
self.format = self.format.__and__(other)
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def __xor__(self, other):
|
|
115
|
+
if isinstance(other, ResultsFormatting):
|
|
116
|
+
self.transforms = self.transforms.__xor__(other)
|
|
117
|
+
elif isinstance(other, InlineResultsProcessing):
|
|
118
|
+
self.format = self.format.__xor__(other)
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return f"Format: {str(self.format)}. Transforms: {str(self.transforms)}."
|
|
123
|
+
|
|
124
|
+
def __eq__(self, other):
|
|
125
|
+
return self.format == other.format and self.transforms == other.transforms
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TketOptimizations(Flag):
|
|
129
|
+
"""Flags for the various Tket optimizations we can apply."""
|
|
130
|
+
|
|
131
|
+
Empty = auto()
|
|
132
|
+
DefaultMappingPass = auto()
|
|
133
|
+
FullPeepholeOptimise = auto()
|
|
134
|
+
ContextSimp = auto()
|
|
135
|
+
DirectionalCXGates = auto()
|
|
136
|
+
CliffordSimp = auto()
|
|
137
|
+
DecomposeArbitrarilyControlledGates = auto()
|
|
138
|
+
# EulerAngleReduction = auto()
|
|
139
|
+
GlobalisePhasedX = auto()
|
|
140
|
+
# GuidedPauliSimp = auto()
|
|
141
|
+
KAKDecomposition = auto()
|
|
142
|
+
# OptimisePhaseGadgets = auto()
|
|
143
|
+
# PauliSimp = auto()
|
|
144
|
+
# PauliSquash = auto()
|
|
145
|
+
PeepholeOptimise2Q = auto()
|
|
146
|
+
RemoveDiscarded = auto()
|
|
147
|
+
RemoveBarriers = auto()
|
|
148
|
+
RemoveRedundancies = auto()
|
|
149
|
+
ThreeQubitSquash = auto()
|
|
150
|
+
SimplifyMeasured = auto()
|
|
151
|
+
|
|
152
|
+
One = DefaultMappingPass | DirectionalCXGates
|
|
153
|
+
Two = One | FullPeepholeOptimise | ContextSimp
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class QiskitOptimizations(Flag):
|
|
157
|
+
"""Flags for the various Qiskit optimizations we can apply."""
|
|
158
|
+
|
|
159
|
+
Empty = auto()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class QatOptimizations(Flag):
|
|
163
|
+
"""Flags for the various Qat optimizations we can apply."""
|
|
164
|
+
|
|
165
|
+
Empty = auto()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class MetricsType(Flag):
|
|
169
|
+
Empty = auto()
|
|
170
|
+
|
|
171
|
+
# Returns circuit after optimizations have been run.
|
|
172
|
+
OptimizedCircuit = auto()
|
|
173
|
+
|
|
174
|
+
# Count of transformed instructions after all forms of optimizations have
|
|
175
|
+
# been performed.
|
|
176
|
+
OptimizedInstructionCount = auto()
|
|
177
|
+
|
|
178
|
+
# Set of basic metrics that should be returned at all times.
|
|
179
|
+
Default = OptimizedCircuit | OptimizedInstructionCount
|
|
180
|
+
|
|
181
|
+
def is_composite(self):
|
|
182
|
+
"""
|
|
183
|
+
Any flags that are only composed of other ones should be signaled here. This
|
|
184
|
+
is used for automatic metric generation and whether to build/validate this
|
|
185
|
+
particular value.
|
|
186
|
+
"""
|
|
187
|
+
return self == self.Default or self == self.Empty
|
|
188
|
+
|
|
189
|
+
def snake_case_name(self):
|
|
190
|
+
"""
|
|
191
|
+
Generate the Python field name that'll be used to hold the results of this
|
|
192
|
+
metric.
|
|
193
|
+
"""
|
|
194
|
+
name = self.name
|
|
195
|
+
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
|
196
|
+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ErrorMitigationConfig(Flag):
|
|
200
|
+
Empty = auto()
|
|
201
|
+
MatrixMitigation = auto()
|
|
202
|
+
LinearMitigation = auto()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ExperimentalFeatures:
|
|
206
|
+
error_mitigation = ErrorMitigationConfig.Empty
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class CompilerConfig:
|
|
210
|
+
"""
|
|
211
|
+
Full settings for the compiler. All values are defaulted on initialization.
|
|
212
|
+
|
|
213
|
+
If no explicit optimizations are passed then the default set of optimization for the
|
|
214
|
+
language you're attempting to compile will be applied.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
repeats=None,
|
|
220
|
+
repetition_period=None,
|
|
221
|
+
results_format: QuantumResultsFormat = None,
|
|
222
|
+
metrics=MetricsType.Default,
|
|
223
|
+
active_calibrations=None,
|
|
224
|
+
optimizations: "OptimizationConfig" = None,
|
|
225
|
+
error_mitigation: ErrorMitigationConfig = None,
|
|
226
|
+
):
|
|
227
|
+
self.repeats: Optional[int] = repeats
|
|
228
|
+
self.repetition_period: Optional[float] = repetition_period
|
|
229
|
+
self.results_format: QuantumResultsFormat = results_format or QuantumResultsFormat()
|
|
230
|
+
self.metrics: MetricsType = metrics
|
|
231
|
+
self.active_calibrations: List[CalibrationArguments] = active_calibrations or []
|
|
232
|
+
self.optimizations: Optional[OptimizationConfig] = optimizations
|
|
233
|
+
self.error_mitigation: Optional[ErrorMitigationConfig] = error_mitigation
|
|
234
|
+
|
|
235
|
+
def to_json(self):
|
|
236
|
+
return json_dumps(self, serializable_types=get_serializable_types())
|
|
237
|
+
|
|
238
|
+
def from_json(self, json: str):
|
|
239
|
+
vars(self).update(
|
|
240
|
+
vars(json_loads(json, serializable_types=get_serializable_types()))
|
|
241
|
+
)
|
|
242
|
+
return self
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def create_from_json(cls, json: str):
|
|
246
|
+
return CompilerConfig().from_json(json)
|
|
247
|
+
|
|
248
|
+
def validate(self, hardware):
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
self.error_mitigation is not None
|
|
252
|
+
and self.error_mitigation != ErrorMitigationConfig.Empty
|
|
253
|
+
):
|
|
254
|
+
if (
|
|
255
|
+
hardware.error_mitigation is None
|
|
256
|
+
or not hardware.error_mitigation.readout_mitigation
|
|
257
|
+
):
|
|
258
|
+
raise ValueError("Error mitigation not calibrated on this device.")
|
|
259
|
+
if ResultsFormatting.BinaryCount not in self.results_format:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"Binary Count format required for readout error mitigation"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class CalibrationArguments:
|
|
266
|
+
"""Base class for individual calibration arguments."""
|
|
267
|
+
|
|
268
|
+
def to_json(self):
|
|
269
|
+
return json_dumps(self)
|
|
270
|
+
|
|
271
|
+
def from_json(self, json: str):
|
|
272
|
+
self.from_dict(json_loads(json))
|
|
273
|
+
|
|
274
|
+
def _get_field_names(self):
|
|
275
|
+
"""Get existing field names for all attributes and properties"""
|
|
276
|
+
existing_field_names = set(vars(self).keys())
|
|
277
|
+
# Include class properties
|
|
278
|
+
class_type = type(self)
|
|
279
|
+
for prop in dir(class_type):
|
|
280
|
+
if isinstance(getattr(class_type, prop), property):
|
|
281
|
+
existing_field_names.add(prop)
|
|
282
|
+
|
|
283
|
+
return existing_field_names
|
|
284
|
+
|
|
285
|
+
def from_dict(self, dict_values):
|
|
286
|
+
"""
|
|
287
|
+
Loads this dictionary into the arguments. Throws if key dosen't exist on the
|
|
288
|
+
object.
|
|
289
|
+
"""
|
|
290
|
+
valid_names = self._get_field_names()
|
|
291
|
+
invalid_fields = [val for val in dict_values.keys() if val not in valid_names]
|
|
292
|
+
if any(invalid_fields):
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"Field(s) {','.join(invalid_fields)} are not valid for "
|
|
295
|
+
f"{self.__class__.__name__}."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
vars(self).update(dict_values)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class Languages(IntEnum):
|
|
302
|
+
Empty, Qasm2, Qasm3, QIR = range(4)
|
|
303
|
+
|
|
304
|
+
def __repr__(self):
|
|
305
|
+
return self.name
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class OptimizationConfig:
|
|
309
|
+
"""
|
|
310
|
+
Base class for instantiated optimizations as well as mix-in classes. Built this way
|
|
311
|
+
so we can mix and match optimization objects across multiple setups and languages
|
|
312
|
+
without duplication.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def __init__(self):
|
|
316
|
+
super().__init__()
|
|
317
|
+
|
|
318
|
+
def default(self):
|
|
319
|
+
"""Apply default set of optimizations to the current set."""
|
|
320
|
+
return self
|
|
321
|
+
|
|
322
|
+
def disable(self):
|
|
323
|
+
"""Disable all optimizations."""
|
|
324
|
+
return self
|
|
325
|
+
|
|
326
|
+
def minimum(self):
|
|
327
|
+
"""
|
|
328
|
+
Apply minimum working set for current optimizations.
|
|
329
|
+
"""
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
def __contains__(self, item):
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class Tket(OptimizationConfig):
|
|
337
|
+
def __init__(self, tket_optimization=None):
|
|
338
|
+
super().__init__()
|
|
339
|
+
self.tket_optimizations: TketOptimizations = TketOptimizations.Empty
|
|
340
|
+
if tket_optimization is not None:
|
|
341
|
+
self.tket_optimizations = tket_optimization
|
|
342
|
+
|
|
343
|
+
def default(self):
|
|
344
|
+
self.tket_optimizations = TketOptimizations.One
|
|
345
|
+
return self
|
|
346
|
+
|
|
347
|
+
def disable(self):
|
|
348
|
+
self.tket_optimizations = TketOptimizations.Empty
|
|
349
|
+
return self
|
|
350
|
+
|
|
351
|
+
def minimum(self):
|
|
352
|
+
self.tket_optimizations = TketOptimizations.DefaultMappingPass
|
|
353
|
+
return self
|
|
354
|
+
|
|
355
|
+
def __contains__(self, item):
|
|
356
|
+
if isinstance(item, TketOptimizations) and item in self.tket_optimizations:
|
|
357
|
+
return True
|
|
358
|
+
return super().__contains__(item)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class Qiskit(OptimizationConfig):
|
|
362
|
+
def __init__(self):
|
|
363
|
+
super().__init__()
|
|
364
|
+
self.qiskit_optimizations: QiskitOptimizations = QiskitOptimizations.Empty
|
|
365
|
+
|
|
366
|
+
def default(self):
|
|
367
|
+
self.qiskit_optimizations = QiskitOptimizations.Empty
|
|
368
|
+
return self
|
|
369
|
+
|
|
370
|
+
def __contains__(self, item):
|
|
371
|
+
if isinstance(item, QiskitOptimizations) and item in self.qiskit_optimizations:
|
|
372
|
+
return True
|
|
373
|
+
return super().__contains__(item)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class Qasm2Optimizations(Tket, Qiskit):
|
|
377
|
+
def __init__(self):
|
|
378
|
+
super().__init__()
|
|
379
|
+
self.default()
|
|
380
|
+
|
|
381
|
+
def __repr__(self):
|
|
382
|
+
return f"Qiskit: {self.qiskit_optimizations}. Tket: {self.tket_optimizations}."
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class Qasm3Optimizations(OptimizationConfig):
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class QIROptimizations(OptimizationConfig):
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_optimizer_config(lang: Languages) -> Optional[OptimizationConfig]:
|
|
394
|
+
"""
|
|
395
|
+
Returns the optimization config for this particular language. None if no valid ones
|
|
396
|
+
found.
|
|
397
|
+
"""
|
|
398
|
+
if lang == Languages.Qasm2:
|
|
399
|
+
return Qasm2Optimizations()
|
|
400
|
+
elif lang == Languages.Qasm3:
|
|
401
|
+
return Qasm3Optimizations()
|
|
402
|
+
elif lang == Languages.QIR:
|
|
403
|
+
return QIROptimizations()
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def get_config(lang: Languages, **kwargs):
|
|
408
|
+
"""
|
|
409
|
+
Helper method to build a compiler config for a particular language. Forwards
|
|
410
|
+
keywords to the CompilerConfig constructor.
|
|
411
|
+
"""
|
|
412
|
+
config = CompilerConfig(**kwargs)
|
|
413
|
+
config.optimizations = get_optimizer_config(lang)
|
|
414
|
+
return config
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
serializable_types_dict = None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def get_serializable_types():
|
|
421
|
+
global serializable_types_dict
|
|
422
|
+
|
|
423
|
+
if serializable_types_dict is not None:
|
|
424
|
+
return serializable_types_dict
|
|
425
|
+
|
|
426
|
+
serializable_types = {}
|
|
427
|
+
|
|
428
|
+
def update_dict(type):
|
|
429
|
+
if issubclass(type, Enum):
|
|
430
|
+
serializable_types.update({f"<enum '{type.__module__}.{type.__name__}'>": type})
|
|
431
|
+
else:
|
|
432
|
+
serializable_types.update({str(type): type})
|
|
433
|
+
|
|
434
|
+
def get_serializable_types_dict(type):
|
|
435
|
+
update_dict(type)
|
|
436
|
+
|
|
437
|
+
for subclass in type.__subclasses__():
|
|
438
|
+
get_serializable_types_dict(subclass)
|
|
439
|
+
|
|
440
|
+
types_in_module = inspect.getmembers(sys.modules[__name__], inspect.isclass)
|
|
441
|
+
|
|
442
|
+
for name, typ in types_in_module:
|
|
443
|
+
get_serializable_types_dict(typ)
|
|
444
|
+
|
|
445
|
+
serializable_types_dict = serializable_types
|
|
446
|
+
|
|
447
|
+
return serializable_types_dict
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
2
|
+
# Copyright (c) 2023 Oxford Quantum Circuits Ltd
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import asdict, is_dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from importlib import import_module
|
|
9
|
+
from json import JSONDecoder, JSONEncoder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Set of common methods so we don't have to add/remove the custom serializer.
|
|
13
|
+
def json_dumps(*args, serializable_types=None, **kwargs):
|
|
14
|
+
kwargs.setdefault("cls", CustomJSONEncoder)
|
|
15
|
+
return json.dumps(*args, serializable_types=serializable_types, **kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def json_loads(*args, serializable_types=None, **kwargs):
|
|
19
|
+
kwargs.setdefault("cls", CustomJsonDecoder)
|
|
20
|
+
return json.loads(*args, serializable_types=serializable_types, **kwargs)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def json_dump(*args, serializable_types=None, **kwargs):
|
|
24
|
+
kwargs.setdefault("cls", CustomJSONEncoder)
|
|
25
|
+
return json.dump(*args, serializable_types=serializable_types, **kwargs)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def json_load(*args, serializable_types=None, **kwargs):
|
|
29
|
+
kwargs.setdefault("cls", CustomJsonDecoder)
|
|
30
|
+
return json.load(*args, serializable_types=serializable_types, **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CustomJsonDecoder(JSONDecoder):
|
|
34
|
+
def __init__(self, *args, serializable_types=None, model=None, **kwargs):
|
|
35
|
+
self.serializable_types = serializable_types
|
|
36
|
+
super().__init__(object_hook=self.default, *args, **kwargs)
|
|
37
|
+
|
|
38
|
+
def default(self, obj):
|
|
39
|
+
if not isinstance(obj, dict):
|
|
40
|
+
return obj
|
|
41
|
+
|
|
42
|
+
# Components are objects directly related to hardware and you never want to serialize them, so we re-link
|
|
43
|
+
# upon deserialization.
|
|
44
|
+
|
|
45
|
+
obj_type = obj.get("$type")
|
|
46
|
+
if obj_type is None:
|
|
47
|
+
return obj
|
|
48
|
+
|
|
49
|
+
if obj_type == "<class 'tuple'>":
|
|
50
|
+
return tuple(obj["$data"])
|
|
51
|
+
|
|
52
|
+
if self.serializable_types is not None:
|
|
53
|
+
old_paths = ["scc.compiler.config", "qat.purr.compiler.config"]
|
|
54
|
+
for old_path in old_paths:
|
|
55
|
+
if old_path in obj_type:
|
|
56
|
+
obj_type = obj_type.replace(old_path, "compiler_config.config")
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
typ = self.serializable_types.get(obj_type)
|
|
60
|
+
if typ is None:
|
|
61
|
+
raise ValueError(f"Invalid type attempted to be serialized: {obj_type}.")
|
|
62
|
+
else:
|
|
63
|
+
typ = _get_type(obj_type)
|
|
64
|
+
|
|
65
|
+
if issubclass(typ, Enum):
|
|
66
|
+
return typ(obj["$value"])
|
|
67
|
+
|
|
68
|
+
if "$data" in obj:
|
|
69
|
+
data = obj["$data"]
|
|
70
|
+
if obj.get("$dataclass", False):
|
|
71
|
+
fields = self.default(data)
|
|
72
|
+
return typ(**fields)
|
|
73
|
+
elif isinstance(data, dict):
|
|
74
|
+
new_obj = object.__new__(typ)
|
|
75
|
+
new_obj.__dict__ = {key: self.default(value) for key, value in data.items()}
|
|
76
|
+
return new_obj
|
|
77
|
+
elif isinstance(data, str):
|
|
78
|
+
return typ(data)
|
|
79
|
+
else:
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
return obj
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CustomJSONEncoder(JSONEncoder):
|
|
86
|
+
"""
|
|
87
|
+
It is a customised JSON encoder, which allows the serialization of the more complex
|
|
88
|
+
objects.
|
|
89
|
+
|
|
90
|
+
There are four major cases, based on the provided object to be serialized:
|
|
91
|
+
|
|
92
|
+
- if the type of the object is supported by the default :class:`JSONEncoder`, than
|
|
93
|
+
the default method is used.
|
|
94
|
+
- if the class of the object is a :class:`dataclass`, then the serialization will
|
|
95
|
+
contain the name of the type, ``dataclass`` flag in order to help at the
|
|
96
|
+
deserialization, and the dictionary of the fields.
|
|
97
|
+
- if the object is none from the above, then the type name is saved, and the
|
|
98
|
+
interior data using ``__dict__``.
|
|
99
|
+
- if an exception is encountered from any cases from above (e.g. ``__dict__`` is not
|
|
100
|
+
available in case of complex numbers), then the type name is saved, and the data
|
|
101
|
+
is the string representation of the object.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, *args, serializable_types=None, **kwargs):
|
|
105
|
+
self.serializable_types = serializable_types
|
|
106
|
+
super().__init__(*args, **kwargs)
|
|
107
|
+
|
|
108
|
+
def default(self, obj):
|
|
109
|
+
typ = type(obj)
|
|
110
|
+
if issubclass(typ, Enum):
|
|
111
|
+
typ_str = f"<enum '{obj.__module__}.{typ.__name__}'>"
|
|
112
|
+
else:
|
|
113
|
+
typ_str = str(typ)
|
|
114
|
+
|
|
115
|
+
if self.serializable_types is not None:
|
|
116
|
+
if (
|
|
117
|
+
typ_str not in self.serializable_types
|
|
118
|
+
and type(obj).__module__ != "builtins"
|
|
119
|
+
):
|
|
120
|
+
raise ValueError(f"Invalid type attempted to be serialized: {(type(obj))}.")
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
|
|
124
|
+
# TODO: Acquire is a special wrapper component, not an actual component. Have a few too many special-cases
|
|
125
|
+
# for it now, think about reverting its special status.
|
|
126
|
+
if is_dataclass(obj):
|
|
127
|
+
return {
|
|
128
|
+
"$type": typ_str,
|
|
129
|
+
"$dataclass": True,
|
|
130
|
+
"$data": self.default(asdict(obj)),
|
|
131
|
+
}
|
|
132
|
+
elif isinstance(obj, Enum):
|
|
133
|
+
return {"$type": typ_str, "$value": obj.value}
|
|
134
|
+
elif isinstance(obj, complex):
|
|
135
|
+
return {"$type": typ_str, "$data": str(obj)}
|
|
136
|
+
elif isinstance(obj, tuple):
|
|
137
|
+
return {
|
|
138
|
+
"$type": typ_str,
|
|
139
|
+
"$data": tuple(self.default(val) for val in obj),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if hasattr(obj, "__dict__"):
|
|
143
|
+
return {
|
|
144
|
+
"$type": typ_str,
|
|
145
|
+
"$data": {
|
|
146
|
+
key: self.default(value) for key, value in obj.__dict__.items()
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
except (TypeError, AttributeError):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
return obj
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_type_name_matcher = r"<(?:class|enum) '((?:\w+\.)*)(\w+)'>"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_type(s: str):
|
|
159
|
+
match = re.match(_type_name_matcher, s)
|
|
160
|
+
if match:
|
|
161
|
+
namespace = match.group(1)
|
|
162
|
+
if namespace != "":
|
|
163
|
+
expanded_namespace = namespace[:-1].split(".")
|
|
164
|
+
module_name = expanded_namespace.pop(0)
|
|
165
|
+
imported_module = import_module(module_name)
|
|
166
|
+
for item in expanded_namespace:
|
|
167
|
+
module_name = f"{module_name}.{item}"
|
|
168
|
+
try:
|
|
169
|
+
imported_module = getattr(imported_module, item)
|
|
170
|
+
except AttributeError:
|
|
171
|
+
import_module(module_name)
|
|
172
|
+
imported_module = getattr(imported_module, item)
|
|
173
|
+
else:
|
|
174
|
+
imported_module = sys.modules["builtins"]
|
|
175
|
+
try:
|
|
176
|
+
return getattr(imported_module, match.group(2))
|
|
177
|
+
except AttributeError:
|
|
178
|
+
raise AttributeError(
|
|
179
|
+
f"Class {s} not found in built-in modules or namespace '{namespace}'"
|
|
180
|
+
)
|
|
181
|
+
return None
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "compiler-config"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["jamie <jfriel@oxfordquantumcircuits.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
packages = [
|
|
9
|
+
{ include = "compiler_config" }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = ">=3.8.1,<3.13"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
pytest = "^8.3.2"
|
|
19
|
+
isort = "^5.13.2"
|
|
20
|
+
black = "^24.4.2"
|
|
21
|
+
autoflake = "^2.3.1"
|
|
22
|
+
pip-audit = "^2.7.3"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
[tool.autoflake]
|
|
26
|
+
remove-all-unused-imports = true
|
|
27
|
+
ignore-init-module-imports = true
|
|
28
|
+
ignore-pass-after-docstring = true
|
|
29
|
+
in-place = true
|
|
30
|
+
recursive = true
|
|
31
|
+
|
|
32
|
+
[tool.isort]
|
|
33
|
+
py_version = 39
|
|
34
|
+
profile = "black"
|
|
35
|
+
line_length = 92
|
|
36
|
+
known_first_party = ["qat"]
|
|
37
|
+
|
|
38
|
+
[tool.black]
|
|
39
|
+
line-length = 92
|
|
40
|
+
target-version = ["py38", "py39", "py310"]
|
|
41
|
+
|
|
42
|
+
[tool.poetry.scripts]
|
|
43
|
+
format-code = "poetry_scripts:format_code"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["poetry-core"]
|
|
48
|
+
build-backend = "poetry.core.masonry.api"
|