qilisdk 0.1.4__py3-none-any.whl → 0.1.6__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/__init__.py +11 -2
- qilisdk/__init__.pyi +2 -3
- qilisdk/_logging.py +135 -0
- qilisdk/_optionals.py +5 -7
- qilisdk/analog/__init__.py +3 -18
- qilisdk/analog/exceptions.py +2 -4
- qilisdk/analog/hamiltonian.py +455 -110
- qilisdk/analog/linear_schedule.py +121 -0
- qilisdk/analog/schedule.py +275 -79
- qilisdk/{extras → backends}/__init__.py +9 -4
- qilisdk/{common/model.py → backends/__init__.pyi} +3 -1
- qilisdk/backends/backend.py +117 -0
- qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
- qilisdk/backends/qutip_backend.py +473 -0
- qilisdk/core/__init__.py +63 -0
- qilisdk/{common → core}/algorithm.py +2 -1
- qilisdk/{extras/qaas/qaas_settings.py → core/exceptions.py} +12 -6
- qilisdk/core/model.py +1034 -0
- qilisdk/core/parameterizable.py +75 -0
- qilisdk/core/qtensor.py +666 -0
- qilisdk/{common → core}/result.py +2 -1
- qilisdk/core/variables.py +1969 -0
- qilisdk/cost_functions/__init__.py +18 -0
- qilisdk/cost_functions/cost_function.py +77 -0
- qilisdk/cost_functions/model_cost_function.py +145 -0
- qilisdk/cost_functions/observable_cost_function.py +109 -0
- qilisdk/digital/__init__.py +3 -22
- qilisdk/digital/ansatz.py +200 -160
- qilisdk/digital/circuit.py +81 -9
- qilisdk/digital/exceptions.py +12 -6
- qilisdk/digital/gates.py +229 -86
- qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
- qilisdk/functionals/functional.py +39 -0
- qilisdk/{common/backend.py → functionals/functional_result.py} +3 -1
- qilisdk/functionals/sampling.py +81 -0
- qilisdk/functionals/sampling_result.py +92 -0
- qilisdk/functionals/time_evolution.py +98 -0
- qilisdk/functionals/time_evolution_result.py +84 -0
- qilisdk/functionals/variational_program.py +80 -0
- qilisdk/functionals/variational_program_result.py +69 -0
- qilisdk/logging_config.yaml +16 -0
- qilisdk/{common → optimizers}/__init__.py +1 -1
- qilisdk/optimizers/optimizer.py +39 -0
- qilisdk/{common → optimizers}/optimizer_result.py +3 -12
- qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
- qilisdk/settings.py +78 -0
- qilisdk/speqtrum/__init__.py +41 -0
- qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
- qilisdk/speqtrum/experiments/__init__.py +25 -0
- qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
- qilisdk/speqtrum/experiments/experiment_result.py +231 -0
- qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
- qilisdk/speqtrum/speqtrum.py +587 -0
- qilisdk/speqtrum/speqtrum_models.py +467 -0
- qilisdk/utils/__init__.py +0 -14
- qilisdk/utils/openqasm2.py +1 -1
- qilisdk/utils/serialization.py +1 -1
- qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
- qilisdk/utils/visualization/__init__.py +24 -0
- qilisdk/utils/visualization/circuit_renderers.py +781 -0
- qilisdk/utils/visualization/schedule_renderers.py +166 -0
- qilisdk/utils/visualization/style.py +154 -0
- qilisdk/utils/visualization/themes.py +76 -0
- qilisdk/yaml.py +126 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/METADATA +186 -140
- qilisdk-0.1.6.dist-info/RECORD +69 -0
- qilisdk/analog/algorithms.py +0 -111
- qilisdk/analog/analog_backend.py +0 -43
- qilisdk/analog/analog_result.py +0 -114
- qilisdk/analog/quantum_objects.py +0 -596
- qilisdk/digital/digital_algorithm.py +0 -20
- qilisdk/digital/digital_backend.py +0 -90
- qilisdk/digital/digital_result.py +0 -145
- qilisdk/digital/vqe.py +0 -166
- qilisdk/extras/cuda/__init__.py +0 -13
- qilisdk/extras/cuda/cuda_analog_result.py +0 -19
- qilisdk/extras/cuda/cuda_digital_result.py +0 -19
- qilisdk/extras/qaas/__init__.py +0 -13
- qilisdk/extras/qaas/models.py +0 -132
- qilisdk/extras/qaas/qaas_backend.py +0 -255
- qilisdk/extras/qaas/qaas_digital_result.py +0 -20
- qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
- qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
- qilisdk-0.1.4.dist-info/RECORD +0 -51
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/WHEEL +0 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/licenses/LICENCE +0 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
# Copyright 2025 Qilimanjaro Quantum Tech
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import json
|
|
18
|
+
import time
|
|
19
|
+
from base64 import urlsafe_b64encode
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, overload
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
from loguru import logger
|
|
25
|
+
from pydantic import TypeAdapter
|
|
26
|
+
|
|
27
|
+
from qilisdk.functionals import (
|
|
28
|
+
Sampling,
|
|
29
|
+
SamplingResult,
|
|
30
|
+
TimeEvolution,
|
|
31
|
+
TimeEvolutionResult,
|
|
32
|
+
VariationalProgram,
|
|
33
|
+
VariationalProgramResult,
|
|
34
|
+
)
|
|
35
|
+
from qilisdk.functionals.functional_result import FunctionalResult
|
|
36
|
+
from qilisdk.settings import get_settings
|
|
37
|
+
from qilisdk.speqtrum.experiments import (
|
|
38
|
+
RabiExperiment,
|
|
39
|
+
RabiExperimentResult,
|
|
40
|
+
T1Experiment,
|
|
41
|
+
T1ExperimentResult,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from .keyring import delete_credentials, load_credentials, store_credentials
|
|
45
|
+
from .speqtrum_models import (
|
|
46
|
+
Device,
|
|
47
|
+
ExecutePayload,
|
|
48
|
+
ExecuteType,
|
|
49
|
+
JobDetail,
|
|
50
|
+
JobHandle,
|
|
51
|
+
JobId,
|
|
52
|
+
JobInfo,
|
|
53
|
+
JobStatus,
|
|
54
|
+
JobType,
|
|
55
|
+
RabiExperimentPayload,
|
|
56
|
+
SamplingPayload,
|
|
57
|
+
T1ExperimentPayload,
|
|
58
|
+
TimeEvolutionPayload,
|
|
59
|
+
Token,
|
|
60
|
+
TypedJobDetail,
|
|
61
|
+
VariationalProgramPayload,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if TYPE_CHECKING:
|
|
65
|
+
from qilisdk.functionals.functional import Functional, PrimitiveFunctional
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
ResultT = TypeVar("ResultT", bound=FunctionalResult)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SpeQtrum:
|
|
72
|
+
"""Synchronous client for the Qilimanjaro SpeQtrum API."""
|
|
73
|
+
|
|
74
|
+
def __init__(self) -> None:
|
|
75
|
+
logger.debug("Initializing QaaS client")
|
|
76
|
+
credentials = load_credentials()
|
|
77
|
+
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)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def _get_headers(cls) -> dict:
|
|
98
|
+
from qilisdk import __version__ # noqa: PLC0415
|
|
99
|
+
|
|
100
|
+
return {"User-Agent": f"qilisdk/{__version__}"}
|
|
101
|
+
|
|
102
|
+
def _get_authorized_headers(self) -> dict:
|
|
103
|
+
return {**self._get_headers(), "Authorization": f"Bearer {self._token.access_token}"}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def login(
|
|
107
|
+
cls,
|
|
108
|
+
username: str | None = None,
|
|
109
|
+
apikey: str | None = None,
|
|
110
|
+
) -> bool:
|
|
111
|
+
"""Authenticate and cache credentials in the system keyring.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
username: SpeQtrum account user name. If ``None``, the value is read
|
|
115
|
+
from the environment.
|
|
116
|
+
apikey: SpeQtrum API key. If ``None``, the value is read from the
|
|
117
|
+
environment.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
``True`` if authentication succeeds, otherwise ``False``.
|
|
121
|
+
|
|
122
|
+
Note:
|
|
123
|
+
The resulting tokens are stored securely in the OS keyring so that future
|
|
124
|
+
:class:`SpeQtrum` constructions require no explicit credentials.
|
|
125
|
+
"""
|
|
126
|
+
# Use provided parameters or fall back to environment variables via Settings()
|
|
127
|
+
settings = get_settings()
|
|
128
|
+
username = username or settings.speqtrum_username
|
|
129
|
+
apikey = apikey or settings.speqtrum_apikey
|
|
130
|
+
|
|
131
|
+
if not username or not apikey:
|
|
132
|
+
logger.warning("Login called without credentials - aborting")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# Send login request to QaaS
|
|
136
|
+
logger.debug("Attempting login for user '{}'", username)
|
|
137
|
+
try:
|
|
138
|
+
assertion = {
|
|
139
|
+
"username": username,
|
|
140
|
+
"api_key": apikey,
|
|
141
|
+
"audience": settings.speqtrum_audience,
|
|
142
|
+
"iat": int(datetime.now(timezone.utc).timestamp()),
|
|
143
|
+
}
|
|
144
|
+
encoded_assertion = urlsafe_b64encode(json.dumps(assertion, indent=2).encode("utf-8")).decode("utf-8")
|
|
145
|
+
with httpx.Client(timeout=10.0) as client:
|
|
146
|
+
response = client.post(
|
|
147
|
+
settings.speqtrum_api_url + "/authorisation-tokens",
|
|
148
|
+
json={
|
|
149
|
+
"grantType": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
150
|
+
"assertion": encoded_assertion,
|
|
151
|
+
"scope": "user profile",
|
|
152
|
+
},
|
|
153
|
+
headers=cls._get_headers(),
|
|
154
|
+
)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
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")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
store_credentials(username=username, token=token)
|
|
165
|
+
logger.success("Login successful for user '{}'", username)
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def logout(cls) -> None:
|
|
170
|
+
"""Delete cached credentials from the keyring."""
|
|
171
|
+
delete_credentials()
|
|
172
|
+
logger.info("Cached credentials removed - user logged out")
|
|
173
|
+
|
|
174
|
+
def list_devices(self, where: Callable[[Device], bool] | None = None) -> list[Device]:
|
|
175
|
+
"""Return all visible devices, optionally filtered.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
where: A predicate that retains a device when it evaluates to
|
|
179
|
+
``True``. Pass ``None`` to disable filtering.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A list of :class:`~qilisdk.models.Device` objects.
|
|
183
|
+
"""
|
|
184
|
+
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
|
+
|
|
191
|
+
logger.success("{} devices retrieved", len(devices))
|
|
192
|
+
return [d for d in devices if where(d)] if where else devices
|
|
193
|
+
|
|
194
|
+
def list_jobs(self, where: Callable[[JobInfo], bool] | None = None) -> list[JobInfo]:
|
|
195
|
+
"""Return lightweight job summaries.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
where: Optional predicate applied client-side. A
|
|
199
|
+
:class:`~qilisdk.models.JobInfo` remains in the list if the
|
|
200
|
+
predicate returns ``True``. ``None`` disables filtering.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
A list of :class:`~qilisdk.models.JobInfo` objects.
|
|
204
|
+
"""
|
|
205
|
+
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
|
+
|
|
212
|
+
logger.success("{} jobs retrieved", len(jobs))
|
|
213
|
+
return [j for j in jobs if where(j)] if where else jobs
|
|
214
|
+
|
|
215
|
+
@overload
|
|
216
|
+
def get_job(self, job: JobHandle[ResultT]) -> TypedJobDetail[ResultT]: ...
|
|
217
|
+
|
|
218
|
+
@overload
|
|
219
|
+
def get_job(self, job: int) -> JobDetail: ...
|
|
220
|
+
|
|
221
|
+
def get_job(self, job: int | JobHandle[Any]) -> JobDetail | TypedJobDetail[Any]:
|
|
222
|
+
"""Fetch the complete record of *job*.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
job: Either the integer identifier or a previously returned `JobHandle`.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
A :class:`~qilisdk.models.JobDetail` snapshot. When a handle is supplied the
|
|
229
|
+
result is wrapped in :class:`~qilisdk.models.TypedJobDetail` to expose typed accessors.
|
|
230
|
+
"""
|
|
231
|
+
job_id = job.id if isinstance(job, JobHandle) else job
|
|
232
|
+
logger.debug("Retrieving job {} details", job_id)
|
|
233
|
+
with httpx.Client() as client:
|
|
234
|
+
response = client.get(
|
|
235
|
+
f"{self._settings.speqtrum_api_url}/jobs/{job_id}",
|
|
236
|
+
headers=self._get_authorized_headers(),
|
|
237
|
+
params={
|
|
238
|
+
"payload": True,
|
|
239
|
+
"result": True,
|
|
240
|
+
"logs": True,
|
|
241
|
+
"error_logs": True,
|
|
242
|
+
"error": True,
|
|
243
|
+
},
|
|
244
|
+
)
|
|
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)
|
|
251
|
+
|
|
252
|
+
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)
|
|
257
|
+
|
|
258
|
+
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")
|
|
262
|
+
|
|
263
|
+
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")
|
|
267
|
+
|
|
268
|
+
job_detail = TypeAdapter(JobDetail).validate_python(data)
|
|
269
|
+
logger.debug("Job {} details retrieved (status {})", job_id, job_detail.status.value)
|
|
270
|
+
if isinstance(job, JobHandle):
|
|
271
|
+
return job.bind(job_detail)
|
|
272
|
+
return job_detail
|
|
273
|
+
|
|
274
|
+
@overload
|
|
275
|
+
def wait_for_job(
|
|
276
|
+
self,
|
|
277
|
+
job: JobHandle[ResultT],
|
|
278
|
+
*,
|
|
279
|
+
poll_interval: float = 5.0,
|
|
280
|
+
timeout: float | None = None,
|
|
281
|
+
) -> TypedJobDetail[ResultT]: ...
|
|
282
|
+
|
|
283
|
+
@overload
|
|
284
|
+
def wait_for_job(
|
|
285
|
+
self,
|
|
286
|
+
job: int,
|
|
287
|
+
*,
|
|
288
|
+
poll_interval: float = 5.0,
|
|
289
|
+
timeout: float | None = None,
|
|
290
|
+
) -> JobDetail: ...
|
|
291
|
+
|
|
292
|
+
def wait_for_job(
|
|
293
|
+
self,
|
|
294
|
+
job: int | JobHandle[Any],
|
|
295
|
+
*,
|
|
296
|
+
poll_interval: float = 5.0,
|
|
297
|
+
timeout: float | None = None,
|
|
298
|
+
) -> JobDetail | TypedJobDetail[Any]:
|
|
299
|
+
"""Block until the job referenced by *job* reaches a terminal state.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
job: Either the integer job identifier or a previously returned `JobHandle`.
|
|
303
|
+
poll_interval: Seconds between successive polls. Defaults to ``5``.
|
|
304
|
+
timeout: Maximum wait time in seconds. ``None`` waits indefinitely.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Final :class:`~qilisdk.models.JobDetail` snapshot, optionally wrapped with type-safe accessors.
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
TimeoutError: If *timeout* elapses before the job finishes.
|
|
311
|
+
"""
|
|
312
|
+
job_id = job.id if isinstance(job, JobHandle) else job
|
|
313
|
+
logger.info("Waiting for job {} (poll={}s, timeout={}s)…", job_id, poll_interval, timeout)
|
|
314
|
+
start_t = time.monotonic()
|
|
315
|
+
terminal_states = {
|
|
316
|
+
JobStatus.COMPLETED,
|
|
317
|
+
JobStatus.ERROR,
|
|
318
|
+
JobStatus.CANCELLED,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# poll until we hit a terminal state or timeout
|
|
322
|
+
while True:
|
|
323
|
+
current = self.get_job(job_id)
|
|
324
|
+
|
|
325
|
+
if current.status in terminal_states:
|
|
326
|
+
logger.success("Job {} reached terminal state {}", job_id, current.status.value)
|
|
327
|
+
if isinstance(job, JobHandle):
|
|
328
|
+
return job.bind(current)
|
|
329
|
+
return current
|
|
330
|
+
|
|
331
|
+
if timeout is not None and (time.monotonic() - start_t) >= timeout:
|
|
332
|
+
logger.error(
|
|
333
|
+
"Timeout while waiting for job {} after {}s (last status {})",
|
|
334
|
+
job_id,
|
|
335
|
+
timeout,
|
|
336
|
+
current.status.value,
|
|
337
|
+
)
|
|
338
|
+
raise TimeoutError(
|
|
339
|
+
f"Timed out after {timeout}s while waiting for job {job_id} (last status {current.status.value!r})"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
logger.debug("Job {} still {}, sleeping {}s", job_id, current.status.value, poll_interval)
|
|
343
|
+
time.sleep(poll_interval)
|
|
344
|
+
|
|
345
|
+
@overload
|
|
346
|
+
def submit(self, functional: Sampling, device: str, job_name: str | None = None) -> JobHandle[SamplingResult]: ...
|
|
347
|
+
|
|
348
|
+
@overload
|
|
349
|
+
def submit(
|
|
350
|
+
self, functional: TimeEvolution, device: str, job_name: str | None = None
|
|
351
|
+
) -> JobHandle[TimeEvolutionResult]: ...
|
|
352
|
+
|
|
353
|
+
@overload
|
|
354
|
+
def submit(
|
|
355
|
+
self, functional: VariationalProgram[Sampling], device: str, job_name: str | None = None
|
|
356
|
+
) -> JobHandle[VariationalProgramResult[SamplingResult]]: ...
|
|
357
|
+
|
|
358
|
+
@overload
|
|
359
|
+
def submit(
|
|
360
|
+
self, functional: VariationalProgram[TimeEvolution], device: str, job_name: str | None = None
|
|
361
|
+
) -> JobHandle[VariationalProgramResult[TimeEvolutionResult]]: ...
|
|
362
|
+
|
|
363
|
+
@overload
|
|
364
|
+
def submit(
|
|
365
|
+
self, functional: VariationalProgram[PrimitiveFunctional[ResultT]], device: str, job_name: str | None = None
|
|
366
|
+
) -> JobHandle[VariationalProgramResult[ResultT]]: ...
|
|
367
|
+
|
|
368
|
+
@overload
|
|
369
|
+
def submit(
|
|
370
|
+
self, functional: RabiExperiment, device: str, job_name: str | None = None
|
|
371
|
+
) -> JobHandle[RabiExperimentResult]: ...
|
|
372
|
+
|
|
373
|
+
@overload
|
|
374
|
+
def submit(
|
|
375
|
+
self, functional: T1Experiment, device: str, job_name: str | None = None
|
|
376
|
+
) -> JobHandle[T1ExperimentResult]: ...
|
|
377
|
+
|
|
378
|
+
def submit(self, functional: Functional, device: str, job_name: str | None = None) -> JobHandle[FunctionalResult]:
|
|
379
|
+
"""
|
|
380
|
+
Submit a quantum functional for execution on the selected device.
|
|
381
|
+
|
|
382
|
+
The concrete subclass of
|
|
383
|
+
:class:`~qilisdk.functionals.functional.Functional` provided in
|
|
384
|
+
*functional* determines which private ``_execute_*`` routine is
|
|
385
|
+
invoked. Supported types are:
|
|
386
|
+
|
|
387
|
+
* :class:`~qilisdk.functionals.sampling.Sampling`
|
|
388
|
+
* :class:`~qilisdk.functionals.time_evolution.TimeEvolution`
|
|
389
|
+
* :class:`~qilisdk.functionals.variational_program.VariationalProgram`
|
|
390
|
+
* :class:`~qilisdk.speqtrum.experiments.experiment_functional.RabiExperiment`
|
|
391
|
+
* :class:`~qilisdk.speqtrum.experiments.experiment_functional.T1Experiment`
|
|
392
|
+
|
|
393
|
+
A backend device must be selected beforehand with
|
|
394
|
+
:py:meth:`set_device`.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
functional: A fully configured functional instance (e.g.,
|
|
398
|
+
``Sampling`` or ``TimeEvolution``) that defines the quantum
|
|
399
|
+
workload to be executed.
|
|
400
|
+
device: Device code returned by :py:meth:`list_devices`.
|
|
401
|
+
job_name (optional): The name of the job, this can help you identify different jobs easier. Default: None.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
JobHandle: A typed handle carrying the numeric job identifier and result type metadata.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
NotImplementedError: If *functional* is not of a supported type.
|
|
408
|
+
"""
|
|
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
|
|
434
|
+
|
|
435
|
+
def _submit_sampling(
|
|
436
|
+
self, sampling: Sampling, device: str, job_name: str | None = None
|
|
437
|
+
) -> JobHandle[SamplingResult]:
|
|
438
|
+
payload = ExecutePayload(
|
|
439
|
+
type=ExecuteType.SAMPLING,
|
|
440
|
+
sampling_payload=SamplingPayload(sampling=sampling),
|
|
441
|
+
)
|
|
442
|
+
json = {
|
|
443
|
+
"device_code": device,
|
|
444
|
+
"payload": payload.model_dump_json(),
|
|
445
|
+
"job_type": JobType.DIGITAL,
|
|
446
|
+
"meta": {},
|
|
447
|
+
}
|
|
448
|
+
if job_name:
|
|
449
|
+
json["name"] = job_name
|
|
450
|
+
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())
|
|
459
|
+
return JobHandle.sampling(job.id)
|
|
460
|
+
|
|
461
|
+
def _submit_rabi_program(
|
|
462
|
+
self, rabi_experiment: RabiExperiment, device: str, job_name: str | None = None
|
|
463
|
+
) -> JobHandle[RabiExperimentResult]:
|
|
464
|
+
payload = ExecutePayload(
|
|
465
|
+
type=ExecuteType.RABI_EXPERIMENT,
|
|
466
|
+
rabi_experiment_payload=RabiExperimentPayload(rabi_experiment=rabi_experiment),
|
|
467
|
+
)
|
|
468
|
+
json = {
|
|
469
|
+
"device_code": device,
|
|
470
|
+
"payload": payload.model_dump_json(),
|
|
471
|
+
"job_type": JobType.PULSE,
|
|
472
|
+
"meta": {},
|
|
473
|
+
}
|
|
474
|
+
if job_name:
|
|
475
|
+
json["name"] = job_name
|
|
476
|
+
logger.debug("Executing Rabi experiment on device {}", device)
|
|
477
|
+
with httpx.Client() as client:
|
|
478
|
+
response = client.post(
|
|
479
|
+
self._settings.speqtrum_api_url + "/execute",
|
|
480
|
+
headers=self._get_authorized_headers(),
|
|
481
|
+
json=json,
|
|
482
|
+
)
|
|
483
|
+
response.raise_for_status()
|
|
484
|
+
job = JobId(**response.json())
|
|
485
|
+
logger.info("Rabi experiment job submitted: {}", job.id)
|
|
486
|
+
return JobHandle.rabi_experiment(job.id)
|
|
487
|
+
|
|
488
|
+
def _submit_t1_program(
|
|
489
|
+
self, t1_experiment: T1Experiment, device: str, job_name: str | None = None
|
|
490
|
+
) -> JobHandle[T1ExperimentResult]:
|
|
491
|
+
payload = ExecutePayload(
|
|
492
|
+
type=ExecuteType.T1_EXPERIMENT,
|
|
493
|
+
t1_experiment_payload=T1ExperimentPayload(t1_experiment=t1_experiment),
|
|
494
|
+
)
|
|
495
|
+
json = {
|
|
496
|
+
"device_code": device,
|
|
497
|
+
"payload": payload.model_dump_json(),
|
|
498
|
+
"job_type": JobType.PULSE,
|
|
499
|
+
"meta": {},
|
|
500
|
+
}
|
|
501
|
+
if job_name:
|
|
502
|
+
json["name"] = job_name
|
|
503
|
+
logger.debug("Executing T1 experiment on device {}", device)
|
|
504
|
+
with httpx.Client() as client:
|
|
505
|
+
response = client.post(
|
|
506
|
+
self._settings.speqtrum_api_url + "/execute",
|
|
507
|
+
headers=self._get_authorized_headers(),
|
|
508
|
+
json=json,
|
|
509
|
+
)
|
|
510
|
+
response.raise_for_status()
|
|
511
|
+
job = JobId(**response.json())
|
|
512
|
+
logger.info("T1 experiment job submitted: {}", job.id)
|
|
513
|
+
return JobHandle.t1_experiment(job.id)
|
|
514
|
+
|
|
515
|
+
def _submit_time_evolution(
|
|
516
|
+
self, time_evolution: TimeEvolution, device: str, job_name: str | None = None
|
|
517
|
+
) -> JobHandle[TimeEvolutionResult]:
|
|
518
|
+
payload = ExecutePayload(
|
|
519
|
+
type=ExecuteType.TIME_EVOLUTION,
|
|
520
|
+
time_evolution_payload=TimeEvolutionPayload(time_evolution=time_evolution),
|
|
521
|
+
)
|
|
522
|
+
json = {
|
|
523
|
+
"device_code": device,
|
|
524
|
+
"payload": payload.model_dump_json(),
|
|
525
|
+
"job_type": JobType.ANALOG,
|
|
526
|
+
"meta": {},
|
|
527
|
+
}
|
|
528
|
+
if job_name:
|
|
529
|
+
json["name"] = job_name
|
|
530
|
+
logger.debug("Executing time evolution on device {}", device)
|
|
531
|
+
with httpx.Client() as client:
|
|
532
|
+
response = client.post(
|
|
533
|
+
self._settings.speqtrum_api_url + "/execute",
|
|
534
|
+
headers=self._get_authorized_headers(),
|
|
535
|
+
json=json,
|
|
536
|
+
)
|
|
537
|
+
response.raise_for_status()
|
|
538
|
+
job = JobId(**response.json())
|
|
539
|
+
logger.info("Time evolution job submitted: {}", job.id)
|
|
540
|
+
return JobHandle.time_evolution(job.id)
|
|
541
|
+
|
|
542
|
+
@overload
|
|
543
|
+
def _submit_variational_program(
|
|
544
|
+
self, variational_program: VariationalProgram[Sampling], device: str, job_name: str | None = None
|
|
545
|
+
) -> JobHandle[VariationalProgramResult[SamplingResult]]: ...
|
|
546
|
+
|
|
547
|
+
@overload
|
|
548
|
+
def _submit_variational_program(
|
|
549
|
+
self, variational_program: VariationalProgram[TimeEvolution], device: str, job_name: str | None = None
|
|
550
|
+
) -> JobHandle[VariationalProgramResult[TimeEvolutionResult]]: ...
|
|
551
|
+
|
|
552
|
+
@overload
|
|
553
|
+
def _submit_variational_program(
|
|
554
|
+
self, variational_program: VariationalProgram[Any], device: str, job_name: str | None = None
|
|
555
|
+
) -> JobHandle[VariationalProgramResult]: ...
|
|
556
|
+
|
|
557
|
+
def _submit_variational_program(
|
|
558
|
+
self, variational_program: VariationalProgram[Any], device: str, job_name: str | None = None
|
|
559
|
+
) -> JobHandle[VariationalProgramResult]:
|
|
560
|
+
payload = ExecutePayload(
|
|
561
|
+
type=ExecuteType.VARIATIONAL_PROGRAM,
|
|
562
|
+
variational_program_payload=VariationalProgramPayload(variational_program=variational_program),
|
|
563
|
+
)
|
|
564
|
+
json = {
|
|
565
|
+
"device_code": device,
|
|
566
|
+
"payload": payload.model_dump_json(),
|
|
567
|
+
"job_type": JobType.VARIATIONAL,
|
|
568
|
+
"meta": {},
|
|
569
|
+
}
|
|
570
|
+
if job_name:
|
|
571
|
+
json["name"] = job_name
|
|
572
|
+
logger.debug("Executing variational program on device {}", device)
|
|
573
|
+
with httpx.Client() as client:
|
|
574
|
+
response = client.post(
|
|
575
|
+
self._settings.speqtrum_api_url + "/execute",
|
|
576
|
+
headers=self._get_authorized_headers(),
|
|
577
|
+
json=json,
|
|
578
|
+
)
|
|
579
|
+
response.raise_for_status()
|
|
580
|
+
job = JobId(**response.json())
|
|
581
|
+
logger.info("Variational program job submitted: {}", job.id)
|
|
582
|
+
inner = variational_program.functional
|
|
583
|
+
if isinstance(inner, Sampling):
|
|
584
|
+
return JobHandle.variational_program(job.id, result_type=SamplingResult)
|
|
585
|
+
if isinstance(inner, TimeEvolution):
|
|
586
|
+
return JobHandle.variational_program(job.id, result_type=TimeEvolutionResult)
|
|
587
|
+
return JobHandle.variational_program(job.id)
|