boulder-opal-scale-up-sdk 1.0.3__py3-none-any.whl → 1.0.5__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 (49) hide show
  1. {boulder_opal_scale_up_sdk-1.0.3.dist-info → boulder_opal_scale_up_sdk-1.0.5.dist-info}/METADATA +1 -1
  2. boulder_opal_scale_up_sdk-1.0.5.dist-info/RECORD +73 -0
  3. boulderopalscaleupsdk/agent/worker.py +23 -4
  4. boulderopalscaleupsdk/common/dtypes.py +31 -1
  5. boulderopalscaleupsdk/device/__init__.py +0 -7
  6. boulderopalscaleupsdk/device/config_loader.py +1 -0
  7. boulderopalscaleupsdk/device/controller/qblox.py +222 -25
  8. boulderopalscaleupsdk/device/defcal.py +5 -44
  9. boulderopalscaleupsdk/device/device.py +45 -3
  10. boulderopalscaleupsdk/device/processor/__init__.py +0 -2
  11. boulderopalscaleupsdk/device/processor/common.py +1 -6
  12. boulderopalscaleupsdk/device/processor/superconducting_processor.py +34 -7
  13. boulderopalscaleupsdk/experiments/__init__.py +24 -2
  14. boulderopalscaleupsdk/experiments/chi01_scan.py +18 -15
  15. boulderopalscaleupsdk/experiments/common.py +0 -16
  16. boulderopalscaleupsdk/experiments/drag_leakage_calibration.py +66 -0
  17. boulderopalscaleupsdk/experiments/fine_amplitude_calibration.py +54 -0
  18. boulderopalscaleupsdk/experiments/power_rabi.py +22 -16
  19. boulderopalscaleupsdk/experiments/power_rabi_ef.py +67 -0
  20. boulderopalscaleupsdk/experiments/ramsey.py +21 -18
  21. boulderopalscaleupsdk/experiments/readout_classifier_calibration.py +30 -8
  22. boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +20 -18
  23. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +23 -20
  24. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +22 -21
  25. boulderopalscaleupsdk/experiments/t1.py +16 -22
  26. boulderopalscaleupsdk/experiments/t2.py +19 -16
  27. boulderopalscaleupsdk/experiments/t2_echo.py +51 -0
  28. boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +26 -24
  29. boulderopalscaleupsdk/experiments/transmon_spectroscopy.py +19 -17
  30. boulderopalscaleupsdk/experiments/voltage_bias_fine_tune.py +58 -0
  31. boulderopalscaleupsdk/experiments/waveforms.py +63 -0
  32. boulderopalscaleupsdk/experiments/zz_ramsey.py +59 -0
  33. boulderopalscaleupsdk/plotting/dtypes.py +6 -3
  34. boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +9 -3
  35. boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +14 -0
  36. boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +34 -0
  37. boulderopalscaleupsdk/protobuf/v1/device_pb2.py +32 -32
  38. boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +8 -4
  39. boulderopalscaleupsdk/routines/__init__.py +25 -1
  40. boulderopalscaleupsdk/routines/common.py +13 -0
  41. boulderopalscaleupsdk/routines/one_qubit_calibration.py +36 -0
  42. boulderopalscaleupsdk/routines/resonator_mapping.py +17 -1
  43. boulderopalscaleupsdk/routines/transmon_coherence.py +34 -0
  44. boulderopalscaleupsdk/routines/transmon_discovery.py +41 -0
  45. boulderopalscaleupsdk/routines/transmon_retuning.py +41 -0
  46. boulderopalscaleupsdk/stubs/maps.py +11 -2
  47. boulder_opal_scale_up_sdk-1.0.3.dist-info/RECORD +0 -62
  48. {boulder_opal_scale_up_sdk-1.0.3.dist-info → boulder_opal_scale_up_sdk-1.0.5.dist-info}/LICENSE +0 -0
  49. {boulder_opal_scale_up_sdk-1.0.3.dist-info → boulder_opal_scale_up_sdk-1.0.5.dist-info}/WHEEL +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
