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.
Files changed (50) hide show
  1. boulder_opal_scale_up_sdk-1.0.0.dist-info/METADATA +38 -0
  2. boulder_opal_scale_up_sdk-1.0.0.dist-info/RECORD +50 -0
  3. boulder_opal_scale_up_sdk-1.0.0.dist-info/WHEEL +4 -0
  4. boulderopalscaleupsdk/__init__.py +14 -0
  5. boulderopalscaleupsdk/agent/__init__.py +29 -0
  6. boulderopalscaleupsdk/agent/worker.py +244 -0
  7. boulderopalscaleupsdk/common/__init__.py +12 -0
  8. boulderopalscaleupsdk/common/dtypes.py +353 -0
  9. boulderopalscaleupsdk/common/typeclasses.py +85 -0
  10. boulderopalscaleupsdk/device/__init__.py +16 -0
  11. boulderopalscaleupsdk/device/common.py +58 -0
  12. boulderopalscaleupsdk/device/config_loader.py +88 -0
  13. boulderopalscaleupsdk/device/controller/__init__.py +32 -0
  14. boulderopalscaleupsdk/device/controller/base.py +18 -0
  15. boulderopalscaleupsdk/device/controller/qblox.py +664 -0
  16. boulderopalscaleupsdk/device/controller/quantum_machines.py +139 -0
  17. boulderopalscaleupsdk/device/device.py +35 -0
  18. boulderopalscaleupsdk/device/processor/__init__.py +23 -0
  19. boulderopalscaleupsdk/device/processor/common.py +148 -0
  20. boulderopalscaleupsdk/device/processor/superconducting_processor.py +291 -0
  21. boulderopalscaleupsdk/experiments/__init__.py +44 -0
  22. boulderopalscaleupsdk/experiments/common.py +96 -0
  23. boulderopalscaleupsdk/experiments/power_rabi.py +60 -0
  24. boulderopalscaleupsdk/experiments/ramsey.py +55 -0
  25. boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +64 -0
  26. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +76 -0
  27. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +64 -0
  28. boulderopalscaleupsdk/grpc_interceptors/__init__.py +16 -0
  29. boulderopalscaleupsdk/grpc_interceptors/auth.py +101 -0
  30. boulderopalscaleupsdk/plotting/__init__.py +24 -0
  31. boulderopalscaleupsdk/plotting/dtypes.py +221 -0
  32. boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +48 -0
  33. boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +53 -0
  34. boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +138 -0
  35. boulderopalscaleupsdk/protobuf/v1/device_pb2.py +71 -0
  36. boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +110 -0
  37. boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +274 -0
  38. boulderopalscaleupsdk/protobuf/v1/task_pb2.py +53 -0
  39. boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +118 -0
  40. boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +119 -0
  41. boulderopalscaleupsdk/py.typed +0 -0
  42. boulderopalscaleupsdk/routines/__init__.py +9 -0
  43. boulderopalscaleupsdk/routines/common.py +10 -0
  44. boulderopalscaleupsdk/routines/resonator_mapping.py +13 -0
  45. boulderopalscaleupsdk/third_party/__init__.py +14 -0
  46. boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +51 -0
  47. boulderopalscaleupsdk/third_party/quantum_machines/config.py +597 -0
  48. boulderopalscaleupsdk/third_party/quantum_machines/constants.py +20 -0
  49. boulderopalscaleupsdk/utils/__init__.py +12 -0
  50. 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."""