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.
- boulder_opal_scale_up_sdk-1.0.0.dist-info/METADATA +38 -0
- boulder_opal_scale_up_sdk-1.0.0.dist-info/RECORD +50 -0
- boulder_opal_scale_up_sdk-1.0.0.dist-info/WHEEL +4 -0
- boulderopalscaleupsdk/__init__.py +14 -0
- boulderopalscaleupsdk/agent/__init__.py +29 -0
- boulderopalscaleupsdk/agent/worker.py +244 -0
- boulderopalscaleupsdk/common/__init__.py +12 -0
- boulderopalscaleupsdk/common/dtypes.py +353 -0
- boulderopalscaleupsdk/common/typeclasses.py +85 -0
- boulderopalscaleupsdk/device/__init__.py +16 -0
- boulderopalscaleupsdk/device/common.py +58 -0
- boulderopalscaleupsdk/device/config_loader.py +88 -0
- boulderopalscaleupsdk/device/controller/__init__.py +32 -0
- boulderopalscaleupsdk/device/controller/base.py +18 -0
- boulderopalscaleupsdk/device/controller/qblox.py +664 -0
- boulderopalscaleupsdk/device/controller/quantum_machines.py +139 -0
- boulderopalscaleupsdk/device/device.py +35 -0
- boulderopalscaleupsdk/device/processor/__init__.py +23 -0
- boulderopalscaleupsdk/device/processor/common.py +148 -0
- boulderopalscaleupsdk/device/processor/superconducting_processor.py +291 -0
- boulderopalscaleupsdk/experiments/__init__.py +44 -0
- boulderopalscaleupsdk/experiments/common.py +96 -0
- boulderopalscaleupsdk/experiments/power_rabi.py +60 -0
- boulderopalscaleupsdk/experiments/ramsey.py +55 -0
- boulderopalscaleupsdk/experiments/resonator_spectroscopy.py +64 -0
- boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_bias.py +76 -0
- boulderopalscaleupsdk/experiments/resonator_spectroscopy_by_power.py +64 -0
- boulderopalscaleupsdk/grpc_interceptors/__init__.py +16 -0
- boulderopalscaleupsdk/grpc_interceptors/auth.py +101 -0
- boulderopalscaleupsdk/plotting/__init__.py +24 -0
- boulderopalscaleupsdk/plotting/dtypes.py +221 -0
- boulderopalscaleupsdk/protobuf/v1/agent_pb2.py +48 -0
- boulderopalscaleupsdk/protobuf/v1/agent_pb2.pyi +53 -0
- boulderopalscaleupsdk/protobuf/v1/agent_pb2_grpc.py +138 -0
- boulderopalscaleupsdk/protobuf/v1/device_pb2.py +71 -0
- boulderopalscaleupsdk/protobuf/v1/device_pb2.pyi +110 -0
- boulderopalscaleupsdk/protobuf/v1/device_pb2_grpc.py +274 -0
- boulderopalscaleupsdk/protobuf/v1/task_pb2.py +53 -0
- boulderopalscaleupsdk/protobuf/v1/task_pb2.pyi +118 -0
- boulderopalscaleupsdk/protobuf/v1/task_pb2_grpc.py +119 -0
- boulderopalscaleupsdk/py.typed +0 -0
- boulderopalscaleupsdk/routines/__init__.py +9 -0
- boulderopalscaleupsdk/routines/common.py +10 -0
- boulderopalscaleupsdk/routines/resonator_mapping.py +13 -0
- boulderopalscaleupsdk/third_party/__init__.py +14 -0
- boulderopalscaleupsdk/third_party/quantum_machines/__init__.py +51 -0
- boulderopalscaleupsdk/third_party/quantum_machines/config.py +597 -0
- boulderopalscaleupsdk/third_party/quantum_machines/constants.py +20 -0
- boulderopalscaleupsdk/utils/__init__.py +12 -0
- boulderopalscaleupsdk/utils/serial_utils.py +62 -0
@@ -0,0 +1,664 @@
|
|
1
|
+
# Copyright 2025 Q-CTRL. All rights reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Q-CTRL Terms of service (the "License"). Unauthorized
|
4
|
+
# copying or use of this file, via any medium, is strictly prohibited.
|
5
|
+
# Proprietary and confidential. You may not use this file except in compliance
|
6
|
+
# with the License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# https://q-ctrl.com/terms
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS. See the
|
12
|
+
# License for the specific language.
|
13
|
+
|
14
|
+
"""
|
15
|
+
QBLOX Quantum Control Stack
|
16
|
+
"""
|
17
|
+
|
18
|
+
__all__ = (
|
19
|
+
"DEFAULT_MODULE_CONSTRAINTS",
|
20
|
+
"AcquisitionConfig",
|
21
|
+
"ChannelType",
|
22
|
+
"ComplexChannel",
|
23
|
+
"IndexedData",
|
24
|
+
"ModuleAddr",
|
25
|
+
"ModuleAddrType",
|
26
|
+
"ModuleConstraints",
|
27
|
+
"ModuleType",
|
28
|
+
"OutputAcquisition",
|
29
|
+
"OutputBinnedAcquisition",
|
30
|
+
"OutputBinnedAcquisitionIntegrationData",
|
31
|
+
"OutputIndexedAcquisition",
|
32
|
+
"OutputScopedAcquisition",
|
33
|
+
"OutputScopedAcquisitionData",
|
34
|
+
"OutputSequencerAcquisitions",
|
35
|
+
"PortAddr",
|
36
|
+
"PortAddrType",
|
37
|
+
"PreparedProgram",
|
38
|
+
"PreparedSequenceProgram",
|
39
|
+
"RealChannel",
|
40
|
+
"SequenceProgram",
|
41
|
+
"SequencerAddr",
|
42
|
+
"SequencerAddrType",
|
43
|
+
"validate_channel",
|
44
|
+
)
|
45
|
+
|
46
|
+
import dataclasses
|
47
|
+
import enum
|
48
|
+
import re
|
49
|
+
from dataclasses import dataclass
|
50
|
+
from typing import Annotated, Any, Literal, TypeVar
|
51
|
+
|
52
|
+
from pydantic import BaseModel, BeforeValidator, Field, PlainSerializer, model_validator
|
53
|
+
|
54
|
+
from boulderopalscaleupsdk.common.dtypes import Self
|
55
|
+
|
56
|
+
# ==================================================================================================
|
57
|
+
# Addressing
|
58
|
+
# ==================================================================================================
|
59
|
+
_RE_SEQUENCER_ADDR = re.compile(r"^(?P<cluster>[^:]+):(?P<slot>\d+):s(?P<num>\d+)$")
|
60
|
+
_RE_MODULE_ADDR = re.compile(r"^(?P<cluster>[^:]+):(?P<slot>\d+)$")
|
61
|
+
_RE_PORT_ADDR = re.compile(
|
62
|
+
r"^(?P<cluster>[^:]+):(?P<slot>\d+):p(?P<dir>(O|I))(?P<num>\d+)$",
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
class ModuleType(str, enum.Enum):
|
67
|
+
"""Enumeration of QBLOX modules."""
|
68
|
+
|
69
|
+
QCM = "QCM"
|
70
|
+
QRM = "QRM"
|
71
|
+
QCM_RF = "QCM_RF"
|
72
|
+
QRM_RF = "QRM_RF"
|
73
|
+
QTM = "QTM"
|
74
|
+
QDM = "QDM"
|
75
|
+
EOM = "EOM"
|
76
|
+
LINQ = "LINQ"
|
77
|
+
QRC = "QRC"
|
78
|
+
|
79
|
+
|
80
|
+
@dataclass(frozen=True, eq=True)
|
81
|
+
class ModuleAddr:
|
82
|
+
"""Address to a module in a QBLOX control stack."""
|
83
|
+
|
84
|
+
cluster: str
|
85
|
+
slot: int
|
86
|
+
|
87
|
+
def __str__(self) -> str:
|
88
|
+
"""Address as a string.
|
89
|
+
|
90
|
+
This is used for serialization/deserialization and must match the Regex pattern defined in
|
91
|
+
this module. See `_RE_MODULE_ADDR`
|
92
|
+
"""
|
93
|
+
return f"{self.cluster}:{self.slot}"
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def parse(cls, data: str) -> Self:
|
97
|
+
mch = _RE_MODULE_ADDR.match(data)
|
98
|
+
if mch is None:
|
99
|
+
raise ValueError("could not parse module address")
|
100
|
+
return cls(cluster=mch.group("cluster"), slot=int(mch.group("slot")))
|
101
|
+
|
102
|
+
|
103
|
+
@dataclass(frozen=True, eq=True)
|
104
|
+
class SequencerAddr:
|
105
|
+
"""Address to a sequencer (located on a specific module) in a QBLOX control stack."""
|
106
|
+
|
107
|
+
cluster: str
|
108
|
+
slot: int
|
109
|
+
number: int
|
110
|
+
|
111
|
+
@property
|
112
|
+
def module(self) -> ModuleAddr:
|
113
|
+
return ModuleAddr(self.cluster, self.slot)
|
114
|
+
|
115
|
+
def __str__(self) -> str:
|
116
|
+
"""Address as a string.
|
117
|
+
|
118
|
+
This is used for serialization/deserialization and must match the Regex pattern defined in
|
119
|
+
this module. See `_RE_SEQUENCER_ADDR`
|
120
|
+
"""
|
121
|
+
return f"{self.cluster}:{self.slot}:s{self.number}"
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
def parse(cls, data: str) -> Self:
|
125
|
+
mch = _RE_SEQUENCER_ADDR.match(data)
|
126
|
+
if mch is None:
|
127
|
+
raise ValueError("could not parse sequencer address")
|
128
|
+
return cls(
|
129
|
+
cluster=mch.group("cluster"),
|
130
|
+
slot=int(mch.group("slot")),
|
131
|
+
number=int(mch.group("num")),
|
132
|
+
)
|
133
|
+
|
134
|
+
|
135
|
+
@dataclass(frozen=True, eq=True)
|
136
|
+
class PortAddr:
|
137
|
+
"""Address to a hardware port (located on a specific module) in a QBLOX control stack."""
|
138
|
+
|
139
|
+
cluster: str
|
140
|
+
slot: int
|
141
|
+
direction: Literal["out", "in"]
|
142
|
+
number: int
|
143
|
+
|
144
|
+
@property
|
145
|
+
def module(self) -> ModuleAddr:
|
146
|
+
return ModuleAddr(self.cluster, self.slot)
|
147
|
+
|
148
|
+
def __str__(self) -> str:
|
149
|
+
"""Address as a string.
|
150
|
+
|
151
|
+
This is used for serialization/deserialization and must match the Regex pattern defined in
|
152
|
+
this module. See `_RE_PORT_ADDR`
|
153
|
+
"""
|
154
|
+
direction = "O" if self.direction == "out" else "I"
|
155
|
+
return f"{self.cluster}:{self.slot}:p{direction}{self.number}"
|
156
|
+
|
157
|
+
@classmethod
|
158
|
+
def parse(cls, data: str) -> Self:
|
159
|
+
mch = _RE_PORT_ADDR.match(data)
|
160
|
+
if mch is None:
|
161
|
+
raise ValueError("could not parse port address")
|
162
|
+
direction: Literal["out", "in"] = "out" if mch.group("dir") == "O" else "in"
|
163
|
+
return cls(
|
164
|
+
cluster=mch.group("cluster"),
|
165
|
+
slot=int(mch.group("slot")),
|
166
|
+
direction=direction,
|
167
|
+
number=int(mch.group("num")),
|
168
|
+
)
|
169
|
+
|
170
|
+
|
171
|
+
T = TypeVar("T", bound=ModuleAddr | SequencerAddr | PortAddr)
|
172
|
+
|
173
|
+
|
174
|
+
def _addr_validator(dtype: type[T]) -> BeforeValidator:
|
175
|
+
"""Return a Pydantic BeforeValidator to adapt address type with Pydantic."""
|
176
|
+
|
177
|
+
def _validator(obj: object):
|
178
|
+
if isinstance(obj, dtype): # Allow instantiation with Python object
|
179
|
+
return obj
|
180
|
+
if isinstance(obj, str): # Parse from JSON
|
181
|
+
return dtype.parse(obj)
|
182
|
+
raise TypeError(f"Invalid type {type(obj).__name__} for {type(dtype).__name__}")
|
183
|
+
|
184
|
+
return BeforeValidator(_validator)
|
185
|
+
|
186
|
+
|
187
|
+
# Annotated types with Pydantic validator and serializer.
|
188
|
+
ModuleAddrType = Annotated[ModuleAddr, _addr_validator(ModuleAddr), PlainSerializer(str)]
|
189
|
+
SequencerAddrType = Annotated[SequencerAddr, _addr_validator(SequencerAddr), PlainSerializer(str)]
|
190
|
+
PortAddrType = Annotated[PortAddr, _addr_validator(PortAddr), PlainSerializer(str)]
|
191
|
+
|
192
|
+
|
193
|
+
# ==================================================================================================
|
194
|
+
# Signalling Channels
|
195
|
+
# ==================================================================================================
|
196
|
+
class RealChannel(BaseModel):
|
197
|
+
"""A real channel targeting a single hardware port on a QBLOX module."""
|
198
|
+
|
199
|
+
mode: Literal["real"] = "real"
|
200
|
+
port: PortAddrType
|
201
|
+
|
202
|
+
@property
|
203
|
+
def module(self) -> ModuleAddr:
|
204
|
+
return ModuleAddr(self.port.cluster, self.port.slot)
|
205
|
+
|
206
|
+
@property
|
207
|
+
def direction(self) -> Literal["out", "in"]:
|
208
|
+
return self.port.direction
|
209
|
+
|
210
|
+
def __str__(self) -> str:
|
211
|
+
return f"{self.port!s}[R]"
|
212
|
+
|
213
|
+
|
214
|
+
class ComplexChannel(BaseModel):
|
215
|
+
"""A Complex channel targeting a single hardware port on a QBLOX module."""
|
216
|
+
|
217
|
+
mode: Literal["complex"] = "complex"
|
218
|
+
i_port: PortAddrType
|
219
|
+
q_port: PortAddrType
|
220
|
+
|
221
|
+
@property
|
222
|
+
def module(self) -> ModuleAddr:
|
223
|
+
return ModuleAddr(self.i_port.cluster, self.i_port.slot)
|
224
|
+
|
225
|
+
@property
|
226
|
+
def direction(self) -> Literal["out", "in"]:
|
227
|
+
return self.i_port.direction
|
228
|
+
|
229
|
+
def __str__(self) -> str:
|
230
|
+
return f"{self.i_port!s}_{self.q_port.number}[Z]"
|
231
|
+
|
232
|
+
@model_validator(mode="after")
|
233
|
+
def validate_i_q_ports(self) -> "ComplexChannel":
|
234
|
+
ii = self.i_port
|
235
|
+
qq = self.q_port
|
236
|
+
if ii.cluster != qq.cluster or ii.slot != qq.slot:
|
237
|
+
raise ValueError("I and Q ports must be on the same cluster+module")
|
238
|
+
if ii.direction != qq.direction:
|
239
|
+
raise ValueError("I and Q ports must be the same direction")
|
240
|
+
return self
|
241
|
+
|
242
|
+
|
243
|
+
ChannelType = RealChannel | ComplexChannel
|
244
|
+
|
245
|
+
|
246
|
+
# ==================================================================================================
|
247
|
+
# Controller information
|
248
|
+
# ==================================================================================================
|
249
|
+
class ElementConnection(BaseModel): # pragma: no cover
|
250
|
+
"""
|
251
|
+
The connections involved for a control element.
|
252
|
+
|
253
|
+
Attributes
|
254
|
+
----------
|
255
|
+
ch_out: ChannelType
|
256
|
+
The output channel that will signal towards the control element.
|
257
|
+
ch_in: ChannelType, optional
|
258
|
+
The input channel from which signals will be acquired from the element. This is optional, as
|
259
|
+
not all modules support acquisitions. If an input channel is specified, it must be located
|
260
|
+
on the same module as the output channel.
|
261
|
+
|
262
|
+
Notes
|
263
|
+
-----
|
264
|
+
The direction of channels is referenced against the QBLOX control stack. I.e. the "out"
|
265
|
+
direction is outwards from the control stack. The following diagram depicts a simple setup with
|
266
|
+
the arrows indicating a control channel.
|
267
|
+
|
268
|
+
┌────────┐ ┌────────────┐
|
269
|
+
│ │─── out ──►│Element: xy1│
|
270
|
+
│ QBLOX │ └────────────┘
|
271
|
+
│ Stack │ ┌────────────┐
|
272
|
+
│ │─── out ──►│Element: ro1│
|
273
|
+
│ │◄── in ────│ │
|
274
|
+
└────────┘ └────────────┘
|
275
|
+
"""
|
276
|
+
|
277
|
+
ch_out: ChannelType
|
278
|
+
ch_in: ChannelType | None = None
|
279
|
+
|
280
|
+
@model_validator(mode="after")
|
281
|
+
def validate_channels(self) -> "ElementConnection":
|
282
|
+
if self.ch_in is not None and self.ch_in.module != self.ch_out.module:
|
283
|
+
raise ValueError("I/O channels for an element must be on the same module.")
|
284
|
+
return self
|
285
|
+
|
286
|
+
|
287
|
+
class ControllerInfo(BaseModel): # pragma: no cover
|
288
|
+
"""
|
289
|
+
Controller information needed for program compilation and control.
|
290
|
+
|
291
|
+
Attributes
|
292
|
+
----------
|
293
|
+
modules: dict[ModuleAddrType, ModuleType]
|
294
|
+
The modules connected to the QBLOX stack.
|
295
|
+
elements: dict[str, ElementConnection]
|
296
|
+
The addressable control elements for the stack.
|
297
|
+
"""
|
298
|
+
|
299
|
+
modules: dict[ModuleAddrType, ModuleType]
|
300
|
+
elements: dict[str, ElementConnection]
|
301
|
+
|
302
|
+
|
303
|
+
# ==================================================================================================
|
304
|
+
# Instrument management
|
305
|
+
# ==================================================================================================
|
306
|
+
class SequencerParams(BaseModel):
|
307
|
+
nco_freq: float | None = Field(default=None, gt=0)
|
308
|
+
gain_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
|
309
|
+
offset_awg_path0: float | None = Field(default=None, ge=-1.0, le=1.0)
|
310
|
+
gain_awg_path1: float | None = Field(default=None, ge=-1.0, le=1.0)
|
311
|
+
offset_awg_path1: float | None = Field(default=None, ge=-1.0, le=1.0)
|
312
|
+
marker_ovr_en: bool | None = Field(default=None)
|
313
|
+
marker_ovr_value: int | None = Field(default=None)
|
314
|
+
mod_en_awg: bool | None = Field(default=None)
|
315
|
+
demod_en_acq: bool | None = Field(default=None)
|
316
|
+
sync_en: bool | None = Field(default=True)
|
317
|
+
nco_prop_delay_comp_en: bool | None = Field(default=True)
|
318
|
+
integration_length_acq: int | None = Field(default=None)
|
319
|
+
|
320
|
+
|
321
|
+
class ModuleParams(BaseModel):
|
322
|
+
out0_in0_lo_freq: float | None = Field(default=None, gt=0)
|
323
|
+
out0_in0_lo_en: bool | None = Field(default=None)
|
324
|
+
out0_lo_freq: float | None = Field(default=None, gt=0)
|
325
|
+
out0_lo_en: bool | None = Field(default=None)
|
326
|
+
|
327
|
+
|
328
|
+
# ==================================================================================================
|
329
|
+
# Programs
|
330
|
+
# ==================================================================================================
|
331
|
+
class IndexedData(BaseModel):
|
332
|
+
"""Used for sequence waveforms and weights."""
|
333
|
+
|
334
|
+
data: list[float]
|
335
|
+
index: int
|
336
|
+
|
337
|
+
def data_equal(self, samples: list[float]) -> bool:
|
338
|
+
"""Whether the samples provided match the data in this object."""
|
339
|
+
if len(samples) != len(self.data):
|
340
|
+
return False
|
341
|
+
return all(
|
342
|
+
sample_1 == sample_2 for sample_1, sample_2 in zip(samples, self.data, strict=False)
|
343
|
+
)
|
344
|
+
|
345
|
+
|
346
|
+
class AcquisitionConfig(BaseModel):
|
347
|
+
"""Acquisition configuration for Q1ASM programs."""
|
348
|
+
|
349
|
+
num_bins: int
|
350
|
+
index: int
|
351
|
+
|
352
|
+
|
353
|
+
class SequenceProgram(BaseModel):
|
354
|
+
"""A Q1 Sequence Program."""
|
355
|
+
|
356
|
+
program: str
|
357
|
+
waveforms: dict[str, IndexedData] = {}
|
358
|
+
weights: dict[str, IndexedData] = {}
|
359
|
+
acquisitions: dict[str, AcquisitionConfig] = {}
|
360
|
+
acquisition_scopes: list[str] = []
|
361
|
+
|
362
|
+
def sequence_data(self) -> dict[str, Any]:
|
363
|
+
return self.model_dump(include={"program", "waveforms", "weights", "acquisitions"})
|
364
|
+
|
365
|
+
def dump(self) -> bytes:
|
366
|
+
return self.model_dump_json().encode("utf-8")
|
367
|
+
|
368
|
+
|
369
|
+
class PreparedSequenceProgram(BaseModel): # pragma: no cover
|
370
|
+
"""A sequence program that is mapped to a specific module & sequencer."""
|
371
|
+
|
372
|
+
sequence_program: SequenceProgram
|
373
|
+
sequencer_number: int
|
374
|
+
ch_out: ChannelType
|
375
|
+
ch_in: ChannelType | None = None
|
376
|
+
sequencer_params: SequencerParams = SequencerParams()
|
377
|
+
|
378
|
+
@property
|
379
|
+
def sequencer_addr(self) -> SequencerAddr:
|
380
|
+
mod_addr = self.ch_out.module
|
381
|
+
return SequencerAddr(
|
382
|
+
cluster=mod_addr.cluster,
|
383
|
+
slot=mod_addr.slot,
|
384
|
+
number=self.sequencer_number,
|
385
|
+
)
|
386
|
+
|
387
|
+
|
388
|
+
class PreparedModule(BaseModel):
|
389
|
+
params: ModuleParams = ModuleParams()
|
390
|
+
|
391
|
+
|
392
|
+
class PreparedProgram(BaseModel):
|
393
|
+
"""A program representing a multi-element circuit."""
|
394
|
+
|
395
|
+
modules: dict[ModuleAddrType, PreparedModule] # The set of modules this program will target.
|
396
|
+
sequence_programs: dict[str, PreparedSequenceProgram] # The individual element programs.
|
397
|
+
|
398
|
+
def dump(self) -> bytes:
|
399
|
+
return self.model_dump_json().encode("utf-8")
|
400
|
+
|
401
|
+
|
402
|
+
# ==================================================================================================
|
403
|
+
# Results
|
404
|
+
# ==================================================================================================
|
405
|
+
class OutputScopedAcquisitionData(BaseModel): # pragma: no cover
|
406
|
+
"""
|
407
|
+
Scoped acquisition data for a single path in `OutputScopedAcquisition`.
|
408
|
+
|
409
|
+
This schema is defined by QBLOX.
|
410
|
+
"""
|
411
|
+
|
412
|
+
data: list[float]
|
413
|
+
out_of_range: bool = Field(validation_alias="out-of-range")
|
414
|
+
avg_cnt: int
|
415
|
+
|
416
|
+
|
417
|
+
class OutputScopedAcquisition(BaseModel): # pragma: no cover
|
418
|
+
"""
|
419
|
+
Scoped acquisition data for a single acquisition index in the SequenceProgram.
|
420
|
+
|
421
|
+
This schema is defined by QBLOX.
|
422
|
+
"""
|
423
|
+
|
424
|
+
path0: OutputScopedAcquisitionData
|
425
|
+
path1: OutputScopedAcquisitionData
|
426
|
+
|
427
|
+
|
428
|
+
class OutputBinnedAcquisitionIntegrationData(BaseModel): # pragma: no cover
|
429
|
+
"""
|
430
|
+
Binned values in `OutputBinnedAcquisition`.
|
431
|
+
|
432
|
+
This schema is defined by QBLOX.
|
433
|
+
"""
|
434
|
+
|
435
|
+
path0: list[float]
|
436
|
+
path1: list[float]
|
437
|
+
|
438
|
+
|
439
|
+
class OutputBinnedAcquisition(BaseModel): # pragma: no cover
|
440
|
+
"""
|
441
|
+
Binned acquisition data for a single acquisition index in the SequenceProgram.
|
442
|
+
|
443
|
+
This schema is defined by QBLOX.
|
444
|
+
"""
|
445
|
+
|
446
|
+
integration: OutputBinnedAcquisitionIntegrationData
|
447
|
+
threshold: list[float]
|
448
|
+
avg_cnt: list[int]
|
449
|
+
|
450
|
+
|
451
|
+
class OutputAcquisition(BaseModel): # pragma: no cover
|
452
|
+
"""
|
453
|
+
Acquisition data for a single acquisition index in the SequenceProgram.
|
454
|
+
|
455
|
+
Note, this type is wrapped by `OutputIndexedAcquisition`.
|
456
|
+
|
457
|
+
This schema is defined by QBLOX.
|
458
|
+
"""
|
459
|
+
|
460
|
+
scope: OutputScopedAcquisition
|
461
|
+
bins: OutputBinnedAcquisition
|
462
|
+
|
463
|
+
|
464
|
+
class OutputIndexedAcquisition(BaseModel): # pragma: no cover
|
465
|
+
"""
|
466
|
+
Acquisition data (wrapper) for a single acquisition index in the SequenceProgram.
|
467
|
+
|
468
|
+
This type simply wraps `OutputAcquisition` with an additional `index` attribute. The index in
|
469
|
+
`SequenceProgram.acquisitions[...].index` will correspond to `OutputIndexedAcquisition.index`.
|
470
|
+
|
471
|
+
Note, this type is used as the values in the `OutputSequencerAcquisitions` dict-type; the keys
|
472
|
+
will correspond to the acquisition name.
|
473
|
+
|
474
|
+
This schema is defined by QBLOX.
|
475
|
+
"""
|
476
|
+
|
477
|
+
index: int
|
478
|
+
acquisition: OutputAcquisition
|
479
|
+
|
480
|
+
|
481
|
+
# Results returned by a single sequencer.
|
482
|
+
# This schema is defined by QBLOX.
|
483
|
+
# Example result in JSON (redacted for brevity):
|
484
|
+
#
|
485
|
+
# // {
|
486
|
+
# // 'weighted': {
|
487
|
+
# // 'index': 0
|
488
|
+
# // 'acquisition': {
|
489
|
+
# // 'scope': {
|
490
|
+
# // 'path0': {
|
491
|
+
# // 'out_of_range': False,
|
492
|
+
# // 'avg_cnt': 0,
|
493
|
+
# // 'data': [...]
|
494
|
+
# // },
|
495
|
+
# // 'path1': {
|
496
|
+
# // 'out_of_range': False,
|
497
|
+
# // 'avg_cnt': 0,
|
498
|
+
# // 'data': [...]
|
499
|
+
# // }
|
500
|
+
# // },
|
501
|
+
# // 'bins': {
|
502
|
+
# // 'integration': {
|
503
|
+
# // 'path0': [10.0],
|
504
|
+
# // 'path1': [10.0],
|
505
|
+
# // },
|
506
|
+
# // 'threshold': [1.0],
|
507
|
+
# // 'avg_cnt': [1],
|
508
|
+
# // }
|
509
|
+
# // }
|
510
|
+
# // }
|
511
|
+
# // }
|
512
|
+
#
|
513
|
+
# This results must come from a SequenceProgram that defines
|
514
|
+
#
|
515
|
+
# // acquisitions = {
|
516
|
+
# // {'weighed': {'num_bins': 1, 'index': 0}}
|
517
|
+
# // }
|
518
|
+
OutputSequencerAcquisitions = dict[str, OutputIndexedAcquisition] # pragma: no cover
|
519
|
+
|
520
|
+
|
521
|
+
# ==================================================================================================
|
522
|
+
# Utilities
|
523
|
+
# ==================================================================================================
|
524
|
+
@dataclasses.dataclass
|
525
|
+
class ModuleConstraints:
|
526
|
+
"""Physical constraints of a module."""
|
527
|
+
|
528
|
+
n_sequencers: int
|
529
|
+
n_markers: int = 0
|
530
|
+
n_ch_out: int = 0
|
531
|
+
n_ch_in: int = 0
|
532
|
+
n_digital_io: int = 0
|
533
|
+
|
534
|
+
# TODO: Confirm if ordering is important.
|
535
|
+
ch_out_iq_pairs: list[set[int]] = dataclasses.field(default_factory=list)
|
536
|
+
ch_in_iq_pairs: list[set[int]] = dataclasses.field(default_factory=list)
|
537
|
+
|
538
|
+
|
539
|
+
# Default module constraints by module type.
|
540
|
+
DEFAULT_MODULE_CONSTRAINTS: dict[ModuleType, ModuleConstraints] = {
|
541
|
+
ModuleType.QCM: ModuleConstraints(
|
542
|
+
n_sequencers=6,
|
543
|
+
n_markers=4,
|
544
|
+
n_ch_out=4,
|
545
|
+
n_ch_in=0,
|
546
|
+
ch_out_iq_pairs=[{0, 1}, {2, 3}],
|
547
|
+
),
|
548
|
+
ModuleType.QCM_RF: ModuleConstraints(
|
549
|
+
n_sequencers=6,
|
550
|
+
n_markers=2,
|
551
|
+
n_ch_out=2,
|
552
|
+
n_ch_in=0,
|
553
|
+
),
|
554
|
+
ModuleType.QRM: ModuleConstraints(
|
555
|
+
n_sequencers=6,
|
556
|
+
n_markers=4,
|
557
|
+
n_ch_out=2,
|
558
|
+
n_ch_in=2,
|
559
|
+
ch_out_iq_pairs=[{0, 1}],
|
560
|
+
ch_in_iq_pairs=[{2, 3}],
|
561
|
+
),
|
562
|
+
ModuleType.QRM_RF: ModuleConstraints(
|
563
|
+
n_sequencers=6,
|
564
|
+
n_markers=2,
|
565
|
+
n_ch_out=1,
|
566
|
+
n_ch_in=1,
|
567
|
+
),
|
568
|
+
ModuleType.QRC: ModuleConstraints(
|
569
|
+
n_sequencers=12,
|
570
|
+
n_ch_out=6,
|
571
|
+
n_ch_in=2,
|
572
|
+
),
|
573
|
+
ModuleType.QTM: ModuleConstraints(
|
574
|
+
n_sequencers=8,
|
575
|
+
n_digital_io=8,
|
576
|
+
),
|
577
|
+
ModuleType.QDM: ModuleConstraints(n_sequencers=0),
|
578
|
+
ModuleType.EOM: ModuleConstraints(n_sequencers=0),
|
579
|
+
ModuleType.LINQ: ModuleConstraints(n_sequencers=0),
|
580
|
+
}
|
581
|
+
|
582
|
+
|
583
|
+
def validate_channel(ch: ChannelType, constraint: ModuleConstraints) -> list[str]:
|
584
|
+
"""Validates a channel against a module constraint.
|
585
|
+
|
586
|
+
Parameters
|
587
|
+
----------
|
588
|
+
ch: ChannelType
|
589
|
+
The channel to validate
|
590
|
+
constraint: ModuleConstraints
|
591
|
+
The module's physical constraints
|
592
|
+
|
593
|
+
Returns
|
594
|
+
-------
|
595
|
+
list[str]
|
596
|
+
A list of issue descriptions
|
597
|
+
|
598
|
+
Notes
|
599
|
+
-----
|
600
|
+
Possible issues reported:
|
601
|
+
|
602
|
+
- "module has no <input/output> ports."
|
603
|
+
- "<input/output> port number # out-of-bounds for module, must be between [#, #)."
|
604
|
+
- "module does not support complex <input/output> channels."
|
605
|
+
- "invalid <input/output> IQ pair {#, #}, module only supports pairs [{#, #}, ...]."
|
606
|
+
"""
|
607
|
+
# TODO: Consider simplifying these separate input/output private validators into a single func
|
608
|
+
# since they share the same overall logic, and simply require different error messaging
|
609
|
+
# and attribute access...
|
610
|
+
if ch.direction == "out":
|
611
|
+
return _validate_output_channel(ch, constraint)
|
612
|
+
return _validate_input_channel(ch, constraint)
|
613
|
+
|
614
|
+
|
615
|
+
def _validate_output_channel(ch_out: ChannelType, constraint: ModuleConstraints) -> list[str]:
|
616
|
+
issues = []
|
617
|
+
if isinstance(ch_out, RealChannel):
|
618
|
+
po_out = ch_out.port
|
619
|
+
if constraint.n_ch_out == 0:
|
620
|
+
issues.append("module has no output ports.")
|
621
|
+
elif po_out.number < 0 or po_out.number >= constraint.n_ch_out:
|
622
|
+
issues.append(
|
623
|
+
f"output port number {po_out.number} out-of-bounds for module, "
|
624
|
+
f"must be between [0, {constraint.n_ch_out}).",
|
625
|
+
)
|
626
|
+
else:
|
627
|
+
valid_pairs = constraint.ch_out_iq_pairs
|
628
|
+
if not valid_pairs:
|
629
|
+
issues.append("module does not support complex output channels.")
|
630
|
+
else:
|
631
|
+
po_out_i = ch_out.i_port
|
632
|
+
po_out_q = ch_out.q_port
|
633
|
+
if {po_out_i.number, po_out_q.number} not in valid_pairs:
|
634
|
+
issues.append(
|
635
|
+
f"invalid output IQ pair {{{po_out_i.number}, {po_out_q.number}}}, module only "
|
636
|
+
f"supports pairs {valid_pairs}.",
|
637
|
+
)
|
638
|
+
return issues
|
639
|
+
|
640
|
+
|
641
|
+
def _validate_input_channel(ch_in: ChannelType, constraint: ModuleConstraints) -> list[str]:
|
642
|
+
issues = []
|
643
|
+
if isinstance(ch_in, RealChannel):
|
644
|
+
po_in = ch_in.port
|
645
|
+
if constraint.n_ch_in == 0:
|
646
|
+
issues.append("module has no input ports.")
|
647
|
+
elif po_in.number < 0 or po_in.number >= constraint.n_ch_in:
|
648
|
+
issues.append(
|
649
|
+
f"input port number {po_in.number} out-of-bounds for module, "
|
650
|
+
f"must be between [0, {constraint.n_ch_in}).",
|
651
|
+
)
|
652
|
+
else:
|
653
|
+
valid_pairs = constraint.ch_in_iq_pairs
|
654
|
+
if not valid_pairs:
|
655
|
+
issues.append("module does not support complex input channels.")
|
656
|
+
else:
|
657
|
+
po_in_i = ch_in.i_port
|
658
|
+
po_in_q = ch_in.q_port
|
659
|
+
if {po_in_i.number, po_in_q.number} not in valid_pairs:
|
660
|
+
issues.append(
|
661
|
+
f"invalid input IQ pair {{{po_in_i.number}, {po_in_q.number}}}, module only "
|
662
|
+
f"supports pairs {valid_pairs}.",
|
663
|
+
)
|
664
|
+
return issues
|