@@ -0,0 +1,73 @@
1
+ boulderopalscaleupsdk/__init__.py,sha256=nD3YDqPiE52mmuUrIlDUrYSyljpMsDJvc5HsubBUSs4,592
2
+ boulderopalscaleupsdk/agent/__init__.py,sha256=aFkAtHJDOdXA126JklxYz0ix1k4lCcLLS9DQp8zUKMk,1092
3
+ boulderopalscaleupsdk/agent/worker.py,sha256=uQ7VxTGtx_CHeHE0n7V1_KmFpk0OwKoIZxiAtjeRiug,8703
4
+ boulderopalscaleupsdk/common/__init__.py,sha256=vLaJ1FP36xV5eX6VD7nShqxVBm3UzsBMIK1pmSiTIag,550
5
+ boulderopalscaleupsdk/common/dtypes.py,sha256=8YDF2SzJerA0HQ6UwyY6CU3e6_eeSC2VboA-YI7aD-A,10180
6
+ boulderopalscaleupsdk/common/typeclasses.py,sha256=dUpM4vDNQCxVuB45LTyEWO7SuLyzUlG6mJuseM80Chw,3668
7
+ boulderopalscaleupsdk/device/__init__.py,sha256=vLaJ1FP36xV5eX6VD7nShqxVBm3UzsBMIK1pmSiTIag,550
8
+ boulderopalscaleupsdk/device/common.py,sha256=NxiFdpHXd1Es2_8iqUeAqhL4R4W-POzHoWYtu13JCyo,1875
9
+ boulderopalscaleupsdk/device/config_loader.py,sha256=kGUaUtYAREYN7grtQ9SUO3dPUqiHVUnVOQbXFZpXvtw,3414
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=c5RXqS9z2e-6Kso_eJIznIqJMR0kqL1NJRvJboYLafE,29377
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=3IPmcLx89ZDv9sCPncozWA-hhnZRZK3OiOiBcAW05lI,739
16
+ boulderopalscaleupsdk/device/device.py,sha256=smNgGtPs4xx2xyBiEtu178EiAW0ylMklv-wOSSnvoGA,2212
17
+ boulderopalscaleupsdk/device/processor/__init__.py,sha256=YLn1UTCIX0CYfah1muLXrhujJx7sz56JZQ4Pn4DnKd0,972
18
+ boulderopalscaleupsdk/device/processor/common.py,sha256=1ot1MbK6P1rZspX9bpfFXUSzlzr70x0GIDAVsFKcMTA,9667
19
+ boulderopalscaleupsdk/device/processor/superconducting_processor.py,sha256=pt4hWdRoBD8EWYlG1HDPfumZNdRzo7Be9FDqHoRAFDw,13394
20
+ boulderopalscaleupsdk/experiments/__init__.py,sha256=nXAkD_BQYYDbd4_ZGLWoO2NsYllJRKV1oVr0i2V1yQ0,2304
21
+ boulderopalscaleupsdk/experiments/chi01_scan.py,sha256=vcvRbC7UyE_7UfduhcaVJEo5qJxwXQcPTjigXOKBtCc,2280
22
+ boulderopalscaleupsdk/experiments/common.py,sha256=m-A91hOcLYHBqL9jUI4-ia_13U0-wv711aMgCyjiGqw,2535
23
+ boulderopalscaleupsdk/experiments/drag_leakage_calibration.py,sha256=ey0g5hJBGgrtGdwtrMXGCYvd6TvgWIG63wfSJ6ahGzE,2400
24
+ boulderopalscaleupsdk/experiments/fine_amplitude_calibration.py,sha256=3Us-C9Ss5Ks-1TYSIYToEY1hgZr2VF2CY22cgOWk530,1963
25
+ boulderopalscaleupsdk/experiments/power_rabi.py,sha256=-0iT1KHY-HecEvjJ5nQcmWyRNd1505xfVmG_j1gqZXs,2453
26
+ boulderopalscaleupsdk/experiments/power_rabi_ef.py,sha256=j-XoGcNXPtb-NiwwuXhqZaGl70Fh5RKsdZNFSO2hHz0,2461
27
+ boulderopalscaleupsdk/experiments/ramsey.py,sha256=Ym1dqAs3b3nPfNp4T3TPIMTDDMT6eBJ-CUCua9CyKl8,2192
28
+ boulderopalscaleupsdk/experiments/readout_classifier_calibration.py,sha256=k7P1PDbpSz_KjRt4A39ilhA_Ke9IpMKAjoFqT_9RfWY,1841
29
+ boulderopalscaleupsdk/experiments/resonator_spectroscopy.py,sha256=QMEh52zh-ZsraYn2sq7qnE0g0z0dhBsSndIepBrBzVM,2262
30
+ boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py,sha256=CCZ6AkQ_Q5zfuDpcFe-AI-SLHUUtFnxiPUwS4Y5Xo2Q,2888
31
+ boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py,sha256=jtQHeeQyshlg9_WJwwFONk6FKWRbE88zURwyihY4ZxI,2383
32
+ boulderopalscaleupsdk/experiments/t1.py,sha256=Tia4siBGcQ2-WkKw8F5y6smwW5_h3INbKUxM1nPiufI,1635
33
+ boulderopalscaleupsdk/experiments/t2.py,sha256=LQHwFyOLa0MiVUxRCcovGBVC32bfA7xzfBILgXq0CCs,1790
34
+ boulderopalscaleupsdk/experiments/t2_echo.py,sha256=dgohfuliI2f3KsiyhfSMrwJjwq5r3meakEPmH-3HUAY,1804
35
+ boulderopalscaleupsdk/experiments/transmon_anharmonicity.py,sha256=QELWV3X_sD2ZN1_JwUcS8E4ykYGSW4a7zFPpKuXbjdk,2841
36
+ boulderopalscaleupsdk/experiments/transmon_spectroscopy.py,sha256=y6aZwW2jys5JOYqozfTKpLU5ZXlwP1AQVm73MWO-zB4,2402
37
+ boulderopalscaleupsdk/experiments/voltage_bias_fine_tune.py,sha256=P5RuASiP3UUudLQIFWIznTuhCOK0ndj9FN9l6bHv_xU,2060
38
+ boulderopalscaleupsdk/experiments/waveforms.py,sha256=zyzADCra9T0wphltrhfj75wB2y4qwTSvcU2mBJQNrjU,1712
39
+ boulderopalscaleupsdk/experiments/zz_ramsey.py,sha256=mpek0jKVdbOUnXb0KNYsxGTo3MnMR8SG4cYiHOA1KY4,2017
40
+ boulderopalscaleupsdk/grpc_interceptors/__init__.py,sha256=PkdOlllLbRWycofvGCCxDY60Ydp_U0QM5l4u3Fsnno4,616
41
+ boulderopalscaleupsdk/grpc_interceptors/auth.py,sha256=PSe_b9ckqhcb3xf1Y4jpn1TjfEWfokkarqfUHIz6HIs,4165
42
+ boulderopalscaleupsdk/plotting/__init__.py,sha256=pkdCky3YHqc3PLRy_h9puJKAO4AqSJGebb26x7XayR4,959
43
+ boulderopalscaleupsdk/plotting/dtypes.py,sha256=Ir-aFywqHeq0cI2jHY1uexjJHYpXOZwsg5nts9tpeQc,4892
44
+ boulderopalscaleupsdk/protobuf/v1/agent_pb2.py,sha256=r8vE_d7e5iG04UCDeV0FgxOVOSflOlwmSs2I5CotDBU,6147
45
+ boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi,sha256=bwRNbnwbwkqx7OSE4EurVS6865zqdtqmTcRjwJNH7OI,3082
46
+ boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py,sha256=gfv0Ncc6_MS38Lzj6NHYXhwJVo7qK7XYQiCsi-KTl5w,8593
47
+ boulderopalscaleupsdk/protobuf/v1/device_pb2.py,sha256=yvgsfFYM-O3LsMlcoVah3CngYxg1djHkkaztD__PE7Q,11599
48
+ boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi,sha256=GqrIb_5V6x4yQHzbJHilS_GGnlDsyhFr_RCratS2KFc,6846
49
+ boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py,sha256=0CtrO8gKkcfHn7flQLFAySyOVpdDgzct_EaMMF73rUY,19445
50
+ boulderopalscaleupsdk/protobuf/v1/task_pb2.py,sha256=Sd-xzEOJkEZAFw_A0cNXVKOZ_Q71XrIkMl8OTuL9zoo,7132
51
+ boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi,sha256=o4QeMmUCT_x19eL7dlNE0LtdqJi1e9kv2gMiD7Z4pwg,6048
52
+ boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py,sha256=yP2FZ148RmSYFHivatCXuM13oRBDA5wBwmvhZQNBTXQ,5826
53
+ boulderopalscaleupsdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
+ boulderopalscaleupsdk/routines/__init__.py,sha256=4oxML7Ztq7gRWtvDZW_-QUr_Svez8kKIxXIuYhT3LyU,1009
55
+ boulderopalscaleupsdk/routines/common.py,sha256=BvZ1DvCJDFHkauOEq8z-Mkj_mwO-1BSRLHkiTaIbJ0U,775
56
+ boulderopalscaleupsdk/routines/one_qubit_calibration.py,sha256=n3QifgKVcQu0Idkxj0Hk9husi3xcsoMJz7t3nTvBwEk,1018
57
+ boulderopalscaleupsdk/routines/resonator_mapping.py,sha256=Jp7ubmP0knRaoSE6IZnww2q0tixmjZ1HG3NjvEW0Rhs,1186
58
+ boulderopalscaleupsdk/routines/transmon_coherence.py,sha256=qwFlrDgwRdo4rWwXJoQ0pLJSKiUnzMYasK6kMHWhlHI,1074
59
+ boulderopalscaleupsdk/routines/transmon_discovery.py,sha256=ZI-mQQSlUWhU1J_H1ie9Hz4TOK3aMxZqtfX2AgHNrKI,1453
60
+ boulderopalscaleupsdk/routines/transmon_retuning.py,sha256=3C9iSE7D3dnPbj-RJw5pogAwS3Hs9C-9uBLQT81qeAA,1423
61
+ boulderopalscaleupsdk/stubs/__init__.py,sha256=vLaJ1FP36xV5eX6VD7nShqxVBm3UzsBMIK1pmSiTIag,550
62
+ boulderopalscaleupsdk/stubs/dtypes.py,sha256=VH2QqmmaSegv03KEaZBqB-REHhvk5h3GPfQgCJTAYQE,1361
63
+ boulderopalscaleupsdk/stubs/maps.py,sha256=oSGfEOCGSAZjqDRVbHr0B7S3VnyV1pvdRBa5NqnXxuA,1087
64
+ boulderopalscaleupsdk/third_party/__init__.py,sha256=b9T2IVfz9bMenSsLQuaf4A14ySinjNnSGx68hLsFHZE,596
65
+ boulderopalscaleupsdk/third_party/quantum_machines/__init__.py,sha256=ORD6crMw6gtoIp8dmusdwSiU4pje739fOacOfHXSOls,2541
66
+ boulderopalscaleupsdk/third_party/quantum_machines/config.py,sha256=tQsWx_nSXSFT4fRuMzuvliplyRsB9Sy70kHqsKuTA-A,20756
67
+ boulderopalscaleupsdk/third_party/quantum_machines/constants.py,sha256=5PpAi6MZ53yEinBHerY2ssi30BR37JnLBWL21irpZ8Y,870
68
+ boulderopalscaleupsdk/utils/__init__.py,sha256=vLaJ1FP36xV5eX6VD7nShqxVBm3UzsBMIK1pmSiTIag,550
69
+ boulderopalscaleupsdk/utils/serial_utils.py,sha256=SzvY-WFD0b8IGSh6PTv7F9y1W2IPPYmj8pY3AsX-xlM,2062
70
+ boulder_opal_scale_up_sdk-1.0.5.dist-info/LICENSE,sha256=wqX4S5Brcwkwo750l9grSspwm0cyMsZtZEa1Vx3_WiE,36587
71
+ boulder_opal_scale_up_sdk-1.0.5.dist-info/METADATA,sha256=6XkaUQrGLbPksehJqwigcC3NAMujG9JNeS4vWrsbCi8,2419
72
+ boulder_opal_scale_up_sdk-1.0.5.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
73
+ boulder_opal_scale_up_sdk-1.0.5.dist-info/RECORD,,
@@ -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
@@ -10,10 +10,3 @@
10
10
  # Unless required by applicable law or agreed to in writing, software
