boulder-opal-scale-up-sdk 1.0.4__tar.gz → 1.0.6__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 (83) hide show
  1. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/PKG-INFO +1 -1
  2. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/agent/worker.py +36 -4
  3. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/common/dtypes.py +41 -2
  4. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/constants.py +15 -0
  5. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/controller/qblox.py +94 -26
  6. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/controller/quantum_machines.py +86 -17
  7. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/device.py +5 -1
  8. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/processor/common.py +3 -3
  9. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/processor/superconducting_processor.py +23 -3
  10. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/errors.py +21 -0
  11. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/__init__.py +16 -2
  12. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/chi01_scan.py +9 -7
  13. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/cz_spectroscopy_by_bias.py +84 -0
  14. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/drag_leakage_calibration.py +66 -0
  15. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/fine_amplitude_calibration.py +54 -0
  16. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/power_rabi.py +10 -7
  17. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/power_rabi_ef.py +10 -9
  18. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/ramsey.py +9 -7
  19. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/ramsey_ef.py +62 -0
  20. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/experiments/readout_classifier_calibration.py → boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/readout_classifier.py +16 -6
  21. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/readout_optimization.py +57 -0
  22. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +9 -7
  23. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +12 -11
  24. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +10 -9
  25. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/t1.py +8 -6
  26. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/t2.py +12 -10
  27. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/t2_echo.py +12 -10
  28. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +13 -12
  29. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/transmon_spectroscopy.py +10 -8
  30. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/voltage_bias_fine_tune.py +58 -0
  31. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/zz_ramsey.py +59 -0
  32. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/grpc_interceptors/error.py +318 -0
  33. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/plotting/dtypes.py +10 -8
  34. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +9 -3
  35. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +14 -0
  36. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +34 -0
  37. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/protobuf/v1/device_pb2.py +97 -0
  38. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +67 -41
  39. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +100 -66
  40. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/protobuf/v1/job_pb2.py +47 -0
  41. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/protobuf/v1/job_pb2.pyi +54 -0
  42. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/protobuf/v1/job_pb2_grpc.py +138 -0
  43. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/routines/__init__.py +10 -1
  44. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/routines/transmon_retuning.py → boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/routines/one_qubit_calibration.py +10 -5
  45. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/routines/resonator_mapping.py +1 -1
  46. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/routines/transmon_coherence.py +34 -0
  47. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/routines/transmon_discovery.py +5 -9
  48. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/routines/transmon_retuning.py +41 -0
  49. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +1 -1
  50. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/third_party/quantum_machines/config.py +51 -48
  51. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/pyproject.toml +2 -4
  52. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/protobuf/v1/device_pb2.py +0 -89
  53. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/stubs/dtypes.py +0 -47
  54. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/stubs/maps.py +0 -18
  55. boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/utils/__init__.py +0 -12
  56. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/LICENSE +0 -0
  57. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/README.md +0 -0
  58. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/__init__.py +0 -0
  59. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/agent/__init__.py +0 -0
  60. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/common/__init__.py +0 -0
  61. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/common/typeclasses.py +0 -0
  62. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/__init__.py +0 -0
  63. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/common.py +0 -0
  64. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/config_loader.py +0 -0
  65. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/controller/__init__.py +0 -0
  66. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/controller/base.py +0 -0
  67. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/controller/resolver.py +0 -0
  68. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/defcal.py +0 -0
  69. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/device/processor/__init__.py +0 -0
  70. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/common.py +0 -0
  71. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/experiments/waveforms.py +0 -0
  72. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/grpc_interceptors/__init__.py +0 -0
  73. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/grpc_interceptors/auth.py +0 -0
  74. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/plotting/__init__.py +0 -0
  75. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/task_pb2.py +0 -0
  76. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +0 -0
  77. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +0 -0
  78. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/py.typed +0 -0
  79. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/routines/common.py +0 -0
  80. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/third_party/__init__.py +0 -0
  81. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/boulderopalscaleupsdk/third_party/quantum_machines/constants.py +0 -0
  82. {boulder_opal_scale_up_sdk-1.0.4/boulderopalscaleupsdk/stubs → boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/utils}/__init__.py +0 -0
  83. {boulder_opal_scale_up_sdk-1.0.4 → boulder_opal_scale_up_sdk-1.0.6}/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.4
