Writing a New Provider
A step-by-step guide to implementing a new provider class.
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:
Provider Setup
Implement a QuantumProvider
subclass that manages authentication and
remote access to the available device(s).
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.
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:
GET
/devices
(or similar) - To retrieve metadata about available quantum devices.POST
/job
(or similar) - To submit a quantum job for execution on a specified device.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.
API Reference:
qbraid.runtime.Session ↗
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",
)
API Reference:
qbraid.runtime.TargetProfile ↗
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]
API Reference:
qbraid.runtime.QuantumProvider ↗
QuantumDevice
The qbraid.runtime.QuantumDevice
class describes the unique parameters and operational settings necessary for executing quantum
programs on specific hardware.
API Reference:
qbraid.runtime.QuantumDevice ↗
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.
API Reference:
qbraid.runtime.QuantumJob ↗
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.
API Reference:
qbraid.runtime.QuantumJobResult ↗
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)
API Reference:
qbraid.runtime.GateModelJobResult ↗