Introduction to Primitive Unified Blocs
Package versions
The code on this page was developed using the following requirements. We recommend using these versions or newer.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
The Estimator and Sampler primitives help you 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 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.
When invoking an Estimator or Sampler 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 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.
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 7Examples 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.

# 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)
# >> pub result has shape (2, 3, 6)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, )Next Steps
- Learn about Estimator input and output in depth.
- Learn about Sampler input and output in depth.