boulder-opal-scale-up-sdk 1.0.0__py3-none-any.whl
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.
- boulder_opal_scale_up_sdk-1.0.0.dist-info/METADATA +38 -0
- boulder_opal_scale_up_sdk-1.0.0.dist-info/RECORD +50 -0
- boulder_opal_scale_up_sdk-1.0.0.dist-info/WHEEL +4 -0
- boulderopalscaleupsdk/__init__.py +14 -0
- boulderopalscaleupsdk/agent/__init__.py +29 -0
- boulderopalscaleupsdk/agent/worker.py +244 -0
- boulderopalscaleupsdk/common/__init__.py +12 -0
- boulderopalscaleupsdk/common/dtypes.py +353 -0
- boulderopalscaleupsdk/common/typeclasses.py +85 -0
- boulderopalscaleupsdk/device/__init__.py +16 -0
- boulderopalscaleupsdk/device/common.py +58 -0
- boulderopalscaleupsdk/device/config_loader.py +88 -0
- boulderopalscaleupsdk/device/controller/__init__.py +32 -0
- boulderopalscaleupsdk/device/controller/base.py +18 -0
- boulderopalscaleupsdk/device/controller/qblox.py +664 -0
- boulderopalscaleupsdk/device/controller/quantum_machines.py +139 -0
- boulderopalscaleupsdk/device/device.py +35 -0
- boulderopalscaleupsdk/device/processor/__init__.py +23 -0
- boulderopalscaleupsdk/device/processor/common.py +148 -0
- boulderopalscaleupsdk/device/processor/superconducting_processor.py +291 -0
- boulderopalscaleupsdk/experiments/__init__.py +44 -0
- boulderopalscaleupsdk/experiments/common.py +96 -0
- boulderopalscaleupsdk/experiments/power_rabi.py +60 -0
- boulderopalscaleupsdk/experiments/ramsey.py +55 -0
- boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +64 -0
- boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +76 -0
- boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +64 -0
- boulderopalscaleupsdk/grpc_interceptors/__init__.py +16 -0
- boulderopalscaleupsdk/grpc_interceptors/auth.py +101 -0
- boulderopalscaleupsdk/plotting/__init__.py +24 -0
- boulderopalscaleupsdk/plotting/dtypes.py +221 -0
- boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +48 -0
- boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +53 -0
- boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +138 -0
- boulderopalscaleupsdk/protobuf/v1/device_pb2.py +71 -0
- boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +110 -0
- boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +274 -0
- boulderopalscaleupsdk/protobuf/v1/task_pb2.py +53 -0
- boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +118 -0
- boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +119 -0
- boulderopalscaleupsdk/py.typed +0 -0
- boulderopalscaleupsdk/routines/__init__.py +9 -0
- boulderopalscaleupsdk/routines/common.py +10 -0
- boulderopalscaleupsdk/routines/resonator_mapping.py +13 -0
- boulderopalscaleupsdk/third_party/__init__.py +14 -0
- boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +51 -0
- boulderopalscaleupsdk/third_party/quantum_machines/config.py +597 -0
- boulderopalscaleupsdk/third_party/quantum_machines/constants.py +20 -0
- boulderopalscaleupsdk/utils/__init__.py +12 -0
- boulderopalscaleupsdk/utils/serial_utils.py +62 -0
@@ -0,0 +1,353 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
from __future__ import annotations
|
15
|
+
|
16
|
+
from typing import overload
|
17
|
+
|
18
|
+
__all__ = [
|
19
|
+
"Duration",
|
20
|
+
"Self",
|
21
|
+
"TimeUnit",
|
22
|
+
]
|
23
|
+
|
24
|
+
import sys
|
25
|
+
from dataclasses import dataclass, field
|
26
|
+
from datetime import datetime, timedelta
|
27
|
+
from decimal import Decimal
|
28
|
+
from enum import Enum
|
29
|
+
from typing import Annotated, Any, Literal
|
30
|
+
|
31
|
+
import numpy as np
|
32
|
+
from dateutil.parser import isoparse
|
33
|
+
from pydantic import BeforeValidator, PlainSerializer, TypeAdapter
|
34
|
+
from pydantic.dataclasses import dataclass as pydantic_dataclass
|
35
|
+
|
36
|
+
if sys.version_info >= (3, 11):
|
37
|
+
from typing import Self
|
38
|
+
else:
|
39
|
+
from typing_extensions import Self
|
40
|
+
|
41
|
+
|
42
|
+
class BaseType: ...
|
43
|
+
|
44
|
+
|
45
|
+
class FrequencyUnit(str, Enum):
|
46
|
+
Hz = "Hz"
|
47
|
+
|
48
|
+
|
49
|
+
@pydantic_dataclass
|
50
|
+
class Frequency:
|
51
|
+
value: float
|
52
|
+
unit: FrequencyUnit # No default to guarantee clarity of units
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def from_float_hz(cls, value: float) -> Frequency:
|
56
|
+
return cls(value, FrequencyUnit.Hz)
|
57
|
+
|
58
|
+
def to_int_hz(self) -> int:
|
59
|
+
return int(self.to_float_hz())
|
60
|
+
|
61
|
+
def to_float_hz(self) -> float:
|
62
|
+
match self.unit:
|
63
|
+
case FrequencyUnit.Hz:
|
64
|
+
return self.value
|
65
|
+
|
66
|
+
def __gt__(self, other: Frequency) -> bool:
|
67
|
+
return self.to_float_hz() > other.to_float_hz()
|
68
|
+
|
69
|
+
def __ge__(self, other: Frequency) -> bool:
|
70
|
+
return self.to_float_hz() >= other.to_float_hz()
|
71
|
+
|
72
|
+
def __lt__(self, other: Frequency) -> bool:
|
73
|
+
return self.to_float_hz() < other.to_float_hz()
|
74
|
+
|
75
|
+
def __le__(self, other: Frequency) -> bool:
|
76
|
+
return self.to_float_hz() <= other.to_float_hz()
|
77
|
+
|
78
|
+
def __sub__(self, rhs: Frequency) -> Frequency:
|
79
|
+
if self.unit == rhs.unit:
|
80
|
+
return Frequency(self.value - rhs.value, self.unit)
|
81
|
+
raise NotImplementedError
|
82
|
+
|
83
|
+
def __add__(self, rhs: Frequency) -> Frequency:
|
84
|
+
if self.unit == rhs.unit:
|
85
|
+
return Frequency(self.value + rhs.value, self.unit)
|
86
|
+
raise NotImplementedError
|
87
|
+
|
88
|
+
def __abs__(self) -> Frequency:
|
89
|
+
return Frequency(abs(self.value), self.unit)
|
90
|
+
|
91
|
+
def __str__(self):
|
92
|
+
return f"{self.value} {self.unit.value}"
|
93
|
+
|
94
|
+
@overload # Division by a scalar: e.g. 4.4 Hz // 2.0 = 2.2 Hz
|
95
|
+
def __truediv__(self, rhs: float) -> Frequency: ...
|
96
|
+
|
97
|
+
@overload
|
98
|
+
def __truediv__(self, rhs: Frequency) -> float: ...
|
99
|
+
|
100
|
+
def __truediv__(self, rhs: float | Frequency) -> Frequency | float:
|
101
|
+
if isinstance(rhs, Frequency):
|
102
|
+
return self.to_float_hz() / rhs.to_float_hz()
|
103
|
+
return Frequency(self.value / rhs, self.unit)
|
104
|
+
|
105
|
+
@overload # Floor division by a scalar: e.g. 2.2 Hz // 2.0 = 1 Hz
|
106
|
+
def __floordiv__(self, rhs: float) -> Frequency: ...
|
107
|
+
|
108
|
+
@overload
|
109
|
+
def __floordiv__(self, rhs: Frequency) -> float: ...
|
110
|
+
|
111
|
+
def __floordiv__(self, rhs: float | Frequency) -> Frequency | float:
|
112
|
+
if isinstance(rhs, Frequency):
|
113
|
+
return self.to_float_hz() // rhs.to_float_hz()
|
114
|
+
return Frequency(self.value // rhs, self.unit)
|
115
|
+
|
116
|
+
def __mul__(self, rhs: float) -> Frequency:
|
117
|
+
return Frequency(self.value * rhs, self.unit)
|
118
|
+
|
119
|
+
def __rmul__(self, lhs: float) -> Frequency:
|
120
|
+
return self.__mul__(lhs)
|
121
|
+
|
122
|
+
|
123
|
+
class TimeUnit(str, Enum):
|
124
|
+
S = "s"
|
125
|
+
MS = "ms"
|
126
|
+
US = "us"
|
127
|
+
NS = "ns"
|
128
|
+
DT = "dt"
|
129
|
+
|
130
|
+
|
131
|
+
_SI_TIME = Literal[TimeUnit.S, TimeUnit.MS, TimeUnit.US, TimeUnit.NS]
|
132
|
+
|
133
|
+
|
134
|
+
@pydantic_dataclass(order=True)
|
135
|
+
class Duration(BaseType):
|
136
|
+
"""
|
137
|
+
A wrapper of _SiDuration and _DtDuration to manage the conversion.
|
138
|
+
"""
|
139
|
+
|
140
|
+
value: int = field(compare=False)
|
141
|
+
unit: TimeUnit = field(compare=False)
|
142
|
+
dtype: Literal["duration"] = "duration"
|
143
|
+
_value: _SiDuration | _DtDuration = field(init=False, repr=False)
|
144
|
+
|
145
|
+
def __post_init__(self):
|
146
|
+
self._value = (
|
147
|
+
_DtDuration(self.value)
|
148
|
+
if self.unit == TimeUnit.DT
|
149
|
+
else _SiDuration(self.value, self.unit)
|
150
|
+
)
|
151
|
+
|
152
|
+
def is_si(self) -> bool:
|
153
|
+
return self.unit != TimeUnit.DT
|
154
|
+
|
155
|
+
@staticmethod
|
156
|
+
def from_si(d: Duration, name: str) -> Duration:
|
157
|
+
if d.unit == TimeUnit.DT:
|
158
|
+
raise TypeError(f"{name} must use SI time unit.")
|
159
|
+
return d
|
160
|
+
|
161
|
+
@staticmethod
|
162
|
+
def from_intlike(val: float, unit: TimeUnit) -> Duration:
|
163
|
+
if not np.double(val).is_integer():
|
164
|
+
raise ValueError("fail to create a Duration object. value must be an integer.")
|
165
|
+
return Duration(int(val), unit)
|
166
|
+
|
167
|
+
def convert(self, target: Duration | _SI_TIME) -> Duration:
|
168
|
+
"""
|
169
|
+
In particular, we only allow the following conversions:
|
170
|
+
|
171
|
+
# ((1000, "ms"), "s") -> (1, "s")
|
172
|
+
(_SiDuration, _SI_TIME) -> _SiDuration
|
173
|
+
|
174
|
+
# ((4, "ns"), (2, "ns")) -> (2, "dt")
|
175
|
+
(_SiDuration, _SiDuration) -> _DtDuration
|
176
|
+
|
177
|
+
# ((2, "dt"), (2, "ns")) -> (4, "ns")
|
178
|
+
(_DtDuration, _SiDuration) _> _SiDuration
|
179
|
+
"""
|
180
|
+
match self._value, getattr(target, "_value", target):
|
181
|
+
case _SiDuration(
|
182
|
+
_,
|
183
|
+
_,
|
184
|
+
), TimeUnit.S | TimeUnit.MS | TimeUnit.US | TimeUnit.NS:
|
185
|
+
converted = self._value.convert_to_si(target) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
|
186
|
+
case _SiDuration(_, _), _SiDuration(_, _):
|
187
|
+
converted = self._value.convert_to_dt(target) # type: ignore[arg-type, assignment] # pyright: ignore[reportArgumentType]
|
188
|
+
case _DtDuration(_, _), _SiDuration(_, _):
|
189
|
+
converted = self._value.convert(target) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
|
190
|
+
case _:
|
191
|
+
raise TypeError(f"cant't convert type {self.unit} to {target}")
|
192
|
+
return Duration(converted.value, converted.unit)
|
193
|
+
|
194
|
+
|
195
|
+
@dataclass(order=True)
|
196
|
+
class _DtDuration(BaseType):
|
197
|
+
value: int
|
198
|
+
unit: Literal[TimeUnit.DT] = TimeUnit.DT
|
199
|
+
|
200
|
+
def convert(self, target: _SiDuration) -> _SiDuration:
|
201
|
+
return _SiDuration(self.value * target.value, target.unit)
|
202
|
+
|
203
|
+
|
204
|
+
@dataclass(order=True)
|
205
|
+
class _SiDuration(BaseType):
|
206
|
+
value: int = field(compare=False)
|
207
|
+
unit: _SI_TIME = field(compare=False)
|
208
|
+
_np_rep: np.timedelta64 = field(init=False, repr=False)
|
209
|
+
|
210
|
+
def __post_init__(self):
|
211
|
+
err = TypeError(
|
212
|
+
f"value must be an integer, got {self.value}{self.unit}. Choose a different unit to "
|
213
|
+
"scale it.",
|
214
|
+
)
|
215
|
+
dec = Decimal(self.value)
|
216
|
+
exponent = dec.as_tuple().exponent
|
217
|
+
if not isinstance(exponent, int) or exponent < 0: # pragma: no cover
|
218
|
+
raise err
|
219
|
+
|
220
|
+
self.value = int(self.value)
|
221
|
+
try:
|
222
|
+
self._np_rep = np.timedelta64(self.value, self.unit)
|
223
|
+
except ValueError as e:
|
224
|
+
raise err from e
|
225
|
+
|
226
|
+
def convert_to_dt(self, clock: _SiDuration) -> _DtDuration:
|
227
|
+
try:
|
228
|
+
converted_si = self.convert_to_si(clock.unit)
|
229
|
+
except TypeError as e:
|
230
|
+
raise TypeError(
|
231
|
+
"fail to convert to dt type. Consider rescaling the clock time.",
|
232
|
+
) from e
|
233
|
+
val = np.double(converted_si.value / clock.value)
|
234
|
+
# N.B, this might be too strict. Some rounding might be necessary.
|
235
|
+
if not val.is_integer():
|
236
|
+
raise TypeError(
|
237
|
+
"fail to convert to dt type. Consider rescaling the clock time.",
|
238
|
+
)
|
239
|
+
return _DtDuration(int(val))
|
240
|
+
|
241
|
+
def convert_to_si(self, unit: _SI_TIME) -> _SiDuration:
|
242
|
+
if self._np_rep is None:
|
243
|
+
raise TypeError("`convert` only support SI time unit.")
|
244
|
+
val: np.float64 = self._np_rep / np.timedelta64(1, unit)
|
245
|
+
if not val.is_integer():
|
246
|
+
raise TypeError(
|
247
|
+
f"fail to convert to {unit} with {self.value}{self.unit}.",
|
248
|
+
)
|
249
|
+
return _SiDuration(int(val), unit)
|
250
|
+
|
251
|
+
def to_seconds(self) -> float:
|
252
|
+
return float(self._np_rep / np.timedelta64(1, "s"))
|
253
|
+
|
254
|
+
|
255
|
+
def ensure_frequency_hz(value: Any) -> Any:
|
256
|
+
match value:
|
257
|
+
case Frequency():
|
258
|
+
return value
|
259
|
+
case float() | int():
|
260
|
+
return Frequency(value, FrequencyUnit.Hz)
|
261
|
+
case dict():
|
262
|
+
return TypeAdapter(Frequency).validate_python(value)
|
263
|
+
case _:
|
264
|
+
raise ValueError("Frequency needs to be numeric.")
|
265
|
+
|
266
|
+
|
267
|
+
FrequencyHzLike = Annotated[
|
268
|
+
Frequency,
|
269
|
+
BeforeValidator(ensure_frequency_hz),
|
270
|
+
PlainSerializer(lambda x: x.to_float_hz(), return_type=float),
|
271
|
+
]
|
272
|
+
|
273
|
+
|
274
|
+
def ensure_duration_ns(value: Any) -> Any:
|
275
|
+
match value:
|
276
|
+
case Duration():
|
277
|
+
return value.convert(TimeUnit.NS)
|
278
|
+
case float() | int():
|
279
|
+
return Duration.from_intlike(value, TimeUnit.NS)
|
280
|
+
case dict():
|
281
|
+
return TypeAdapter(Duration).validate_python(value)
|
282
|
+
case _:
|
283
|
+
raise ValueError("Duration needs to be numeric")
|
284
|
+
|
285
|
+
|
286
|
+
DurationNsLike = Annotated[Duration, BeforeValidator(ensure_duration_ns)]
|
287
|
+
|
288
|
+
|
289
|
+
@pydantic_dataclass
|
290
|
+
class ISO8601Datetime:
|
291
|
+
value: datetime
|
292
|
+
|
293
|
+
def __post_init__(self):
|
294
|
+
self.value = _validate_iso_datetime(self.value)
|
295
|
+
|
296
|
+
def __str__(self):
|
297
|
+
return _serialize_datetime(self.value)
|
298
|
+
|
299
|
+
def strftime(self, fmt: str) -> str:
|
300
|
+
"""
|
301
|
+
Format the datetime value using the given format string.
|
302
|
+
|
303
|
+
Parameters
|
304
|
+
----------
|
305
|
+
fmt : str
|
306
|
+
The format string to use for formatting.
|
307
|
+
|
308
|
+
Returns
|
309
|
+
-------
|
310
|
+
str
|
311
|
+
The formatted datetime string.
|
312
|
+
"""
|
313
|
+
return self.value.strftime(fmt)
|
314
|
+
|
315
|
+
|
316
|
+
def _validate_iso_datetime(value: Any) -> datetime:
|
317
|
+
def _raise_invalid_timezone_error():
|
318
|
+
raise ValueError("Datetime must be in UTC timezone.")
|
319
|
+
|
320
|
+
if isinstance(value, ISO8601Datetime):
|
321
|
+
return value.value
|
322
|
+
if isinstance(value, datetime):
|
323
|
+
if value.tzinfo is None or value.tzinfo.utcoffset(value) != timedelta(0):
|
324
|
+
_raise_invalid_timezone_error()
|
325
|
+
else:
|
326
|
+
return value
|
327
|
+
if isinstance(value, str):
|
328
|
+
try:
|
329
|
+
parsed_datetime = isoparse(value)
|
330
|
+
if parsed_datetime.tzinfo is None or parsed_datetime.tzinfo.utcoffset(
|
331
|
+
parsed_datetime,
|
332
|
+
) != timedelta(0):
|
333
|
+
_raise_invalid_timezone_error()
|
334
|
+
else:
|
335
|
+
return parsed_datetime
|
336
|
+
except Exception as e:
|
337
|
+
raise ValueError("Invalid ISO8601 datetime string.") from e
|
338
|
+
raise ValueError(
|
339
|
+
"Value must be a datetime object, an ISO8601Datetime instance, or a valid ISO8601 string.",
|
340
|
+
)
|
341
|
+
|
342
|
+
|
343
|
+
def _serialize_datetime(value: datetime) -> str:
|
344
|
+
if value.tzinfo is None or value.tzinfo.utcoffset(value) != timedelta(0):
|
345
|
+
raise ValueError("Datetime must be in UTC timezone.")
|
346
|
+
return value.isoformat()
|
347
|
+
|
348
|
+
|
349
|
+
ISO8601DatetimeUTCLike = Annotated[
|
350
|
+
datetime,
|
351
|
+
BeforeValidator(_validate_iso_datetime),
|
352
|
+
PlainSerializer(_serialize_datetime),
|
353
|
+
]
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
from abc import abstractmethod
|
15
|
+
from collections.abc import Callable
|
16
|
+
from copy import deepcopy
|
17
|
+
from typing import Any, Protocol, TypeVar, overload
|
18
|
+
|
19
|
+
T = TypeVar("T")
|
20
|
+
|
21
|
+
|
22
|
+
class Combine(Protocol[T]):
|
23
|
+
"""The combine typeclass defines the behaviour when types are combined."""
|
24
|
+
|
25
|
+
@abstractmethod
|
26
|
+
def combine(self, first: T, second: T) -> T: ...
|
27
|
+
|
28
|
+
@overload
|
29
|
+
def combine_option(self, first: T, second: T | None) -> T: ...
|
30
|
+
|
31
|
+
@overload
|
32
|
+
def combine_option(self, first: T | None, second: T) -> T: ...
|
33
|
+
|
34
|
+
@overload
|
35
|
+
def combine_option(self, first: None, second: None) -> None: ...
|
36
|
+
|
37
|
+
def combine_option(self, first: T | None, second: T | None) -> T | None:
|
38
|
+
return Combine[T].option(self).combine(first, second)
|
39
|
+
|
40
|
+
@classmethod
|
41
|
+
def create(cls, combine_fn: Callable[[T, T], T]) -> "Combine[T]":
|
42
|
+
class _Combine(Combine):
|
43
|
+
def combine(self, first: T, second: T) -> T:
|
44
|
+
return combine_fn(first, second)
|
45
|
+
|
46
|
+
return _Combine()
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def replace(cls) -> "Combine[T]":
|
50
|
+
def _combine(_first: T, second: T) -> T:
|
51
|
+
return second
|
52
|
+
|
53
|
+
return Combine[T].create(_combine)
|
54
|
+
|
55
|
+
@classmethod
|
56
|
+
def option(cls, combine: "Combine[T]") -> "Combine[T | None]":
|
57
|
+
def _combine(first: T | None, second: T | None) -> T | None:
|
58
|
+
match first, second:
|
59
|
+
case None, None:
|
60
|
+
return None
|
61
|
+
case first, None:
|
62
|
+
return first
|
63
|
+
case None, second:
|
64
|
+
return second
|
65
|
+
case _:
|
66
|
+
return combine.combine(first, second) # type: ignore[arg-type]
|
67
|
+
|
68
|
+
return Combine.create(_combine)
|
69
|
+
|
70
|
+
@staticmethod
|
71
|
+
def deep_merge(combine_value: "Combine[Any]") -> "Combine[dict[Any, Any]]":
|
72
|
+
def _combine_inplace_recursively(source: dict[Any, Any], dest: dict[Any, Any]):
|
73
|
+
for key, value in source.items():
|
74
|
+
if isinstance(value, dict):
|
75
|
+
node = dest.setdefault(key, {})
|
76
|
+
_combine_inplace_recursively(value, node)
|
77
|
+
else:
|
78
|
+
dest[key] = Combine.option(combine_value).combine(dest.get(key), value)
|
79
|
+
|
80
|
+
def _combine(first: dict[Any, Any], second: dict[Any, Any]) -> dict[Any, Any]:
|
81
|
+
destination = deepcopy(first)
|
82
|
+
_combine_inplace_recursively(source=second, dest=destination)
|
83
|
+
return destination
|
84
|
+
|
85
|
+
return Combine.create(_combine)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
__all__ = ["Device", "InvalidDevice", "InvalidDeviceComponent"]
|
15
|
+
|
16
|
+
from boulderopalscaleupsdk.device.device import Device, InvalidDevice, InvalidDeviceComponent
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
from collections.abc import Callable
|
15
|
+
from typing import Any, Generic, TypeVar
|
16
|
+
|
17
|
+
from pydantic import BaseModel
|
18
|
+
|
19
|
+
TraitT = TypeVar("TraitT", bound=str)
|
20
|
+
JsonDict = dict[str, Any]
|
21
|
+
ExtraType = JsonDict | Callable[[JsonDict], None] | None
|
22
|
+
|
23
|
+
ComponentRef = str
|
24
|
+
|
25
|
+
|
26
|
+
class Processor(BaseModel): ...
|
27
|
+
|
28
|
+
|
29
|
+
class Component(BaseModel, Generic[TraitT]):
|
30
|
+
traits: list[TraitT]
|
31
|
+
|
32
|
+
def get_summary_dict(self) -> dict[str, dict[str, Any]]:
|
33
|
+
summary_dict = {} # {field: value}
|
34
|
+
|
35
|
+
for field, value in self.model_dump().items():
|
36
|
+
if field not in ["dtype", "traits"] and value:
|
37
|
+
tmp = {}
|
38
|
+
v = value.get("value", None)
|
39
|
+
|
40
|
+
tmp["value"] = v["value"] if isinstance(v, dict) else v
|
41
|
+
tmp["err_minus"] = value.get("err_minus", None)
|
42
|
+
tmp["err_plus"] = value.get("err_plus", None)
|
43
|
+
tmp["calibration_status"] = value.get("calibration_status", None)
|
44
|
+
|
45
|
+
extra = self.model_fields[field].json_schema_extra
|
46
|
+
if isinstance(extra, dict):
|
47
|
+
tmp["display"] = extra.get("display", {})
|
48
|
+
elif callable(extra):
|
49
|
+
tmp["display"] = extra({"display": "value"})
|
50
|
+
summary_dict[field] = tmp
|
51
|
+
|
52
|
+
return summary_dict
|
53
|
+
|
54
|
+
|
55
|
+
class ComponentUpdate(BaseModel): ...
|
56
|
+
|
57
|
+
|
58
|
+
class ProcessorUpdate(BaseModel): ...
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
from enum import Enum
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Any
|
17
|
+
|
18
|
+
import yaml
|
19
|
+
from pydantic import BaseModel, ConfigDict
|
20
|
+
|
21
|
+
from boulderopalscaleupsdk.device.controller.quantum_machines import QuantumMachinesControllerInfo
|
22
|
+
from boulderopalscaleupsdk.device.processor import (
|
23
|
+
SuperconductingProcessor,
|
24
|
+
SuperconductingProcessorTemplate,
|
25
|
+
)
|
26
|
+
from boulderopalscaleupsdk.utils.serial_utils import sanitize_keys
|
27
|
+
|
28
|
+
|
29
|
+
class ProcessorArchitecture(str, Enum):
|
30
|
+
Superconducting = "superconducting"
|
31
|
+
|
32
|
+
|
33
|
+
class DeviceInfo(BaseModel):
|
34
|
+
controller_info: QuantumMachinesControllerInfo # | OtherControllerInfoTypes
|
35
|
+
processor: SuperconductingProcessor # | OtherSDKProcessorType
|
36
|
+
|
37
|
+
model_config = ConfigDict(use_enum_values=True)
|
38
|
+
|
39
|
+
def to_dict(self) -> dict[str, Any]:
|
40
|
+
return sanitize_keys(self.model_dump(by_alias=True, mode="json"))
|
41
|
+
|
42
|
+
|
43
|
+
class DeviceConfigLoader:
|
44
|
+
def __init__(self, config_path: Path):
|
45
|
+
self.config_path = config_path
|
46
|
+
|
47
|
+
def load(self) -> dict[str, Any]:
|
48
|
+
device_config_data = self._load_yaml_file(self.config_path)
|
49
|
+
|
50
|
+
layout_file = device_config_data.pop("layout_file", None)
|
51
|
+
if layout_file is None:
|
52
|
+
raise ValueError("layout file is missing in the device configuration data.")
|
53
|
+
self._validate_file_is_filename(layout_file)
|
54
|
+
|
55
|
+
layout_path = self.config_path.parent / layout_file
|
56
|
+
device_layout_data = self._load_yaml_file(layout_path)
|
57
|
+
|
58
|
+
processed_device_config = {**device_config_data, **device_layout_data}
|
59
|
+
return sanitize_keys(processed_device_config)
|
60
|
+
|
61
|
+
def load_device_info(self) -> DeviceInfo:
|
62
|
+
device_config_dict = self.load()
|
63
|
+
match device_config_dict["device_arch"]:
|
64
|
+
case "superconducting":
|
65
|
+
superconducting_template = SuperconductingProcessorTemplate.model_validate(
|
66
|
+
device_config_dict,
|
67
|
+
)
|
68
|
+
device_info = DeviceInfo(
|
69
|
+
controller_info=QuantumMachinesControllerInfo.model_validate(
|
70
|
+
device_config_dict["controller_info"],
|
71
|
+
),
|
72
|
+
processor=SuperconductingProcessor.from_template(superconducting_template),
|
73
|
+
)
|
74
|
+
case other:
|
75
|
+
raise ValueError(f"Invalid or unsupported architecture {other}")
|
76
|
+
return device_info
|
77
|
+
|
78
|
+
@staticmethod
|
79
|
+
def _load_yaml_file(yaml_file_path: Path) -> dict[str, Any]:
|
80
|
+
with yaml_file_path.open("rb") as fd:
|
81
|
+
return yaml.safe_load(fd)
|
82
|
+
|
83
|
+
@staticmethod
|
84
|
+
def _validate_file_is_filename(file_name: str) -> None:
|
85
|
+
if "/" in file_name or "\\" in file_name:
|
86
|
+
raise ValueError(
|
87
|
+
f"'{file_name}' must be a file name, not a path.",
|
88
|
+
)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"BaseControllerInfo",
|
16
|
+
"DrivePortConfig",
|
17
|
+
"FluxPortConfig",
|
18
|
+
"OctaveConfig",
|
19
|
+
"PortRef",
|
20
|
+
"QuantumMachinesControllerInfo",
|
21
|
+
"ReadoutPortConfig",
|
22
|
+
]
|
23
|
+
|
24
|
+
from .base import BaseControllerInfo
|
25
|
+
from .quantum_machines import (
|
26
|
+
DrivePortConfig,
|
27
|
+
FluxPortConfig,
|
28
|
+
OctaveConfig,
|
29
|
+
PortRef,
|
30
|
+
QuantumMachinesControllerInfo,
|
31
|
+
ReadoutPortConfig,
|
32
|
+
)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
from pydantic import BaseModel
|
15
|
+
|
16
|
+
|
17
|
+
class BaseControllerInfo(BaseModel):
|
18
|
+
"""Base controller info."""
|