iqm-client 28.0.0__py3-none-any.whl → 29.1.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/cirq_iqm/examples/demo_iqm_execution.py +3 -3
- iqm/iqm_client/api.py +24 -121
- iqm/iqm_client/authentication.py +1 -1
- iqm/iqm_client/iqm_client.py +284 -569
- iqm/iqm_client/models.py +0 -1
- iqm/iqm_client/transpile.py +3 -3
- iqm/iqm_client/validation.py +276 -0
- iqm/qiskit_iqm/examples/resonance_example.py +4 -0
- iqm/qiskit_iqm/iqm_circuit_validation.py +3 -2
- {iqm_client-28.0.0.dist-info → iqm_client-29.1.0.dist-info}/METADATA +9 -1
- {iqm_client-28.0.0.dist-info → iqm_client-29.1.0.dist-info}/RECORD +16 -15
- {iqm_client-28.0.0.dist-info → iqm_client-29.1.0.dist-info}/AUTHORS.rst +0 -0
- {iqm_client-28.0.0.dist-info → iqm_client-29.1.0.dist-info}/LICENSE.txt +0 -0
- {iqm_client-28.0.0.dist-info → iqm_client-29.1.0.dist-info}/WHEEL +0 -0
- {iqm_client-28.0.0.dist-info → iqm_client-29.1.0.dist-info}/entry_points.txt +0 -0
- {iqm_client-28.0.0.dist-info → iqm_client-29.1.0.dist-info}/top_level.txt +0 -0
iqm/iqm_client/iqm_client.py
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
-
from collections.abc import Callable, Iterable
|
|
19
18
|
from datetime import datetime
|
|
19
|
+
from functools import lru_cache
|
|
20
|
+
from http import HTTPStatus
|
|
20
21
|
from importlib.metadata import version
|
|
21
|
-
import itertools
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
24
|
import platform
|
|
@@ -27,7 +27,7 @@ from typing import Any, TypeVar
|
|
|
27
27
|
from uuid import UUID
|
|
28
28
|
import warnings
|
|
29
29
|
|
|
30
|
-
from iqm.iqm_client.api import APIConfig, APIEndpoint
|
|
30
|
+
from iqm.iqm_client.api import APIConfig, APIEndpoint
|
|
31
31
|
from iqm.iqm_client.authentication import TokenManager
|
|
32
32
|
from iqm.iqm_client.errors import (
|
|
33
33
|
APITimeoutError,
|
|
@@ -39,19 +39,13 @@ from iqm.iqm_client.errors import (
|
|
|
39
39
|
JobAbortionError,
|
|
40
40
|
)
|
|
41
41
|
from iqm.iqm_client.models import (
|
|
42
|
-
_SUPPORTED_OPERATIONS,
|
|
43
42
|
CalibrationSet,
|
|
44
|
-
Circuit,
|
|
45
43
|
CircuitBatch,
|
|
46
44
|
CircuitCompilationOptions,
|
|
47
45
|
ClientLibrary,
|
|
48
46
|
ClientLibraryDict,
|
|
49
|
-
DictDict,
|
|
50
47
|
DynamicQuantumArchitecture,
|
|
51
|
-
Instruction,
|
|
52
|
-
MoveGateValidationMode,
|
|
53
48
|
QualityMetricSet,
|
|
54
|
-
QuantumArchitecture,
|
|
55
49
|
QuantumArchitectureSpecification,
|
|
56
50
|
RunCounts,
|
|
57
51
|
RunRequest,
|
|
@@ -62,12 +56,18 @@ from iqm.iqm_client.models import (
|
|
|
62
56
|
serialize_qubit_mapping,
|
|
63
57
|
validate_circuit,
|
|
64
58
|
)
|
|
59
|
+
from iqm.iqm_client.validation import validate_circuit_instructions, validate_qubit_mapping
|
|
60
|
+
from iqm.models.channel_properties import AWGProperties
|
|
65
61
|
from packaging.version import parse
|
|
66
|
-
from pydantic import BaseModel
|
|
67
|
-
from pydantic import ValidationError as PydanticValidationError
|
|
62
|
+
from pydantic import BaseModel, ValidationError
|
|
68
63
|
import requests
|
|
69
64
|
from requests import HTTPError
|
|
70
65
|
|
|
66
|
+
from iqm.station_control.client.iqm_server.iqm_server_client import IqmServerClient
|
|
67
|
+
from iqm.station_control.client.utils import init_station_control
|
|
68
|
+
from iqm.station_control.interface.models import ObservationLite
|
|
69
|
+
from iqm.station_control.interface.station_control import StationControlInterface
|
|
70
|
+
|
|
71
71
|
T_BaseModel = TypeVar("T_BaseModel", bound=BaseModel)
|
|
72
72
|
|
|
73
73
|
REQUESTS_TIMEOUT = float(os.environ.get("IQM_CLIENT_REQUESTS_TIMEOUT", 120.0))
|
|
@@ -92,8 +92,6 @@ class IQMClient:
|
|
|
92
92
|
If ``auth_server_url`` is given also ``username`` and ``password`` must be given.
|
|
93
93
|
username: Username to log in to authentication server.
|
|
94
94
|
password: Password to log in to authentication server.
|
|
95
|
-
api_variant: API variant to use. Default is ``APIVariant.V1``.
|
|
96
|
-
Configurable also by environment variable ``IQM_CLIENT_API_VARIANT``.
|
|
97
95
|
|
|
98
96
|
Alternatively, the user authentication related keyword arguments can also be given in
|
|
99
97
|
environment variables :envvar:`IQM_TOKEN`, :envvar:`IQM_TOKENS_FILE`, :envvar:`IQM_AUTH_SERVER`,
|
|
@@ -113,7 +111,6 @@ class IQMClient:
|
|
|
113
111
|
auth_server_url: str | None = None,
|
|
114
112
|
username: str | None = None,
|
|
115
113
|
password: str | None = None,
|
|
116
|
-
api_variant: APIVariant | None = None,
|
|
117
114
|
):
|
|
118
115
|
if not url.startswith(("http:", "https:")):
|
|
119
116
|
raise ClientConfigurationError(f"The URL schema has to be http or https. Incorrect schema in URL: {url}")
|
|
@@ -134,104 +131,29 @@ class IQMClient:
|
|
|
134
131
|
self._static_architecture: StaticQuantumArchitecture | None = None
|
|
135
132
|
self._dynamic_architectures: dict[UUID, DynamicQuantumArchitecture] = {}
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
self._station_control: StationControlInterface = init_station_control(
|
|
135
|
+
root_url=url,
|
|
136
|
+
get_token_callback=self._token_manager.get_bearer_token,
|
|
137
|
+
client_signature=client_signature,
|
|
138
|
+
)
|
|
139
|
+
self._api = APIConfig(url)
|
|
141
140
|
if (version_incompatibility_msg := self._check_versions()) is not None:
|
|
142
141
|
warnings.warn(version_incompatibility_msg)
|
|
143
142
|
|
|
144
143
|
def __del__(self):
|
|
145
144
|
try:
|
|
146
|
-
#
|
|
145
|
+
# Try our best to close the auth session, doesn't matter if it fails
|
|
147
146
|
self.close_auth_session()
|
|
148
147
|
except Exception:
|
|
149
148
|
pass
|
|
150
149
|
|
|
151
|
-
def
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
The current implementation of the server side can run out of network connections
|
|
155
|
-
and silently drop incoming connections making IQM Client to fail with 502 errors.
|
|
156
|
-
"""
|
|
157
|
-
while True:
|
|
158
|
-
result = request()
|
|
159
|
-
if result.status_code == 502:
|
|
160
|
-
time.sleep(SECONDS_BETWEEN_CALLS)
|
|
161
|
-
continue
|
|
162
|
-
break
|
|
163
|
-
|
|
164
|
-
return result
|
|
150
|
+
def get_about(self) -> dict:
|
|
151
|
+
"""Return information about the IQM client."""
|
|
152
|
+
return self._station_control.get_about()
|
|
165
153
|
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
endpoint_args: tuple[str, ...] = (),
|
|
170
|
-
*,
|
|
171
|
-
timeout: float,
|
|
172
|
-
retry: bool = False,
|
|
173
|
-
allow_errors: bool = False,
|
|
174
|
-
) -> requests.Response:
|
|
175
|
-
"""Make a HTTP GET request to an IQM server endpoint.
|
|
176
|
-
|
|
177
|
-
Contains all the boilerplate code for making a simple GET request.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
api_endpoint: API endpoint to GET.
|
|
181
|
-
endpoint_args: Arguments for the endpoint.
|
|
182
|
-
timeout: HTTP request timeout (in seconds).
|
|
183
|
-
retry: Iff True, keep trying if you get a 502 error.
|
|
184
|
-
allow_errors: Iff true, don't raise exceptions for error responses.
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
HTTP response to the request.
|
|
188
|
-
|
|
189
|
-
Raises:
|
|
190
|
-
ClientAuthenticationError: No valid authentication provided.
|
|
191
|
-
HTTPError: Various HTTP exceptions.
|
|
192
|
-
|
|
193
|
-
"""
|
|
194
|
-
url = self._api.url(api_endpoint, *endpoint_args)
|
|
195
|
-
|
|
196
|
-
def request():
|
|
197
|
-
return requests.get(
|
|
198
|
-
url,
|
|
199
|
-
headers=self._default_headers(),
|
|
200
|
-
timeout=timeout,
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
response = self._retry_request_on_error(request) if retry else request()
|
|
204
|
-
if not allow_errors:
|
|
205
|
-
self._check_not_found_error(response)
|
|
206
|
-
self._check_authentication_errors(response)
|
|
207
|
-
response.raise_for_status()
|
|
208
|
-
return response
|
|
209
|
-
|
|
210
|
-
def _deserialize_response(
|
|
211
|
-
self,
|
|
212
|
-
response: requests.Response,
|
|
213
|
-
model_class: type[T_BaseModel],
|
|
214
|
-
) -> T_BaseModel:
|
|
215
|
-
"""Deserialize a HTTP endpoint response.
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
response: HTTP response data.
|
|
219
|
-
model_class: Pydantic model to deserialize the data into.
|
|
220
|
-
|
|
221
|
-
Returns:
|
|
222
|
-
Deserialized endpoint response.
|
|
223
|
-
|
|
224
|
-
Raises:
|
|
225
|
-
EndpointRequestError: Did not understand the endpoint response.
|
|
226
|
-
|
|
227
|
-
"""
|
|
228
|
-
try:
|
|
229
|
-
model = model_class.model_validate(response.json())
|
|
230
|
-
# TODO this would be faster but MockJsonResponse.text in our unit tests cannot handle UUID
|
|
231
|
-
# model = model_class.model_validate_json(response.text)
|
|
232
|
-
except json.decoder.JSONDecodeError as e:
|
|
233
|
-
raise EndpointRequestError(f"Invalid response: {response.text}, {e!r}") from e
|
|
234
|
-
return model
|
|
154
|
+
def get_health(self) -> dict:
|
|
155
|
+
"""Return the status of the station control service."""
|
|
156
|
+
return self._station_control.get_health()
|
|
235
157
|
|
|
236
158
|
def submit_circuits(
|
|
237
159
|
self,
|
|
@@ -317,19 +239,20 @@ class IQMClient:
|
|
|
317
239
|
e.__traceback__
|
|
318
240
|
)
|
|
319
241
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
self._validate_qubit_mapping(architecture, circuits, qubit_mapping)
|
|
323
|
-
serialized_qubit_mapping = serialize_qubit_mapping(qubit_mapping) if qubit_mapping else None
|
|
242
|
+
dynamic_quantum_architecture = self.get_dynamic_quantum_architecture(calibration_set_id)
|
|
324
243
|
|
|
244
|
+
validate_qubit_mapping(dynamic_quantum_architecture, circuits, qubit_mapping)
|
|
325
245
|
# validate the circuit against the calibration-dependent dynamic quantum architecture
|
|
326
|
-
|
|
327
|
-
|
|
246
|
+
validate_circuit_instructions(
|
|
247
|
+
dynamic_quantum_architecture,
|
|
328
248
|
circuits,
|
|
329
249
|
qubit_mapping,
|
|
330
250
|
validate_moves=options.move_gate_validation,
|
|
331
251
|
must_close_sandwiches=False,
|
|
332
252
|
)
|
|
253
|
+
|
|
254
|
+
serialized_qubit_mapping = serialize_qubit_mapping(qubit_mapping) if qubit_mapping else None
|
|
255
|
+
|
|
333
256
|
return RunRequest(
|
|
334
257
|
qubit_mapping=serialized_qubit_mapping,
|
|
335
258
|
circuits=circuits,
|
|
@@ -362,25 +285,24 @@ class IQMClient:
|
|
|
362
285
|
**self._default_headers(),
|
|
363
286
|
}
|
|
364
287
|
try:
|
|
365
|
-
#
|
|
288
|
+
# Check if someone is trying to profile us with OpenTelemetry
|
|
366
289
|
from opentelemetry import propagate
|
|
367
290
|
|
|
368
291
|
propagate.inject(headers)
|
|
369
292
|
except ImportError as _:
|
|
370
|
-
#
|
|
293
|
+
# No OpenTelemetry, no problem
|
|
371
294
|
pass
|
|
372
295
|
|
|
373
296
|
if os.environ.get("IQM_CLIENT_DEBUG") == "1":
|
|
374
297
|
print(f"\nIQM CLIENT DEBUGGING ENABLED\nSUBMITTING RUN REQUEST:\n{run_request}\n")
|
|
375
298
|
|
|
376
|
-
#
|
|
377
|
-
result =
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
)
|
|
299
|
+
# Use UTF-8 encoding for the JSON payload
|
|
300
|
+
result = requests.post(
|
|
301
|
+
# TODO SW-1434: Use station control client
|
|
302
|
+
self._api.url(APIEndpoint.SUBMIT_JOB),
|
|
303
|
+
data=run_request.model_dump_json(exclude_none=True).encode("utf-8"),
|
|
304
|
+
headers=headers | {"Content-Type": "application/json; charset=UTF-8"},
|
|
305
|
+
timeout=REQUESTS_TIMEOUT,
|
|
384
306
|
)
|
|
385
307
|
|
|
386
308
|
self._check_not_found_error(result)
|
|
@@ -399,275 +321,21 @@ class IQMClient:
|
|
|
399
321
|
except (json.decoder.JSONDecodeError, KeyError) as e:
|
|
400
322
|
raise CircuitExecutionError(f"Invalid response: {result.text}, {e}") from e
|
|
401
323
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
architecture: DynamicQuantumArchitecture,
|
|
405
|
-
circuits: CircuitBatch,
|
|
406
|
-
qubit_mapping: dict[str, str] | None = None,
|
|
407
|
-
) -> None:
|
|
408
|
-
"""Validate the given qubit mapping.
|
|
409
|
-
|
|
410
|
-
Args:
|
|
411
|
-
architecture: Quantum architecture to check against.
|
|
412
|
-
circuits: Circuits to be checked.
|
|
413
|
-
qubit_mapping: Mapping of logical qubit names to physical qubit names.
|
|
414
|
-
Can be set to ``None`` if all ``circuits`` already use physical qubit names.
|
|
415
|
-
Note that the ``qubit_mapping`` is used for all ``circuits``.
|
|
416
|
-
|
|
417
|
-
Raises:
|
|
418
|
-
CircuitValidationError: There was something wrong with ``circuits``.
|
|
419
|
-
|
|
420
|
-
"""
|
|
421
|
-
if qubit_mapping is None:
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
# check if qubit mapping is injective
|
|
425
|
-
target_qubits = set(qubit_mapping.values())
|
|
426
|
-
if not len(target_qubits) == len(qubit_mapping):
|
|
427
|
-
raise CircuitValidationError("Multiple logical qubits map to the same physical qubit.")
|
|
428
|
-
|
|
429
|
-
# check if qubit mapping covers all qubits in the circuits
|
|
430
|
-
for i, circuit in enumerate(circuits):
|
|
431
|
-
diff = circuit.all_qubits() - set(qubit_mapping)
|
|
432
|
-
if diff:
|
|
433
|
-
raise CircuitValidationError(
|
|
434
|
-
f"The qubits {diff} in circuit '{circuit.name}' at index {i} "
|
|
435
|
-
f"are not found in the provided qubit mapping."
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
# check that each mapped qubit is defined in the quantum architecture
|
|
439
|
-
for _logical, physical in qubit_mapping.items():
|
|
440
|
-
if physical not in architecture.components:
|
|
441
|
-
raise CircuitValidationError(f"Component {physical} not present in dynamic quantum architecture")
|
|
442
|
-
|
|
443
|
-
@staticmethod
|
|
444
|
-
def _validate_circuit_instructions(
|
|
445
|
-
architecture: DynamicQuantumArchitecture,
|
|
446
|
-
circuits: CircuitBatch,
|
|
447
|
-
qubit_mapping: dict[str, str] | None = None,
|
|
448
|
-
validate_moves: MoveGateValidationMode = MoveGateValidationMode.STRICT,
|
|
449
|
-
*,
|
|
450
|
-
must_close_sandwiches: bool = True,
|
|
451
|
-
) -> None:
|
|
452
|
-
"""Validate the given circuits against the given quantum architecture.
|
|
453
|
-
|
|
454
|
-
Args:
|
|
455
|
-
architecture: Quantum architecture to check against.
|
|
456
|
-
circuits: Circuits to be checked.
|
|
457
|
-
qubit_mapping: Mapping of logical qubit names to physical qubit names.
|
|
458
|
-
Can be set to ``None`` if all ``circuits`` already use physical qubit names.
|
|
459
|
-
Note that the ``qubit_mapping`` is used for all ``circuits``.
|
|
460
|
-
validate_moves: Determines how MOVE gate validation works.
|
|
461
|
-
must_close_sandwiches: Iff True, MOVE sandwiches cannot be left open when the circuit ends.
|
|
462
|
-
|
|
463
|
-
Raises:
|
|
464
|
-
CircuitValidationError: validation failed
|
|
465
|
-
|
|
466
|
-
"""
|
|
467
|
-
for index, circuit in enumerate(circuits):
|
|
468
|
-
measurement_keys: set[str] = set()
|
|
469
|
-
for instr in circuit.instructions:
|
|
470
|
-
IQMClient._validate_instruction(architecture, instr, qubit_mapping)
|
|
471
|
-
# check measurement key uniqueness
|
|
472
|
-
if instr.name in {"measure", "measurement"}:
|
|
473
|
-
key = instr.args["key"]
|
|
474
|
-
if key in measurement_keys:
|
|
475
|
-
raise CircuitValidationError(f"Circuit {index}: {instr!r} has a non-unique measurement key.")
|
|
476
|
-
measurement_keys.add(key)
|
|
477
|
-
IQMClient._validate_circuit_moves(
|
|
478
|
-
architecture,
|
|
479
|
-
circuit,
|
|
480
|
-
qubit_mapping,
|
|
481
|
-
validate_moves=validate_moves,
|
|
482
|
-
must_close_sandwiches=must_close_sandwiches,
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
@staticmethod
|
|
486
|
-
def _validate_instruction(
|
|
487
|
-
architecture: DynamicQuantumArchitecture,
|
|
488
|
-
instruction: Instruction,
|
|
489
|
-
qubit_mapping: dict[str, str] | None = None,
|
|
490
|
-
) -> None:
|
|
491
|
-
"""Validate an instruction against the dynamic quantum quantum architecture.
|
|
492
|
-
|
|
493
|
-
Checks that the instruction uses a valid implementation, and targets a valid locus.
|
|
324
|
+
def get_run(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> RunResult:
|
|
325
|
+
"""Query the status and results of a submitted job.
|
|
494
326
|
|
|
495
327
|
Args:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
qubit_mapping: Mapping of logical qubit names to physical qubit names.
|
|
499
|
-
Can be set to ``None`` if ``instruction`` already uses physical qubit names.
|
|
500
|
-
|
|
501
|
-
Raises:
|
|
502
|
-
CircuitValidationError: validation failed
|
|
503
|
-
|
|
504
|
-
"""
|
|
505
|
-
op_info = _SUPPORTED_OPERATIONS.get(instruction.name)
|
|
506
|
-
if op_info is None:
|
|
507
|
-
raise CircuitValidationError(f"Unknown quantum operation '{instruction.name}'.")
|
|
508
|
-
|
|
509
|
-
# apply the qubit mapping if any
|
|
510
|
-
mapped_qubits = tuple(qubit_mapping[q] for q in instruction.qubits) if qubit_mapping else instruction.qubits
|
|
511
|
-
|
|
512
|
-
def check_locus_components(allowed_components: Iterable[str], msg: str) -> None:
|
|
513
|
-
"""Checks that the instruction locus consists of the allowed components only."""
|
|
514
|
-
for q, mapped_q in zip(instruction.qubits, mapped_qubits):
|
|
515
|
-
if mapped_q not in allowed_components:
|
|
516
|
-
raise CircuitValidationError(
|
|
517
|
-
f"{instruction!r}: Component {q} = {mapped_q} {msg}."
|
|
518
|
-
if qubit_mapping
|
|
519
|
-
else f"{instruction!r}: Component {q} {msg}."
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
if op_info.no_calibration_needed:
|
|
523
|
-
# all QPU loci are allowed
|
|
524
|
-
check_locus_components(architecture.components, msg="does not exist on the QPU")
|
|
525
|
-
return
|
|
526
|
-
|
|
527
|
-
gate_info = architecture.gates.get(instruction.name)
|
|
528
|
-
if gate_info is None:
|
|
529
|
-
raise CircuitValidationError(
|
|
530
|
-
f"Operation '{instruction.name}' is not supported by the dynamic quantum architecture."
|
|
531
|
-
)
|
|
532
|
-
|
|
533
|
-
if instruction.implementation is not None:
|
|
534
|
-
# specific implementation requested
|
|
535
|
-
impl_info = gate_info.implementations.get(instruction.implementation)
|
|
536
|
-
if impl_info is None:
|
|
537
|
-
raise CircuitValidationError(
|
|
538
|
-
f"Operation '{instruction.name}' implementation '{instruction.implementation}' "
|
|
539
|
-
f"is not supported by the dynamic quantum architecture."
|
|
540
|
-
)
|
|
541
|
-
allowed_loci = impl_info.loci
|
|
542
|
-
instruction_name = f"{instruction.name}.{instruction.implementation}"
|
|
543
|
-
else:
|
|
544
|
-
# any implementation is fine
|
|
545
|
-
allowed_loci = gate_info.loci
|
|
546
|
-
instruction_name = f"{instruction.name}"
|
|
547
|
-
|
|
548
|
-
if op_info.factorizable:
|
|
549
|
-
# Check that all the locus components are allowed by the architecture
|
|
550
|
-
check_locus_components(
|
|
551
|
-
{q for locus in allowed_loci for q in locus}, msg=f"is not allowed as locus for '{instruction_name}'"
|
|
552
|
-
)
|
|
553
|
-
return
|
|
554
|
-
|
|
555
|
-
# Check that locus matches one of the allowed loci
|
|
556
|
-
all_loci = (
|
|
557
|
-
tuple(tuple(x) for locus in allowed_loci for x in itertools.permutations(locus))
|
|
558
|
-
if op_info.symmetric
|
|
559
|
-
else allowed_loci
|
|
560
|
-
)
|
|
561
|
-
if mapped_qubits not in all_loci:
|
|
562
|
-
raise CircuitValidationError(
|
|
563
|
-
f"{instruction.qubits} = {tuple(mapped_qubits)} is not allowed as locus for '{instruction_name}'"
|
|
564
|
-
if qubit_mapping
|
|
565
|
-
else f"{instruction.qubits} is not allowed as locus for '{instruction_name}'"
|
|
566
|
-
)
|
|
567
|
-
|
|
568
|
-
@staticmethod
|
|
569
|
-
def _validate_circuit_moves(
|
|
570
|
-
architecture: DynamicQuantumArchitecture,
|
|
571
|
-
circuit: Circuit,
|
|
572
|
-
qubit_mapping: dict[str, str] | None = None,
|
|
573
|
-
validate_moves: MoveGateValidationMode = MoveGateValidationMode.STRICT,
|
|
574
|
-
*,
|
|
575
|
-
must_close_sandwiches: bool = True,
|
|
576
|
-
) -> None:
|
|
577
|
-
"""Raise an error if the MOVE gates in the circuit are not valid in the given architecture.
|
|
328
|
+
job_id: ID of the job to query.
|
|
329
|
+
timeout_secs: Network request timeout (seconds).
|
|
578
330
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
circuit: Quantum circuit to validate.
|
|
582
|
-
qubit_mapping: Mapping of logical qubit names to physical qubit names.
|
|
583
|
-
Can be set to ``None`` if the ``circuit`` already uses physical qubit names.
|
|
584
|
-
validate_moves: Option for bypassing full or partial MOVE gate validation.
|
|
585
|
-
must_close_sandwiches: Iff True, MOVE sandwiches cannot be left open when the circuit ends.
|
|
331
|
+
Returns:
|
|
332
|
+
Result of the job (can be pending).
|
|
586
333
|
|
|
587
334
|
Raises:
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
"""
|
|
591
|
-
if validate_moves == MoveGateValidationMode.NONE:
|
|
592
|
-
return
|
|
593
|
-
move_gate = "move"
|
|
594
|
-
# Check if MOVE gates are allowed on this architecture
|
|
595
|
-
if move_gate not in architecture.gates:
|
|
596
|
-
if any(i.name == move_gate for i in circuit.instructions):
|
|
597
|
-
raise CircuitValidationError("MOVE instruction is not supported by the given device architecture.")
|
|
598
|
-
return
|
|
599
|
-
|
|
600
|
-
# some gates are allowed in MOVE sandwiches
|
|
601
|
-
allowed_gates = {"barrier"}
|
|
602
|
-
if validate_moves == MoveGateValidationMode.ALLOW_PRX:
|
|
603
|
-
allowed_gates.add("prx")
|
|
604
|
-
|
|
605
|
-
all_resonators = set(architecture.computational_resonators)
|
|
606
|
-
all_qubits = set(architecture.qubits)
|
|
607
|
-
if qubit_mapping:
|
|
608
|
-
reverse_mapping = {phys: log for log, phys in qubit_mapping.items()}
|
|
609
|
-
all_resonators = {reverse_mapping[q] if q in reverse_mapping else q for q in all_resonators}
|
|
610
|
-
all_qubits = {reverse_mapping[q] if q in reverse_mapping else q for q in all_qubits}
|
|
611
|
-
|
|
612
|
-
# Mapping from resonator to the qubit whose state it holds. Resonators not in the map hold no qubit state.
|
|
613
|
-
resonator_occupations: dict[str, str] = {}
|
|
614
|
-
# Qubits whose states are currently moved to a resonator
|
|
615
|
-
moved_qubits: set[str] = set()
|
|
616
|
-
|
|
617
|
-
for inst in circuit.instructions:
|
|
618
|
-
if inst.name == "move":
|
|
619
|
-
qubit, resonator = inst.qubits
|
|
620
|
-
if not (qubit in all_qubits and resonator in all_resonators):
|
|
621
|
-
raise CircuitValidationError(
|
|
622
|
-
f"MOVE instructions are only allowed between qubit and resonator, not {inst.qubits}."
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
if (resonator_qubit := resonator_occupations.get(resonator)) is None:
|
|
626
|
-
# Beginning MOVE: check that the qubit hasn't been moved to another resonator
|
|
627
|
-
if qubit in moved_qubits:
|
|
628
|
-
raise CircuitValidationError(
|
|
629
|
-
f"MOVE instruction {inst.qubits}: state of {qubit} is "
|
|
630
|
-
f"in another resonator: {resonator_occupations}."
|
|
631
|
-
)
|
|
632
|
-
resonator_occupations[resonator] = qubit
|
|
633
|
-
moved_qubits.add(qubit)
|
|
634
|
-
else:
|
|
635
|
-
# Ending MOVE: check that the qubit matches to the qubit that was moved to the resonator
|
|
636
|
-
if resonator_qubit != qubit:
|
|
637
|
-
raise CircuitValidationError(
|
|
638
|
-
f"MOVE instruction {inst.qubits} to an already occupied resonator: {resonator_occupations}."
|
|
639
|
-
)
|
|
640
|
-
del resonator_occupations[resonator]
|
|
641
|
-
moved_qubits.remove(qubit)
|
|
642
|
-
elif moved_qubits:
|
|
643
|
-
# Validate that moved qubits are not used during MOVE operations
|
|
644
|
-
if inst.name not in allowed_gates:
|
|
645
|
-
if overlap := set(inst.qubits) & moved_qubits:
|
|
646
|
-
raise CircuitValidationError(
|
|
647
|
-
f"Instruction {inst.name} acts on {inst.qubits} while the state(s) of {overlap} "
|
|
648
|
-
f"are in a resonator. Current resonator occupation: {resonator_occupations}."
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
# Finally validate that all MOVE sandwiches have been ended before the circuit ends
|
|
652
|
-
if must_close_sandwiches and resonator_occupations:
|
|
653
|
-
raise CircuitValidationError(
|
|
654
|
-
f"Circuit ends while qubit state(s) are still in a resonator: {resonator_occupations}."
|
|
655
|
-
)
|
|
335
|
+
CircuitExecutionError: IQM server specific exceptions
|
|
336
|
+
HTTPException: HTTP exceptions
|
|
656
337
|
|
|
657
|
-
def _get_run_v1(self, job_id: UUID, timeout_secs: float = REQUESTS_TIMEOUT) -> RunResult:
|
|
658
|
-
"""V1 API (Cocos circuit execution and Resonance) has an inefficient `GET /jobs/<id>` endpoint
|
|
659
|
-
that returns the full job status, including the result and the original request, in a single call.
|
|
660
338
|
"""
|
|
661
|
-
response = self._get_request(
|
|
662
|
-
APIEndpoint.GET_JOB_RESULT,
|
|
663
|
-
(str(job_id),),
|
|
664
|
-
timeout=timeout_secs,
|
|
665
|
-
retry=True,
|
|
666
|
-
)
|
|
667
|
-
return self._deserialize_response(response, RunResult)
|
|
668
|
-
|
|
669
|
-
def _get_run_v2(self, job_id: UUID, timeout_secs: float = REQUESTS_TIMEOUT) -> RunResult:
|
|
670
|
-
"""V2 API (Station-based circuit execution) has granular endpoints for job status and result."""
|
|
671
339
|
status_response = self._get_request(
|
|
672
340
|
APIEndpoint.GET_JOB_STATUS,
|
|
673
341
|
(str(job_id),),
|
|
@@ -677,9 +345,7 @@ class IQMClient:
|
|
|
677
345
|
if Status(status["status"]) not in Status.terminal_statuses():
|
|
678
346
|
return RunResult.from_dict({"status": status["status"], "metadata": {}})
|
|
679
347
|
|
|
680
|
-
result = self._get_request(
|
|
681
|
-
APIEndpoint.GET_JOB_RESULT, (str(job_id),), timeout=timeout_secs, retry=True, allow_errors=True
|
|
682
|
-
)
|
|
348
|
+
result = self._get_request(APIEndpoint.GET_JOB_RESULT, (str(job_id),), timeout=timeout_secs, allow_errors=True)
|
|
683
349
|
|
|
684
350
|
error_log_response = self._get_request(
|
|
685
351
|
APIEndpoint.GET_JOB_ERROR_LOG, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
@@ -696,63 +362,46 @@ class IQMClient:
|
|
|
696
362
|
error_message = None
|
|
697
363
|
|
|
698
364
|
if result.status_code == 404:
|
|
699
|
-
|
|
365
|
+
run_result = RunResult.from_dict({"status": status["status"], "message": error_message, "metadata": {}})
|
|
700
366
|
else:
|
|
701
367
|
result.raise_for_status()
|
|
702
368
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
369
|
+
measurements = result.json()
|
|
370
|
+
request_parameters = self._get_request(
|
|
371
|
+
APIEndpoint.GET_JOB_REQUEST_PARAMETERS, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
372
|
+
).json()
|
|
373
|
+
calibration_set_id = self._get_request(
|
|
374
|
+
APIEndpoint.GET_JOB_CALIBRATION_SET_ID, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
375
|
+
).json()
|
|
376
|
+
circuits_batch = self._get_request(
|
|
377
|
+
APIEndpoint.GET_JOB_CIRCUITS_BATCH, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
378
|
+
).json()
|
|
379
|
+
timeline = self._get_request(
|
|
380
|
+
APIEndpoint.GET_JOB_TIMELINE, (str(job_id),), timeout=timeout_secs, allow_errors=True
|
|
381
|
+
).json()
|
|
382
|
+
|
|
383
|
+
run_result = RunResult.from_dict(
|
|
384
|
+
{
|
|
385
|
+
"measurements": measurements,
|
|
386
|
+
"status": status["status"],
|
|
387
|
+
"message": error_message,
|
|
388
|
+
"metadata": {
|
|
389
|
+
"calibration_set_id": calibration_set_id,
|
|
390
|
+
"circuits_batch": circuits_batch,
|
|
391
|
+
"parameters": {
|
|
392
|
+
"shots": request_parameters["shots"],
|
|
393
|
+
"max_circuit_duration_over_t2": request_parameters.get(
|
|
394
|
+
"max_circuit_duration_over_t2", None
|
|
395
|
+
),
|
|
396
|
+
"heralding_mode": request_parameters["heralding_mode"],
|
|
397
|
+
"move_validation_mode": request_parameters["move_validation_mode"],
|
|
398
|
+
"move_gate_frame_tracking_mode": request_parameters["move_gate_frame_tracking_mode"],
|
|
399
|
+
},
|
|
400
|
+
"timestamps": {datapoint["status"]: datapoint["timestamp"] for datapoint in timeline},
|
|
731
401
|
},
|
|
732
|
-
"
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
)
|
|
736
|
-
|
|
737
|
-
def get_run(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> RunResult:
|
|
738
|
-
"""Query the status and results of a submitted job.
|
|
739
|
-
|
|
740
|
-
Args:
|
|
741
|
-
job_id: ID of the job to query.
|
|
742
|
-
timeout_secs: Network request timeout (seconds).
|
|
743
|
-
|
|
744
|
-
Returns:
|
|
745
|
-
Result of the job (can be pending).
|
|
746
|
-
|
|
747
|
-
Raises:
|
|
748
|
-
CircuitExecutionError: IQM server specific exceptions
|
|
749
|
-
HTTPException: HTTP exceptions
|
|
750
|
-
|
|
751
|
-
"""
|
|
752
|
-
if self._api.variant == APIVariant.V2:
|
|
753
|
-
run_result = self._get_run_v2(job_id, timeout_secs)
|
|
754
|
-
else:
|
|
755
|
-
run_result = self._get_run_v1(job_id, timeout_secs)
|
|
402
|
+
"warnings": status.get("warnings", []),
|
|
403
|
+
}
|
|
404
|
+
)
|
|
756
405
|
|
|
757
406
|
if run_result.warnings:
|
|
758
407
|
for warning in run_result.warnings:
|
|
@@ -780,7 +429,6 @@ class IQMClient:
|
|
|
780
429
|
APIEndpoint.GET_JOB_STATUS,
|
|
781
430
|
(str(job_id),),
|
|
782
431
|
timeout=timeout_secs,
|
|
783
|
-
retry=True,
|
|
784
432
|
)
|
|
785
433
|
run_status = self._deserialize_response(response, RunStatus)
|
|
786
434
|
|
|
@@ -809,7 +457,7 @@ class IQMClient:
|
|
|
809
457
|
if status in Status.terminal_statuses() | {Status.PENDING_EXECUTION, Status.COMPILATION_ENDED}:
|
|
810
458
|
return self.get_run(job_id)
|
|
811
459
|
time.sleep(SECONDS_BETWEEN_CALLS)
|
|
812
|
-
raise APITimeoutError(f"The job compilation didn't finish in {timeout_secs} seconds.")
|
|
460
|
+
raise APITimeoutError(f"The job {job_id} compilation didn't finish in {timeout_secs} seconds.")
|
|
813
461
|
|
|
814
462
|
def wait_for_results(self, job_id: UUID, timeout_secs: float = DEFAULT_TIMEOUT_SECONDS) -> RunResult:
|
|
815
463
|
"""Poll results until a job is either ready, failed, aborted, or timed out.
|
|
@@ -835,7 +483,7 @@ class IQMClient:
|
|
|
835
483
|
if status in Status.terminal_statuses():
|
|
836
484
|
return self.get_run(job_id)
|
|
837
485
|
time.sleep(SECONDS_BETWEEN_CALLS)
|
|
838
|
-
raise APITimeoutError(f"The job didn't finish in {timeout_secs} seconds.")
|
|
486
|
+
raise APITimeoutError(f"The job {job_id} didn't finish in {timeout_secs} seconds.")
|
|
839
487
|
|
|
840
488
|
def abort_job(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> None:
|
|
841
489
|
"""Abort a job that was submitted for execution.
|
|
@@ -856,44 +504,11 @@ class IQMClient:
|
|
|
856
504
|
if result.status_code not in (200, 204): # CoCos returns 200, station-control 204.
|
|
857
505
|
raise JobAbortionError(result.text)
|
|
858
506
|
|
|
859
|
-
def
|
|
860
|
-
"""Retrieve quantum architecture from server.
|
|
861
|
-
|
|
862
|
-
Caches the result and returns it on later invocations.
|
|
863
|
-
|
|
864
|
-
Args:
|
|
865
|
-
timeout_secs: Network request timeout (seconds).
|
|
866
|
-
|
|
867
|
-
Returns:
|
|
868
|
-
Quantum architecture of the server.
|
|
869
|
-
|
|
870
|
-
Raises:
|
|
871
|
-
EndpointRequestError: did not understand the endpoint response
|
|
872
|
-
ClientAuthenticationError: no valid authentication provided
|
|
873
|
-
HTTPException: HTTP exceptions
|
|
874
|
-
|
|
875
|
-
"""
|
|
876
|
-
if self._architecture:
|
|
877
|
-
return self._architecture
|
|
878
|
-
|
|
879
|
-
response = self._get_request(
|
|
880
|
-
APIEndpoint.QUANTUM_ARCHITECTURE,
|
|
881
|
-
timeout=timeout_secs,
|
|
882
|
-
)
|
|
883
|
-
qa = self._deserialize_response(response, QuantumArchitecture).quantum_architecture
|
|
884
|
-
|
|
885
|
-
# Cache architecture so that later invocations do not need to query it again
|
|
886
|
-
self._architecture = qa
|
|
887
|
-
return qa
|
|
888
|
-
|
|
889
|
-
def get_static_quantum_architecture(self, *, timeout_secs: float = REQUESTS_TIMEOUT) -> StaticQuantumArchitecture:
|
|
507
|
+
def get_static_quantum_architecture(self) -> StaticQuantumArchitecture:
|
|
890
508
|
"""Retrieve the static quantum architecture (SQA) from the server.
|
|
891
509
|
|
|
892
510
|
Caches the result and returns it on later invocations.
|
|
893
511
|
|
|
894
|
-
Args:
|
|
895
|
-
timeout_secs: Network request timeout (seconds).
|
|
896
|
-
|
|
897
512
|
Returns:
|
|
898
513
|
Static quantum architecture of the server.
|
|
899
514
|
|
|
@@ -906,25 +521,17 @@ class IQMClient:
|
|
|
906
521
|
if self._static_architecture:
|
|
907
522
|
return self._static_architecture
|
|
908
523
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
sqa = self._deserialize_response(response, StaticQuantumArchitecture)
|
|
914
|
-
|
|
915
|
-
# Cache the architecture so that later invocations do not need to query it again
|
|
916
|
-
self._static_architecture = sqa
|
|
917
|
-
return sqa
|
|
524
|
+
dut_label = self._get_dut_label()
|
|
525
|
+
static_quantum_architecture = self._station_control.get_static_quantum_architecture(dut_label)
|
|
526
|
+
self._static_architecture = StaticQuantumArchitecture(**static_quantum_architecture.model_dump())
|
|
527
|
+
return self._static_architecture
|
|
918
528
|
|
|
919
|
-
def get_quality_metric_set(
|
|
920
|
-
self, calibration_set_id: UUID | None = None, *, timeout_secs: float = REQUESTS_TIMEOUT
|
|
921
|
-
) -> QualityMetricSet:
|
|
529
|
+
def get_quality_metric_set(self, calibration_set_id: UUID | None = None) -> QualityMetricSet:
|
|
922
530
|
"""Retrieve the latest quality metric set for the given calibration set from the server.
|
|
923
531
|
|
|
924
532
|
Args:
|
|
925
533
|
calibration_set_id: ID of the calibration set for which the quality metrics are returned.
|
|
926
534
|
If ``None``, the current default calibration set is used.
|
|
927
|
-
timeout_secs: Network request timeout (seconds).
|
|
928
535
|
|
|
929
536
|
Returns:
|
|
930
537
|
Requested quality metric set.
|
|
@@ -935,27 +542,53 @@ class IQMClient:
|
|
|
935
542
|
HTTPException: HTTP exceptions
|
|
936
543
|
|
|
937
544
|
"""
|
|
938
|
-
if
|
|
939
|
-
|
|
940
|
-
else:
|
|
941
|
-
calibration_set_id_str = str(calibration_set_id)
|
|
545
|
+
if isinstance(self._station_control, IqmServerClient):
|
|
546
|
+
raise ValueError("'get_quality_metric_set' method is not supported for IqmServerClient.")
|
|
942
547
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
548
|
+
if not calibration_set_id:
|
|
549
|
+
quality_metrics = self._station_control.get_default_calibration_set_quality_metrics()
|
|
550
|
+
else:
|
|
551
|
+
quality_metrics = self._station_control.get_calibration_set_quality_metrics(calibration_set_id)
|
|
552
|
+
|
|
553
|
+
calibration_set = quality_metrics.calibration_set
|
|
554
|
+
|
|
555
|
+
return QualityMetricSet(
|
|
556
|
+
**{
|
|
557
|
+
"calibration_set_id": calibration_set.observation_set_id,
|
|
558
|
+
"calibration_set_dut_label": calibration_set.dut_label,
|
|
559
|
+
"calibration_set_number_of_observations": len(calibration_set.observation_ids),
|
|
560
|
+
"calibration_set_created_timestamp": str(calibration_set.created_timestamp.isoformat()),
|
|
561
|
+
"calibration_set_end_timestamp": None
|
|
562
|
+
if calibration_set.end_timestamp is None
|
|
563
|
+
else str(calibration_set.end_timestamp.isoformat()),
|
|
564
|
+
"calibration_set_is_invalid": calibration_set.invalid,
|
|
565
|
+
"quality_metric_set_id": quality_metrics.observation_set_id,
|
|
566
|
+
"quality_metric_set_dut_label": quality_metrics.dut_label,
|
|
567
|
+
"quality_metric_set_created_timestamp": str(quality_metrics.created_timestamp.isoformat()),
|
|
568
|
+
"quality_metric_set_end_timestamp": None
|
|
569
|
+
if quality_metrics.end_timestamp is None
|
|
570
|
+
else str(quality_metrics.end_timestamp.isoformat()),
|
|
571
|
+
"quality_metric_set_is_invalid": quality_metrics.invalid,
|
|
572
|
+
"metrics": {
|
|
573
|
+
observation.dut_field: {
|
|
574
|
+
"value": str(observation.value),
|
|
575
|
+
"unit": observation.unit,
|
|
576
|
+
"uncertainty": str(observation.uncertainty),
|
|
577
|
+
# created_timestamp is the interesting one, since observations are effectively immutable
|
|
578
|
+
"timestamp": str(observation.created_timestamp.isoformat()),
|
|
579
|
+
}
|
|
580
|
+
for observation in quality_metrics.observations
|
|
581
|
+
if not observation.invalid
|
|
582
|
+
},
|
|
583
|
+
}
|
|
947
584
|
)
|
|
948
|
-
return self._deserialize_response(response, QualityMetricSet)
|
|
949
585
|
|
|
950
|
-
def get_calibration_set(
|
|
951
|
-
self, calibration_set_id: UUID | None = None, *, timeout_secs: float = REQUESTS_TIMEOUT
|
|
952
|
-
) -> CalibrationSet:
|
|
586
|
+
def get_calibration_set(self, calibration_set_id: UUID | None = None) -> CalibrationSet:
|
|
953
587
|
"""Retrieve the given calibration set from the server.
|
|
954
588
|
|
|
955
589
|
Args:
|
|
956
590
|
calibration_set_id: ID of the calibration set to retrieve.
|
|
957
591
|
If ``None``, the current default calibration set is retrieved.
|
|
958
|
-
timeout_secs: Network request timeout (seconds).
|
|
959
592
|
|
|
960
593
|
Returns:
|
|
961
594
|
Requested calibration set.
|
|
@@ -966,21 +599,36 @@ class IQMClient:
|
|
|
966
599
|
HTTPException: HTTP exceptions
|
|
967
600
|
|
|
968
601
|
"""
|
|
969
|
-
if calibration_set_id is None:
|
|
970
|
-
calibration_set_id_str = "default"
|
|
971
|
-
else:
|
|
972
|
-
calibration_set_id_str = str(calibration_set_id)
|
|
973
602
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
(
|
|
977
|
-
|
|
603
|
+
def _observation_lite_to_json(obs: ObservationLite) -> dict[str, Any]:
|
|
604
|
+
"""Convert ObservationLite to JSON serializable dictionary."""
|
|
605
|
+
json_dict = obs.model_dump()
|
|
606
|
+
json_dict["created_timestamp"] = obs.created_timestamp.isoformat(timespec="microseconds")
|
|
607
|
+
json_dict["modified_timestamp"] = obs.modified_timestamp.isoformat(timespec="microseconds")
|
|
608
|
+
return json_dict
|
|
609
|
+
|
|
610
|
+
if isinstance(self._station_control, IqmServerClient):
|
|
611
|
+
raise ValueError("'get_calibration_set' method is not supported for IqmServerClient.")
|
|
612
|
+
|
|
613
|
+
if not calibration_set_id:
|
|
614
|
+
calibration_set = self._station_control.get_default_calibration_set()
|
|
615
|
+
else:
|
|
616
|
+
calibration_set = self._station_control.get_observation_set(calibration_set_id)
|
|
617
|
+
|
|
618
|
+
observations = self._station_control.get_observation_set_observations(calibration_set.observation_set_id)
|
|
619
|
+
|
|
620
|
+
return CalibrationSet(
|
|
621
|
+
calibration_set_id=calibration_set.observation_set_id,
|
|
622
|
+
calibration_set_dut_label=calibration_set.dut_label,
|
|
623
|
+
calibration_set_created_timestamp=str(calibration_set.created_timestamp.isoformat()),
|
|
624
|
+
calibration_set_end_timestamp=str(calibration_set.end_timestamp.isoformat()),
|
|
625
|
+
calibration_set_is_invalid=calibration_set.invalid,
|
|
626
|
+
observations={
|
|
627
|
+
observation.dut_field: _observation_lite_to_json(observation) for observation in observations
|
|
628
|
+
},
|
|
978
629
|
)
|
|
979
|
-
return self._deserialize_response(response, CalibrationSet)
|
|
980
630
|
|
|
981
|
-
def get_dynamic_quantum_architecture(
|
|
982
|
-
self, calibration_set_id: UUID | None = None, *, timeout_secs: float = REQUESTS_TIMEOUT
|
|
983
|
-
) -> DynamicQuantumArchitecture:
|
|
631
|
+
def get_dynamic_quantum_architecture(self, calibration_set_id: UUID | None = None) -> DynamicQuantumArchitecture:
|
|
984
632
|
"""Retrieve the dynamic quantum architecture (DQA) for the given calibration set from the server.
|
|
985
633
|
|
|
986
634
|
Caches the result and returns the same result on later invocations, unless ``calibration_set_id`` is ``None``.
|
|
@@ -990,7 +638,6 @@ class IQMClient:
|
|
|
990
638
|
Args:
|
|
991
639
|
calibration_set_id: ID of the calibration set for which the DQA is retrieved.
|
|
992
640
|
If ``None``, use current default calibration set on the server.
|
|
993
|
-
timeout_secs: Network request timeout (seconds).
|
|
994
641
|
|
|
995
642
|
Returns:
|
|
996
643
|
Dynamic quantum architecture corresponding to the given calibration set.
|
|
@@ -1001,35 +648,28 @@ class IQMClient:
|
|
|
1001
648
|
HTTPException: HTTP exceptions
|
|
1002
649
|
|
|
1003
650
|
"""
|
|
1004
|
-
if calibration_set_id
|
|
1005
|
-
calibration_set_id_str = "default"
|
|
1006
|
-
elif calibration_set_id in self._dynamic_architectures:
|
|
651
|
+
if calibration_set_id in self._dynamic_architectures:
|
|
1007
652
|
return self._dynamic_architectures[calibration_set_id]
|
|
1008
|
-
else:
|
|
1009
|
-
calibration_set_id_str = str(calibration_set_id)
|
|
1010
653
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
654
|
+
if not calibration_set_id:
|
|
655
|
+
if isinstance(self._station_control, IqmServerClient):
|
|
656
|
+
dut_label = self._get_dut_label()
|
|
657
|
+
calibration_set_id = self._station_control.get_latest_calibration_set_id(dut_label)
|
|
658
|
+
else:
|
|
659
|
+
calibration_set_id = self._station_control.get_default_calibration_set().observation_set_id
|
|
660
|
+
data = self._station_control.get_dynamic_quantum_architecture(calibration_set_id)
|
|
661
|
+
dynamic_quantum_architecture = DynamicQuantumArchitecture(**data.model_dump())
|
|
1017
662
|
|
|
1018
663
|
# Cache architecture so that later invocations do not need to query it again
|
|
1019
|
-
self._dynamic_architectures[
|
|
1020
|
-
return
|
|
664
|
+
self._dynamic_architectures[dynamic_quantum_architecture.calibration_set_id] = dynamic_quantum_architecture
|
|
665
|
+
return dynamic_quantum_architecture
|
|
1021
666
|
|
|
1022
|
-
def get_feedback_groups(self
|
|
667
|
+
def get_feedback_groups(self) -> tuple[frozenset[str], ...]:
|
|
1023
668
|
"""Retrieve groups of qubits that can receive real-time feedback signals from each other.
|
|
1024
669
|
|
|
1025
670
|
Real-time feedback enables conditional gates such as `cc_prx`.
|
|
1026
671
|
Some hardware configurations support routing real-time feedback only between certain qubits.
|
|
1027
672
|
|
|
1028
|
-
This method is only supported for the API variant V2.
|
|
1029
|
-
|
|
1030
|
-
Args:
|
|
1031
|
-
timeout_secs: Network request timeout (seconds).
|
|
1032
|
-
|
|
1033
673
|
Returns:
|
|
1034
674
|
Feedback groups. Within a group, any qubit can receive real-time feedback from any other qubit in
|
|
1035
675
|
the same group. A qubit can belong to multiple groups.
|
|
@@ -1041,16 +681,9 @@ class IQMClient:
|
|
|
1041
681
|
HTTPException: HTTP exceptions
|
|
1042
682
|
|
|
1043
683
|
"""
|
|
1044
|
-
|
|
1045
|
-
APIEndpoint.CHANNEL_PROPERTIES,
|
|
1046
|
-
timeout=timeout_secs,
|
|
1047
|
-
)
|
|
1048
|
-
try:
|
|
1049
|
-
channel_properties = DictDict.validate_json(response.text)
|
|
1050
|
-
except PydanticValidationError as e:
|
|
1051
|
-
raise EndpointRequestError(f"Invalid response: {response.text}, {e!r}") from e
|
|
684
|
+
channel_properties = self._station_control.get_channel_properties()
|
|
1052
685
|
|
|
1053
|
-
all_qubits = self.
|
|
686
|
+
all_qubits = self.get_static_quantum_architecture().qubits
|
|
1054
687
|
groups: dict[str, set[str]] = {}
|
|
1055
688
|
# All qubits that can read from the same source belong to the same group.
|
|
1056
689
|
# A qubit may belong to multiple groups.
|
|
@@ -1059,13 +692,70 @@ class IQMClient:
|
|
|
1059
692
|
qubit = channel_name.split("__")[0]
|
|
1060
693
|
if qubit not in all_qubits:
|
|
1061
694
|
continue
|
|
1062
|
-
|
|
1063
|
-
|
|
695
|
+
if isinstance(properties, AWGProperties):
|
|
696
|
+
for source in properties.fast_feedback_sources:
|
|
697
|
+
groups.setdefault(source, set()).add(qubit)
|
|
1064
698
|
# Merge identical groups
|
|
1065
699
|
unique_groups: set[frozenset[str]] = {frozenset(group) for group in groups.values()}
|
|
1066
700
|
# Sort by group size
|
|
1067
701
|
return tuple(sorted(unique_groups, key=len, reverse=True))
|
|
1068
702
|
|
|
703
|
+
def get_run_counts(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> RunCounts:
|
|
704
|
+
"""Query the counts of an executed job.
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
job_id: ID of the job to query.
|
|
708
|
+
timeout_secs: Network request timeout (seconds).
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
Measurement results of the job in histogram representation.
|
|
712
|
+
|
|
713
|
+
Raises:
|
|
714
|
+
EndpointRequestError: did not understand the endpoint response
|
|
715
|
+
ClientAuthenticationError: no valid authentication provided
|
|
716
|
+
HTTPException: HTTP exceptions
|
|
717
|
+
|
|
718
|
+
"""
|
|
719
|
+
response = self._get_request(
|
|
720
|
+
APIEndpoint.GET_JOB_COUNTS,
|
|
721
|
+
(str(job_id),),
|
|
722
|
+
timeout=timeout_secs,
|
|
723
|
+
)
|
|
724
|
+
return self._deserialize_response(response, RunCounts)
|
|
725
|
+
|
|
726
|
+
def get_supported_client_libraries(self, timeout_secs: float = REQUESTS_TIMEOUT) -> dict[str, ClientLibrary] | None:
|
|
727
|
+
"""Retrieve information about supported client libraries from the server.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
timeout_secs: Network request timeout (seconds).
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Mapping from library identifiers to their metadata.
|
|
734
|
+
|
|
735
|
+
Raises:
|
|
736
|
+
EndpointRequestError: did not understand the endpoint response
|
|
737
|
+
ClientAuthenticationError: no valid authentication provided
|
|
738
|
+
HTTPException: HTTP exceptions
|
|
739
|
+
|
|
740
|
+
"""
|
|
741
|
+
# TODO: Remove "client-libraries" usage after using versioned URLs in station control
|
|
742
|
+
# Version incompatibility shouldn't be a problem after that anymore,
|
|
743
|
+
# so we can delete this "client-libraries" implementation and usage.
|
|
744
|
+
response = requests.get(
|
|
745
|
+
# "/info/client-libraries" is implemented by Nginx so it won't work on locally running service.
|
|
746
|
+
# We will simply give warning in that case, so that IQMClient can be initialized also locally.
|
|
747
|
+
# "/station" is set by Nginx, so we will drop it to get the correct root for "/info/client-libraries".
|
|
748
|
+
self._api.station_control_url.replace("/station", "") + "/info/client-libraries",
|
|
749
|
+
headers=self._default_headers(),
|
|
750
|
+
timeout=timeout_secs,
|
|
751
|
+
)
|
|
752
|
+
if response.status_code == HTTPStatus.NOT_FOUND:
|
|
753
|
+
return None
|
|
754
|
+
try:
|
|
755
|
+
return ClientLibraryDict.validate_json(response.text)
|
|
756
|
+
except ValidationError as e:
|
|
757
|
+
raise EndpointRequestError(f"Invalid response: {response.text}, {e!r}") from e
|
|
758
|
+
|
|
1069
759
|
def close_auth_session(self) -> bool:
|
|
1070
760
|
"""Terminate session with authentication server if there is one.
|
|
1071
761
|
|
|
@@ -1119,6 +809,8 @@ class IQMClient:
|
|
|
1119
809
|
"""
|
|
1120
810
|
try:
|
|
1121
811
|
libraries = self.get_supported_client_libraries()
|
|
812
|
+
if not libraries:
|
|
813
|
+
return "Got 'Not found' response from server. Couldn't check version compatibility."
|
|
1122
814
|
compatible_iqm_client = libraries.get(
|
|
1123
815
|
"iqm-client",
|
|
1124
816
|
libraries.get("iqm_client"),
|
|
@@ -1140,50 +832,73 @@ class IQMClient:
|
|
|
1140
832
|
check_error = e
|
|
1141
833
|
return f"Could not verify IQM Client compatibility with the server. You might encounter issues. {check_error}"
|
|
1142
834
|
|
|
1143
|
-
|
|
1144
|
-
|
|
835
|
+
@lru_cache(maxsize=1)
|
|
836
|
+
def _get_dut_label(self) -> str:
|
|
837
|
+
duts = self._station_control.get_duts()
|
|
838
|
+
if len(duts) != 1:
|
|
839
|
+
raise RuntimeError(f"Expected exactly 1 DUT, but got {len(duts)}.")
|
|
840
|
+
return duts[0].label
|
|
841
|
+
|
|
842
|
+
def _get_request(
|
|
843
|
+
self,
|
|
844
|
+
api_endpoint: APIEndpoint,
|
|
845
|
+
endpoint_args: tuple[str, ...] = (),
|
|
846
|
+
*,
|
|
847
|
+
timeout: float,
|
|
848
|
+
headers: dict | None = None,
|
|
849
|
+
allow_errors: bool = False,
|
|
850
|
+
) -> requests.Response:
|
|
851
|
+
"""Make an HTTP GET request to an IQM server endpoint.
|
|
852
|
+
|
|
853
|
+
Contains all the boilerplate code for making a simple GET request.
|
|
1145
854
|
|
|
1146
855
|
Args:
|
|
1147
|
-
|
|
1148
|
-
|
|
856
|
+
api_endpoint: API endpoint to GET.
|
|
857
|
+
endpoint_args: Arguments for the endpoint.
|
|
858
|
+
timeout: HTTP request timeout (in seconds).
|
|
1149
859
|
|
|
1150
860
|
Returns:
|
|
1151
|
-
|
|
861
|
+
HTTP response to the request.
|
|
1152
862
|
|
|
1153
863
|
Raises:
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
HTTPException: HTTP exceptions
|
|
864
|
+
ClientAuthenticationError: No valid authentication provided.
|
|
865
|
+
HTTPError: Various HTTP exceptions.
|
|
1157
866
|
|
|
1158
867
|
"""
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
868
|
+
url = self._api.url(api_endpoint, *endpoint_args)
|
|
869
|
+
response = requests.get(
|
|
870
|
+
url,
|
|
871
|
+
headers=headers or self._default_headers(),
|
|
872
|
+
timeout=timeout,
|
|
1164
873
|
)
|
|
1165
|
-
|
|
874
|
+
if not allow_errors:
|
|
875
|
+
self._check_not_found_error(response)
|
|
876
|
+
self._check_authentication_errors(response)
|
|
877
|
+
response.raise_for_status()
|
|
878
|
+
return response
|
|
1166
879
|
|
|
1167
|
-
|
|
1168
|
-
|
|
880
|
+
@staticmethod
|
|
881
|
+
def _deserialize_response(
|
|
882
|
+
response: requests.Response,
|
|
883
|
+
model_class: type[T_BaseModel],
|
|
884
|
+
) -> T_BaseModel:
|
|
885
|
+
"""Deserialize a HTTP endpoint response.
|
|
1169
886
|
|
|
1170
887
|
Args:
|
|
1171
|
-
|
|
888
|
+
response: HTTP response data.
|
|
889
|
+
model_class: Pydantic model to deserialize the data into.
|
|
1172
890
|
|
|
1173
891
|
Returns:
|
|
1174
|
-
|
|
892
|
+
Deserialized endpoint response.
|
|
1175
893
|
|
|
1176
894
|
Raises:
|
|
1177
|
-
EndpointRequestError:
|
|
1178
|
-
ClientAuthenticationError: no valid authentication provided
|
|
1179
|
-
HTTPException: HTTP exceptions
|
|
895
|
+
EndpointRequestError: Did not understand the endpoint response.
|
|
1180
896
|
|
|
1181
897
|
"""
|
|
1182
|
-
response = self._get_request(
|
|
1183
|
-
APIEndpoint.CLIENT_LIBRARIES,
|
|
1184
|
-
timeout=timeout_secs,
|
|
1185
|
-
)
|
|
1186
898
|
try:
|
|
1187
|
-
|
|
1188
|
-
|
|
899
|
+
model = model_class.model_validate(response.json())
|
|
900
|
+
# TODO this would be faster but MockJsonResponse.text in our unit tests cannot handle UUID
|
|
901
|
+
# model = model_class.model_validate_json(response.text)
|
|
902
|
+
except json.decoder.JSONDecodeError as e:
|
|
1189
903
|
raise EndpointRequestError(f"Invalid response: {response.text}, {e!r}") from e
|
|
904
|
+
return model
|