Overview

To integrate a quantum device or simulator with qBraid Runtime, you’ll need to create a custom provider class. A provider includes a collection of devices and runtime specifications. Each QuantumProvider should implement a get_devices method to retrieve QuantumDevice objects, which describe the device’s supported operations, the program types accepted as run input, and other dynamic runtime instructions. Finally, devices must implement a submit() method to execute or queue programs, returning a QuantumJob object. This standardization allows users and APIs to uniformly submit jobs and retrieve results, ensuring compatibility across different providers and devices.

Here are the high-level components required to establish a complete runtime implementation:

1

Provider Setup

Implement a QuantumProvider subclass that manages authentication and remote access to the available device(s).

2

Device + Runtime Configuration

Implement a QuantumDevice subclass and its submit() method. Optionally incorporate a custom ConversionScheme to broaden the range of quantum program types that can be accepted as run input.

3

Job Management

Implement a QuantumJob subclass that handles interactions with a running job. Optionally incorporate a QuantumJobResult subclass to systematically process and present data collected from job executions.

QuantumProvider

The provider class is responsible for managing interactions with various quantum services and for converting device metadata into accessible Python objects.

For a simple provider implementation example, see qbraid.runtime.ionq.provider.py ↗

Server Requests

Creating a provider class requires the following REST API endpoints:

  1. GET /devices (or similar) - To retrieve metadata about available quantum devices.
  2. POST /job (or similar) - To submit a quantum job for execution on a specified device.
  3. GET /job (or similar) - To retrieve the status and results of an executed quantum job.

If you already have a Python client for interacting with your REST API server, you can skip this section and go directly to Provider Setup.

For those who need to set up a Python client, the qbraid.runtime.Session, a subclass of requests.Session, is designed to manage secure HTTP connections to custom endpoints. It ensures encrypted and authenticated communication for quantum providers and offers customizations for management of headers and secret keys, configurable retries for 5xx errors, and more.

Below is a minimal example demonstrating how one could set up authenticated requests to the previously mentioned endpoints:

from typing import Any

from qbraid.runtime import Session


class MySession(Session):

    def __init__(self, api_key: str):
        super().__init__(
            base_url="https://api.example.com/fake-endpoint",
            headers={"Content-Type": "application/json"},
            auth_headers={"apiKey": api_key},
        )
        self.api_key = api_key

    def get_device(self, device_id: str) -> dict[str, Any]:
        devices = self.get_devices(device_id=device_id)
        if not devices:
            raise ValueError(f"Device {device_id} not found")
        return devices[0]

    def get_devices(self, **kwargs) -> list[dict[str, Any]]:
        return self.get("/devices", **kwargs).json()

    def create_job(self, data: dict[str, Any]) -> dict[str, Any]:
        return self.post("/jobs", json=data).json()

    def get_job(self, job_id: str) -> dict[str, Any]:
        return self.get(f"/jobs/{job_id}").json()

In the above example, base_url corresponds to your API endpoint. The qBraid Session class distinguishes between headers and auth_headers, ensuring that values in auth_headers are masked in all responses to safeguard against the inadvertent exposure of secret keys.

The qBraid Session class offers additional customizable options such as the total number of retries for requests, the number of connection retries, the backoff factor between retry attempts, and more. For further details, refer to the linked API Reference below.

TargetProfile

The qbraid.runtime.TargetProfile encapsulates the configuration settings and runtime protocol(s) for a quantum device, presenting them as a read-only dictionary. This class plays a crucial role in orchestrating the processes required for the submission of quantum jobs in the current environment.

Specifically, the TargetProfile class specifies domain and device-specific instructions to tailor quantum programs to the intermediate representation (IR) required for submission through the provider’s API and execution on the quantum backend. This includes compilation steps, type conversions, data mappings, and other essential runtime transformations.

Below is an example implementation of a TargetProfile:

from unittest.mock import Mock

from qbraid.programs import ProgramSpec
from qbraid.runtime import DeviceActionType, DeviceType, TargetProfile


profile = TargetProfile(
    device_id="abc123",
    num_qubits=7,
    device_type=DeviceType.QPU,
    action_type=DeviceActionType.OPENQASM,
    program_spec=ProgramSpec(Mock, alias="mock"),
    basis_gates=["h", "x", "z", "cx", "s", "t"],
    provider_name="myprovider",
)

