Skip to main contentIBM Quantum Documentation Preview
This is a preview build of IBM Quantumâ„¢ documentation. Refer to docs.quantum.ibm.com for the official documentation.

Migrate third-party provider interfaces from backend.run to primitives


Why should third-party providers implement primitives?

The Qiskit SDK is built to support third parties in creating external providers of quantum resources. The evolution of user access patterns for QPUs led to the Qiskit primitives (Sampler and Estimator) superseding the backend.run interface. To ensure Qiskit users can seamlessly transition between providers, all third-party providers should implement native support of the primitives for performant access to their hardware or simulators.

This guide is for you if the following applies:

  • You are in charge of a third-party provider.
  • Your interface is still based on backend.run.
  • You are looking for guidance to implement your own native Sampler and Estimator primitives.

After you migrate your interface to primitives, your users with primitives-based code can seamlessly transition between providers.


Exposing native Sampler and Estimator implementations

Qiskit defines base interfaces (BaseEstimatorV2 and BaseSamplerV2) that set the standards for input and output handling in the primitives. Native primitive implementations can be developed to wrap any service provider hardware access function (for example, execute_workload(QPU) or resource.access()) or local simulator, as long as the final inputs and outputs conform to the established standards set by the base interfaces.

The Estimator interface exposes a tool to calculate expectation values of observables with respect to quantum states, while the Sampler interface outputs the counts or probability distributions obtained by sampling a quantum state in a QPU. These base interfaces also provide standard methods for execution, querying job status, and returning the job results. See the qiskit.primitives API documentation for more information.

The primitive unified bloc (PUB) data model defines the primitive input format, as described in Overview of PUBs. While different types of data can be accepted in a PUB, both SamplerPub and EstimatorPub have a coerce() method that can be used to convert them into the respective model. The following code snippets provide an example for each of the primitives.

The PrimitiveResult defines the output of a primitive job. It is comprised of additional data models such as PubResult and DataBin. See Overview of primitive results for more information.


Example of a third-party Sampler implementation

An example of a custom Sampler implementation for a third-party provider might look like:

from qiskit.primitives import (
    BaseSamplerV2
    BitArray,
    DataBin,
    PrimitiveResult,
    SamplerPubResult,
    SamplerPubLike,
    PrimitiveJob
)
from qiskit.primitives.containers.sampler_pub import SamplerPub
 
class CustomSampler(BaseSamplerV2):
    """
    Custom Sampler implementation for a third-party `backend`.
    """
    def __init__(
        self,
        *,
        backend: BackendV2, # object representing QPU (hardware resource)
        options: dict # options can be a dict or a subclass of the qiskit Options class
    ):
        self._hw_resource = backend
        self._options = options
        self._default_shots = 1024
 
    @property
    def backend(self) -> BackendV2:
        """ Return the Sampler's backend (hardware resource)"""
        return self._hw_resource
 
    def run(
        self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None
    ) -> BasePrimitiveJob[PrimitiveResult[SamplerPubResult]]:
        """ 
        Execution method. Steps to implement: 
            1. Define a default number of shots if none is given. 
            2. Validate pub format.
            3. Instantiate an object that inherits from BasePrimitiveJob 
                containing pub and runtime information.
            4. Send the job to the provider's execution service.
            5. Return the data in some format.
        """
 
        # 1. Define default shots.
        if shots is None:
            shots = self._default_shots
 
        # 2. Validate SamplerPub format.
        coerced_pubs = [SamplerPub.coerce(pub, shots) for pub in pubs]
 
        # 3. Instantiate job object. See _run and _run_pub for more details.
        job = PrimitiveJob(self._run, coerced_pubs)
 
        # 4. Send the job to the provider's execution service.
        job._submit()
 
        # 5. Return the data.
        return job
 
    def _run(self, pubs: Iterable[SamplerPub]) -> PrimitiveResult[SamplerPubResult]:
        """
        Iterate over input pubs and call _run_pub.
        """
        results = [self._run_pub(pub) for pub in pubs]
        # return results wrapped in PrimitiveResult
        return PrimitiveResult(results, metadata={"version": 2})
 
    def _run_pub(self, pub: SamplerPub) -> SamplerPubResult:
        """
        This internal method handles the provider resource access.
        """
        # pre-process the sampling inputs to fit the required format
        circuit, qargs, meas_info = _preprocess_circuit(pub.circuit)
        bound_circuits = pub.parameter_values.bind_all(circuit)
        arrays = {
            item.creg_name: np.zeros(
                bound_circuits.shape + (pub.shots, item.num_bytes), dtype=np.uint8
            )
            for item in meas_info
        }
 
        # iterate over pub elements
        for index, bound_circuit in enumerate(bound_circuits):
            # ACCESS PROVIDER RESOURCE HERE
            # This case shows an illustrative implementation
            samples_array = self._hw_resource.sample(bound_circuit)
 
            # post-process the sampling output to fit the required format
            for item in meas_info:
                ary = _samples_to_packed_array(samples_array, item.num_bits, item.qreg_indices)
                arrays[item.creg_name][index] = ary
        meas = {
            item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info
        }
        # return results wrapped in SamplerPubResult
        return SamplerPubResult(
            DataBin(**meas, shape=pub.shape),
            metadata={"shots": pub.shots, "circuit_metadata": pub.circuit.metadata},
        )

