qilisdk 0.1.6__py3-none-any.whl → 0.1.7__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 (34) hide show
  1. qilisdk/analog/__init__.py +1 -2
  2. qilisdk/analog/hamiltonian.py +1 -68
  3. qilisdk/analog/schedule.py +288 -313
  4. qilisdk/backends/backend.py +5 -1
  5. qilisdk/backends/cuda_backend.py +9 -5
  6. qilisdk/backends/qutip_backend.py +23 -12
  7. qilisdk/core/__init__.py +4 -0
  8. qilisdk/core/interpolator.py +406 -0
  9. qilisdk/core/parameterizable.py +66 -10
  10. qilisdk/core/variables.py +150 -7
  11. qilisdk/digital/circuit.py +1 -0
  12. qilisdk/digital/circuit_transpiler.py +46 -0
  13. qilisdk/digital/circuit_transpiler_passes/__init__.py +18 -0
  14. qilisdk/digital/circuit_transpiler_passes/circuit_transpiler_pass.py +36 -0
  15. qilisdk/digital/circuit_transpiler_passes/decompose_multi_controlled_gates_pass.py +216 -0
  16. qilisdk/digital/circuit_transpiler_passes/numeric_helpers.py +82 -0
  17. qilisdk/digital/gates.py +12 -2
  18. qilisdk/{speqtrum/experiments → experiments}/__init__.py +13 -2
  19. qilisdk/{speqtrum/experiments → experiments}/experiment_functional.py +90 -2
  20. qilisdk/{speqtrum/experiments → experiments}/experiment_result.py +16 -0
  21. qilisdk/functionals/sampling.py +8 -1
  22. qilisdk/functionals/time_evolution.py +6 -2
  23. qilisdk/functionals/variational_program.py +58 -0
  24. qilisdk/speqtrum/speqtrum.py +360 -130
  25. qilisdk/speqtrum/speqtrum_models.py +108 -19
  26. qilisdk/utils/openfermion/__init__.py +38 -0
  27. qilisdk/{core/algorithm.py → utils/openfermion/__init__.pyi} +2 -3
  28. qilisdk/utils/openfermion/openfermion.py +45 -0
  29. qilisdk/utils/visualization/schedule_renderers.py +16 -8
  30. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/METADATA +74 -24
  31. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/RECORD +33 -26
  32. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/WHEEL +1 -1
  33. qilisdk/analog/linear_schedule.py +0 -121
  34. {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/licenses/LICENCE +0 -0
@@ -14,16 +14,27 @@
14
14
  from __future__ import annotations
15
15
 
16
16
  import base64
17
+ import binascii
17
18
  import json
18
19
  import time
19
20
  from base64 import urlsafe_b64encode
20
21
  from datetime import datetime, timezone
21
- from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, overload
22
+ from typing import TYPE_CHECKING, Any, Callable, Generator, TypeAlias, TypeVar, cast, overload
22
23
 
23
24
  import httpx
24
25
  from loguru import logger
25
- from pydantic import TypeAdapter
26
+ from pydantic import TypeAdapter, ValidationError
26
27
 
28
+ from qilisdk.experiments import (
29
+ RabiExperiment,
30
+ RabiExperimentResult,
31
+ T1Experiment,
32
+ T1ExperimentResult,
33
+ T2Experiment,
34
+ T2ExperimentResult,
35
+ TwoTonesExperiment,
36
+ TwoTonesExperimentResult,
37
+ )
27
38
  from qilisdk.functionals import (
28
39
  Sampling,
29
40
  SamplingResult,
@@ -34,12 +45,6 @@ from qilisdk.functionals import (
34
45
  )
35
46
  from qilisdk.functionals.functional_result import FunctionalResult
36
47
  from qilisdk.settings import get_settings
37
- from qilisdk.speqtrum.experiments import (
38
- RabiExperiment,
39
- RabiExperimentResult,
40
- T1Experiment,
41
- T1ExperimentResult,
42
- )
43
48
 
44
49
  from .keyring import delete_credentials, load_credentials, store_credentials
45
50
  from .speqtrum_models import (
@@ -55,8 +60,10 @@ from .speqtrum_models import (
55
60
  RabiExperimentPayload,
56
61
  SamplingPayload,
57
62
  T1ExperimentPayload,
63
+ T2ExperimentPayload,
58
64
  TimeEvolutionPayload,
59
65
  Token,
66
+ TwoTonesExperimentPayload,
60
67
  TypedJobDetail,
61
68
  VariationalProgramPayload,
62
69
  )
@@ -65,33 +72,199 @@ if TYPE_CHECKING:
65
72
  from qilisdk.functionals.functional import Functional, PrimitiveFunctional
66
73
 
67
74
 
68
- ResultT = TypeVar("ResultT", bound=FunctionalResult)
75
+ TFunctionalResult = TypeVar("TFunctionalResult", bound=FunctionalResult)
76
+ JSONValue: TypeAlias = dict[str, "JSONValue"] | list["JSONValue"] | str | int | float | bool | None
77
+
78
+ _SKIP_ENSURE_OK_EXTENSION = "qilisdk.skip_ensure_ok"
79
+ _CONTEXT_EXTENSION = "qilisdk.request_context"
80
+
81
+
82
+ class SpeQtrumAPIError(httpx.HTTPStatusError):
83
+ """Raised when the SpeQtrum API responds with a non-success HTTP status."""
84
+
85
+ def __init__(self, message: str, *, request: httpx.Request, response: httpx.Response) -> None:
86
+ super().__init__(message, request=request, response=response)
87
+
88
+
89
+ def _safe_json_loads(value: str, *, context: str) -> JSONValue | None:
90
+ try:
91
+ result = json.loads(value)
92
+ return cast("JSONValue", result)
93
+ except json.JSONDecodeError as exc:
94
+ logger.warning("Failed to decode JSON for {}: {}", context, exc)
95
+ return None
96
+
97
+
98
+ def _safe_b64_decode(value: str, *, context: str) -> str | None:
99
+ try:
100
+ decoded_bytes = base64.b64decode(value)
101
+ except (binascii.Error, ValueError) as exc:
102
+ logger.warning("Failed to base64 decode {}: {}", context, exc)
103
+ return None
104
+ try:
105
+ return decoded_bytes.decode("utf-8")
106
+ except UnicodeDecodeError as exc:
107
+ logger.warning("Failed to UTF-8 decode {}: {}", context, exc)
108
+ return None
109
+
110
+
111
+ def _safe_b64_json(value: str, *, context: str) -> JSONValue | None:
112
+ decoded_text = _safe_b64_decode(value, context=context)
113
+ if decoded_text is None:
114
+ return None
115
+ return _safe_json_loads(decoded_text, context=context)
116
+
117
+
118
+ def _request_extensions(*, context: str | None = None, skip_ensure_ok: bool = False) -> dict[str, Any] | None:
119
+ extensions: dict[str, Any] = {}
120
+ if context:
121
+ extensions[_CONTEXT_EXTENSION] = context
122
+ if skip_ensure_ok:
123
+ extensions[_SKIP_ENSURE_OK_EXTENSION] = True
124
+ return extensions or None
125
+
126
+
127
+ def _response_context(response: httpx.Response) -> str:
128
+ request = response.request
129
+ if request is None:
130
+ return "SpeQtrum API call"
131
+ context = request.extensions.get(_CONTEXT_EXTENSION)
132
+ if isinstance(context, str) and context.strip():
133
+ return context
134
+ return f"{request.method} {request.url}"
135
+
136
+
137
+ def _stringify_payload(payload: JSONValue | None) -> str | None:
138
+ if payload is None:
139
+ return None
140
+ if isinstance(payload, dict):
141
+ for key in ("message", "detail", "error", "error_description", "title"):
142
+ value = payload.get(key)
143
+ if isinstance(value, str) and value.strip():
144
+ code = payload.get("code") or payload.get("error_code") or payload.get("errorCode")
145
+ code_suffix = f" (code={code})" if isinstance(code, (str, int)) else ""
146
+ return value.strip() + code_suffix
147
+ try:
148
+ return json.dumps(payload, sort_keys=True)
149
+ except (TypeError, ValueError):
150
+ return str(payload)
151
+ if isinstance(payload, list):
152
+ preview = ", ".join(str(item) for item in payload[:3])
153
+ return preview or str(payload)
154
+ if isinstance(payload, (str, int, float, bool)):
155
+ return str(payload)
156
+ return None
157
+
158
+
159
+ def _summarize_error_payload(response: httpx.Response) -> str:
160
+ context = _response_context(response)
161
+ try:
162
+ body_text = response.text or ""
163
+ except httpx.ResponseNotRead:
164
+ try:
165
+ response.read() # ensure body is buffered so we can reuse it later
166
+ body_text = response.text or ""
167
+ except Exception as exc: # noqa: BLE001
168
+ logger.debug("Failed to read response body for {}: {}", context, exc)
169
+ body_text = ""
170
+ payload = _safe_json_loads(body_text, context=f"{context} error body") if body_text else None
171
+ detail = _stringify_payload(payload)
172
+ if detail:
173
+ return detail
174
+ if body_text.strip():
175
+ return body_text.strip()
176
+ return "no response body"
177
+
178
+
179
+ def _ensure_ok(response: httpx.Response) -> None:
180
+ request = response.request
181
+ if request is not None and request.extensions.get(_SKIP_ENSURE_OK_EXTENSION):
182
+ return
183
+ if response.status_code == httpx.codes.UNAUTHORIZED:
184
+ return
185
+ try:
186
+ response.raise_for_status()
187
+ except httpx.HTTPStatusError:
188
+ context = _response_context(response)
189
+ detail = _summarize_error_payload(response)
190
+ logger.error(
191
+ "{} failed with status {} {}: {}",
192
+ context,
193
+ response.status_code,
194
+ response.reason_phrase,
195
+ detail,
196
+ )
197
+ message = f"{context} failed with status {response.status_code}: {detail}"
198
+ raise SpeQtrumAPIError(message, request=response.request, response=response) from None
199
+
200
+
201
+ class _BearerAuth(httpx.Auth):
202
+ """Bearer token auth handler with automatic refresh support."""
203
+
204
+ requires_response_body = True
205
+
206
+ def __init__(self, client: SpeQtrum) -> None:
207
+ self._client = client
208
+
209
+ def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
210
+ request.headers["Authorization"] = f"Bearer {self._client.token.access_token}"
211
+ response = yield request
212
+
213
+ if response.status_code == httpx.codes.UNAUTHORIZED:
214
+ settings = get_settings()
215
+ refresh_request = httpx.Request(
216
+ "POST",
217
+ settings.speqtrum_api_url + "/authorisation-tokens/refresh",
218
+ headers={"Authorization": f"Bearer {self._client.token.refresh_token}"},
219
+ extensions=_request_extensions(context="Refreshing SpeQtrum token", skip_ensure_ok=True),
220
+ )
221
+ refresh_response = yield refresh_request
222
+
223
+ try:
224
+ refresh_response.raise_for_status()
225
+ except httpx.HTTPStatusError as exc:
226
+ logger.error(
227
+ "Token refresh failed with status {} {}",
228
+ exc.response.status_code,
229
+ exc.response.reason_phrase,
230
+ )
231
+ raise
232
+
233
+ try:
234
+ payload = refresh_response.json()
235
+ except json.JSONDecodeError as exc:
236
+ logger.error("Token refresh returned invalid JSON: {}", exc)
237
+ raise RuntimeError("SpeQtrum token refresh failed: invalid JSON payload") from exc
238
+
239
+ if not isinstance(payload, dict):
240
+ logger.error("Token refresh returned non-object payload: {}", type(payload).__name__)
241
+ raise RuntimeError("SpeQtrum token refresh failed: malformed token payload")
242
+
243
+ payload.pop("refreshToken", None)
244
+
245
+ try:
246
+ token = Token(**payload, refreshToken=self._client.token.refresh_token)
247
+ except (TypeError, ValidationError) as exc:
248
+ logger.error("Token refresh returned malformed payload: {}", exc)
249
+ raise RuntimeError("SpeQtrum token refresh failed: malformed token payload") from exc
250
+
251
+ self._client.token = token
252
+ store_credentials(self._client.username, self._client.token)
253
+ request.headers["Authorization"] = f"Bearer {self._client.token.access_token}"
254
+ yield request
69
255
 
70
256
 
71
257
  class SpeQtrum:
72
258
  """Synchronous client for the Qilimanjaro SpeQtrum API."""
73
259
 
74
260
  def __init__(self) -> None:
75
- logger.debug("Initializing QaaS client")
261
+ logger.debug("Initializing SpeQtrum client")
76
262
  credentials = load_credentials()
77
263
  if credentials is None:
78
- logger.error("No QaaS credentials found. Call `.login()` or set env vars before instantiation.")
79
- raise RuntimeError("Missing QaaS credentials - invoke SpeQtrum.login() first.")
80
- self._username, self._token = credentials
81
- self._handlers: dict[type[Functional], Callable[[Functional, str, str | None], JobHandle[Any]]] = {
82
- Sampling: lambda f, device, job_name: self._submit_sampling(cast("Sampling", f), device, job_name),
83
- TimeEvolution: lambda f, device, job_name: self._submit_time_evolution(
84
- cast("TimeEvolution", f), device, job_name
85
- ),
86
- RabiExperiment: lambda f, device, job_name: self._submit_rabi_program(
87
- cast("RabiExperiment", f), device, job_name
88
- ),
89
- T1Experiment: lambda f, device, job_name: self._submit_t1_program(
90
- cast("T1Experiment", f), device, job_name
91
- ),
92
- }
93
- self._settings = get_settings()
94
- logger.success("QaaS client initialised for user '{}'", self._username)
264
+ logger.error("No credentials found. Call `SpeQtrum.login()` before instantiation.")
265
+ raise RuntimeError("Missing credentials - invoke SpeQtrum.login() first.")
266
+ self.username, self.token = credentials
267
+ logger.success("SpeQtrum client initialised for user '{}'", self.username)
95
268
 
96
269
  @classmethod
97
270
  def _get_headers(cls) -> dict:
@@ -99,8 +272,15 @@ class SpeQtrum:
99
272
 
100
273
  return {"User-Agent": f"qilisdk/{__version__}"}
101
274
 
102
- def _get_authorized_headers(self) -> dict:
103
- return {**self._get_headers(), "Authorization": f"Bearer {self._token.access_token}"}
275
+ def _create_client(self) -> httpx.Client:
276
+ """Return a freshly configured HTTP client for SpeQtrum interactions."""
277
+ settings = get_settings()
278
+ return httpx.Client(
279
+ base_url=settings.speqtrum_api_url,
280
+ headers=self._get_headers(),
281
+ auth=_BearerAuth(self),
282
+ event_hooks={"response": [_ensure_ok]},
283
+ )
104
284
 
105
285
  @classmethod
106
286
  def login(
@@ -129,7 +309,7 @@ class SpeQtrum:
129
309
  apikey = apikey or settings.speqtrum_apikey
130
310
 
131
311
  if not username or not apikey:
132
- logger.warning("Login called without credentials - aborting")
312
+ logger.error("Login called without credentials.")
133
313
  return False
134
314
 
135
315
  # Send login request to QaaS
@@ -142,23 +322,24 @@ class SpeQtrum:
142
322
  "iat": int(datetime.now(timezone.utc).timestamp()),
143
323
  }
144
324
  encoded_assertion = urlsafe_b64encode(json.dumps(assertion, indent=2).encode("utf-8")).decode("utf-8")
145
- with httpx.Client(timeout=10.0) as client:
325
+ with httpx.Client(
326
+ base_url=settings.speqtrum_api_url,
327
+ headers=cls._get_headers(),
328
+ timeout=10.0,
329
+ event_hooks={"response": [_ensure_ok]},
330
+ ) as client:
146
331
  response = client.post(
147
- settings.speqtrum_api_url + "/authorisation-tokens",
332
+ "/authorisation-tokens",
148
333
  json={
149
334
  "grantType": "urn:ietf:params:oauth:grant-type:jwt-bearer",
150
335
  "assertion": encoded_assertion,
151
336
  "scope": "user profile",
152
337
  },
153
- headers=cls._get_headers(),
338
+ extensions=_request_extensions(context="Authenticating user"),
154
339
  )
155
340
  response.raise_for_status()
156
341
  token = Token(**response.json())
157
- except httpx.HTTPStatusError as exc:
158
- logger.error("Login failed - server returned {} {}", exc.response.status_code, exc.response.reason_phrase)
159
- return False
160
- except httpx.RequestError:
161
- logger.exception("Network error while logging in to QaaS")
342
+ except httpx.HTTPError:
162
343
  return False
163
344
 
164
345
  store_credentials(username=username, token=token)
@@ -182,12 +363,9 @@ class SpeQtrum:
182
363
  A list of :class:`~qilisdk.models.Device` objects.
183
364
  """
184
365
  logger.debug("Fetching device list from server…")
185
- with httpx.Client() as client:
186
- response = client.get(self._settings.speqtrum_api_url + "/devices", headers=self._get_authorized_headers())
187
- response.raise_for_status()
188
-
189
- devices = TypeAdapter(list[Device]).validate_python(response.json()["items"])
190
-
366
+ with self._create_client() as client:
367
+ response = client.get("/devices", extensions=_request_extensions(context="Fetching device list"))
368
+ devices = TypeAdapter(list[Device]).validate_python(response.json()["items"])
191
369
  logger.success("{} devices retrieved", len(devices))
192
370
  return [d for d in devices if where(d)] if where else devices
193
371
 
@@ -203,17 +381,14 @@ class SpeQtrum:
203
381
  A list of :class:`~qilisdk.models.JobInfo` objects.
204
382
  """
205
383
  logger.debug("Fetching job list…")
206
- with httpx.Client() as client:
207
- response = client.get(self._settings.speqtrum_api_url + "/jobs", headers=self._get_authorized_headers())
208
- response.raise_for_status()
209
-
210
- jobs = TypeAdapter(list[JobInfo]).validate_python(response.json()["items"])
211
-
384
+ with self._create_client() as client:
385
+ response = client.get("/jobs", extensions=_request_extensions(context="Fetching job list"))
386
+ jobs = TypeAdapter(list[JobInfo]).validate_python(response.json()["items"])
212
387
  logger.success("{} jobs retrieved", len(jobs))
213
388
  return [j for j in jobs if where(j)] if where else jobs
214
389
 
215
390
  @overload
216
- def get_job(self, job: JobHandle[ResultT]) -> TypedJobDetail[ResultT]: ...
391
+ def get_job(self, job: JobHandle[TFunctionalResult]) -> TypedJobDetail[TFunctionalResult]: ...
217
392
 
218
393
  @overload
219
394
  def get_job(self, job: int) -> JobDetail: ...
@@ -230,10 +405,9 @@ class SpeQtrum:
230
405
  """
231
406
  job_id = job.id if isinstance(job, JobHandle) else job
232
407
  logger.debug("Retrieving job {} details", job_id)
233
- with httpx.Client() as client:
408
+ with self._create_client() as client:
234
409
  response = client.get(
235
- f"{self._settings.speqtrum_api_url}/jobs/{job_id}",
236
- headers=self._get_authorized_headers(),
410
+ f"/jobs/{job_id}",
237
411
  params={
238
412
  "payload": True,
239
413
  "result": True,
@@ -241,29 +415,28 @@ class SpeQtrum:
241
415
  "error_logs": True,
242
416
  "error": True,
243
417
  },
418
+ extensions=_request_extensions(context=f"Fetching job {job_id}"),
244
419
  )
245
- response.raise_for_status()
246
- data = response.json()
247
-
248
- raw_payload = data["payload"]
249
- if raw_payload is not None:
250
- data["payload"] = json.loads(raw_payload)
420
+ data = response.json()
421
+ raw_payload = data.get("payload")
422
+ if isinstance(raw_payload, str):
423
+ data["payload"] = _safe_json_loads(raw_payload, context=f"job {job_id} payload")
251
424
 
252
425
  raw_result = data.get("result")
253
- if raw_result is not None:
254
- decoded_result: bytes = base64.b64decode(raw_result)
255
- text_result = decoded_result.decode("utf-8")
256
- data["result"] = json.loads(text_result)
426
+ if isinstance(raw_result, str):
427
+ data["result"] = _safe_b64_json(raw_result, context=f"job {job_id} result")
257
428
 
258
429
  raw_error = data.get("error")
259
- if raw_error is not None:
260
- decoded_error: bytes = base64.b64decode(raw_error)
261
- data["error"] = decoded_error.decode("utf-8")
430
+ if isinstance(raw_error, str):
431
+ data["error"] = _safe_b64_decode(raw_error, context=f"job {job_id} error")
262
432
 
263
433
  raw_logs = data.get("logs")
264
- if raw_logs is not None:
265
- decoded_logs: bytes = base64.b64decode(raw_logs)
266
- data["logs"] = decoded_logs.decode("utf-8")
434
+ if isinstance(raw_logs, str):
435
+ data["logs"] = _safe_b64_decode(raw_logs, context=f"job {job_id} logs")
436
+
437
+ raw_error_logs = data.get("error_logs")
438
+ if isinstance(raw_error_logs, str):
439
+ data["error_logs"] = _safe_b64_decode(raw_error_logs, context=f"job {job_id} error logs")
267
440
 
268
441
  job_detail = TypeAdapter(JobDetail).validate_python(data)
269
442
  logger.debug("Job {} details retrieved (status {})", job_id, job_detail.status.value)
@@ -274,11 +447,11 @@ class SpeQtrum:
274
447
  @overload
275
448
  def wait_for_job(
276
449
  self,
277
- job: JobHandle[ResultT],
450
+ job: JobHandle[TFunctionalResult],
278
451
  *,
279
452
  poll_interval: float = 5.0,
280
453
  timeout: float | None = None,
281
- ) -> TypedJobDetail[ResultT]: ...
454
+ ) -> TypedJobDetail[TFunctionalResult]: ...
282
455
 
283
456
  @overload
284
457
  def wait_for_job(
@@ -362,8 +535,11 @@ class SpeQtrum:
362
535
 
363
536
  @overload
364
537
  def submit(
365
- self, functional: VariationalProgram[PrimitiveFunctional[ResultT]], device: str, job_name: str | None = None
366
- ) -> JobHandle[VariationalProgramResult[ResultT]]: ...
538
+ self,
539
+ functional: VariationalProgram[PrimitiveFunctional[TFunctionalResult]],
540
+ device: str,
541
+ job_name: str | None = None,
542
+ ) -> JobHandle[VariationalProgramResult[TFunctionalResult]]: ...
367
543
 
368
544
  @overload
369
545
  def submit(
@@ -406,31 +582,41 @@ class SpeQtrum:
406
582
  Raises:
407
583
  NotImplementedError: If *functional* is not of a supported type.
408
584
  """
409
- try:
410
- if isinstance(functional, VariationalProgram):
411
- inner = functional.functional
412
- if isinstance(inner, Sampling):
413
- return self._submit_variational_program(cast("VariationalProgram[Sampling]", functional), device)
414
- if isinstance(inner, TimeEvolution):
415
- return self._submit_variational_program(
416
- cast("VariationalProgram[TimeEvolution]", functional), device
417
- )
418
-
419
- # Fallback to untyped handle for custom primitives.
420
- job_handle = self._submit_variational_program(cast("VariationalProgram[Any]", functional), device)
421
- return cast("JobHandle[FunctionalResult]", job_handle)
422
-
423
- handler = self._handlers[type(functional)]
424
- except KeyError as exc:
425
- logger.error("Unsupported functional type: {}", type(functional).__qualname__)
426
- raise NotImplementedError(
427
- f"{type(self).__qualname__} does not support {type(functional).__qualname__}"
428
- ) from exc
429
-
430
- logger.info("Submitting {}", type(functional).__qualname__)
431
- job_handle = handler(functional, device, job_name)
432
- logger.success("Submission complete - job {}", job_handle.id)
433
- return job_handle
585
+ if isinstance(functional, VariationalProgram):
586
+ inner = functional.functional
587
+ if isinstance(inner, Sampling):
588
+ return self._submit_variational_program(
589
+ cast("VariationalProgram[Sampling]", functional), device, job_name
590
+ )
591
+ if isinstance(inner, TimeEvolution):
592
+ return self._submit_variational_program(
593
+ cast("VariationalProgram[TimeEvolution]", functional), device, job_name
594
+ )
595
+
596
+ # Fallback to untyped handle for custom primitives.
597
+ job_handle = self._submit_variational_program(cast("VariationalProgram[Any]", functional), device, job_name)
598
+ return cast("JobHandle[FunctionalResult]", job_handle)
599
+
600
+ if isinstance(functional, Sampling):
601
+ return self._submit_sampling(functional, device, job_name)
602
+
603
+ if isinstance(functional, TimeEvolution):
604
+ return self._submit_time_evolution(functional, device, job_name)
605
+
606
+ if isinstance(functional, RabiExperiment):
607
+ return self._submit_rabi(functional, device, job_name)
608
+
609
+ if isinstance(functional, T1Experiment):
610
+ return self._submit_t1(functional, device, job_name)
611
+
612
+ if isinstance(functional, T2Experiment):
613
+ return self._submit_t2(functional, device, job_name)
614
+
615
+ if isinstance(functional, TwoTonesExperiment):
616
+ return self._submit_two_tones(functional, device, job_name)
617
+
618
+ logger.error("Unsupported functional type: {}", type(functional).__qualname__)
619
+ raise NotImplementedError(f"{type(self).__qualname__} does not support {type(functional).__qualname__}")
434
620
 
435
621
  def _submit_sampling(
436
622
  self, sampling: Sampling, device: str, job_name: str | None = None
@@ -448,17 +634,13 @@ class SpeQtrum:
448
634
  if job_name:
449
635
  json["name"] = job_name
450
636
  logger.debug("Executing Sampling on device {}", device)
451
- with httpx.Client() as client:
452
- response = client.post(
453
- self._settings.speqtrum_api_url + "/execute",
454
- headers=self._get_authorized_headers(),
455
- json=json,
456
- )
457
- response.raise_for_status()
458
- job = JobId(**response.json())
637
+ with self._create_client() as client:
638
+ response = client.post("/execute", json=json, extensions=_request_extensions(context="Executing Sampling"))
639
+ job = JobId(**response.json())
640
+ logger.info("Sampling job submitted: {}", job.id)
459
641
  return JobHandle.sampling(job.id)
460
642
 
461
- def _submit_rabi_program(
643
+ def _submit_rabi(
462
644
  self, rabi_experiment: RabiExperiment, device: str, job_name: str | None = None
463
645
  ) -> JobHandle[RabiExperimentResult]:
464
646
  payload = ExecutePayload(
@@ -474,18 +656,17 @@ class SpeQtrum:
474
656
  if job_name:
475
657
  json["name"] = job_name
476
658
  logger.debug("Executing Rabi experiment on device {}", device)
477
- with httpx.Client() as client:
659
+ with self._create_client() as client:
478
660
  response = client.post(
479
- self._settings.speqtrum_api_url + "/execute",
480
- headers=self._get_authorized_headers(),
661
+ "/execute",
481
662
  json=json,
663
+ extensions=_request_extensions(context="Executing Rabi experiment"),
482
664
  )
483
- response.raise_for_status()
484
- job = JobId(**response.json())
665
+ job = JobId(**response.json())
485
666
  logger.info("Rabi experiment job submitted: {}", job.id)
486
667
  return JobHandle.rabi_experiment(job.id)
487
668
 
488
- def _submit_t1_program(
669
+ def _submit_t1(
489
670
  self, t1_experiment: T1Experiment, device: str, job_name: str | None = None
490
671
  ) -> JobHandle[T1ExperimentResult]:
491
672
  payload = ExecutePayload(
@@ -501,17 +682,68 @@ class SpeQtrum:
501
682
  if job_name:
502
683
  json["name"] = job_name
503
684
  logger.debug("Executing T1 experiment on device {}", device)
504
- with httpx.Client() as client:
685
+ with self._create_client() as client:
505
686
  response = client.post(
506
- self._settings.speqtrum_api_url + "/execute",
507
- headers=self._get_authorized_headers(),
687
+ "/execute",
508
688
  json=json,
689
+ extensions=_request_extensions(context="Executing T1 experiment"),
509
690
  )
510
- response.raise_for_status()
511
- job = JobId(**response.json())
691
+ job = JobId(**response.json())
512
692
  logger.info("T1 experiment job submitted: {}", job.id)
513
693
  return JobHandle.t1_experiment(job.id)
514
694
 
695
+ def _submit_t2(
696
+ self, t2_experiment: T2Experiment, device: str, job_name: str | None = None
697
+ ) -> JobHandle[T2ExperimentResult]:
698
+ payload = ExecutePayload(
699
+ type=ExecuteType.T2_EXPERIMENT,
700
+ t2_experiment_payload=T2ExperimentPayload(t2_experiment=t2_experiment),
701
+ )
702
+ json = {
703
+ "device_code": device,
704
+ "payload": payload.model_dump_json(),
705
+ "job_type": JobType.PULSE,
706
+ "meta": {},
707
+ }
708
+ if job_name:
709
+ json["name"] = job_name
710
+ logger.debug("Executing T2 experiment on device {}", device)
711
+ with self._create_client() as client:
712
+ response = client.post(
713
+ "/execute",
714
+ json=json,
715
+ extensions=_request_extensions(context="Executing T2 experiment"),
716
+ )
717
+ job = JobId(**response.json())
718
+ logger.info("T2 experiment job submitted: {}", job.id)
719
+ return JobHandle.t2_experiment(job.id)
720
+
721
+ def _submit_two_tones(
722
+ self, two_tones_experiment: TwoTonesExperiment, device: str, job_name: str | None = None
723
+ ) -> JobHandle[TwoTonesExperimentResult]:
724
+ payload = ExecutePayload(
725
+ type=ExecuteType.TWO_TONES_EXPERIMENT,
726
+ two_tones_experiment_payload=TwoTonesExperimentPayload(two_tones_experiment=two_tones_experiment),
727
+ )
728
+ json = {
729
+ "device_code": device,
730
+ "payload": payload.model_dump_json(),
731
+ "job_type": JobType.PULSE,
732
+ "meta": {},
733
+ }
734
+ if job_name:
735
+ json["name"] = job_name
736
+ logger.debug("Executing Two-Tones experiment on device {}", device)
737
+ with self._create_client() as client:
738
+ response = client.post(
739
+ "/execute",
740
+ json=json,
741
+ extensions=_request_extensions(context="Executing Two-Tones experiment"),
742
+ )
743
+ job = JobId(**response.json())
744
+ logger.info("Two-Tones experiment job submitted: {}", job.id)
745
+ return JobHandle.two_tones_experiment(job.id)
746
+
515
747
  def _submit_time_evolution(
516
748
  self, time_evolution: TimeEvolution, device: str, job_name: str | None = None
517
749
  ) -> JobHandle[TimeEvolutionResult]:
@@ -528,15 +760,14 @@ class SpeQtrum:
528
760
  if job_name:
529
761
  json["name"] = job_name
530
762
  logger.debug("Executing time evolution on device {}", device)
531
- with httpx.Client() as client:
763
+ with self._create_client() as client:
532
764
  response = client.post(
533
- self._settings.speqtrum_api_url + "/execute",
534
- headers=self._get_authorized_headers(),
765
+ "/execute",
535
766
  json=json,
767
+ extensions=_request_extensions(context="Executing time evolution"),
536
768
  )
537
- response.raise_for_status()
538
- job = JobId(**response.json())
539
- logger.info("Time evolution job submitted: {}", job.id)
769
+ job = JobId(**response.json())
770
+ logger.info("Time Evolution job submitted: {}", job.id)
540
771
  return JobHandle.time_evolution(job.id)
541
772
 
542
773
  @overload
@@ -570,14 +801,13 @@ class SpeQtrum:
570
801
  if job_name:
571
802
  json["name"] = job_name
572
803
  logger.debug("Executing variational program on device {}", device)
573
- with httpx.Client() as client:
804
+ with self._create_client() as client:
574
805
  response = client.post(
575
- self._settings.speqtrum_api_url + "/execute",
576
- headers=self._get_authorized_headers(),
806
+ "/execute",
577
807
  json=json,
808
+ extensions=_request_extensions(context="Executing variational program"),
578
809
  )
579
- response.raise_for_status()
580
- job = JobId(**response.json())
810
+ job = JobId(**response.json())
581
811
  logger.info("Variational program job submitted: {}", job.id)
582
812
  inner = variational_program.functional
583
813
  if isinstance(inner, Sampling):