boulder-opal-scale-up-sdk 1.0.6__tar.gz → 1.0.8__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 (87) hide show
  1. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/PKG-INFO +4 -6
  2. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/common/dtypes.py +36 -10
  3. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/controller/qblox.py +159 -54
  4. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/device.py +0 -1
  5. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/processor/superconducting_processor.py +21 -1
  6. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/chi01_scan.py +1 -1
  7. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/experiments/cz_spectroscopy_by_bias.py +65 -0
  8. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/drag_leakage_calibration.py +1 -4
  9. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/fine_amplitude_calibration.py +4 -3
  10. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/power_rabi.py +3 -3
  11. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/power_rabi_ef.py +3 -3
  12. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/ramsey.py +1 -1
  13. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/ramsey_ef.py +1 -1
  14. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/readout_classifier.py +2 -2
  15. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/readout_optimization.py +3 -3
  16. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +1 -1
  17. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +3 -3
  18. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +1 -1
  19. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/t1.py +1 -1
  20. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/t2.py +2 -2
  21. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/t2_echo.py +1 -1
  22. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/transmon_anharmonicity.py +1 -1
  23. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/transmon_spectroscopy.py +2 -2
  24. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/voltage_bias_fine_tune.py +7 -5
  25. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/waveforms.py +15 -0
  26. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/zz_ramsey.py +8 -14
  27. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/plotting/dtypes.py +1 -1
  28. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/device_pb2.py +33 -39
  29. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +10 -18
  30. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +0 -34
  31. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/protobuf/v1/resource_pb2.py +40 -0
  32. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/protobuf/v1/resource_pb2.pyi +52 -0
  33. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/protobuf/v1/resource_pb2_grpc.py +104 -0
  34. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/routines/__init__.py +2 -0
  35. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/routines/coupler_discovery.py +37 -0
  36. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/routines/one_qubit_calibration.py +4 -0
  37. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/routines/resonator_mapping.py +1 -1
  38. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/routines/transmon_retuning.py +0 -4
  39. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/solutions/__init__.py +22 -0
  40. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/solutions/common.py +23 -0
  41. boulder_opal_scale_up_sdk-1.0.8/boulderopalscaleupsdk/solutions/placeholder_solution.py +28 -0
  42. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +15 -0
  43. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/third_party/quantum_machines/config.py +0 -3
  44. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/pyproject.toml +24 -16
  45. boulder_opal_scale_up_sdk-1.0.6/boulderopalscaleupsdk/experiments/cz_spectroscopy_by_bias.py +0 -84
  46. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/LICENSE +0 -0
  47. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/README.md +0 -0
  48. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/__init__.py +0 -0
  49. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/agent/__init__.py +0 -0
  50. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/agent/worker.py +0 -0
  51. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/common/__init__.py +0 -0
  52. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/common/typeclasses.py +0 -0
  53. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/constants.py +0 -0
  54. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/__init__.py +0 -0
  55. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/common.py +0 -0
  56. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/config_loader.py +0 -0
  57. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/controller/__init__.py +0 -0
  58. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/controller/base.py +0 -0
  59. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/controller/quantum_machines.py +0 -0
  60. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/controller/resolver.py +0 -0
  61. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/defcal.py +0 -0
  62. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/processor/__init__.py +0 -0
  63. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/device/processor/common.py +0 -0
  64. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/errors.py +0 -0
  65. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/__init__.py +0 -0
  66. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/experiments/common.py +0 -0
  67. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/grpc_interceptors/__init__.py +0 -0
  68. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/grpc_interceptors/auth.py +0 -0
  69. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/grpc_interceptors/error.py +0 -0
  70. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/plotting/__init__.py +0 -0
  71. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +0 -0
  72. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +0 -0
  73. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +0 -0
  74. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/job_pb2.py +0 -0
  75. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/job_pb2.pyi +0 -0
  76. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/job_pb2_grpc.py +0 -0
  77. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/task_pb2.py +0 -0
  78. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +0 -0
  79. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +0 -0
  80. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/py.typed +0 -0
  81. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/routines/common.py +0 -0
  82. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/routines/transmon_coherence.py +0 -0
  83. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/routines/transmon_discovery.py +0 -0
  84. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/third_party/__init__.py +0 -0
  85. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/third_party/quantum_machines/constants.py +0 -0
  86. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/boulderopalscaleupsdk/utils/__init__.py +0 -0
  87. {boulder_opal_scale_up_sdk-1.0.6 → boulder_opal_scale_up_sdk-1.0.8}/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.6
