iqm-client 32.0.0__py3-none-any.whl → 33.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. iqm/cirq_iqm/devices/iqm_device_metadata.py +2 -1
  2. iqm/cirq_iqm/examples/demo_common.py +1 -1
  3. iqm/cirq_iqm/examples/demo_iqm_execution.py +3 -3
  4. iqm/cirq_iqm/iqm_sampler.py +47 -29
  5. iqm/cirq_iqm/serialize.py +1 -1
  6. iqm/cirq_iqm/transpiler.py +3 -1
  7. iqm/iqm_client/__init__.py +0 -2
  8. iqm/iqm_client/errors.py +6 -17
  9. iqm/iqm_client/iqm_client.py +199 -602
  10. iqm/iqm_client/models.py +20 -611
  11. iqm/iqm_client/transpile.py +11 -8
  12. iqm/iqm_client/validation.py +18 -9
  13. iqm/iqm_server_client/__init__.py +14 -0
  14. iqm/iqm_server_client/errors.py +6 -0
  15. iqm/iqm_server_client/iqm_server_client.py +755 -0
  16. iqm/iqm_server_client/models.py +179 -0
  17. iqm/iqm_server_client/py.typed +0 -0
  18. iqm/qiskit_iqm/__init__.py +8 -0
  19. iqm/qiskit_iqm/examples/bell_measure.py +2 -2
  20. iqm/qiskit_iqm/examples/transpile_example.py +9 -4
  21. iqm/qiskit_iqm/fake_backends/fake_adonis.py +2 -1
  22. iqm/qiskit_iqm/fake_backends/fake_aphrodite.py +2 -1
  23. iqm/qiskit_iqm/fake_backends/fake_apollo.py +2 -1
  24. iqm/qiskit_iqm/fake_backends/fake_deneb.py +2 -1
  25. iqm/qiskit_iqm/fake_backends/iqm_fake_backend.py +8 -7
  26. iqm/qiskit_iqm/iqm_backend.py +3 -4
  27. iqm/qiskit_iqm/iqm_circuit_validation.py +8 -7
  28. iqm/qiskit_iqm/iqm_job.py +106 -88
  29. iqm/qiskit_iqm/iqm_move_layout.py +2 -1
  30. iqm/qiskit_iqm/iqm_naive_move_pass.py +115 -56
  31. iqm/qiskit_iqm/iqm_provider.py +49 -36
  32. iqm/qiskit_iqm/iqm_target.py +12 -8
  33. iqm/qiskit_iqm/iqm_transpilation.py +219 -26
  34. iqm/qiskit_iqm/qiskit_to_iqm.py +150 -41
  35. iqm/qiskit_iqm/transpiler_plugins.py +11 -8
  36. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/METADATA +4 -14
  37. iqm_client-33.0.0.dist-info/RECORD +63 -0
  38. iqm/iqm_client/api.py +0 -90
  39. iqm/iqm_client/authentication.py +0 -206
  40. iqm_client-32.0.0.dist-info/RECORD +0 -60
  41. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/AUTHORS.rst +0 -0
  42. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/LICENSE.txt +0 -0
  43. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/WHEEL +0 -0
  44. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/entry_points.txt +0 -0
  45. {iqm_client-32.0.0.dist-info → iqm_client-33.0.0.dist-info}/top_level.txt +0 -0
@@ -15,85 +15,65 @@
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- from datetime import datetime
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 platform
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
- CircuitCompilationOptions,
46
- ClientLibrary,
47
- ClientLibraryDict,
47
+ CircuitMeasurementCountsBatch,
48
+ CircuitMeasurementResults, # noqa: F401
49
+ CircuitMeasurementResultsBatch,
48
50
  DynamicQuantumArchitecture,
49
51
  QIRCode,