3
+ Version: 1.0.6
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
@@ -17,6 +17,7 @@ Contains logic for agent worker.
17
17
  """
18
18
 
19
19
  import logging
20
+ import os
20
21
  from abc import abstractmethod
21
22
  from typing import Protocol
22
23
 
@@ -28,6 +29,10 @@ from grpc import ssl_channel_credentials
28
29
  from pydantic_settings import BaseSettings, SettingsConfigDict
29
30
 
30
31
  from boulderopalscaleupsdk.common.dtypes import GrpcMetadata, JobId
32
+ from boulderopalscaleupsdk.constants import (
33
+ DEFAULT_GRPC_MAX_RECEIVE_MESSAGE_LENGTH,
34
+ DEFAULT_GRPC_MAX_SEND_MESSAGE_LENGTH,
35
+ )
31
36
  from boulderopalscaleupsdk.protobuf.v1 import agent_pb2, task_pb2, task_pb2_grpc
32
37
 
33
38
  LOG = logging.getLogger(__name__)
@@ -54,11 +59,13 @@ class TaskHandler(Protocol):
54
59
  self,
55
60
  request: agent_pb2.RunProgramRequest
56
61
  | agent_pb2.RunQuantumMachinesMixerCalibrationRequest
57
- | agent_pb2.DisplayResultsRequest,
62
+ | agent_pb2.DisplayResultsRequest
63
+ | agent_pb2.AskRequest,
58
64
  ) -> (
59
65
  agent_pb2.RunProgramResponse
60
66
  | agent_pb2.RunQuantumMachinesMixerCalibrationResponse
61
67
  | agent_pb2.DisplayResultsResponse
68
+ | agent_pb2.AskResponse
62
69
  | task_pb2.TaskErrorDetail
63
70
  ): ...
64
71
 
@@ -67,9 +74,8 @@ class TaskHandler(Protocol):
67
74
  task: task_pb2.Task,
68
75
  ) -> any_pb2.Any | task_pb2.TaskErrorDetail:
69
76
  request = (
70
- _as_run_program_request(
71
- task.data,
72
- )
77
+ _as_run_program_request(task.data)
78
+ or _as_ask_request(task.data)
73
79
  or _as_run_qua_calibration_request(task.data)
74
80
  or _as_display_results_request(task.data)
75
81
  )
@@ -78,6 +84,7 @@ class TaskHandler(Protocol):
78
84
  agent_pb2.RunProgramRequest()
79
85
  | agent_pb2.RunQuantumMachinesMixerCalibrationRequest()
80
86
  | agent_pb2.DisplayResultsRequest()
87
+ | agent_pb2.AskRequest()
81
88
  ):
82
89
  return _as_any_message(await self.handle(request))
83
90
  case None:
@@ -104,6 +111,17 @@ def _as_run_program_request(
104
111
  return request
105
112
 
106
113
 
114
+ def _as_ask_request(
115
+ task_result: any_pb2.Any,
116
+ ) -> agent_pb2.AskRequest | None:
117
+ request = agent_pb2.AskRequest()
118
+ unpacked: bool = task_result.Unpack(request) # type: ignore[reportUnknownMemberType]
119
+ if not unpacked:
120
+ return None
121
+
122
+ return request
123
+
124
+
107
125
  def _as_run_qua_calibration_request(
108
126
  task_result: any_pb2.Any,
109
127
  ) -> agent_pb2.RunQuantumMachinesMixerCalibrationRequest | None:
@@ -158,16 +176,30 @@ class Agent:
158
176
  Create a gRPC channel.
159
177
  """
160
178
  host = url.split(":")[0]
