boulder-opal-scale-up-sdk 1.0.3__tar.gz → 1.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/PKG-INFO +1 -1
  2. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/agent/worker.py +23 -4
  3. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/common/dtypes.py +31 -1
  4. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/config_loader.py +1 -0
  5. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/controller/qblox.py +222 -25
  6. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/device/device.py → boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/device/defcal.py +7 -9
  7. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/device/device.py +67 -0
  8. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/processor/__init__.py +0 -2
  9. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/processor/common.py +1 -6
  10. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/processor/superconducting_processor.py +34 -7
  11. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/__init__.py +24 -2
  12. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/chi01_scan.py +18 -15
  13. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/common.py +0 -16
  14. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/drag_leakage_calibration.py +66 -0
  15. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/fine_amplitude_calibration.py +54 -0
  16. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/power_rabi.py +22 -16
  17. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/power_rabi_ef.py +67 -0
  18. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/ramsey.py +62 -0
  19. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +50 -0
  20. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +20 -18
  21. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +23 -20
  22. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +22 -21
  23. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/t1.py +16 -22
  24. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/t2.py +51 -0
  25. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/t2_echo.py +51 -0
  26. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +26 -24
  27. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/experiments/transmon_spectroscopy.py +19 -17
  28. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/experiments/t2.py → boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/voltage_bias_fine_tune.py +22 -12
  29. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/waveforms.py +63 -0
  30. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/experiments/ramsey.py → boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/experiments/zz_ramsey.py +19 -19
  31. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/plotting/dtypes.py +6 -3
  32. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +9 -3
  33. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +14 -0
  34. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +34 -0
  35. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/protobuf/v1/device_pb2.py +89 -0
  36. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +8 -4
  37. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/routines/__init__.py +30 -0
  38. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/device/__init__.py → boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/routines/common.py +9 -5
  39. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/routines/one_qubit_calibration.py +36 -0
  40. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/routines/resonator_mapping.py +35 -0
  41. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/routines/transmon_coherence.py +34 -0
  42. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/routines/transmon_discovery.py +41 -0
  43. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/routines/transmon_retuning.py +41 -0
  44. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/stubs/maps.py +11 -2
  45. boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/utils/__init__.py +12 -0
  46. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/pyproject.toml +2 -4
  47. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/device/defcal.py +0 -62
  48. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +0 -28
  49. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/protobuf/v1/device_pb2.py +0 -89
  50. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/routines/__init__.py +0 -6
  51. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/routines/common.py +0 -10
  52. boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/routines/resonator_mapping.py +0 -19
  53. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/LICENSE +0 -0
  54. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/README.md +0 -0
  55. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/__init__.py +0 -0
  56. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/agent/__init__.py +0 -0
  57. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/common/__init__.py +0 -0
  58. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/common/typeclasses.py +0 -0
  59. {boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/stubs → boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/device}/__init__.py +0 -0
  60. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/common.py +0 -0
  61. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/controller/__init__.py +0 -0
  62. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/controller/base.py +0 -0
  63. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/controller/quantum_machines.py +0 -0
  64. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/device/controller/resolver.py +0 -0
  65. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/grpc_interceptors/__init__.py +0 -0
  66. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/grpc_interceptors/auth.py +0 -0
  67. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/plotting/__init__.py +0 -0
  68. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +0 -0
  69. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/task_pb2.py +0 -0
  70. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +0 -0
  71. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +0 -0
  72. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/py.typed +0 -0
  73. {boulder_opal_scale_up_sdk-1.0.3/boulderopalscaleupsdk/utils → boulder_opal_scale_up_sdk-1.0.5/boulderopalscaleupsdk/stubs}/__init__.py +0 -0
  74. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/stubs/dtypes.py +0 -0
  75. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/third_party/__init__.py +0 -0
  76. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +0 -0
  77. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/third_party/quantum_machines/config.py +0 -0
  78. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/third_party/quantum_machines/constants.py +0 -0
  79. {boulder_opal_scale_up_sdk-1.0.3 → boulder_opal_scale_up_sdk-1.0.5}/boulderopalscaleupsdk/utils/serial_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boulder-opal-scale-up-sdk
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: Q-CTRL Boulder Opal Scale Up Python SDK
5
5
  License: https://q-ctrl.com/terms