3
+ Version: 1.0.8
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
@@ -14,10 +14,8 @@ 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 :: Other/Proprietary License
18
17
  Classifier: Natural Language :: English
19
18
  Classifier: Operating System :: OS Independent
20
- Classifier: Programming Language :: Python :: 3
21
19
  Classifier: Programming Language :: Python :: 3.11
22
20
  Classifier: Programming Language :: Python :: 3.12
23
21
  Classifier: Topic :: Internet :: WWW/HTTP
@@ -27,13 +25,13 @@ Classifier: Topic :: Software Development :: Embedded Systems
27
25
  Classifier: Topic :: System :: Distributed Computing
28
26
  Provides-Extra: quantum-machines
29
27
  Requires-Dist: attrs (>=25.1.0,<26.0.0)
30
- Requires-Dist: googleapis-common-protos (>=1.69.2,<2.0.0)
31
- Requires-Dist: numpy (>=1.26.4,<2.0.0)
28
+ Requires-Dist: googleapis-common-protos (>=1.69.2,<1.70.0)
29
+ Requires-Dist: numpy (>=2.3.3,<3.0.0)
32
30
  Requires-Dist: pydantic (>=2.10.4,<3.0.0)
33
31
  Requires-Dist: pydantic-settings (>=2.7.0,<3.0.0)
34
32
  Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
35
33
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
36
- Requires-Dist: qm-qua (==1.2.1) ; extra == "quantum-machines"
34
+ Requires-Dist: qm-qua ; extra == "quantum-machines"
37
35
  Project-URL: Facebook, https://www.facebook.com/qctrl
38
36
  Project-URL: GitHub, https://github.com/qctrl
39
37
  Project-URL: Homepage, https://q-ctrl.com
@@ -33,7 +33,7 @@ from dateutil.parser import isoparse
33
33
  from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer, TypeAdapter
34
34
  from pydantic.dataclasses import dataclass
35
35
 
36
- from boulderopalscaleupsdk.plotting.dtypes import Plot # noqa: TC001
36
+ from boulderopalscaleupsdk.plotting import Plot
37
37
 
38
38
  GrpcMetadata = list[tuple[str, str | bytes]]
39
39
 
@@ -158,7 +158,7 @@ class Duration:
158
158
 
159
159
  self.value = int(self.value)
160
160
  try:
161
- self._np_rep = np.timedelta64(self.value, self.unit)
161
+ self._np_rep = np.timedelta64(self.value, self.unit.value)
162
162
  except ValueError as e:
163
163
  raise err from e
164
164
 
@@ -179,12 +179,17 @@ class Duration:
179
179
  return bool(self._np_rep == other._np_rep)
180
180
  return False
181
181
 
182
- def convert(self, unit: TimeUnit) -> Duration | InvalidDurationConversion:
183
- val: np.float64 = self._np_rep / np.timedelta64(1, unit)
182
+ def __str__(self):
183
+ return f"{self.value} {self.unit.value}"
184
+
185
+ def convert(self, unit: TimeUnit | str) -> Duration | InvalidDurationConversion:
186
+ unit_enum = TimeUnit(unit) if isinstance(unit, str) else unit
187
+
188
+ val = np.float64(self._np_rep / np.timedelta64(1, unit_enum.value))
184
189
  if val.is_integer():
185
- return Duration(int(val), unit)
190
+ return Duration(int(val), unit_enum)
186
191
  return InvalidDurationConversion(
187
- f"fail to convert to {unit} with {self.value} {self.unit}.",
192
+ f"fail to convert to {unit_enum} with {self.value} {self.unit}.",
188
193
  )
189
194
 
190
195
  def to_ns(self) -> Duration:
@@ -342,14 +347,17 @@ class JobSummary(BaseModel):
342
347
 
343
348
  class JobDataEntry(BaseModel):
344
349
  message: str
345
- created_at: ISO8601DatetimeUTCLike
346
- dt: str
347
- elapsed_time: str
348
- plots: list[Plot] = []
349
350
 
350
351
  class Config:
351
352
  extra = "allow"
352
353
 
354
+ def get_display_items(self) -> list[str | Plot]:
355
+ items: list[str | Plot] = [self.message]
356
+
357
+ if plots := getattr(self, "plots", []):
358
+ items.extend(TypeAdapter(Plot).validate_python(plot) for plot in plots)
359
+ return items
360
+
353
361
 
