iqm-station-control-client 9.7.0__tar.gz → 9.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/CHANGELOG.rst +16 -0
  2. {iqm_station_control_client-9.7.0/src/iqm_station_control_client.egg-info → iqm_station_control_client-9.9.0}/PKG-INFO +3 -3
  3. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/requirements/base.in +2 -2
  4. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/requirements/base.txt +2 -2
  5. iqm_station_control_client-9.9.0/src/iqm/station_control/client/qon.py +590 -0
  6. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0/src/iqm_station_control_client.egg-info}/PKG-INFO +3 -3
  7. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm_station_control_client.egg-info/SOURCES.txt +1 -0
  8. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm_station_control_client.egg-info/requires.txt +2 -2
  9. iqm_station_control_client-9.9.0/version.txt +1 -0
  10. iqm_station_control_client-9.7.0/version.txt +0 -1
  11. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/LICENSE.txt +0 -0
  12. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/MANIFEST.in +0 -0
  13. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/README.rst +0 -0
  14. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/API.rst +0 -0
  15. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/Makefile +0 -0
  16. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/_static/css/custom.css +0 -0
  17. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/_static/images/favicon.ico +0 -0
  18. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/_static/images/logo.png +0 -0
  19. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/_templates/autosummary-class-template.rst +0 -0
  20. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/_templates/autosummary-module-template.rst +0 -0
  21. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/changelog.rst +0 -0
  22. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/conf.py +0 -0
  23. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/index.rst +0 -0
  24. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/docs/license.rst +0 -0
  25. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/pyproject.toml +0 -0
  26. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/setup.cfg +0 -0
  27. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/setup.py +0 -0
  28. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/__init__.py +0 -0
  29. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/__init__.py +0 -0
  30. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/error.py +0 -0
  31. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/grpc_utils.py +0 -0
  32. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/iqm_server_client.py +0 -0
  33. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/__init__.py +0 -0
  34. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/calibration_pb2.py +0 -0
  35. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/calibration_pb2.pyi +0 -0
  36. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/calibration_pb2_grpc.py +0 -0
  37. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/common_pb2.py +0 -0
  38. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/common_pb2.pyi +0 -0
  39. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/common_pb2_grpc.py +0 -0
  40. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/job_pb2.py +0 -0
  41. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/job_pb2.pyi +0 -0
  42. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/job_pb2_grpc.py +0 -0
  43. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/qc_pb2.py +0 -0
  44. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/qc_pb2.pyi +0 -0
  45. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/qc_pb2_grpc.py +0 -0
  46. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/uuid_pb2.py +0 -0
  47. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/uuid_pb2.pyi +0 -0
  48. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/proto/uuid_pb2_grpc.py +0 -0
  49. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/testing/__init__.py +0 -0
  50. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/iqm_server/testing/iqm_server_mock.py +0 -0
  51. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/list_models.py +0 -0
  52. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/py.typed +0 -0
  53. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/__init__.py +0 -0
  54. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/channel_property_serializer.py +0 -0
  55. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/datetime_serializers.py +0 -0
  56. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/playlist_serializers.py +0 -0
  57. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/run_serializers.py +0 -0
  58. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/setting_node_serializer.py +0 -0
  59. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/struct_serializer.py +0 -0
  60. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/sweep_serializers.py +0 -0
  61. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/serializers/task_serializers.py +0 -0
  62. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/station_control.py +0 -0
  63. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/client/utils.py +0 -0
  64. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/__init__.py +0 -0
  65. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/list_with_meta.py +0 -0
  66. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/__init__.py +0 -0
  67. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/dut.py +0 -0
  68. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/dynamic_quantum_architecture.py +0 -0
  69. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/jobs.py +0 -0
  70. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/monitor.py +0 -0
  71. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/observation.py +0 -0
  72. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/observation_set.py +0 -0
  73. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/run.py +0 -0
  74. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/sequence.py +0 -0
  75. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/static_quantum_architecture.py +0 -0
  76. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/sweep.py +0 -0
  77. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/models/type_aliases.py +0 -0
  78. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/py.typed +0 -0
  79. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/pydantic_base.py +0 -0
  80. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm/station_control/interface/station_control.py +0 -0
  81. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm_station_control_client.egg-info/dependency_links.txt +0 -0
  82. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/src/iqm_station_control_client.egg-info/top_level.txt +0 -0
  83. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/tests/.pylintrc +0 -0
  84. {iqm_station_control_client-9.7.0 → iqm_station_control_client-9.9.0}/tests/__init__.py +0 -0
@@ -2,6 +2,22 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ Version 9.9.0 (2025-07-23)
6
+ ==========================
7
+
8
+ Bug fixes
9
+ ---------
10
+
11
+ - Relax PyYAML and opentelemetry-exporter-otlp dependencies
12
+
13
+ Version 9.8.0 (2025-07-16)
14
+ ==========================
15
+
16
+ Features
17
+ --------
18
+
19
+ - Unified parsing of observation names.
20
+
5
21
  Version 9.7.0 (2025-07-10)
6
22
  ==========================
