iqm-client 32.1.1__py3-none-any.whl → 33.0.1__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/cirq_iqm/devices/iqm_device_metadata.py +2 -1
- iqm/cirq_iqm/examples/demo_common.py +1 -1
- iqm/cirq_iqm/examples/demo_iqm_execution.py +3 -3
- iqm/cirq_iqm/iqm_sampler.py +47 -29
- iqm/cirq_iqm/serialize.py +1 -1
- iqm/cirq_iqm/transpiler.py +3 -1
- iqm/iqm_client/__init__.py +0 -2
- iqm/iqm_client/errors.py +6 -17
- iqm/iqm_client/iqm_client.py +199 -602
- iqm/iqm_client/models.py +20 -611
- iqm/iqm_client/transpile.py +11 -8
- iqm/iqm_client/validation.py +18 -9
- iqm/iqm_server_client/__init__.py +14 -0
- iqm/iqm_server_client/errors.py +6 -0
- iqm/iqm_server_client/iqm_server_client.py +755 -0
- iqm/iqm_server_client/models.py +179 -0
- iqm/iqm_server_client/py.typed +0 -0
- iqm/qiskit_iqm/__init__.py +8 -0
- iqm/qiskit_iqm/examples/bell_measure.py +5 -5
- iqm/qiskit_iqm/examples/transpile_example.py +13 -6
- iqm/qiskit_iqm/fake_backends/fake_adonis.py +2 -1
- iqm/qiskit_iqm/fake_backends/fake_aphrodite.py +2 -1
- iqm/qiskit_iqm/fake_backends/fake_apollo.py +2 -1
- iqm/qiskit_iqm/fake_backends/fake_deneb.py +2 -1
- iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +8 -7
- iqm/qiskit_iqm/iqm_backend.py +3 -4
- iqm/qiskit_iqm/iqm_circuit_validation.py +8 -7
- iqm/qiskit_iqm/iqm_job.py +106 -88
- iqm/qiskit_iqm/iqm_move_layout.py +2 -1
- iqm/qiskit_iqm/iqm_naive_move_pass.py +114 -55
- iqm/qiskit_iqm/iqm_provider.py +49 -36
- iqm/qiskit_iqm/iqm_target.py +4 -6
- iqm/qiskit_iqm/qiskit_to_iqm.py +62 -25
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.1.dist-info}/METADATA +17 -24
- iqm_client-33.0.1.dist-info/RECORD +63 -0
- iqm/iqm_client/api.py +0 -90
- iqm/iqm_client/authentication.py +0 -206
- iqm_client-32.1.1.dist-info/RECORD +0 -60
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.1.dist-info}/AUTHORS.rst +0 -0
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.1.dist-info}/LICENSE.txt +0 -0
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.1.dist-info}/WHEEL +0 -0
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.1.dist-info}/entry_points.txt +0 -0
- {iqm_client-32.1.1.dist-info → iqm_client-33.0.1.dist-info}/top_level.txt +0 -0
iqm/iqm_client/iqm_client.py
CHANGED
|
@@ -15,85 +15,65 @@
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
from
|
|
19
|
-
from functools import lru_cache
|
|
20
|
-
from http import HTTPStatus
|
|
21
|
-
from importlib.metadata import version
|
|
22
|
-
import json
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from functools import cache, lru_cache
|
|
23
20
|
import logging
|
|
24
21
|
import os
|
|
25
|
-
import
|
|
26
|
-
import time
|
|
27
|
-
from typing import Any, TypeVar
|
|
22
|
+
from typing import Any
|
|
28
23
|
from uuid import UUID
|
|
29
24
|
import warnings
|
|
30
25
|
|
|
31
|
-
from iqm.iqm_client.api import APIConfig, APIEndpoint
|
|
32
|
-
from iqm.iqm_client.authentication import TokenManager
|
|
33
26
|
from iqm.iqm_client.errors import (
|
|
34
|
-
APITimeoutError,
|
|
35
|
-
CircuitExecutionError,
|
|
36
27
|
CircuitValidationError,
|
|
37
|
-
ClientAuthenticationError,
|
|
38
|
-
ClientConfigurationError,
|
|
39
|
-
EndpointRequestError,
|
|
40
|
-
JobAbortionError,
|
|
41
28
|
)
|
|
42
|
-
from iqm.iqm_client.models import
|
|
29
|
+
from iqm.iqm_client.models import CircuitCompilationOptions, CircuitJobParameters, validate_circuit
|
|
30
|
+
from iqm.iqm_client.validation import validate_circuit_instructions, validate_qubit_mapping
|
|
31
|
+
from iqm.iqm_server_client.iqm_server_client import (
|
|
32
|
+
DEFAULT_TIMEOUT_SECONDS, # noqa: F401
|
|
33
|
+
IQMServerClientJob,
|
|
34
|
+
StrUUIDOrDefault,
|
|
35
|
+
_IQMServerClient,
|
|
36
|
+
)
|
|
37
|
+
from iqm.iqm_server_client.models import (
|
|
43
38
|
CalibrationSet,
|
|
39
|
+
JobStatus,
|
|
40
|
+
QualityMetricSet,
|
|
41
|
+
)
|
|
42
|
+
from iqm.models.channel_properties import AWGProperties
|
|
43
|
+
|
|
44
|
+
from iqm.station_control.client.qon import ObservationFinder
|
|
45
|
+
from iqm.station_control.interface.models import (
|
|
44
46
|
CircuitBatch,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
CircuitMeasurementCountsBatch,
|
|
48
|
+
CircuitMeasurementResults, # noqa: F401
|
|
49
|
+
CircuitMeasurementResultsBatch,
|
|
48
50
|
DynamicQuantumArchitecture,
|
|
49
51
|
QIRCode,
|
|
50
|
-
|
|
51
|
-
QuantumArchitectureSpecification,
|
|
52
|
-
RunCounts,
|
|
52
|
+
QubitMapping,
|
|
53
53
|
RunRequest,
|
|
54
|
-
RunResult,
|
|
55
|
-
RunStatus,
|
|
56
54
|
StaticQuantumArchitecture,
|
|
57
|
-
Status,
|
|
58
|
-
serialize_qubit_mapping,
|
|
59
|
-
validate_circuit,
|
|
60
55
|
)
|
|
61
|
-
from iqm.
|
|
62
|
-
from iqm.models.channel_properties import AWGProperties
|
|
63
|
-
from packaging.version import parse
|
|
64
|
-
from pydantic import BaseModel, ValidationError
|
|
65
|
-
import requests
|
|
66
|
-
from requests import HTTPError
|
|
67
|
-
|
|
68
|
-
from iqm.station_control.client.iqm_server.iqm_server_client import IqmServerClient
|
|
69
|
-
from iqm.station_control.client.qon import ObservationFinder
|
|
70
|
-
from iqm.station_control.client.utils import init_station_control
|
|
71
|
-
from iqm.station_control.interface.models import ObservationLite
|
|
72
|
-
from iqm.station_control.interface.station_control import StationControlInterface
|
|
73
|
-
|
|
74
|
-
T_BaseModel = TypeVar("T_BaseModel", bound=BaseModel)
|
|
75
|
-
|
|
76
|
-
REQUESTS_TIMEOUT = float(os.environ.get("IQM_CLIENT_REQUESTS_TIMEOUT", 120.0))
|
|
77
|
-
DEFAULT_TIMEOUT_SECONDS = 900
|
|
78
|
-
SECONDS_BETWEEN_CALLS = float(os.environ.get("IQM_CLIENT_SECONDS_BETWEEN_CALLS", 1.0))
|
|
79
|
-
|
|
56
|
+
from iqm.station_control.interface.models.circuit import _Circuit
|
|
80
57
|
|
|
81
58
|
logger = logging.getLogger(__name__)
|
|
82
59
|
|
|
83
60
|
|
|
84
61
|
class IQMClient:
|
|
85
|
-
"""Provides access to IQM quantum computers
|
|
62
|
+
"""Provides access to IQM quantum computers, enabling quantum circuit execution with
|
|
63
|
+
the selected quantum computer.
|
|
86
64
|
|
|
87
65
|
Args:
|
|
88
|
-
|
|
66
|
+
iqm_server_url: URL for accessing the IQM Server. Has to start with http or https.
|
|
67
|
+
quantum_computer: ID or alias of the quantum computer to connect to, if the IQM Server
|
|
68
|
+
instance controls more than one.
|
|
69
|
+
token: Long-lived authentication token in plain text format.
|
|
70
|
+
If ``token`` is given no other user authentication parameters should be given.
|
|
71
|
+
tokens_file: Path to a tokens file used for authentication.
|
|
72
|
+
If ``tokens_file`` is given no other user authentication parameters should be given.
|
|
89
73
|
client_signature: String that IQMClient adds to User-Agent header of requests
|
|
90
74
|
it sends to the server. The signature is appended to IQMClient's own version
|
|
91
75
|
information and is intended to carry additional version information,
|
|
92
76
|
for example the version information of the caller.
|
|
93
|
-
token: Long-lived authentication token in plain text format. Used by IQM Resonance.
|
|
94
|
-
If ``token`` is given no other user authentication parameters should be given.
|
|
95
|
-
tokens_file: Path to a tokens file used for authentication.
|
|
96
|
-
If ``tokens_file`` is given no other user authentication parameters should be given.
|
|
97
77
|
|
|
98
78
|
Alternatively, the user authentication related keyword arguments can also be given in
|
|
99
79
|
environment variables :envvar:`IQM_TOKEN`, :envvar:`IQM_TOKENS_FILE`.
|
|
@@ -105,55 +85,41 @@ class IQMClient:
|
|
|
105
85
|
"""
|
|
106
86
|
|
|
107
87
|
def __init__(
|
|
108
|
-
self,
|
|
88
|
+
self,
|
|
89
|
+
iqm_server_url: str,
|
|
90
|
+
*,
|
|
91
|
+
quantum_computer: str | None = None,
|
|
92
|
+
token: str | None = None,
|
|
93
|
+
tokens_file: str | None = None,
|
|
94
|
+
client_signature: str | None = None,
|
|
109
95
|
):
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
self._signature = f"{platform.platform(terse=True)}"
|
|
115
|
-
self._signature += f", python {platform.python_version()}"
|
|
116
|
-
self._signature += f", iqm-client {version(version_string)}"
|
|
117
|
-
if client_signature:
|
|
118
|
-
self._signature += f", {client_signature}"
|
|
119
|
-
self._architecture: QuantumArchitectureSpecification | None = None
|
|
120
|
-
self._static_architecture: StaticQuantumArchitecture | None = None
|
|
121
|
-
self._dynamic_architectures: dict[UUID, DynamicQuantumArchitecture] = {}
|
|
122
|
-
|
|
123
|
-
self._station_control: StationControlInterface = init_station_control(
|
|
124
|
-
root_url=url,
|
|
125
|
-
get_token_callback=self._token_manager.get_bearer_token, # type:ignore[arg-type]
|
|
96
|
+
self._iqm_server_client = _IQMServerClient(
|
|
97
|
+
iqm_server_url=iqm_server_url,
|
|
98
|
+
token=token,
|
|
99
|
+
tokens_file=tokens_file,
|
|
126
100
|
client_signature=client_signature,
|
|
101
|
+
quantum_computer=quantum_computer,
|
|
127
102
|
)
|
|
128
|
-
self.
|
|
129
|
-
if (version_incompatibility_msg := self._check_versions()) is not None:
|
|
130
|
-
warnings.warn(version_incompatibility_msg)
|
|
103
|
+
self._dynamic_quantum_architectures: dict[UUID, DynamicQuantumArchitecture] = {}
|
|
131
104
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
self.close_auth_session()
|
|
136
|
-
except Exception:
|
|
137
|
-
pass
|
|
105
|
+
def get_health(self) -> dict[str, Any]:
|
|
106
|
+
"""Status of the quantum computer."""
|
|
107
|
+
return self._iqm_server_client.get_health()
|
|
138
108
|
|
|
139
|
-
def get_about(self) -> dict:
|
|
140
|
-
"""
|
|
141
|
-
return self.
|
|
142
|
-
|
|
143
|
-
def get_health(self) -> dict:
|
|
144
|
-
"""Return the status of the station control service."""
|
|
145
|
-
return self._station_control.get_health()
|
|
109
|
+
def get_about(self) -> dict[str, Any]:
|
|
110
|
+
"""Information about the quantum computer."""
|
|
111
|
+
return self._iqm_server_client.get_about()
|
|
146
112
|
|
|
147
113
|
def submit_circuits(
|
|
148
114
|
self,
|
|
149
115
|
circuits: CircuitBatch,
|
|
150
116
|
*,
|
|
151
|
-
qubit_mapping:
|
|
152
|
-
custom_settings: dict[str, Any] | None = None,
|
|
117
|
+
qubit_mapping: QubitMapping | None = None,
|
|
153
118
|
calibration_set_id: UUID | None = None,
|
|
154
119
|
shots: int = 1,
|
|
155
120
|
options: CircuitCompilationOptions | None = None,
|
|
156
|
-
|
|
121
|
+
use_timeslot: bool = False,
|
|
122
|
+
) -> CircuitJob:
|
|
157
123
|
"""Submit a batch of quantum circuits for execution on a quantum computer.
|
|
158
124
|
|
|
159
125
|
Args:
|
|
@@ -161,33 +127,33 @@ class IQMClient:
|
|
|
161
127
|
qubit_mapping: Mapping of logical qubit names to physical qubit names.
|
|
162
128
|
Can be set to ``None`` if all ``circuits`` already use physical qubit names.
|
|
163
129
|
Note that the ``qubit_mapping`` is used for all ``circuits``.
|
|
164
|
-
custom_settings: Custom settings to override default settings and calibration data.
|
|
165
|
-
Note: This field should always be ``None`` in normal use.
|
|
166
130
|
calibration_set_id: ID of the calibration set to use, or ``None`` to use the current default calibration.
|
|
167
131
|
shots: Number of times ``circuits`` are executed. Must be greater than zero.
|
|
168
132
|
options: Various discrete options for compiling quantum circuits to instruction schedules.
|
|
133
|
+
use_timeslot: Submits the job to the timeslot queue if set to ``True``. If set to ``False``,
|
|
134
|
+
the job is submitted to the normal on-demand queue.
|
|
169
135
|
|
|
170
136
|
Returns:
|
|
171
|
-
|
|
137
|
+
Job object, containing the ID for the created job.
|
|
138
|
+
This ID is needed to query the job status and the execution results.
|
|
139
|
+
Alternatively you can use the methods of the job object.
|
|
172
140
|
|
|
173
141
|
"""
|
|
174
142
|
run_request = self.create_run_request(
|
|
175
143
|
circuits=circuits,
|
|
176
144
|
qubit_mapping=qubit_mapping,
|
|
177
|
-
custom_settings=custom_settings,
|
|
178
145
|
calibration_set_id=calibration_set_id,
|
|
179
146
|
shots=shots,
|
|
180
147
|
options=options,
|
|
181
148
|
)
|
|
182
|
-
|
|
183
|
-
return
|
|
149
|
+
job = self.submit_run_request(run_request, use_timeslot=use_timeslot)
|
|
150
|
+
return job
|
|
184
151
|
|
|
185
152
|
def create_run_request(
|
|
186
153
|
self,
|
|
187
154
|
circuits: CircuitBatch,
|
|
188
155
|
*,
|
|
189
|
-
qubit_mapping:
|
|
190
|
-
custom_settings: dict[str, Any] | None = None,
|
|
156
|
+
qubit_mapping: QubitMapping | None = None,
|
|
191
157
|
calibration_set_id: UUID | None = None,
|
|
192
158
|
shots: int = 1,
|
|
193
159
|
options: CircuitCompilationOptions | None = None,
|
|
@@ -204,8 +170,6 @@ class IQMClient:
|
|
|
204
170
|
qubit_mapping: Mapping of logical qubit names to physical qubit names.
|
|
205
171
|
Can be set to ``None`` if all ``circuits`` already use physical qubit names.
|
|
206
172
|
Note that the ``qubit_mapping`` is used for all ``circuits``.
|
|
207
|
-
custom_settings: Custom settings to override default settings and calibration data.
|
|
208
|
-
Note: This field should always be ``None`` in normal use.
|
|
209
173
|
calibration_set_id: ID of the calibration set to use, or ``None`` to use the current default calibration.
|
|
210
174
|
shots: Number of times ``circuits`` are executed. Must be greater than zero.
|
|
211
175
|
options: Various discrete options for compiling quantum circuits to instruction schedules.
|
|
@@ -221,7 +185,10 @@ class IQMClient:
|
|
|
221
185
|
|
|
222
186
|
for i, circuit in enumerate(circuits):
|
|
223
187
|
try:
|
|
224
|
-
if isinstance(circuit,
|
|
188
|
+
if isinstance(circuit, _Circuit):
|
|
189
|
+
raise CircuitValidationError(f"Circuit {i}: obsolete circuit type.")
|
|
190
|
+
if isinstance(circuit, QIRCode):
|
|
191
|
+
# do not validate
|
|
225
192
|
continue
|
|
226
193
|
# validate the circuit against the static information in iqm.iqm_client.models._SUPPORTED_OPERATIONS
|
|
227
194
|
validate_circuit(circuit)
|
|
@@ -242,259 +209,73 @@ class IQMClient:
|
|
|
242
209
|
must_close_sandwiches=False,
|
|
243
210
|
)
|
|
244
211
|
|
|
245
|
-
serialized_qubit_mapping = serialize_qubit_mapping(qubit_mapping) if qubit_mapping else None
|
|
246
|
-
|
|
247
212
|
return RunRequest(
|
|
248
|
-
qubit_mapping=
|
|
213
|
+
qubit_mapping=qubit_mapping,
|
|
249
214
|
circuits=circuits,
|
|
250
|
-
custom_settings=custom_settings,
|
|
251
215
|
calibration_set_id=calibration_set_id,
|
|
252
216
|
shots=shots,
|
|
253
217
|
max_circuit_duration_over_t2=options.max_circuit_duration_over_t2,
|
|
254
218
|
heralding_mode=options.heralding_mode,
|
|
255
|
-
|
|
256
|
-
|
|
219
|
+
move_gate_validation=options.move_gate_validation,
|
|
220
|
+
move_gate_frame_tracking=options.move_gate_frame_tracking,
|
|
257
221
|
active_reset_cycles=options.active_reset_cycles,
|
|
258
222
|
dd_mode=options.dd_mode,
|
|
259
223
|
dd_strategy=options.dd_strategy,
|
|
260
224
|
)
|
|
261
225
|
|
|
262
|
-
def submit_run_request(self, run_request: RunRequest) ->
|
|
226
|
+
def submit_run_request(self, run_request: RunRequest, use_timeslot: bool = False) -> CircuitJob:
|
|
263
227
|
"""Submit a run request for execution on a quantum computer.
|
|
264
228
|
|
|
265
229
|
This is called in :meth:`submit_circuits` and does not need to be called separately in normal usage.
|
|
266
230
|
|
|
267
231
|
Args:
|
|
268
232
|
run_request: Run request to be submitted for execution.
|
|
233
|
+
use_timeslot: Submits the job to the timeslot queue if set to ``True``. If set to ``False``,
|
|
234
|
+
the job is submitted to the normal on-demand queue.
|
|
269
235
|
|
|
270
236
|
Returns:
|
|
271
|
-
|
|
237
|
+
Job object, containing the ID for the created job.
|
|
238
|
+
This ID is needed to query the job status and the execution results.
|
|
239
|
+
Alternatively you can use the methods of the job object.
|
|
272
240
|
|
|
273
241
|
"""
|
|
274
|
-
headers = {
|
|
275
|
-
"Expect": "100-Continue",
|
|
276
|
-
**self._default_headers(),
|
|
277
|
-
}
|
|
278
|
-
try:
|
|
279
|
-
# Check if someone is trying to profile us with OpenTelemetry
|
|
280
|
-
from opentelemetry import propagate
|
|
281
|
-
|
|
282
|
-
propagate.inject(headers)
|
|
283
|
-
except ImportError as _:
|
|
284
|
-
# No OpenTelemetry, no problem
|
|
285
|
-
pass
|
|
286
|
-
|
|
287
242
|
if os.environ.get("IQM_CLIENT_DEBUG") == "1":
|
|
288
243
|
print(f"\nIQM CLIENT DEBUGGING ENABLED\nSUBMITTING RUN REQUEST:\n{run_request}\n")
|
|
289
244
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
# TODO SW-1434: Use station control client
|
|
293
|
-
self._api.url(APIEndpoint.SUBMIT_JOB),
|
|
294
|
-
data=run_request.model_dump_json(exclude_none=True).encode("utf-8"),
|
|
295
|
-
headers=headers | {"Content-Type": "application/json; charset=UTF-8"},
|
|
296
|
-
timeout=REQUESTS_TIMEOUT,
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
self._check_not_found_error(result)
|
|
300
|
-
|
|
301
|
-
if result.status_code == 401:
|
|
302
|
-
raise ClientAuthenticationError(f"Authentication failed: {result.text}")
|
|
303
|
-
|
|
304
|
-
if 400 <= result.status_code < 500:
|
|
305
|
-
raise ClientConfigurationError(f"Client configuration error: {result.text}")
|
|
245
|
+
job_data = self._iqm_server_client.submit_circuits(run_request, use_timeslot=use_timeslot)
|
|
246
|
+
return CircuitJob(_iqm_client=self, data=job_data)
|
|
306
247
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
try:
|
|
310
|
-
job_id = UUID(result.json()["id"])
|
|
311
|
-
return job_id
|
|
312
|
-
except (json.decoder.JSONDecodeError, KeyError) as e:
|
|
313
|
-
raise CircuitExecutionError(f"Invalid response: {result.text}, {e}") from e
|
|
314
|
-
|
|
315
|
-
def get_run(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> RunResult:
|
|
248
|
+
def get_job(self, job_id: UUID) -> CircuitJob:
|
|
316
249
|
"""Query the status and results of a submitted job.
|
|
317
250
|
|
|
318
251
|
Args:
|
|
319
252
|
job_id: ID of the job to query.
|
|
320
|
-
timeout_secs: Network request timeout (seconds).
|
|
321
253
|
|
|
322
254
|
Returns:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
Raises:
|
|
326
|
-
CircuitExecutionError: IQM server specific exceptions
|
|
327
|
-
HTTPException: HTTP exceptions
|
|
255
|
+
Status of the job, can be used to query the results if the job has finished.
|
|
328
256
|
|
|
329
257
|
"""
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
(str(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
status = status_response.json()
|
|
336
|
-
if Status(status["status"]) not in Status.terminal_statuses():
|
|
337
|
-
return RunResult.from_dict({"status": status["status"], "metadata": {}})
|
|
338
|
-
|
|
339
|
-
result = self._get_request(APIEndpoint.GET_JOB_RESULT, (str(job_id),), timeout=timeout_secs, allow_errors=True)
|
|
340
|
-
|
|
341
|
-
error_log_response = self._get_request(
|
|
342
|
-
APIEndpoint.GET_JOB_ERROR_LOG, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
343
|
-
)
|
|
344
|
-
if error_log_response.status_code == 200:
|
|
345
|
-
error_log = error_log_response.json()
|
|
346
|
-
if isinstance(error_log, dict) and "user_error_message" in error_log:
|
|
347
|
-
error_message = error_log["user_error_message"]
|
|
348
|
-
else:
|
|
349
|
-
# backwards compatibility for older error_log format
|
|
350
|
-
# TODO: remove when not needed anymore
|
|
351
|
-
error_message = error_log_response.text
|
|
352
|
-
else:
|
|
353
|
-
error_message = None
|
|
354
|
-
|
|
355
|
-
if result.status_code == 404:
|
|
356
|
-
run_result = RunResult.from_dict({"status": status["status"], "message": error_message, "metadata": {}})
|
|
357
|
-
else:
|
|
358
|
-
result.raise_for_status()
|
|
359
|
-
|
|
360
|
-
measurements = result.json()
|
|
361
|
-
request_parameters = self._get_request(
|
|
362
|
-
APIEndpoint.GET_JOB_REQUEST_PARAMETERS, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
363
|
-
).json()
|
|
364
|
-
calibration_set_id = self._get_request(
|
|
365
|
-
APIEndpoint.GET_JOB_CALIBRATION_SET_ID, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
366
|
-
).json()
|
|
367
|
-
circuits_batch = self._get_request(
|
|
368
|
-
APIEndpoint.GET_JOB_CIRCUITS_BATCH, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
369
|
-
).json()
|
|
370
|
-
timeline = self._get_request(
|
|
371
|
-
APIEndpoint.GET_JOB_TIMELINE, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
372
|
-
).json()
|
|
373
|
-
|
|
374
|
-
run_result = RunResult.from_dict(
|
|
375
|
-
{
|
|
376
|
-
"measurements": measurements,
|
|
377
|
-
"status": status["status"],
|
|
378
|
-
"message": error_message,
|
|
379
|
-
"metadata": {
|
|
380
|
-
"calibration_set_id": calibration_set_id,
|
|
381
|
-
"circuits_batch": circuits_batch,
|
|
382
|
-
"parameters": {
|
|
383
|
-
"shots": request_parameters["shots"],
|
|
384
|
-
"max_circuit_duration_over_t2": request_parameters.get(
|
|
385
|
-
"max_circuit_duration_over_t2", None
|
|
386
|
-
),
|
|
387
|
-
"heralding_mode": request_parameters["heralding_mode"],
|
|
388
|
-
"move_validation_mode": request_parameters["move_validation_mode"],
|
|
389
|
-
"move_gate_frame_tracking_mode": request_parameters["move_gate_frame_tracking_mode"],
|
|
390
|
-
},
|
|
391
|
-
"timestamps": {datapoint["status"]: datapoint["timestamp"] for datapoint in timeline},
|
|
392
|
-
},
|
|
393
|
-
"warnings": status.get("warnings", []),
|
|
394
|
-
}
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
if run_result.warnings:
|
|
398
|
-
for warning in run_result.warnings:
|
|
399
|
-
warnings.warn(warning)
|
|
400
|
-
if run_result.status == Status.FAILED:
|
|
401
|
-
raise CircuitExecutionError(run_result.message)
|
|
402
|
-
return run_result
|
|
403
|
-
|
|
404
|
-
def get_run_status(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> RunStatus:
|
|
405
|
-
"""Query the status of a submitted job.
|
|
406
|
-
|
|
407
|
-
Args:
|
|
408
|
-
job_id: ID of the job to query.
|
|
409
|
-
timeout_secs: Network request timeout (seconds).
|
|
410
|
-
|
|
411
|
-
Returns:
|
|
412
|
-
Job status.
|
|
413
|
-
|
|
414
|
-
Raises:
|
|
415
|
-
CircuitExecutionError: IQM server specific exceptions
|
|
416
|
-
HTTPException: HTTP exceptions
|
|
417
|
-
|
|
418
|
-
"""
|
|
419
|
-
response = self._get_request(
|
|
420
|
-
APIEndpoint.GET_JOB_STATUS,
|
|
421
|
-
(str(job_id),),
|
|
422
|
-
timeout=timeout_secs,
|
|
423
|
-
)
|
|
424
|
-
run_status = self._deserialize_response(response, RunStatus)
|
|
425
|
-
|
|
426
|
-
if run_status.warnings:
|
|
427
|
-
for warning in run_status.warnings:
|
|
428
|
-
warnings.warn(warning)
|
|
429
|
-
return run_status
|
|
430
|
-
|
|
431
|
-
def wait_for_compilation(self, job_id: UUID, timeout_secs: float = DEFAULT_TIMEOUT_SECONDS) -> RunResult:
|
|
432
|
-
"""Poll results until a job is either compiled, pending execution, ready, failed, aborted, or timed out.
|
|
433
|
-
|
|
434
|
-
Args:
|
|
435
|
-
job_id: ID of the job to wait for.
|
|
436
|
-
timeout_secs: How long to wait for a response before raising an APITimeoutError (seconds).
|
|
258
|
+
job_data = self._iqm_server_client.get_job(job_id)
|
|
259
|
+
for message in job_data.messages:
|
|
260
|
+
warnings.warn(str(message))
|
|
261
|
+
for error in job_data.errors:
|
|
262
|
+
warnings.warn(str(error))
|
|
437
263
|
|
|
438
|
-
|
|
439
|
-
Job result.
|
|
264
|
+
return CircuitJob(_iqm_client=self, data=job_data)
|
|
440
265
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
"""
|
|
445
|
-
start_time = datetime.now()
|
|
446
|
-
while (datetime.now() - start_time).total_seconds() < timeout_secs:
|
|
447
|
-
status = self.get_run_status(job_id).status
|
|
448
|
-
if status in Status.terminal_statuses() | {Status.PENDING_EXECUTION, Status.COMPILATION_ENDED}:
|
|
449
|
-
return self.get_run(job_id)
|
|
450
|
-
time.sleep(SECONDS_BETWEEN_CALLS)
|
|
451
|
-
raise APITimeoutError(f"The job {job_id} compilation didn't finish in {timeout_secs} seconds.")
|
|
452
|
-
|
|
453
|
-
def wait_for_results(self, job_id: UUID, timeout_secs: float = DEFAULT_TIMEOUT_SECONDS) -> RunResult:
|
|
454
|
-
"""Poll results until a job is either ready, failed, aborted, or timed out.
|
|
455
|
-
|
|
456
|
-
Note that jobs handling on the server side is async and if we try to request the results
|
|
457
|
-
right after submitting the job (which is usually the case)
|
|
458
|
-
we will find the job is still pending at least for the first query.
|
|
266
|
+
def cancel_job(self, job_id: UUID) -> None:
|
|
267
|
+
"""Cancel a job that was submitted for execution.
|
|
459
268
|
|
|
460
269
|
Args:
|
|
461
|
-
job_id: ID of the job to
|
|
462
|
-
timeout_secs: How long to wait for a response before raising an APITimeoutError (seconds).
|
|
463
|
-
|
|
464
|
-
Returns:
|
|
465
|
-
Job result.
|
|
466
|
-
|
|
467
|
-
Raises:
|
|
468
|
-
APITimeoutError: time exceeded the set timeout
|
|
270
|
+
job_id: ID of the job to be canceled.
|
|
469
271
|
|
|
470
272
|
"""
|
|
471
|
-
|
|
472
|
-
while (datetime.now() - start_time).total_seconds() < timeout_secs:
|
|
473
|
-
status = self.get_run_status(job_id).status
|
|
474
|
-
if status in Status.terminal_statuses():
|
|
475
|
-
return self.get_run(job_id)
|
|
476
|
-
time.sleep(SECONDS_BETWEEN_CALLS)
|
|
477
|
-
raise APITimeoutError(f"The job {job_id} didn't finish in {timeout_secs} seconds.")
|
|
478
|
-
|
|
479
|
-
def abort_job(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> None:
|
|
480
|
-
"""Abort a job that was submitted for execution.
|
|
481
|
-
|
|
482
|
-
Args:
|
|
483
|
-
job_id: ID of the job to be aborted.
|
|
484
|
-
timeout_secs: Network request timeout (seconds).
|
|
485
|
-
|
|
486
|
-
Raises:
|
|
487
|
-
JobAbortionError: aborting the job failed
|
|
273
|
+
self._iqm_server_client.cancel_job(job_id)
|
|
488
274
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
self._api.url(APIEndpoint.ABORT_JOB, str(job_id)),
|
|
492
|
-
headers=self._default_headers(),
|
|
493
|
-
timeout=timeout_secs,
|
|
494
|
-
)
|
|
495
|
-
if result.status_code not in (200, 204): # CoCos returns 200, station-control 204.
|
|
496
|
-
raise JobAbortionError(result.text)
|
|
275
|
+
def delete_job(self, job_id: UUID) -> None:
|
|
276
|
+
self._iqm_server_client.delete_job(job_id)
|
|
497
277
|
|
|
278
|
+
@cache
|
|
498
279
|
def get_static_quantum_architecture(self) -> StaticQuantumArchitecture:
|
|
499
280
|
"""Retrieve the static quantum architecture (SQA) from the server.
|
|
500
281
|
|
|
@@ -504,18 +285,12 @@ class IQMClient:
|
|
|
504
285
|
Static quantum architecture of the server.
|
|
505
286
|
|
|
506
287
|
Raises:
|
|
507
|
-
EndpointRequestError: did not understand the endpoint response
|
|
508
288
|
ClientAuthenticationError: no valid authentication provided
|
|
509
|
-
HTTPException: HTTP exceptions
|
|
510
289
|
|
|
511
290
|
"""
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
dut_label = self._get_dut_label()
|
|
516
|
-
static_quantum_architecture = self._station_control.get_static_quantum_architecture(dut_label)
|
|
517
|
-
self._static_architecture = StaticQuantumArchitecture(**static_quantum_architecture.model_dump())
|
|
518
|
-
return self._static_architecture
|
|
291
|
+
self._get_dut_label() # Called just to make sure that there will be only one DUT available
|
|
292
|
+
static_quantum_architectures = self._iqm_server_client.get_static_quantum_architectures()
|
|
293
|
+
return static_quantum_architectures[0]
|
|
519
294
|
|
|
520
295
|
def get_quality_metric_set(self, calibration_set_id: UUID | None = None) -> QualityMetricSet:
|
|
521
296
|
"""Retrieve the latest quality metric set for the given calibration set from the server.
|
|
@@ -528,51 +303,12 @@ class IQMClient:
|
|
|
528
303
|
Requested quality metric set.
|
|
529
304
|
|
|
530
305
|
Raises:
|
|
531
|
-
EndpointRequestError: did not understand the endpoint response
|
|
532
306
|
ClientAuthenticationError: no valid authentication provided
|
|
533
|
-
HTTPException: HTTP exceptions
|
|
534
307
|
|
|
535
308
|
"""
|
|
536
|
-
if
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if not calibration_set_id:
|
|
540
|
-
quality_metrics = self._station_control.get_default_calibration_set_quality_metrics()
|
|
541
|
-
else:
|
|
542
|
-
quality_metrics = self._station_control.get_calibration_set_quality_metrics(calibration_set_id)
|
|
543
|
-
|
|
544
|
-
calibration_set = quality_metrics.calibration_set
|
|
545
|
-
|
|
546
|
-
return QualityMetricSet(
|
|
547
|
-
**{ # type:ignore[arg-type]
|
|
548
|
-
"calibration_set_id": calibration_set.observation_set_id,
|
|
549
|
-
"calibration_set_dut_label": calibration_set.dut_label,
|
|
550
|
-
"calibration_set_number_of_observations": len(calibration_set.observation_ids),
|
|
551
|
-
"calibration_set_created_timestamp": str(calibration_set.created_timestamp.isoformat()),
|
|
552
|
-
"calibration_set_end_timestamp": None
|
|
553
|
-
if calibration_set.end_timestamp is None
|
|
554
|
-
else str(calibration_set.end_timestamp.isoformat()),
|
|
555
|
-
"calibration_set_is_invalid": calibration_set.invalid,
|
|
556
|
-
"quality_metric_set_id": quality_metrics.observation_set_id,
|
|
557
|
-
"quality_metric_set_dut_label": quality_metrics.dut_label,
|
|
558
|
-
"quality_metric_set_created_timestamp": str(quality_metrics.created_timestamp.isoformat()),
|
|
559
|
-
"quality_metric_set_end_timestamp": None
|
|
560
|
-
if quality_metrics.end_timestamp is None
|
|
561
|
-
else str(quality_metrics.end_timestamp.isoformat()),
|
|
562
|
-
"quality_metric_set_is_invalid": quality_metrics.invalid,
|
|
563
|
-
"metrics": {
|
|
564
|
-
observation.dut_field: {
|
|
565
|
-
"value": str(observation.value),
|
|
566
|
-
"unit": observation.unit,
|
|
567
|
-
"uncertainty": str(observation.uncertainty),
|
|
568
|
-
# created_timestamp is the interesting one, since observations are effectively immutable
|
|
569
|
-
"timestamp": str(observation.created_timestamp.isoformat()),
|
|
570
|
-
}
|
|
571
|
-
for observation in quality_metrics.observations
|
|
572
|
-
if not observation.invalid
|
|
573
|
-
},
|
|
574
|
-
}
|
|
575
|
-
)
|
|
309
|
+
_calibration_set_id: StrUUIDOrDefault = calibration_set_id if calibration_set_id is not None else "default"
|
|
310
|
+
quality_metrics = self._iqm_server_client.get_calibration_set_quality_metric_set(_calibration_set_id)
|
|
311
|
+
return QualityMetricSet(**quality_metrics.model_dump())
|
|
576
312
|
|
|
577
313
|
def get_calibration_set(self, calibration_set_id: UUID | None = None) -> CalibrationSet:
|
|
578
314
|
"""Retrieve the given calibration set from the server.
|
|
@@ -585,41 +321,12 @@ class IQMClient:
|
|
|
585
321
|
Requested calibration set.
|
|
586
322
|
|
|
587
323
|
Raises:
|
|
588
|
-
EndpointRequestError: did not understand the endpoint response
|
|
589
324
|
ClientAuthenticationError: no valid authentication provided
|
|
590
|
-
HTTPException: HTTP exceptions
|
|
591
325
|
|
|
592
326
|
"""
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
json_dict = obs.model_dump()
|
|
597
|
-
json_dict["created_timestamp"] = obs.created_timestamp.isoformat(timespec="microseconds")
|
|
598
|
-
json_dict["modified_timestamp"] = obs.modified_timestamp.isoformat(timespec="microseconds")
|
|
599
|
-
return json_dict
|
|
600
|
-
|
|
601
|
-
if isinstance(self._station_control, IqmServerClient):
|
|
602
|
-
raise ValueError("'get_calibration_set' method is not supported for IqmServerClient.")
|
|
603
|
-
|
|
604
|
-
if not calibration_set_id:
|
|
605
|
-
calibration_set = self._station_control.get_default_calibration_set()
|
|
606
|
-
else:
|
|
607
|
-
calibration_set = self._station_control.get_observation_set(calibration_set_id)
|
|
608
|
-
|
|
609
|
-
observations = self._station_control.get_observation_set_observations(calibration_set.observation_set_id)
|
|
610
|
-
|
|
611
|
-
return CalibrationSet(
|
|
612
|
-
calibration_set_id=calibration_set.observation_set_id,
|
|
613
|
-
calibration_set_dut_label=calibration_set.dut_label, # type:ignore[arg-type]
|
|
614
|
-
calibration_set_created_timestamp=str(calibration_set.created_timestamp.isoformat()),
|
|
615
|
-
calibration_set_end_timestamp=""
|
|
616
|
-
if calibration_set.end_timestamp is None
|
|
617
|
-
else str(calibration_set.end_timestamp.isoformat()),
|
|
618
|
-
calibration_set_is_invalid=calibration_set.invalid,
|
|
619
|
-
observations={
|
|
620
|
-
observation.dut_field: _observation_lite_to_json(observation) for observation in observations
|
|
621
|
-
},
|
|
622
|
-
)
|
|
327
|
+
_calibration_set_id: StrUUIDOrDefault = calibration_set_id if calibration_set_id is not None else "default"
|
|
328
|
+
calibration_set = self._iqm_server_client.get_calibration_set(_calibration_set_id)
|
|
329
|
+
return calibration_set
|
|
623
330
|
|
|
624
331
|
def get_dynamic_quantum_architecture(self, calibration_set_id: UUID | None = None) -> DynamicQuantumArchitecture:
|
|
625
332
|
"""Retrieve the dynamic quantum architecture (DQA) for the given calibration set from the server.
|
|
@@ -636,25 +343,18 @@ class IQMClient:
|
|
|
636
343
|
Dynamic quantum architecture corresponding to the given calibration set.
|
|
637
344
|
|
|
638
345
|
Raises:
|
|
639
|
-
EndpointRequestError: did not understand the endpoint response
|
|
640
346
|
ClientAuthenticationError: no valid authentication provided
|
|
641
|
-
HTTPException: HTTP exceptions
|
|
642
347
|
|
|
643
348
|
"""
|
|
644
|
-
if calibration_set_id in self.
|
|
645
|
-
return self.
|
|
646
|
-
|
|
647
|
-
if not
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
data = self._station_control.get_dynamic_quantum_architecture(calibration_set_id)
|
|
654
|
-
dynamic_quantum_architecture = DynamicQuantumArchitecture(**data.model_dump())
|
|
655
|
-
|
|
656
|
-
# Cache architecture so that later invocations do not need to query it again
|
|
657
|
-
self._dynamic_architectures[dynamic_quantum_architecture.calibration_set_id] = dynamic_quantum_architecture
|
|
349
|
+
if calibration_set_id in self._dynamic_quantum_architectures:
|
|
350
|
+
return self._dynamic_quantum_architectures[calibration_set_id]
|
|
351
|
+
|
|
352
|
+
_calibration_set_id: StrUUIDOrDefault = calibration_set_id if calibration_set_id is not None else "default"
|
|
353
|
+
dynamic_quantum_architecture = self._iqm_server_client.get_dynamic_quantum_architecture(_calibration_set_id)
|
|
354
|
+
|
|
355
|
+
self._dynamic_quantum_architectures[dynamic_quantum_architecture.calibration_set_id] = (
|
|
356
|
+
dynamic_quantum_architecture
|
|
357
|
+
)
|
|
658
358
|
return dynamic_quantum_architecture
|
|
659
359
|
|
|
660
360
|
def get_feedback_groups(self) -> tuple[frozenset[str], ...]:
|
|
@@ -669,12 +369,10 @@ class IQMClient:
|
|
|
669
369
|
If there is only one group, there are no restrictions regarding feedback routing.
|
|
670
370
|
|
|
671
371
|
Raises:
|
|
672
|
-
EndpointRequestError: did not understand the endpoint response
|
|
673
372
|
ClientAuthenticationError: no valid authentication provided
|
|
674
|
-
HTTPException: HTTP exceptions
|
|
675
373
|
|
|
676
374
|
"""
|
|
677
|
-
channel_properties = self.
|
|
375
|
+
channel_properties = self._iqm_server_client.get_channel_properties()
|
|
678
376
|
|
|
679
377
|
all_qubits = self.get_static_quantum_architecture().qubits
|
|
680
378
|
groups: dict[str, set[str]] = {}
|
|
@@ -693,210 +391,53 @@ class IQMClient:
|
|
|
693
391
|
# Sort by group size
|
|
694
392
|
return tuple(sorted(unique_groups, key=len, reverse=True))
|
|
695
393
|
|
|
696
|
-
def
|
|
697
|
-
"""Query the counts of an executed job.
|
|
394
|
+
def get_job_measurement_counts(self, job_id: UUID) -> CircuitMeasurementCountsBatch:
|
|
395
|
+
"""Query the measurement counts of an executed job.
|
|
698
396
|
|
|
699
397
|
Args:
|
|
700
398
|
job_id: ID of the job to query.
|
|
701
|
-
timeout_secs: Network request timeout (seconds).
|
|
702
399
|
|
|
703
400
|
Returns:
|
|
704
|
-
|
|
401
|
+
For each circuit in the batch, measurement results in histogram representation.
|
|
705
402
|
|
|
706
403
|
Raises:
|
|
707
|
-
EndpointRequestError: did not understand the endpoint response
|
|
708
404
|
ClientAuthenticationError: no valid authentication provided
|
|
709
|
-
HTTPException: HTTP exceptions
|
|
710
405
|
|
|
711
406
|
"""
|
|
712
|
-
|
|
713
|
-
APIEndpoint.GET_JOB_COUNTS,
|
|
714
|
-
(str(job_id),),
|
|
715
|
-
timeout=timeout_secs,
|
|
716
|
-
)
|
|
717
|
-
return self._deserialize_response(response, RunCounts)
|
|
407
|
+
return self._iqm_server_client.get_job_artifact_measurement_counts(job_id)
|
|
718
408
|
|
|
719
|
-
def
|
|
720
|
-
"""
|
|
409
|
+
def get_job_measurements(self, job_id: UUID) -> CircuitMeasurementResultsBatch:
|
|
410
|
+
"""Query the measurement results of an executed job.
|
|
721
411
|
|
|
722
412
|
Args:
|
|
723
|
-
|
|
413
|
+
job_id: ID of the job to query.
|
|
724
414
|
|
|
725
415
|
Returns:
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
Raises:
|
|
729
|
-
EndpointRequestError: did not understand the endpoint response
|
|
730
|
-
ClientAuthenticationError: no valid authentication provided
|
|
731
|
-
HTTPException: HTTP exceptions
|
|
416
|
+
For each circuit in the batch, the measurement results.
|
|
732
417
|
|
|
733
418
|
"""
|
|
734
|
-
|
|
735
|
-
# Version incompatibility shouldn't be a problem after that anymore,
|
|
736
|
-
# so we can delete this "client-libraries" implementation and usage.
|
|
737
|
-
response = requests.get(
|
|
738
|
-
# "/info/client-libraries" is implemented by Nginx so it won't work on locally running service.
|
|
739
|
-
# We will simply give warning in that case, so that IQMClient can be initialized also locally.
|
|
740
|
-
# "/station" is set by Nginx, so we will drop it to get the correct root for "/info/client-libraries".
|
|
741
|
-
self._api.station_control_url.replace("/station", "") + "/info/client-libraries",
|
|
742
|
-
headers=self._default_headers(),
|
|
743
|
-
timeout=timeout_secs,
|
|
744
|
-
)
|
|
745
|
-
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
746
|
-
return None
|
|
747
|
-
try:
|
|
748
|
-
return ClientLibraryDict.validate_json(response.text)
|
|
749
|
-
except ValidationError as e:
|
|
750
|
-
raise EndpointRequestError(f"Invalid response: {response.text}, {e!r}") from e
|
|
419
|
+
return self._iqm_server_client.get_job_artifact_measurements(job_id)
|
|
751
420
|
|
|
752
|
-
def
|
|
753
|
-
"""
|
|
754
|
-
|
|
755
|
-
Returns:
|
|
756
|
-
True iff session was successfully closed.
|
|
757
|
-
|
|
758
|
-
Raises:
|
|
759
|
-
ClientAuthenticationError: logout failed
|
|
760
|
-
ClientAuthenticationError: asked to close externally managed authentication session
|
|
421
|
+
def _get_submit_circuits_payload(self, job_id: UUID) -> RunRequest:
|
|
422
|
+
"""Get the original payload that created the given circuit job.
|
|
761
423
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
def _default_headers(self) -> dict[str, str]:
|
|
766
|
-
"""Default headers for HTTP requests to the IQM server."""
|
|
767
|
-
headers = {"User-Agent": self._signature}
|
|
768
|
-
if bearer_token := self._token_manager.get_bearer_token():
|
|
769
|
-
headers["Authorization"] = bearer_token
|
|
770
|
-
return headers
|
|
771
|
-
|
|
772
|
-
@staticmethod
|
|
773
|
-
def _check_authentication_errors(result: requests.Response) -> None:
|
|
774
|
-
"""Raise ClientAuthenticationError with appropriate message if the authentication failed for some reason."""
|
|
775
|
-
# for not strictly authenticated endpoints,
|
|
776
|
-
# we need to handle 302 redirects to the auth server login page
|
|
777
|
-
if result.history and any(
|
|
778
|
-
response.status_code == 302 for response in result.history
|
|
779
|
-
): # pragma: no cover (generators are broken in coverage)
|
|
780
|
-
raise ClientAuthenticationError("Authentication is required.")
|
|
781
|
-
if result.status_code == 401:
|
|
782
|
-
raise ClientAuthenticationError(f"Authentication failed: {result.text}")
|
|
783
|
-
|
|
784
|
-
def _check_not_found_error(self, response: requests.Response) -> None:
|
|
785
|
-
"""Raise HTTPError with appropriate message if ``response.status_code == 404``."""
|
|
786
|
-
if response.status_code == 404:
|
|
787
|
-
version_message = ""
|
|
788
|
-
if (version_incompatibility_msg := self._check_versions()) is not None:
|
|
789
|
-
version_message = (
|
|
790
|
-
f" This may be caused by the server version not supporting this endpoint. "
|
|
791
|
-
f"{version_incompatibility_msg}"
|
|
792
|
-
)
|
|
793
|
-
raise HTTPError(f"{response.url} not found.{version_message}", response=response)
|
|
794
|
-
|
|
795
|
-
def _check_versions(self) -> str | None:
|
|
796
|
-
"""Check the client version against compatible client versions reported by server.
|
|
424
|
+
Args:
|
|
425
|
+
job_id: ID of the job to query.
|
|
797
426
|
|
|
798
427
|
Returns:
|
|
799
|
-
|
|
800
|
-
compatibility could not be confirmed, ``None`` if they are compatible.
|
|
428
|
+
Payload of the original circuit job.
|
|
801
429
|
|
|
802
430
|
"""
|
|
803
|
-
|
|
804
|
-
libraries = self.get_supported_client_libraries()
|
|
805
|
-
if not libraries:
|
|
806
|
-
return "Got 'Not found' response from server. Couldn't check version compatibility."
|
|
807
|
-
compatible_iqm_client = libraries.get(
|
|
808
|
-
"iqm-client",
|
|
809
|
-
libraries.get("iqm_client"),
|
|
810
|
-
)
|
|
811
|
-
if compatible_iqm_client is None:
|
|
812
|
-
return "Could not verify IQM Client compatibility with the server. You might encounter issues."
|
|
813
|
-
min_version = parse(compatible_iqm_client.min)
|
|
814
|
-
max_version = parse(compatible_iqm_client.max)
|
|
815
|
-
client_version = parse(version("iqm-client"))
|
|
816
|
-
if client_version < min_version or client_version >= max_version:
|
|
817
|
-
return (
|
|
818
|
-
f"Your IQM Client version {client_version} was built for a different version of IQM Server. "
|
|
819
|
-
f"You might encounter issues. For the best experience, consider using a version "
|
|
820
|
-
f"of IQM Client that satisfies {min_version} <= iqm-client < {max_version}."
|
|
821
|
-
)
|
|
822
|
-
return None
|
|
823
|
-
except Exception as e:
|
|
824
|
-
# we don't want the version check to prevent usage of IQMClient in any situation
|
|
825
|
-
check_error = e
|
|
826
|
-
return f"Could not verify IQM Client compatibility with the server. You might encounter issues. {check_error}"
|
|
431
|
+
return self._iqm_server_client.get_submit_circuits_payload(job_id)
|
|
827
432
|
|
|
828
433
|
@lru_cache(maxsize=1)
|
|
829
434
|
def _get_dut_label(self) -> str:
|
|
830
|
-
|
|
435
|
+
"""Get the singular dut_label of the quantum computer, or raise an error."""
|
|
436
|
+
duts = self._iqm_server_client.get_duts()
|
|
831
437
|
if len(duts) != 1:
|
|
832
438
|
raise RuntimeError(f"Expected exactly 1 DUT, but got {len(duts)}.")
|
|
833
439
|
return duts[0].label
|
|
834
440
|
|
|
835
|
-
def _get_request(
|
|
836
|
-
self,
|
|
837
|
-
api_endpoint: APIEndpoint,
|
|
838
|
-
endpoint_args: tuple[str, ...] = (),
|
|
839
|
-
*,
|
|
840
|
-
timeout: float,
|
|
841
|
-
headers: dict | None = None,
|
|
842
|
-
allow_errors: bool = False,
|
|
843
|
-
) -> requests.Response:
|
|
844
|
-
"""Make an HTTP GET request to an IQM server endpoint.
|
|
845
|
-
|
|
846
|
-
Contains all the boilerplate code for making a simple GET request.
|
|
847
|
-
|
|
848
|
-
Args:
|
|
849
|
-
api_endpoint: API endpoint to GET.
|
|
850
|
-
endpoint_args: Arguments for the endpoint.
|
|
851
|
-
timeout: HTTP request timeout (in seconds).
|
|
852
|
-
|
|
853
|
-
Returns:
|
|
854
|
-
HTTP response to the request.
|
|
855
|
-
|
|
856
|
-
Raises:
|
|
857
|
-
ClientAuthenticationError: No valid authentication provided.
|
|
858
|
-
HTTPError: Various HTTP exceptions.
|
|
859
|
-
|
|
860
|
-
"""
|
|
861
|
-
url = self._api.url(api_endpoint, *endpoint_args)
|
|
862
|
-
response = requests.get(
|
|
863
|
-
url,
|
|
864
|
-
headers=headers or self._default_headers(),
|
|
865
|
-
timeout=timeout,
|
|
866
|
-
)
|
|
867
|
-
if not allow_errors:
|
|
868
|
-
self._check_not_found_error(response)
|
|
869
|
-
self._check_authentication_errors(response)
|
|
870
|
-
response.raise_for_status()
|
|
871
|
-
return response
|
|
872
|
-
|
|
873
|
-
@staticmethod
|
|
874
|
-
def _deserialize_response(
|
|
875
|
-
response: requests.Response,
|
|
876
|
-
model_class: type[T_BaseModel],
|
|
877
|
-
) -> T_BaseModel:
|
|
878
|
-
"""Deserialize a HTTP endpoint response.
|
|
879
|
-
|
|
880
|
-
Args:
|
|
881
|
-
response: HTTP response data.
|
|
882
|
-
model_class: Pydantic model to deserialize the data into.
|
|
883
|
-
|
|
884
|
-
Returns:
|
|
885
|
-
Deserialized endpoint response.
|
|
886
|
-
|
|
887
|
-
Raises:
|
|
888
|
-
EndpointRequestError: Did not understand the endpoint response.
|
|
889
|
-
|
|
890
|
-
"""
|
|
891
|
-
try:
|
|
892
|
-
model = model_class.model_validate(response.json())
|
|
893
|
-
# TODO this would be faster but MockJsonResponse.text in our unit tests cannot handle UUID
|
|
894
|
-
# model = model_class.model_validate_json(response.text)
|
|
895
|
-
except json.decoder.JSONDecodeError as e:
|
|
896
|
-
raise EndpointRequestError(f"Invalid response: {response.text}, {e!r}") from e
|
|
897
|
-
|
|
898
|
-
return model
|
|
899
|
-
|
|
900
441
|
def get_calibration_quality_metrics(self, calibration_set_id: UUID | None = None) -> ObservationFinder:
|
|
901
442
|
"""Retrieve the given calibration set and related quality metrics from the server.
|
|
902
443
|
|
|
@@ -914,9 +455,7 @@ class IQMClient:
|
|
|
914
455
|
Requested calibration set and related quality metrics in a searchable structure.
|
|
915
456
|
|
|
916
457
|
Raises:
|
|
917
|
-
EndpointRequestError: did not understand the endpoint response
|
|
918
458
|
ClientAuthenticationError: no valid authentication provided
|
|
919
|
-
HTTPException: HTTP exceptions
|
|
920
459
|
|
|
921
460
|
"""
|
|
922
461
|
logger.warning(
|
|
@@ -927,15 +466,73 @@ class IQMClient:
|
|
|
927
466
|
|
|
928
467
|
def _get_calibration_quality_metrics(self, calibration_set_id: UUID | None = None) -> ObservationFinder:
|
|
929
468
|
"""See :meth:`get_calibration_quality_metrics`."""
|
|
930
|
-
if
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
469
|
+
_calibration_set_id: StrUUIDOrDefault = calibration_set_id if calibration_set_id is not None else "default"
|
|
470
|
+
calibration_set = self._iqm_server_client.get_calibration_set(_calibration_set_id)
|
|
471
|
+
quality_metrics = self._iqm_server_client.get_calibration_set_quality_metric_set(_calibration_set_id)
|
|
472
|
+
return ObservationFinder(calibration_set.observations + quality_metrics.observations)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@dataclass
|
|
476
|
+
class CircuitJob(IQMServerClientJob):
|
|
477
|
+
"""Status and results of a quantum circuit execution job.
|
|
478
|
+
|
|
479
|
+
If the job succeeded, :meth:`result` returns the output of the batch of circuits.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
_iqm_client: IQMClient
|
|
483
|
+
"""Client instance used to create the job."""
|
|
484
|
+
|
|
485
|
+
_result: CircuitMeasurementResultsBatch | None = None
|
|
486
|
+
"""If the job has finished successfully, the measurement results for the circuit(s).
|
|
487
|
+
Populated by :meth:`result`
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
_circuits: CircuitBatch | None = None
|
|
491
|
+
"""Circuits batch submitted for execution. Populated by :meth:`payload`."""
|
|
492
|
+
|
|
493
|
+
_parameters: CircuitJobParameters | None = None
|
|
494
|
+
"""Job parameters sent in the execution request. Populated by :meth:`payload`."""
|
|
495
|
+
|
|
496
|
+
@property
|
|
497
|
+
def _iqm_server_client(self) -> _IQMServerClient:
|
|
498
|
+
return self._iqm_client._iqm_server_client
|
|
499
|
+
|
|
500
|
+
def result(self) -> CircuitMeasurementResultsBatch | None:
|
|
501
|
+
"""Get (and cache) the job result, if the job has completed.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Circuit measurement results for a completed job, or None if the results are not (yet?) available.
|
|
505
|
+
|
|
506
|
+
"""
|
|
507
|
+
# TODO should we name this method something more specific like "measurements"?
|
|
508
|
+
if not self._result:
|
|
509
|
+
self.update()
|
|
510
|
+
# if successful, get the results
|
|
511
|
+
if self.status != JobStatus.COMPLETED:
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
self._result = self._iqm_client.get_job_measurements(self.job_id)
|
|
515
|
+
# TODO refactor RunRequest
|
|
516
|
+
# Consider replacing CircuitCompilationOptions with CircuitJobParameters
|
|
517
|
+
|
|
518
|
+
for message in self.data.messages:
|
|
519
|
+
warnings.warn(f"{message.source}: {message.message}")
|
|
520
|
+
|
|
521
|
+
return self._result
|
|
522
|
+
|
|
523
|
+
def payload(self) -> tuple[CircuitBatch, CircuitJobParameters]:
|
|
524
|
+
"""Get the circuit job payload.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Circuits sent for execution, circuit execution options used.
|
|
528
|
+
|
|
529
|
+
"""
|
|
530
|
+
if not self._circuits or not self._parameters:
|
|
531
|
+
run_request = self._iqm_client._get_submit_circuits_payload(self.job_id)
|
|
532
|
+
# TODO: Remove "exclude" when deprecated computed fields are removed from the model
|
|
533
|
+
# `move_validation_mode` and `move_gate_frame_tracking_mode` are deprecated since 2025-10-17
|
|
534
|
+
run_request_dict = run_request.model_dump(exclude={"move_validation_mode", "move_gate_frame_tracking_mode"})
|
|
535
|
+
self._circuits = run_request_dict.pop("circuits")
|
|
536
|
+
self._parameters = CircuitJobParameters(**run_request_dict)
|
|
537
|
+
|
|
538
|
+
return self._circuits, self._parameters
|