iqm-station-control-client 9.7.0__py3-none-any.whl → 9.8.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/qon.py +590 -0
- {iqm_station_control_client-9.7.0.dist-info → iqm_station_control_client-9.8.0.dist-info}/METADATA +1 -1
- {iqm_station_control_client-9.7.0.dist-info → iqm_station_control_client-9.8.0.dist-info}/RECORD +6 -5
- {iqm_station_control_client-9.7.0.dist-info → iqm_station_control_client-9.8.0.dist-info}/LICENSE.txt +0 -0
- {iqm_station_control_client-9.7.0.dist-info → iqm_station_control_client-9.8.0.dist-info}/WHEEL +0 -0
- {iqm_station_control_client-9.7.0.dist-info → iqm_station_control_client-9.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,590 @@
|
|
|
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
|
+
"""Qualified observation name parsing and creation."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import StrEnum
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Any, Final, TypeAlias
|
|
23
|
+
|
|
24
|
+
from iqm.station_control.interface.models.observation import ObservationBase
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_FIELD_SEPARATOR: Final[str] = "."
|
|
30
|
+
"""Separates fields/settings tree path elements in an Observation dut_field."""
|
|
31
|
+
|
|
32
|
+
_SUFFIX_SEPARATOR: Final[str] = ":"
|
|
33
|
+
"""Separates suffixes in an Observation dut_field."""
|
|
34
|
+
|
|
35
|
+
LOCUS_SEPARATOR: Final[str] = "__"
|
|
36
|
+
"""Separates QPU components in a locus string."""
|
|
37
|
+
|
|
38
|
+
Locus: TypeAlias = tuple[str, ...]
|
|
39
|
+
"""Sequence of QPU component physical names a quantum operation acts on. The order may matter."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def locus_to_locus_str(locus: Locus) -> str:
|
|
43
|
+
"""Convert a locus into a locus string."""
|
|
44
|
+
return LOCUS_SEPARATOR.join(locus)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Domain(StrEnum):
|
|
48
|
+
"""Known observation domains/categories."""
|
|
49
|
+
|
|
50
|
+
CONTROLLER_SETTING = "controllers"
|
|
51
|
+
"""Settings for the control instruments. Calibration data."""
|
|
52
|
+
GATE_PARAMETER = "gates"
|
|
53
|
+
"""Parameters for quantum operations. Calibration data."""
|
|
54
|
+
CHARACTERIZATION = "characterization"
|
|
55
|
+
"""Characterization data for the QPU."""
|
|
56
|
+
METRIC = "metrics"
|
|
57
|
+
"""Quality metrics for quantum operations."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UnknownObservationError(RuntimeError):
|
|
61
|
+
"""Observation name was syntactically correct but contained unknown elements."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_suffixes(suffixes: Iterable[str]) -> dict[str, str]:
|
|
65
|
+
"""Parse the given suffixes and return them in a sorted dictionary."""
|
|
66
|
+
suffix_dict = {}
|
|
67
|
+
for suffix in suffixes:
|
|
68
|
+
if "=" not in suffix:
|
|
69
|
+
raise ValueError(f"Invalid suffix: {suffix}")
|
|
70
|
+
key, value = suffix.split("=")
|
|
71
|
+
suffix_dict[key] = value
|
|
72
|
+
# We're sorting the suffixes to ensure that the suffixes are always in the same order
|
|
73
|
+
# (they are supposed to be in lexical order according to the key)
|
|
74
|
+
return dict(sorted(suffix_dict.items()))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class QON:
|
|
79
|
+
"""Qualified observation name.
|
|
80
|
+
|
|
81
|
+
Used for representing, creating and parsing observation names that conform to the current convention.
|
|
82
|
+
When the convention changes, the first thing you should do is to update the classes
|
|
83
|
+
in this module.
|
|
84
|
+
|
|
85
|
+
.. note::
|
|
86
|
+
|
|
87
|
+
This class provides a somewhat reliable way to encode more than one data item in
|
|
88
|
+
:attr:`ObservationBase.dut_field`, aka "observation name". Eventually a more viable
|
|
89
|
+
solution could be to give each of these items their own fields in the observation structure.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def domain(self) -> Domain:
|
|
95
|
+
"""Type/purpose of the observation."""
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
def __str__(self) -> str:
|
|
99
|
+
"""String representation of the qualified observation name."""
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_str(cls, name: str) -> "QON":
|
|
104
|
+
"""Parse an observation name into a QON object.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
name: Observation name (aka dut_field) to parse.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Corresponding qualified observation name object.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: Failed to parse ``name`` because it was syntactically incorrect.
|
|
114
|
+
UnknownObservationError: Failed to parse ``name`` because it contains unknown elements.
|
|
115
|
+
|
|
116
|
+
"""
|
|
117
|
+
parts = name.split(_FIELD_SEPARATOR, maxsplit=2)
|
|
118
|
+
if len(parts) < 3:
|
|
119
|
+
raise ValueError("Unparseable observation name.")
|
|
120
|
+
|
|
121
|
+
# check the category/domain of the observation
|
|
122
|
+
match parts[0]:
|
|
123
|
+
case Domain.METRIC:
|
|
124
|
+
return QONMetric._parse(parts[1], parts[2])
|
|
125
|
+
case Domain.CHARACTERIZATION if parts[1] == "model":
|
|
126
|
+
return QONCharacterization._parse(parts[2])
|
|
127
|
+
case Domain.CONTROLLER_SETTING:
|
|
128
|
+
return QONControllerSetting._parse(parts[1], parts[2])
|
|
129
|
+
case Domain.GATE_PARAMETER:
|
|
130
|
+
return QONGateParam._parse(parts[1], parts[2])
|
|
131
|
+
|
|
132
|
+
raise UnknownObservationError("Unknown observation domain.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class QONCharacterization(QON):
|
|
137
|
+
"""QON representing a QPU property.
|
|
138
|
+
|
|
139
|
+
Has the form ``characterization.model.{component}.{property}``
|
|
140
|
+
|
|
141
|
+
Can parse e.g.
|
|
142
|
+
|
|
143
|
+
characterization.model.QB5.t2_time
|
|
144
|
+
|
|
145
|
+
component: QB5
|
|
146
|
+
quantity: t2_time
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
component: str
|
|
150
|
+
"""Names of QPU component(s) that the observation describes."""
|
|
151
|
+
quantity: str
|
|
152
|
+
"""Name of the quantity described by the observation."""
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def domain(self) -> Domain:
|
|
156
|
+
return Domain.CHARACTERIZATION
|
|
157
|
+
|
|
158
|
+
def __str__(self) -> str:
|
|
159
|
+
return _FIELD_SEPARATOR.join([self.domain, "model", self.component, self.quantity])
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def _parse(cls, rest: str) -> "QONCharacterization":
|
|
163
|
+
"""Parse a characterization observation name."""
|
|
164
|
+
parts = rest.split(_FIELD_SEPARATOR, maxsplit=1)
|
|
165
|
+
if len(parts) < 2:
|
|
166
|
+
raise ValueError("characterization.model observation name has less than 4 parts")
|
|
167
|
+
|
|
168
|
+
component, quantity = parts
|
|
169
|
+
return cls(
|
|
170
|
+
component=component,
|
|
171
|
+
quantity=quantity,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True)
|
|
176
|
+
class QONMetric(QON):
|
|
177
|
+
"""QON representing a gate quality metric.
|
|
178
|
+
|
|
179
|
+
Has the form ``metrics.{method}.{method_specific_part}``.
|
|
180
|
+
|
|
181
|
+
Can parse/represent e.g. the following metrics:
|
|
182
|
+
|
|
183
|
+
``metrics.ssro.measure.constant.QB1.fidelity:par=d1:aaa=bbb``
|
|
184
|
+
|
|
185
|
+
method: ssro
|
|
186
|
+
gate: measure
|
|
187
|
+
implementation: constant
|
|
188
|
+
locus: QB1
|
|
189
|
+
metric: fidelity
|
|
190
|
+
suffixes: {"aaa": "bbb", "par": "d1"}
|
|
191
|
+
|
|
192
|
+
``metrics.ssro.measure.constant.QB1.fidelity``
|
|
193
|
+
|
|
194
|
+
method: ssro
|
|
195
|
+
gate: measure
|
|
196
|
+
implementation: constant
|
|
197
|
+
locus: QB1
|
|
198
|
+
metric: fidelity
|
|
199
|
+
suffixes: {}
|
|
200
|
+
|
|
201
|
+
``metrics.rb.prx.drag_crf.QB4.fidelity:par=d2``
|
|
202
|
+
|
|
203
|
+
method: rb
|
|
204
|
+
gate: prx
|
|
205
|
+
implementation: drag_crf
|
|
206
|
+
locus: QB4
|
|
207
|
+
metric: fidelity
|
|
208
|
+
suffixes: {"par": "d2"}
|
|
209
|
+
|
|
210
|
+
``metrics.ghz_state.QB1__QB2.coherence_lower_bound``
|
|
211
|
+
|
|
212
|
+
method: ghz_state
|
|
213
|
+
gate: None
|
|
214
|
+
implementation: None
|
|
215
|
+
locus: QB1__QB2
|
|
216
|
+
metric: coherence_lower_bound
|
|
217
|
+
suffixes: {}
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
method: str
|
|
222
|
+
gate: str | None
|
|
223
|
+
"""Name of the gate/quantum operation."""
|
|
224
|
+
implementation: str | None
|
|
225
|
+
"""Name of the gate implementation."""
|
|
226
|
+
locus: str
|
|
227
|
+
"""Sequence of names of QPU components on which the gate/operation is applied."""
|
|
228
|
+
metric: str
|
|
229
|
+
"""Measured metric."""
|
|
230
|
+
suffixes: dict[str, str] = field(default_factory=dict)
|
|
231
|
+
"""Suffixes defining the metric further (if any)."""
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def domain(self) -> Domain:
|
|
235
|
+
return Domain.METRIC
|
|
236
|
+
|
|
237
|
+
def __str__(self) -> str:
|
|
238
|
+
if self.method == "ghz_state":
|
|
239
|
+
parts = [self.domain, self.method, self.locus, self.metric]
|
|
240
|
+
else:
|
|
241
|
+
gate_str = self.gate if self.gate is not None else ""
|
|
242
|
+
impl_str = self.implementation if self.implementation is not None else ""
|
|
243
|
+
parts = [self.domain, self.method, gate_str, impl_str, self.locus, self.metric]
|
|
244
|
+
name = _FIELD_SEPARATOR.join(parts)
|
|
245
|
+
suffixes = _SUFFIX_SEPARATOR.join(f"{key}={value}" for key, value in self.suffixes.items())
|
|
246
|
+
if suffixes:
|
|
247
|
+
return f"{name}:{suffixes}"
|
|
248
|
+
return name
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def _parse(cls, method: str, method_specific_part: str) -> "QONMetric":
|
|
252
|
+
"""Parse a gate quality metric name.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
method: Method used to measure the gate metric.
|
|
256
|
+
method_specific_part: Dot-separated fields specific to ``method``, possibly followed by suffixes.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
ValueError: Failed to parse ``name`` because it was syntactically incorrect.
|
|
260
|
+
UnknownObservationError: Failed to parse ``name`` because it contains unknown elements.
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
# gate metrics may have suffixes, split them off
|
|
264
|
+
fragments = method_specific_part.split(_SUFFIX_SEPARATOR)
|
|
265
|
+
suffixes = _parse_suffixes(fragments[1:])
|
|
266
|
+
|
|
267
|
+
# parse the rest of the method specific part
|
|
268
|
+
fields = fragments[0]
|
|
269
|
+
if method in ("rb", "irb", "ssro"):
|
|
270
|
+
# {gate}.{implementation}.{locus_str}.{metric}"
|
|
271
|
+
parts = fields.split(_FIELD_SEPARATOR, maxsplit=3)
|
|
272
|
+
if len(parts) < 4:
|
|
273
|
+
raise ValueError(f"{method} gate quality metric name has less than 6 parts")
|
|
274
|
+
gate, implementation, locus, metric = parts
|
|
275
|
+
return cls(
|
|
276
|
+
method=method,
|
|
277
|
+
gate=gate,
|
|
278
|
+
implementation=implementation,
|
|
279
|
+
locus=locus,
|
|
280
|
+
metric=metric,
|
|
281
|
+
suffixes=suffixes,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if method == "ghz_state":
|
|
285
|
+
# {locus_str}.{metric}
|
|
286
|
+
parts = fields.split(_FIELD_SEPARATOR, maxsplit=1)
|
|
287
|
+
if len(parts) < 2:
|
|
288
|
+
raise ValueError(f"{method} gate quality metric name has less than 4 parts")
|
|
289
|
+
locus, metric = parts
|
|
290
|
+
return cls(
|
|
291
|
+
method=method,
|
|
292
|
+
gate=None,
|
|
293
|
+
implementation=None,
|
|
294
|
+
locus=locus,
|
|
295
|
+
metric=metric,
|
|
296
|
+
suffixes=suffixes,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
raise UnknownObservationError("Unknown gate quality metric.")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass(frozen=True)
|
|
303
|
+
class QONControllerSetting(QON):
|
|
304
|
+
"""QON representing a controller setting observation.
|
|
305
|
+
|
|
306
|
+
Has the form ``controllers.{controller}[.{subcontroller}]*.{setting}``.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
controller: str
|
|
310
|
+
"""Name of the controller."""
|
|
311
|
+
rest: str
|
|
312
|
+
"""Possible subcontroller names in a dotted structure, ending in the setting name."""
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def domain(self) -> Domain:
|
|
316
|
+
return Domain.CONTROLLER_SETTING
|
|
317
|
+
|
|
318
|
+
def __str__(self) -> str:
|
|
319
|
+
return _FIELD_SEPARATOR.join([self.domain, self.controller, self.rest])
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def _parse(cls, controller: str, controller_specific_part: str) -> "QONControllerSetting":
|
|
323
|
+
"""Parse a controller setting observation name."""
|
|
324
|
+
return cls(
|
|
325
|
+
controller=controller,
|
|
326
|
+
rest=controller_specific_part,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass(frozen=True)
|
|
331
|
+
class QONGateParam(QON):
|
|
332
|
+
"""QON representing a gate parameter observation.
|
|
333
|
+
|
|
334
|
+
Has the form ``gates.{gate}.{implementation}.{locus_str}.{parameter}``.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
gate: str
|
|
338
|
+
"""Name of the gate/quantum operation."""
|
|
339
|
+
implementation: str | None
|
|
340
|
+
"""Name of the gate implementation."""
|
|
341
|
+
locus: str
|
|
342
|
+
"""Sequence of names of QPU components on which the gate is applied."""
|
|
343
|
+
parameter: str
|
|
344
|
+
"""Name of the gate parameter. May have further dotted structure."""
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def domain(self) -> Domain:
|
|
348
|
+
return Domain.GATE_PARAMETER
|
|
349
|
+
|
|
350
|
+
def __str__(self) -> str:
|
|
351
|
+
impl_str = self.implementation if self.implementation is not None else ""
|
|
352
|
+
return _FIELD_SEPARATOR.join([self.domain, self.gate, impl_str, self.locus, self.parameter])
|
|
353
|
+
|
|
354
|
+
@classmethod
|
|
355
|
+
def _parse(cls, gate: str, rest: str) -> "QONGateParam":
|
|
356
|
+
"""Parse a gate parameter observation name."""
|
|
357
|
+
parts = rest.split(_FIELD_SEPARATOR, maxsplit=2)
|
|
358
|
+
if len(parts) < 3:
|
|
359
|
+
raise ValueError("Gate parameter observation name has less than 5 parts")
|
|
360
|
+
implementation, locus, param = parts
|
|
361
|
+
return cls(
|
|
362
|
+
gate=gate,
|
|
363
|
+
implementation=implementation,
|
|
364
|
+
locus=locus,
|
|
365
|
+
parameter=param,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _split_obs_name(obs_name: str) -> tuple[list[str], dict[str, Any]]:
|
|
370
|
+
"""Split the given observation name into path elements and suffixes."""
|
|
371
|
+
# some observation names may have suffixes, split them off
|
|
372
|
+
fragments = obs_name.split(_SUFFIX_SEPARATOR)
|
|
373
|
+
suffixes = _parse_suffixes(fragments[1:])
|
|
374
|
+
|
|
375
|
+
# split the path elements
|
|
376
|
+
path = fragments[0].split(_FIELD_SEPARATOR)
|
|
377
|
+
return path, suffixes
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
GATE_FIDELITY_METHODS = {
|
|
381
|
+
"prx": "rb",
|
|
382
|
+
"measure": "ssro",
|
|
383
|
+
}
|
|
384
|
+
"""Mapping from quantum operation name to the standard methods for obtaining its fidelity.
|
|
385
|
+
The default is "irb" for ops not mentioned here."""
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class ObservationFinder(dict):
|
|
389
|
+
"""Query structure for a set of observations.
|
|
390
|
+
|
|
391
|
+
This class enables reasonably efficient filtering of an observation set based on the observation
|
|
392
|
+
name elements (e.g. find all T1 times / parameters of a particular gate/impl/locus etc. in the set).
|
|
393
|
+
|
|
394
|
+
The class has utility methods for querying specific types observations. The idea is to keep
|
|
395
|
+
all the logic related to the structure of the observation names encapsulated in this class/module.
|
|
396
|
+
|
|
397
|
+
Currently implemented using a nested dictionary that follows the dotted structure of the observation names.
|
|
398
|
+
The nested dictionary is not ideal for all searches/filterings, but it's just an implementation detail that
|
|
399
|
+
can be improved later on without affecting the public API of this class.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
observations: Observations to include in the query structure.
|
|
403
|
+
skip_unparseable: If True, ignore any observation whose name cannot be parsed, otherwise
|
|
404
|
+
raise an exception.
|
|
405
|
+
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
def __init__(self, observations: Iterable[ObservationBase], skip_unparseable: bool = False):
|
|
409
|
+
def parse_observation_into_dict(name: str, dictionary: dict[str, Any]) -> tuple[dict[str, Any], str]:
|
|
410
|
+
"""Insert the given observation name, split into path elements, into a nested dictionary.
|
|
411
|
+
|
|
412
|
+
The returned values allow the caller to insert whatever they want under the last path element
|
|
413
|
+
of ``name`` in the nested dict.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
name: Observation name (aka dut_field) to be split into path elements.
|
|
417
|
+
dictionary: Nested dictionary in which the path elements of ``name`` are inserted.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
The dict corresponding to the second-last path element of ``name``, last path element of ``name``.
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ValueError: Failed to parse ``name`` because it was syntactically incorrect.
|
|
424
|
+
UnknownObservationError: Failed to parse ``name`` because it contains unknown elements.
|
|
425
|
+
|
|
426
|
+
"""
|
|
427
|
+
path, suffixes = _split_obs_name(name)
|
|
428
|
+
# check the category/domain of the observation
|
|
429
|
+
match path[0]:
|
|
430
|
+
case Domain.METRIC:
|
|
431
|
+
if len(path) < 4:
|
|
432
|
+
raise ValueError("Quality metric observation name has less than 4 parts.")
|
|
433
|
+
case Domain.CHARACTERIZATION:
|
|
434
|
+
if len(path) < 4:
|
|
435
|
+
raise ValueError("Characterization observation name has less than 4 parts.")
|
|
436
|
+
case Domain.CONTROLLER_SETTING:
|
|
437
|
+
if len(path) < 3:
|
|
438
|
+
raise ValueError("Controller setting observation name has less than 3 parts.")
|
|
439
|
+
case Domain.GATE_PARAMETER:
|
|
440
|
+
if len(path) < 5:
|
|
441
|
+
raise ValueError("Gate parameter observation name has less than 5 parts.")
|
|
442
|
+
case _:
|
|
443
|
+
raise UnknownObservationError("Unknown observation domain.")
|
|
444
|
+
for path_element in path[:-1]:
|
|
445
|
+
dictionary = dictionary.setdefault(path_element, {})
|
|
446
|
+
return dictionary, path[-1]
|
|
447
|
+
|
|
448
|
+
for obs in observations:
|
|
449
|
+
try:
|
|
450
|
+
last_dict, last_element = parse_observation_into_dict(obs.dut_field, self)
|
|
451
|
+
last_dict[last_element] = obs
|
|
452
|
+
except (ValueError, UnknownObservationError) as err:
|
|
453
|
+
message = f"{obs.dut_field}: {err}"
|
|
454
|
+
if skip_unparseable:
|
|
455
|
+
logger.warning(message)
|
|
456
|
+
else:
|
|
457
|
+
raise err.__class__(message)
|
|
458
|
+
|
|
459
|
+
def _build_dict(self, pre_path: Iterable[str], keys: Iterable[str], post_path: Iterable[str]) -> dict[str, float]:
|
|
460
|
+
"""Get the same property for multiple path elements, if it exists.
|
|
461
|
+
|
|
462
|
+
Follows ``pre_path`` to a base node, then for every item in ``keys`` follows ``[key] + post_path``
|
|
463
|
+
and gets the corresponding value.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
pre_path: Initial path in the tree to the base node.
|
|
467
|
+
keys: Path elements under the base node to retrieve.
|
|
468
|
+
post_path: Final path to follow for each of ``keys``.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Mapping from ``keys`` to the corresponding values. If a key is missing or
|
|
472
|
+
``[key] + post_path`` could not be followed, that particular key does not appear in the mapping.
|
|
473
|
+
|
|
474
|
+
Raises:
|
|
475
|
+
KeyError: Could not follow ``pre_path``.
|
|
476
|
+
|
|
477
|
+
"""
|
|
478
|
+
base_node: dict[str, Any] = self
|
|
479
|
+
for step in pre_path:
|
|
480
|
+
next_node = base_node.get(step)
|
|
481
|
+
if not next_node or not isinstance(next_node, dict):
|
|
482
|
+
raise KeyError(f"pre_path step {step} could not be found")
|
|
483
|
+
base_node = next_node
|
|
484
|
+
|
|
485
|
+
result = {}
|
|
486
|
+
for key in keys:
|
|
487
|
+
node: dict[str, Any] | ObservationBase | None = base_node.get(key)
|
|
488
|
+
if node:
|
|
489
|
+
for step in post_path:
|
|
490
|
+
if isinstance(node, ObservationBase):
|
|
491
|
+
break # skip this key
|
|
492
|
+
if isinstance(node, dict):
|
|
493
|
+
node = node.get(step)
|
|
494
|
+
if not node:
|
|
495
|
+
break # skip this key
|
|
496
|
+
else:
|
|
497
|
+
break # skip this key
|
|
498
|
+
else:
|
|
499
|
+
if isinstance(node, ObservationBase):
|
|
500
|
+
try:
|
|
501
|
+
result[key] = float(node.value) # type: ignore[arg-type]
|
|
502
|
+
except (ValueError, TypeError):
|
|
503
|
+
pass # skip this key if conversion fails
|
|
504
|
+
return result
|
|
505
|
+
|
|
506
|
+
def _get_path_value(self, path: Iterable[str]) -> float:
|
|
507
|
+
"""Follow ``path``, return the final value."""
|
|
508
|
+
node: dict[str, Any] | ObservationBase = self
|
|
509
|
+
for step in path:
|
|
510
|
+
if isinstance(node, ObservationBase):
|
|
511
|
+
raise KeyError(f"path step '{step}' could not be found")
|
|
512
|
+
next_node = node.get(step)
|
|
513
|
+
if next_node is None:
|
|
514
|
+
raise KeyError(f"path step '{step}' could not be found")
|
|
515
|
+
node = next_node
|
|
516
|
+
|
|
517
|
+
if not isinstance(node, ObservationBase):
|
|
518
|
+
raise KeyError(f"path {path} does not end in an observation")
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
return float(node.value) # type: ignore[arg-type]
|
|
522
|
+
except (ValueError, TypeError) as e:
|
|
523
|
+
raise ValueError(f"Cannot convert value to float: {type(node.value)}") from e
|
|
524
|
+
|
|
525
|
+
def _get_path_node(self, path: Iterable[str]) -> dict[str, Any]:
|
|
526
|
+
"""Follow ``path``, return the final node."""
|
|
527
|
+
node: dict[str, Any] | ObservationBase = self
|
|
528
|
+
for step in path:
|
|
529
|
+
if isinstance(node, ObservationBase):
|
|
530
|
+
raise KeyError(f"path step '{step}' could not be found")
|
|
531
|
+
next_node = node.get(step)
|
|
532
|
+
if next_node is None:
|
|
533
|
+
raise KeyError(f"path step '{step}' could not be found")
|
|
534
|
+
node = next_node
|
|
535
|
+
if isinstance(node, ObservationBase):
|
|
536
|
+
raise KeyError(f"path {path} does not end in a node")
|
|
537
|
+
return node
|
|
538
|
+
|
|
539
|
+
def get_coherence_times(self, components: Iterable[str]) -> tuple[dict[str, float], dict[str, float]]:
|
|
540
|
+
"""T1 and T2 coherence times for the given QPU components.
|
|
541
|
+
|
|
542
|
+
If not found, the component will not appear in the corresponding dict.
|
|
543
|
+
"""
|
|
544
|
+
try:
|
|
545
|
+
t1 = self._build_dict(["characterization", "model"], components, ["t1_time"])
|
|
546
|
+
t2 = self._build_dict(["characterization", "model"], components, ["t2_time"])
|
|
547
|
+
except KeyError as exc:
|
|
548
|
+
logger.warning("Missing characterization.model data: %s", exc)
|
|
549
|
+
return {}, {}
|
|
550
|
+
|
|
551
|
+
return t1, t2
|
|
552
|
+
|
|
553
|
+
def get_gate_duration(self, gate_name: str, impl_name: str, locus: Locus) -> float | None:
|
|
554
|
+
"""Duration for the given gate/implementation/locus (in s), or None if not found."""
|
|
555
|
+
locus_str = locus_to_locus_str(locus)
|
|
556
|
+
try:
|
|
557
|
+
return self._get_path_value(["gates", gate_name, impl_name, locus_str, "duration"])
|
|
558
|
+
except KeyError:
|
|
559
|
+
logger.warning("Missing duration for %s.%s.%s", gate_name, impl_name, locus_str)
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
def get_gate_fidelity(self, gate_name: str, impl_name: str, locus: Locus) -> float | None:
|
|
563
|
+
"""Fidelity of the given gate/implementation/locus, or None if not found."""
|
|
564
|
+
# irb is the default method
|
|
565
|
+
method = GATE_FIDELITY_METHODS.get(gate_name, "irb")
|
|
566
|
+
locus_str = locus_to_locus_str(locus)
|
|
567
|
+
try:
|
|
568
|
+
return self._get_path_value(["metrics", method, gate_name, impl_name, locus_str, "fidelity"])
|
|
569
|
+
except KeyError:
|
|
570
|
+
logger.warning("Missing fidelity for %s.%s.%s", gate_name, impl_name, locus_str)
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
def get_measure_errors(self, gate_name: str, impl_name: str, locus: Locus) -> tuple[float, float] | None:
|
|
574
|
+
"""Measurement errors of the given gate/implementation/locus, or None if not found."""
|
|
575
|
+
locus_str = locus_to_locus_str(locus)
|
|
576
|
+
try:
|
|
577
|
+
node = self._get_path_node(["metrics", "ssro", gate_name, impl_name, locus_str])
|
|
578
|
+
error_0_to_1 = node["error_0_to_1"].value
|
|
579
|
+
error_1_to_0 = node["error_1_to_0"].value
|
|
580
|
+
|
|
581
|
+
def convert_to_float(value):
|
|
582
|
+
try:
|
|
583
|
+
return float(value) # type: ignore[arg-type]
|
|
584
|
+
except (ValueError, TypeError) as e:
|
|
585
|
+
raise ValueError(f"Cannot convert value to float: {type(value)}") from e
|
|
586
|
+
|
|
587
|
+
return convert_to_float(error_0_to_1), convert_to_float(error_1_to_0)
|
|
588
|
+
except KeyError:
|
|
589
|
+
logger.warning("Missing errors for %s.%s.%s.", gate_name, impl_name, locus_str)
|
|
590
|
+
return None
|
{iqm_station_control_client-9.7.0.dist-info → iqm_station_control_client-9.8.0.dist-info}/RECORD
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
iqm/station_control/client/__init__.py,sha256=1ND-AkIE9xLGIscH3WN44eyll9nlFhXeyCm-8EDFGQ4,942
|
|
2
2
|
iqm/station_control/client/list_models.py,sha256=7JJyn5jCBOvMBJWdsVsPRLhtChMxqLa6O9hElpeXEt8,2701
|
|
3
3
|
iqm/station_control/client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
iqm/station_control/client/qon.py,sha256=7OLZPYwLUt-wx0bk8cmG3eVplGAgdTIQXpwKomFc9Q0,22495
|
|
4
5
|
iqm/station_control/client/station_control.py,sha256=cQqHuTu3K7iVtZzdtIuihTXa_GPydyZFIjZtyCPzh7s,29208
|
|
5
6
|
iqm/station_control/client/utils.py,sha256=-6K4KgOgA4iyUCqX-w26JvFxlwlGBehGj4tIWCEbn74,3360
|
|
6
7
|
iqm/station_control/client/iqm_server/__init__.py,sha256=nLsRHN1rnOKXwuzaq_liUpAYV3sis5jkyHccSdacV7U,624
|
|
@@ -51,8 +52,8 @@ iqm/station_control/interface/models/sequence.py,sha256=boWlMfP3woVgVObW3OaNbxsU
|
|
|
51
52
|
iqm/station_control/interface/models/static_quantum_architecture.py,sha256=gsfJKlYsfZVEK3dqEKXkBSIHiY14DGwNbhPJdNHMtNM,1435
|
|
52
53
|
iqm/station_control/interface/models/sweep.py,sha256=HFoFIrKhlYmHIBfGltY2O9_J28OvkkZILRbDHuqR0wc,2509
|
|
53
54
|
iqm/station_control/interface/models/type_aliases.py,sha256=gEYJ8zOpV1m0NVyYUbAL43psp9Lxw_0t68mYlI65Sds,1262
|
|
54
|
-
iqm_station_control_client-9.
|
|
55
|
-
iqm_station_control_client-9.
|
|
56
|
-
iqm_station_control_client-9.
|
|
57
|
-
iqm_station_control_client-9.
|
|
58
|
-
iqm_station_control_client-9.
|
|
55
|
+
iqm_station_control_client-9.8.0.dist-info/LICENSE.txt,sha256=R6Q7eUrLyoCQgWYorQ8WJmVmWKYU3dxA3jYUp0wwQAw,11332
|
|
56
|
+
iqm_station_control_client-9.8.0.dist-info/METADATA,sha256=2KR6OuIUaYaP9Vnky4FyklA27cbqaTnau3dPHjhY2Hk,14096
|
|
57
|
+
iqm_station_control_client-9.8.0.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
|
58
|
+
iqm_station_control_client-9.8.0.dist-info/top_level.txt,sha256=NB4XRfyDS6_wG9gMsyX-9LTU7kWnTQxNvkbzIxGv3-c,4
|
|
59
|
+
iqm_station_control_client-9.8.0.dist-info/RECORD,,
|
|
File without changes
|
{iqm_station_control_client-9.7.0.dist-info → iqm_station_control_client-9.8.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|