6
6
  Keywords: black opal,boulder opal,fire opal,nisq,open controls,q control,q ctrl,q-control,q-ctrl,qcontrol,qctrl,quantum,quantum algorithms,quantum circuits,quantum coding,quantum coding software,quantum computing,quantum control,quantum control software,quantum control theory,quantum engineering,quantum error correction,quantum firmware,quantum fundamentals,quantum sensing,qubit,qudit
@@ -54,11 +54,13 @@ class TaskHandler(Protocol):
54
54
  self,
55
55
  request: agent_pb2.RunProgramRequest
56
56
  | agent_pb2.RunQuantumMachinesMixerCalibrationRequest
57
- | agent_pb2.DisplayResultsRequest,
57
+ | agent_pb2.DisplayResultsRequest
58
+ | agent_pb2.AskRequest,
58
59
  ) -> (
59
60
  agent_pb2.RunProgramResponse
60
61
  | agent_pb2.RunQuantumMachinesMixerCalibrationResponse
61
62
  | agent_pb2.DisplayResultsResponse
63
+ | agent_pb2.AskResponse
62
64
  | task_pb2.TaskErrorDetail
63
65
  ): ...
64
66
 
@@ -67,9 +69,8 @@ class TaskHandler(Protocol):
67
69
  task: task_pb2.Task,
68
70
  ) -> any_pb2.Any | task_pb2.TaskErrorDetail:
69
71
  request = (
70
- _as_run_program_request(
71
- task.data,
72
- )
72
+ _as_run_program_request(task.data)
73
+ or _as_ask_request(task.data)
73
74
  or _as_run_qua_calibration_request(task.data)
74
75
  or _as_display_results_request(task.data)
75
76
  )
@@ -78,6 +79,7 @@ class TaskHandler(Protocol):
78
79
  agent_pb2.RunProgramRequest()
79
80
  | agent_pb2.RunQuantumMachinesMixerCalibrationRequest()
80
81
  | agent_pb2.DisplayResultsRequest()
82
+ | agent_pb2.AskRequest()
81
83
  ):
82
84
  return _as_any_message(await self.handle(request))
83
85
  case None:
