boulder-opal-scale-up-sdk 1.0.0__py3-none-any.whl → 1.0.2__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.2.dist-info/LICENSE +805 -0
  2. {boulder_opal_scale_up_sdk-1.0.0.dist-info → boulder_opal_scale_up_sdk-1.0.2.dist-info}/METADATA +17 -7
  3. boulder_opal_scale_up_sdk-1.0.2.dist-info/RECORD +60 -0
  4. boulderopalscaleupsdk/agent/worker.py +22 -11
  5. boulderopalscaleupsdk/common/dtypes.py +81 -114
  6. boulderopalscaleupsdk/device/__init__.py +5 -1
  7. boulderopalscaleupsdk/device/config_loader.py +15 -10
  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 +62 -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 +61 -15
  18. boulderopalscaleupsdk/experiments/__init__.py +8 -0
  19. boulderopalscaleupsdk/experiments/chi01_scan.py +56 -0
  20. boulderopalscaleupsdk/experiments/common.py +8 -4
  21. boulderopalscaleupsdk/experiments/power_rabi.py +3 -3
  22. boulderopalscaleupsdk/experiments/ramsey.py +15 -8
  23. boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +24 -0
  24. boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +3 -3
  25. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +10 -10
  26. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +3 -3
  27. boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +68 -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 +43 -25
  36. boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +50 -8
  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.2.dist-info}/WHEEL +0 -0
@@ -1,24 +1,23 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boulder-opal-scale-up-sdk
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Q-CTRL Boulder Opal Scale Up Python SDK
5
- License: Apache-2.0
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
7
7
  Author: Q-CTRL
8
8
  Author-email: support@q-ctrl.com
9
9
  Maintainer: Q-CTRL
10
10
  Maintainer-email: support@q-ctrl.com
11
- Requires-Python: >=3.10,<3.13
11
+ Requires-Python: >=3.11,<3.13
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: Education
16
16
  Classifier: Intended Audience :: Science/Research
17
- Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: License :: Other/Proprietary License
18
18
  Classifier: Natural Language :: English
19
19
  Classifier: Operating System :: OS Independent
20
20
  Classifier: Programming Language :: Python :: 3
21
- Classifier: Programming Language :: Python :: 3.10
22
21
  Classifier: Programming Language :: Python :: 3.11
23
22
  Classifier: Programming Language :: Python :: 3.12
24
23
  Classifier: Topic :: Internet :: WWW/HTTP
@@ -26,13 +25,24 @@ Classifier: Topic :: Scientific/Engineering :: Physics
26
25
  Classifier: Topic :: Scientific/Engineering :: Visualization
27
26
  Classifier: Topic :: Software Development :: Embedded Systems
28
27
  Classifier: Topic :: System :: Distributed Computing
29
- Requires-Dist: async-timeout (>=4.0.3,<5.0.0) ; python_version < "3.11"
28
+ Provides-Extra: quantum-machines
30
29
  Requires-Dist: attrs (>=25.1.0,<26.0.0)
31
30
  Requires-Dist: googleapis-common-protos (>=1.69.2,<2.0.0)
32
- Requires-Dist: qctrl-client (>=10.1.0,<11.0.0)
31
+ Requires-Dist: numpy (>=1.26.4,<2.0.0)
32
+ Requires-Dist: pydantic (>=2.10.4,<3.0.0)
33
+ Requires-Dist: pydantic-settings (>=2.7.0,<3.0.0)
34
+ Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
35
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
36
+ Requires-Dist: qm-qua (==1.2.1) ; extra == "quantum-machines"
33
37
  Project-URL: Facebook, https://www.facebook.com/qctrl
34
38
  Project-URL: GitHub, https://github.com/qctrl
35
39
  Project-URL: Homepage, https://q-ctrl.com
36
40
  Project-URL: LinkedIn, https://www.linkedin.com/company/q-ctrl/
37
41
  Project-URL: X, https://x.com/qctrlHQ
38
42
  Project-URL: YouTube, https://www.youtube.com/qctrl
