Skip to main contentIBM Quantum Documentation Preview

Primitive inputs and outputs

This page gives an overview of the inputs and outputs of the Qiskit Runtime primitives that execute workloads on IBM Quantum™ compute resources. These primitives provide you with the ability to efficiently define vectorized workloads by using a data structure known as a Primitive Unified Bloc (PUB). These PUBs are the fundamental unit of work a QPU needs to execute these workloads. They are used as inputs to the run() method for the Sampler and Estimator primitives, which execute the defined workload as a job. Then, after the job has completed, the results are returned in a format that is dependent on both the PUBs used as well as the runtime options specified from the Sampler or Estimator primitives.


Overview of PUBs

When invoking a primitive's run() method, the main argument that is required is a list of one or more tuples -- one for each circuit being executed by the primitive. Each of these tuples is considered a PUB, and the required elements of each tuple in the list depends on the the primitive used. The data provided to these tuples can also be arranged in a variety of shapes to provide flexibility in a workload through broadcasting -- the rules of which are described in a following section.

Estimator PUB

For the Estimator primitive, the format of the PUB should contain at most four values:

  • A single QuantumCircuit, which may contain one or more Parameter objects
  • A list of one or more observables, which specify the expectation values to estimate, arranged into an array (for example, a single observable represented as a 0-d array, a list of observables as a 1-d array, and so on). The data can be in any one of the ObservablesArrayLike format such as Pauli, SparsePauliOp, PauliList, or str.
  • A collection of parameter values to bind the circuit against. This can be specified as a single array-like object where the last index is over circuit Parameter objects, or omitted (or equivalently, set to None) if the circuit has no Parameter objects.
  • (Optionally) a target precision for expectation values to estimate

Sampler PUB

For the Sampler primitive, the format of the PUB tuple contains at most three values:

  • A single QuantumCircuit, which may contain one or more Parameter objects
    • Note: these circuits should also include measurement instructions for each of the qubits to be sampled.
  • A collection of parameter values to bind the circuit against θk\theta_k (only needed if any Parameter objects are used that must be bound at runtime)
  • (Optionally) a number of shots to measure the circuit with

The following code demonstrates an example set of vectorized inputs to the Estimator primitive and executes them on an IBM® backend as a single RuntimeJobV2 object.