179
+ grpc_max_send_message_length = os.getenv(
180
+ "GRPC_MAX_SEND_MESSAGE_LENGTH",
181
+ DEFAULT_GRPC_MAX_SEND_MESSAGE_LENGTH,
182
+ )
183
+ grpc_max_receive_message_length = os.getenv(
184
+ "GRPC_MAX_RECEIVE_MESSAGE_LENGTH",
185
+ DEFAULT_GRPC_MAX_RECEIVE_MESSAGE_LENGTH,
186
+ )
187
+ options = [
188
+ ("grpc.max_send_message_length", grpc_max_send_message_length),
189
+ ("grpc.max_receive_message_length", grpc_max_receive_message_length),
190
+ ]
161
191
  if host in ["localhost", "127.0.0.1", "0.0.0.0", "::"]:
162
192
  channel = grpc.insecure_channel(
163
193
  url,
164
194
  interceptors=interceptors,
195
+ options=options,
165
196
  )
166
197
  else:
167
198
  channel = grpc.secure_channel(
168
199
  url,
169
200
  credentials=ssl_channel_credentials(),
170
201
  interceptors=interceptors,
202
+ options=options,
171
203
  )
172
204
  return channel
173
205
 
@@ -14,7 +14,9 @@
14
14
  from __future__ import annotations
15
15
 
16
16
  import enum
17
- from typing import overload
17
+ from typing import TypeAlias, overload
18
+
19
+ import grpc
18
20
 