50
- QualityMetricSet,
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.iqm_client.validation import validate_circuit_instructions, validate_qubit_mapping
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
- url: Endpoint for accessing the server. Has to start with http or https.
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, url: str, *, client_signature: str | None = None, token: str | None = None, tokens_file: str | None = None
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
- if not url.startswith(("http:", "https:")):
111
- raise ClientConfigurationError(f"The URL schema has to be http or https. Incorrect schema in URL: {url}")
112
- self._token_manager = TokenManager(token, tokens_file)
113
- version_string = "iqm-client"
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._api = APIConfig(url)
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 __del__(self):
133
- try:
134
- # Try our best to close the auth session, doesn't matter if it fails
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
- """Return information about the IQM client."""
141
- return self._station_control.get_about()
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: dict[str, str] | None = None,
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
- ) -> UUID:
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
- ID for the created job. This ID is needed to query the job status and the execution results.
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
- job_id = self.submit_run_request(run_request)
183
- return job_id
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: dict[str, str] | None = None,
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, (QIRCode)):
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=serialized_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
- move_validation_mode=options.move_gate_validation,
256
- move_gate_frame_tracking_mode=options.move_gate_frame_tracking,
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) -> UUID:
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
- ID for the created job. This ID is needed to query the job status and the execution results.
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
- # Use UTF-8 encoding for the JSON payload
291
- result = requests.post(
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
- result.raise_for_status()
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
- Result of the job (can be pending).
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
- status_response = self._get_request(
331
- APIEndpoint.GET_JOB_STATUS,
332
- (str(job_id),),
333
- timeout=timeout_secs,
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
- Returns:
439
- Job result.
264
+ return CircuitJob(_iqm_client=self, data=job_data)
440
265
 
441
- Raises:
442
- APITimeoutError: time exceeded the set timeout
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 wait for.
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
- start_time = datetime.now()
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
- result = requests.post(
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
- if self._static_architecture:
513
- return self._static_architecture
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 isinstance(self._station_control, IqmServerClient):
537
- raise ValueError("'get_quality_metric_set' method is not supported for IqmServerClient.")
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
- def _observation_lite_to_json(obs: ObservationLite) -> dict[str, Any]:
595
- """Convert ObservationLite to JSON serializable dictionary."""
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._dynamic_architectures:
645
- return self._dynamic_architectures[calibration_set_id]
646
-
647
- if not calibration_set_id:
648
- if isinstance(self._station_control, IqmServerClient):
649
- dut_label = self._get_dut_label()
650
- calibration_set_id = self._station_control.get_latest_calibration_set_id(dut_label)
651
- else:
652
- calibration_set_id = self._station_control.get_default_calibration_set().observation_set_id
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._station_control.get_channel_properties()
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 get_run_counts(self, job_id: UUID, *, timeout_secs: float = REQUESTS_TIMEOUT) -> RunCounts:
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
- Measurement results of the job in histogram representation.
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
- response = self._get_request(
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 get_supported_client_libraries(self, timeout_secs: float = REQUESTS_TIMEOUT) -> dict[str, ClientLibrary] | None:
720
- """Retrieve information about supported client libraries from the server.
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
- timeout_secs: Network request timeout (seconds).
413
+ job_id: ID of the job to query.
724
414
 
725
415
  Returns:
726
- Mapping from library identifiers to their metadata.
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
- # TODO: Remove "client-libraries" usage after using versioned URLs in station control
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 close_auth_session(self) -> bool:
753
- """Terminate session with authentication server if there is one.
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
- return self._token_manager.close()
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
- Message about client incompatibility with the server if the versions are incompatible or if the
800
- compatibility could not be confirmed, ``None`` if they are compatible.
428
+ Payload of the original circuit job.
801
429
 
802
430
  """
803
- try:
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
- duts = self._station_control.get_duts()
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 isinstance(self._station_control, IqmServerClient):
931
- raise ValueError("The _get_calibration_quality_metrics method is not supported by IqmServerClient.")
932
-
933
- if not calibration_set_id:
934
- # find out the default calset id
935
- default_calset = self._station_control.get_default_calibration_set()
936
- calibration_set_id = default_calset.observation_set_id
937
-
938
- calset_obs = self._station_control.get_observation_set_observations(calibration_set_id)
939
- quality_metrics = self._station_control.get_calibration_set_quality_metrics(calibration_set_id)
940
- qm_obs = quality_metrics.observations
941
- return ObservationFinder(calset_obs + qm_obs)
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