Provider Setup

Each QuantumProvider subclass must implement both a get_device and a get_devices method. These methods process raw device data, adapting it into a TargetProfile for each device, and return either a single QuantumDevice object or a list of them. In this example, we use the MySession class to handle API requests; however, this is illustrative and not mandatory. API interactions can also be managed directly through other means.

In cases where the API data does not directly conform to the format needed to instantiate a TargetProfile, additional mappings and adaptations will typically be necessary. Below is an example implementation of a provider using MySession to construct a MyDevice object. We will explore implementations of QuantumDevice subclasses in the next section.

from qbraid.runtime import QuantumProvider, TargetProfile


class MyProvider(QuantumProvider):

    def __init__(self, api_key: str):
        super().__init__()
        self.session = MySession(api_key)

    def _build_profile(self, data: dict[str, Any]) -> TargetProfile:
        return TargetProfile(**data)

    def get_device(self, device_id: str) -> MyDevice:
        data = self.get_device(device_id=device_id)
        profile = self._build_profile(data)
        return MyDevice(profile, self.session)

    def get_devices(self, **kwargs) -> list[MyDevice]:
        data = self.session.get_devices(**kwargs)
        profiles = [self._build_profile(item) for item in data]
        return [MyDevice(profile, self.session) for profile in profiles]

QuantumDevice

The qbraid.runtime.QuantumDevice class describes the unique parameters and operational settings necessary for executing quantum programs on specific hardware.

The device objects are the core component of the providers. These objects are how users can interface between quantum computing frameworks and hardware/simulators to execute circuits. Any QuantumDevice subclass must implement both a status and submit method.

A minimum working example could look like:

from qbraid.runtime import DeviceStatus, QuantumDevice


class MyDevice(QuantumDevice):

    def __init__(self, profile: TargetProfile, session: MySession):
        super.__init__(profile=profile)
        self.session = session

    def status(self) -> DeviceStatus:
        data = self.session.get_device(self.id)
        status = data.get("status")
        if status == "online":
            return DeviceStatus.ONLINE
        return DeviceStatus.OFFLINE

    def transform(self, run_input: Mock) -> str:
        program_ir = str(run_input)
        return program_ir

    def submit(self, run_input: str, shots=1000) -> MyJob:
        job_data = {"target": self.id, "inpput": run_input, "shots": shots}
        job_data = self.session.create_job(job_data)
        job_id = job_data["job_id"]
        return MyJob(job_id, session=self.session, device=self, shots=shots)

QuantumJob

The qbraid.runtime.QuantumJob class represents the transitional states of quantum programs, managing both ongoing and completed quantum computations.

from qbraid.runtime import GateModelJobResult, JobStatus, QuantumJob


class MyJob(QuantumJob):

    def __init__(self, job_id: str, session: MySession, **kwargs):
        super().__init__(job_id=job_id, **kwargs)
        self.session = session

    def status(self):
        data = self.session.get_job(self.id)
        status = data.get("status")
        if status == "completed":
            return JobStatus.COMPLETED
        if status == "failed":
            return JobStatus.FAILED
        return JobStatus.QUEUED

    def result(self) -> GateModelJobResult:
        self.wait_for_final_state()
        data = self.session.get_job(self.id)
        success = data.get("status") == "completed"
        if not success:
            raise RuntimeError("Job failed")
        return GateModelJobResult(data.get("result"))

QuantumJobResult

The qbraid.runtime.QuantumJobResult class captures and structures the output from quantum computations, including histograms and raw data, for analysis and interpretation.

GateModelJobResult

Abstract interface for gate model quantum job results.

To create a custom GateModelJobResult class one must implement the abstract measurements and raw_counts methods:

import numpy as np

from qbraid.runtime.result import QuantumJobResult


class MyResult(QuantumJobResult):

    def raw_counts(self, **kwargs) -> dict[str, int]:
        return self._result.get("counts", {})

    def measurements(self) -> np.ndarray:
        counts = self.raw_counts()
        res = []
        for state in counts:
            new_state = []
            for bit in state:
                new_state.append(int(bit))
            for _ in range(counts[state]):
                res.append(new_state)
        return np.array(res, dtype=int)