# Copyright 2025 Qilimanjaro Quantum Tech
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import base64
import binascii
import json
import time
from base64 import urlsafe_b64encode
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Callable, Generator, TypeAlias, TypeVar, cast, overload
import httpx
from loguru import logger
from pydantic import TypeAdapter, ValidationError
from qilisdk.experiments import ExperimentFunctional
from qilisdk.functionals import (
AnalogEvolution,
DigitalPropagation,
FunctionalResult,
QuantumReservoir,
VariationalProgram,
VariationalProgramResult,
)
from qilisdk.settings import get_settings
from .keyring import delete_credentials, load_credentials, store_credentials
from .speqtrum_models import (
AnalogEvolutionPayload,
Device,
DigitalPropagationPayload,
ExecutePayload,
ExecuteType,
ExperimentPayload,
JobDetail,
JobHandle,
JobId,
JobInfo,
JobStatus,
JobType,
QuantumReservoirPayload,
Token,
TypedJobDetail,
VariationalProgramPayload,
)
if TYPE_CHECKING:
from qilisdk.experiments.experiment_result import ExperimentResult
from qilisdk.functionals.functional import Functional, PrimitiveFunctional
from qilisdk.readout import E, Readout, S, T
[documents]
TFunctionalResult = TypeVar("TFunctionalResult", bound=FunctionalResult)
[documents]
JSONValue: TypeAlias = dict[str, "JSONValue"] | list["JSONValue"] | str | int | float | bool | None
_SKIP_ENSURE_OK_EXTENSION = "qilisdk.skip_ensure_ok"
_CONTEXT_EXTENSION = "qilisdk.request_context"
_EXECUTE_URL = "/execute"
[documents]
class SpeQtrumAPIError(httpx.HTTPStatusError):
"""Raised when the SpeQtrum API responds with a non-success HTTP status.
Wraps :class:`httpx.HTTPStatusError` with a human-readable error message
extracted from the response body when possible.
"""
def __init__(self, message: str, *, request: httpx.Request, response: httpx.Response) -> None:
"""Initialise the error.
Args:
message (str): Human-readable description of the failure.
request (httpx.Request): The outgoing HTTP request that triggered
the error.
response (httpx.Response): The HTTP response carrying the error
status.
"""
super().__init__(message, request=request, response=response)
def _safe_json_loads(value: str, *, context: str) -> JSONValue | None:
"""Attempt to parse *value* as JSON, returning ``None`` on failure.
Args:
value (str): Raw JSON string.
context (str): Label used in warning messages on parse failure.
Returns:
JSONValue | None: Parsed JSON value, or ``None`` if decoding fails.
"""
try:
result = json.loads(value)
return cast("JSONValue", result)
except json.JSONDecodeError as exc:
logger.warning("[SpeQtrum] Failed to decode JSON for {}: {}", context, exc)
return None
def _safe_b64_decode(value: str, *, context: str) -> str | None:
"""Base64-decode *value* and return the UTF-8 string, or ``None`` on failure.
Args:
value (str): Base64-encoded string.
context (str): Label used in warning messages on decode failure.
Returns:
str | None: Decoded UTF-8 string, or ``None`` if any decoding step fails.
"""
try:
decoded_bytes = base64.b64decode(value)
except (binascii.Error, ValueError) as exc:
logger.warning("[SpeQtrum] Failed to base64 decode {}: {}", context, exc)
return None
try:
return decoded_bytes.decode("utf-8")
except UnicodeDecodeError as exc:
logger.warning("[SpeQtrum] Failed to UTF-8 decode {}: {}", context, exc)
return None
def _safe_b64_json(value: str, *, context: str) -> JSONValue | None:
"""Base64-decode *value*, then JSON-parse the result.
Args:
value (str): Base64-encoded JSON string.
context (str): Label used in warning messages on failure.
Returns:
JSONValue | None: Parsed JSON value, or ``None`` if any step fails.
"""
decoded_text = _safe_b64_decode(value, context=context)
if decoded_text is None:
return None
return _safe_json_loads(decoded_text, context=context)
def _request_extensions(*, context: str | None = None, skip_ensure_ok: bool = False) -> dict[str, Any] | None:
"""Build an ``httpx`` request extensions dictionary.
Args:
context (str | None): Human-readable label attached to the request for
error reporting. ``None`` omits the key.
skip_ensure_ok (bool): When ``True``, the ``_ensure_ok`` event hook
will not raise on non-success status codes.
Returns:
dict[str, Any] | None: Extensions mapping, or ``None`` when no flags
are set.
"""
extensions: dict[str, Any] = {}
if context:
extensions[_CONTEXT_EXTENSION] = context
if skip_ensure_ok:
extensions[_SKIP_ENSURE_OK_EXTENSION] = True
return extensions or None
def _response_context(response: httpx.Response) -> str:
"""Extract a human-readable context label from a response's originating request.
Args:
response (httpx.Response): The HTTP response to inspect.
Returns:
str: The context string stored in the request extensions, or a
fallback ``"METHOD URL"`` string.
"""
request = response.request
if request is None:
return "SpeQtrum API call"
context = request.extensions.get(_CONTEXT_EXTENSION)
if isinstance(context, str) and context.strip():
return context
return f"{request.method} {request.url}"
def _stringify_payload(payload: JSONValue | None) -> str | None:
"""Convert a parsed JSON error payload into a one-line human-readable string.
Looks for common error-message keys (``message``, ``detail``, ``error``,
etc.) and falls back to ``json.dumps`` when none are found.
Args:
payload (JSONValue | None): Parsed JSON body of the error response.
Returns:
str | None: A concise error description, or ``None`` when *payload*
is ``None``.
"""
if payload is None:
return None
if isinstance(payload, dict):
for key in ("message", "detail", "error", "error_description", "title"):
value = payload.get(key)
if isinstance(value, str) and value.strip():
code = payload.get("code") or payload.get("error_code") or payload.get("errorCode")
code_suffix = f" (code={code})" if isinstance(code, (str, int)) else ""
return value.strip() + code_suffix
try:
return json.dumps(payload, sort_keys=True)
except (TypeError, ValueError):
return str(payload)
if isinstance(payload, list):
preview = ", ".join(str(item) for item in payload[:3])
return preview or str(payload)
if isinstance(payload, (str, int, float, bool)):
return str(payload)
return None
def _summarize_error_payload(response: httpx.Response) -> str:
"""Produce a single-line summary of the error body carried by *response*.
Args:
response (httpx.Response): The non-success HTTP response.
Returns:
str: A best-effort textual summary of the error payload.
"""
context = _response_context(response)
try:
body_text = response.text or ""
except httpx.ResponseNotRead:
try:
response.read() # ensure body is buffered so we can reuse it later
body_text = response.text or ""
except Exception as exc: # ruff: ignore[blind-except]
logger.debug("[SpeQtrum] Failed to read response body for {}: {}", context, exc)
body_text = ""
payload = _safe_json_loads(body_text, context=f"{context} error body") if body_text else None
detail = _stringify_payload(payload)
if detail:
return detail
if body_text.strip():
return body_text.strip()
return "no response body"
def _ensure_ok(response: httpx.Response) -> None:
"""``httpx`` event hook that raises :class:`SpeQtrumAPIError` on non-success status codes.
Skips the check when the request carries the
``_SKIP_ENSURE_OK_EXTENSION`` flag or when the status is
``401 Unauthorized`` (handled separately by token refresh logic).
Args:
response (httpx.Response): The HTTP response to validate.
Raises:
SpeQtrumAPIError: If the response status indicates an error.
"""
request = response.request
if request is not None and request.extensions.get(_SKIP_ENSURE_OK_EXTENSION):
return
if response.status_code == httpx.codes.UNAUTHORIZED:
return
try:
response.raise_for_status()
except httpx.HTTPStatusError:
context = _response_context(response)
detail = _summarize_error_payload(response)
logger.error(
"[SpeQtrum] {} failed with status {} {}: {}",
context,
response.status_code,
response.reason_phrase,
detail,
)
message = f"{context} failed with status {response.status_code}: {detail}"
raise SpeQtrumAPIError(message, request=response.request, response=response) from None
class _BearerAuth(httpx.Auth):
"""Bearer-token authentication handler with automatic token refresh.
Implements the ``httpx`` auth-flow protocol. On a ``401 Unauthorized``
response the handler transparently refreshes the access token using the
stored refresh token and retries the original request.
"""
requires_response_body = True
def __init__(self, client: SpeQtrum) -> None:
"""Initialise the auth handler.
Args:
client (SpeQtrum): The parent ``SpeQtrum`` client whose token
will be read and updated.
"""
self._client = client
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
"""Yield authenticated requests, refreshing the token on ``401``.
Args:
request (httpx.Request): The outgoing request to authenticate.
Yields:
httpx.Request: Requests decorated with an ``Authorization`` header.
Raises:
RuntimeError: if Speqtrum token refresh fails.
HTTPStatusError: if the refresh token fails.
"""
request.headers["Authorization"] = f"Bearer {self._client.token.access_token}"
response = yield request
if response.status_code == httpx.codes.UNAUTHORIZED:
settings = get_settings()
refresh_request = httpx.Request(
"POST",
settings.speqtrum_api_url + "/authorisation-tokens/refresh",
headers={"Authorization": f"Bearer {self._client.token.refresh_token}"},
extensions=_request_extensions(context="Refreshing SpeQtrum token", skip_ensure_ok=True),
)
refresh_response = yield refresh_request
try:
refresh_response.raise_for_status()
except httpx.HTTPStatusError as exc:
logger.error(
"[SpeQtrum] Token refresh failed with status {} {}",
exc.response.status_code,
exc.response.reason_phrase,
)
raise
try:
payload = refresh_response.json()
except json.JSONDecodeError as exc:
logger.error("[SpeQtrum] Token refresh returned invalid JSON: {}", exc)
raise RuntimeError("SpeQtrum token refresh failed: invalid JSON payload") from exc
if not isinstance(payload, dict):
logger.error("[SpeQtrum] Token refresh returned non-object payload: {}", type(payload).__name__)
raise RuntimeError("SpeQtrum token refresh failed: malformed token payload")
payload.pop("refreshToken", None)
try:
token = Token(**payload, refreshToken=self._client.token.refresh_token)
except (TypeError, ValidationError) as exc:
logger.error("[SpeQtrum] Token refresh returned malformed payload: {}", exc)
raise RuntimeError("SpeQtrum token refresh failed: malformed token payload") from exc
self._client.token = token
store_credentials(self._client.username, self._client.token)
request.headers["Authorization"] = f"Bearer {self._client.token.access_token}"
yield request
[documents]
class SpeQtrum:
"""Synchronous client for the Qilimanjaro SpeQtrum API.
Provides methods for authentication, device discovery, job submission,
and result retrieval. Credentials are loaded from the system keyring;
call :meth:`login` first if no credentials have been cached.
Supported functional types for submission include
:class:`~qilisdk.functionals.digital_propagation.DigitalPropagation`,
:class:`~qilisdk.functionals.analog_evolution.AnalogEvolution`,
:class:`~qilisdk.functionals.variational_program.VariationalProgram`,
:class:`~qilisdk.functionals.quantum_reservoir.QuantumReservoir`, and
various experiment types.
"""
def __init__(self) -> None:
"""Initialise the ``SpeQtrum`` client using cached keyring credentials.
Raises:
RuntimeError: If no credentials are found in the keyring. Call
:meth:`login` before instantiation.
"""
logger.debug("[SpeQtrum] Initializing SpeQtrum client")
credentials = load_credentials()
if credentials is None:
logger.error("[SpeQtrum] No credentials found. Call `SpeQtrum.login()` before instantiation.")
raise RuntimeError("Missing credentials - invoke SpeQtrum.login() first.")
self.username, self.token = credentials
logger.info("[SpeQtrum] SpeQtrum client initialised for user '{}'", self.username)
@classmethod
def _get_headers(cls) -> dict:
"""Return default HTTP headers including the SDK ``User-Agent``.
Returns:
dict: Headers dictionary with at least the ``User-Agent`` key.
"""
from qilisdk import __version__ # ruff: ignore[import-outside-top-level]
return {"User-Agent": f"qilisdk/{__version__}"}
def _create_client(self) -> httpx.Client:
"""Return a freshly configured HTTP client for SpeQtrum interactions."""
settings = get_settings()
return httpx.Client(
base_url=settings.speqtrum_api_url,
headers=self._get_headers(),
auth=_BearerAuth(self),
event_hooks={"response": [_ensure_ok]},
)
@classmethod
[documents]
def login(
cls,
username: str | None = None,
apikey: str | None = None,
) -> bool:
"""Authenticate and cache credentials in the system keyring.
Args:
username: SpeQtrum account user name. If ``None``, the value is read
from the environment.
apikey: SpeQtrum API key. If ``None``, the value is read from the
environment.
Returns:
``True`` if authentication succeeds, otherwise ``False``.
Note:
The resulting tokens are stored securely in the OS keyring so that future
:class:`SpeQtrum` constructions require no explicit credentials.
"""
# Use provided parameters or fall back to environment variables via Settings()
settings = get_settings()
username = username or settings.speqtrum_username
apikey = apikey or (
settings.speqtrum_apikey.get_secret_value() if settings.speqtrum_apikey is not None else None
)
if not username or not apikey:
logger.error("[SpeQtrum] Login called without credentials.")
return False
# Send login request to QaaS
logger.debug("[SpeQtrum] Attempting login for user '{}'", username)
assertion = {
"username": username,
"api_key": apikey,
"audience": settings.speqtrum_audience,
"iat": int(datetime.now(timezone.utc).timestamp()),
}
encoded_assertion = urlsafe_b64encode(json.dumps(assertion, indent=2).encode("utf-8")).decode("utf-8")
try:
with httpx.Client(
base_url=settings.speqtrum_api_url,
headers=cls._get_headers(),
timeout=10.0,
event_hooks={"response": [_ensure_ok]},
) as client:
response = client.post(
"/authorisation-tokens",
json={
"grantType": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": encoded_assertion,
"scope": "user profile",
},
extensions=_request_extensions(context="Authenticating user"),
)
response.raise_for_status()
token = Token(**response.json())
except httpx.HTTPError:
return False
store_credentials(username=username, token=token)
logger.info("[SpeQtrum] Login successful for user '{}'", username)
return True
@classmethod
[documents]
def logout(cls) -> None:
"""Delete cached credentials from the keyring."""
delete_credentials()
logger.info("[SpeQtrum] Cached credentials removed - user logged out")
[documents]
def list_devices(self, where: Callable[[Device], bool] | None = None) -> list[Device]:
"""Return all visible devices, optionally filtered.
Args:
where: A predicate that retains a device when it evaluates to
``True``. Pass ``None`` to disable filtering.
Returns:
A list of :class:`~qilisdk.models.Device` objects.
"""
logger.debug("[SpeQtrum] Fetching device list from server…")
with self._create_client() as client:
response = client.get("/devices", extensions=_request_extensions(context="Fetching device list"))
devices = TypeAdapter(list[Device]).validate_python(response.json()["items"])
logger.info("[SpeQtrum] {} devices retrieved", len(devices))
return [d for d in devices if where(d)] if where else devices
[documents]
def list_jobs(self, where: Callable[[JobInfo], bool] | None = None) -> list[JobInfo]:
"""Return lightweight job summaries.
Args:
where: Optional predicate applied client-side. A
:class:`~qilisdk.models.JobInfo` remains in the list if the
predicate returns ``True``. ``None`` disables filtering.
Returns:
A list of :class:`~qilisdk.models.JobInfo` objects.
"""
logger.debug("[SpeQtrum] Fetching job list…")
with self._create_client() as client:
response = client.get("/jobs", extensions=_request_extensions(context="Fetching job list"))
jobs = TypeAdapter(list[JobInfo]).validate_python(response.json()["items"])
logger.info("[SpeQtrum] {} jobs retrieved", len(jobs))
return [j for j in jobs if where(j)] if where else jobs
@overload
[documents]
def get_job(self, job: JobHandle[TFunctionalResult]) -> TypedJobDetail[TFunctionalResult]: ...
@overload
def get_job(self, job: int) -> JobDetail: ...
def get_job(self, job: int | JobHandle[Any]) -> JobDetail | TypedJobDetail[Any]:
"""Fetch the complete record of *job*.
Args:
job: Either the integer identifier or a previously returned `JobHandle`.
Returns:
A :class:`~qilisdk.models.JobDetail` snapshot. When a handle is supplied the
result is wrapped in :class:`~qilisdk.models.TypedJobDetail` to expose typed accessors.
"""
job_id = job.id if isinstance(job, JobHandle) else job
logger.debug("[SpeQtrum] Retrieving job {} details", job_id)
with self._create_client() as client:
response = client.get(
f"/jobs/{job_id}",
params={
"payload": True,
"result": True,
"logs": True,
"error_logs": True,
"error": True,
},
extensions=_request_extensions(context=f"Fetching job {job_id}"),
)
data = response.json()
raw_payload = data.get("payload")
if isinstance(raw_payload, str):
data["payload"] = _safe_json_loads(raw_payload, context=f"job {job_id} payload")
raw_result = data.get("result")
if isinstance(raw_result, str):
data["result"] = _safe_b64_json(raw_result, context=f"job {job_id} result")
raw_error = data.get("error")
if isinstance(raw_error, str):
data["error"] = _safe_b64_decode(raw_error, context=f"job {job_id} error")
raw_logs = data.get("logs")
if isinstance(raw_logs, str):
data["logs"] = _safe_b64_decode(raw_logs, context=f"job {job_id} logs")
raw_error_logs = data.get("error_logs")
if isinstance(raw_error_logs, str):
data["error_logs"] = _safe_b64_decode(raw_error_logs, context=f"job {job_id} error logs")
job_detail = TypeAdapter(JobDetail).validate_python(data)
logger.debug("[SpeQtrum] Job {} details retrieved (status {})", job_id, job_detail.status.value)
if isinstance(job, JobHandle):
return job.bind(job_detail)
return job_detail
@overload
[documents]
def wait_for_job(
self,
job: JobHandle[TFunctionalResult],
*,
poll_interval: float = 5.0,
timeout: float | None = None,
) -> TypedJobDetail[TFunctionalResult]: ...
@overload
def wait_for_job(
self,
job: int,
*,
poll_interval: float = 5.0,
timeout: float | None = None,
) -> JobDetail: ...
def wait_for_job(
self,
job: int | JobHandle[Any],
*,
poll_interval: float = 5.0,
timeout: float | None = None,
) -> JobDetail | TypedJobDetail[Any]:
"""Block until the job referenced by *job* reaches a terminal state.
Args:
job: Either the integer job identifier or a previously returned `JobHandle`.
poll_interval: Seconds between successive polls. Defaults to ``5``.
timeout: Maximum wait time in seconds. ``None`` waits indefinitely.
Returns:
Final :class:`~qilisdk.models.JobDetail` snapshot, optionally wrapped with type-safe accessors.
Raises:
TimeoutError: If *timeout* elapses before the job finishes.
"""
job_id = job.id if isinstance(job, JobHandle) else job
logger.info("[SpeQtrum] Waiting for job {} (poll={}s, timeout={}s)…", job_id, poll_interval, timeout)
start_t = time.monotonic()
terminal_states = {
JobStatus.COMPLETED,
JobStatus.ERROR,
JobStatus.CANCELLED,
}
# poll until we hit a terminal state or timeout
while True:
current = self.get_job(job_id)
if current.status in terminal_states:
logger.info("[SpeQtrum] Job {} reached terminal state {}", job_id, current.status.value)
if isinstance(job, JobHandle):
return job.bind(current)
return current
if timeout is not None and (time.monotonic() - start_t) >= timeout:
logger.error(
"[SpeQtrum] Timeout while waiting for job {} after {}s (last status {})",
job_id,
timeout,
current.status.value,
)
raise TimeoutError(
f"Timed out after {timeout}s while waiting for job {job_id} (last status {current.status.value!r})"
)
logger.debug("[SpeQtrum] Job {} still {}, sleeping {}s", job_id, current.status.value, poll_interval)
time.sleep(poll_interval)
@overload
[documents]
def submit(
self,
functional: PrimitiveFunctional,
device: str,
readout: Readout[S, E, T],
job_name: str | None = None,
) -> JobHandle[FunctionalResult[S, E, T]]: ...
@overload
def submit(
self,
functional: VariationalProgram,
device: str,
readout: Readout[S, E, T],
job_name: str | None = None,
) -> JobHandle[VariationalProgramResult[FunctionalResult[S, E, T]]]: ...
@overload
def submit(
self, functional: ExperimentFunctional, device: str, job_name: str | None = None
) -> JobHandle[ExperimentResult]: ...
def submit(
self,
functional: Functional,
device: str,
readout: Readout[S, E, T] | None = None, # type: ignore[type-arg]
job_name: str | None = None,
) -> JobHandle:
"""
Submit a quantum functional for execution on the selected device.
Supported types:
* :class:`~qilisdk.functionals.digital_propagation.DigitalPropagation`
* :class:`~qilisdk.functionals.analog_evolution.AnalogEvolution`
* :class:`~qilisdk.functionals.variational_program.VariationalProgram`
* :class:`~qilisdk.experiments.experiment_functional.ExperimentFunctional`
(e.g. those provided by the ``qili-experiments`` plugin library)
Args:
functional: A fully configured functional instance that defines the quantum workload.
device: Device code returned by :py:meth:`list_devices`.
readout: Readout method(s) specifying how results should be measured.
Required for ``DigitalPropagation``, ``AnalogEvolution``, and ``VariationalProgram``.
job_name (optional): The name of the job. Default: None.
Returns:
JobHandle: A typed handle carrying the numeric job identifier and result type metadata.
Raises:
NotImplementedError: If *functional* is not of a supported type.
ValueError: If *readout* is required but not provided, or contains invalid methods.
"""
# Experiments (no readout) are dispatched generically via the plugin registry.
if isinstance(functional, ExperimentFunctional):
return self._submit_experiment(functional, device, job_name)
# Functionals with readout
if readout is None:
raise ValueError("Readout can't be none when submitting a functional")
if isinstance(functional, VariationalProgram):
return self._submit_variational_program(functional, device, readout, job_name)
if isinstance(functional, DigitalPropagation):
return self._submit_digital_propagation(functional, device, readout, job_name)
if isinstance(functional, AnalogEvolution):
return self._submit_analog_evolution(functional, device, readout, job_name)
if isinstance(functional, QuantumReservoir):
return self._submit_quantum_reservoir_functional(functional, device, readout, job_name)
logger.error("[SpeQtrum] Unsupported functional type: {}", type(functional).__qualname__)
raise NotImplementedError(f"{type(self).__qualname__} does not support {type(functional).__qualname__}")
def _submit_digital_propagation(
self, functional: DigitalPropagation, device: str, readout: Readout[S, E, T], job_name: str | None = None
) -> JobHandle[FunctionalResult]:
"""Submit a ``DigitalPropagation`` functional to the SpeQtrum API.
Args:
functional (DigitalPropagation): The digital propagation to execute.
device (str): Target device code.
readout (Readout[S, E, T]): Readout methods for measurement.
job_name (str | None): Optional human-readable job name.
Returns:
JobHandle[FunctionalResult]: A handle for tracking the submitted job.
"""
payload = ExecutePayload(
type=ExecuteType.DIGITAL_PROPAGATION,
digital_propagation_payload=DigitalPropagationPayload(digital_propagation=functional, readout=readout),
)
json = {
"device_code": device,
"payload": payload.model_dump_json(),
"job_type": JobType.DIGITAL,
"meta": {},
}
if job_name:
json["name"] = job_name
logger.debug("[SpeQtrum] Executing DigitalPropagation on device {}", device)
with self._create_client() as client:
response = client.post(
_EXECUTE_URL, json=json, extensions=_request_extensions(context="Executing DigitalPropagation")
)
job = JobId(**response.json())
logger.info("[SpeQtrum] DigitalPropagation job submitted: {}", job.id)
if job.message is not None:
logger.warning("{}", job.message)
return JobHandle.functional(job.id)
def _submit_experiment(
self, experiment: ExperimentFunctional, device: str, job_name: str | None = None
) -> JobHandle[ExperimentResult]:
"""Submit any experiment functional to the SpeQtrum API (generic path).
The concrete experiment type is carried by the serialized payload's YAML
tag, so no per-type wiring is needed here.
Args:
experiment (ExperimentFunctional): The experiment to execute.
device (str): Target device code.
job_name (str | None): Optional human-readable job name.
Returns:
JobHandle[ExperimentResult]: A handle for tracking the submitted job.
"""
experiment_name = type(experiment).__qualname__
payload = ExecutePayload(
type=ExecuteType.EXPERIMENT,
experiment_payload=ExperimentPayload(experiment=experiment),
)
json = {
"device_code": device,
"payload": payload.model_dump_json(),
"job_type": JobType.PULSE,
"meta": {},
}
if job_name:
json["name"] = job_name
logger.debug("[SpeQtrum] Executing {} on device {}", experiment_name, device)
with self._create_client() as client:
response = client.post(
_EXECUTE_URL,
json=json,
extensions=_request_extensions(context=f"Executing {experiment_name}"),
)
job = JobId(**response.json())
logger.info("[SpeQtrum] {} job submitted: {}", experiment_name, job.id)
if job.message is not None:
logger.warning("{}", job.message)
return JobHandle.experiment(job.id, result_type=experiment.result_type)
def _submit_analog_evolution(
self, functional: AnalogEvolution, device: str, readout: Readout[S, E, T], job_name: str | None = None
) -> JobHandle[FunctionalResult]:
"""Submit an ``AnalogEvolution`` functional to the SpeQtrum API.
Args:
functional (AnalogEvolution): The analog evolution to execute.
device (str): Target device code.
readout (Readout[S, E, T]): Readout methods for measurement.
job_name (str | None): Optional human-readable job name.
Returns:
JobHandle[FunctionalResult]: A handle for tracking the submitted job.
"""
payload = ExecutePayload(
type=ExecuteType.ANALOG_EVOLUTION,
analog_evolution_payload=AnalogEvolutionPayload(analog_evolution=functional, readout=readout),
)
json = {
"device_code": device,
"payload": payload.model_dump_json(),
"job_type": JobType.ANALOG,
"meta": {},
}
if job_name:
json["name"] = job_name
logger.debug("[SpeQtrum] Executing AnalogEvolution on device {}", device)
with self._create_client() as client:
response = client.post(
_EXECUTE_URL,
json=json,
extensions=_request_extensions(context="Executing AnalogEvolution"),
)
job = JobId(**response.json())
logger.info("[SpeQtrum] AnalogEvolution job submitted: {}", job.id)
if job.message is not None:
logger.warning("{}", job.message)
return JobHandle.functional(job.id)
def _submit_quantum_reservoir_functional(
self, functional: QuantumReservoir, device: str, readout: Readout[S, E, T], job_name: str | None = None
) -> JobHandle[FunctionalResult]:
"""Submit a ``QuantumReservoir`` functional to the SpeQtrum API.
Args:
functional (QuantumReservoir): The quantum reservoir to execute.
device (str): Target device code.
readout (Readout[S, E, T]): Readout methods for measurement.
job_name (str | None): Optional human-readable job name.
Returns:
JobHandle[FunctionalResult]: A handle for tracking the submitted job.
"""
payload = ExecutePayload(
type=ExecuteType.QUANTUM_RESERVOIR,
quantum_reservoir_payload=QuantumReservoirPayload(quantum_reservoir=functional, readout=readout),
)
json = {
"device_code": device,
"payload": payload.model_dump_json(),
"job_type": JobType.QUANTUM_RESERVOIR,
"meta": {},
}
if job_name:
json["name"] = job_name
logger.debug("[SpeQtrum] Executing QuantumReservoir on device {}", device)
with self._create_client() as client:
response = client.post(
_EXECUTE_URL,
json=json,
extensions=_request_extensions(context="Executing QuantumReservoir"),
)
job = JobId(**response.json())
logger.info("[SpeQtrum] QuantumReservoir job submitted: {}", job.id)
if job.message is not None:
logger.warning("{}", job.message)
return JobHandle.functional(job.id)
def _submit_variational_program(
self,
variational_program: VariationalProgram,
device: str,
readout: Readout[S, E, T],
job_name: str | None = None,
) -> JobHandle[VariationalProgramResult[FunctionalResult[S, E, T]]]:
"""Submit a ``VariationalProgram`` to the SpeQtrum API.
Args:
variational_program (VariationalProgram): The variational program
to execute.
device (str): Target device code.
readout (Readout[S, E, T]): Readout methods for measurement.
job_name (str | None): Optional human-readable job name.
Returns:
JobHandle[VariationalProgramResult]: A handle for tracking the
submitted job.
"""
payload = ExecutePayload(
type=ExecuteType.VARIATIONAL_PROGRAM,
variational_program_payload=VariationalProgramPayload(
variational_program=variational_program, readout=readout
),
)
json = {
"device_code": device,
"payload": payload.model_dump_json(),
"job_type": JobType.VARIATIONAL,
"meta": {},
}
if job_name:
json["name"] = job_name
logger.debug("[SpeQtrum] Executing variational program on device {}", device)
with self._create_client() as client:
response = client.post(
_EXECUTE_URL,
json=json,
extensions=_request_extensions(context="Executing variational program"),
)
job = JobId(**response.json())
logger.info("[SpeQtrum] Variational program job submitted: {}", job.id)
if job.message is not None:
logger.warning("{}", job.message)
return JobHandle.variational_program(job.id)
def __repr__(self) -> str:
"""Return a string representation of the client.
Returns:
str: A string of the form ``SpeQtrum(username=...)``.
"""
return f"{type(self).__name__}(username={self.username})"