qilisdk 0.1.5__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/analog/hamiltonian.py +3 -3
- qilisdk/analog/linear_schedule.py +5 -2
- qilisdk/analog/schedule.py +8 -5
- qilisdk/backends/cuda_backend.py +1 -1
- qilisdk/backends/qutip_backend.py +2 -21
- qilisdk/{common → core}/model.py +7 -7
- qilisdk/{common → core}/qtensor.py +1 -1
- qilisdk/{common → core}/variables.py +42 -4
- qilisdk/cost_functions/cost_function.py +1 -1
- qilisdk/cost_functions/model_cost_function.py +5 -5
- qilisdk/cost_functions/observable_cost_function.py +2 -2
- qilisdk/digital/ansatz.py +0 -3
- qilisdk/digital/circuit.py +2 -2
- qilisdk/digital/gates.py +3 -3
- qilisdk/functionals/functional.py +2 -2
- qilisdk/functionals/functional_result.py +1 -1
- qilisdk/functionals/sampling.py +1 -1
- qilisdk/functionals/time_evolution.py +3 -3
- qilisdk/functionals/time_evolution_result.py +2 -2
- qilisdk/optimizers/optimizer_result.py +1 -1
- qilisdk/speqtrum/__init__.py +2 -0
- qilisdk/speqtrum/speqtrum.py +216 -61
- qilisdk/speqtrum/speqtrum_models.py +167 -0
- qilisdk/utils/visualization/schedule_renderers.py +6 -1
- {qilisdk-0.1.5.dist-info → qilisdk-0.1.6.dist-info}/METADATA +17 -17
- {qilisdk-0.1.5.dist-info → qilisdk-0.1.6.dist-info}/RECORD +33 -33
- /qilisdk/{common → core}/__init__.py +0 -0
- /qilisdk/{common → core}/algorithm.py +0 -0
- /qilisdk/{common → core}/exceptions.py +0 -0
- /qilisdk/{common → core}/parameterizable.py +0 -0
- /qilisdk/{common → core}/result.py +0 -0
- {qilisdk-0.1.5.dist-info → qilisdk-0.1.6.dist-info}/WHEEL +0 -0
- {qilisdk-0.1.5.dist-info → qilisdk-0.1.6.dist-info}/licenses/LICENCE +0 -0
qilisdk/speqtrum/speqtrum.py
CHANGED
|
@@ -18,15 +18,28 @@ import json
|
|
|
18
18
|
import time
|
|
19
19
|
from base64 import urlsafe_b64encode
|
|
20
20
|
from datetime import datetime, timezone
|
|
21
|
-
from typing import TYPE_CHECKING, Callable, cast
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast, overload
|
|
22
22
|
|
|
23
23
|
import httpx
|
|
24
24
|
from loguru import logger
|
|
25
25
|
from pydantic import TypeAdapter
|
|
26
26
|
|
|
27
|
-
from qilisdk.functionals import
|
|
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
|
|
28
36
|
from qilisdk.settings import get_settings
|
|
29
|
-
from qilisdk.speqtrum.experiments import
|
|
37
|
+
from qilisdk.speqtrum.experiments import (
|
|
38
|
+
RabiExperiment,
|
|
39
|
+
RabiExperimentResult,
|
|
40
|
+
T1Experiment,
|
|
41
|
+
T1ExperimentResult,
|
|
42
|
+
)
|
|
30
43
|
|
|
31
44
|
from .keyring import delete_credentials, load_credentials, store_credentials
|
|
32
45
|
from .speqtrum_models import (
|
|
@@ -34,6 +47,7 @@ from .speqtrum_models import (
|
|
|
34
47
|
ExecutePayload,
|
|
35
48
|
ExecuteType,
|
|
36
49
|
JobDetail,
|
|
50
|
+
JobHandle,
|
|
37
51
|
JobId,
|
|
38
52
|
JobInfo,
|
|
39
53
|
JobStatus,
|
|
@@ -43,6 +57,7 @@ from .speqtrum_models import (
|
|
|
43
57
|
T1ExperimentPayload,
|
|
44
58
|
TimeEvolutionPayload,
|
|
45
59
|
Token,
|
|
60
|
+
TypedJobDetail,
|
|
46
61
|
VariationalProgramPayload,
|
|
47
62
|
)
|
|
48
63
|
|
|
@@ -50,6 +65,9 @@ if TYPE_CHECKING:
|
|
|
50
65
|
from qilisdk.functionals.functional import Functional, PrimitiveFunctional
|
|
51
66
|
|
|
52
67
|
|
|
68
|
+
ResultT = TypeVar("ResultT", bound=FunctionalResult)
|
|
69
|
+
|
|
70
|
+
|
|
53
71
|
class SpeQtrum:
|
|
54
72
|
"""Synchronous client for the Qilimanjaro SpeQtrum API."""
|
|
55
73
|
|
|
@@ -60,14 +78,17 @@ class SpeQtrum:
|
|
|
60
78
|
logger.error("No QaaS credentials found. Call `.login()` or set env vars before instantiation.")
|
|
61
79
|
raise RuntimeError("Missing QaaS credentials - invoke SpeQtrum.login() first.")
|
|
62
80
|
self._username, self._token = credentials
|
|
63
|
-
self._handlers: dict[type[Functional], Callable[[Functional, str],
|
|
64
|
-
Sampling: lambda f, device: self._submit_sampling(cast("Sampling", f), device),
|
|
65
|
-
TimeEvolution: lambda f, device: self._submit_time_evolution(
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
68
91
|
),
|
|
69
|
-
RabiExperiment: lambda f, device: self._submit_rabi_program(cast("RabiExperiment", f), device),
|
|
70
|
-
T1Experiment: lambda f, device: self._submit_t1_program(cast("T1Experiment", f), device),
|
|
71
92
|
}
|
|
72
93
|
self._settings = get_settings()
|
|
73
94
|
logger.success("QaaS client initialised for user '{}'", self._username)
|
|
@@ -191,20 +212,27 @@ class SpeQtrum:
|
|
|
191
212
|
logger.success("{} jobs retrieved", len(jobs))
|
|
192
213
|
return [j for j in jobs if where(j)] if where else jobs
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
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*.
|
|
196
223
|
|
|
197
224
|
Args:
|
|
198
|
-
|
|
225
|
+
job: Either the integer identifier or a previously returned `JobHandle`.
|
|
199
226
|
|
|
200
227
|
Returns:
|
|
201
|
-
A :class:`~qilisdk.models.JobDetail`
|
|
202
|
-
result
|
|
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.
|
|
203
230
|
"""
|
|
204
|
-
|
|
231
|
+
job_id = job.id if isinstance(job, JobHandle) else job
|
|
232
|
+
logger.debug("Retrieving job {} details", job_id)
|
|
205
233
|
with httpx.Client() as client:
|
|
206
234
|
response = client.get(
|
|
207
|
-
f"{self._settings.speqtrum_api_url}/jobs/{
|
|
235
|
+
f"{self._settings.speqtrum_api_url}/jobs/{job_id}",
|
|
208
236
|
headers=self._get_authorized_headers(),
|
|
209
237
|
params={
|
|
210
238
|
"payload": True,
|
|
@@ -238,30 +266,51 @@ class SpeQtrum:
|
|
|
238
266
|
data["logs"] = decoded_logs.decode("utf-8")
|
|
239
267
|
|
|
240
268
|
job_detail = TypeAdapter(JobDetail).validate_python(data)
|
|
241
|
-
logger.debug("Job {} details retrieved (status {})",
|
|
269
|
+
logger.debug("Job {} details retrieved (status {})", job_id, job_detail.status.value)
|
|
270
|
+
if isinstance(job, JobHandle):
|
|
271
|
+
return job.bind(job_detail)
|
|
242
272
|
return job_detail
|
|
243
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
|
|
244
284
|
def wait_for_job(
|
|
245
285
|
self,
|
|
246
|
-
|
|
286
|
+
job: int,
|
|
247
287
|
*,
|
|
248
288
|
poll_interval: float = 5.0,
|
|
249
289
|
timeout: float | None = None,
|
|
250
|
-
) -> JobDetail:
|
|
251
|
-
|
|
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.
|
|
252
300
|
|
|
253
301
|
Args:
|
|
254
|
-
|
|
302
|
+
job: Either the integer job identifier or a previously returned `JobHandle`.
|
|
255
303
|
poll_interval: Seconds between successive polls. Defaults to ``5``.
|
|
256
304
|
timeout: Maximum wait time in seconds. ``None`` waits indefinitely.
|
|
257
305
|
|
|
258
306
|
Returns:
|
|
259
|
-
Final :class:`~qilisdk.models.JobDetail` snapshot.
|
|
307
|
+
Final :class:`~qilisdk.models.JobDetail` snapshot, optionally wrapped with type-safe accessors.
|
|
260
308
|
|
|
261
309
|
Raises:
|
|
262
310
|
TimeoutError: If *timeout* elapses before the job finishes.
|
|
263
311
|
"""
|
|
264
|
-
|
|
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)
|
|
265
314
|
start_t = time.monotonic()
|
|
266
315
|
terminal_states = {
|
|
267
316
|
JobStatus.COMPLETED,
|
|
@@ -271,24 +320,62 @@ class SpeQtrum:
|
|
|
271
320
|
|
|
272
321
|
# poll until we hit a terminal state or timeout
|
|
273
322
|
while True:
|
|
274
|
-
current = self.
|
|
323
|
+
current = self.get_job(job_id)
|
|
275
324
|
|
|
276
325
|
if current.status in terminal_states:
|
|
277
|
-
logger.success("Job {} reached terminal state {}",
|
|
326
|
+
logger.success("Job {} reached terminal state {}", job_id, current.status.value)
|
|
327
|
+
if isinstance(job, JobHandle):
|
|
328
|
+
return job.bind(current)
|
|
278
329
|
return current
|
|
279
330
|
|
|
280
331
|
if timeout is not None and (time.monotonic() - start_t) >= timeout:
|
|
281
332
|
logger.error(
|
|
282
|
-
"Timeout while waiting for job {} after {}s (last status {})",
|
|
333
|
+
"Timeout while waiting for job {} after {}s (last status {})",
|
|
334
|
+
job_id,
|
|
335
|
+
timeout,
|
|
336
|
+
current.status.value,
|
|
283
337
|
)
|
|
284
338
|
raise TimeoutError(
|
|
285
|
-
f"Timed out after {timeout}s while waiting for job {
|
|
339
|
+
f"Timed out after {timeout}s while waiting for job {job_id} (last status {current.status.value!r})"
|
|
286
340
|
)
|
|
287
341
|
|
|
288
|
-
logger.debug("Job {} still {}, sleeping {}s",
|
|
342
|
+
logger.debug("Job {} still {}, sleeping {}s", job_id, current.status.value, poll_interval)
|
|
289
343
|
time.sleep(poll_interval)
|
|
290
344
|
|
|
291
|
-
|
|
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]:
|
|
292
379
|
"""
|
|
293
380
|
Submit a quantum functional for execution on the selected device.
|
|
294
381
|
|
|
@@ -299,6 +386,9 @@ class SpeQtrum:
|
|
|
299
386
|
|
|
300
387
|
* :class:`~qilisdk.functionals.sampling.Sampling`
|
|
301
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`
|
|
302
392
|
|
|
303
393
|
A backend device must be selected beforehand with
|
|
304
394
|
:py:meth:`set_device`.
|
|
@@ -308,14 +398,28 @@ class SpeQtrum:
|
|
|
308
398
|
``Sampling`` or ``TimeEvolution``) that defines the quantum
|
|
309
399
|
workload to be executed.
|
|
310
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.
|
|
311
402
|
|
|
312
403
|
Returns:
|
|
313
|
-
|
|
404
|
+
JobHandle: A typed handle carrying the numeric job identifier and result type metadata.
|
|
314
405
|
|
|
315
406
|
Raises:
|
|
316
407
|
NotImplementedError: If *functional* is not of a supported type.
|
|
317
408
|
"""
|
|
318
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
|
+
|
|
319
423
|
handler = self._handlers[type(functional)]
|
|
320
424
|
except KeyError as exc:
|
|
321
425
|
logger.error("Unsupported functional type: {}", type(functional).__qualname__)
|
|
@@ -324,16 +428,25 @@ class SpeQtrum:
|
|
|
324
428
|
) from exc
|
|
325
429
|
|
|
326
430
|
logger.info("Submitting {}", type(functional).__qualname__)
|
|
327
|
-
|
|
328
|
-
logger.success("Submission complete - job {}",
|
|
329
|
-
return
|
|
431
|
+
job_handle = handler(functional, device, job_name)
|
|
432
|
+
logger.success("Submission complete - job {}", job_handle.id)
|
|
433
|
+
return job_handle
|
|
330
434
|
|
|
331
|
-
def _submit_sampling(
|
|
435
|
+
def _submit_sampling(
|
|
436
|
+
self, sampling: Sampling, device: str, job_name: str | None = None
|
|
437
|
+
) -> JobHandle[SamplingResult]:
|
|
332
438
|
payload = ExecutePayload(
|
|
333
439
|
type=ExecuteType.SAMPLING,
|
|
334
440
|
sampling_payload=SamplingPayload(sampling=sampling),
|
|
335
441
|
)
|
|
336
|
-
json = {
|
|
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
|
|
337
450
|
logger.debug("Executing Sampling on device {}", device)
|
|
338
451
|
with httpx.Client() as client:
|
|
339
452
|
response = client.post(
|
|
@@ -343,14 +456,23 @@ class SpeQtrum:
|
|
|
343
456
|
)
|
|
344
457
|
response.raise_for_status()
|
|
345
458
|
job = JobId(**response.json())
|
|
346
|
-
return job.id
|
|
459
|
+
return JobHandle.sampling(job.id)
|
|
347
460
|
|
|
348
|
-
def _submit_rabi_program(
|
|
461
|
+
def _submit_rabi_program(
|
|
462
|
+
self, rabi_experiment: RabiExperiment, device: str, job_name: str | None = None
|
|
463
|
+
) -> JobHandle[RabiExperimentResult]:
|
|
349
464
|
payload = ExecutePayload(
|
|
350
465
|
type=ExecuteType.RABI_EXPERIMENT,
|
|
351
466
|
rabi_experiment_payload=RabiExperimentPayload(rabi_experiment=rabi_experiment),
|
|
352
467
|
)
|
|
353
|
-
json = {
|
|
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
|
|
354
476
|
logger.debug("Executing Rabi experiment on device {}", device)
|
|
355
477
|
with httpx.Client() as client:
|
|
356
478
|
response = client.post(
|
|
@@ -361,14 +483,23 @@ class SpeQtrum:
|
|
|
361
483
|
response.raise_for_status()
|
|
362
484
|
job = JobId(**response.json())
|
|
363
485
|
logger.info("Rabi experiment job submitted: {}", job.id)
|
|
364
|
-
return job.id
|
|
486
|
+
return JobHandle.rabi_experiment(job.id)
|
|
365
487
|
|
|
366
|
-
def _submit_t1_program(
|
|
488
|
+
def _submit_t1_program(
|
|
489
|
+
self, t1_experiment: T1Experiment, device: str, job_name: str | None = None
|
|
490
|
+
) -> JobHandle[T1ExperimentResult]:
|
|
367
491
|
payload = ExecutePayload(
|
|
368
492
|
type=ExecuteType.T1_EXPERIMENT,
|
|
369
493
|
t1_experiment_payload=T1ExperimentPayload(t1_experiment=t1_experiment),
|
|
370
494
|
)
|
|
371
|
-
json = {
|
|
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
|
|
372
503
|
logger.debug("Executing T1 experiment on device {}", device)
|
|
373
504
|
with httpx.Client() as client:
|
|
374
505
|
response = client.post(
|
|
@@ -379,14 +510,23 @@ class SpeQtrum:
|
|
|
379
510
|
response.raise_for_status()
|
|
380
511
|
job = JobId(**response.json())
|
|
381
512
|
logger.info("T1 experiment job submitted: {}", job.id)
|
|
382
|
-
return job.id
|
|
513
|
+
return JobHandle.t1_experiment(job.id)
|
|
383
514
|
|
|
384
|
-
def _submit_time_evolution(
|
|
515
|
+
def _submit_time_evolution(
|
|
516
|
+
self, time_evolution: TimeEvolution, device: str, job_name: str | None = None
|
|
517
|
+
) -> JobHandle[TimeEvolutionResult]:
|
|
385
518
|
payload = ExecutePayload(
|
|
386
519
|
type=ExecuteType.TIME_EVOLUTION,
|
|
387
520
|
time_evolution_payload=TimeEvolutionPayload(time_evolution=time_evolution),
|
|
388
521
|
)
|
|
389
|
-
json = {
|
|
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
|
|
390
530
|
logger.debug("Executing time evolution on device {}", device)
|
|
391
531
|
with httpx.Client() as client:
|
|
392
532
|
response = client.post(
|
|
@@ -397,23 +537,29 @@ class SpeQtrum:
|
|
|
397
537
|
response.raise_for_status()
|
|
398
538
|
job = JobId(**response.json())
|
|
399
539
|
logger.info("Time evolution job submitted: {}", job.id)
|
|
400
|
-
return job.id
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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]:
|
|
412
560
|
payload = ExecutePayload(
|
|
413
561
|
type=ExecuteType.VARIATIONAL_PROGRAM,
|
|
414
|
-
variational_program_payload=VariationalProgramPayload(
|
|
415
|
-
variational_program=variational_program,
|
|
416
|
-
),
|
|
562
|
+
variational_program_payload=VariationalProgramPayload(variational_program=variational_program),
|
|
417
563
|
)
|
|
418
564
|
json = {
|
|
419
565
|
"device_code": device,
|
|
@@ -421,6 +567,9 @@ class SpeQtrum:
|
|
|
421
567
|
"job_type": JobType.VARIATIONAL,
|
|
422
568
|
"meta": {},
|
|
423
569
|
}
|
|
570
|
+
if job_name:
|
|
571
|
+
json["name"] = job_name
|
|
572
|
+
logger.debug("Executing variational program on device {}", device)
|
|
424
573
|
with httpx.Client() as client:
|
|
425
574
|
response = client.post(
|
|
426
575
|
self._settings.speqtrum_api_url + "/execute",
|
|
@@ -429,4 +578,10 @@ class SpeQtrum:
|
|
|
429
578
|
)
|
|
430
579
|
response.raise_for_status()
|
|
431
580
|
job = JobId(**response.json())
|
|
432
|
-
|
|
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)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# ruff: noqa: ANN001, ANN202, PLR6301
|
|
15
15
|
from email.utils import parsedate_to_datetime
|
|
16
16
|
from enum import Enum
|
|
17
|
+
from typing import Any, Callable, Generic, TypeVar, cast, overload
|
|
17
18
|
|
|
18
19
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, field_serializer, field_validator
|
|
19
20
|
|
|
@@ -25,6 +26,7 @@ from qilisdk.functionals import (
|
|
|
25
26
|
VariationalProgram,
|
|
26
27
|
VariationalProgramResult,
|
|
27
28
|
)
|
|
29
|
+
from qilisdk.functionals.functional_result import FunctionalResult
|
|
28
30
|
from qilisdk.speqtrum.experiments import RabiExperiment, RabiExperimentResult, T1Experiment, T1ExperimentResult
|
|
29
31
|
from qilisdk.utils.serialization import deserialize, serialize
|
|
30
32
|
|
|
@@ -226,6 +228,142 @@ class ExecuteResult(SpeQtrumModel):
|
|
|
226
228
|
return v
|
|
227
229
|
|
|
228
230
|
|
|
231
|
+
ResultT_co = TypeVar("ResultT_co", bound=FunctionalResult, covariant=True)
|
|
232
|
+
VariationalInnerResultT = TypeVar("VariationalInnerResultT", bound=FunctionalResult)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
ResultExtractor = Callable[[ExecuteResult], ResultT_co]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# these helpers live outside the models so they can be referenced by default values
|
|
239
|
+
def _require_sampling_result(result: ExecuteResult) -> SamplingResult:
|
|
240
|
+
if result.sampling_result is None:
|
|
241
|
+
raise RuntimeError("SpeQtrum did not return a sampling_result for a sampling execution.")
|
|
242
|
+
return result.sampling_result
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _require_time_evolution_result(result: ExecuteResult) -> TimeEvolutionResult:
|
|
246
|
+
if result.time_evolution_result is None:
|
|
247
|
+
raise RuntimeError("SpeQtrum did not return a time_evolution_result for a time evolution execution.")
|
|
248
|
+
return result.time_evolution_result
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _require_variational_program_result(result: ExecuteResult) -> VariationalProgramResult:
|
|
252
|
+
if result.variational_program_result is None:
|
|
253
|
+
raise RuntimeError("SpeQtrum did not return a variational_program_result for a variational program execution.")
|
|
254
|
+
return result.variational_program_result
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _require_rabi_experiment_result(result: ExecuteResult) -> RabiExperimentResult:
|
|
258
|
+
if result.rabi_experiment_result is None:
|
|
259
|
+
raise RuntimeError("SpeQtrum did not return a rabi_experiment_result for a Rabi experiment execution.")
|
|
260
|
+
return result.rabi_experiment_result
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _require_t1_experiment_result(result: ExecuteResult) -> T1ExperimentResult:
|
|
264
|
+
if result.t1_experiment_result is None:
|
|
265
|
+
raise RuntimeError("SpeQtrum did not return a t1_experiment_result for a T1 experiment execution.")
|
|
266
|
+
return result.t1_experiment_result
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _require_variational_program_result_typed(
|
|
270
|
+
inner_result_type: type[VariationalInnerResultT],
|
|
271
|
+
) -> ResultExtractor[VariationalProgramResult[VariationalInnerResultT]]:
|
|
272
|
+
def _extractor(result: ExecuteResult) -> VariationalProgramResult[VariationalInnerResultT]:
|
|
273
|
+
variational_result = _require_variational_program_result(result)
|
|
274
|
+
optimal_results = variational_result.optimal_execution_results
|
|
275
|
+
if not isinstance(optimal_results, inner_result_type):
|
|
276
|
+
raise RuntimeError(
|
|
277
|
+
"SpeQtrum returned a variational program result whose optimal execution result "
|
|
278
|
+
f"({type(optimal_results).__qualname__}) does not match the expected "
|
|
279
|
+
f"{inner_result_type.__qualname__}."
|
|
280
|
+
)
|
|
281
|
+
return cast("VariationalProgramResult[VariationalInnerResultT]", variational_result)
|
|
282
|
+
|
|
283
|
+
return _extractor
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class JobHandle(SpeQtrumModel, Generic[ResultT_co]):
|
|
287
|
+
"""Strongly typed reference to a submitted SpeQtrum job."""
|
|
288
|
+
|
|
289
|
+
id: int
|
|
290
|
+
execute_type: ExecuteType
|
|
291
|
+
extractor: ResultExtractor[ResultT_co] = Field(repr=False, exclude=True)
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def sampling(cls, job_id: int) -> "JobHandle[SamplingResult]":
|
|
295
|
+
return cls(id=job_id, execute_type=ExecuteType.SAMPLING, extractor=_require_sampling_result) # type: ignore[return-value, arg-type]
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def time_evolution(cls, job_id: int) -> "JobHandle[TimeEvolutionResult]":
|
|
299
|
+
return cls(id=job_id, execute_type=ExecuteType.TIME_EVOLUTION, extractor=_require_time_evolution_result) # type: ignore[return-value, arg-type]
|
|
300
|
+
|
|
301
|
+
@overload
|
|
302
|
+
@classmethod
|
|
303
|
+
def variational_program(cls, job_id: int) -> "JobHandle[VariationalProgramResult]": ...
|
|
304
|
+
|
|
305
|
+
@overload
|
|
306
|
+
@classmethod
|
|
307
|
+
def variational_program(
|
|
308
|
+
cls, job_id: int, *, result_type: type[VariationalInnerResultT]
|
|
309
|
+
) -> "JobHandle[VariationalProgramResult[VariationalInnerResultT]]": ...
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def variational_program(
|
|
313
|
+
cls, job_id: int, *, result_type: type[VariationalInnerResultT] | None = None
|
|
314
|
+
) -> "JobHandle[Any]":
|
|
315
|
+
"""Create a variational-program handle for an existing job identifier.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
job_id: Numeric identifier returned by the SpeQtrum service.
|
|
319
|
+
result_type: Optional functional result type expected within the
|
|
320
|
+
variational program payload. When provided the returned handle
|
|
321
|
+
enforces that the optimiser output matches this type.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
JobHandle: A handle whose ``get_results`` invocation yields a
|
|
325
|
+
``VariationalProgramResult`` preserving the requested inner result
|
|
326
|
+
type when supplied.
|
|
327
|
+
"""
|
|
328
|
+
if result_type is None:
|
|
329
|
+
handle = cls(
|
|
330
|
+
id=job_id,
|
|
331
|
+
execute_type=ExecuteType.VARIATIONAL_PROGRAM,
|
|
332
|
+
extractor=_require_variational_program_result, # type: ignore[arg-type]
|
|
333
|
+
)
|
|
334
|
+
return cast("JobHandle[VariationalProgramResult]", handle)
|
|
335
|
+
|
|
336
|
+
extractor = _require_variational_program_result_typed(result_type)
|
|
337
|
+
handle = cls(id=job_id, execute_type=ExecuteType.VARIATIONAL_PROGRAM, extractor=extractor) # type: ignore[arg-type]
|
|
338
|
+
return cast("JobHandle[VariationalProgramResult[VariationalInnerResultT]]", handle)
|
|
339
|
+
|
|
340
|
+
@classmethod
|
|
341
|
+
def rabi_experiment(cls, job_id: int) -> "JobHandle[RabiExperimentResult]":
|
|
342
|
+
return cls(id=job_id, execute_type=ExecuteType.RABI_EXPERIMENT, extractor=_require_rabi_experiment_result) # type: ignore[return-value, arg-type]
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def t1_experiment(cls, job_id: int) -> "JobHandle[T1ExperimentResult]":
|
|
346
|
+
return cls(id=job_id, execute_type=ExecuteType.T1_EXPERIMENT, extractor=_require_t1_experiment_result) # type: ignore[return-value, arg-type]
|
|
347
|
+
|
|
348
|
+
def bind(self, detail: "JobDetail") -> "TypedJobDetail[ResultT_co]":
|
|
349
|
+
"""Attach this handle's typing information to a concrete job detail.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
detail: Un-typed job detail payload returned by the SpeQtrum API.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
TypedJobDetail: Wrapper exposing ``get_results`` with the typing
|
|
356
|
+
captured when the handle was created.
|
|
357
|
+
"""
|
|
358
|
+
return TypedJobDetail.model_validate(
|
|
359
|
+
{
|
|
360
|
+
**detail.model_dump(),
|
|
361
|
+
"expected_type": self.execute_type,
|
|
362
|
+
"extractor": self.extractor,
|
|
363
|
+
}
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
229
367
|
class JobStatus(str, Enum):
|
|
230
368
|
"Job has not been submitted to the Lab api"
|
|
231
369
|
|
|
@@ -298,3 +436,32 @@ class JobDetail(JobInfo):
|
|
|
298
436
|
logs: str | None = None
|
|
299
437
|
error: str | None = None
|
|
300
438
|
error_logs: str | None = None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class TypedJobDetail(JobDetail, Generic[ResultT_co]):
|
|
442
|
+
"""`JobDetail` subclass that exposes a strongly typed `get_results` method."""
|
|
443
|
+
|
|
444
|
+
expected_type: ExecuteType = Field(repr=False)
|
|
445
|
+
extractor: ResultExtractor[ResultT_co] = Field(repr=False, exclude=True)
|
|
446
|
+
|
|
447
|
+
def get_results(self) -> ResultT_co:
|
|
448
|
+
"""Return the strongly typed execution result.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
ResultT_co: Result payload associated with the completed job,
|
|
452
|
+
respecting the type information carried by the originating
|
|
453
|
+
``JobHandle``.
|
|
454
|
+
|
|
455
|
+
Raises:
|
|
456
|
+
RuntimeError: If SpeQtrum has not populated the result payload or
|
|
457
|
+
the execute type disagrees with the handle.
|
|
458
|
+
"""
|
|
459
|
+
if self.result is None:
|
|
460
|
+
raise RuntimeError("The job completed without a result payload; inspect `error` or `logs` for details.")
|
|
461
|
+
|
|
462
|
+
if self.result.type != self.expected_type:
|
|
463
|
+
raise RuntimeError(
|
|
464
|
+
f"Expected a result of type '{self.expected_type.value}' but received '{self.result.type.value}'."
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return self.extractor(self.result)
|
|
@@ -21,7 +21,7 @@ import matplotlib.pyplot as plt
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from qilisdk.analog.schedule import Schedule
|
|
24
|
-
from qilisdk.
|
|
24
|
+
from qilisdk.core.variables import Number
|
|
25
25
|
|
|
26
26
|
from qilisdk.utils.visualization.style import ScheduleStyle
|
|
27
27
|
|
|
@@ -146,6 +146,11 @@ class MatplotlibScheduleRenderer:
|
|
|
146
146
|
|
|
147
147
|
self.ax.figure.savefig(filename, bbox_inches="tight") # type: ignore[union-attr]
|
|
148
148
|
|
|
149
|
+
def show(self) -> None: # noqa: PLR6301
|
|
150
|
+
"""Show the current figure."""
|
|
151
|
+
|
|
152
|
+
plt.show()
|
|
153
|
+
|
|
149
154
|
@staticmethod
|
|
150
155
|
def _make_axes(dpi: int, style: ScheduleStyle) -> plt.Axes:
|
|
151
156
|
"""
|