[1] :
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit_ibm_runtime import (
    EstimatorV2 as Estimator,
    SamplerV2 as Sampler,
    QiskitRuntimeService,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import Pauli, SparsePauliOp
import numpy as np
 
# Instantiate runtime service and get
# the least busy backend
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
 
# Define a circuit with two parameters.
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.ry(Parameter("a"), 0)
circuit.rz(Parameter("b"), 0)
circuit.cx(0, 1)
circuit.h(0)
 
# Transpile the circuit
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
transpiled_circuit = pm.run(circuit)
layout = transpiled_circuit.layout
 
 
# Now define a sweep over parameter values, the last axis of dimension 2 is
# for the two parameters "a" and "b"
params = np.vstack(
    [
        np.linspace(-np.pi, np.pi, 100),
        np.linspace(-4 * np.pi, 4 * np.pi, 100),
    ]
).T
 
# Define three observables. The inner length-1 lists cause this array of
# observables to have shape (3, 1), rather than shape (3,) if they were
# omitted.
observables = [
    [SparsePauliOp(["XX", "IY"], [0.5, 0.5])],
    [SparsePauliOp("XX")],
    [SparsePauliOp("IY")],
]
# Apply the same layout as the transpiled circuit.
observables = [
    [observable.apply_layout(layout) for observable in observable_set]
    for observable_set in observables
]
 
# Estimate the expectation value for all 300 combinations of observables
# and parameter values, where the pub result will have shape (3, 100).
#
# This shape is due to our array of parameter bindings having shape
# (100, 2), combined with our array of observables having shape (3, 1).
estimator_pub = (transpiled_circuit, observables, params)
 
# Instantiate the new estimator object, then run the transpiled circuit
# using the set of parameters and observables.
estimator = Estimator(mode=backend)
job = estimator.run([estimator_pub])
result = job.result()

Broadcasting rules

The PUBs aggregate elements from multiple arrays (observables and parameter values) by following the same broadcasting rules as NumPy. This section briefly summarizes those rules. For a detailed explanation, see the NumPy broadcasting rules documentation.

Rules:

  • Input arrays do not need to have the same number of dimensions.
    • The resulting array will have the same number of dimensions as the input array with the largest dimension.
    • The size of each dimension is the largest size of the corresponding dimension.
    • Missing dimensions are assumed to have size one.
  • Shape comparisons start with the rightmost dimension and continue to the left.
  • Two dimensions are compatible if their sizes are equal or if one of them is 1.

Examples of array pairs that broadcast:

A1     (1d array):      1
A2     (2d array):  3 x 5
Result (2d array):  3 x 5


A1     (3d array):  11 x 2 x 7
A2     (3d array):  11 x 1 x 7
Result (3d array):  11 x 2 x 7

Examples of array pairs that do not broadcast:

A1     (1d array):  5
A2     (1d array):  3

A1     (2d array):      2 x 1
A2     (3d array):  6 x 5 x 4 # This would work if the middle dimension were 2, but it is 5.

EstimatorV2 returns one expectation value estimate for each element of the broadcasted shape.

Here are some examples of common patterns expressed in terms of array broadcasting. Their accompanying visual representation is shown in the figure that follows:

Parameter value sets are represented by n x m arrays, and observable arrays are represented by one or more single-column arrays. For each example in the previous code, the parameter value sets are combined with their observable array to create the resulting expectation value estimates. Example 1 (broadcast single observable) has a parameter value set that is a 5x1 array and a 1x1 observables array. The one item in the observables array is combined with each item in the parameter value set to create a single 5x1 array where each item is a combination of the original item in the parameter value set with the item in the observables array. Example 2 (zip) has a 5x1 parameter value set and a 5x1 observables array. The output is a 5x1 array where each item is a combination of the nth item in the parameter value set with the nth item in the observables array. Example 3 (outer/product) has a 1x6 parameter value set and a 4x1 observables array. Their combination results in a 4x6 array that is created by combining each item in the parameter value set with every item in the observables array, and thus each parameter value becomes an entire column in the output. Example 4 (Standard nd generalization) has a 3x6 parameter value set array and two 3x1 observables array. These combine to create two 3x6 output arrays in a similar manner to the previous example.

This image illustrates several visual representations of array broadcasting
Visual representation of broadcasting
[2] :
# Broadcast single observable
 
 
parameter_values = np.random.uniform(size=(5,))  # shape (5,)
observables = SparsePauliOp("ZZZ")  # shape ()
#>> pub result has shape (5,)
 
# Zip
parameter_values = np.random.uniform(size=(5,))  # shape (5,)
observables = [SparsePauliOp(pauli) for pauli in ["III", "XXX", "YYY", "ZZZ", "XYZ"]]  # shape (5,)
#>> pub result has shape (5,)
 
# Outer/Product
parameter_values = np.random.uniform(size=(1, 6))  # shape (1, 6)
observables = [[SparsePauliOp(pauli)] for pauli in ["III", "XXX", "YYY", "ZZZ"]]  # shape (4, 1)
#>> pub result has shape (4, 6)
 
# Standard nd generalization
parameter_values = np.random.uniform(size=(3, 6))  # shape (3, 6)
observables = [
    [[SparsePauliOp(['XII'])], [SparsePauliOp(['IXI'])], [SparsePauliOp(['IIX'])]],
    [[SparsePauliOp(['ZII'])], [SparsePauliOp(['IZI'])], [SparsePauliOp(['IIZ'])]]
]  # shape (2, 3, 1)
SparsePauliOp

Each SparsePauliOp counts as a single element in this context, regardless of the number of Paulis contained in the SparsePauliOp. Thus, for the purpose of these broadcasting rules, all of the following elements have the same shape:

a = SparsePauliOp("Z") # shape ()
b = SparsePauliOp("IIIIZXYIZ") # shape ()
c = SparsePauliOp.from_list(["XX", "XY", "IZ"]) # shape ()

The following lists of operators, while equivalent in terms of information contained, have different shapes:

list1 = SparsePauliOp.from_list(["XX", "XY", "IZ"]) # shape ()
list2 = [SparsePauliOp("XX"), SparsePauliOp("XY"), SparsePauliOp("IZ")] # shape (3, )

Overview of primitive results

Once one or more PUBs are sent to a QPU for execution and a job successfully completes, the data is returned as a PrimitiveResult container object accessed by calling the RuntimeJobV2.result() method. The PrimitiveResult contains an iterable list of PubResult objects that contain the execution results for each PUB. Depending on the primitive used, these data will be either expectation values and their error bars in the case of the Estimator, or samples of the circuit output in the case of the Sampler.

Each element of this list corresponds to each PUB submitted to the primitive's run() method (for example, a job submitted with 20 PUBs will return a PrimitiveResult object that contains a list of 20 PubResults, one corresponding to each PUB).

Each of these PubResult objects possess both a data and a metadata attribute. The data attribute is a customized DataBin that contains the actual measurement values, standard deviations, and so forth. This DataBin has various attributes depending on the shape or structure of the associated PUB as well as the error mitigation options specified by the primitive used to submit the job (for example, ZNE or PEC). Meanwhile, the metadata attribute contains information about the runtime and error mitigation options used (explained later in the Result metadata section of this page).

Put simply, a single job returns a PrimitiveResult object and contains a list of one or more PubResults. These PubResults are where the measurement data is stored for each of the PUBs that were submitted to the job.

Estimator output

Each PubResult for the Estimator primitive contains at least an array of expectation values (PubResult.data.evs) and associated standard deviations (either PubResult.data.stds or PubResult.data.ensemble_standard_error depending on the resilience_level used), but can contain more data depending on the error mitigation options that were specified.

The below code snippet describes the PrimitiveResult (and associated PubResult) format for the job created above.

[3] :
print(f'The result of the submitted job had {len(result)} PUB and has a value:\n {result}\n')
print(f'The associated PubResult of this job has the following DataBins:\n {result[0].data}\n')
print(f'And this DataBin has attributes: {result[0].data.keys()}')
print(f'Recall that this shape is due to our array of parameter binding sets having shape (100,), combined with \n\
  our array of observables having shape (3, 1), where 2 is the number of parameters in the circuit.\n')
print(f'The expectation values measured from this PUB are: \n{result[0].data.evs}')

Output:

The result of the submitted job had 1 PUB and has a value:
 PrimitiveResult([PubResult(data=DataBin(evs=np.ndarray(<shape=(3, 100), dtype=float64>), stds=np.ndarray(<shape=(3, 100), dtype=float64>), ensemble_standard_error=np.ndarray(<shape=(3, 100), dtype=float64>), shape=(3, 100)), metadata={'shots': 8192, 'target_precision': 0.015625, 'circuit_metadata': {}, 'resilience': {}, 'num_randomizations': 64})], metadata={'dynamical_decoupling': {'enable': False, 'sequence_type': 'XX', 'extra_slack_distribution': 'middle', 'scheduling_method': 'alap'}, 'twirling': {'enable_gates': False, 'enable_measure': True, 'num_randomizations': 'auto', 'shots_per_randomization': 'auto', 'interleave_randomizations': True, 'strategy': 'active-accum'}, 'resilience': {'measure_mitigation': True, 'zne_mitigation': False, 'pec_mitigation': False}, 'version': 2})

The associated PubResult of this job has the following DataBins:
 DataBin(evs=np.ndarray(<shape=(3, 100), dtype=float64>), stds=np.ndarray(<shape=(3, 100), dtype=float64>), ensemble_standard_error=np.ndarray(<shape=(3, 100), dtype=float64>), shape=(3, 100))

And this DataBin has attributes: dict_keys(['evs', 'stds', 'ensemble_standard_error'])
Recall that this shape is due to our array of parameter binding sets having shape (100,), combined with 
  our array of observables having shape (3, 1), where 2 is the number of parameters in the circuit.

The expectation values measured from this PUB are: 
[[ 4.02497613e-02  1.89142564e-01  3.23456693e-01  4.65164404e-01
   5.35049808e-01  6.21778646e-01  6.51841895e-01  6.57341091e-01
   6.35820154e-01  5.84831999e-01  5.26136304e-01  4.59124773e-01
   3.99334308e-01  3.60721387e-01  3.15751606e-01  2.70691070e-01
   2.65899101e-01  2.77848364e-01  3.06078549e-01  3.23777193e-01
   3.72343609e-01  4.27311036e-01  4.51821946e-01  4.76076129e-01
   5.06263654e-01  4.95435324e-01  4.70889257e-01  4.47038153e-01
   3.94008444e-01  3.47931629e-01  2.80516202e-01  2.48804665e-01
   1.99709260e-01  1.97581041e-01  1.89994506e-01  2.02818603e-01
   2.56640569e-01  3.24764040e-01  4.01862335e-01  4.69156757e-01
   5.48260632e-01  5.65461357e-01  6.03837990e-01  6.28567200e-01
   5.90048304e-01  5.41259501e-01  4.50955287e-01  3.23703609e-01
   1.90124505e-01  4.69549207e-02 -1.23709005e-01 -2.47276566e-01
  -3.89675968e-01 -5.14402888e-01 -5.75765603e-01 -6.32779489e-01
  -6.75665202e-01 -6.32477794e-01 -6.00414688e-01 -5.93178087e-01
  -4.97601154e-01 -4.45562380e-01 -3.59130332e-01 -3.30271411e-01
  -2.95618958e-01 -2.78583389e-01 -2.68266061e-01 -3.05076985e-01
  -3.24463162e-01 -3.74284598e-01 -4.20866692e-01 -4.55330279e-01
  -4.86761378e-01 -5.10974680e-01 -5.09465386e-01 -5.21595339e-01
  -4.66943507e-01 -4.33192052e-01 -3.76143826e-01 -3.14588975e-01
  -2.68514612e-01 -2.16282066e-01 -1.96773249e-01 -1.92520080e-01
  -1.80391762e-01 -2.46893110e-01 -2.98295789e-01 -3.49978088e-01
  -4.18287973e-01 -4.86907729e-01 -5.77671435e-01 -5.95695486e-01
  -6.15909076e-01 -6.21891475e-01 -5.62002080e-01 -4.86014089e-01
  -4.00240212e-01 -2.60362296e-01 -1.32413203e-01  4.90815052e-02]
 [ 8.90336591e-02  1.30293160e-01  1.71009772e-01  2.48099891e-01
   3.17589577e-01  3.81107492e-01  4.46254072e-01  4.72312704e-01
   5.40716612e-01  5.79804560e-01  6.54180239e-01  6.94353963e-01
   7.52985885e-01  8.00760043e-01  8.46362649e-01  8.76221498e-01
   9.02280130e-01  9.19652552e-01  9.76112921e-01  9.72855592e-01
   9.76112921e-01  1.00271444e+00  1.00705755e+00  1.01791531e+00
   1.01302932e+00  1.00542888e+00  9.93485342e-01  1.00000000e+00
   9.58197611e-01  9.54397394e-01  9.30510315e-01  9.08251900e-01
   8.64277959e-01  8.34419110e-01  7.79587405e-01  7.53528773e-01
   7.03040174e-01  6.59066232e-01  6.13463626e-01  5.60803474e-01
   5.41802389e-01  4.38653637e-01  3.97937025e-01  3.68078176e-01
   2.85016287e-01  2.49185668e-01  1.84039088e-01  1.04234528e-01
   3.63735071e-02 -1.95439739e-02 -1.12377850e-01 -1.47665581e-01
  -2.23127036e-01 -2.95874050e-01 -3.52334419e-01 -4.06623236e-01
  -5.19001086e-01 -5.11943540e-01 -5.80347448e-01 -6.74809989e-01
  -6.82953312e-01 -7.27470141e-01 -7.78501629e-01 -8.18675353e-01
  -8.58306189e-01 -8.92508143e-01 -8.98479913e-01 -9.49511401e-01
  -9.77741585e-01 -9.80998914e-01 -1.00488599e+00 -1.01357220e+00
  -1.02171553e+00 -1.03148751e+00 -1.01140065e+00 -1.01357220e+00
  -1.00868621e+00 -9.98914224e-01 -9.83713355e-01 -9.55483170e-01
  -9.25081433e-01 -9.04451683e-01 -8.72964169e-01 -8.33333333e-01
  -7.57871878e-01 -7.50814332e-01 -7.16069490e-01 -6.48751357e-01
  -5.83061889e-01 -5.45602606e-01 -5.16286645e-01 -4.28338762e-01
  -3.71878393e-01 -3.24104235e-01 -2.33441911e-01 -1.81867535e-01
  -1.50380022e-01 -6.18892508e-02  9.77198697e-03  8.36047774e-02]
 [-8.53413655e-03  2.47991968e-01  4.75903614e-01  6.82228916e-01
   7.52510040e-01  8.62449799e-01  8.57429719e-01  8.42369478e-01
   7.30923695e-01  5.89859438e-01  3.98092369e-01  2.23895582e-01
   4.56827309e-02 -7.93172691e-02 -2.14859438e-01 -3.34839357e-01
  -3.70481928e-01 -3.63955823e-01 -3.63955823e-01 -3.25301205e-01
  -2.31425703e-01 -1.48092369e-01 -1.03413655e-01 -6.57630522e-02
  -5.02008032e-04 -1.45582329e-02 -5.17068273e-02 -1.05923695e-01
  -1.70180723e-01 -2.58534137e-01 -3.69477912e-01 -4.10642570e-01
  -4.64859438e-01 -4.39257028e-01 -3.99598394e-01 -3.47891566e-01
  -1.89759036e-01 -9.53815261e-03  1.90261044e-01  3.77510040e-01
   5.54718876e-01  6.92269076e-01  8.09738956e-01  8.89056225e-01
   8.95080321e-01  8.33333333e-01  7.17871486e-01  5.43172691e-01
   3.43875502e-01  1.13453815e-01 -1.35040161e-01 -3.46887550e-01
  -5.56224900e-01 -7.32931727e-01 -7.99196787e-01 -8.58935743e-01
  -8.32329317e-01 -7.53012048e-01 -6.20481928e-01 -5.11546185e-01
  -3.12248996e-01 -1.63654618e-01  6.02409639e-02  1.58132530e-01
   2.67068273e-01  3.35341365e-01  3.61947791e-01  3.39357430e-01
   3.28815261e-01  2.32429719e-01  1.63152610e-01  1.02911647e-01
   4.81927711e-02  9.53815261e-03 -7.53012048e-03 -2.96184739e-02
   7.47991968e-02  1.32530120e-01  2.31425703e-01  3.26305221e-01
   3.88052209e-01  4.71887550e-01  4.79417671e-01  4.48293173e-01
   3.97088353e-01  2.57028112e-01  1.19477912e-01 -5.12048193e-02
  -2.53514056e-01 -4.28212851e-01 -6.39056225e-01 -7.63052209e-01
  -8.59939759e-01 -9.19678715e-01 -8.90562249e-01 -7.90160643e-01
  -6.50100402e-01 -4.58835341e-01 -2.74598394e-01  1.45582329e-02]]

Sampler output

The Sampler primitive outputs job results in a similar format, with the exception that each DataBin will contain one or more BitArray objects which store the samples of the circuit attached to a particular ClassicalRegister, typically one bitstring per shot. The attribute label for each bit array object depends on the ClassicalRegisters defined in the circuit being executed. The measurement data from these BitArrays can then be processed into a dictionary with key-value pairs corresponding to each bitstring measured (for example, '1011001') and the number of times (or counts) it was measured.

For example, a circuit that has measurement instructions added by the QuantumCircuit.measure_all() function possesses a classical register with the label 'meas'. After execution, a count data dictionary can be created by executing:

[4] :
# Add measurement instructions to the example circuit
circuit.measure_all()
 
# Transpile the circuit
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
transpiled_circuit = pm.run(circuit)
 
# Create a PUB for the Sampler primitive using the same parameters defined earlier
sampler_pub = (transpiled_circuit, params)
 
 
sampler = Sampler(mode=backend)
job = sampler.run([sampler_pub])
result = job.result()
[5] :
print(f'The result of the submitted job had {len(result)} PUB and has a value:\n {result}\n')
print(f'The associated PubResult of this Sampler job has the following DataBins:\n {result[0].data}\n')
print(f'It has a key-value pair dict: \n{result[0].data.items()}\n')
print(f'And the raw data can be converted to a bitstring-count format: \n{result[0].data.meas.get_counts()}')

Output:

The result of the submitted job had 1 PUB and has a value:
 PrimitiveResult([PubResult(data=DataBin(meas=BitArray(<shape=(100,), num_shots=4096, num_bits=2>), shape=(100,)), metadata={'circuit_metadata': {}})], metadata={'execution': {'execution_spans': {'__type__': 'ExecutionSpanCollection', '__value__': {'spans': [{'__type__': 'ExecutionSpan', '__value__': {'start': datetime.datetime(2024, 9, 15, 11, 54, 6, 794307, tzinfo=tzlocal()), 'stop': datetime.datetime(2024, 9, 15, 11, 56, 46, 26278, tzinfo=tzlocal()), 'data_slices': {'0': [[100, 4096], 0, 409600]}}}]}}}, 'version': 2})

The associated PubResult of this Sampler job has the following DataBins:
 DataBin(meas=BitArray(<shape=(100,), num_shots=4096, num_bits=2>), shape=(100,))

It has a key-value pair dict: 
dict_items([('meas', BitArray(<shape=(100,), num_shots=4096, num_bits=2>))])

And the raw data can be converted to a bitstring-count format: 
{'11': 104388, '10': 101555, '01': 101240, '00': 102417}

Result metadata

In addition to the execution results, both the PrimitiveResult and PubResult objects contain a metadata attribute about the job that was submitted. The metadata containing information for all submitted PUBs (such as the various runtime options available) can be found in the PrimitiveResult.metatada, while the metadata specific to each PUB is found in PubResult.metadata.

[6] :
# Print out the results metadata
print(f'The metadata of the PrimitiveResult is:')
for key, val in result.metadata.items():
    print(f"'{key}' : {val},")
 
print(f'\nThe metadata of the PubResult result is:')
for key, val in result[0].metadata.items():
    print(f"'{key}' : {val},")

Output:

The metadata of the PrimitiveResult is:
'execution' : {'execution_spans': {'__type__': 'ExecutionSpanCollection', '__value__': {'spans': [{'__type__': 'ExecutionSpan', '__value__': {'start': datetime.datetime(2024, 9, 15, 11, 54, 6, 794307, tzinfo=tzlocal()), 'stop': datetime.datetime(2024, 9, 15, 11, 56, 46, 26278, tzinfo=tzlocal()), 'data_slices': {'0': [[100, 4096], 0, 409600]}}}]}}},
'version' : 2,

The metadata of the PubResult result is:
'circuit_metadata' : {},

For Sampler jobs, you can also review the result metadata to understand when certain data was run; this is called the execution span.