boulder-opal-scale-up-sdk 1.0.0__py3-none-any.whl → 1.0.1__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.1.dist-info/LICENSE +805 -0
- {boulder_opal_scale_up_sdk-1.0.0.dist-info → boulder_opal_scale_up_sdk-1.0.1.dist-info}/METADATA +17 -7
- boulder_opal_scale_up_sdk-1.0.1.dist-info/RECORD +60 -0
- boulderopalscaleupsdk/agent/worker.py +22 -11
- boulderopalscaleupsdk/common/dtypes.py +68 -111
- boulderopalscaleupsdk/device/__init__.py +5 -1
- boulderopalscaleupsdk/device/config_loader.py +10 -8
- boulderopalscaleupsdk/device/controller/__init__.py +17 -14
- boulderopalscaleupsdk/device/controller/base.py +12 -3
- boulderopalscaleupsdk/device/controller/qblox.py +43 -17
- boulderopalscaleupsdk/device/controller/quantum_machines.py +60 -59
- boulderopalscaleupsdk/device/controller/resolver.py +117 -0
- boulderopalscaleupsdk/device/defcal.py +61 -0
- boulderopalscaleupsdk/device/device.py +8 -2
- boulderopalscaleupsdk/device/processor/__init__.py +9 -1
- boulderopalscaleupsdk/device/processor/common.py +129 -20
- boulderopalscaleupsdk/device/processor/superconducting_processor.py +59 -13
- boulderopalscaleupsdk/experiments/__init__.py +8 -0
- boulderopalscaleupsdk/experiments/common.py +8 -4
- boulderopalscaleupsdk/experiments/power_rabi.py +3 -3
- boulderopalscaleupsdk/experiments/ramsey.py +13 -6
- boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +24 -0
- boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +3 -3
- boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +10 -10
- boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +3 -3
- boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +68 -0
- boulderopalscaleupsdk/experiments/transmon_resonator_chi_scan.py +56 -0
- boulderopalscaleupsdk/experiments/transmon_spectroscopy.py +69 -0
- boulderopalscaleupsdk/grpc_interceptors/auth.py +5 -2
- boulderopalscaleupsdk/plotting/__init__.py +20 -2
- boulderopalscaleupsdk/plotting/dtypes.py +81 -117
- boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +17 -17
- boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +8 -6
- boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +13 -13
- boulderopalscaleupsdk/protobuf/v1/device_pb2.py +35 -17
- boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +40 -6
- boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +116 -14
- boulderopalscaleupsdk/routines/__init__.py +1 -4
- boulderopalscaleupsdk/routines/resonator_mapping.py +8 -2
- boulderopalscaleupsdk/stubs/__init__.py +12 -0
- boulderopalscaleupsdk/stubs/dtypes.py +47 -0
- boulderopalscaleupsdk/stubs/maps.py +9 -0
- boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +32 -0
- boulderopalscaleupsdk/third_party/quantum_machines/config.py +30 -9
- boulder_opal_scale_up_sdk-1.0.0.dist-info/RECORD +0 -50
- {boulder_opal_scale_up_sdk-1.0.0.dist-info → boulder_opal_scale_up_sdk-1.0.1.dist-info}/WHEEL +0 -0
@@ -47,11 +47,11 @@ import dataclasses
|
|
47
47
|
import enum
|
48
48
|
import re
|
49
49
|
from dataclasses import dataclass
|
50
|
-
from typing import Annotated, Any, Literal, TypeVar
|
50
|
+
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar
|
51
51
|
|
52
52
|
from pydantic import BaseModel, BeforeValidator, Field, PlainSerializer, model_validator
|
53
53
|
|
54
|
-
from boulderopalscaleupsdk.
|
54
|
+
from boulderopalscaleupsdk.device.controller.base import Backend, ControllerType
|
55
55
|
|
56
56
|
# ==================================================================================================
|
57
57
|
# Addressing
|
@@ -96,11 +96,11 @@ class ModuleAddr:
|
|
96
96
|
def parse(cls, data: str) -> Self:
|
97
97
|
mch = _RE_MODULE_ADDR.match(data)
|
98
98
|
if mch is None:
|
99
|
-
raise ValueError("
|
99
|
+
raise ValueError("Could not parse module address.")
|
100
100
|
return cls(cluster=mch.group("cluster"), slot=int(mch.group("slot")))
|
101
101
|
|
102
102
|
|
103
|
-
@dataclass(frozen=True
|
103
|
+
@dataclass(frozen=True)
|
104
104
|
class SequencerAddr:
|
105
105
|
"""Address to a sequencer (located on a specific module) in a QBLOX control stack."""
|
106
106
|
|
@@ -112,6 +112,12 @@ class SequencerAddr:
|
|
112
112
|
def module(self) -> ModuleAddr:
|
113
113
|
return ModuleAddr(self.cluster, self.slot)
|
114
114
|
|
115
|
+
def __hash__(self) -> int:
|
116
|
+
return hash(str(self))
|
117
|
+
|
118
|
+
def __eq__(self, other) -> bool:
|
119
|
+
return isinstance(other, SequencerAddr) and str(other) == str(self)
|
120
|
+
|
115
121
|
def __str__(self) -> str:
|
116
122
|
"""Address as a string.
|
117
123
|
|
@@ -124,7 +130,7 @@ class SequencerAddr:
|
|
124
130
|
def parse(cls, data: str) -> Self:
|
125
131
|
mch = _RE_SEQUENCER_ADDR.match(data)
|
126
132
|
if mch is None:
|
127
|
-
raise ValueError("
|
133
|
+
raise ValueError("Could not parse sequencer address.")
|
128
134
|
return cls(
|
129
135
|
cluster=mch.group("cluster"),
|
130
136
|
slot=int(mch.group("slot")),
|
@@ -158,7 +164,7 @@ class PortAddr:
|
|
158
164
|
def parse(cls, data: str) -> Self:
|
159
165
|
mch = _RE_PORT_ADDR.match(data)
|
160
166
|
if mch is None:
|
161
|
-
raise ValueError("
|
167
|
+
raise ValueError("Could not parse port address.")
|
162
168
|
direction: Literal["out", "in"] = "out" if mch.group("dir") == "O" else "in"
|
163
169
|
return cls(
|
164
170
|
cluster=mch.group("cluster"),
|
@@ -179,7 +185,7 @@ def _addr_validator(dtype: type[T]) -> BeforeValidator:
|
|
179
185
|
return obj
|
180
186
|
if isinstance(obj, str): # Parse from JSON
|
181
187
|
return dtype.parse(obj)
|
182
|
-
raise TypeError(f"Invalid type {type(obj).__name__} for {type(dtype).__name__}")
|
188
|
+
raise TypeError(f"Invalid type {type(obj).__name__} for {type(dtype).__name__}.")
|
183
189
|
|
184
190
|
return BeforeValidator(_validator)
|
185
191
|
|
@@ -234,9 +240,9 @@ class ComplexChannel(BaseModel):
|
|
234
240
|
ii = self.i_port
|
235
241
|
qq = self.q_port
|
236
242
|
if ii.cluster != qq.cluster or ii.slot != qq.slot:
|
237
|
-
raise ValueError("I and Q ports must be on the same cluster+module")
|
243
|
+
raise ValueError("I and Q ports must be on the same cluster+module.")
|
238
244
|
if ii.direction != qq.direction:
|
239
|
-
raise ValueError("I and Q ports must be the same direction")
|
245
|
+
raise ValueError("I and Q ports must be in the same direction.")
|
240
246
|
return self
|
241
247
|
|
242
248
|
|
@@ -283,19 +289,26 @@ class ElementConnection(BaseModel): # pragma: no cover
|
|
283
289
|
raise ValueError("I/O channels for an element must be on the same module.")
|
284
290
|
return self
|
285
291
|
|
292
|
+
@property
|
293
|
+
def module(self) -> ModuleAddr:
|
294
|
+
return self.ch_out.module
|
295
|
+
|
286
296
|
|
287
|
-
class
|
297
|
+
class QBLOXControllerInfo(BaseModel): # pragma: no cover
|
288
298
|
"""
|
289
299
|
Controller information needed for program compilation and control.
|
290
300
|
|
291
301
|
Attributes
|
292
302
|
----------
|
303
|
+
controller_type: Literal[ControllerType.QBLOX]
|
304
|
+
The type of controller, which is always `ControllerType.QBLOX` for this class.
|
293
305
|
modules: dict[ModuleAddrType, ModuleType]
|
294
306
|
The modules connected to the QBLOX stack.
|
295
307
|
elements: dict[str, ElementConnection]
|
296
308
|
The addressable control elements for the stack.
|
297
309
|
"""
|
298
310
|
|
311
|
+
controller_type: Literal[ControllerType.QBLOX] = ControllerType.QBLOX
|
299
312
|
modules: dict[ModuleAddrType, ModuleType]
|
300
313
|
elements: dict[str, ElementConnection]
|
301
314
|
|
@@ -313,7 +326,7 @@ class SequencerParams(BaseModel):
|
|
313
326
|
marker_ovr_value: int | None = Field(default=None)
|
314
327
|
mod_en_awg: bool | None = Field(default=None)
|
315
328
|
demod_en_acq: bool | None = Field(default=None)
|
316
|
-
sync_en: bool | None = Field(default=
|
329
|
+
sync_en: bool | None = Field(default=None)
|
317
330
|
nco_prop_delay_comp_en: bool | None = Field(default=True)
|
318
331
|
integration_length_acq: int | None = Field(default=None)
|
319
332
|
|
@@ -353,17 +366,27 @@ class AcquisitionConfig(BaseModel):
|
|
353
366
|
class SequenceProgram(BaseModel):
|
354
367
|
"""A Q1 Sequence Program."""
|
355
368
|
|
369
|
+
backend: ClassVar = Backend.QBLOX_Q1ASM
|
370
|
+
|
356
371
|
program: str
|
357
372
|
waveforms: dict[str, IndexedData] = {}
|
358
373
|
weights: dict[str, IndexedData] = {}
|
359
374
|
acquisitions: dict[str, AcquisitionConfig] = {}
|
360
375
|
acquisition_scopes: list[str] = []
|
376
|
+
params: SequencerParams = SequencerParams()
|
377
|
+
params_only: bool = False
|
361
378
|
|
362
|
-
def sequence_data(self) -> dict[str, Any]:
|
379
|
+
def sequence_data(self) -> dict[str, Any] | None:
|
380
|
+
if self.params_only:
|
381
|
+
return None
|
363
382
|
return self.model_dump(include={"program", "waveforms", "weights", "acquisitions"})
|
364
383
|
|
365
|
-
def
|
366
|
-
return self.model_dump_json()
|
384
|
+
def dumps(self) -> str:
|
385
|
+
return self.model_dump_json()
|
386
|
+
|
387
|
+
@classmethod
|
388
|
+
def loads(cls, data: str) -> Self:
|
389
|
+
return cls.model_validate_json(data)
|
367
390
|
|
368
391
|
|
369
392
|
class PreparedSequenceProgram(BaseModel): # pragma: no cover
|
@@ -373,7 +396,6 @@ class PreparedSequenceProgram(BaseModel): # pragma: no cover
|
|
373
396
|
sequencer_number: int
|
374
397
|
ch_out: ChannelType
|
375
398
|
ch_in: ChannelType | None = None
|
376
|
-
sequencer_params: SequencerParams = SequencerParams()
|
377
399
|
|
378
400
|
@property
|
379
401
|
def sequencer_addr(self) -> SequencerAddr:
|
@@ -395,8 +417,12 @@ class PreparedProgram(BaseModel):
|
|
395
417
|
modules: dict[ModuleAddrType, PreparedModule] # The set of modules this program will target.
|
396
418
|
sequence_programs: dict[str, PreparedSequenceProgram] # The individual element programs.
|
397
419
|
|
398
|
-
def
|
399
|
-
return self.model_dump_json()
|
420
|
+
def dumps(self) -> str:
|
421
|
+
return self.model_dump_json()
|
422
|
+
|
423
|
+
@classmethod
|
424
|
+
def loads(cls, data: str) -> Self:
|
425
|
+
return cls.model_validate_json(data)
|
400
426
|
|
401
427
|
|
402
428
|
# ==================================================================================================
|
@@ -11,105 +11,103 @@
|
|
11
11
|
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
12
|
# License for the specific language.
|
13
13
|
|
14
|
-
import
|
15
|
-
|
16
|
-
from
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
ControllerConfigType,
|
24
|
-
OctaveConfig121,
|
25
|
-
OPX1000ControllerConfigType,
|
14
|
+
from typing import Literal, Self
|
15
|
+
|
16
|
+
from pydantic import (
|
17
|
+
BaseModel,
|
18
|
+
ConfigDict,
|
19
|
+
Field,
|
20
|
+
field_serializer,
|
21
|
+
field_validator,
|
22
|
+
model_validator,
|
26
23
|
)
|
24
|
+
|
25
|
+
from boulderopalscaleupsdk.common.dtypes import Duration, DurationNsLike, TimeUnit
|
26
|
+
from boulderopalscaleupsdk.device.controller.base import ControllerType
|
27
|
+
from boulderopalscaleupsdk.third_party import quantum_machines as qm
|
28
|
+
from boulderopalscaleupsdk.third_party.quantum_machines import config as qm_config
|
27
29
|
from boulderopalscaleupsdk.third_party.quantum_machines.constants import (
|
28
30
|
MIN_TIME_OF_FLIGHT,
|
29
31
|
QUA_CLOCK_CYCLE,
|
30
32
|
)
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
# Disable QM logging and telemetry
|
35
|
-
os.environ["QM_DISABLE_STREAMOUTPUT"] = "True" # Used in 1.1.0
|
36
|
-
_qm_logger = logging.getLogger("qm")
|
37
|
-
_qm_logger.disabled = True
|
38
|
-
|
39
|
-
# Disable unwanted telemetry/logging modules in QM
|
40
|
-
_qm_patch_targets = [
|
41
|
-
"qm._loc._get_loc",
|
42
|
-
"qm.program.expressions._get_loc",
|
43
|
-
"qm.program.StatementsCollection._get_loc",
|
44
|
-
"qm.qua._get_loc",
|
45
|
-
"qm.qua._dsl._get_loc",
|
46
|
-
"qm.qua._expressions._get_loc",
|
47
|
-
"qm.qua.AnalogMeasureProcess._get_loc",
|
48
|
-
"qm.qua.DigitalMeasureProcess._get_loc",
|
49
|
-
"qm.datadog_api.DatadogHandler",
|
50
|
-
]
|
51
|
-
for target in _qm_patch_targets:
|
52
|
-
try:
|
53
|
-
_m = patch(target).__enter__()
|
54
|
-
_m.return_value = ""
|
55
|
-
except (AttributeError, ModuleNotFoundError): # noqa: PERF203
|
56
|
-
pass
|
57
|
-
|
58
|
-
PortRef = str
|
34
|
+
QPUPortRef = str
|
59
35
|
OctaveRef = str
|
60
36
|
ControllerRef = str
|
61
|
-
|
62
|
-
|
37
|
+
ControllerPort = int
|
38
|
+
|
39
|
+
|
40
|
+
class QuaProgram(BaseModel):
|
41
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
42
|
+
|
43
|
+
program: qm.QuaProgramMessage
|
44
|
+
config: qm_config.QuaConfig
|
45
|
+
|
46
|
+
@field_serializer("program")
|
47
|
+
def serialize_program(self, program: qm.QuaProgramMessage) -> str:
|
48
|
+
return program.to_json()
|
63
49
|
|
50
|
+
@field_validator("program", mode="before")
|
51
|
+
@classmethod
|
52
|
+
def deserialize_program(cls, program: object) -> qm.QuaProgramMessage:
|
53
|
+
if isinstance(program, qm.QuaProgramMessage):
|
54
|
+
return program
|
55
|
+
if isinstance(program, str | bytes):
|
56
|
+
return qm.QuaProgramMessage().from_json(program)
|
57
|
+
raise TypeError(f"Could not parse program from {type(program).__name__}.")
|
64
58
|
|
65
|
-
|
59
|
+
def dumps(self) -> str:
|
60
|
+
return self.model_dump_json()
|
61
|
+
|
62
|
+
@classmethod
|
63
|
+
def loads(cls, data: str) -> Self:
|
64
|
+
return cls.model_validate_json(data)
|
65
|
+
|
66
|
+
|
67
|
+
class OctaveConfig(qm_config.OctaveConfig121):
|
66
68
|
host: str | None = Field(default=None)
|
67
69
|
port: int | None = Field(default=None)
|
68
70
|
|
69
|
-
def to_qm_octave_config_121(self) -> OctaveConfig121:
|
70
|
-
return OctaveConfig121.model_validate(self.model_dump())
|
71
|
+
def to_qm_octave_config_121(self) -> qm_config.OctaveConfig121:
|
72
|
+
return qm_config.OctaveConfig121.model_validate(self.model_dump())
|
71
73
|
|
72
74
|
|
73
75
|
class DrivePortConfig(BaseModel):
|
74
76
|
port_type: Literal["drive"] = "drive"
|
75
|
-
port_mapping:
|
77
|
+
port_mapping: tuple[OctaveRef, ControllerPort]
|
76
78
|
|
77
79
|
|
78
80
|
class FluxPortConfig(BaseModel):
|
79
81
|
port_type: Literal["flux"] = "flux"
|
80
|
-
port_mapping:
|
82
|
+
port_mapping: tuple[ControllerRef, ControllerPort]
|
81
83
|
|
82
84
|
|
83
85
|
class ReadoutPortConfig(BaseModel):
|
84
86
|
port_type: Literal["readout"] = "readout"
|
85
|
-
port_mapping:
|
87
|
+
port_mapping: tuple[OctaveRef, ControllerPort]
|
86
88
|
time_of_flight: DurationNsLike
|
87
89
|
smearing: DurationNsLike = Field(default=Duration(0, TimeUnit.NS))
|
88
90
|
|
89
91
|
@model_validator(mode="after")
|
90
92
|
def _validate_readout_port_config(self) -> Self:
|
91
|
-
|
92
|
-
time_of_flight_ns
|
93
|
-
smearing_ns = self.smearing.convert(TimeUnit.NS).value
|
94
|
-
qua_clock_cycle_ns = QUA_CLOCK_CYCLE.convert(TimeUnit.NS).value
|
95
|
-
|
96
|
-
if time_of_flight_ns < min_time_of_flight_ns:
|
93
|
+
time_of_flight_ns = self.time_of_flight.to_ns().value
|
94
|
+
if time_of_flight_ns < MIN_TIME_OF_FLIGHT.to_ns().value:
|
97
95
|
raise ValueError(f"time_of_flight must be >= {MIN_TIME_OF_FLIGHT}")
|
98
96
|
|
99
|
-
if time_of_flight_ns %
|
97
|
+
if time_of_flight_ns % QUA_CLOCK_CYCLE.to_ns().value != 0:
|
100
98
|
raise ValueError(f"time_of_flight must be a multiple of {QUA_CLOCK_CYCLE}")
|
101
99
|
|
102
|
-
if
|
100
|
+
if self.smearing.to_ns().value > time_of_flight_ns - 8:
|
103
101
|
raise ValueError(f"smearing must be at most {time_of_flight_ns - 8} ns")
|
104
102
|
|
105
103
|
return self
|
106
104
|
|
107
105
|
|
108
|
-
OPXControllerConfig = ControllerConfigType
|
109
|
-
OPX1000ControllerConfig = OPX1000ControllerConfigType
|
106
|
+
OPXControllerConfig = qm_config.ControllerConfigType
|
107
|
+
OPX1000ControllerConfig = qm_config.OPX1000ControllerConfigType
|
110
108
|
|
111
109
|
|
112
|
-
class QuantumMachinesControllerInfo(
|
110
|
+
class QuantumMachinesControllerInfo(BaseModel):
|
113
111
|
"""
|
114
112
|
QuantumMachinesControllerInfo is a data model that represents the configuration
|
115
113
|
and port settings for quantum machine controllers.
|
@@ -119,6 +117,8 @@ class QuantumMachinesControllerInfo(BaseControllerInfo):
|
|
119
117
|
|
120
118
|
Attributes
|
121
119
|
----------
|
120
|
+
controller_type : Literal[ControllerType.QUANTUM_MACHINES]
|
121
|
+
The type of controller, which is always `ControllerType.QUANTUM_MACHINES`.
|
122
122
|
controllers : dict[ControllerRef, OPXControllerConfig | OPX1000ControllerConfig]
|
123
123
|
A dictionary mapping controller references to their respective configurations.
|
124
124
|
The configurations can be either OPXControllerConfig or OPX1000ControllerConfig.
|
@@ -132,8 +132,9 @@ class QuantumMachinesControllerInfo(BaseControllerInfo):
|
|
132
132
|
Not derived from OPX Config, this is our custom config.
|
133
133
|
"""
|
134
134
|
|
135
|
+
controller_type: Literal[ControllerType.QUANTUM_MACHINES] = ControllerType.QUANTUM_MACHINES
|
135
136
|
controllers: dict[ControllerRef, OPXControllerConfig | OPX1000ControllerConfig] = Field(
|
136
137
|
default={},
|
137
138
|
)
|
138
139
|
octaves: dict[OctaveRef, OctaveConfig] = Field(default={})
|
139
|
-
port_config: dict[
|
140
|
+
port_config: dict[QPUPortRef, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
|
@@ -0,0 +1,117 @@
|
|
1
|
+
from google.protobuf.json_format import MessageToDict
|
2
|
+
from google.protobuf.struct_pb2 import Struct
|
3
|
+
|
4
|
+
from boulderopalscaleupsdk.device.controller import (
|
5
|
+
QBLOXControllerInfo,
|
6
|
+
QuantumMachinesControllerInfo,
|
7
|
+
)
|
8
|
+
from boulderopalscaleupsdk.device.controller.base import (
|
9
|
+
Backend,
|
10
|
+
ControllerType,
|
11
|
+
)
|
12
|
+
from boulderopalscaleupsdk.protobuf.v1 import agent_pb2
|
13
|
+
|
14
|
+
|
15
|
+
class ControllerResolverService:
|
16
|
+
"""
|
17
|
+
Service for resolving controller-related types and backends.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def resolve_backend_from_controller(
|
21
|
+
self,
|
22
|
+
controller_type: ControllerType | QBLOXControllerInfo | QuantumMachinesControllerInfo,
|
23
|
+
) -> Backend:
|
24
|
+
"""
|
25
|
+
Resolve the backend based on the controller type.
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
controller_type : ControllerType | QBLOXControllerInfo | QuantumMachinesControllerInfo
|
30
|
+
The type of the controller, either as an enum or a data structure.
|
31
|
+
|
32
|
+
Returns
|
33
|
+
-------
|
34
|
+
Backend
|
35
|
+
The corresponding backend for the controller type.
|
36
|
+
"""
|
37
|
+
match controller_type:
|
38
|
+
case QuantumMachinesControllerInfo() | ControllerType.QUANTUM_MACHINES:
|
39
|
+
return Backend.QUA
|
40
|
+
case QBLOXControllerInfo() | ControllerType.QBLOX:
|
41
|
+
return Backend.QBLOX_Q1ASM
|
42
|
+
|
43
|
+
def resolve_controller_type_from_request(
|
44
|
+
self,
|
45
|
+
program_request: agent_pb2.RunProgramRequest,
|
46
|
+
) -> ControllerType:
|
47
|
+
"""
|
48
|
+
Resolve the controller type from a RunProgramRequest.
|
49
|
+
|
50
|
+
Parameters
|
51
|
+
----------
|
52
|
+
program_request : agent_pb2.RunProgramRequest
|
53
|
+
The request containing the controller type.
|
54
|
+
|
55
|
+
Returns
|
56
|
+
-------
|
57
|
+
ControllerType
|
58
|
+
The resolved controller type.
|
59
|
+
|
60
|
+
Raises
|
61
|
+
------
|
62
|
+
TypeError
|
63
|
+
If the controller type is unknown or not set.
|
64
|
+
ValueError
|
65
|
+
If the controller type in the request is invalid.
|
66
|
+
"""
|
67
|
+
try:
|
68
|
+
controller_type = ControllerType(program_request.controller_type)
|
69
|
+
except ValueError as err:
|
70
|
+
raise TypeError(
|
71
|
+
f"Unknown controller type: {program_request.controller_type}",
|
72
|
+
) from err
|
73
|
+
|
74
|
+
return controller_type
|
75
|
+
|
76
|
+
def resolve_controller_info_from_controller_data_struct(
|
77
|
+
self,
|
78
|
+
data: Struct,
|
79
|
+
) -> QBLOXControllerInfo | QuantumMachinesControllerInfo:
|
80
|
+
"""
|
81
|
+
Resolve the controller info from a Struct data structure.
|
82
|
+
|
83
|
+
Parameters
|
84
|
+
----------
|
85
|
+
data : Struct
|
86
|
+
The data structure containing the controller information.
|
87
|
+
|
88
|
+
Returns
|
89
|
+
-------
|
90
|
+
QBLOXControllerInfo | QuantumMachinesControllerInfo
|
91
|
+
The resolved controller info type.
|
92
|
+
|
93
|
+
Raises
|
94
|
+
------
|
95
|
+
TypeError
|
96
|
+
If the controller type is unknown or not set.
|
97
|
+
"""
|
98
|
+
ref = MessageToDict(data).get("controller_type", None)
|
99
|
+
controller_type: type[QuantumMachinesControllerInfo | QBLOXControllerInfo]
|
100
|
+
match ref:
|
101
|
+
case ControllerType.QUANTUM_MACHINES.value:
|
102
|
+
controller_type = QuantumMachinesControllerInfo
|
103
|
+
case ControllerType.QBLOX.value:
|
104
|
+
controller_type = QBLOXControllerInfo
|
105
|
+
case None:
|
106
|
+
raise TypeError(
|
107
|
+
"Controller type is not set in the response. "
|
108
|
+
"This may indicate that the device does not have a controller set.",
|
109
|
+
)
|
110
|
+
case _:
|
111
|
+
raise TypeError(
|
112
|
+
f"Unknown controller type: {ref}. "
|
113
|
+
"This may indicate that the device does not have a controller set.",
|
114
|
+
)
|
115
|
+
return controller_type.model_validate(
|
116
|
+
MessageToDict(data),
|
117
|
+
)
|
@@ -0,0 +1,61 @@
|
|
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
|
+
import re
|
15
|
+
from typing import Annotated, Any, Literal
|
16
|
+
|
17
|
+
from pydantic import BeforeValidator, PlainSerializer, TypeAdapter
|
18
|
+
from pydantic.dataclasses import dataclass
|
19
|
+
|
20
|
+
QubitAddr = tuple[int, ...]
|
21
|
+
GateName = str
|
22
|
+
|
23
|
+
_KEY_PATTERN = r"^.+#\d+(?:-\d+)*$"
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass(frozen=True)
|
27
|
+
class DataKey:
|
28
|
+
gate: GateName
|
29
|
+
addr: QubitAddr
|
30
|
+
|
31
|
+
def as_tuple(self) -> tuple[GateName, QubitAddr]:
|
32
|
+
return (self.gate, self.addr)
|
33
|
+
|
34
|
+
@staticmethod
|
35
|
+
def from_string(value: Any) -> "DataKey | None":
|
36
|
+
match value:
|
37
|
+
case str():
|
38
|
+
if not re.match(_KEY_PATTERN, value):
|
39
|
+
raise KeyError(f"invalid string for DataKey {value}")
|
40
|
+
gate, qubit_str = value.split("#", 1)
|
41
|
+
qubit_addr = tuple(int(q) for q in qubit_str.split("-"))
|
42
|
+
return DataKey(gate=gate, addr=qubit_addr)
|
43
|
+
case _:
|
44
|
+
return None
|
45
|
+
|
46
|
+
def serialize_as_string(self) -> str:
|
47
|
+
return f"{self.gate}#{'-'.join(str(i) for i in self.addr)}"
|
48
|
+
|
49
|
+
|
50
|
+
DataKeyTypeAdapter = TypeAdapter(DataKey)
|
51
|
+
DataKeyLike = Annotated[
|
52
|
+
DataKey,
|
53
|
+
BeforeValidator(lambda x: DataKey.from_string(x) or x),
|
54
|
+
PlainSerializer(lambda x: x.serialize_as_string(), return_type=str),
|
55
|
+
]
|
56
|
+
|
57
|
+
|
58
|
+
@dataclass
|
59
|
+
class DefCalData:
|
60
|
+
body: str
|
61
|
+
status: Literal["calibrated", "uncalibrated"]
|
@@ -11,10 +11,15 @@
|
|
11
11
|
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
12
|
# License for the specific language.
|
13
13
|
|
14
|
+
|
14
15
|
from pydantic import BaseModel
|
15
16
|
from pydantic.dataclasses import dataclass
|
16
17
|
|
17
|
-
from boulderopalscaleupsdk.device.controller
|
18
|
+
from boulderopalscaleupsdk.device.controller import (
|
19
|
+
QBLOXControllerInfo,
|
20
|
+
QuantumMachinesControllerInfo,
|
21
|
+
)
|
22
|
+
from boulderopalscaleupsdk.device.defcal import DataKeyLike, DefCalData
|
18
23
|
from boulderopalscaleupsdk.device.processor import SuperconductingProcessor
|
19
24
|
|
20
25
|
|
@@ -22,7 +27,8 @@ class Device(BaseModel):
|
|
22
27
|
"""Device specification."""
|
23
28
|
|
24
29
|
processor: SuperconductingProcessor # | OtherProcessorTypes
|
25
|
-
controller_info:
|
30
|
+
controller_info: QBLOXControllerInfo | QuantumMachinesControllerInfo
|
31
|
+
defcals: dict[DataKeyLike, DefCalData]
|
26
32
|
|
27
33
|
|
28
34
|
@dataclass
|
@@ -12,12 +12,20 @@
|
|
12
12
|
# License for the specific language.
|
13
13
|
|
14
14
|
__all__ = [
|
15
|
+
"CalibrationThresholds",
|
15
16
|
"ComponentParameter",
|
16
17
|
"DurationComponentParameter",
|
17
18
|
"FloatComponentParameter",
|
18
19
|
"SuperconductingProcessor",
|
19
20
|
"SuperconductingProcessorTemplate",
|
21
|
+
"update_parameter",
|
20
22
|
]
|
21
23
|
|
22
|
-
from .common import
|
24
|
+
from .common import (
|
25
|
+
CalibrationThresholds,
|
26
|
+
ComponentParameter,
|
27
|
+
DurationComponentParameter,
|
28
|
+
FloatComponentParameter,
|
29
|
+
update_parameter,
|
30
|
+
)
|
23
31
|
from .superconducting_processor import SuperconductingProcessor, SuperconductingProcessorTemplate
|