43
+ Description-Content-Type: text/markdown
44
+
45
+ # Boulder Opal Scale Up SDK
46
+
47
+ The Boulder Opal Scale Up SDK package is a Python SDK for Q-CTRL Boulder Opal Scale Up. Scale Up provides a tailored solution to characterize and calibrate quantum hardware.
48
+
@@ -0,0 +1,60 @@
1
+ boulderopalscaleupsdk/__init__.py,sha256=nD3YDqPiE52mmuUrIlDUrYSyljpMsDJvc5HsubBUSs4,592
2
+ boulderopalscaleupsdk/agent/__init__.py,sha256=aFkAtHJDOdXA126JklxYz0ix1k4lCcLLS9DQp8zUKMk,1092
3
+ boulderopalscaleupsdk/agent/worker.py,sha256=XlbjvAjQgKMpRbbI04jzVY9NzqW5nO_D5wFq7fJC-qM,7937
4
+ boulderopalscaleupsdk/common/__init__.py,sha256=vLaJ1FP36xV5eX6VD7nShqxVBm3UzsBMIK1pmSiTIag,550
5
+ boulderopalscaleupsdk/common/dtypes.py,sha256=1Lf-kulA0vxFMCW1E5nfHifoCTOtq76fDwQTSco_qCQ,9601
6
+ boulderopalscaleupsdk/common/typeclasses.py,sha256=FZHyTcuCQH5niqFPZmIyoO2zN6mA661h8kyvDcYFryg,3033
7
+ boulderopalscaleupsdk/device/__init__.py,sha256=Bbo9KYQqNZloj2H9sl2PdfFvYMDS78q1Mu0Q0q5Ze5o,727
8
+ boulderopalscaleupsdk/device/common.py,sha256=ZLXpmORKQYntX7DeKZ8pd49Ju1jzidR2q2EeN3TNwTI,1909
9
+ boulderopalscaleupsdk/device/config_loader.py,sha256=RxCD77Z52lVaMYcimdyxoz9V3hb1Q5ta4xk1eU7wwxM,3367
10
+ boulderopalscaleupsdk/device/controller/__init__.py,sha256=ePkrCbX1ORm05fS0BEQ8cx7DPxGGN6IkUoNH6eQL1DM,1136
11
+ boulderopalscaleupsdk/device/controller/base.py,sha256=xuT4VzRulknxppiQpzYYn_TwuRNfV9wHMqjN2okA8Lw,793
12
+ boulderopalscaleupsdk/device/controller/qblox.py,sha256=c-wG9jAZHy6hCTO7ynKvwJbW1MPNJJh9ryfedthuuz8,22543
13
+ boulderopalscaleupsdk/device/controller/quantum_machines.py,sha256=m0_zO7Cn-Ea1-ewG7qgjEnt4GXtQMvBv_oz-dnOmI9k,5320
14
+ boulderopalscaleupsdk/device/controller/resolver.py,sha256=hW5U58dmJf_IGmD4EK_bbn0XU-N9autFBFHmqbRnY2Y,3812
15
+ boulderopalscaleupsdk/device/defcal.py,sha256=CL9T-oy94st20NyQZWW8mHJU4Nlx2DKZ-o5x1mf6L8c,1912
16
+ boulderopalscaleupsdk/device/device.py,sha256=e3jW6dTFmGAxniPLTSTRVQs5HUkSUvrqolOlv2xJKUw,1243
17
+ boulderopalscaleupsdk/device/processor/__init__.py,sha256=O9d4tMcii3OOGez845qXp06D_AxYMoHr-7ICEStlNro,1038
18
+ boulderopalscaleupsdk/device/processor/common.py,sha256=kRR5nufc8PoTDLxXbSeuprlCgBDrPHSdPom_go9fDOs,9136
19
+ boulderopalscaleupsdk/device/processor/superconducting_processor.py,sha256=c6Au_j5db611xOJF0zy6bpOanhm-7QYAMWerfKMwjzQ,12269
20
+ boulderopalscaleupsdk/experiments/__init__.py,sha256=kskcl-oMR8tRMo8_liSqmty82mtbl3WzB-IY5g88ng4,1628
21
+ boulderopalscaleupsdk/experiments/chi01_scan.py,sha256=qPFa6R1IJmctRId8K-DPejX5P0LXqXUmC-NO6-xmCQU,1862
22
+ boulderopalscaleupsdk/experiments/common.py,sha256=sT06O3I71iEY5AmXGRZfx-V6RQlBEq5B7hCRxnG64tM,2482
23
+ boulderopalscaleupsdk/experiments/power_rabi.py,sha256=_keqv3tW10d-1vnUPTsky9FvLnjFcKF3pcnYfOyNvaI,2059
24
+ boulderopalscaleupsdk/experiments/ramsey.py,sha256=R3pptmkqwHYhhlAyRbYBaSscuiB3RffodC23kKuqwFY,2050
25
+ boulderopalscaleupsdk/experiments/readout_classifier_calibration.py,sha256=mUgKgKiXTv4owykR6nYj-DYZvplR5Xr9Akj36b7WGA4,732
26
+ boulderopalscaleupsdk/experiments/resonator_spectroscopy.py,sha256=EmmLbDq0d-67F1D6biLlUd1-UYP4uiICSkrIeez2T1I,2191
27
+ boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py,sha256=ypb3ZX-OCzQGy3vSlpRct5MGZmvMMZk7pi8dD9EU5VU,2765
28
+ boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py,sha256=N1ibW6ZWSrsRs6-es0bBGkRSc-1WeqU4pwpKRSpZBhE,2225
29
+ boulderopalscaleupsdk/experiments/transmon_anharmonicity.py,sha256=5nb_wTHJ_rhNGAR7THAL2YNvBZKPFJmMwjamPKgP8ko,2706
30
+ boulderopalscaleupsdk/experiments/transmon_spectroscopy.py,sha256=Ib1OdJwCE7m0Q3N_l0Mx8uKBO6GjjM56BDroLDVCBRI,2324
31
+ boulderopalscaleupsdk/grpc_interceptors/__init__.py,sha256=PkdOlllLbRWycofvGCCxDY60Ydp_U0QM5l4u3Fsnno4,616
32
+ boulderopalscaleupsdk/grpc_interceptors/auth.py,sha256=PSe_b9ckqhcb3xf1Y4jpn1TjfEWfokkarqfUHIz6HIs,4165
33
+ boulderopalscaleupsdk/plotting/__init__.py,sha256=pkdCky3YHqc3PLRy_h9puJKAO4AqSJGebb26x7XayR4,959
34
+ boulderopalscaleupsdk/plotting/dtypes.py,sha256=PlVzGpoZfrqVULPJfpRQq3JmgjUgVH7ZPdPkIys_AnE,4737
35
+ boulderopalscaleupsdk/protobuf/v1/agent_pb2.py,sha256=yExXTLC3g_TnHJWkfYsF_kctsftxONyZOBtOFDTTuec,5364
36
+ boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi,sha256=YJdOKaOtI2PGk7ct_LuC8_6NeKsLCXMriVEt4-0TWYM,2536
37
+ boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py,sha256=UH8u9ZmvMjteAYKtTmA-IWSGZmhCQLHdg0_OuKCaYew,6875
38
+ boulderopalscaleupsdk/protobuf/v1/device_pb2.py,sha256=lu-FLsrwT_xnG0411zN2lE1yvtpIiMF22M8hvCpP4Zk,11429
39
+ boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi,sha256=NCrnlIXEsTKZaXjgEdy1cXxgOu4Xli0qbUm4_mOXqf8,6527
40
+ boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py,sha256=0CtrO8gKkcfHn7flQLFAySyOVpdDgzct_EaMMF73rUY,19445
41
+ boulderopalscaleupsdk/protobuf/v1/task_pb2.py,sha256=7NWFFBEPij8B9dvexRia3B_T9-oOCDwayASCglvBtO4,7086
42
+ boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi,sha256=QWciGKFD1AcaAhqgym0ZwNtIlNT85HhOTBdivEC6QJE,5952
43
+ boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py,sha256=yP2FZ148RmSYFHivatCXuM13oRBDA5wBwmvhZQNBTXQ,5826
44
+ boulderopalscaleupsdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ boulderopalscaleupsdk/routines/__init__.py,sha256=jSrTzE_d-4z-wA8NK9akzVtUJJ_CG026qs8orMuWKfk,143
46
+ boulderopalscaleupsdk/routines/common.py,sha256=Lav4eH4GJyRJXBZwmhA3b34UTgck5x4URex3SBSctwM,224
47
+ boulderopalscaleupsdk/routines/resonator_mapping.py,sha256=A7Z5jGicezYMxvC4yeRrLu-MbYHfMaZ6h2AuMwBPrBo,475
48
+ boulderopalscaleupsdk/stubs/__init__.py,sha256=vLaJ1FP36xV5eX6VD7nShqxVBm3UzsBMIK1pmSiTIag,550
49
+ boulderopalscaleupsdk/stubs/dtypes.py,sha256=VH2QqmmaSegv03KEaZBqB-REHhvk5h3GPfQgCJTAYQE,1361
50
+ boulderopalscaleupsdk/stubs/maps.py,sha256=fgEwVyaJ1i5io_NCAABJb7Cyyl_zgdjb8xot75ujw8U,575
51
+ boulderopalscaleupsdk/third_party/__init__.py,sha256=b9T2IVfz9bMenSsLQuaf4A14ySinjNnSGx68hLsFHZE,596
52
+ boulderopalscaleupsdk/third_party/quantum_machines/__init__.py,sha256=ORD6crMw6gtoIp8dmusdwSiU4pje739fOacOfHXSOls,2541
53
+ boulderopalscaleupsdk/third_party/quantum_machines/config.py,sha256=tQsWx_nSXSFT4fRuMzuvliplyRsB9Sy70kHqsKuTA-A,20756
54
+ boulderopalscaleupsdk/third_party/quantum_machines/constants.py,sha256=5PpAi6MZ53yEinBHerY2ssi30BR37JnLBWL21irpZ8Y,870
55
+ boulderopalscaleupsdk/utils/__init__.py,sha256=vLaJ1FP36xV5eX6VD7nShqxVBm3UzsBMIK1pmSiTIag,550
56
+ boulderopalscaleupsdk/utils/serial_utils.py,sha256=SzvY-WFD0b8IGSh6PTv7F9y1W2IPPYmj8pY3AsX-xlM,2062
57
+ boulder_opal_scale_up_sdk-1.0.2.dist-info/LICENSE,sha256=wqX4S5Brcwkwo750l9grSspwm0cyMsZtZEa1Vx3_WiE,36587
58
+ boulder_opal_scale_up_sdk-1.0.2.dist-info/METADATA,sha256=SPWPGe2xUsza1tnuUBCgVzXHcBrJCxCVbz0Zrr5HlII,2419
59
+ boulder_opal_scale_up_sdk-1.0.2.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
60
+ boulder_opal_scale_up_sdk-1.0.2.dist-info/RECORD,,
@@ -27,6 +27,7 @@ from grpc import aio as grpc
27
27
  from grpc import ssl_channel_credentials
