iqm-station-control-client 11.2.1__py3-none-any.whl → 12.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.
- iqm/station_control/client/authentication.py +239 -0
- iqm/station_control/client/iqm_server/error.py +0 -30
- iqm/station_control/client/iqm_server/grpc_utils.py +0 -156
- iqm/station_control/client/iqm_server/iqm_server_client.py +0 -503
- iqm/station_control/client/list_models.py +16 -11
- iqm/station_control/client/qon.py +1 -1
- iqm/station_control/client/serializers/run_serializers.py +5 -4
- iqm/station_control/client/serializers/struct_serializer.py +1 -1
- iqm/station_control/client/station_control.py +140 -154
- iqm/station_control/client/utils.py +4 -42
- iqm/station_control/interface/models/__init__.py +21 -2
- iqm/station_control/interface/models/circuit.py +348 -0
- iqm/station_control/interface/models/dynamic_quantum_architecture.py +61 -3
- iqm/station_control/interface/models/jobs.py +42 -13
- iqm/station_control/interface/models/observation_set.py +28 -4
- iqm/station_control/interface/models/run.py +8 -8
- iqm/station_control/interface/models/sweep.py +7 -1
- iqm/station_control/interface/models/type_aliases.py +1 -2
- iqm/station_control/interface/station_control.py +1 -1
- {iqm_station_control_client-11.2.1.dist-info → iqm_station_control_client-12.0.0.dist-info}/METADATA +35 -36
- iqm_station_control_client-12.0.0.dist-info/RECORD +42 -0
- .DS_Store +0 -0
- iqm/.DS_Store +0 -0
- iqm/station_control/.DS_Store +0 -0
- iqm/station_control/client/.DS_Store +0 -0
- iqm/station_control/client/iqm_server/.DS_Store +0 -0
- iqm/station_control/client/iqm_server/__init__.py +0 -14
- iqm/station_control/client/iqm_server/proto/__init__.py +0 -43
- iqm/station_control/client/iqm_server/proto/calibration_pb2.py +0 -48
- iqm/station_control/client/iqm_server/proto/calibration_pb2.pyi +0 -45
- iqm/station_control/client/iqm_server/proto/calibration_pb2_grpc.py +0 -152
- iqm/station_control/client/iqm_server/proto/common_pb2.py +0 -43
- iqm/station_control/client/iqm_server/proto/common_pb2.pyi +0 -32
- iqm/station_control/client/iqm_server/proto/common_pb2_grpc.py +0 -17
- iqm/station_control/client/iqm_server/proto/job_pb2.py +0 -57
- iqm/station_control/client/iqm_server/proto/job_pb2.pyi +0 -107
- iqm/station_control/client/iqm_server/proto/job_pb2_grpc.py +0 -436
- iqm/station_control/client/iqm_server/proto/qc_pb2.py +0 -51
- iqm/station_control/client/iqm_server/proto/qc_pb2.pyi +0 -57
- iqm/station_control/client/iqm_server/proto/qc_pb2_grpc.py +0 -163
- iqm/station_control/client/iqm_server/proto/uuid_pb2.py +0 -39
- iqm/station_control/client/iqm_server/proto/uuid_pb2.pyi +0 -26
- iqm/station_control/client/iqm_server/proto/uuid_pb2_grpc.py +0 -17
- iqm/station_control/client/iqm_server/testing/__init__.py +0 -13
- iqm/station_control/client/iqm_server/testing/iqm_server_mock.py +0 -102
- iqm_station_control_client-11.2.1-py3-none-any.whl +0 -0
- iqm_station_control_client-11.2.1.dist-info/RECORD +0 -65
- {iqm_station_control_client-11.2.1.dist-info → iqm_station_control_client-12.0.0.dist-info}/LICENSE.txt +0 -0
- {iqm_station_control_client-11.2.1.dist-info → iqm_station_control_client-12.0.0.dist-info}/WHEEL +0 -0
- {iqm_station_control_client-11.2.1.dist-info → iqm_station_control_client-12.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# Copyright 2025 IQM
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Models related to quantum circuit execution."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
from typing import TYPE_CHECKING, Annotated, Any, TypeAlias
|
|
21
|
+
from uuid import UUID
|
|
22
|
+
|
|
23
|
+
from pydantic import AliasChoices, BeforeValidator, Field, PlainSerializer, WithJsonSchema, computed_field
|
|
24
|
+
|
|
25
|
+
from exa.common.helpers.deprecation import format_deprecated
|
|
26
|
+
from iqm.pulse import Circuit
|
|
27
|
+
from iqm.station_control.interface.pydantic_base import PydanticBase
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from iqm.pulse import Circuit, CircuitOperation
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
PRXSequence: TypeAlias = list[tuple[float, float]]
|
|
34
|
+
"""Sequence of PRX gates. A generic PRX gate is defined by rotation angle and phase angle, theta and phi,
|
|
35
|
+
respectively."""
|
|
36
|
+
|
|
37
|
+
QIRCode: TypeAlias = str
|
|
38
|
+
"""QIR program code in string representation."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# TODO: remove when CUDA-Q supports the new circuit format
|
|
42
|
+
@dataclass
|
|
43
|
+
class _Instruction:
|
|
44
|
+
"""An instruction in a quantum circuit. Old format."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
implementation: str | None = None
|
|
48
|
+
qubits: tuple[str, ...] = field(default_factory=tuple)
|
|
49
|
+
args: dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
def to_cpc_type(self) -> CircuitOperation:
|
|
52
|
+
"""Convert the model to a dataclass."""
|
|
53
|
+
return CircuitOperation(
|
|
54
|
+
name=self.name,
|
|
55
|
+
implementation=self.implementation,
|
|
56
|
+
locus=self.qubits,
|
|
57
|
+
args=self.args,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# TODO: remove when CUDA-Q supports the new circuit format
|
|
62
|
+
@dataclass
|
|
63
|
+
class _Circuit:
|
|
64
|
+
"""Quantum circuit to be executed. Old format."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
instructions: tuple[_Instruction, ...] = field(default_factory=tuple)
|
|
68
|
+
metadata: dict[str, Any] | None = None
|
|
69
|
+
|
|
70
|
+
def to_cpc_type(self) -> Circuit:
|
|
71
|
+
"""Convert the model to a dataclass."""
|
|
72
|
+
return Circuit(
|
|
73
|
+
name=self.name,
|
|
74
|
+
instructions=tuple(instruction.to_cpc_type() for instruction in self.instructions),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
CircuitBatch: TypeAlias = list[Circuit | _Circuit | QIRCode]
|
|
79
|
+
"""Sequence of quantum circuits to be executed together in a single batch."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
CircuitMeasurementResults: TypeAlias = dict[str, list[list[int]]]
|
|
83
|
+
"""Measurement results from a single circuit.
|
|
84
|
+
|
|
85
|
+
For each measurement operation in the circuit, maps the measurement key to the corresponding results.
|
|
86
|
+
``results[key][shot][qubit_index]`` is the result of measuring the
|
|
87
|
+
``qubit_index``'th qubit in measurement operation ``key`` in the shot ``shot``.
|
|
88
|
+
The results are non-negative integers representing the computational basis state (for qubits, 0 or 1)
|
|
89
|
+
that was the measurement outcome.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
CircuitMeasurementResultsBatch: TypeAlias = list[CircuitMeasurementResults]
|
|
93
|
+
"""Type that represents measurement results for a batch of circuits."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class HeraldingMode(StrEnum):
|
|
97
|
+
"""Heralding mode for circuit execution.
|
|
98
|
+
|
|
99
|
+
Heralding is the practice of generating data about the state of qubits prior to execution of a circuit.
|
|
100
|
+
This can be achieved by measuring the qubits immediately before executing each shot for a circuit.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
NONE = "none"
|
|
104
|
+
"""Do not do any heralding."""
|
|
105
|
+
ZEROS = "zeros"
|
|
106
|
+
"""For each circuit, perform a heralding measurement after the initial reset on all the QPU components
|
|
107
|
+
used in the circuit that have the "measure" operation available in the calset.
|
|
108
|
+
Only retain shots where all the components are measured to be in the zero state.
|
|
109
|
+
|
|
110
|
+
Note: in this mode, the number of shots returned after execution will be <= the requested amount
|
|
111
|
+
due to the post-selection based on heralding data.
|
|
112
|
+
If zero shots would be returned, the job will have the FAILED status."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MoveGateValidationMode(StrEnum):
|
|
116
|
+
"""MOVE gate validation mode for circuit compilation. This options is meant for advanced users."""
|
|
117
|
+
|
|
118
|
+
STRICT = "strict"
|
|
119
|
+
"""Perform standard MOVE gate validation: MOVE(qubit, resonator) gates must only
|
|
120
|
+
appear in sandwiches (pairs). Inside a sandwich there must be no gates acting on the
|
|
121
|
+
MOVE qubit, and no other MOVE gates acting on the resonator."""
|
|
122
|
+
ALLOW_PRX = "allow_prx"
|
|
123
|
+
"""Allow PRX gates on the MOVE qubit inside MOVE sandwiches during validation."""
|
|
124
|
+
NONE = "none"
|
|
125
|
+
"""Do not perform any MOVE gate validation."""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MoveGateFrameTrackingMode(StrEnum):
|
|
129
|
+
"""MOVE gate reference frame tracking mode for circuit compilation. This option is meant for advanced users."""
|
|
130
|
+
|
|
131
|
+
FULL = "full"
|
|
132
|
+
"""Apply both explicit z rotations on the resonator, and a dynamic phase correction
|
|
133
|
+
due to qubit-resonator detuning, to the qubit at the end of a MOVE sandwich."""
|
|
134
|
+
NO_DETUNING_CORRECTION = "no_detuning_correction"
|
|
135
|
+
"""Only apply explicit z rotations on the resonator to the qubit at the end of the sandwich.
|
|
136
|
+
Do not apply a detuning correction, the user is expected to do this manually."""
|
|
137
|
+
NONE = "none"
|
|
138
|
+
"""Do not perform any MOVE gate frame tracking. The user is expected to do this manually."""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class DDMode(StrEnum):
|
|
142
|
+
"""Dynamical Decoupling (DD) mode for circuit execution."""
|
|
143
|
+
|
|
144
|
+
DISABLED = "disabled"
|
|
145
|
+
"""Do not apply dynamical decoupling."""
|
|
146
|
+
ENABLED = "enabled"
|
|
147
|
+
"""Apply dynamical decoupling."""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DDStrategy(PydanticBase):
|
|
151
|
+
"""Describes a particular dynamical decoupling strategy.
|
|
152
|
+
|
|
153
|
+
The current standard DD stategy can be found in :attr:`~iqm.cpc.compiler.dd.STANDARD_DD_STRATEGY`,
|
|
154
|
+
but users can use this class to provide their own dynamical decoupling strategies.
|
|
155
|
+
|
|
156
|
+
See Ezzell et al., Phys. Rev. Appl. 20, 064027 (2022) for information on DD sequences.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
# TODO station-control-client docs need to support bibtex citations.
|
|
160
|
+
# TODO :cite:`Ezzell_2022`
|
|
161
|
+
|
|
162
|
+
merge_contiguous_waits: bool = Field(default=True)
|
|
163
|
+
"""Merge contiguous ``Wait`` instructions into one if they are separated only by ``Block`` instructions."""
|
|
164
|
+
|
|
165
|
+
target_qubits: frozenset[str] | None = Field(default=None)
|
|
166
|
+
"""Qubits on which dynamical decoupling should be applied. If ``None``, all qubits are targeted."""
|
|
167
|
+
|
|
168
|
+
skip_leading_wait: bool = Field(default=True)
|
|
169
|
+
"""Skip processing leading ``Wait`` instructions."""
|
|
170
|
+
|
|
171
|
+
skip_trailing_wait: bool = Field(default=True)
|
|
172
|
+
"""Skip processing trailing ``Wait`` instructions."""
|
|
173
|
+
|
|
174
|
+
gate_sequences: list[tuple[int, str | PRXSequence, str]] = Field(default_factory=list)
|
|
175
|
+
"""Available decoupling gate sequences to choose from in this strategy.
|
|
176
|
+
|
|
177
|
+
Each sequence is defined by a tuple of ``(ratio, gate pattern, align)``:
|
|
178
|
+
|
|
179
|
+
* ratio: Minimal duration for the sequence (in PRX gate durations).
|
|
180
|
+
|
|
181
|
+
* gate pattern: Gate pattern can be defined in two ways. It can be a string containing "X" and "Y" characters,
|
|
182
|
+
encoding a PRX gate sequence. For example, "YXYX" corresponds to the
|
|
183
|
+
XY4 sequence, "XYXYYXYX" to the EDD sequence, etc. If more flexibility is needed, a gate pattern can be
|
|
184
|
+
defined as a sequence of PRX gate argument tuples (that contain a rotation angle and a phase angle). For
|
|
185
|
+
example, sequence "YX" could be written as ``[(math.pi, math.pi / 2), (math.pi, 0)]``.
|
|
186
|
+
|
|
187
|
+
* align: Controls the alignment of the sequence within the time window it is inserted in. Supported values:
|
|
188
|
+
|
|
189
|
+
- "asap": Corresponds to a ASAP-aligned sequence with no waiting time before the first pulse.
|
|
190
|
+
- "center": Corresponds to a symmetric sequence.
|
|
191
|
+
- "alap": Corresponds to a ALAP-aligned sequence.
|
|
192
|
+
|
|
193
|
+
The Dynamical Decoupling algorithm uses the best fitting gate sequence by first sorting them
|
|
194
|
+
by ``ratio`` in descending order. Then the longest fitting pattern is determined by comparing ``ratio``
|
|
195
|
+
with the duration of the time window divided by the PRX gate duration.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _parse_legacy_qubit_mapping(value: Any) -> dict[str, str] | None:
|
|
200
|
+
if value is None:
|
|
201
|
+
return None
|
|
202
|
+
if isinstance(value, dict):
|
|
203
|
+
return {str(key): str(value) for key, value in value.items()}
|
|
204
|
+
# Deprecated since 2025-11-03.
|
|
205
|
+
# `qubit_mapping` currently uses the legacy list format on the wire,
|
|
206
|
+
# but it should eventually be replaced with the new `QubitMapping` (dict-based) format.
|
|
207
|
+
# The switch can only be made once all legacy clients have been updated,
|
|
208
|
+
# as they still depend on receiving the old response format.
|
|
209
|
+
return {item["logical_name"]: item["physical_name"] for item in value}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _serialize_as_legacy_qubit_mapping(mapping: dict[str, str] | None) -> list[dict[str, str]] | None:
|
|
213
|
+
if mapping is None:
|
|
214
|
+
return None
|
|
215
|
+
# Deprecated since 2025-11-03.
|
|
216
|
+
# `qubit_mapping` currently uses the legacy list format on the wire,
|
|
217
|
+
# but it should eventually be replaced with the new `QubitMapping` (dict-based) format.
|
|
218
|
+
# The switch can only be made once all legacy clients have been updated,
|
|
219
|
+
# as they still depend on receiving the old response format.
|
|
220
|
+
return [{"logical_name": k, "physical_name": v} for k, v in mapping.items()]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
LEGACY_QUBIT_MAPPING_SCHEMA = {
|
|
224
|
+
"anyOf": [
|
|
225
|
+
{
|
|
226
|
+
"type": "array",
|
|
227
|
+
"items": {
|
|
228
|
+
"type": "object",
|
|
229
|
+
"required": ["logical_name", "physical_name"],
|
|
230
|
+
"properties": {
|
|
231
|
+
"logical_name": {"type": "string"},
|
|
232
|
+
"physical_name": {"type": "string"},
|
|
233
|
+
},
|
|
234
|
+
"additionalProperties": False,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
{"type": "null"},
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
QubitMapping = Annotated[
|
|
243
|
+
dict[str, str],
|
|
244
|
+
BeforeValidator(_parse_legacy_qubit_mapping),
|
|
245
|
+
PlainSerializer(_serialize_as_legacy_qubit_mapping),
|
|
246
|
+
WithJsonSchema(LEGACY_QUBIT_MAPPING_SCHEMA),
|
|
247
|
+
]
|
|
248
|
+
"""Mapping from logical qubit names to physical qubit names."""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ATTENTION: Do **not** rename RunRequest model!
|
|
252
|
+
# IQM Server implements circuit validation by loading the StationControl OpenAPI specification
|
|
253
|
+
# and using this (JSON) schema for the validation. OpenAPI specification contains schemas for
|
|
254
|
+
# all station control models so the name of the schema (= name of this dataclass) is used to
|
|
255
|
+
# select the correct schema. If you intend to rename or move this model, consult IQM Server
|
|
256
|
+
# team to ensure that nothing gets broken!! Sub-schemas (e.g. Circuit) can be renamed freely
|
|
257
|
+
# - FastAPI uses local schema references so the renamed references are resolved correctly,
|
|
258
|
+
# as long as all the referenced schemas are added to the OpenAPI specification
|
|
259
|
+
# (which is done automatically by FastAPI/Pydantic,
|
|
260
|
+
# unless there are some really weird stuff in the dataclas definition).
|
|
261
|
+
#
|
|
262
|
+
# In addition to the schema name, IQM Server depends on the following features:
|
|
263
|
+
#
|
|
264
|
+
# * RunRequest has "shots" integer property
|
|
265
|
+
# * RunRequest has "circuits" array property
|
|
266
|
+
#
|
|
267
|
+
# If you change those properties, coordinate the changes with IQM Server team!
|
|
268
|
+
class PostJobsRequest(PydanticBase):
|
|
269
|
+
"""Request to Station Control run a job that executes a batch of quantum circuits."""
|
|
270
|
+
|
|
271
|
+
circuits: CircuitBatch = Field(...)
|
|
272
|
+
"""Batch of quantum circuit(s) to execute."""
|
|
273
|
+
calibration_set_id: UUID | None = Field(None)
|
|
274
|
+
"""ID of the calibration set to use, or None to use the current default calibration set."""
|
|
275
|
+
qubit_mapping: QubitMapping | None = Field(None)
|
|
276
|
+
"""Mapping from logical qubit names to physical qubit names, or None if ``circuits`` use physical qubit names."""
|
|
277
|
+
shots: int = Field(1, gt=0)
|
|
278
|
+
"""How many times to execute each circuit in the batch, must be greater than zero."""
|
|
279
|
+
max_circuit_duration_over_t2: float | None = Field(None)
|
|
280
|
+
"""Circuits are disqualified on the server if they are longer than this fraction
|
|
281
|
+
of the T2 time of the worst qubit used.
|
|
282
|
+
If set to 0.0, no circuits are disqualified. If set to None the server default value is used."""
|
|
283
|
+
heralding_mode: HeraldingMode = Field(HeraldingMode.NONE)
|
|
284
|
+
"""Which heralding mode to use during the execution of circuits in this request."""
|
|
285
|
+
move_gate_validation: MoveGateValidationMode = Field(
|
|
286
|
+
MoveGateValidationMode.STRICT,
|
|
287
|
+
validation_alias=AliasChoices("move_gate_validation", "move_validation_mode"),
|
|
288
|
+
)
|
|
289
|
+
"""Which method of MOVE gate validation to use in circuit compilation."""
|
|
290
|
+
move_gate_frame_tracking: MoveGateFrameTrackingMode = Field(
|
|
291
|
+
MoveGateFrameTrackingMode.FULL,
|
|
292
|
+
validation_alias=AliasChoices("move_gate_frame_tracking", "move_gate_frame_tracking_mode"),
|
|
293
|
+
)
|
|
294
|
+
"""Which method of MOVE gate frame tracking to use for circuit compilation."""
|
|
295
|
+
active_reset_cycles: int | None = Field(None)
|
|
296
|
+
"""Number of active ``reset`` operations inserted at the beginning of each circuit for each active qubit.
|
|
297
|
+
``None`` means active reset is not used but instead reset is done by waiting (relaxation). Integer values smaller
|
|
298
|
+
than 1 result in neither active nor reset by wait being used, in which case any reset operations must be explicitly
|
|
299
|
+
added in the circuit."""
|
|
300
|
+
dd_mode: DDMode = Field(DDMode.DISABLED)
|
|
301
|
+
"""Whether dynamical decoupling is enabled or disabled during the execution."""
|
|
302
|
+
dd_strategy: DDStrategy | None = Field(None)
|
|
303
|
+
"""Dynamical decoupling strategy to be used during the execution, if DD is enabled.
|
|
304
|
+
If None, use the server default strategy."""
|
|
305
|
+
|
|
306
|
+
@computed_field(
|
|
307
|
+
json_schema_extra={
|
|
308
|
+
"deprecated": True,
|
|
309
|
+
"description": format_deprecated(
|
|
310
|
+
old="`move_validation_mode`", new="`move_gate_validation`", since="2025-10-17"
|
|
311
|
+
),
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
def move_validation_mode(self) -> MoveGateValidationMode:
|
|
315
|
+
return self.move_gate_validation
|
|
316
|
+
|
|
317
|
+
@computed_field(
|
|
318
|
+
json_schema_extra={
|
|
319
|
+
"deprecated": True,
|
|
320
|
+
"description": format_deprecated(
|
|
321
|
+
old="`move_gate_frame_tracking_mode`", new="`move_gate_frame_tracking`", since="2025-10-17"
|
|
322
|
+
),
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
def move_gate_frame_tracking_mode(self) -> MoveGateFrameTrackingMode:
|
|
326
|
+
return self.move_gate_frame_tracking
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
RunRequest: TypeAlias = PostJobsRequest
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class CircuitMeasurementCounts(PydanticBase):
|
|
333
|
+
"""Circuit measurement counts in histogram representation."""
|
|
334
|
+
|
|
335
|
+
measurement_keys: list[str]
|
|
336
|
+
"""Measurement keys in the order they are concatenated to form the state bitstrings in :attr:`counts`.
|
|
337
|
+
|
|
338
|
+
For example, if :attr:`measurement_keys` is ``['mk_1', 'mk2']`` and ``'mk_1'`` measures ``QB1``
|
|
339
|
+
and ``'mk_2'`` measures ``QB3`` and ``QB5``, then :attr:`counts` could contains keys such as ``'010'`` representing
|
|
340
|
+
shots where ``QB1`, ``QB3`` and ``QB5`` were observed to be in the state :math:`|010\rangle`.
|
|
341
|
+
"""
|
|
342
|
+
counts: dict[str, int]
|
|
343
|
+
"""Mapping from computational basis states, represented as bitstrings, to the number of times they were observed
|
|
344
|
+
when executing the circuit."""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
CircuitMeasurementCountsBatch: TypeAlias = list[CircuitMeasurementCounts]
|
|
348
|
+
"""Measurement results in histogram representation for each circuit in the batch."""
|
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
"""Dynamic quantum architecture (DQA) related interface models."""
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from functools import cached_property
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any, TypeAlias
|
|
17
19
|
from uuid import UUID
|
|
18
20
|
|
|
19
|
-
from pydantic import Field,
|
|
21
|
+
from pydantic import Field, field_validator
|
|
20
22
|
|
|
21
23
|
from iqm.station_control.interface.pydantic_base import PydanticBase
|
|
22
24
|
|
|
23
|
-
Locus = tuple[
|
|
25
|
+
Locus: TypeAlias = tuple[str, ...]
|
|
24
26
|
"""Names of the QPU components (typically qubits) a quantum operation instance is acting on, e.g. `("QB1", "QB2")`."""
|
|
25
27
|
|
|
26
28
|
|
|
@@ -80,6 +82,42 @@ class GateInfo(PydanticBase):
|
|
|
80
82
|
return new_value
|
|
81
83
|
raise ValueError("'override_default_implementation' must be a dict.")
|
|
82
84
|
|
|
85
|
+
@cached_property
|
|
86
|
+
def loci(self) -> tuple[Locus, ...]:
|
|
87
|
+
"""Returns all loci which are available for at least one of the implementations.
|
|
88
|
+
|
|
89
|
+
The loci are sorted first based on the first locus component, then the second, etc.
|
|
90
|
+
The sorting of individual locus components is first done alphabetically based on their
|
|
91
|
+
non-numeric part, and then components with the same non-numeric part are sorted numerically.
|
|
92
|
+
An example of loci sorted this way would be:
|
|
93
|
+
|
|
94
|
+
('QB1', 'QB2'),
|
|
95
|
+
('QB2', 'COMPR1'),
|
|
96
|
+
('QB2', 'QB3'),
|
|
97
|
+
('QB3', 'COMPR1'),
|
|
98
|
+
('QB3', 'COMPR2'),
|
|
99
|
+
('QB3', 'QB1'),
|
|
100
|
+
('QB10', 'QB2'),
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
loci_set = {locus for impl in self.implementations.values() for locus in impl.loci}
|
|
104
|
+
loci_sorted = sorted(loci_set, key=lambda locus: tuple(map(_component_sort_key, locus)))
|
|
105
|
+
return tuple(loci_sorted)
|
|
106
|
+
|
|
107
|
+
def get_default_implementation(self, locus: Locus) -> str:
|
|
108
|
+
"""Default implementation of this gate for the given locus.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
locus: gate locus
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Name of the default implementation of this gate for ``locus``.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
if (impl := self.override_default_implementation.get(locus)) is not None:
|
|
118
|
+
return impl
|
|
119
|
+
return self.default_implementation
|
|
120
|
+
|
|
83
121
|
|
|
84
122
|
class DynamicQuantumArchitecture(PydanticBase):
|
|
85
123
|
"""The dynamic quantum architecture (DQA).
|
|
@@ -117,3 +155,23 @@ class DynamicQuantumArchitecture(PydanticBase):
|
|
|
117
155
|
],
|
|
118
156
|
)
|
|
119
157
|
"""Mapping of gate names to information about the gates."""
|
|
158
|
+
|
|
159
|
+
@cached_property
|
|
160
|
+
def components(self) -> tuple[str, ...]:
|
|
161
|
+
"""All locus components (qubits and computational resonators) sorted.
|
|
162
|
+
|
|
163
|
+
The components are first sorted alphabetically based on their non-numeric part, and then
|
|
164
|
+
components with the same non-numeric part are sorted numerically. An example of components
|
|
165
|
+
sorted this way would be: ('COMPR1', 'COMPR2', 'QB1', 'QB2', 'QB3', 'QB10', 'QB11', 'QB20').
|
|
166
|
+
"""
|
|
167
|
+
return tuple(sorted(self.qubits + self.computational_resonators, key=_component_sort_key))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _component_sort_key(component_name: str) -> tuple[str, int, str]:
|
|
171
|
+
"""Sorting key for QPU component names."""
|
|
172
|
+
|
|
173
|
+
def get_numeric_id(name: str) -> int:
|
|
174
|
+
match = re.search(r"(\d+)", name)
|
|
175
|
+
return int(match.group(1)) if match else 0
|
|
176
|
+
|
|
177
|
+
return re.sub(r"[^a-zA-Z]", "", component_name), get_numeric_id(component_name), component_name
|
|
@@ -11,60 +11,89 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
-
"""Job
|
|
14
|
+
"""Job-related models."""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
from collections.abc import Callable
|
|
18
19
|
from datetime import datetime
|
|
19
20
|
from enum import Enum
|
|
20
21
|
import functools
|
|
22
|
+
from typing import TypeAlias
|
|
21
23
|
from uuid import UUID
|
|
22
24
|
|
|
23
25
|
from iqm.station_control.interface.pydantic_base import PydanticBase
|
|
24
26
|
|
|
27
|
+
_Progress: TypeAlias = tuple[str, int, int]
|
|
28
|
+
"""Describes the progress of an arbitrary task: (label, value, max_value)."""
|
|
29
|
+
|
|
30
|
+
ProgressCallback: TypeAlias = Callable[[list[_Progress]], None]
|
|
31
|
+
"""Callback function for reporting progress on a list of tasks."""
|
|
32
|
+
|
|
33
|
+
Statuses: TypeAlias = list[_Progress]
|
|
34
|
+
"""Progress of the parallel sweeps of a job.
|
|
35
|
+
Used in Station Control. Deprecated, should not be used anywhere else."""
|
|
36
|
+
|
|
25
37
|
|
|
26
38
|
class TimelineEntry(PydanticBase):
|
|
27
|
-
"""Status and timestamp pair
|
|
39
|
+
"""Status and timestamp pair for a job timeline."""
|
|
28
40
|
|
|
29
41
|
status: JobExecutorStatus
|
|
42
|
+
"""Job status that was reached."""
|
|
30
43
|
timestamp: datetime
|
|
44
|
+
"""Time at which ``status`` was reached."""
|
|
31
45
|
|
|
32
46
|
|
|
33
47
|
class JobResult(PydanticBase):
|
|
34
|
-
"""Progress information
|
|
48
|
+
"""Progress information for the JobExecutorStatus.EXECUTION_STARTED stage of a job."""
|
|
49
|
+
|
|
50
|
+
# TODO redesign, should not be called JobResult since it's a progress indicator
|
|
35
51
|
|
|
36
52
|
job_id: UUID
|
|
37
|
-
|
|
53
|
+
"""ID of the job."""
|
|
54
|
+
parallel_sweep_progress: list[_Progress]
|
|
55
|
+
"""Progress of the sweeps if we are in the JobExecutorStatus.EXECUTION_STARTED stage, otherwise empty."""
|
|
38
56
|
interrupted: bool
|
|
57
|
+
"""True iff the job was canceled."""
|
|
39
58
|
|
|
40
59
|
|
|
41
60
|
class JobError(PydanticBase):
|
|
42
|
-
"""Error
|
|
61
|
+
"""Error message for a job."""
|
|
43
62
|
|
|
44
63
|
full_error_log: str
|
|
64
|
+
"""Full error message for logging."""
|
|
45
65
|
user_error_message: str
|
|
66
|
+
"""Short, human-readable error message for users."""
|
|
46
67
|
|
|
47
68
|
|
|
48
69
|
class JobData(PydanticBase):
|
|
49
70
|
"""Status, artifacts and metadata of a job."""
|
|
50
71
|
|
|
72
|
+
# TODO redesign
|
|
73
|
+
|
|
51
74
|
job_id: UUID
|
|
52
75
|
"""Unique ID of the job."""
|
|
53
76
|
job_status: JobExecutorStatus
|
|
54
77
|
"""Current job status."""
|
|
55
|
-
job_result: JobResult
|
|
56
|
-
"""Progress information for the job."""
|
|
57
|
-
# job_result: The output of a progressing or a successful job. This includes progress indicators.
|
|
78
|
+
job_result: JobResult # TODO should not be called JobResult, it's progress info for the execution stage
|
|
79
|
+
"""Progress information for the JobExecutorStatus.EXECUTION_STARTED stage of a job."""
|
|
58
80
|
job_error: JobError | None
|
|
59
|
-
"""Error message(s) for a failed job."""
|
|
81
|
+
"""Error message(s) for a failed job, otherwise None."""
|
|
60
82
|
position: int | None
|
|
61
|
-
"""
|
|
62
|
-
|
|
83
|
+
"""Number of jobs ahead of this job in its current queue.
|
|
84
|
+
None means the job has reached a terminal status.
|
|
85
|
+
"""
|
|
63
86
|
|
|
64
87
|
|
|
88
|
+
# NOTE: Keep JobExecutorStatus inheriting from Enum (not StrEnum).
|
|
89
|
+
# Our tests do ordering like: str(JobExecutorStatus.X) > JobExecutorStatus.Y
|
|
90
|
+
# With Enum, the right operand isn’t a str, so Python dispatches to the Enum’s comparison methods,
|
|
91
|
+
# where we implement definition-order (<, >) logic.
|
|
92
|
+
# With StrEnum, members are str subclasses; when a plain str is on the left, Python uses str.__gt__
|
|
93
|
+
# (lexicographic) and never calls our enum’s ordering, so string↔enum ordering fails.
|
|
65
94
|
@functools.total_ordering
|
|
66
95
|
class JobExecutorStatus(Enum):
|
|
67
|
-
"""Different
|
|
96
|
+
"""Different statuses a job can be in.
|
|
68
97
|
|
|
69
98
|
The ordering of these statuses is important, and execution logic relies on it.
|
|
70
99
|
Thus, if a new status is added, ensure that it is slotted
|
|
@@ -89,7 +118,7 @@ class JobExecutorStatus(Enum):
|
|
|
89
118
|
COMPILATION_STARTED = "compilation_started"
|
|
90
119
|
"""The job is being compiled."""
|
|
91
120
|
COMPILATION_ENDED = "compilation_ended"
|
|
92
|
-
"""The job has been
|
|
121
|
+
"""The job has been successfully compiled."""
|
|
93
122
|
|
|
94
123
|
# Executing sweep
|
|
95
124
|
SAVE_SWEEP_METADATA_STARTED = "save_sweep_metadata_started"
|
|
@@ -17,8 +17,9 @@ from datetime import datetime
|
|
|
17
17
|
import enum
|
|
18
18
|
import uuid
|
|
19
19
|
|
|
20
|
-
from pydantic import ConfigDict, Field
|
|
20
|
+
from pydantic import ConfigDict, Field, computed_field
|
|
21
21
|
|
|
22
|
+
from exa.common.helpers.deprecation import format_deprecated
|
|
22
23
|
from iqm.station_control.interface.models.observation import ObservationLite
|
|
23
24
|
from iqm.station_control.interface.pydantic_base import PydanticBase
|
|
24
25
|
|
|
@@ -41,8 +42,6 @@ class ObservationSetBase(PydanticBase):
|
|
|
41
42
|
|
|
42
43
|
observation_set_type: ObservationSetType
|
|
43
44
|
"""Indicates the type (i.e. purpose) of the observation set."""
|
|
44
|
-
observation_ids: list[int]
|
|
45
|
-
"""Database IDs of the observations belonging to the observation set."""
|
|
46
45
|
describes_id: uuid.UUID | None = Field(default=None)
|
|
47
46
|
"""Unique identifier of the observation set this observation set describes."""
|
|
48
47
|
invalid: bool = Field(default=False)
|
|
@@ -52,6 +51,9 @@ class ObservationSetBase(PydanticBase):
|
|
|
52
51
|
class ObservationSetDefinition(ObservationSetBase):
|
|
53
52
|
"""The content of the observation set object when creating it."""
|
|
54
53
|
|
|
54
|
+
observation_ids: list[int]
|
|
55
|
+
"""Database IDs of the observations belonging to the observation set."""
|
|
56
|
+
|
|
55
57
|
model_config = ConfigDict(
|
|
56
58
|
extra="forbid", # Forbid any extra attributes
|
|
57
59
|
)
|
|
@@ -60,6 +62,8 @@ class ObservationSetDefinition(ObservationSetBase):
|
|
|
60
62
|
class ObservationSetData(ObservationSetBase):
|
|
61
63
|
"""The content of the observation set stored in the database."""
|
|
62
64
|
|
|
65
|
+
observation_ids: list[int]
|
|
66
|
+
"""Database IDs of the observations belonging to the observation set."""
|
|
63
67
|
dut_label: str | None
|
|
64
68
|
"""String representation of the DUT the observation set is associated with. Can only be None for generic sets."""
|
|
65
69
|
observation_set_id: uuid.UUID
|
|
@@ -90,12 +94,32 @@ class ObservationSetUpdate(PydanticBase):
|
|
|
90
94
|
"""Flag indicating if the set is invalid. Automated systems must not use invalid sets."""
|
|
91
95
|
|
|
92
96
|
|
|
93
|
-
class ObservationSetWithObservations(
|
|
97
|
+
class ObservationSetWithObservations(ObservationSetBase):
|
|
94
98
|
"""The content of the observation set stored in the database, with a list of observations."""
|
|
95
99
|
|
|
100
|
+
dut_label: str | None
|
|
101
|
+
"""String representation of the DUT the observation set is associated with. Can only be None for generic sets."""
|
|
102
|
+
observation_set_id: uuid.UUID
|
|
103
|
+
"""Unique identifier of the observation set."""
|
|
104
|
+
created_timestamp: datetime
|
|
105
|
+
"""Time when the object was created in the database."""
|
|
106
|
+
end_timestamp: datetime | None
|
|
107
|
+
"""Time when the observation set was finalized. If ``None``, the set is not finalized yet."""
|
|
96
108
|
observations: list[ObservationLite]
|
|
97
109
|
"""Observations belonging to the observation set."""
|
|
98
110
|
|
|
111
|
+
@computed_field(
|
|
112
|
+
return_type=list[int],
|
|
113
|
+
json_schema_extra={
|
|
114
|
+
"deprecated": True,
|
|
115
|
+
"description": format_deprecated(old="`observation_ids`", new="`observations`", since="2025-10-06"),
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
def observation_ids(self) -> list[int]:
|
|
119
|
+
"""Database IDs of the observations belonging to the observation set."""
|
|
120
|
+
# "observation_ids" is deprecated to unify the format with IQM Server which uses "observations"
|
|
121
|
+
return [observation.observation_id for observation in self.observations]
|
|
122
|
+
|
|
99
123
|
|
|
100
124
|
class QualityMetrics(ObservationSetWithObservations):
|
|
101
125
|
"""The content of the quality metric set stored in the database, with a list of observations and calibration set."""
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
"""Run related station control interface models."""
|
|
15
15
|
|
|
16
|
-
from dataclasses import dataclass
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
from typing import Any
|
|
19
19
|
import uuid
|
|
@@ -34,9 +34,9 @@ class RunBase:
|
|
|
34
34
|
"""Identifier of the Experiment (:attr:`.Experiment.name`)."""
|
|
35
35
|
experiment_label: str
|
|
36
36
|
"""Freeform label of the Experiment. As opposed to `experiment_name`, no core logic relies on this value."""
|
|
37
|
-
options: dict[str, Any]
|
|
37
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
38
38
|
"""Experiment-specific options or toggles that generated the run."""
|
|
39
|
-
software_version_set_id: int
|
|
39
|
+
software_version_set_id: int = 0
|
|
40
40
|
"""Unique identifier of the software version set of the current Python runtime."""
|
|
41
41
|
|
|
42
42
|
|
|
@@ -44,9 +44,9 @@ class RunBase:
|
|
|
44
44
|
class RunConfigurationBase:
|
|
45
45
|
"""Abstract base class of the run configuration data."""
|
|
46
46
|
|
|
47
|
-
additional_run_properties: dict[str, Any]
|
|
47
|
+
additional_run_properties: dict[str, Any] = field(default_factory=dict)
|
|
48
48
|
"""A free-form dictionary of data, used to store information that does not fall into other categories."""
|
|
49
|
-
hard_sweeps: dict[str, NdSweep]
|
|
49
|
+
hard_sweeps: dict[str, NdSweep] = field(default_factory=dict)
|
|
50
50
|
"""Maps :attr:`.SweepBase.return_parameters` to "hardware sweep specification" which specifies
|
|
51
51
|
how the data measured at each spot should be interpreted and shaped.
|
|
52
52
|
The hard sweep specification is in the same format as :attr:`.SweepBase.sweeps`,
|
|
@@ -54,12 +54,12 @@ class RunConfigurationBase:
|
|
|
54
54
|
An empty list is interpreted such that the return parameter is a scalar.
|
|
55
55
|
The hard sweep specification can also be `None`,
|
|
56
56
|
in which case the shape will be whatever the instrument returns."""
|
|
57
|
-
components: list[str]
|
|
57
|
+
components: list[str] = field(default_factory=list)
|
|
58
58
|
"""Components that participate in this run."""
|
|
59
|
-
default_data_parameters: list[str]
|
|
59
|
+
default_data_parameters: list[str] = field(default_factory=list)
|
|
60
60
|
"""The subset of :attr:`.SweepBase.return_parameters` that were added by default, not by the user.
|
|
61
61
|
Used to select which data to analyze and plot."""
|
|
62
|
-
default_sweep_parameters: list[str]
|
|
62
|
+
default_sweep_parameters: list[str] = field(default_factory=list)
|
|
63
63
|
"""The subset of :attr:`.SweepBase.sweeps` parameters were added by default, not by the user.
|
|
64
64
|
Used to select which data to analyze and plot."""
|
|
65
65
|
|