7
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-station-control-client
3
- Version: 9.7.0
3
+ Version: 9.9.0
4
4
  Summary: Python client for communicating with Station Control Service
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -215,12 +215,12 @@ Description-Content-Type: text/x-rst
215
215
  License-File: LICENSE.txt
216
216
  Requires-Dist: iqm-exa-common<27,>=26
217
217
  Requires-Dist: iqm-data-definitions<3.0,>=2.13
218
- Requires-Dist: opentelemetry-exporter-otlp==1.25.0
218
+ Requires-Dist: opentelemetry-exporter-otlp<2.0,>=1.25.0
219
219
  Requires-Dist: protobuf<5.0,>=4.25.3
220
220
  Requires-Dist: types-protobuf
221
221
  Requires-Dist: grpcio<2.0,>=1.65.4
222
222
  Requires-Dist: pydantic<3.0,>=2.10.4
223
- Requires-Dist: PyYAML==6.0
223
+ Requires-Dist: PyYAML<7.0,>=6.0
224
224
  Requires-Dist: requests==2.32.3
225
225
  Requires-Dist: types-requests
226
226
  Requires-Dist: tqdm>=4.59.0
@@ -1,10 +1,10 @@
1
1
  iqm-data-definitions >= 2.13, < 3.0
2
- opentelemetry-exporter-otlp == 1.25.0
2
+ opentelemetry-exporter-otlp >= 1.25.0, < 2.0
3
3
  protobuf >= 4.25.3, < 5.0
4
4
  types-protobuf
5
5
  grpcio >= 1.65.4, < 2.0
6
6
  pydantic >= 2.10.4, <3.0
7
- PyYAML == 6.0
7
+ PyYAML >= 6.0, < 7.0
8
8
  requests == 2.32.3
9
9
  types-requests
10
10
  tqdm >= 4.59.0
@@ -1,11 +1,11 @@
1
1
  iqm-exa-common>=26,<27
2
2
  iqm-data-definitions >= 2.13, < 3.0
3
- opentelemetry-exporter-otlp == 1.25.0
3
+ opentelemetry-exporter-otlp >= 1.25.0, < 2.0
4
4
  protobuf >= 4.25.3, < 5.0
5
5
  types-protobuf
6
6
  grpcio >= 1.65.4, < 2.0
7
7
  pydantic >= 2.10.4, <3.0
8
- PyYAML == 6.0
8
+ PyYAML >= 6.0, < 7.0
9
9
  requests == 2.32.3
10
10
  types-requests
11
11
  tqdm >= 4.59.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: iqm-station-control-client
3
- Version: 9.7.0
3
+ Version: 9.9.0
4
4
  Summary: Python client for communicating with Station Control Service
5
5
  Author-email: IQM Finland Oy <info@meetiqm.com>
6
6
  License: Apache License
@@ -215,12 +215,12 @@ Description-Content-Type: text/x-rst
215
215
  License-File: LICENSE.txt
216
216
  Requires-Dist: iqm-exa-common<27,>=26
217
217
  Requires-Dist: iqm-data-definitions<3.0,>=2.13
218
- Requires-Dist: opentelemetry-exporter-otlp==1.25.0
218
+ Requires-Dist: opentelemetry-exporter-otlp<2.0,>=1.25.0
219
219
  Requires-Dist: protobuf<5.0,>=4.25.3
220
220
  Requires-Dist: types-protobuf
221
221
  Requires-Dist: grpcio<2.0,>=1.65.4
222
222
  Requires-Dist: pydantic<3.0,>=2.10.4
223
- Requires-Dist: PyYAML==6.0
223
+ Requires-Dist: PyYAML<7.0,>=6.0
224
224
  Requires-Dist: requests==2.32.3
225
225
  Requires-Dist: types-requests
226
226
  Requires-Dist: tqdm>=4.59.0
@@ -21,6 +21,7 @@ requirements/base.txt
21
21
  src/iqm/station_control/client/__init__.py
22
22
  src/iqm/station_control/client/list_models.py
23
23
  src/iqm/station_control/client/py.typed
24
+ src/iqm/station_control/client/qon.py
24
25
  src/iqm/station_control/client/station_control.py
25
26
  src/iqm/station_control/client/utils.py
26
27
  src/iqm/station_control/client/iqm_server/__init__.py
@@ -1,11 +1,11 @@
1
1
  iqm-exa-common<27,>=26
2
2
  iqm-data-definitions<3.0,>=2.13
3
- opentelemetry-exporter-otlp==1.25.0
3
+ opentelemetry-exporter-otlp<2.0,>=1.25.0
4
4
  protobuf<5.0,>=4.25.3
5
5
  types-protobuf
6
6
  grpcio<2.0,>=1.65.4
7
7
  pydantic<3.0,>=2.10.4
8
- PyYAML==6.0
8
+ PyYAML<7.0,>=6.0
9
9
  requests==2.32.3
10
10
  types-requests
11
11
  tqdm>=4.59.0
@@ -0,0 +1 @@
1
+ 9.9.0
@@ -1 +0,0 @@
1
- 9.7.0