28
28
  from pydantic_settings import BaseSettings, SettingsConfigDict
29
29
 
30
+ from boulderopalscaleupsdk.common.dtypes import GrpcMetadata
30
31
  from boulderopalscaleupsdk.protobuf.v1 import agent_pb2, task_pb2, task_pb2_grpc
31
32
 
32
33
  LOG = logging.getLogger(__name__)
@@ -51,11 +52,11 @@ class TaskHandler(Protocol):
51
52
  @abstractmethod
52
53
  async def handle(
53
54
  self,
54
- request: agent_pb2.RunQuaProgramRequest
55
+ request: agent_pb2.RunProgramRequest
55
56
  | agent_pb2.RunQuantumMachinesMixerCalibrationRequest
56
57
  | agent_pb2.DisplayResultsRequest,
57
58
  ) -> (
58
- agent_pb2.RunQuaProgramResponse
59
+ agent_pb2.RunProgramResponse
59
60
  | agent_pb2.RunQuantumMachinesMixerCalibrationResponse
60
61
  | agent_pb2.DisplayResultsResponse
61
62
  | task_pb2.TaskErrorDetail
@@ -66,7 +67,7 @@ class TaskHandler(Protocol):
66
67
  task: task_pb2.Task,
67
68
  ) -> any_pb2.Any | task_pb2.TaskErrorDetail:
