qilisdk 0.1.4__py3-none-any.whl → 0.1.5__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 (83) 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 +118 -0
  9. qilisdk/analog/schedule.py +272 -79
  10. qilisdk/backends/__init__.py +45 -0
  11. qilisdk/{digital/digital_algorithm.py → backends/__init__.pyi} +3 -5
  12. qilisdk/backends/backend.py +117 -0
  13. qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
  14. qilisdk/backends/qutip_backend.py +492 -0
  15. qilisdk/common/__init__.py +48 -2
  16. qilisdk/common/algorithm.py +2 -1
  17. qilisdk/{extras/qaas/qaas_settings.py → common/exceptions.py} +12 -6
  18. qilisdk/common/model.py +1019 -1
  19. qilisdk/common/parameterizable.py +75 -0
  20. qilisdk/common/qtensor.py +666 -0
  21. qilisdk/common/result.py +2 -1
  22. qilisdk/common/variables.py +1931 -0
  23. qilisdk/{extras/cuda/cuda_analog_result.py → cost_functions/__init__.py} +3 -4
  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 +203 -160
  29. qilisdk/digital/circuit.py +81 -9
  30. qilisdk/digital/exceptions.py +12 -6
  31. qilisdk/digital/gates.py +228 -85
  32. qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
  33. qilisdk/functionals/functional.py +39 -0
  34. qilisdk/{extras/cuda/cuda_digital_result.py → functionals/functional_result.py} +3 -4
  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/backend.py → optimizers/__init__.py} +2 -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/{extras → speqtrum}/__init__.py +7 -8
  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 +432 -0
  54. qilisdk/speqtrum/speqtrum_models.py +300 -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 +161 -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.5.dist-info}/METADATA +180 -134
  66. qilisdk-0.1.5.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_backend.py +0 -90
  72. qilisdk/digital/digital_result.py +0 -145
  73. qilisdk/digital/vqe.py +0 -166
  74. qilisdk/extras/cuda/__init__.py +0 -13
  75. qilisdk/extras/qaas/__init__.py +0 -13
  76. qilisdk/extras/qaas/models.py +0 -132
  77. qilisdk/extras/qaas/qaas_backend.py +0 -255
  78. qilisdk/extras/qaas/qaas_digital_result.py +0 -20
  79. qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
  80. qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
  81. qilisdk-0.1.4.dist-info/RECORD +0 -51
  82. {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
  83. {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/licenses/LICENCE +0 -0
@@ -0,0 +1,432 @@
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, Callable, cast
22
+
23
+ import httpx
24
+ from loguru import logger
25
+ from pydantic import TypeAdapter
26
+
27
+ from qilisdk.functionals import Sampling, TimeEvolution, VariationalProgram
28
+ from qilisdk.settings import get_settings
29
+ from qilisdk.speqtrum.experiments import ExperimentFunctional, RabiExperiment, T1Experiment
30
+
31
+ from .keyring import delete_credentials, load_credentials, store_credentials
32
+ from .speqtrum_models import (
33
+ Device,
34
+ ExecutePayload,
35
+ ExecuteType,
36
+ JobDetail,
37
+ JobId,
38
+ JobInfo,
39
+ JobStatus,
40
+ JobType,
41
+ RabiExperimentPayload,
42
+ SamplingPayload,
43
+ T1ExperimentPayload,
44
+ TimeEvolutionPayload,
45
+ Token,
46
+ VariationalProgramPayload,
47
+ )
48
+
49
+ if TYPE_CHECKING:
50
+ from qilisdk.functionals.functional import Functional, PrimitiveFunctional
51
+
52
+
53
+ class SpeQtrum:
54
+ """Synchronous client for the Qilimanjaro SpeQtrum API."""
55
+
56
+ def __init__(self) -> None:
57
+ logger.debug("Initializing QaaS client")
58
+ credentials = load_credentials()
59
+ if credentials is None:
60
+ logger.error("No QaaS credentials found. Call `.login()` or set env vars before instantiation.")
61
+ raise RuntimeError("Missing QaaS credentials - invoke SpeQtrum.login() first.")
62
+ self._username, self._token = credentials
63
+ self._handlers: dict[type[Functional], Callable[[Functional, str], int]] = {
64
+ Sampling: lambda f, device: self._submit_sampling(cast("Sampling", f), device),
65
+ TimeEvolution: lambda f, device: self._submit_time_evolution(cast("TimeEvolution", f), device),
66
+ VariationalProgram: lambda f, device: self._submit_variational_program(
67
+ cast("VariationalProgram", f), device
68
+ ),
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
+ }
72
+ self._settings = get_settings()
73
+ logger.success("QaaS client initialised for user '{}'", self._username)
74
+
75
+ @classmethod
76
+ def _get_headers(cls) -> dict:
77
+ from qilisdk import __version__ # noqa: PLC0415
78
+
79
+ return {"User-Agent": f"qilisdk/{__version__}"}
80
+
81
+ def _get_authorized_headers(self) -> dict:
82
+ return {**self._get_headers(), "Authorization": f"Bearer {self._token.access_token}"}
83
+
84
+ @classmethod
85
+ def login(
86
+ cls,
87
+ username: str | None = None,
88
+ apikey: str | None = None,
89
+ ) -> bool:
90
+ """Authenticate and cache credentials in the system keyring.
91
+
92
+ Args:
93
+ username: SpeQtrum account user name. If ``None``, the value is read
94
+ from the environment.
95
+ apikey: SpeQtrum API key. If ``None``, the value is read from the
96
+ environment.
97
+
98
+ Returns:
99
+ ``True`` if authentication succeeds, otherwise ``False``.
100
+
101
+ Note:
102
+ The resulting tokens are stored securely in the OS keyring so that future
103
+ :class:`SpeQtrum` constructions require no explicit credentials.
104
+ """
105
+ # Use provided parameters or fall back to environment variables via Settings()
106
+ settings = get_settings()
107
+ username = username or settings.speqtrum_username
108
+ apikey = apikey or settings.speqtrum_apikey
109
+
110
+ if not username or not apikey:
111
+ logger.warning("Login called without credentials - aborting")
112
+ return False
113
+
114
+ # Send login request to QaaS
115
+ logger.debug("Attempting login for user '{}'", username)
116
+ try:
117
+ assertion = {
118
+ "username": username,
119
+ "api_key": apikey,
120
+ "audience": settings.speqtrum_audience,
121
+ "iat": int(datetime.now(timezone.utc).timestamp()),
122
+ }
123
+ encoded_assertion = urlsafe_b64encode(json.dumps(assertion, indent=2).encode("utf-8")).decode("utf-8")
124
+ with httpx.Client(timeout=10.0) as client:
125
+ response = client.post(
126
+ settings.speqtrum_api_url + "/authorisation-tokens",
127
+ json={
128
+ "grantType": "urn:ietf:params:oauth:grant-type:jwt-bearer",
129
+ "assertion": encoded_assertion,
130
+ "scope": "user profile",
131
+ },
132
+ headers=cls._get_headers(),
133
+ )
134
+ response.raise_for_status()
135
+ token = Token(**response.json())
136
+ except httpx.HTTPStatusError as exc:
137
+ logger.error("Login failed - server returned {} {}", exc.response.status_code, exc.response.reason_phrase)
138
+ return False
139
+ except httpx.RequestError:
140
+ logger.exception("Network error while logging in to QaaS")
141
+ return False
142
+
143
+ store_credentials(username=username, token=token)
144
+ logger.success("Login successful for user '{}'", username)
145
+ return True
146
+
147
+ @classmethod
148
+ def logout(cls) -> None:
149
+ """Delete cached credentials from the keyring."""
150
+ delete_credentials()
151
+ logger.info("Cached credentials removed - user logged out")
152
+
153
+ def list_devices(self, where: Callable[[Device], bool] | None = None) -> list[Device]:
154
+ """Return all visible devices, optionally filtered.
155
+
156
+ Args:
157
+ where: A predicate that retains a device when it evaluates to
158
+ ``True``. Pass ``None`` to disable filtering.
159
+
160
+ Returns:
161
+ A list of :class:`~qilisdk.models.Device` objects.
162
+ """
163
+ logger.debug("Fetching device list from server…")
164
+ with httpx.Client() as client:
165
+ response = client.get(self._settings.speqtrum_api_url + "/devices", headers=self._get_authorized_headers())
166
+ response.raise_for_status()
167
+
168
+ devices = TypeAdapter(list[Device]).validate_python(response.json()["items"])
169
+
170
+ logger.success("{} devices retrieved", len(devices))
171
+ return [d for d in devices if where(d)] if where else devices
172
+
173
+ def list_jobs(self, where: Callable[[JobInfo], bool] | None = None) -> list[JobInfo]:
174
+ """Return lightweight job summaries.
175
+
176
+ Args:
177
+ where: Optional predicate applied client-side. A
178
+ :class:`~qilisdk.models.JobInfo` remains in the list if the
179
+ predicate returns ``True``. ``None`` disables filtering.
180
+
181
+ Returns:
182
+ A list of :class:`~qilisdk.models.JobInfo` objects.
183
+ """
184
+ logger.debug("Fetching job list…")
185
+ with httpx.Client() as client:
186
+ response = client.get(self._settings.speqtrum_api_url + "/jobs", headers=self._get_authorized_headers())
187
+ response.raise_for_status()
188
+
189
+ jobs = TypeAdapter(list[JobInfo]).validate_python(response.json()["items"])
190
+
191
+ logger.success("{} jobs retrieved", len(jobs))
192
+ return [j for j in jobs if where(j)] if where else jobs
193
+
194
+ def get_job_details(self, id: int) -> JobDetail:
195
+ """Fetch the complete record of *id*.
196
+
197
+ Args:
198
+ id: Identifier of the job.
199
+
200
+ Returns:
201
+ A :class:`~qilisdk.models.JobDetail` instance containing payload,
202
+ result, logs and error information.
203
+ """
204
+ logger.debug("Retrieving job {} details", id)
205
+ with httpx.Client() as client:
206
+ response = client.get(
207
+ f"{self._settings.speqtrum_api_url}/jobs/{id}",
208
+ headers=self._get_authorized_headers(),
209
+ params={
210
+ "payload": True,
211
+ "result": True,
212
+ "logs": True,
213
+ "error_logs": True,
214
+ "error": True,
215
+ },
216
+ )
217
+ response.raise_for_status()
218
+ data = response.json()
219
+
220
+ raw_payload = data["payload"]
221
+ if raw_payload is not None:
222
+ data["payload"] = json.loads(raw_payload)
223
+
224
+ raw_result = data.get("result")
225
+ if raw_result is not None:
226
+ decoded_result: bytes = base64.b64decode(raw_result)
227
+ text_result = decoded_result.decode("utf-8")
228
+ data["result"] = json.loads(text_result)
229
+
230
+ raw_error = data.get("error")
231
+ if raw_error is not None:
232
+ decoded_error: bytes = base64.b64decode(raw_error)
233
+ data["error"] = decoded_error.decode("utf-8")
234
+
235
+ raw_logs = data.get("logs")
236
+ if raw_logs is not None:
237
+ decoded_logs: bytes = base64.b64decode(raw_logs)
238
+ data["logs"] = decoded_logs.decode("utf-8")
239
+
240
+ job_detail = TypeAdapter(JobDetail).validate_python(data)
241
+ logger.debug("Job {} details retrieved (status {})", id, job_detail.status.value)
242
+ return job_detail
243
+
244
+ def wait_for_job(
245
+ self,
246
+ id: int,
247
+ *,
248
+ poll_interval: float = 5.0,
249
+ timeout: float | None = None,
250
+ ) -> JobDetail:
251
+ """Block until *id* reaches a terminal state.
252
+
253
+ Args:
254
+ id: Job identifier.
255
+ poll_interval: Seconds between successive polls. Defaults to ``5``.
256
+ timeout: Maximum wait time in seconds. ``None`` waits indefinitely.
257
+
258
+ Returns:
259
+ Final :class:`~qilisdk.models.JobDetail` snapshot.
260
+
261
+ Raises:
262
+ TimeoutError: If *timeout* elapses before the job finishes.
263
+ """
264
+ logger.info("Waiting for job {} (poll={}s, timeout={}s)…", id, poll_interval, timeout)
265
+ start_t = time.monotonic()
266
+ terminal_states = {
267
+ JobStatus.COMPLETED,
268
+ JobStatus.ERROR,
269
+ JobStatus.CANCELLED,
270
+ }
271
+
272
+ # poll until we hit a terminal state or timeout
273
+ while True:
274
+ current = self.get_job_details(id)
275
+
276
+ if current.status in terminal_states:
277
+ logger.success("Job {} reached terminal state {}", id, current.status.value)
278
+ return current
279
+
280
+ if timeout is not None and (time.monotonic() - start_t) >= timeout:
281
+ logger.error(
282
+ "Timeout while waiting for job {} after {}s (last status {})", id, timeout, current.status.value
283
+ )
284
+ raise TimeoutError(
285
+ f"Timed out after {timeout}s while waiting for job {id} (last status {current.status.value!r})"
286
+ )
287
+
288
+ logger.debug("Job {} still {}, sleeping {}s", id, current.status.value, poll_interval)
289
+ time.sleep(poll_interval)
290
+
291
+ def submit(self, functional: PrimitiveFunctional | ExperimentFunctional, device: str) -> int:
292
+ """
293
+ Submit a quantum functional for execution on the selected device.
294
+
295
+ The concrete subclass of
296
+ :class:`~qilisdk.functionals.functional.Functional` provided in
297
+ *functional* determines which private ``_execute_*`` routine is
298
+ invoked. Supported types are:
299
+
300
+ * :class:`~qilisdk.functionals.sampling.Sampling`
301
+ * :class:`~qilisdk.functionals.time_evolution.TimeEvolution`
302
+
303
+ A backend device must be selected beforehand with
304
+ :py:meth:`set_device`.
305
+
306
+ Args:
307
+ functional: A fully configured functional instance (e.g.,
308
+ ``Sampling`` or ``TimeEvolution``) that defines the quantum
309
+ workload to be executed.
310
+ device: Device code returned by :py:meth:`list_devices`.
311
+
312
+ Returns:
313
+ int: The numeric identifier of the created job on SpeQtrum.
314
+
315
+ Raises:
316
+ NotImplementedError: If *functional* is not of a supported type.
317
+ """
318
+ try:
319
+ handler = self._handlers[type(functional)]
320
+ except KeyError as exc:
321
+ logger.error("Unsupported functional type: {}", type(functional).__qualname__)
322
+ raise NotImplementedError(
323
+ f"{type(self).__qualname__} does not support {type(functional).__qualname__}"
324
+ ) from exc
325
+
326
+ logger.info("Submitting {}", type(functional).__qualname__)
327
+ job_id = handler(functional, device)
328
+ logger.success("Submission complete - job {}", job_id)
329
+ return job_id
330
+
331
+ def _submit_sampling(self, sampling: Sampling, device: str) -> int:
332
+ payload = ExecutePayload(
333
+ type=ExecuteType.SAMPLING,
334
+ sampling_payload=SamplingPayload(sampling=sampling),
335
+ )
336
+ json = {"device_code": device, "payload": payload.model_dump_json(), "job_type": JobType.DIGITAL, "meta": {}}
337
+ logger.debug("Executing Sampling on device {}", device)
338
+ with httpx.Client() as client:
339
+ response = client.post(
340
+ self._settings.speqtrum_api_url + "/execute",
341
+ headers=self._get_authorized_headers(),
342
+ json=json,
343
+ )
344
+ response.raise_for_status()
345
+ job = JobId(**response.json())
346
+ return job.id
347
+
348
+ def _submit_rabi_program(self, rabi_experiment: RabiExperiment, device: str) -> int:
349
+ payload = ExecutePayload(
350
+ type=ExecuteType.RABI_EXPERIMENT,
351
+ rabi_experiment_payload=RabiExperimentPayload(rabi_experiment=rabi_experiment),
352
+ )
353
+ json = {"device_code": device, "payload": payload.model_dump_json(), "job_type": JobType.PULSE, "meta": {}}
354
+ logger.debug("Executing Rabi experiment on device {}", device)
355
+ with httpx.Client() as client:
356
+ response = client.post(
357
+ self._settings.speqtrum_api_url + "/execute",
358
+ headers=self._get_authorized_headers(),
359
+ json=json,
360
+ )
361
+ response.raise_for_status()
362
+ job = JobId(**response.json())
363
+ logger.info("Rabi experiment job submitted: {}", job.id)
364
+ return job.id
365
+
366
+ def _submit_t1_program(self, t1_experiment: T1Experiment, device: str) -> int:
367
+ payload = ExecutePayload(
368
+ type=ExecuteType.T1_EXPERIMENT,
369
+ t1_experiment_payload=T1ExperimentPayload(t1_experiment=t1_experiment),
370
+ )
371
+ json = {"device_code": device, "payload": payload.model_dump_json(), "job_type": JobType.PULSE, "meta": {}}
372
+ logger.debug("Executing T1 experiment on device {}", device)
373
+ with httpx.Client() as client:
374
+ response = client.post(
375
+ self._settings.speqtrum_api_url + "/execute",
376
+ headers=self._get_authorized_headers(),
377
+ json=json,
378
+ )
379
+ response.raise_for_status()
380
+ job = JobId(**response.json())
381
+ logger.info("T1 experiment job submitted: {}", job.id)
382
+ return job.id
383
+
384
+ def _submit_time_evolution(self, time_evolution: TimeEvolution, device: str) -> int:
385
+ payload = ExecutePayload(
386
+ type=ExecuteType.TIME_EVOLUTION,
387
+ time_evolution_payload=TimeEvolutionPayload(time_evolution=time_evolution),
388
+ )
389
+ json = {"device_code": device, "payload": payload.model_dump_json(), "job_type": JobType.ANALOG, "meta": {}}
390
+ logger.debug("Executing time evolution on device {}", device)
391
+ with httpx.Client() as client:
392
+ response = client.post(
393
+ self._settings.speqtrum_api_url + "/execute",
394
+ headers=self._get_authorized_headers(),
395
+ json=json,
396
+ )
397
+ response.raise_for_status()
398
+ job = JobId(**response.json())
399
+ logger.info("Time evolution job submitted: {}", job.id)
400
+ return job.id
401
+
402
+ def _submit_variational_program(self, variational_program: VariationalProgram, device: str) -> int:
403
+ """Run a Variational Program on the selected device.
404
+
405
+ Args:
406
+ variational_program (VariationalProgram): Problem definition containing Hamiltonian and ansatz.
407
+ device (str): The SpeQtrum device's code to execute the variational program upon.
408
+
409
+ Returns:
410
+ The numeric identifier of the created job.
411
+ """
412
+ payload = ExecutePayload(
413
+ type=ExecuteType.VARIATIONAL_PROGRAM,
414
+ variational_program_payload=VariationalProgramPayload(
415
+ variational_program=variational_program,
416
+ ),
417
+ )
418
+ json = {
419
+ "device_code": device,
420
+ "payload": payload.model_dump_json(),
421
+ "job_type": JobType.VARIATIONAL,
422
+ "meta": {},
423
+ }
424
+ with httpx.Client() as client:
425
+ response = client.post(
426
+ self._settings.speqtrum_api_url + "/execute",
427
+ headers=self._get_authorized_headers(),
428
+ json=json,
429
+ )
430
+ response.raise_for_status()
431
+ job = JobId(**response.json())
432
+ return job.id