boulder-opal-scale-up-sdk 1.0.0__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 (50) hide show
  1. boulder_opal_scale_up_sdk-1.0.0.dist-info/METADATA +38 -0
  2. boulder_opal_scale_up_sdk-1.0.0.dist-info/RECORD +50 -0
  3. boulder_opal_scale_up_sdk-1.0.0.dist-info/WHEEL +4 -0
  4. boulderopalscaleupsdk/__init__.py +14 -0
  5. boulderopalscaleupsdk/agent/__init__.py +29 -0
  6. boulderopalscaleupsdk/agent/worker.py +244 -0
  7. boulderopalscaleupsdk/common/__init__.py +12 -0
  8. boulderopalscaleupsdk/common/dtypes.py +353 -0
  9. boulderopalscaleupsdk/common/typeclasses.py +85 -0
  10. boulderopalscaleupsdk/device/__init__.py +16 -0
  11. boulderopalscaleupsdk/device/common.py +58 -0
  12. boulderopalscaleupsdk/device/config_loader.py +88 -0
  13. boulderopalscaleupsdk/device/controller/__init__.py +32 -0
  14. boulderopalscaleupsdk/device/controller/base.py +18 -0
  15. boulderopalscaleupsdk/device/controller/qblox.py +664 -0
  16. boulderopalscaleupsdk/device/controller/quantum_machines.py +139 -0
  17. boulderopalscaleupsdk/device/device.py +35 -0
  18. boulderopalscaleupsdk/device/processor/__init__.py +23 -0
  19. boulderopalscaleupsdk/device/processor/common.py +148 -0
  20. boulderopalscaleupsdk/device/processor/superconducting_processor.py +291 -0
  21. boulderopalscaleupsdk/experiments/__init__.py +44 -0
  22. boulderopalscaleupsdk/experiments/common.py +96 -0
  23. boulderopalscaleupsdk/experiments/power_rabi.py +60 -0
  24. boulderopalscaleupsdk/experiments/ramsey.py +55 -0
  25. boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +64 -0
  26. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +76 -0
  27. boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +64 -0
  28. boulderopalscaleupsdk/grpc_interceptors/__init__.py +16 -0
  29. boulderopalscaleupsdk/grpc_interceptors/auth.py +101 -0
  30. boulderopalscaleupsdk/plotting/__init__.py +24 -0
  31. boulderopalscaleupsdk/plotting/dtypes.py +221 -0
  32. boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +48 -0
  33. boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +53 -0
  34. boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +138 -0
  35. boulderopalscaleupsdk/protobuf/v1/device_pb2.py +71 -0
  36. boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +110 -0
  37. boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +274 -0
  38. boulderopalscaleupsdk/protobuf/v1/task_pb2.py +53 -0
  39. boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +118 -0
  40. boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +119 -0
  41. boulderopalscaleupsdk/py.typed +0 -0
  42. boulderopalscaleupsdk/routines/__init__.py +9 -0
  43. boulderopalscaleupsdk/routines/common.py +10 -0
  44. boulderopalscaleupsdk/routines/resonator_mapping.py +13 -0
  45. boulderopalscaleupsdk/third_party/__init__.py +14 -0
  46. boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +51 -0
  47. boulderopalscaleupsdk/third_party/quantum_machines/config.py +597 -0
  48. boulderopalscaleupsdk/third_party/quantum_machines/constants.py +20 -0
  49. boulderopalscaleupsdk/utils/__init__.py +12 -0
  50. boulderopalscaleupsdk/utils/serial_utils.py +62 -0