This implementation is based on the StatevectorSampler source code, with some additions to represent the backend (hardware) resource access, as the StatevectorSampler class is designed for local statevector simulators.


Example of a third-party Estimator implementation

An example of a custom Estimator implementation for a third-party provider might look like:

from .base import BaseEstimatorV2
from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult
from .containers.estimator_pub import EstimatorPub
from .primitive_job import PrimitiveJob
...
 
class CustomEstimator(BaseEstimatorV2):
 
    """
    Custom Estimator implementation for a third-party `backend`.
    """
    def __init__(
        self,
        *,
        backend: BackendV2, # object representing QPU (hardware) resource
        options: dict # options can be a dict or a subclass of the qiskit Options class
    ):
        self._hw_resource = backend
        self._options = options
        self._default_precision = 1024
 
    @property
    def backend(self) -> BackendV2:
        """ Return the Estimator's backend (hardware) resource"""
        return self._hw_resource
 
    def run(
        self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None
    ) -> PrimitiveJob[PrimitiveResult[PubResult]]:
        """ 
        Execution method. Steps to implement: 
            1. Define a default precision if none is given. 
            2. Validate the PUB format.
            3. Instantiate an object that inherits from BasePrimitiveJob 
                containing PUB and runtime information.
            4. Send the job to the provider's execution service.
            5. Return the data in some format.
        """
 
        # 1. Define default precision
        if precision is None:
            precision = self._default_precision
 
        # 2. Validate EstimatorPub format
        coerced_pubs = [EstimatorPub.coerce(pub, precision) for pub in pubs]
 
        # 3. Instantiate the job object. See _run and _run_pub for more details
        job = PrimitiveJob(self._run, coerced_pubs)
 
        # 4. Send the job to the provider's execution service
        job._submit()
 
        # 5. Return the data
        return job
 
    def _run(self, pubs: list[EstimatorPub]) -> PrimitiveResult[PubResult]:
        """
        Iterate over input pubs and call _run_pub.
        """
        return PrimitiveResult([self._run_pub(pub) for pub in pubs], metadata={"version": 2})
 
    def _run_pub(self, pub: EstimatorPub) -> PubResult:
        """
        This internal method handles the provider resource access.
        """
        # pre-process the estimator inputs to fit the required format
        rng = np.random.default_rng(self._seed)
        circuit = pub.circuit
        observables = pub.observables
        parameter_values = pub.parameter_values
        precision = pub.precision
        bound_circuits = parameter_values.bind_all(circuit)
        bc_circuits, bc_obs = np.broadcast_arrays(bound_circuits, observables)
        evs = np.zeros_like(bc_circuits, dtype=np.float64)
        stds = np.zeros_like(bc_circuits, dtype=np.float64)
 
        # iterate over PUB elements
        for index in np.ndindex(*bc_circuits.shape):
            # further pre-processing of the inputs to fit the required format
            bound_circuit = bc_circuits[index]
            observable = bc_obs[index]
            paulis, coeffs = zip(*observable.items())
            obs = SparsePauliOp(paulis, coeffs)
 
            # ACCESS PROVIDER RESOURCE HERE
            # This case shows an illustrative implementation
            samples_array = self._hw_resource.sample(bound_circuit, rng, precision)
 
            # post-process the sampling output to extract expectation value
            expectation_value = compute_expectation_value(samples_array, obs)
            evs[index] = expectation_value
 
        # return results wrapped in PubResult
        data = DataBin(evs=evs, stds=stds, shape=evs.shape)
        return PubResult(
            data, metadata={"target_precision": precision, "circuit_metadata": pub.circuit.metadata}
        )

Similar to the Sampler example, this implementation is based on the StatevectorEstimator source code, with some additions to represent the backend (hardware) resource access, as the StatevectorEstimator class is designed for local statevector simulators.