Integrate external quantum resources with Qiskit
The Qiskit SDK is built to support third parties in creating external providers of quantum resources.
This means that any organization which develops or deploys quantum compute resources can integrate their services into Qiskit and tap into its userbase.
Doing so requires creating a package which supports requests for quantum compute resources and returns them to the user.
Additionally, the package must allow users to submit jobs and retrieve their results through an implementation of the qiskit.primitives
objects.
Providing access to backends
In order for users to transpile and execute QuantumCircuit
objects using external resources, they need to instantiate an object containing a Target
which provides information about a QPU's constraints such as its connectivity, basis gates, and number of qubits. This can be provided through an interface similar to the QiskitRuntimeService
through which a user can make requests for a QPU. This object should, at minimum, contain a Target
, but a simpler approach would be to return a BackendV2
instance.
An example implementation may look something like:
from qiskit.transpiler import Target
from qsikit.providers import BackendV2
class ProviderService:
""" Class for interacting with a provider's service"""
def __init__(
self,
#Receive arguments for authentication/instantiation
):
""" Initiate a connection with the provider service, given some method
of authentication """
def return_target(name: Str) -> Target:
""" Interact with the service and return a Target object """
return target
def return_backend(name: Str) -> BackendV2:
""" Interact with the service and return a BackendV2 object """
return backend
Providing an interface for execution
In addition to providing a service returning hardware configurations, a service providing access to external QPU resources also might also support the execution of quantum workloads. Exposing that capability can be done by creating implementations of the Qiskit primitives interfaces; for example the BasePrimitiveJob
, BaseEstimatorV2
and BaseSamplerV2
among others. At minimum, these interfaces should be able to provide a method for execution, querying job status, and returning the job results.
To handle job status and results, the Qiskit SDK provides a DataBin
, PubResult
, PrimitiveResult
, and BasePrimitiveJob
objects should be used.
See the qiskit.primitives
API documentation as well as the reference implementations BackendEstimatorV2
and BackendSampleV2
for more information.
An example implementation of the Estimator primitive may look like:
from qiskit.primitives import BaseEstimatorV2, BaseSamplerV2, EstimatorPubLike
from qiskit.primitives import DataBin, PubResult, PrimitiveResult, BasePrimitiveJob
from qiskit.providers import BackendV2
class EstimatorImplementation(BaseEstimatorV2):
""" Class for interacting with the provider's Estimator service """
def __init__(
self,
*,
backend: BackendV2,
options: dict
# Receive other arguments to instantiate an Estimator primitive with the service
):
self._backend = backend
self._options = options
self._default_precision = 0.01
@property
def backend(self) -> BackendV2:
""" Return the backend """
return self._backend
def run(
self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None
) -> BasePrimitiveJob[PrimitiveResult[PubResult]]:
""" Steps to implement:
1. Define a default precision if none is given
2. Validate pub format
3. Instantiate an object which inherits from BasePrimitiveJob
containing pub and runtime information
4. Send the job to the execution service of the provider
"""
job = BasePrimitiveJob(pubs, precision)
job_with_results = job.submit()
return job_with_results
And an implementation of the Sampler primitive may look like:
class SamplerImplentation(BaseSamplerV2):
""" Class for interacting with the provider's Sampler service """
def __init__(
self,
*,
backend: BackendV2,
options: dict
# Receive other arguments to instantiate an Estimator primitive with the service
):
self._backend = backend
self._options = options
self._default_shots = 1024
@property
def backend(self) -> BackendV2:
""" Return the Sampler's backend """
return self._backend
def run(
self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None
) -> BasePrimitiveJob[PrimitiveResult[SamplerPubResult]]:
""" Steps to implement:
1. Define a default number of shots if none is given
2. Validate pub format
3. Instantiate an object which inherits from BasePrimitiveJob
containing pub and runtime information
4. Send the job to the execution service of the provider
5. Return the data in some format
"""
job = BasePrimitiveJob(pubs, shots)
job_with_results = job.submit()
return job_with_results