@@ -0,0 +1,51 @@
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
+ # pyright: reportPrivateImportUsage=false
15
+ """qm-qua imports.
16
+
17
+ This module standardizes all the qm-qua imports across the various versions we will
18
+ support.
19
+ """
20
+
21
+ __all__ = [
22
+ "Constants",
23
+ "QuaExpression",
24
+ "QuaProgram",
25
+ "version",
26
+ ]
27
+
28
+
29
+ import importlib.metadata
30
+
31
+ from packaging.version import Version
32
+
33
+ version = Version(importlib.metadata.version("qm-qua"))
34
+ if version >= Version("1.2.0"):
35
+ from qm.api.models.capabilities import OPX_FEM_IDX
36
+ from qm.program import Program as QuaProgram
37
+ from qm.qua._expressions import QuaExpression
38
+ else:
39
+ from qm.qua import Program as QuaProgram # type: ignore[attr-defined,no-redef]
40
+ from qm.qua._dsl import ( # type: ignore[attr-defined,no-redef]
41
+ _Expression as QuaExpression, # pyright: ignore[reportAttributeAccessIssue]
42
+ )
43
+
44
+ OPX_FEM_IDX = None # type: ignore[assignment]
45
+
46
+
47
+ class Constants:
48
+ """QM-Qua constants."""
49
+
50
+ opx_fem_idx: int | None = OPX_FEM_IDX
51
+ """The default FEM port for OPX. Only available for >=1.2.0"""
@@ -0,0 +1,597 @@
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
+ """Qua Configuration.
15
+
16
+ Root model & managing qua versions
17
+ ----------------------------------
18
+ A `QuaConfig` Pydantic RootModel definition should be the singular entrypoint for
19
+ parsing and validating Qua configuration dictionaries. This will use Pydantic's
20
+ discriminated union feature to automatically resolve the correct configuration version
21
+ as a function of the `version` string field.
22
+
23
+ See: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
24
+
25
+ Adding a new version
26
+ --------------------
27
+ To add a new configuration version:
28
+
29
+ 1. If `_BaseQuaConfig` has fields NOT present in your new version, then update
30
+ `_BaseQuaConfig` to ensure it only contains common fields across all versions.
31
+ Correspondingly, update all existing subclasses of `_BaseQuaConfig` to explicitly
32
+ declare the removed field.
33
+
34
+ 2. Subclass `_BaseQuaConfig` to create your new version.
35
+
36
+ * The class name should be `_QuaConfig<VersionTagSlug>`
37
+ * The class must have a PrivateAttr field `_qm_version_spec: str` that follows PyPA
38
+ version specifiers (see references).
39
+
40
+ Note: the overridden fields will need to use `# type: ignore[assignment]` as MyPy will
41
+ complain about mismatched types from the base class.
42
+
43
+ Type conventions
44
+ ----------------
45
+ On using Generics for standard collections defined in `typing` module...
46
+
47
+ * As of Python 3.9 / PEP 585, the standard collection now implement generics,
48
+ removing the necessity of a parallel type hierarchy in the `typing` modules.
49
+ See: https://peps.python.org/pep-0585
50
+ * qm-qua still uses the alternative type hierarchy for its typing; however since we
51
+ do not intend to support Python <=3.9, this is not necessary for us.
52
+ * Hence, we will override their typing with the standard collection
53
+ (e.g. `Dict` -> `dict`).
54
+
55
+ On `typing.Mapping` vs `dict`...
56
+
57
+ * qm-qua 1.2.1 changes `typing.Dict` typing to `typing.Mapping` typing, the latter
58
+ being a more abstract/generic type that (only) defines the `__getitem__`,
59
+ `__len__`, and `__iter__` magic methods. Further, `typing.Mapping` is covariant
60
+ whilst `dict` is invariant.
61
+ * We should preference `typing.MutableMapping` wherever possible, since we want to
62
+ the broad support of `typing.Mapping`, but need to mutate the config as we build
63
+ programs.
64
+ * A type alias is added here `Mapping = typing.MutableMapping`
65
+
66
+ References
67
+ ----------
68
+ For field enumeration:
69
+
70
+ * In `qm-qua` module: `qm.type_hinting.config_types`
71
+ * Configuration API: https://docs.quantum-machines.co/1.2.1/assets/qua_config.html
72
+
73
+ For validation details:
74
+
75
+ * In `qm-qua` module: `qm.program._qua_config_schema`
76
+ * In `qm-qua` module: `qm.program._qua_config_to_pb`
77
+
78
+ For information about PyPA's version specifiers:
79
+
80
+ * https://packaging.python.org/en/latest/specifications/version-specifiers/#id5
81
+ """
82
+
83
+ # TODO: Migrate more validations from qm-qua.
84
+
85
+ # ruff: noqa: D101, UP007, N815, E741
86
+ from collections.abc import MutableMapping
87
+ from typing import Annotated, Any, Literal, TypeVar, Union
88
+
89
+ from packaging.specifiers import SpecifierSet
90
+ from packaging.version import Version
91
+ from pydantic import (
92
+ BaseModel,
93
+ BeforeValidator,
94
+ Discriminator,
95
+ Field,
96
+ PrivateAttr,
97
+ RootModel,
98
+ Tag,
99
+ field_validator,
100
+ model_validator,
101
+ )
102
+ from pydantic_settings import BaseSettings, SettingsConfigDict
103
+
104
+ from boulderopalscaleupsdk.common.dtypes import Self
105
+
106
+ Mapping = MutableMapping
107
+ Number = Union[int, float]
108
+
109
+ T = TypeVar("T", bound=BaseModel)
110
+
111
+
112
+ class Constants(BaseSettings):
113
+ # TODO: Revise how we propagate defaults
114
+ # We don't want to manage environments and environment overrides, esp since
115
+ # this will limit our deployment strategy. Consider a approach where default
116
+ # values are added to the requests schema, and enriched for all inbound
117
+ # requests as a function of customer. Roughly, something like this:
118
+ # Controller(customer) --> connect_to_processor(**customer_defaults) --> ...
119
+
120
+ model_config = SettingsConfigDict(
121
+ env_prefix="QCSU_QM_QUA_",
122
+ )
123
+ octave_n_rf_out: int = 5
124
+ octave_default_lo_freq: float = 4e9
125
+
126
+
127
+ CONST = Constants()
128
+
129
+
130
+ StandardPort = tuple[str, int, int]
131
+ PortReferenceType = Union[tuple[str, int], StandardPort]
132
+
133
+
134
+ class AnalogOutputFilterConfigType(BaseModel):
135
+ feedforward: list[float] = []
136
+ feedback: list[float] = []
137
+
138
+
139
+ def ensure_int(value: Any) -> Any:
140
+ match value:
141
+ case int():
142
+ return value
143
+ case str():
144
+ return int(value)
145
+ case _:
146
+ raise ValueError("Field must be convertible to int.")
147
+
148
+
149
+ IntLike = Annotated[int, BeforeValidator(ensure_int)]
150
+
151
+
152
+ class AnalogOutputPortConfigType(BaseModel):
153
+ offset: Number | None = None
154
+ filter: AnalogOutputFilterConfigType | None = None
155
+ delay: int | None = None
156
+ crosstalk: Mapping[IntLike, Number] = {}
157
+ shareable: bool | None = None
158
+
159
+
160
+ class AnalogInputPortConfigType(BaseModel):
161
+ offset: Number | None = None
162
+ gain_db: int | None = None
163
+ shareable: bool | None = None
164
+ sampling_rate: float | None = None
165
+
166
+
167
+ class DigitalOutputPortConfigType(BaseModel):
168
+ shareable: bool | None = None
169
+ inverted: bool | None = None
170
+ level: Literal["TTL", "LVTTL"] | None = None
171
+
172
+
173
+ class DigitalInputPortConfigType(BaseModel):
174
+ shareable: bool | None = None
175
+ deadtime: int | None = None
176
+ polarity: Literal["RISING", "FALLING"] | None = None
177
+ threshold: Number | None = None
178
+
179
+
180
+ class AnalogOutputPortConfigTypeOctoDac(BaseModel):
181
+ offset: Number | None = None
182
+ filter: AnalogOutputFilterConfigType | None = None
183
+ delay: int | None = None
184
+ crosstalk: Mapping[int, Number] = {}
185
+ shareable: bool | None = None
186
+ connectivity: tuple[str, str] | None = None
187
+ sampling_rate: float | None = None
188
+ upsampling_mode: Literal["mw", "pulse"] | None = None
189
+ output_mode: Literal["direct", "amplified"] | None = None
190
+
191
+
192
+ class LfFemConfigType(BaseModel):
193
+ type: Literal["LF"] | None = None
194
+ analog_outputs: Mapping[int | str, AnalogOutputPortConfigTypeOctoDac] = {}
195
+ analog_inputs: Mapping[int | str, AnalogInputPortConfigType] = {}
196
+ digital_outputs: Mapping[int | str, DigitalOutputPortConfigType] = {}
197
+ digital_inputs: Mapping[int | str, DigitalInputPortConfigType] = {}
198
+
199
+
200
+ Band = Literal[1, 2, 3]
201
+
202
+
203
+ class MwFemAnalogInputPortConfigType(BaseModel):
204
+ sampling_rate: float | None = None
205
+ gain_db: int | None = None
206
+ shareable: bool | None = None
207
+ band: Band | None = None
208
+ downconverter_frequency: float | None = None
209
+
210
+
211
+ class MwUpconverterConfigType(BaseModel):
212
+ frequency: float | None = None
213
+
214
+
215
+ class MwFemAnalogOutputPortConfigType(BaseModel):
216
+ sampling_rate: float | None = None
217
+ full_scale_power_dbm: int | None = None
218
+ band: Band | None = None
219
+ delay: int | None = None
220
+ shareable: bool | None = None
221
+ upconverters: Mapping[IntLike, MwUpconverterConfigType] = {}
222
+ upconverter_frequency: float | None = None
223
+
224
+
225
+ class MwFemConfigType(BaseModel):
226
+ type: Literal["MW"] | None = None
227
+ analog_outputs: Mapping[int | str, MwFemAnalogOutputPortConfigType] = {}
228
+ analog_inputs: Mapping[int | str, MwFemAnalogInputPortConfigType] = {}
229
+ digital_outputs: Mapping[int | str, DigitalOutputPortConfigType] = {}
230
+ digital_inputs: Mapping[int | str, DigitalInputPortConfigType] = {}
231
+
232
+
233
+ class ControllerConfigType(BaseModel):
234
+ type: Literal["opx", "opx1"] | None = None
235
+ analog_outputs: Mapping[int | str, AnalogOutputPortConfigType] = {}
236
+ analog_inputs: Mapping[int | str, AnalogInputPortConfigType] = {}
237
+ digital_outputs: Mapping[int | str, DigitalOutputPortConfigType] = {}
238
+ digital_inputs: Mapping[int | str, DigitalInputPortConfigType] = {}
239
+
240
+
241
+ class OctaveRFOutputConfigType(BaseModel):
242
+ LO_frequency: float = Field(default=CONST.octave_default_lo_freq, ge=2e9, le=18e9)
243
+ LO_source: Literal["internal", "external"] = "internal"
244
+ output_mode: Literal[
245
+ "always_on",
246
+ "always_off",
247
+ "triggered",
248
+ "triggered_reversed",
249
+ ] = "always_off"
250
+ gain: int | float = Field(default=0, ge=-20, le=20, multiple_of=0.5)
251
+ input_attenuators: Literal["ON", "OFF"] = "OFF"
252
+ I_connection: PortReferenceType | None = None
253
+ Q_connection: PortReferenceType | None = None
254
+
255
+
256
+ _RF_SOURCES = Literal[
257
+ "RF_in",
258
+ "loopback_1",
259
+ "loopback_2",
260
+ "loopback_3",
261
+ "loopback_4",
262
+ "loopback_5",
263
+ ]
264
+
265
+
266
+ class OctaveRFInputConfigType(BaseModel):
267
+ RF_source: _RF_SOURCES | None = None
268
+ LO_frequency: float | None = None
269
+ LO_source: Literal["internal", "external", "analyzer"] | None = None
270
+ IF_mode_I: Literal["direct", "mixer", "envelope", "off"] | None = None
271
+ IF_mode_Q: Literal["direct", "mixer", "envelope", "off"] | None = None
272
+
273
+
274
+ class OctaveSingleIfOutputConfigType(BaseModel):
275
+ port: PortReferenceType | None = None
276
+ name: str | None = None
277
+
278
+
279
+ class OctaveIfOutputsConfigType(BaseModel):
280
+ IF_out1: OctaveSingleIfOutputConfigType | None = None
281
+ IF_out2: OctaveSingleIfOutputConfigType | None = None
282
+
283
+
284
+ FEM_IDX = Literal[1, 2, 3, 4, 5, 6, 7, 8, "1", "2", "3", "4", "5", "6", "7", "8"]
285
+
286
+
287
+ class OPX1000ControllerConfigType(BaseModel):
288
+ type: Literal["opx1000"] | None = None
289
+ fems: Mapping[FEM_IDX, LfFemConfigType | MwFemConfigType] = {}
290
+
291
+
292
+ LoopbackType = tuple[
293
+ tuple[str, Literal["Synth1", "Synth2", "Synth3", "Synth4", "Synth5"]],
294
+ Literal["Dmd1LO", "Dmd2LO", "LO1", "LO2", "LO3", "LO4", "LO5"],
295
+ ]
296
+
297
+
298
+ class OctaveConfig(BaseModel):
299
+ """Octave configuration for qm-qua 1.1.7."""
300
+
301
+ RF_outputs: Mapping[IntLike, OctaveRFOutputConfigType] = {}
302
+ """
303
+ RF Outputs in Octave's up-converter chain.
304
+ OPX/AnalogOutput -> Octave/IFInput -> Octave/RFOutput -> Fridge.
305
+ """
306
+
307
+ RF_inputs: Mapping[IntLike, OctaveRFInputConfigType] = {}
308
+ """RF Inputs in Octave's down-converter chain. See IF_Outputs."""
309
+
310
+ IF_outputs: OctaveIfOutputsConfigType | None = None
311
+ """
312
+ IF Outputs in Octave's down-converter chain.
313
+ Fridge -> Octave/RFInput -> Octave/IFOutput -> OPX/AnalogInput
314
+ """
315
+
316
+ loopbacks: list[LoopbackType] = []
317
+ """
318
+ Loopbacks connected to Octave.
319
+ Each loopback is ((octave_name, octave_port), target_port).
320
+ """
321
+
322
+ connectivity: str | None = None
323
+ """
324
+ Default connectivity to OPX (either in host, or host,FEM_IDX format).
325
+ This cannot be set when RF_outputs I/Q connections are set.
326
+ """
327
+
328
+ @model_validator(mode="after")
329
+ def validate_connectivity(self) -> Self:
330
+ if self.connectivity is not None:
331
+ for output_num, output in self.RF_outputs.items():
332
+ if output.I_connection or output.Q_connection:
333
+ raise ValueError(
334
+ "Octave has ambiguous connectivity; "
335
+ f"both connectivity set and RF outputs set for {output_num}",
336
+ )
337
+ return self
338
+
339
+
340
+ class OctaveConfig121(OctaveConfig):
341
+ """Octave configuration for qm-qua 1.2.1."""
342
+
343
+ connectivity: Union[str, tuple[str, int]] | None = None # type: ignore[assignment]
344
+
345
+
346
+ class DigitalInputConfigType(BaseModel):
347
+ delay: int | None = None
348
+ buffer: int | None = None
349
+ port: PortReferenceType | None = None
350
+
351
+
352
+ class IntegrationWeightConfigType(BaseModel):
353
+ cosine: list[tuple[float, int]] | list[float] = []
354
+ sine: list[tuple[float, int]] | list[float] = []
355
+
356
+
357
+ class ConstantWaveFormConfigType(BaseModel):
358
+ type: Literal["constant"] | None = None
359
+ sample: float | None = None
360
+
361
+
362
+ class CompressedWaveFormConfigType(BaseModel):
363
+ type: str | None = None
364
+ samples: list[float] = []
365
+ sample_rate: float | None = None
366
+
367
+
368
+ class ArbitraryWaveFormConfigType(BaseModel):
369
+ type: Literal["arbitrary"] | None = None
370
+ samples: list[float] = []
371
+ max_allowed_error: float | None = None
372
+ sampling_rate: Number | None = None
373
+ is_overridable: bool | None = None
374
+
375
+
376
+ class DigitalWaveformConfigType(BaseModel):
377
+ samples: list[tuple[int, int]] = []
378
+
379
+
380
+ class MixerConfigType(BaseModel):
381
+ intermediate_frequency: float | None = None
382
+ lo_frequency: float | None = None
383
+ correction: tuple[Number, Number, Number, Number] | None = None
384
+
385
+
386
+ class PulseConfigType(BaseModel):
387
+ operation: Literal["measurement", "control"] | None = None
388
+ length: int | None = None
389
+ waveforms: Mapping[str, str] = {}
390
+ digital_marker: str | None = None
391
+ integration_weights: Mapping[str, str] = {}
392
+
393
+
394
+ class SingleInputConfigType(BaseModel):
395
+ port: PortReferenceType | None = None
396
+
397
+
398
+ class MwInputConfigType(BaseModel):
399
+ port: PortReferenceType | None = None
400
+ upconverter: int | None = None
401
+
402
+
403
+ class MwOutputConfigType(BaseModel):
404
+ port: PortReferenceType | None = None
405
+
406
+
407
+ class HoldOffsetConfigType(BaseModel):
408
+ duration: int | None = None
409
+
410
+
411
+ class StickyConfigType(BaseModel):
412
+ analog: bool | None = None
413
+ digital: bool | None = None
414
+ duration: int | None = None
415
+
416
+
417
+ class MixInputConfigType(BaseModel):
418
+ I: PortReferenceType | None = None
419
+ Q: PortReferenceType | None = None
420
+ mixer: str | None = None
421
+ lo_frequency: float | None = None
422
+
423
+
424
+ class InputCollectionConfigType(BaseModel):
425
+ inputs: Mapping[str, PortReferenceType] = {}
426
+
427
+
428
+ class OscillatorConfigType(BaseModel):
429
+ intermediate_frequency: float | None = None
430
+ mixer: str | None = None
431
+ lo_frequency: float | None = None
432
+
433
+
434
+ class OutputPulseParameterConfigType(BaseModel):
435
+ signalThreshold: int | None = None
436
+ signalPolarity: Literal["ABOVE", "ASCENDING", "BELOW", "DESCENDING"] | None = None
437
+ derivativeThreshold: int | None = None
438
+ derivativePolarity: Literal["ABOVE", "ASCENDING", "BELOW", "DESCENDING"] | None = None
439
+
440
+
441
+ class ElementConfig(BaseModel):
442
+ """ElementConfigType for qm-qua 1.1.7."""
443
+
444
+ intermediate_frequency: float | None = None
445
+ oscillator: str | None = None
446
+ measurement_qe: str | None = None
447
+ operations: Mapping[str, str] = {}
448
+ singleInput: SingleInputConfigType | None = None
449
+ mixInputs: MixInputConfigType | None = None
450
+ singleInputCollection: InputCollectionConfigType | None = None
451
+ multipleInputs: InputCollectionConfigType | None = None
452
+ time_of_flight: int | None = None
453
+ smearing: int | None = None
454
+ outputs: Mapping[str, PortReferenceType] = {}
455
+ digitalInputs: Mapping[str, DigitalInputConfigType] | None = None
456
+ digitalOutputs: Mapping[str, PortReferenceType] | None = None
457
+ outputPulseParameters: OutputPulseParameterConfigType | None = None
458
+ hold_offset: HoldOffsetConfigType | None = None
459
+ sticky: StickyConfigType | None = None
460
+ thread: str | None = None
461
+ RF_inputs: Mapping[str, tuple[str, int]] | None = None
462
+ RF_outputs: Mapping[str, tuple[str, int]] | None = None
463
+
464
+ @model_validator(mode="after")
465
+ def validator_oscillators(self) -> Self:
466
+ if self.intermediate_frequency and self.oscillator:
467
+ raise ValueError(
468
+ "Intermediate frequency and oscillator cannot be defined together.",
469
+ )
470
+ return self
471
+
472
+
473
+ class ElementConfig121(ElementConfig):
474
+ """ElementConfig for qm-qua 1.2.1."""
475
+
476
+ MWInput: MwInputConfigType | None = None
477
+ MWOutput: MwOutputConfigType | None = None
478
+
479
+ @model_validator(mode="after")
480
+ def validate_outputs(self) -> Self:
481
+ if self.RF_outputs:
482
+ if self.smearing is None:
483
+ raise ValueError("Element with output must have smearing defined.")
484
+ if self.time_of_flight is None:
485
+ raise ValueError(
486
+ "Element with output must have time_of_flight defined.",
487
+ )
488
+ else:
489
+ if self.smearing:
490
+ raise ValueError("smearing only for elements with outputs.")
491
+ if self.time_of_flight:
492
+ raise ValueError("time_of_flight only for elements with outputs.")
493
+
494
+ return self
495
+
496
+
497
+ class _BaseQuaConfig(BaseModel):
498
+ """
499
+ Base Qua configuration.
500
+
501
+ Based off 1.1.7; newer versions should shadow fields that need updating.
502
+ """
503
+
504
+ _qm_version_spec: str
505
+ """The version specification that this model support.
506
+ Uses PyPA version specifiers."""
507
+
508
+ qm_version: str = Field(exclude=True)
509
+ """The qm-qua package version used."""
510
+
511
+ version: str = "1"
512
+ """The configuration version. This is a field used in Qua's configuration API."""
513
+
514
+ oscillators: Mapping[str, OscillatorConfigType] = {}
515
+ """The oscillators used to drive the elements."""
516
+
517
+ elements: Mapping[str, ElementConfig] = {}
518
+ """Elements represents a controllable entity wired to a port on the controller."""
519
+
520
+ controllers: Mapping[str, ControllerConfigType | OPX1000ControllerConfigType] = {}
521
+ """The controllers."""
522
+
523
+ octaves: Mapping[str, OctaveConfig] = {}
524
+ """Any octaves in the stack."""
525
+
526
+ integration_weights: Mapping[str, IntegrationWeightConfigType] = {}
527
+ """The integration weight vectors used in the integration and demodulation of data
528
+ returning from a element."""
529
+
530
+ waveforms: Mapping[
531
+ str,
532
+ ArbitraryWaveFormConfigType | ConstantWaveFormConfigType | CompressedWaveFormConfigType,
533
+ ] = {}
534
+ """The analog waveforms sent to an element when a pulse is played."""
535
+
536
+ digital_waveforms: Mapping[str, DigitalWaveformConfigType] = {}
537
+ """The digital waveforms sent to an element when a pulse is played."""
538
+
539
+ pulses: Mapping[str, PulseConfigType] = {}
540
+ """The pulses to be played to the elements."""
541
+
542
+ mixers: Mapping[str, list[MixerConfigType]] = {}
543
+ """The IQ mixer calibration properties, used to post-shape the pulse to compensate
544
+ for imperfections in the mixers used for up-converting the analog waveforms."""
545
+
546
+ @field_validator("qm_version")
547
+ @classmethod
548
+ def validate_qm_version(cls, vs: str):
549
+ if Version(vs) not in SpecifierSet(cls._qm_version_spec.default): # type: ignore[attr-defined]
550
+ raise ValueError(f"qm-qua version {vs} not supported.")
551
+ return vs
552
+
553
+
554
+ class _QuaConfig117(_BaseQuaConfig):
555
+ _qm_version_spec: str = PrivateAttr("~=1.1.7")
556
+
557
+
558
+ class _QuaConfig121(_BaseQuaConfig):
559
+ _qm_version_spec: str = PrivateAttr("~=1.2.1")
560
+ elements: Mapping[str, ElementConfig121] = {} # type: ignore[assignment]
561
+ octaves: Mapping[str, OctaveConfig121] = {} # type: ignore[assignment]
562
+
563
+
564
+ SUPPORTED_VERSION_SPECS: dict[str, SpecifierSet] = {
565
+ # Nb. the `attr-defined` mypy flag is because Pydantic converts private attributes
566
+ # into pydantic.fields.ModelPrivateAttr, and the value is set in the default.
567
+ cls._qm_version_spec.default: SpecifierSet(cls._qm_version_spec.default) # type: ignore[attr-defined]
568
+ for cls in _BaseQuaConfig.__subclasses__()
569
+ }
570
+
571
+
572
+ def _get_version(data: dict | BaseModel):
573
+ """
574
+ Resolve the correct version Tag from the configuration data.
575
+
576
+ Configuration data version will be
577
+ """
578
+ version_str = (
579
+ data.qm_version if isinstance(data, BaseModel) else data.get("qm_version") # type: ignore[attr-defined]
580
+ )
581
+ if not version_str:
582
+ raise AttributeError("No version specified")
583
+
584
+ version = Version(version_str)
585
+
586
+ for spec_name, spec in SUPPORTED_VERSION_SPECS.items():
587
+ if version in spec:
588
+ return spec_name
589
+
590
+ raise ValueError(f"Version {version_str} not supported")
591
+
592
+
593
+ class QuaConfig(RootModel):
594
+ root: Annotated[
595
+ Annotated[_QuaConfig117, Tag("~=1.1.7")] | Annotated[_QuaConfig121, Tag("~=1.2.1")],
596
+ Discriminator(_get_version),
597
+ ]
@@ -0,0 +1,20 @@
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 boulderopalscaleupsdk.common.dtypes import Duration, TimeUnit
15
+
16
+ # See https://docs.quantum-machines.co/latest/docs/API_references/qua/dsl_main/?h=clock+cycle#qm.qua._dsl.wait
17
+ QUA_CLOCK_CYCLE = Duration(4, TimeUnit.NS)
18
+ MIN_TIME_OF_FLIGHT = Duration(24, TimeUnit.NS)
19
+
20
+ QUA_MAX_DELAY = Duration(2**31 - 1, TimeUnit.NS)
@@ -0,0 +1,12 @@
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.