@@ -104,6 +106,17 @@ def _as_run_program_request(
104
106
  return request
105
107
 
106
108
 
109
+ def _as_ask_request(
110
+ task_result: any_pb2.Any,
111
+ ) -> agent_pb2.AskRequest | None:
112
+ request = agent_pb2.AskRequest()
113
+ unpacked: bool = task_result.Unpack(request) # type: ignore[reportUnknownMemberType]
114
+ if not unpacked:
115
+ return None
116
+
117
+ return request
118
+
119
+
107
120
  def _as_run_qua_calibration_request(
108
121
  task_result: any_pb2.Any,
109
122
  ) -> agent_pb2.RunQuantumMachinesMixerCalibrationRequest | None:
@@ -158,16 +171,22 @@ class Agent:
158
171
  Create a gRPC channel.
159
172
  """
160
173
  host = url.split(":")[0]
174
+ options = [
175
+ ("grpc.max_send_message_length", 1024 * 1024 * 50), # 50MB
176
+ ("grpc.max_receive_message_length", 1024 * 1024 * 50), # 50MB
177
+ ]
161
178
  if host in ["localhost", "127.0.0.1", "0.0.0.0", "::"]:
162
179
  channel = grpc.insecure_channel(
163
180
  url,
164
181
  interceptors=interceptors,
182
+ options=options,
165
183
  )
166
184
  else:
167
185
  channel = grpc.secure_channel(
168
186
  url,
169
187
  credentials=ssl_channel_credentials(),
170
188
  interceptors=interceptors,
189
+ options=options,
171
190
  )
172
191
  return channel
173
192
 
@@ -28,7 +28,7 @@ from typing import Annotated, Any, Literal, Self
28
28
 
29
29
  import numpy as np
30
30
  from dateutil.parser import isoparse
31
- from pydantic import BeforeValidator, ConfigDict, Field, PlainSerializer, TypeAdapter
31
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer, TypeAdapter
32
32
  from pydantic.dataclasses import dataclass
33
33
 
34
34
  GrpcMetadata = list[tuple[str, str | bytes]]
@@ -317,6 +317,36 @@ class JobHistorySortOrder(enum.Enum):
317
317
  CREATED_AT_ASC = 2
318
318
 
319
319
 
320
+ class JobSummary(BaseModel):
321
+ id: str
322
+ name: str
323
+ device_name: str
324
+ session_id: str
325
+ created_at: ISO8601DatetimeUTCLike
326
+
327
+ def __str__(self):
328
+ return f'JobSummary(name="{self.name}", id="{self.id}")'
329
+
330
+
331
+ class JobDataEntry(BaseModel):
332
+ message: str
333
+ created_at: ISO8601DatetimeUTCLike
334
+ dt: str
335
+ elapsed_time: str
336
+
337
+ class Config:
338
+ extra = "allow"
339
+
340
+
341
+ class JobData(BaseModel):
342
+ id: str
343
+ name: str
344
+ session_id: str
345
+ created_at: ISO8601DatetimeUTCLike
346
+ device_name: str
347
+ data: list[JobDataEntry]
348
+
349
+
320
350
  DEFAULT_JOB_HISTORY_PAGE = 1
321
351
  DEFAULT_JOB_HISTORY_PAGE_SIZE = 10
322
352
  DEFAULT_JOB_HISTORY_SORT_ORDER = JobHistorySortOrder.CREATED_AT_DESC
@@ -34,6 +34,7 @@ class ProcessorArchitecture(str, Enum):
34
34
  Superconducting = "superconducting"
35
35
 
36
36
 
37
+ # TODO: Remove this in the next release of SDK
37
38
  class DeviceInfo(BaseModel):
38
39
  controller_info: QBLOXControllerInfo | QuantumMachinesControllerInfo
39
40
  processor: SuperconductingProcessor # | OtherSDKProcessorType
@@ -18,6 +18,7 @@ QBLOX Quantum Control Stack
18
18
  __all__ = (
19
19
  "DEFAULT_MODULE_CONSTRAINTS",
20
20
  "AcquisitionConfig",
21
+ "BitStrideArrayEncoding",
21
22
  "ChannelType",
22
23
  "ComplexChannel",
23
24
  "IndexedData",
@@ -40,15 +41,19 @@ __all__ = (
40
41
  "SequenceProgram",
41
42
  "SequencerAddr",
42
43
  "SequencerAddrType",
44
+ "SequencerResults",
45
+ "process_sequencer_output",
43
46
  "validate_channel",
44
47
  )
45
48
 
46
49
  import dataclasses
47
50
  import enum
51
+ import math
48
52
  import re
49
53
  from dataclasses import dataclass
50
54
  from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar
51
55
 
56
+ import numpy as np
52
57
  from pydantic import BaseModel, BeforeValidator, Field, PlainSerializer, model_validator
53
58
 
54
59
  from boulderopalscaleupsdk.device.controller.base import Backend, ControllerType
@@ -252,18 +257,18 @@ ChannelType = RealChannel | ComplexChannel
252
257
  # ==================================================================================================
253
258
  # Controller information
254
259
  # ==================================================================================================
255
- class ElementConnection(BaseModel): # pragma: no cover
260
+ class PortConnection(BaseModel): # pragma: no cover
256
261
  """
257
- The connections involved for a control element.
262
+ The connections involved for a QPU port.
258
263
 
259
264
  Attributes
260
265
  ----------
261
266
  ch_out: ChannelType
262
- The output channel that will signal towards the control element.
263
- ch_in: ChannelType, optional
264
- The input channel from which signals will be acquired from the element. This is optional, as
265
- not all modules support acquisitions. If an input channel is specified, it must be located
266
- on the same module as the output channel.
267
+ The output channel that will signal towards the QPU port
268
+ ch_in: ChannelType or None, optional
269
+ The input channel from which signals will be acquired from the QPU port. This is optional,
270
+ as not all modules support acquisitions. If an input channel is specified, it must be
271
+ located on the same module as the output channel.
267
272
 
268
273
  Notes
269
274
  -----
@@ -271,20 +276,21 @@ class ElementConnection(BaseModel): # pragma: no cover
271
276
  direction is outwards from the control stack. The following diagram depicts a simple setup with
272
277
  the arrows indicating a control channel.
273
278
 
274
- ┌────────┐ ┌────────────┐
275
- │ │─── out ──►│Element: xy1
276
- │ QBLOX │ └────────────┘
277
- │ Stack │ ┌────────────┐
278
- │ │─── out ──►│Element: ro1
279
- │ │◄── in ────│
280
- └────────┘ └────────────┘
279
+ ┌────────┐ ┌───────────────┐
280
+ │ │─── out ──►│ Port: p_xy1
281
+ │ QBLOX │ └───────────────┘
282
+ │ Stack │ ┌───────────────┐
283
+ │ │─── out ──►│ Port: p_flrr0
284
+ │ │◄── in ────│
285
+ └────────┘ └───────────────┘
286
+ QPU fridge
281
287
  """
282
288
 
283
289
  ch_out: ChannelType
284
290
  ch_in: ChannelType | None = None
285
291
 
286
292
  @model_validator(mode="after")
287
- def validate_channels(self) -> "ElementConnection":
293
+ def validate_channels(self) -> "PortConnection":
288
294
  if self.ch_in is not None and self.ch_in.module != self.ch_out.module:
289
295
  raise ValueError("I/O channels for an element must be on the same module.")
290
296
  return self
@@ -304,38 +310,102 @@ class QBLOXControllerInfo(BaseModel): # pragma: no cover
304
310
  The type of controller, which is always `ControllerType.QBLOX` for this class.
305
311
  modules: dict[ModuleAddrType, ModuleType]
306
312
  The modules connected to the QBLOX stack.
307
- elements: dict[str, ElementConnection]
308
- The addressable control elements for the stack.
313
+ port_config: dict[str, PortConnection]
314
+ The dictionary of ports with their types and addresses.
309
315
  """
310
316
 
311
317
  controller_type: Literal[ControllerType.QBLOX] = ControllerType.QBLOX
312
318
  modules: dict[ModuleAddrType, ModuleType]
313
- elements: dict[str, ElementConnection]
319
+ port_config: dict[str, PortConnection]
314
320
 
315
321
 
316
322
  # ==================================================================================================
317
323
  # Instrument management
318
324
  # ==================================================================================================
319
325
  class SequencerParams(BaseModel):
320
- nco_freq: float | None = Field(default=None, gt=0)
326
+ nco_freq: float | None = Field(default=None)
321
327
  gain_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
322
328
  offset_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
323
329
  gain_awg_path1: float | None = Field(default=None, ge=-1.0, le=1.0)
324
330
  offset_awg_path1: float | None = Field(default=None, ge=-1.0, le=1.0)
325
331
  marker_ovr_en: bool | None = Field(default=None)
326
- marker_ovr_value: int | None = Field(default=None)
332
+ marker_ovr_value: int | None = Field(default=None, ge=0, le=15)
327
333
  mod_en_awg: bool | None = Field(default=None)
328
334
  demod_en_acq: bool | None = Field(default=None)
329
335
  sync_en: bool | None = Field(default=None)
330
336
  nco_prop_delay_comp_en: bool | None = Field(default=True)
331
- integration_length_acq: int | None = Field(default=None)
337
+ integration_length_acq: int | None = Field(default=None, ge=4, le=16777212, multiple_of=4)
332
338
 
333
339
 
334
- class ModuleParams(BaseModel):
335
- out0_in0_lo_freq: float | None = Field(default=None, gt=0)
336
- out0_in0_lo_en: bool | None = Field(default=None)
340
+ class QcmParams(BaseModel):
341
+ out0_offset: float | None = Field(default=None, ge=-2.5, le=2.5)
342
+ out1_offset: float | None = Field(default=None, ge=-2.5, le=2.5)
343
+ out2_offset: float | None = Field(default=None, ge=-2.5, le=2.5)
344
+ out3_offset: float | None = Field(default=None, ge=-2.5, le=2.5)
345
+
346
+ def update(self, other: Self) -> None:
347
+ if self == other:
348
+ return # Nothing to do
349
+
350
+ self.out0_offset = pick_only_one_or_raise(self.out0_offset, other.out0_offset)
351
+ self.out1_offset = pick_only_one_or_raise(self.out1_offset, other.out1_offset)
352
+ self.out2_offset = pick_only_one_or_raise(self.out2_offset, other.out2_offset)
353
+ self.out3_offset = pick_only_one_or_raise(self.out3_offset, other.out3_offset)
354
+
355
+
356
+ class QcmRfParams(BaseModel):
357
+ out0_att: int | None = Field(default=None, ge=0, le=60, multiple_of=2)
358
+ out1_att: int | None = Field(default=None, ge=0, le=60, multiple_of=2)
359
+
337
360
  out0_lo_freq: float | None = Field(default=None, gt=0)
338
361
  out0_lo_en: bool | None = Field(default=None)
362
+ out1_lo_freq: float | None = Field(default=None, gt=0)
363
+ out1_lo_en: bool | None = Field(default=None)
364
+
365
+ def update(self, other: Self) -> None:
366
+ if self == other:
367
+ return # Nothing to do
368
+
369
+ self.out0_att = pick_only_one_or_raise(self.out0_att, other.out0_att)
370
+ self.out1_att = pick_only_one_or_raise(self.out1_att, other.out1_att)
371
+ self.out0_lo_freq = pick_only_one_or_raise(self.out0_lo_freq, other.out0_lo_freq)
372
+ self.out0_lo_en = pick_only_one_or_raise(self.out0_lo_en, other.out0_lo_en)
373
+ self.out1_lo_freq = pick_only_one_or_raise(self.out1_lo_freq, other.out1_lo_freq)
374
+ self.out1_lo_en = pick_only_one_or_raise(self.out1_lo_en, other.out1_lo_en)
375
+
376
+
377
+ class QrmRfParams(BaseModel):
378
+ out0_att: int | None = Field(default=None, ge=0, le=60, multiple_of=2)
379
+
380
+ out0_in0_lo_freq: float | None = Field(default=None, gt=0)
381
+ out0_in0_lo_en: bool | None = Field(default=None)
382
+
383
+ def update(self, other: Self) -> None:
384
+ if self == other:
385
+ return # Nothing to do
386
+
387
+ self.out0_att = pick_only_one_or_raise(self.out0_att, other.out0_att)
388
+ self.out0_in0_lo_freq = pick_only_one_or_raise(
389
+ self.out0_in0_lo_freq,
390
+ other.out0_in0_lo_freq,
391
+ )
392
+ self.out0_in0_lo_en = pick_only_one_or_raise(self.out0_in0_lo_en, other.out0_in0_lo_en)
393
+
394
+
395
+ ModuleParams = QcmParams | QcmRfParams | QrmRfParams
396
+
397
+
398
+ T0 = TypeVar("T0")
399
+
400
+
401
+ def pick_only_one_or_raise(a: T0 | None, b: T0 | None) -> T0 | None:
402
+ if a == b:
403
+ return a
404
+ if a is None:
405
+ return b
406
+ if b is None:
407
+ return a
408
+ raise ValueError(f"Cannot resolve conflict between given parameters {a} and {b}!")
339
409
 
340
410
 
341
411
  # ==================================================================================================
@@ -373,6 +443,7 @@ class SequenceProgram(BaseModel):
373
443
  weights: dict[str, IndexedData] = {}
374
444
  acquisitions: dict[str, AcquisitionConfig] = {}
375
445
  acquisition_scopes: list[str] = []
446
+ acquisition_shapes: dict[str, tuple[int, ...]] = {}
376
447
  params: SequencerParams = SequencerParams()
377
448
  params_only: bool = False
378
449
 
@@ -408,7 +479,7 @@ class PreparedSequenceProgram(BaseModel): # pragma: no cover
408
479
 
409
480
 
410
481
  class PreparedModule(BaseModel):
411
- params: ModuleParams = ModuleParams()
482
+ params: ModuleParams
412
483
 
413
484
 
414
485
  class PreparedProgram(BaseModel):
@@ -417,6 +488,14 @@ class PreparedProgram(BaseModel):
417
488
  modules: dict[ModuleAddrType, PreparedModule] # The set of modules this program will target.
418
489
  sequence_programs: dict[str, PreparedSequenceProgram] # The individual element programs.
419
490
 
491
+ @property
492
+ def sequencers(self) -> dict[SequencerAddr, str]:
493
+ return {psp.sequencer_addr: name for name, psp in self.sequence_programs.items()}
494
+
495
+ def get_sequencer_program(self, seq_addr: SequencerAddr) -> SequenceProgram:
496
+ prog_name = self.sequencers[seq_addr]
497
+ return self.sequence_programs[prog_name].sequence_program
498
+
420
499
  def dumps(self) -> str:
421
500
  return self.model_dump_json()
422
501
 
@@ -428,6 +507,9 @@ class PreparedProgram(BaseModel):
428
507
  # ==================================================================================================
429
508
  # Results
430
509
  # ==================================================================================================
510
+ MAX_ACQUISITION_BINS = 131072
511
+
512
+
431
513
  class OutputScopedAcquisitionData(BaseModel): # pragma: no cover
432
514
  """
433
515
  Scoped acquisition data for a single path in `OutputScopedAcquisition`.
@@ -544,6 +626,121 @@ class OutputIndexedAcquisition(BaseModel): # pragma: no cover
544
626
  OutputSequencerAcquisitions = dict[str, OutputIndexedAcquisition] # pragma: no cover
545
627
 
546
628
 
629
+ @dataclass
630
+ class SequencerResults:
631
+ """
632
+ Sequencer results formatted as a complex signal.
633
+
634
+ The real component corresponds to results on path0, whilst the imaginary component corresponds
635
+ to the results on path1.
636
+ """
637
+
638
+ scopes: dict[str, np.ndarray]
639
+ bins: dict[str, np.ndarray]
640
+
641
+
642
+ def process_sequencer_output(
643
+ program: SequenceProgram,
644
+ output: OutputSequencerAcquisitions,
645
+ ) -> SequencerResults:
646
+ """
647
+ Process the output from executing a sequencer into a simplified SequencerResults data structure.
648
+
649
+ Parameters
650
+ ----------
651
+ program: SequenceProgram
652
+ The corresponding program that was executed
653
+ output: OutputSequencerAcquisitions
654
+ The results of one sequencer's execution
655
+
656
+ Returns
657
+ -------
658
+ SequencerResults
659
+ """
660
+ bins = {}
661
+ scopes = {}
662
+ for acq_ref, acq_result in output.items():
663
+ acquisition = acq_result.acquisition
664
+
665
+ raw_bin = acquisition.bins.integration
666
+ shape = program.acquisition_shapes.get(acq_ref)
667
+ if shape is None or len(shape) == 1:
668
+ bins[acq_ref] = np.array(raw_bin.path0) + 1j * np.array(raw_bin.path1)
669
+ else:
670
+ bse = BitStrideArrayEncoding.from_desired(shape)
671
+ bins[acq_ref] = bse.decode(raw_bin.path0) + 1j * bse.decode(raw_bin.path1)
672
+
673
+ raw_scope = acquisition.scope
674
+ if acq_ref in program.acquisition_scopes:
675
+ scopes[acq_ref] = np.array(raw_scope.path0.data) + 1j * np.array(raw_scope.path1.data)
676
+
677
+ return SequencerResults(scopes=scopes, bins=bins)
678
+
679
+
680
+ @dataclass
681
+ class BitStrideArrayEncoding:
682
+ """
683
+ Encode a multi-dimensional array such that each dimensional index occupies an integer number of
684
+ bits (the bit-stride).
685
+
686
+ In this encoding, a linear index is calculated by left-shifting each element in the index by its
687
+ corresponding bit-stride:
688
+
689
+ linear_index = Σ (ii << bb) ∀ (ii, bb) ∈ zip(index, bit_strides)
690
+
691
+ Examples
692
+ --------
693
+ Given a desired_shape of (3, 5), the first index will have a bit-stride of 2 and the second
694
+ index will have a bit-stride of 3. To determine the linear index for a sample, we will use:
695
+
696
+ def linear_index(idx0: int, idx1: int) -> int:
697
+ return (idx0 << 2) + (idx1 << 3)
698
+
699
+ Note, the encoded data will occupy 2^2 * 2^3 samples and have (2^2 * 2^3 - 3 * 5) unused data
700
+ points.
701
+
702
+ >>> bse = BitStrideArrayEncoding.from_desired((3, 5))
703
+ >>> bse.encoded_shape
704
+ (4, 8)
705
+ >>> bse.bit_stride
706
+ (2, 3)
707
+ """
708
+
709
+ desired_shape: tuple[int, ...]
710
+ encoded_shape: tuple[int, ...]
711
+ bit_stride: tuple[int, ...]
712
+
713
+ @staticmethod
714
+ def _round_power2_32bit(val: int) -> int:
715
+ val -= 1
716
+ val |= val >> 1
717
+ val |= val >> 2
718
+ val |= val >> 4
719
+ val |= val >> 8
720
+ val |= val >> 16
721
+ val += 1
722
+ return val
723
+
724
+ @classmethod
725
+ def from_desired(cls, desired_shape: tuple[int, ...]) -> Self:
726
+ encoded_shape = tuple(
727
+ BitStrideArrayEncoding._round_power2_32bit(dim) for dim in desired_shape
728
+ )
729
+ # Right-most (most nested) dimension takes least significant bits!
730
+ exponents = tuple(int(math.log2(dim)) for dim in encoded_shape)
731
+ n_bits = sum(exponents)
732
+ bit_stride = tuple(n_bits - sum(exponents[: idx + 1]) for idx in range(len(exponents)))
733
+ return cls(
734
+ desired_shape=desired_shape,
735
+ encoded_shape=encoded_shape,
736
+ bit_stride=bit_stride,
737
+ )
738
+
739
+ def decode(self, values: list[float]) -> np.ndarray:
740
+ decoded = np.reshape(values, self.encoded_shape)
741
+ return decoded[tuple(slice(0, dim) for dim in self.desired_shape)]
742
+
743
+
547
744
  # ==================================================================================================
548
745
  # Utilities
549
746
  # ==================================================================================================
@@ -11,15 +11,13 @@
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
+ from typing import Literal
14
15
 
15
- from pydantic.dataclasses import dataclass
16
+ from pydantic import BaseModel
16
17
 
17
18
 
18
- @dataclass
19
- class InvalidDevice:
20
- message: str
21
-
22
-
23
- @dataclass
24
- class InvalidDeviceComponent:
25
- message: str
19
+ class DefCalData(BaseModel):
20
+ gate: str
21
+ addr: list[str]
22
+ body: str
23
+ status: Literal["calibrated", "uncalibrated"]
@@ -0,0 +1,67 @@
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
+
15
+ from pydantic import BaseModel
16
+ from pydantic.dataclasses import dataclass
17
+
18
+ from boulderopalscaleupsdk.common.dtypes import ISO8601DatetimeUTCLike
19
+ from boulderopalscaleupsdk.device.controller import (
20
+ QBLOXControllerInfo,
21
+ QuantumMachinesControllerInfo,
22
+ )
23
+ from boulderopalscaleupsdk.device.defcal import DefCalData
24
+ from boulderopalscaleupsdk.device.processor import SuperconductingProcessor
25
+
26
+
27
+ @dataclass
28
+ class EmptyDefCalData:
29
+ message: str
30
+
31
+
32
+ DeviceName = str
33
+
34
+
35
+ @dataclass
36
+ class DeviceData:
37
+ # TODO: retire DeviceInfo the next SDK release
38
+ qpu: SuperconductingProcessor # | OtherSDKProcessorType
39
+ controller_info: QBLOXControllerInfo | QuantumMachinesControllerInfo
40
+ _defcals: dict[tuple[str, tuple[str, ...]], DefCalData]
41
+
42
+ def get_defcal(self, gate: str, addr: tuple[str, ...]) -> DefCalData | EmptyDefCalData:
43
+ """
44
+ Get the defcal data for a specific gate and address alias.
45
+ """
46
+ if self._defcals == {}:
47
+ return EmptyDefCalData(message="No defcal data available in a fresh device.")
48
+ _addr = tuple(i.lower() for i in sorted(addr))
49
+ defcal = self._defcals.get((gate, _addr))
50
+ if defcal is None:
51
+ return EmptyDefCalData(
52
+ message=f"No defcal data found for gate '{gate}' and address '{_addr}'.",
53
+ )
54
+ return defcal
55
+
56
+
57
+ class DeviceSummary(BaseModel):
58
+ id: str
59
+ organization_id: str
60
+ name: str
61
+ provider: str
62
+ updated_at: ISO8601DatetimeUTCLike
63
+ created_at: ISO8601DatetimeUTCLike
64
+ copied_from: DeviceName | None = None
65
+
66
+ def __str__(self):
67
+ return f'DeviceSummary(name="{self.name}", id="{self.id}")'
@@ -14,7 +14,6 @@
14
14
  __all__ = [
15
15
  "CalibrationThresholds",
16
16
  "ComponentParameter",
17
- "DurationComponentParameter",
18
17
  "FloatComponentParameter",
19
18
  "SuperconductingProcessor",
20
19
  "SuperconductingProcessorTemplate",
@@ -24,7 +23,6 @@ __all__ = [
24
23
  from .common import (
25
24
  CalibrationThresholds,
26
25
  ComponentParameter,
27
- DurationComponentParameter,
28
26
  FloatComponentParameter,
29
27
  update_parameter,
30
28
  )
@@ -200,7 +200,7 @@ class ComponentParameter(Generic[T]):
200
200
  ):
201
201
  self.calibration_status = _get_calibration_status_from_thresholds(
202
202
  value=to_float(self.value),
203
- confidence_interval=to_float(self.err_plus) - to_float(self.err_minus),
203
+ confidence_interval=to_float(self.err_plus) + to_float(self.err_minus),
204
204
  calibration_thresholds=calibration_thresholds,
205
205
  )
206
206
  self.updated_at = datetime.now(UTC)
@@ -272,8 +272,3 @@ FloatComponentParameter = Annotated[
272
272
  ComponentParameter[float],
273
273
  BeforeValidator(lambda value: ComponentParameter.from_value(value, float)),
274
274
  ]
275
-
276
- DurationComponentParameter = Annotated[
277
- ComponentParameter[Duration],
278
- BeforeValidator(lambda value: ComponentParameter.from_value(value, Duration)),
279
- ]