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.
Files changed (86) hide show
  1. qilisdk/__init__.py +11 -2
  2. qilisdk/__init__.pyi +2 -3
  3. qilisdk/_logging.py +135 -0
  4. qilisdk/_optionals.py +5 -7
  5. qilisdk/analog/__init__.py +3 -18
  6. qilisdk/analog/exceptions.py +2 -4
  7. qilisdk/analog/hamiltonian.py +455 -110
  8. qilisdk/analog/linear_schedule.py +121 -0
  9. qilisdk/analog/schedule.py +275 -79
  10. qilisdk/{extras → backends}/__init__.py +9 -4
  11. qilisdk/{common/model.py → backends/__init__.pyi} +3 -1
  12. qilisdk/backends/backend.py +117 -0
  13. qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
  14. qilisdk/backends/qutip_backend.py +473 -0
  15. qilisdk/core/__init__.py +63 -0
  16. qilisdk/{common → core}/algorithm.py +2 -1
  17. qilisdk/{extras/qaas/qaas_settings.py → core/exceptions.py} +12 -6
  18. qilisdk/core/model.py +1034 -0
  19. qilisdk/core/parameterizable.py +75 -0
  20. qilisdk/core/qtensor.py +666 -0
  21. qilisdk/{common → core}/result.py +2 -1
  22. qilisdk/core/variables.py +1969 -0
  23. qilisdk/cost_functions/__init__.py +18 -0
  24. qilisdk/cost_functions/cost_function.py +77 -0
  25. qilisdk/cost_functions/model_cost_function.py +145 -0
  26. qilisdk/cost_functions/observable_cost_function.py +109 -0
  27. qilisdk/digital/__init__.py +3 -22
  28. qilisdk/digital/ansatz.py +200 -160
  29. qilisdk/digital/circuit.py +81 -9
  30. qilisdk/digital/exceptions.py +12 -6
  31. qilisdk/digital/gates.py +229 -86
  32. qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
  33. qilisdk/functionals/functional.py +39 -0
  34. qilisdk/{common/backend.py → functionals/functional_result.py} +3 -1
  35. qilisdk/functionals/sampling.py +81 -0
  36. qilisdk/functionals/sampling_result.py +92 -0
  37. qilisdk/functionals/time_evolution.py +98 -0
  38. qilisdk/functionals/time_evolution_result.py +84 -0
  39. qilisdk/functionals/variational_program.py +80 -0
  40. qilisdk/functionals/variational_program_result.py +69 -0
  41. qilisdk/logging_config.yaml +16 -0
  42. qilisdk/{common → optimizers}/__init__.py +1 -1
  43. qilisdk/optimizers/optimizer.py +39 -0
  44. qilisdk/{common → optimizers}/optimizer_result.py +3 -12
  45. qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
  46. qilisdk/settings.py +78 -0
  47. qilisdk/speqtrum/__init__.py +41 -0
  48. qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
  49. qilisdk/speqtrum/experiments/__init__.py +25 -0
  50. qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
  51. qilisdk/speqtrum/experiments/experiment_result.py +231 -0
  52. qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
  53. qilisdk/speqtrum/speqtrum.py +587 -0
  54. qilisdk/speqtrum/speqtrum_models.py +467 -0
  55. qilisdk/utils/__init__.py +0 -14
  56. qilisdk/utils/openqasm2.py +1 -1
  57. qilisdk/utils/serialization.py +1 -1
  58. qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
  59. qilisdk/utils/visualization/__init__.py +24 -0
  60. qilisdk/utils/visualization/circuit_renderers.py +781 -0
  61. qilisdk/utils/visualization/schedule_renderers.py +166 -0
  62. qilisdk/utils/visualization/style.py +154 -0
  63. qilisdk/utils/visualization/themes.py +76 -0
  64. qilisdk/yaml.py +126 -0
  65. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/METADATA +186 -140
  66. qilisdk-0.1.6.dist-info/RECORD +69 -0
  67. qilisdk/analog/algorithms.py +0 -111
  68. qilisdk/analog/analog_backend.py +0 -43
  69. qilisdk/analog/analog_result.py +0 -114
  70. qilisdk/analog/quantum_objects.py +0 -596
  71. qilisdk/digital/digital_algorithm.py +0 -20
  72. qilisdk/digital/digital_backend.py +0 -90
  73. qilisdk/digital/digital_result.py +0 -145
  74. qilisdk/digital/vqe.py +0 -166
  75. qilisdk/extras/cuda/__init__.py +0 -13
  76. qilisdk/extras/cuda/cuda_analog_result.py +0 -19
  77. qilisdk/extras/cuda/cuda_digital_result.py +0 -19
  78. qilisdk/extras/qaas/__init__.py +0 -13
  79. qilisdk/extras/qaas/models.py +0 -132
  80. qilisdk/extras/qaas/qaas_backend.py +0 -255
  81. qilisdk/extras/qaas/qaas_digital_result.py +0 -20
  82. qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
  83. qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
  84. qilisdk-0.1.4.dist-info/RECORD +0 -51
  85. {qilisdk-0.1.4.dist-info → qilisdk-0.1.6.dist-info}/WHEEL +0 -0
  86. {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)