19
21
  __all__ = [
20
22
  "Duration",
@@ -31,6 +33,8 @@ from dateutil.parser import isoparse
31
33
  from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer, TypeAdapter
32
34
  from pydantic.dataclasses import dataclass
33
35
 
36
+ from boulderopalscaleupsdk.plotting.dtypes import Plot # noqa: TC001
37
+
34
38
  GrpcMetadata = list[tuple[str, str | bytes]]
35
39
 
36
40
  JobId = str
@@ -316,6 +320,14 @@ class JobHistorySortOrder(enum.Enum):
316
320
  CREATED_AT_DESC = 1
317
321
  CREATED_AT_ASC = 2
318
322
 
323
+ @staticmethod
324
+ def from_int(value: int) -> JobHistorySortOrder | None:
325
+ match value:
326
+ case 1 | 2:
327
+ return JobHistorySortOrder(value)
328
+ case _:
329
+ return None
330
+
319
331
 
320
332
  class JobSummary(BaseModel):
321
333
  id: str
@@ -325,9 +337,36 @@ class JobSummary(BaseModel):
325
337
  created_at: ISO8601DatetimeUTCLike
326
338
 
327
339
  def __str__(self):
328
- return f"JobSummary(name = {self.name}, id = {self.id})"
340
+ return f'JobSummary(name="{self.name}", id="{self.id}")'
341
+
342
+
343
+ class JobDataEntry(BaseModel):
344
+ message: str
345
+ created_at: ISO8601DatetimeUTCLike
346
+ dt: str
347
+ elapsed_time: str
348
+ plots: list[Plot] = []
349
+
350
+ class Config:
351
+ extra = "allow"
352
+
353
+
354
+ class JobData(BaseModel):
355
+ id: str
356
+ name: str
357
+ session_id: str
358
+ created_at: ISO8601DatetimeUTCLike
359
+ device_name: str
360
+ data: list[JobDataEntry]
329
361
 
330
362
 
331
363
  DEFAULT_JOB_HISTORY_PAGE = 1
332
364
  DEFAULT_JOB_HISTORY_PAGE_SIZE = 10
333
365
  DEFAULT_JOB_HISTORY_SORT_ORDER = JobHistorySortOrder.CREATED_AT_DESC
366
+
367
+ SyncClientInterceptor: TypeAlias = (
368
+ grpc.UnaryUnaryClientInterceptor
369
+ | grpc.UnaryStreamClientInterceptor
370
+ | grpc.StreamUnaryClientInterceptor
371
+ | grpc.StreamStreamClientInterceptor
372
+ )
@@ -0,0 +1,15 @@
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
+ DEFAULT_GRPC_MAX_RECEIVE_MESSAGE_LENGTH = 1024 * 1024 * 50 # 50MB
15
+ DEFAULT_GRPC_MAX_SEND_MESSAGE_LENGTH = 1024 * 1024 * 50 # 50MB
@@ -257,18 +257,18 @@ ChannelType = RealChannel | ComplexChannel
257
257
  # ==================================================================================================
258
258
  # Controller information
259
259
  # ==================================================================================================
260
- class ElementConnection(BaseModel): # pragma: no cover
260
+ class PortConnection(BaseModel): # pragma: no cover
261
261
  """
262
- The connections involved for a control element.
262
+ The connections involved for a QPU port.
263
263
 
264
264
  Attributes
265
265
  ----------
266
266
  ch_out: ChannelType
267
- The output channel that will signal towards the control element.
268
- ch_in: ChannelType, optional
269
- The input channel from which signals will be acquired from the element. This is optional, as
270
- not all modules support acquisitions. If an input channel is specified, it must be located
271
- 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.
272
272
 
273
273
  Notes
274
274
  -----
@@ -276,20 +276,21 @@ class ElementConnection(BaseModel): # pragma: no cover
276
276
  direction is outwards from the control stack. The following diagram depicts a simple setup with
277
277
  the arrows indicating a control channel.
278
278
 
279
- ┌────────┐ ┌────────────┐
280
- │ │─── out ──►│Element: xy1
281
- │ QBLOX │ └────────────┘
282
- │ Stack │ ┌────────────┐
283
- │ │─── out ──►│Element: ro1
284
- │ │◄── in ────│
285
- └────────┘ └────────────┘
279
+ ┌────────┐ ┌───────────────┐
280
+ │ │─── out ──►│ Port: p_xy1
281
+ │ QBLOX │ └───────────────┘
282
+ │ Stack │ ┌───────────────┐
283
+ │ │─── out ──►│ Port: p_flrr0
284
+ │ │◄── in ────│
285
+ └────────┘ └───────────────┘
286
+ QPU fridge
286
287
  """
287
288
 
288
289
  ch_out: ChannelType
289
290
  ch_in: ChannelType | None = None
290
291
 
291
292
  @model_validator(mode="after")
292
- def validate_channels(self) -> "ElementConnection":
293
+ def validate_channels(self) -> "PortConnection":
293
294
  if self.ch_in is not None and self.ch_in.module != self.ch_out.module:
294
295
  raise ValueError("I/O channels for an element must be on the same module.")
295
296
  return self
@@ -309,38 +310,102 @@ class QBLOXControllerInfo(BaseModel): # pragma: no cover
309
310
  The type of controller, which is always `ControllerType.QBLOX` for this class.
310
311
  modules: dict[ModuleAddrType, ModuleType]
311
312
  The modules connected to the QBLOX stack.
312
- elements: dict[str, ElementConnection]
313
- The addressable control elements for the stack.
313
+ port_config: dict[str, PortConnection]
314
+ The dictionary of ports with their types and addresses.
314
315
  """
315
316
 
316
317
  controller_type: Literal[ControllerType.QBLOX] = ControllerType.QBLOX
317
318
  modules: dict[ModuleAddrType, ModuleType]
318
- elements: dict[str, ElementConnection]
319
+ port_config: dict[str, PortConnection]
319
320
 
320
321
 
321
322
  # ==================================================================================================
322
323
  # Instrument management
323
324
  # ==================================================================================================
324
325
  class SequencerParams(BaseModel):
325
- nco_freq: float | None = Field(default=None, gt=0)
326
+ nco_freq: float | None = Field(default=None)
326
327
  gain_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
327
328
  offset_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
328
329
  gain_awg_path1: float | None = Field(default=None, ge=-1.0, le=1.0)
329
330
  offset_awg_path1: float | None = Field(default=None, ge=-1.0, le=1.0)
330
331
  marker_ovr_en: bool | None = Field(default=None)
331
- marker_ovr_value: int | None = Field(default=None)
332
+ marker_ovr_value: int | None = Field(default=None, ge=0, le=15)
332
333
  mod_en_awg: bool | None = Field(default=None)
333
334
  demod_en_acq: bool | None = Field(default=None)
334
335
  sync_en: bool | None = Field(default=None)
335
336
  nco_prop_delay_comp_en: bool | None = Field(default=True)
336
- integration_length_acq: int | None = Field(default=None)
337
+ integration_length_acq: int | None = Field(default=None, ge=4, le=16777212, multiple_of=4)
337
338
 
338
339
 
339
- class ModuleParams(BaseModel):
340
- out0_in0_lo_freq: float | None = Field(default=None, gt=0)
341
- 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
+
342
360
  out0_lo_freq: float | None = Field(default=None, gt=0)
343
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}!")
344
409
 
345
410
 
346
411
  # ==================================================================================================
@@ -414,7 +479,7 @@ class PreparedSequenceProgram(BaseModel): # pragma: no cover
414
479
 
415
480
 
416
481
  class PreparedModule(BaseModel):
417
- params: ModuleParams = ModuleParams()
482
+ params: ModuleParams
418
483
 
419
484
 
420
485
  class PreparedProgram(BaseModel):
@@ -661,7 +726,10 @@ class BitStrideArrayEncoding:
661
726
  encoded_shape = tuple(
662
727
  BitStrideArrayEncoding._round_power2_32bit(dim) for dim in desired_shape
663
728
  )
664
- bit_stride = tuple(int(math.log2(dim)) for dim in encoded_shape)
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)))
665
733
  return cls(
666
734
  desired_shape=desired_shape,
667
735
  encoded_shape=encoded_shape,
@@ -11,10 +11,11 @@
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, Self
14
+ from typing import Annotated, Any, Literal, Self
15
15
 
16
16
  from pydantic import (
17
17
  BaseModel,
18
+ BeforeValidator,
18
19
  ConfigDict,
19
20
  Field,
20
21
  field_serializer,
@@ -31,10 +32,78 @@ from boulderopalscaleupsdk.third_party.quantum_machines.constants import (
31
32
  QUA_CLOCK_CYCLE,
32
33
  )
33
34
 
34
- QPUPortRef = str
35
- OctaveRef = str
36
- ControllerRef = str
37
- ControllerPort = int
35
+
36
+ class OPXPortMapping(BaseModel):
37
+ type: Literal["controller"] = "controller"
38
+ controller_id: str
39
+ port_id: int
40
+
41
+ def to_native_opx_port_type(self) -> qm_config.NativeOPXPortType:
42
+ return qm_config.NativeOPXPortType(
43
+ controller_id=self.controller_id,
44
+ port_id=self.port_id,
45
+ )
46
+
47
+ @staticmethod
48
+ def from_native_opx_port_type(
49
+ port_type: qm_config.NativeOPXPortType,
50
+ ) -> "OPXPortMapping":
51
+ return OPXPortMapping(
52
+ controller_id=port_type.controller_id,
53
+ port_id=port_type.port_id,
54
+ )
55
+
56
+
57
+ def ensure_opx_port_mapping(value: Any) -> Any:
58
+ match value:
59
+ case OPXPortMapping():
60
+ return value
61
+ case list([str(controller_id), float(port_id)]):
62
+ if not port_id.is_integer():
63
+ raise ValueError("port_id must be an integer")
64
+ return OPXPortMapping(controller_id=controller_id, port_id=int(port_id))
65
+ case list([str(controller_id), int(port_id)]):
66
+ return OPXPortMapping(controller_id=controller_id, port_id=port_id)
67
+ case tuple((str(controller_id), float(port_id))):
68
+ if not port_id.is_integer():
69
+ raise ValueError("port_id must be an integer")
70
+ return OPXPortMapping(controller_id=controller_id, port_id=int(port_id))
71
+ case tuple((str(controller_id), int(port_id))):
72
+ return OPXPortMapping(controller_id=controller_id, port_id=port_id)
73
+ case dict({"type": "controller", "controller_id": str(), "port_id": int() | float()}):
74
+ return OPXPortMapping.model_validate(value)
75
+ case _:
76
+ raise ValueError("Invalid Port Mapping")
77
+
78
+
79
+ OPXPortMappingLike = Annotated[
80
+ OPXPortMapping,
81
+ BeforeValidator(ensure_opx_port_mapping),
82
+ ]
83
+
84
+
85
+ class OPX1000PortMapping(BaseModel):
86
+ type: Literal["frontend_module"] = "frontend_module"
87
+ controller_id: str
88
+ fem_id: int
89
+ port_id: int
90
+
91
+ def to_native_opx1000_port_type(self) -> qm_config.NativeOPX1000PortType:
92
+ return qm_config.NativeOPX1000PortType(
93
+ controller_id=self.controller_id,
94
+ fem_id=self.fem_id,
95
+ port_id=self.port_id,
96
+ )
97
+
98
+ @staticmethod
99
+ def from_native_opx1000_port_type(
100
+ port_type: qm_config.NativeOPX1000PortType,
101
+ ) -> "OPX1000PortMapping":
102
+ return OPX1000PortMapping(
103
+ controller_id=port_type.controller_id,
104
+ fem_id=port_type.fem_id,
105
+ port_id=port_type.port_id,
106
+ )
38
107
 
39
108
 
40
109
  class QuaProgram(BaseModel):
@@ -74,17 +143,17 @@ class OctaveConfig(qm_config.OctaveConfig121):
74
143
 
75
144
  class DrivePortConfig(BaseModel):
76
145
  port_type: Literal["drive"] = "drive"
77
- port_mapping: tuple[OctaveRef, ControllerPort]
146
+ port_mapping: OPXPortMappingLike | OPX1000PortMapping
78
147
 
79
148
 
80
149
  class FluxPortConfig(BaseModel):
81
150
  port_type: Literal["flux"] = "flux"
82
- port_mapping: tuple[ControllerRef, ControllerPort]
151
+ port_mapping: OPXPortMappingLike | OPX1000PortMapping
83
152
 
84
153
 
85
154
  class ReadoutPortConfig(BaseModel):
86
155
  port_type: Literal["readout"] = "readout"
87
- port_mapping: tuple[OctaveRef, ControllerPort]
156
+ port_mapping: OPXPortMappingLike | OPX1000PortMapping
88
157
  time_of_flight: DurationNsLike
89
158
  smearing: DurationNsLike = Field(default=Duration(0, TimeUnit.NS))
90
159
 
@@ -119,22 +188,22 @@ class QuantumMachinesControllerInfo(BaseModel):
119
188
  ----------
120
189
  controller_type : Literal[ControllerType.QUANTUM_MACHINES]
121
190
  The type of controller, which is always `ControllerType.QUANTUM_MACHINES`.
122
- controllers : dict[ControllerRef, OPXControllerConfig | OPX1000ControllerConfig]
123
- A dictionary mapping controller references to their respective configurations.
191
+ controllers : dict[str, OPXControllerConfig | OPX1000ControllerConfig]
192
+ A dictionary mapping controller references (str) to their respective configurations.
124
193
  The configurations can be either OPXControllerConfig or OPX1000ControllerConfig.
125
194
  Derived from OPX Config.
126
- octaves : dict[OctaveRef, OctaveConfig]
127
- A dictionary mapping octave references to their respective configurations.
195
+ octaves : dict[str, OctaveConfig]
196
+ A dictionary mapping octave references (str) to their respective configurations.
128
197
  Derived from OPX Config.
129
- port_config : dict[PortRef, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
130
- A dictionary mapping port references to their respective port configurations.
198
+ port_config : dict[str, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
199
+ A dictionary mapping port references (str) to their respective port configurations.
131
200
  The configurations can be DrivePortConfig, FluxPortConfig, or ReadoutPortConfig.
132
201
  Not derived from OPX Config, this is our custom config.
133
202
  """
134
203
 
135
204
  controller_type: Literal[ControllerType.QUANTUM_MACHINES] = ControllerType.QUANTUM_MACHINES
136
- controllers: dict[ControllerRef, OPXControllerConfig | OPX1000ControllerConfig] = Field(
205
+ controllers: dict[str, OPXControllerConfig | OPX1000ControllerConfig] = Field(
137
206
  default={},
138
207
  )
139
- octaves: dict[OctaveRef, OctaveConfig] = Field(default={})
140
- port_config: dict[QPUPortRef, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
208
+ octaves: dict[str, OctaveConfig] = Field(default={})
209
+ port_config: dict[str, DrivePortConfig | FluxPortConfig | ReadoutPortConfig]
@@ -29,6 +29,9 @@ class EmptyDefCalData:
29
29
  message: str
30
30
 
31
31
 
32
+ DeviceName = str
33
+
34
+
32
35
  @dataclass
33
36
  class DeviceData:
34
37
  # TODO: retire DeviceInfo the next SDK release
@@ -58,6 +61,7 @@ class DeviceSummary(BaseModel):
58
61
  provider: str
59
62
  updated_at: ISO8601DatetimeUTCLike
60
63
  created_at: ISO8601DatetimeUTCLike
64
+ copied_from: DeviceName | None = None
61
65
 
62
66
  def __str__(self):
63
- return f"DeviceSummary(name = {self.name}, id = {self.id})"
67
+ return f'DeviceSummary(name="{self.name}", id="{self.id}")'
@@ -191,8 +191,8 @@ class ComponentParameter(Generic[T]):
191
191
  self.err_minus = None
192
192
  self.err_plus = None
193
193
  else:
194
- self.err_minus = from_float(to_float(std) / 2.0)
195
- self.err_plus = from_float(to_float(std) / 2.0)
194
+ self.err_minus = from_float(to_float(std))
195
+ self.err_plus = from_float(to_float(std))
196
196
  if (
197
197
  not isinstance(calibration_thresholds, EllipsisType)
198
198
  and self.err_minus is not None
@@ -259,7 +259,7 @@ def _get_calibration_status_from_thresholds(
259
259
  confidence_interval: float,
260
260
  calibration_thresholds: CalibrationThresholds,
261
261
  ) -> CalibrationStatusT:
262
- relative_uncertainty = abs(confidence_interval / value)
262
+ relative_uncertainty = 0.5 * abs(confidence_interval / value)
263
263
 
264
264
  if relative_uncertainty < calibration_thresholds.good:
265
265
  return "good"
@@ -161,7 +161,27 @@ class Feedline(Component[Literal["feedline"]]):
161
161
  traits: list = Field(default=[])
162
162
 
163
163
 
164
- SuperconductingComponentType = Transmon | Resonator | Port | Feedline | Filter | Coupler
164
+ class TWPA(Component[Literal["twpa"]]):
165
+ dtype: Literal["twpa"] = "twpa"
166
+ traits: list = Field(default=[])
167
+
168
+ impedance: FloatComponentParameter = Field(
169
+ default=ComponentParameter(value=0.0),
170
+ json_schema_extra={"display": {"label": "impedance", "unit": "Ohm", "scale": 1}},
171
+ )
172
+
173
+ # Tunable TWPA parameters
174
+ pump_power: FloatComponentParameter = Field(
175
+ default=ComponentParameter(value=0.0),
176
+ json_schema_extra={"display": {"label": "pump_power", "unit": "dBm", "scale": 1}},
177
+ )
178
+ pump_freq: FloatComponentParameter = Field(
179
+ default=ComponentParameter(value=(0.0)),
180
+ json_schema_extra={"display": {"label": "pump_freq", "unit": "MHz", "scale": 1e-6}},
181
+ )
182
+
183
+
184
+ SuperconductingComponentType = Transmon | Resonator | Port | Feedline | Filter | Coupler | TWPA
165
185
 
166
186
 
167
187
  class Edge(BaseModel):
@@ -176,14 +196,14 @@ class TemplateParam(BaseModel):
176
196
 
177
197
 
178
198
  class ProcessorTemplate(BaseModel):
179
- elements: dict[str, Transmon | Resonator | Port | Feedline | Filter | Coupler]
199
+ elements: dict[str, SuperconductingComponentType]
180
200
  edges: list[Edge]
181
201
 
182
202
 
183
203
  class SuperconductingProcessorTemplate(BaseModel):
184
204
  build: list[TemplateParam]
185
205
  templates: dict[str, ProcessorTemplate]
186
- device_parameters: dict[str, Transmon | Resonator | Port | Feedline | Filter | Coupler] = Field(
206
+ device_parameters: dict[str, SuperconductingComponentType] = Field(
187
207
  default={},
188
208
  )
189
209
 
@@ -0,0 +1,21 @@
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
+ class ScaleUpServerError(Exception):
16
+ """
17
+ Exception raised by client based on server behavior.
18
+ """
19
+
20
+ def __init__(self, message: str):
21
+ super().__init__(message)