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.
Files changed (46) hide show
  1. boulder_opal_scale_up_sdk-1.0.1.dist-info/LICENSE +805 -0
  2. {boulder_opal_scale_up_sdk-1.0.0.dist-info → boulder_opal_scale_up_sdk-1.0.1.dist-info}/METADATA +17 -7
  3. boulder_opal_scale_up_sdk-1.0.1.dist-info/RECORD +60 -0
  4. boulderopalscaleupsdk/agent/worker.py +22 -11
  5. boulderopalscaleupsdk/common/dtypes.py +68 -111
  6. boulderopalscaleupsdk/device/__init__.py +5 -1
  7. boulderopalscaleupsdk/device/config_loader.py +10 -8
  8. boulderopalscaleupsdk/device/controller/__init__.py +17 -14
  9. boulderopalscaleupsdk/device/controller/base.py +12 -3
  10. boulderopalscaleupsdk/device/controller/qblox.py +43 -17
  11. boulderopalscaleupsdk/device/controller/quantum_machines.py +60 -59
  12. boulderopalscaleupsdk/device/controller/resolver.py +117 -0
  13. boulderopalscaleupsdk/device/defcal.py +61 -0
  14. boulderopalscaleupsdk/device/device.py +8 -2
  15. boulderopalscaleupsdk/device/processor/__init__.py +9 -1
  16. boulderopalscaleupsdk/device/processor/common.py +129 -20
  17. boulderopalscaleupsdk/device/processor/superconducting_processor.py +59 -13
  18. boulderopalscaleupsdk/experiments/__init__.py +8 -0
  19. boulderopalscaleupsdk/experiments/common.py +8 -4
  20. boulderopalscaleupsdk/experiments/power_rabi.py +3 -3
  21. boulderopalscaleupsdk/experiments/ramsey.py +13 -6
  22. boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +24 -0
  23. boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +3 -3
  24. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +10 -10
  25. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +3 -3
  26. boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +68 -0
  27. boulderopalscaleupsdk/experiments/transmon_resonator_chi_scan.py +56 -0
  28. boulderopalscaleupsdk/experiments/transmon_spectroscopy.py +69 -0
  29. boulderopalscaleupsdk/grpc_interceptors/auth.py +5 -2
  30. boulderopalscaleupsdk/plotting/__init__.py +20 -2
  31. boulderopalscaleupsdk/plotting/dtypes.py +81 -117
  32. boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +17 -17
  33. boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +8 -6
  34. boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +13 -13
  35. boulderopalscaleupsdk/protobuf/v1/device_pb2.py +35 -17
  36. boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +40 -6
  37. boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +116 -14
  38. boulderopalscaleupsdk/routines/__init__.py +1 -4
  39. boulderopalscaleupsdk/routines/resonator_mapping.py +8 -2
  40. boulderopalscaleupsdk/stubs/__init__.py +12 -0
  41. boulderopalscaleupsdk/stubs/dtypes.py +47 -0
  42. boulderopalscaleupsdk/stubs/maps.py +9 -0
  43. boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +32 -0
  44. boulderopalscaleupsdk/third_party/quantum_machines/config.py +30 -9
  45. boulder_opal_scale_up_sdk-1.0.0.dist-info/RECORD +0 -50
  46. {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.common.dtypes import Self
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("could not parse module address")
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, eq=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("could not parse sequencer address")
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("could not parse port address")
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 ControllerInfo(BaseModel): # pragma: no cover
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=True)
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 dump(self) -> bytes:
366
- return self.model_dump_json().encode("utf-8")
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 dump(self) -> bytes:
399
- return self.model_dump_json().encode("utf-8")
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 logging
15
- import os
16
- from typing import Literal
17
- from unittest.mock import patch
18
-
19
- from pydantic import BaseModel, Field, model_validator
20
-
21
- from boulderopalscaleupsdk.common.dtypes import Duration, DurationNsLike, Self, TimeUnit
22
- from boulderopalscaleupsdk.third_party.quantum_machines.config import (
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
- from .base import BaseControllerInfo
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
- PortNum = int
62
- PortMapping = tuple[OctaveRef, PortNum]
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
- class OctaveConfig(OctaveConfig121):
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: PortMapping
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: PortMapping
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: PortMapping
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
- min_time_of_flight_ns = MIN_TIME_OF_FLIGHT.convert(TimeUnit.NS).value
92
- time_of_flight_ns = self.time_of_flight.convert(TimeUnit.NS).value
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 % qua_clock_cycle_ns != 0:
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 smearing_ns > time_of_flight_ns - 8:
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(BaseControllerInfo):
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[PortRef, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
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.quantum_machines import QuantumMachinesControllerInfo
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: QuantumMachinesControllerInfo # | OtherControllerInfoTypes
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 ComponentParameter, DurationComponentParameter, FloatComponentParameter
24
+ from .common import (
25
+ CalibrationThresholds,
26
+ ComponentParameter,
27
+ DurationComponentParameter,
28
+ FloatComponentParameter,
29
+ update_parameter,
30
+ )
23
31
  from .superconducting_processor import SuperconductingProcessor, SuperconductingProcessorTemplate