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.
- qilisdk/analog/__init__.py +1 -2
- qilisdk/analog/hamiltonian.py +1 -68
- qilisdk/analog/schedule.py +288 -313
- qilisdk/backends/backend.py +5 -1
- qilisdk/backends/cuda_backend.py +9 -5
- qilisdk/backends/qutip_backend.py +23 -12
- qilisdk/core/__init__.py +4 -0
- qilisdk/core/interpolator.py +406 -0
- qilisdk/core/parameterizable.py +66 -10
- qilisdk/core/variables.py +150 -7
- qilisdk/digital/circuit.py +1 -0
- qilisdk/digital/circuit_transpiler.py +46 -0
- qilisdk/digital/circuit_transpiler_passes/__init__.py +18 -0
- qilisdk/digital/circuit_transpiler_passes/circuit_transpiler_pass.py +36 -0
- qilisdk/digital/circuit_transpiler_passes/decompose_multi_controlled_gates_pass.py +216 -0
- qilisdk/digital/circuit_transpiler_passes/numeric_helpers.py +82 -0
- qilisdk/digital/gates.py +12 -2
- qilisdk/{speqtrum/experiments → experiments}/__init__.py +13 -2
- qilisdk/{speqtrum/experiments → experiments}/experiment_functional.py +90 -2
- qilisdk/{speqtrum/experiments → experiments}/experiment_result.py +16 -0
- qilisdk/functionals/sampling.py +8 -1
- qilisdk/functionals/time_evolution.py +6 -2
- qilisdk/functionals/variational_program.py +58 -0
- qilisdk/speqtrum/speqtrum.py +360 -130
- qilisdk/speqtrum/speqtrum_models.py +108 -19
- qilisdk/utils/openfermion/__init__.py +38 -0
- qilisdk/{core/algorithm.py → utils/openfermion/__init__.pyi} +2 -3
- qilisdk/utils/openfermion/openfermion.py +45 -0
- qilisdk/utils/visualization/schedule_renderers.py +16 -8
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/METADATA +74 -24
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/RECORD +33 -26
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/WHEEL +1 -1
- qilisdk/analog/linear_schedule.py +0 -121
- {qilisdk-0.1.6.dist-info → qilisdk-0.1.7.dist-info}/licenses/LICENCE +0 -0
qilisdk/speqtrum/speqtrum.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
261
|
+
logger.debug("Initializing SpeQtrum client")
|
|
76
262
|
credentials = load_credentials()
|
|
77
263
|
if credentials is None:
|
|
78
|
-
logger.error("No
|
|
79
|
-
raise RuntimeError("Missing
|
|
80
|
-
self.
|
|
81
|
-
|
|
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
|
|
103
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
338
|
+
extensions=_request_extensions(context="Authenticating user"),
|
|
154
339
|
)
|
|
155
340
|
response.raise_for_status()
|
|
156
341
|
token = Token(**response.json())
|
|
157
|
-
except httpx.
|
|
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
|
|
186
|
-
response = client.get(
|
|
187
|
-
|
|
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
|
|
207
|
-
response = client.get(
|
|
208
|
-
|
|
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[
|
|
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
|
|
408
|
+
with self._create_client() as client:
|
|
234
409
|
response = client.get(
|
|
235
|
-
f"
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
254
|
-
|
|
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
|
|
260
|
-
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
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[
|
|
450
|
+
job: JobHandle[TFunctionalResult],
|
|
278
451
|
*,
|
|
279
452
|
poll_interval: float = 5.0,
|
|
280
453
|
timeout: float | None = None,
|
|
281
|
-
) -> TypedJobDetail[
|
|
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,
|
|
366
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
452
|
-
response = client.post(
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
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
|
|
659
|
+
with self._create_client() as client:
|
|
478
660
|
response = client.post(
|
|
479
|
-
|
|
480
|
-
headers=self._get_authorized_headers(),
|
|
661
|
+
"/execute",
|
|
481
662
|
json=json,
|
|
663
|
+
extensions=_request_extensions(context="Executing Rabi experiment"),
|
|
482
664
|
)
|
|
483
|
-
|
|
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
|
|
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
|
|
685
|
+
with self._create_client() as client:
|
|
505
686
|
response = client.post(
|
|
506
|
-
|
|
507
|
-
headers=self._get_authorized_headers(),
|
|
687
|
+
"/execute",
|
|
508
688
|
json=json,
|
|
689
|
+
extensions=_request_extensions(context="Executing T1 experiment"),
|
|
509
690
|
)
|
|
510
|
-
|
|
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
|
|
763
|
+
with self._create_client() as client:
|
|
532
764
|
response = client.post(
|
|
533
|
-
|
|
534
|
-
headers=self._get_authorized_headers(),
|
|
765
|
+
"/execute",
|
|
535
766
|
json=json,
|
|
767
|
+
extensions=_request_extensions(context="Executing time evolution"),
|
|
536
768
|
)
|
|
537
|
-
|
|
538
|
-
|
|
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
|
|
804
|
+
with self._create_client() as client:
|
|
574
805
|
response = client.post(
|
|
575
|
-
|
|
576
|
-
headers=self._get_authorized_headers(),
|
|
806
|
+
"/execute",
|
|
577
807
|
json=json,
|
|
808
|
+
extensions=_request_extensions(context="Executing variational program"),
|
|
578
809
|
)
|
|
579
|
-
|
|
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):
|