11
11
  # distributed under the License is distributed on an "AS IS" BASIS. See the
12
12
  # License for the specific language.
13
-
14
- __all__ = ["InvalidDevice", "InvalidDeviceComponent"]
15
-
16
- from boulderopalscaleupsdk.device.device import (
17
- InvalidDevice,
18
- InvalidDeviceComponent,
19
- )
@@ -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,52 +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
- import re
15
- from typing import Annotated, Any, Literal
14
+ from typing import Literal
16
15
 
17
- from pydantic import BeforeValidator, PlainSerializer, TypeAdapter
18
- from pydantic.dataclasses import dataclass
16
+ from pydantic import BaseModel
19
17
 
20
- QubitAddr = tuple[int, ...]
21
- GateName = str
22
18
 
23
- _KEY_PATTERN = r"^.+#\d+(?:-\d+)*$"
24
-
25
-
26
- @dataclass(frozen=True)
27
- class DataKey:
28
- gate: GateName
29
- addr: QubitAddr
30
-
31
- def as_tuple(self) -> tuple[GateName, QubitAddr]:
32
- return (self.gate, self.addr)
33
-
34
- @staticmethod
35
- def from_string(value: Any) -> "DataKey | None":
36
- match value:
37
- case str():
38
- if not re.match(_KEY_PATTERN, value):
39
- raise KeyError(f"invalid string for DataKey {value}")
40
- gate, qubit_str = value.split("#", 1)
41
- qubit_addr = tuple(int(q) for q in qubit_str.split("-"))
42
- return DataKey(gate=gate, addr=qubit_addr)
43
- case _:
44
- return None
45
-
46
- def serialize_as_string(self) -> str:
47
- return f"{self.gate}#{'-'.join(str(i) for i in self.addr)}"
48
-
49
-
50
- DataKeyTypeAdapter = TypeAdapter(DataKey)
51
- DataKeyLike = Annotated[
52
- DataKey,
53
- BeforeValidator(lambda x: DataKey.from_string(x) or x),
54
- PlainSerializer(lambda x: x.serialize_as_string(), return_type=str),
55
- ]
56
-
57
-
58
- @dataclass
59
- class DefCalData:
19
+ class DefCalData(BaseModel):
20
+ gate: str
21
+ addr: list[str]
60
22
  body: str
61
23
  status: Literal["calibrated", "uncalibrated"]
62
- scope: Literal["public", "internal"]