68
69
  request = (
69
- _as_run_qua_program_request(
70
+ _as_run_program_request(
70
71
  task.data,
71
72
  )
72
73
  or _as_run_qua_calibration_request(task.data)
@@ -74,7 +75,7 @@ class TaskHandler(Protocol):
74
75
  )
75
76
  match request:
76
77
  case (
77
- agent_pb2.RunQuaProgramRequest()
78
+ agent_pb2.RunProgramRequest()
78
79
  | agent_pb2.RunQuantumMachinesMixerCalibrationRequest()
79
80
  | agent_pb2.DisplayResultsRequest()
80
81
  ):
@@ -92,10 +93,10 @@ def _as_any_message(message: Message) -> any_pb2.Any:
92
93
  return msg
93
94
 
94
95
 
95
- def _as_run_qua_program_request(
96
+ def _as_run_program_request(
96
97
  task_result: any_pb2.Any,
97
- ) -> agent_pb2.RunQuaProgramRequest | None:
98
- request = agent_pb2.RunQuaProgramRequest()
98
+ ) -> agent_pb2.RunProgramRequest | None:
99
+ request = agent_pb2.RunProgramRequest()
99
100
  unpacked: bool = task_result.Unpack(request) # type: ignore[reportUnknownMemberType]
100
101
  if not unpacked:
101
102
  return None
@@ -158,7 +159,10 @@ class Agent:
158
159
  """
159
160
  host = url.split(":")[0]
160
161
  if host in ["localhost", "127.0.0.1", "0.0.0.0", "::"]:
161
- channel = grpc.insecure_channel(url)
162
+ channel = grpc.insecure_channel(
163
+ url,
164
+ interceptors=interceptors,
165
+ )
162
166
  else:
163
167
  channel = grpc.secure_channel(
164
168
  url,
@@ -170,12 +174,13 @@ class Agent:
170
174
  async def start_session(
171
175
  self,
172
176
  app: str,
177
+ metadata: GrpcMetadata,
173
178
  device_name: str,
174
179
  routine: str,
175
180
  data: Struct | None,
176
181
  ) -> str:
177
182
  if not self._channel:
178
- raise RuntimeError("Agent is shutdown.")
183
+ raise RuntimeError("Cannot start session: agent is shutdown.")
179
184
  _data = any_pb2.Any()
180
185
  if data:
181
186
  _data.Pack(data)
@@ -188,13 +193,18 @@ class Agent:
188
193
  routine_name=routine,
189
194
  data=_data,
190
195
  ),
196
+ metadata=metadata,
191
197
  )
192
198
  self._state = response.target_state
193
- await self._resume(response)
199
+ await self._resume(response, metadata)
194
200
  await self.shutdown()
195
201
  return response.session_id
196
202
 
197
- async def _resume(self, response: task_pb2.AgentTasksResponse) -> None:
203
+ async def _resume(
204
+ self,
205
+ response: task_pb2.AgentTasksResponse,
206
+ metadata: GrpcMetadata,
207
+ ) -> None:
198
208
  tasks = response.tasks
199
209
  while self._state not in [
200
210
  task_pb2.AGENT_STATE_SHUTDOWN_MANAGER_INITIATED,
@@ -228,6 +238,7 @@ class Agent:
228
238
  results=task_results,
229
239
  task_in_progress=[],
230
240
  ),
241
+ metadata=metadata,
231
242
  )
232
243
 
233
244
  tasks = _resp.tasks
@@ -13,6 +13,7 @@
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
+ import enum
16
17
  from typing import overload
17
18
 
18
19
  __all__ = [
@@ -21,38 +22,32 @@ __all__ = [
21
22
  "TimeUnit",
22
23
  ]
23
24
 
24
- import sys
25
- from dataclasses import dataclass, field
26
25
  from datetime import datetime, timedelta
27
26
  from decimal import Decimal
28
- from enum import Enum
29
- from typing import Annotated, Any, Literal
27
+ from typing import Annotated, Any, Literal, Self
30
28
 
31
29
  import numpy as np
32
30
  from dateutil.parser import isoparse
33
- from pydantic import BeforeValidator, PlainSerializer, TypeAdapter
34
- from pydantic.dataclasses import dataclass as pydantic_dataclass
31
+ from pydantic import BeforeValidator, ConfigDict, Field, PlainSerializer, TypeAdapter
32
+ from pydantic.dataclasses import dataclass
35
33
 
36
- if sys.version_info >= (3, 11):
37
- from typing import Self
38
- else:
39
- from typing_extensions import Self
34
+ GrpcMetadata = list[tuple[str, str | bytes]]
40
35
 
41
36
 
42
37
  class BaseType: ...
43
38
 
44
39
 
45
- class FrequencyUnit(str, Enum):
40
+ class FrequencyUnit(str, enum.Enum):
46
41
  Hz = "Hz"
47
42
 
48
43
 
49
- @pydantic_dataclass
44
+ @dataclass
50
45
  class Frequency:
51
46
  value: float
52
47
  unit: FrequencyUnit # No default to guarantee clarity of units
53
48
 
54
49
  @classmethod
55
- def from_float_hz(cls, value: float) -> Frequency:
50
+ def from_float_hz(cls, value: float) -> Self:
56
51
  return cls(value, FrequencyUnit.Hz)
57
52
 
58
53
  def to_int_hz(self) -> int:
@@ -120,92 +115,30 @@ class Frequency:
120
115
  return self.__mul__(lhs)
121
116
 
122
117
 
123
- class TimeUnit(str, Enum):
118
+ class TimeUnit(str, enum.Enum):
124
119
  S = "s"
125
120
  MS = "ms"
126
121
  US = "us"
127
122
  NS = "ns"
128
- DT = "dt"
129
123
 
130
124
 
131
- _SI_TIME = Literal[TimeUnit.S, TimeUnit.MS, TimeUnit.US, TimeUnit.NS]
125
+ @dataclass
126
+ class InvalidDurationDiv:
127
+ message: str
128
+ data: Duration
132
129
 
133
130
 
134
- @pydantic_dataclass(order=True)
135
- class Duration(BaseType):
136
- """
137
- A wrapper of _SiDuration and _DtDuration to manage the conversion.
138
- """
131
+ @dataclass
132
+ class InvalidDurationConversion:
133
+ message: str
139
134
 
140
- value: int = field(compare=False)
141
- unit: TimeUnit = field(compare=False)
142
- dtype: Literal["duration"] = "duration"
143
- _value: _SiDuration | _DtDuration = field(init=False, repr=False)
144
-
145
- def __post_init__(self):
146
- self._value = (
147
- _DtDuration(self.value)
148
- if self.unit == TimeUnit.DT
149
- else _SiDuration(self.value, self.unit)
150
- )
151
-
152
- def is_si(self) -> bool:
153
- return self.unit != TimeUnit.DT
154
-
155
- @staticmethod
156
- def from_si(d: Duration, name: str) -> Duration:
157
- if d.unit == TimeUnit.DT:
158
- raise TypeError(f"{name} must use SI time unit.")
159
- return d
160
-
161
- @staticmethod
162
- def from_intlike(val: float, unit: TimeUnit) -> Duration:
163
- if not np.double(val).is_integer():
164
- raise ValueError("fail to create a Duration object. value must be an integer.")
165
- return Duration(int(val), unit)
166
-
167
- def convert(self, target: Duration | _SI_TIME) -> Duration:
168
- """
169
- In particular, we only allow the following conversions:
170
-
171
- # ((1000, "ms"), "s") -> (1, "s")
172
- (_SiDuration, _SI_TIME) -> _SiDuration
173
135
 
174
- # ((4, "ns"), (2, "ns")) -> (2, "dt")
175
- (_SiDuration, _SiDuration) -> _DtDuration
176
-
177
- # ((2, "dt"), (2, "ns")) -> (4, "ns")
178
- (_DtDuration, _SiDuration) _> _SiDuration
179
- """
180
- match self._value, getattr(target, "_value", target):
181
- case _SiDuration(
182
- _,
183
- _,
184
- ), TimeUnit.S | TimeUnit.MS | TimeUnit.US | TimeUnit.NS:
185
- converted = self._value.convert_to_si(target) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
186
- case _SiDuration(_, _), _SiDuration(_, _):
187
- converted = self._value.convert_to_dt(target) # type: ignore[arg-type, assignment] # pyright: ignore[reportArgumentType]
188
- case _DtDuration(_, _), _SiDuration(_, _):
189
- converted = self._value.convert(target) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
190
- case _:
191
- raise TypeError(f"cant't convert type {self.unit} to {target}")
192
- return Duration(converted.value, converted.unit)
193
-
194
-
195
- @dataclass(order=True)
196
- class _DtDuration(BaseType):
136
+ @dataclass(config=ConfigDict(arbitrary_types_allowed=True))
137
+ class Duration:
197
138
  value: int
198
- unit: Literal[TimeUnit.DT] = TimeUnit.DT
199
-
200
- def convert(self, target: _SiDuration) -> _SiDuration:
201
- return _SiDuration(self.value * target.value, target.unit)
202
-
203
-
204
- @dataclass(order=True)
205
- class _SiDuration(BaseType):
206
- value: int = field(compare=False)
207
- unit: _SI_TIME = field(compare=False)
208
- _np_rep: np.timedelta64 = field(init=False, repr=False)
139
+ unit: TimeUnit
140
+ dtype: Literal["duration"] = "duration"
141
+ _np_rep: np.timedelta64 = Field(init=False, repr=False, exclude=True)
209
142
 
210
143
  def __post_init__(self):
211
144
  err = TypeError(
@@ -223,30 +156,54 @@ class _SiDuration(BaseType):
223
156
  except ValueError as e:
224
157
  raise err from e
225
158
 
226
- def convert_to_dt(self, clock: _SiDuration) -> _DtDuration:
227
- try:
228
- converted_si = self.convert_to_si(clock.unit)
229
- except TypeError as e:
230
- raise TypeError(
231
- "fail to convert to dt type. Consider rescaling the clock time.",
232
- ) from e
233
- val = np.double(converted_si.value / clock.value)
234
- # N.B, this might be too strict. Some rounding might be necessary.
235
- if not val.is_integer():
236
- raise TypeError(
237
- "fail to convert to dt type. Consider rescaling the clock time.",
238
- )
239
- return _DtDuration(int(val))
240
-
241
- def convert_to_si(self, unit: _SI_TIME) -> _SiDuration:
242
- if self._np_rep is None:
243
- raise TypeError("`convert` only support SI time unit.")
159
+ def __gt__(self, other: Duration) -> bool:
160
+ return bool(self._np_rep > other._np_rep)
161
+
162
+ def __ge__(self, other: Duration) -> bool:
163
+ return bool(self._np_rep >= other._np_rep)
164
+
165
+ def __lt__(self, other: Duration) -> bool:
166
+ return bool(self._np_rep < other._np_rep)
167
+
168
+ def __le__(self, other: Duration) -> bool:
169
+ return bool(self._np_rep <= other._np_rep)
170
+
171
+ def __eq__(self, other: object) -> bool:
172
+ if isinstance(other, Duration):
173
+ return bool(self._np_rep == other._np_rep)
174
+ return False
175
+
176
+ def convert(self, unit: TimeUnit) -> Duration | InvalidDurationConversion:
244
177
  val: np.float64 = self._np_rep / np.timedelta64(1, unit)
245
- if not val.is_integer():
246
- raise TypeError(
247
- f"fail to convert to {unit} with {self.value}{self.unit}.",
248
- )
249
- return _SiDuration(int(val), unit)
178
+ if val.is_integer():
179
+ return Duration(int(val), unit)
180
+ return InvalidDurationConversion(
181
+ f"fail to convert to {unit} with {self.value} {self.unit}.",
182
+ )
183
+
184
+ def to_ns(self) -> Duration:
185
+ match self.convert(TimeUnit.NS):
186
+ case InvalidDurationConversion():
187
+ raise TypeError(f"Cannot convert {self} to nanoseconds.")
188
+ case converted:
189
+ return converted
190
+
191
+ def strict_div(self, other: Duration) -> Duration | InvalidDurationDiv:
192
+ self_ns = self.to_ns()
193
+ other_ns = other.to_ns()
194
+ val = np.double(self_ns.value / other_ns.value)
195
+ if np.isclose(val, 0):
196
+ return Duration(int(val), TimeUnit.NS)
197
+ return InvalidDurationDiv(
198
+ f"{self} is not a multiple of {other}",
199
+ Duration(int(val), TimeUnit.NS),
200
+ )
201
+
202
+ @staticmethod
203
+ def from_intlike(val: float, unit: TimeUnit) -> Duration:
204
+ if not np.double(val).is_integer():
205
+ raise ValueError("Failed to create a Duration object. Value must be an integer.")
206
+ return Duration(int(val), unit)
250
207
 
251
208
  def to_seconds(self) -> float:
252
209
  return float(self._np_rep / np.timedelta64(1, "s"))
@@ -261,7 +218,7 @@ def ensure_frequency_hz(value: Any) -> Any:
261
218
  case dict():
262
219
  return TypeAdapter(Frequency).validate_python(value)
263
220
  case _:
264
- raise ValueError("Frequency needs to be numeric.")
221
+ raise ValueError("Frequency must be numeric.")
265
222
 
266
223
 
267
224
  FrequencyHzLike = Annotated[
@@ -280,13 +237,13 @@ def ensure_duration_ns(value: Any) -> Any:
280
237
  case dict():
281
238
  return TypeAdapter(Duration).validate_python(value)
282
239
  case _:
283
- raise ValueError("Duration needs to be numeric")
240
+ raise ValueError("Duration must be numeric.")
284
241
 
285
242
 
286
243
  DurationNsLike = Annotated[Duration, BeforeValidator(ensure_duration_ns)]
287
244
 
288
245
 
289
- @pydantic_dataclass
246
+ @dataclass
290
247
  class ISO8601Datetime:
291
248
  value: datetime
292
249
 
@@ -351,3 +308,13 @@ ISO8601DatetimeUTCLike = Annotated[
351
308
  BeforeValidator(_validate_iso_datetime),
352
309
  PlainSerializer(_serialize_datetime),
353
310
  ]
311
+
312
+
313
+ class JobHistorySortOrder(enum.Enum):
314
+ CREATED_AT_DESC = 1
315
+ CREATED_AT_ASC = 2
316
+
317
+
318
+ DEFAULT_JOB_HISTORY_PAGE = 1
319
+ DEFAULT_JOB_HISTORY_PAGE_SIZE = 10
320
+ DEFAULT_JOB_HISTORY_SORT_ORDER = JobHistorySortOrder.CREATED_AT_DESC
@@ -13,4 +13,8 @@
13
13
 
14
14
  __all__ = ["Device", "InvalidDevice", "InvalidDeviceComponent"]
15
15
 
16
- from boulderopalscaleupsdk.device.device import Device, InvalidDevice, InvalidDeviceComponent
16
+ from boulderopalscaleupsdk.device.device import (
17
+ Device,
18
+ InvalidDevice,
19
+ InvalidDeviceComponent,
20
+ )
@@ -16,9 +16,13 @@ from pathlib import Path
16
16
  from typing import Any
17
17
 
18
18
  import yaml
19
- from pydantic import BaseModel, ConfigDict
19
+ from pydantic import BaseModel
20
20
 
21
- from boulderopalscaleupsdk.device.controller.quantum_machines import QuantumMachinesControllerInfo
21
+ from boulderopalscaleupsdk.device.controller import (
22
+ ControllerInfoTypeAdapter,
23
+ QBLOXControllerInfo,
24
+ QuantumMachinesControllerInfo,
25
+ )
22
26
  from boulderopalscaleupsdk.device.processor import (
23
27
  SuperconductingProcessor,
24
28
  SuperconductingProcessorTemplate,
@@ -31,11 +35,9 @@ class ProcessorArchitecture(str, Enum):
31
35
 
32
36
 
33
37
  class DeviceInfo(BaseModel):
34
- controller_info: QuantumMachinesControllerInfo # | OtherControllerInfoTypes
38
+ controller_info: QBLOXControllerInfo | QuantumMachinesControllerInfo
35
39
  processor: SuperconductingProcessor # | OtherSDKProcessorType
36
40
 
37
- model_config = ConfigDict(use_enum_values=True)
38
-
39
41
  def to_dict(self) -> dict[str, Any]:
40
42
  return sanitize_keys(self.model_dump(by_alias=True, mode="json"))
41
43
 
@@ -49,10 +51,13 @@ class DeviceConfigLoader:
49
51
 
50
52
  layout_file = device_config_data.pop("layout_file", None)
51
53
  if layout_file is None:
52
- raise ValueError("layout file is missing in the device configuration data.")
53
- self._validate_file_is_filename(layout_file)
54
+ raise ValueError("Layout file is missing from device configuration data.")
55
+
56
+ layout_path = Path(layout_file)
57
+ if not layout_path.is_absolute():
58
+ self._validate_file_is_filename(layout_path.name)
59
+ layout_path = self.config_path.parent / layout_file
54
60
 
55
- layout_path = self.config_path.parent / layout_file
56
61
  device_layout_data = self._load_yaml_file(layout_path)
57
62
 
58
63
  processed_device_config = {**device_config_data, **device_layout_data}
@@ -66,13 +71,13 @@ class DeviceConfigLoader:
66
71
  device_config_dict,
67
72
  )
68
73
  device_info = DeviceInfo(
69
- controller_info=QuantumMachinesControllerInfo.model_validate(
74
+ controller_info=ControllerInfoTypeAdapter.validate_python(
70
75
  device_config_dict["controller_info"],
71
76
  ),
72
77
  processor=SuperconductingProcessor.from_template(superconducting_template),
73
78
  )
74
79
  case other:
75
- raise ValueError(f"Invalid or unsupported architecture {other}")
80
+ raise ValueError(f"Invalid or unsupported architecture {other}.")
76
81
  return device_info
77
82
 
78
83
  @staticmethod
@@ -12,21 +12,24 @@
12
12
  # License for the specific language.
13
13
 
14
14
  __all__ = [
15
- "BaseControllerInfo",
16
- "DrivePortConfig",
17
- "FluxPortConfig",
18
- "OctaveConfig",
19
- "PortRef",
15
+ "ControllerType",
16
+ "QBLOXControllerInfo",
20
17
  "QuantumMachinesControllerInfo",
21
- "ReadoutPortConfig",
22
18
  ]
23
19
 
24
- from .base import BaseControllerInfo
25
- from .quantum_machines import (
26
- DrivePortConfig,
27
- FluxPortConfig,
28
- OctaveConfig,
29
- PortRef,
30
- QuantumMachinesControllerInfo,
31
- ReadoutPortConfig,
20
+ from typing import TypeVar
21
+
22
+ from pydantic import TypeAdapter
23
+
24
+ from .base import ControllerType
25
+ from .qblox import QBLOXControllerInfo
26
+ from .quantum_machines import QuantumMachinesControllerInfo
27
+
28
+ ControllerInfoType = TypeVar(
29
+ "ControllerInfoType",
30
+ bound=QBLOXControllerInfo | QuantumMachinesControllerInfo,
31
+ )
32
+
33
+ ControllerInfoTypeAdapter: TypeAdapter[QBLOXControllerInfo | QuantumMachinesControllerInfo] = (
34
+ TypeAdapter(QBLOXControllerInfo | QuantumMachinesControllerInfo)
32
35
  )
@@ -11,8 +11,17 @@
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 pydantic import BaseModel
15
14
 
15
+ import enum
16
16
 
17
- class BaseControllerInfo(BaseModel):
18
- """Base controller info."""
17
+
18
+ class ControllerType(str, enum.Enum):
19
+ QUANTUM_MACHINES = "quantum_machines"
20
+ QBLOX = "qblox"
21
+
22
+
23
+ class Backend(str, enum.Enum):
24
+ QUA = "QUA"
25
+ QBLOX = "QBLOX"
26
+ OPENQASM = "OPENQASM"
27
+ QBLOX_Q1ASM = "QBLOX_Q1ASM"