SpeQtrum

The speqtrum package provides an optional, synchronous client for the Qilimanjaro SpeQtrum cloud. Through the SpeQtrum class you can authenticate, inspect devices and jobs, and submit digital, analog, or pulse experiments for remote execution.

Installation

SpeQtrum support is shipped as an optional dependency group. Install it alongside QiliSDK with:

pip install "qilisdk[speqtrum]"

Authentication

The API uses short-lived OAuth tokens that are cached in the system keyring. Call SpeQtrum.login once and the credentials will be reused for subsequent sessions.

from qilisdk.speqtrum import SpeQtrum

# Credentials can be provided explicitly…
logged_in = SpeQtrum.login(username="alice", apikey="MY_SECRET_KEY")

# …or read from the environment (QILISDK_SPEQTRUM_USERNAME / QILISDK_SPEQTRUM_APIKEY)
logged_in = SpeQtrum.login()

if not logged_in:
    raise RuntimeError("Authentication failed")

# Remove cached credentials when they are no longer needed
SpeQtrum.logout()

Client Construction

Once credentials are stored, instantiate SpeQtrum to start issuing requests. Construction fails with RuntimeError if no cached credentials exist.

from qilisdk.speqtrum import SpeQtrum

client = SpeQtrum()

Device Catalogue

Devices are represented by Device models containing the device code, number of qubits, hardware type, and status. Use SpeQtrum.list_devices to enumerate them. An optional where predicate allows client-side filtering.

from qilisdk.speqtrum import SpeQtrum, DeviceStatus

client = SpeQtrum()
for device in client.list_devices(where=lambda d: d.status == DeviceStatus.ONLINE):
    print(f"{device.code}: {device.name} ({device.type}) – {device.nqubits} qubits")

Remote Jobs

SpeQtrum.list_jobs returns lightweight JobInfo records. The where predicate works the same way as with devices.

from qilisdk.speqtrum import SpeQtrum
from qilisdk.speqtrum.speqtrum_models import JobStatus

client = SpeQtrum()
running = client.list_jobs(where=lambda job: job.status == JobStatus.RUNNING)
for job in running:
    print(f"{job.id}: {job.status.value} on {job.device_id}")

To inspect complete job metadata (payload, result, logs, decoded errors) call SpeQtrum.get_job_details. Binary fields are returned as decoded strings or structured ExecuteResult objects.

To obtain the results of a completed job, check the result attribute of the returned JobDetail. The specific result type depends on the job type.

response = client.get_job_details(job_id)

# to get the results of the job depending on the type of job
if response.result:
    if response.result.sampling_result:
        print("Sampling results:", response.result.sampling_result)
    elif response.result.variational_program_result:
        print("Variational Program  results:", response.result.variational_program_result)
    elif response.result.time_evolution_result:
        print("Time Evolution results:", response.result.time_evolution_result)
    elif response.result.rabi_experiment_result:
        print("Rabi Experiment results:", response.result.rabi_experiment_result)
    elif response.result.t1_experiment_result:
        print("T1 Experiment results:", response.result.t1_experiment_result)
if response.logs:
    print("Execution logs:\n", response.logs)

Waiting for Completion

Use SpeQtrum.wait_for_job to poll until a job reaches a terminal state (completed, error, or cancelled). The method returns the final JobDetail snapshot and raises TimeoutError if the optional timeout elapses first.

Functional Submission

SpeQtrum accepts the same primitive functionals used by local backends. The SpeQtrum.submit method inspects the functional type and serializes the correct payload. You must supply a device argument with the device code obtained from list_devices().

from qilisdk.digital import Circuit, H, CNOT
from qilisdk.functionals import Sampling
from qilisdk.speqtrum import SpeQtrum

circuit = Circuit(2)
circuit.add(H(0))
circuit.add(CNOT(0, 1))
sampling = Sampling(circuit=circuit, nshots=1_000)

client = SpeQtrum()
device = client.list_devices()[0].code
job_id = client.submit(sampling, device=device)
print("Submitted sampling job:", job_id)

Warning

Physical QPUs currently do not support analog functionals built on TimeEvolution.TimeEvolution; for now, analog hardware can run only pulse experiments from experiments.

Variational Programs

Hybrid optimization is handled through the same VariationalProgram functional used with local backends. Serialize the fully-configured variational program (ansatz, optimizer, cost function) and submit it as any other functional.

from qilisdk.common.model import Model, ObjectiveSense
from qilisdk.common.variables import BinaryVariable, LEQ
from qilisdk.cost_functions import ModelCostFunction
from qilisdk.digital import CNOT, HardwareEfficientAnsatz, U2
from qilisdk.functionals import Sampling
from qilisdk.functionals.variational_program import VariationalProgram
from qilisdk.optimizers.scipy_optimizer import SciPyOptimizer
from qilisdk.speqtrum import SpeQtrum

# Build a small cost model
vars = [BinaryVariable(f"x{i}") for i in range(3)]
model = Model("toy")
model.set_objective(sum(vars), sense=ObjectiveSense.MAXIMIZE)
model.add_constraint("budget", LEQ(vars[0] + vars[1], 1))

ansatz = HardwareEfficientAnsatz(
    nqubits=3,
    layers=2,
    one_qubit_gate=U2,
    two_qubit_gate=CNOT,
    connectivity="linear",
    structure="grouped",
)
functional = Sampling(circuit=ansatz, nshots=1024)
optimizer = SciPyOptimizer(method="Powell")
vprog = VariationalProgram(functional=functional, optimizer=optimizer, cost_function=ModelCostFunction(model))

client = SpeQtrum()
device = client.list_devices()[0].code
job_id = client.submit(vprog, device=device)

Pulse Experiments

The SpeQtrum client also supports calibration-style experiments defined in qilisdk.speqtrum.experiments. These functional objects mirror the interfaces described in the Functionals chapter and return rich result types.

import numpy as np
from qilisdk.speqtrum import DeviceType, SpeQtrum
from qilisdk.speqtrum.experiments import RabiExperiment, T1Experiment

client = SpeQtrum()
device = client.list_devices(
    where=lambda d: d.type in (DeviceType.QPU_ANALOG, DeviceType.QPU_DIGITAL)
)[0].code

# Rabi experiment: sweep drive durations
rabi = RabiExperiment(qubit=0, drive_duration_values=np.linspace(0, 200, 21))
rabi_job = client.submit(rabi, device=device)
rabi_response = client.wait_for_job(rabi_job, timeout=600)

# T1 relaxation experiment: sweep wait durations
t1 = T1Experiment(qubit=0, wait_duration_values=np.linspace(0, 400, 41))
t1_job = client.submit(t1, device=device)
t1_response = client.wait_for_job(t1_job, timeout=600)

The resulting RabiExperimentResult and T1ExperimentResult objects can be retrieved through rabi_response.result.rabi_experiment_result and t1_response.result.t1_experiment_result respectively.