354
362
  class JobData(BaseModel):
355
363
  id: str
@@ -359,6 +367,24 @@ class JobData(BaseModel):
359
367
  device_name: str
360
368
  data: list[JobDataEntry]
361
369
 
370
+ def get_display_items(self) -> list[str | Plot]:
371
+ items: list[str | Plot] = []
372
+ message = "\n".join(
373
+ [
374
+ "JobData summary:",
375
+ f" - id: {self.id}",
376
+ f" - name: {self.name}",
377
+ f" - session_id: {self.session_id}",
378
+ f" - created_at: {self.created_at.isoformat()}",
379
+ f" - device_name: {self.device_name}",
380
+ ],
381
+ )
382
+ items.append(message)
383
+
384
+ for job_data_entry in self.data:
385
+ items.extend(job_data_entry.get_display_items())
386
+ return items
387
+
362
388
 
363
389
  DEFAULT_JOB_HISTORY_PAGE = 1
364
390
  DEFAULT_JOB_HISTORY_PAGE_SIZE = 10
@@ -20,7 +20,8 @@ __all__ = (
20
20
  "AcquisitionConfig",
21
21
  "BitStrideArrayEncoding",
22
22
  "ChannelType",
23
- "ComplexChannel",
23
+ "IQChannel",
24
+ "IQMixedChannel",
24
25
  "IndexedData",
25
26
  "ModuleAddr",
26
27
  "ModuleAddrType",
@@ -37,11 +38,11 @@ __all__ = (
37
38
  "PortAddrType",
38
39
  "PreparedProgram",
39
40
  "PreparedSequenceProgram",
40
- "RealChannel",
41
41
  "SequenceProgram",
42
42
  "SequencerAddr",
43
43
  "SequencerAddrType",
44
44
  "SequencerResults",
45
+ "SingleChannel",
45
46
  "process_sequencer_output",
46
47
  "validate_channel",
47
48
  )
@@ -54,7 +55,7 @@ from dataclasses import dataclass
54
55
  from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar
55
56
 
56
57
  import numpy as np
57
- from pydantic import BaseModel, BeforeValidator, Field, PlainSerializer, model_validator
58
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer, model_validator
58
59
 
59
60
  from boulderopalscaleupsdk.device.controller.base import Backend, ControllerType
60
61
 
@@ -204,10 +205,20 @@ PortAddrType = Annotated[PortAddr, _addr_validator(PortAddr), PlainSerializer(st
204
205
  # ==================================================================================================
205
206
  # Signalling Channels
206
207
  # ==================================================================================================
207
- class RealChannel(BaseModel):
208
- """A real channel targeting a single hardware port on a QBLOX module."""
208
+ class IQMixedChannel(BaseModel):
209
+ """
210
+ An IQ-mixed channel.
211
+
212
+ Both sequencer AWG paths are mixed internally and routed to a single module port.
213
+
214
+ ┌─────────┐Path 0 Port
215
+ │ ├───────►│IQMixed ┌─┐
216
+ │Sequencer│ │───────►│─│
217
+ │ ├───────►│Ch └─┘
218
+ └─────────┘Path 1
219
+ """
209
220
 
210
- mode: Literal["real"] = "real"
221
+ mode: Literal["iq-mixed"] = "iq-mixed"
211
222
  port: PortAddrType
212
223
 
213
224
  @property
@@ -219,13 +230,26 @@ class RealChannel(BaseModel):
219
230
  return self.port.direction
220
231
 
221
232
  def __str__(self) -> str:
222
- return f"{self.port!s}[R]"
233
+ return f"{self.port!s}[1]"
234
+
223
235
 
236
+ class IQChannel(BaseModel):
237
+ """
238
+ An IQ channel.
224
239
 
225
- class ComplexChannel(BaseModel):
226
- """A Complex channel targeting a single hardware port on a QBLOX module."""
240
+ The sequencer AWG paths target separate ports for external IQ mixing.
227
241
 
228
- mode: Literal["complex"] = "complex"
242
+ IPort
243
+ ┌─────────┐Path 0 ┌─┐
244
+ │ ├───────►├───────►│─│
245
+ │Sequencer│ │IQ Ch ├─┤
246
+ │ ├───────►├───────►│─│
247
+ └─────────┘Path 1 └─┘
248
+ QPort
249
+
250
+ """
251
+
252
+ mode: Literal["iq"] = "iq"
229
253
  i_port: PortAddrType
230
254
  q_port: PortAddrType
231
255
 
@@ -238,10 +262,10 @@ class ComplexChannel(BaseModel):
238
262
  return self.i_port.direction
239
263
 
240
264
  def __str__(self) -> str:
241
- return f"{self.i_port!s}_{self.q_port.number}[Z]"
265
+ return f"{self.i_port!s}_{self.q_port.number}[iq]"
242
266
 
243
267
  @model_validator(mode="after")
244
- def validate_i_q_ports(self) -> "ComplexChannel":
268
+ def validate_i_q_ports(self) -> "IQChannel":
245
269
  ii = self.i_port
246
270
  qq = self.q_port
247
271
  if ii.cluster != qq.cluster or ii.slot != qq.slot:
@@ -251,7 +275,36 @@ class ComplexChannel(BaseModel):
251
275
  return self
252
276
 
253
277
 
254
- ChannelType = RealChannel | ComplexChannel
278
+ class SingleChannel(BaseModel):
279
+ """
280
+ Single channel.
281
+
282
+ A single sequencer path is routed to a single port.
283
+
284
+ ┌─────────┐Path 0 Port
285
+ │ ├───────►│Single ┌─┐
286
+ │Sequencer│ ├───────►│─│
287
+ │ │ │Ch └─┘
288
+ └─────────┘
289
+ """
290
+
291
+ mode: Literal["single"] = "single"
292
+ port: PortAddrType
293
+ path: Literal[0, 1] = 0
294
+
295
+ @property
296
+ def module(self) -> ModuleAddr:
297
+ return ModuleAddr(self.port.cluster, self.port.slot)
298
+
299
+ @property
300
+ def direction(self) -> Literal["out", "in"]:
301
+ return self.port.direction
302
+
303
+ def __str__(self) -> str:
304
+ return f"{self.port!s}_{self.port.number}[single]"
305
+
306
+
307
+ ChannelType = IQMixedChannel | IQChannel | SingleChannel
255
308
 
256
309
 
257
310
  # ==================================================================================================
@@ -318,12 +371,28 @@ class QBLOXControllerInfo(BaseModel): # pragma: no cover
318
371
  modules: dict[ModuleAddrType, ModuleType]
319
372
  port_config: dict[str, PortConnection]
320
373
 
374
+ @model_validator(mode="after")
375
+ def validate_channels(self) -> Self:
376
+ for port, port_conn in self.port_config.items():
377
+ mod_addr = port_conn.ch_out.module
378
+ mod_type = self.modules[mod_addr]
379
+
380
+ mod_constraints = DEFAULT_MODULE_CONSTRAINTS[mod_type]
381
+ if ch_iss := validate_channel(port_conn.ch_out, mod_constraints):
382
+ raise ValueError(f"Invalid channel for port {port}: {ch_iss}")
383
+ if port_conn.ch_in and (ch_iss := validate_channel(port_conn.ch_in, mod_constraints)):
384
+ raise ValueError(f"Invalid channel for port {port}: {ch_iss}")
385
+
386
+ return self
387
+
321
388
 
322
389
  # ==================================================================================================
323
390
  # Instrument management
324
391
  # ==================================================================================================
325
392
  class SequencerParams(BaseModel):
326
- nco_freq: float | None = Field(default=None)
393
+ model_config = ConfigDict(validate_assignment=True)
394
+
395
+ nco_freq: float | None = Field(default=None, ge=-500e6, le=500e6)
327
396
  gain_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
328
397
  offset_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
329
398
  gain_awg_path1: float | None = Field(default=None, ge=-1.0, le=1.0)
@@ -338,6 +407,8 @@ class SequencerParams(BaseModel):
338
407
 
339
408
 
340
409
  class QcmParams(BaseModel):
410
+ model_config = ConfigDict(validate_assignment=True)
411
+
341
412
  out0_offset: float | None = Field(default=None, ge=-2.5, le=2.5)
342
413
  out1_offset: float | None = Field(default=None, ge=-2.5, le=2.5)
343
414
  out2_offset: float | None = Field(default=None, ge=-2.5, le=2.5)
@@ -354,6 +425,8 @@ class QcmParams(BaseModel):
354
425
 
355
426
 
356
427
  class QcmRfParams(BaseModel):
428
+ model_config = ConfigDict(validate_assignment=True)
429
+
357
430
  out0_att: int | None = Field(default=None, ge=0, le=60, multiple_of=2)
358
431
  out1_att: int | None = Field(default=None, ge=0, le=60, multiple_of=2)
359
432
 
@@ -375,6 +448,8 @@ class QcmRfParams(BaseModel):
375
448
 
376
449
 
377
450
  class QrmRfParams(BaseModel):
451
+ model_config = ConfigDict(validate_assignment=True)
452
+
378
453
  out0_att: int | None = Field(default=None, ge=0, le=60, multiple_of=2)
379
454
 
380
455
  out0_in0_lo_freq: float | None = Field(default=None, gt=0)
@@ -744,6 +819,9 @@ class BitStrideArrayEncoding:
744
819
  # ==================================================================================================
745
820
  # Utilities
746
821
  # ==================================================================================================
822
+ Q1_MAX_WAVEFORM_MEMORY = 16384 # samples
823
+
824
+
747
825
  @dataclasses.dataclass
748
826
  class ModuleConstraints:
749
827
  """Physical constraints of a module."""
@@ -753,6 +831,7 @@ class ModuleConstraints:
753
831
  n_ch_out: int = 0
754
832
  n_ch_in: int = 0
755
833
  n_digital_io: int = 0
834
+ is_rf: bool = False
756
835
 
757
836
  # TODO: Confirm if ordering is important.
758
837
  ch_out_iq_pairs: list[set[int]] = dataclasses.field(default_factory=list)
@@ -773,6 +852,7 @@ DEFAULT_MODULE_CONSTRAINTS: dict[ModuleType, ModuleConstraints] = {
773
852
  n_markers=2,
774
853
  n_ch_out=2,
775
854
  n_ch_in=0,
855
+ is_rf=True,
776
856
  ),
777
857
  ModuleType.QRM: ModuleConstraints(
778
858
  n_sequencers=6,
@@ -787,6 +867,7 @@ DEFAULT_MODULE_CONSTRAINTS: dict[ModuleType, ModuleConstraints] = {
787
867
  n_markers=2,
788
868
  n_ch_out=1,
789
869
  n_ch_in=1,
870
+ is_rf=True,
790
871
  ),
791
872
  ModuleType.QRC: ModuleConstraints(
792
873
  n_sequencers=12,
@@ -835,53 +916,77 @@ def validate_channel(ch: ChannelType, constraint: ModuleConstraints) -> list[str
835
916
  return _validate_input_channel(ch, constraint)
836
917
 
837
918
 
838
- def _validate_output_channel(ch_out: ChannelType, constraint: ModuleConstraints) -> list[str]:
919
+ def _validate_output_channel(ch_out: ChannelType, constraint: ModuleConstraints) -> list[str]: # noqa: C901
839
920
  issues = []
840
- if isinstance(ch_out, RealChannel):
841
- po_out = ch_out.port
842
- if constraint.n_ch_out == 0:
843
- issues.append("module has no output ports.")
844
- elif po_out.number < 0 or po_out.number >= constraint.n_ch_out:
845
- issues.append(
846
- f"output port number {po_out.number} out-of-bounds for module, "
847
- f"must be between [0, {constraint.n_ch_out}).",
848
- )
849
- else:
850
- valid_pairs = constraint.ch_out_iq_pairs
851
- if not valid_pairs:
852
- issues.append("module does not support complex output channels.")
853
- else:
854
- po_out_i = ch_out.i_port
855
- po_out_q = ch_out.q_port
856
- if {po_out_i.number, po_out_q.number} not in valid_pairs:
921
+ match ch_out:
922
+ case IQMixedChannel():
923
+ po_out = ch_out.port
924
+ if constraint.n_ch_out == 0:
925
+ issues.append("module has no output ports.")
926
+ elif po_out.number < 0 or po_out.number >= constraint.n_ch_out:
857
927
  issues.append(
858
- f"invalid output IQ pair {{{po_out_i.number}, {po_out_q.number}}}, module only "
859
- f"supports pairs {valid_pairs}.",
928
+ f"output port number {po_out.number} out-of-bounds for module, "
929
+ f"must be between [0, {constraint.n_ch_out}).",
930
+ )
931
+ case IQChannel():
932
+ valid_pairs = constraint.ch_out_iq_pairs
933
+ if not valid_pairs:
934
+ issues.append("module does not support IQ output channels.")
935
+ else:
936
+ po_out_i = ch_out.i_port
937
+ po_out_q = ch_out.q_port
938
+ if {po_out_i.number, po_out_q.number} not in valid_pairs:
939
+ issues.append(
940
+ f"invalid output IQ pair {{{po_out_i.number}, {po_out_q.number}}}, "
941
+ f"module only supports pairs {valid_pairs}.",
942
+ )
943
+ case SingleChannel():
944
+ po_out = ch_out.port
945
+ if constraint.is_rf:
946
+ issues.append("RF modules cannot use single channels.")
947
+ if constraint.n_ch_out == 0:
948
+ issues.append("module has no output ports.")
949
+ elif po_out.number < 0 or po_out.number >= constraint.n_ch_out:
950
+ issues.append(
951
+ f"output port number {po_out.number} out-of-bounds for module, "
952
+ f"must be between [0, {constraint.n_ch_out}).",
860
953
  )
861
954
  return issues
862
955
 
863
956
 
864
- def _validate_input_channel(ch_in: ChannelType, constraint: ModuleConstraints) -> list[str]:
957
+ def _validate_input_channel(ch_in: ChannelType, constraint: ModuleConstraints) -> list[str]: # noqa: C901
865
958
  issues = []
866
- if isinstance(ch_in, RealChannel):
867
- po_in = ch_in.port
868
- if constraint.n_ch_in == 0:
869
- issues.append("module has no input ports.")
870
- elif po_in.number < 0 or po_in.number >= constraint.n_ch_in:
871
- issues.append(
872
- f"input port number {po_in.number} out-of-bounds for module, "
873
- f"must be between [0, {constraint.n_ch_in}).",
874
- )
875
- else:
876
- valid_pairs = constraint.ch_in_iq_pairs
877
- if not valid_pairs:
878
- issues.append("module does not support complex input channels.")
879
- else:
880
- po_in_i = ch_in.i_port
881
- po_in_q = ch_in.q_port
882
- if {po_in_i.number, po_in_q.number} not in valid_pairs:
959
+ match ch_in:
960
+ case IQMixedChannel():
961
+ po_in = ch_in.port
962
+ if constraint.n_ch_in == 0:
963
+ issues.append("module has no input ports.")
964
+ elif po_in.number < 0 or po_in.number >= constraint.n_ch_in:
965
+ issues.append(
966
+ f"input port number {po_in.number} out-of-bounds for module, "
967
+ f"must be between [0, {constraint.n_ch_in}).",
968
+ )
969
+ case IQChannel():
970
+ valid_pairs = constraint.ch_in_iq_pairs
971
+ if not valid_pairs:
972
+ issues.append("module does not support IQ input channels.")
973
+ else:
974
+ po_in_i = ch_in.i_port
975
+ po_in_q = ch_in.q_port
976
+ if {po_in_i.number, po_in_q.number} not in valid_pairs:
977
+ issues.append(
978
+ f"invalid input IQ pair {{{po_in_i.number}, {po_in_q.number}}}, "
979
+ f"module only supports pairs {valid_pairs}.",
980
+ )
981
+ case SingleChannel():
982
+ po_in = ch_in.port
983
+ if constraint.is_rf:
984
+ issues.append("RF modules cannot use single channels.")
985
+ if constraint.n_ch_in == 0:
986
+ issues.append("module has no input ports.")
987
+ elif po_in.number < 0 or po_in.number >= constraint.n_ch_in:
883
988
  issues.append(
884
- f"invalid input IQ pair {{{po_in_i.number}, {po_in_q.number}}}, module only "
885
- f"supports pairs {valid_pairs}.",
989
+ f"input port number {po_in.number} out-of-bounds for module, "
990
+ f"must be between [0, {constraint.n_ch_in}).",
886
991
  )
887
992
  return issues
@@ -61,7 +61,6 @@ class DeviceSummary(BaseModel):
61
61
  provider: str
62
62
  updated_at: ISO8601DatetimeUTCLike
63
63
  created_at: ISO8601DatetimeUTCLike
64
- copied_from: DeviceName | None = None
65
64
 
66
65
  def __str__(self):
67
66
  return f'DeviceSummary(name="{self.name}", id="{self.id}")'
@@ -142,6 +142,14 @@ class Coupler(Component[Literal["tunable"]]):
142
142
  default=ComponentParameter(value=0.0),
143
143
  json_schema_extra={"display": {"label": "bias_period", "unit": "V", "scale": 1}},
144
144
  )
145
+ freq_01: FloatComponentParameter = Field(
146
+ default=ComponentParameter(value=(0.0)),
147
+ json_schema_extra={"display": {"label": "freq_01", "unit": "MHz", "scale": 1e-6}},
148
+ )
149
+ anharm: FloatComponentParameter = Field(
150
+ default=ComponentParameter(value=0.0),
151
+ json_schema_extra={"display": {"label": "anharm", "unit": "MHz", "scale": 1e-6}},
152
+ )
145
153
 
146
154
 
147
155
  class Port(Component[Literal["drive", "readout", "flux"]]):
@@ -151,10 +159,19 @@ class Port(Component[Literal["drive", "readout", "flux"]]):
151
159
  )
152
160
 
153
161
 
162
+ # TODO: [SCUP-2169] Rename Filter to PurcellFilter
154
163
  class Filter(Component[Literal["filter"]]):
155
164
  dtype: Literal["filter"] = "filter"
156
165
  traits: list = Field(default=[])
157
166
 
167
+ # TODO: [SCUP-2167] Move resonator filter parameters to Filter/PurcellFilter class
168
+ # TODO: Use better name for this field. "Center Frequency"?
169
+ # TODO: Handle the same rename in the SDK
170
+ frequency: FloatComponentParameter = Field(
171
+ default=ComponentParameter(value=0.0),
172
+ json_schema_extra={"display": {"label": "frequency", "unit": "MHz", "scale": 1}},
173
+ )
174
+
158
175
 
159
176
  class Feedline(Component[Literal["feedline"]]):
160
177
  dtype: Literal["feedline"] = "feedline"
@@ -201,6 +218,7 @@ class ProcessorTemplate(BaseModel):
201
218
 
202
219
 
203
220
  class SuperconductingProcessorTemplate(BaseModel):
221
+ qpu_model: str
204
222
  build: list[TemplateParam]
205
223
  templates: dict[str, ProcessorTemplate]
206
224
  device_parameters: dict[str, SuperconductingComponentType] = Field(
@@ -209,12 +227,14 @@ class SuperconductingProcessorTemplate(BaseModel):
209
227
 
210
228
 
211
229
  class SuperconductingProcessor(BaseModel):
230
+ # TODO: Fast5, remove default after SCUP-2205
231
+ qpu_model: str = Field(default="quantware-soprano-a")
212
232
  nodes: dict[ComponentRef, SuperconductingComponentType]
213
233
  edges: list[Edge]
214
234
 
215
235
  @staticmethod
216
236
  def from_template(template: SuperconductingProcessorTemplate) -> "SuperconductingProcessor":
217
- _qpu = SuperconductingProcessor(nodes={}, edges=[])
237
+ _qpu = SuperconductingProcessor(qpu_model=template.qpu_model, nodes={}, edges=[])
218
238
  for build in template.build:
219
239
  qpu_template = template.templates[build.template]
220
240
  for idx, subs in build.vars.items():
@@ -45,7 +45,7 @@ class Chi01Scan(Experiment):
45
45
  measure_waveform : ConstantWaveform or None, optional
46
46
  The waveform to use for the measurement pulse.
47
47
  Defaults to the measurement defcal.
48
- run_mixer_calibration: bool
48
+ run_mixer_calibration: bool, optional
49
49
  Whether to run mixer calibrations before running a program. Defaults to False.
50
50
  update : "auto" or "off" or "prompt", optional
51
51
  How the device should be updated after an experiment run. Defaults to auto.
@@ -0,0 +1,65 @@
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
+ from typing import Literal
15
+
16
+ from pydantic import PrivateAttr
17
+
18
+ from .common import Experiment, LinspaceIterable, RangeIterable
19
+
20
+
21
+ class CZSpectroscopyByBias(Experiment):
22
+ """
23
+ Parameters for running a CZ spectroscopy by bias experiment.
24
+
25
+ Attributes
26
+ ----------
27
+ control_transmon : str
28
+ The reference for the transmon to use as control.
29
+ target_transmon : str
30
+ The reference for the transmon to use as target.
31
+ vps : LinspaceIterable
32
+ The voltage points to sample, in volts.
33
+ coupler_flux_vp : float
34
+ The flux voltage point for the coupler, in volts.
35
+ durations_ns : RangeIterable, optional
36
+ The pulse durations to sample, in nanoseconds.
37
+ Defaults to RangeIterable(start=16, stop=200, step=8).
38
+ prep_padding_ns : int, optional
39
+ The padding to apply before the CZ pulse, in nanoseconds.
40
+ Defaults to 16 ns.
41
+ measurement_padding_ns : int, optional
42
+ The padding to apply after the CZ pulse, in nanoseconds.
43
+ Defaults to 16 ns.
44
+ recycle_delay_ns : float, optional
45
+ The delay between consecutive shots, in nanoseconds.
46
+ Defaults to 500,000 ns.
47
+ shot_count : int, optional
48
+ The number of shots to take.
49
+ Defaults to 200.
50
+ update : "auto" or "off" or "prompt", optional
51
+ How the device should be updated after an experiment run. Defaults to auto.
52
+ """
53
+
54
+ _experiment_name: str = PrivateAttr("cz_spectroscopy_by_bias")
55
+
56
+ control_transmon: str
57
+ target_transmon: str
58
+ vps: LinspaceIterable
59
+ coupler_flux_vp: float
60
+ durations_ns: RangeIterable = RangeIterable(start=16, stop=200, step=8)
61
+ prep_padding_ns: int = 16
62
+ measurement_padding_ns: int = 16
63
+ recycle_delay_ns: int = 500_000
64
+ shot_count: int = 200
65
+ update: Literal["auto", "off", "prompt"] = "auto"
@@ -27,7 +27,7 @@ class DragLeakageCalibration(Experiment):
27
27
  ----------
28
28
  transmon : str
29
29
  The reference for the transmon to target.
30
- recycle_delay_ns : int
30
+ recycle_delay_ns : int, optional
31
31
  The delay between consecutive shots, in nanoseconds. Defaults to 10,000 ns.
32
32
  shot_count : int, optional
33
33
  The number of shots to take. Defaults to 1,000.
@@ -40,8 +40,6 @@ class DragLeakageCalibration(Experiment):
40
40
  anharmonicity : float or None, optional
41
41
  The anharmonicity of the transmon, in Hz.
42
42
  Defaults to None, in which case the anharmonicity of the transmon will be used.
43
- drag_guess : float
44
- Initial guess for the DRAG parameter.
45
43
  update : "auto" or "off" or "prompt", optional
46
44
  How the device should be updated after an experiment run. Defaults to auto.
47
45
  """
@@ -55,7 +53,6 @@ class DragLeakageCalibration(Experiment):
55
53
  repetitions: list[int]
56
54
  gate: Literal["x", "sx"]
57
55
  anharmonicity: float | None = None
58
- drag_guess: float
59
56
  update: Literal["auto", "off", "prompt"] = "auto"
60
57
 
61
58
  @field_validator("alphas")
@@ -30,12 +30,13 @@ class FineAmplitudeCalibration(Experiment):
30
30
  The reference for the transmon to target.
31
31
  gate : Literal["sx", "x"]
32
32
  The gate to be calibrated.
33
- repetitions : list[int]
33
+ repetitions : list[int], optional
34
34
  List of repetition counts for the calibration experiment.
35
- recycle_delay_ns : int
35
+ Defaults to every fourth number from 0 to 100.
36
+ recycle_delay_ns : int, optional
36
37
  The delay between consecutive shots, in nanoseconds. Defaults to 10,000 ns.
37
38
  shot_count : int, optional
38
- The number of shots to take. Defaults to 1000.
39
+ The number of shots to take. Defaults to 1,000.
39
40
  measure_waveform : ConstantWaveform or None, optional
40
41
  The waveform to use for the measurement pulse.
41
42
  Defaults to the measurement defcal.
@@ -39,16 +39,16 @@ class PowerRabi(Experiment):
39
39
  If None, a default scan will be used.
40
40
  drive_waveform : Waveform
41
41
  The waveform to use for the drive pulse.
42
- recycle_delay_ns : int
42
+ recycle_delay_ns : int, optional
43
43
  The delay between consecutive shots, in nanoseconds. Defaults to 200,000 ns.
44
44
  shot_count : int, optional
45
45
  The number of shots to take. Defaults to 400.
46
46
  pulse_vp : float, optional
47
- The voltage per pulse, in Volts. Defaults to the amplitude of the X gate.
47
+ The voltage per pulse, in volts. Defaults to the amplitude of the X gate.
48
48
  measure_waveform : ConstantWaveform or None, optional
49
49
  The waveform to use for the measurement pulse.
50
50
  Defaults to the measurement defcal.
51
- run_mixer_calibration: bool
51
+ run_mixer_calibration: bool, optional
52
52
  Whether to run mixer calibrations before running a program. Defaults to False.
53
53
  update : "auto" or "off" or "prompt", optional
54
54
  How the device should be updated after an experiment run. Defaults to auto.