# Introduction to primitives

Computing systems are built on multiple layers of abstraction. Abstractions allow to focus on a particular level of detail relevant to the task at hand. The closer you get to the hardware, the lower the level of abstraction you need (for example, you might want to manipulate electrical signals), and vice versa. The more complex the task you want to perform, the higher-level the abstractions will be (for example, you could be using a programming library to perform algebraic calculations).

In this context, a *primitive* is the smallest processing instruction, the simplest building block from which
one can create something useful for a given abstraction level.

The recent progress in quantum computing has increased the need to work at higher levels of abstraction. As we move toward larger systems and more complex workflows, the focus shifts from interacting with individual qubit signals to viewing quantum devices as systems that perform tasks we need.

The two most common tasks quantum computers are used for are sampling quantum states and calculating expectation values. These tasks motivated the design of the Qiskit primitives: Sampler and Estimator.

In short, the computational model introduced by the Qiskit primitives moves quantum programming one step closer to where classical programming is today, where the focus is less on the hardware details and more on the results you are trying to achieve.

## Implementation of Qiskit primitives

The Qiskit primitives are defined by open-source primitive base-classes, from
which different providers can derive their own Sampler and Estimator implementations. Among the implementations
using Qiskit, you can find reference primitive implementations for local simulation in the `qiskit.primitives`

module.
Providers like Qiskit Runtime enable access to appropriate systems through native implementations of
their own primitives.

To ensure faster and more efficient results, as of 1 March 2024, circuits and observables need to be transformed to only use instructions supported by the system (referred to as *instruction set architecture (ISA)* circuits and observables) before being submitted to the Qiskit Runtime primitives. See the transpilation documentation for instructions to transform circuits. Due to this change, the primitives will no longer perform layout or routing operations. Consequently, transpilation options referring to those tasks will no longer have any effect. By default, all primitives except Sampler V2 still optimize the input circuits. To bypass all optimization, set `optimization_level=0`

.

*Exception*: When you initialize the Qiskit Runtime Service with the Q-CTRL channel strategy (example below), abstract circuits are still supported.

`service = QiskitRuntimeService(channel="ibm_cloud", channel_strategy="q-ctrl")`

## Benefits of Qiskit primitives

For Qiskit users, primitives let you write quantum code for a specific system without having to explicitly
manage every detail. In addition, because of the additional layer of abstraction, you might be able to more easily
access advanced hardware capabilities of a given provider. For example, with Qiskit Runtime primitives,
you can leverage the latest advancements in error mitigation and suppression by toggling options such as
`optimization_level`

and `resilience_level`

, rather than building your own implementation of these techniques.

For hardware providers, implementing primitives natively means you can provide your users with a more “out-of-the-box” way to access your hardware features. It is therefore easier for your users to benefit from your hardware's best capabilities.

## V2 primitives

Version 2 (available with qiskit-ibm-runtime 0.21.0) is the first major interface change since the introduction of Qiskit Runtime primitives. Based on user feedback, the new version introduces the following major changes:

- The
**new interface**lets you specify a single circuit and multiple observables (if using Estimator) and parameter value sets for that circuit. - You can
**turn on or off individual error mitigation and suppression methods**. - You can choose to
**get counts or per-shot measurements**instead of quasi-probabilities from Sampler V2. - Due to scaling issues, Sampler V2 does not support specifying a resilience level, and Estimator V2 supports only levels 0 - 2.
- To reduce the total execution time, v2 primitives only support ISA circuits. You can use the Qiskit transpiler (manually) or the AI-driven transpilation service (premium users) to transform the circuits before making the queries to the primitives.

See the EstimatorV2 API reference and SamplerV2 API reference for full details.

### Interface changes

The updated interface uses a *primitive unified bloc* (PUB) for input. Each PUB is a tuple that contains a circuit and the data broadcasted to it. This greatly simplifies your ability to send complex data to a circuit. Previously, you had to specify the same circuit multiple times to match the size of the data to be combined. A summary of the changes to each primitive follows.

#### Estimator V1

- Takes three parameters: circuits, observables, and parameter values.
- Each parameter can be a single value or a list.
- The length of the parameters must match.
- Elements from each are aggregated.

Example:

```
estimator.run([circuit1, circuit2, ...],[observable1, observable2, ...],
[param_values1, param_values2, ...] )
```

#### Estimator V2

- The
`run()`

method takes an array of PUBs. Each PUB is in the format (`<single circuit>`

,`<one or more observables>`

,`<optional one or more parameter values>`

,`<optional precision>`

), where the optional`parameter values`

can be a list or a single parameter. - Combines elements from observables and parameter values by following NumPy broadcasting rules as described below.
- Each input PUB has a corresponding PubResult that contains both data and metadata.

Example:

`estimator.run([(circuit1, observable1, param_values1),(circuit2, observable2, param_values2)])`

##### Broadcasting rules

Estimator V2 aggregates elements from multiple arrays (observables and parameter values) by following the same broadcasting rules as NumPy. This section summarizes those rules. For a detailed explanation, see the NumPy broadcasting rules documentation.(opens in a new tab)

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:

```
# 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, )
```

#### Sampler V1

- Takes two parameters: circuits and parameter values.
- Each parameter can be a single value or a list.
- The length of the parameters must match.
- Elements from each are aggregated.

Example:

`sampler.run([circuit1, circuit2, ...],[observable1, observable2, ...],[param_values1, param_values2, ...] )`

#### Sampler V2

- Takes one parameter: PUBs in the format (
`<single circuit>`

,`<optional one or more parameter values>`

,`<optional shots>`

), where there can be multiple`parameter values`

items, and each item can be either an array or a single parameter, depending on the chosen circuit. - Elements from each are aggregated. For example, each array of parameter values in the PUB is applied to the PUB's circuit.
- Obeys program outputs. Typically this is a bit array but can also be an array of complex numbers (measurement level 1).
- Returns raw data type. Data from each shot is returned (analogous to
`memory=True`

in the`backend.run`

interface), and post-processing is done by using convenience methods. - Output data is grouped by output registers. You need the classical register name to get the results. By default, it is named
`meas`

if you use`measure_all()`

. You can find the classical register name by running`<circuit_name>.cregs`

. For example,`qc.cregs`

. - Supports circuits with classical feedforward and control flow (dynamic circuits).
Note
Dynamic circuits do not support dynamical decoupling.

Example:

```
sampler.run([
(circuit1, param_values1, shots1),
(circuit2, param_values2, shots_2),
])
```

## Estimator

The Estimator behaves differently, depending on what version you are using.

Estimator V2 computes expectation values of observables with respect to states prepared by quantum circuits.
It receives one or more PUBs as the inputs and returns the computed expectation values per pair, along with their
standard error, in `PubResult`

form. Different Estimator implementations support various configuration options. The circuits
can be parametrized, as long as the parameter values are also provided as input to the primitive.

The Estimator primitive computes expectation values of observables with respect to states prepared by quantum circuits. The Estimator receives circuit-observable pairs (with the observable expressed as a weighted sum of Pauli operators) as inputs, and returns the computed expectation values per pair, as well as their variances. Different Estimator implementations support various configuration options. The circuits can be parametrized, as long as the parameter values are also provided as input to the primitive.

## Sampler

The Sampler behaves differently, depending on what version you are using.

Sampler V2 is simplified to focus on its core task of sampling the output register from execution of quantum circuits. It returns the samples, whose type is defined by the program, without weights and therefore does not support resilience levels. The result class, however, has methods to return weighted samples, such as counts.

It receives one or more PUBs as the inputs and returns counts or per-shot measurements, as `PubResult`

s. The circuits
can be parametrized, as long as the parameter values are also provided as input to the primitive.

The Sampler primitive samples from the classical output registers resulting from execution of quantum circuits. For this reason, the inputs to the Sampler are (parametrized) quantum circuits, for which it returns the corresponding quasi-probability distributions of sampled bitstrings. Quasi-probability distributions are similar to regular probabilities, except they might include negative values, which can occur when using certain error mitigation techniques.

## How to use Qiskit primitives

The `qiskit.primitives`

module enables the development of primitive-style quantum programs and was specifically
designed to simplify switching between different types of systems. The module provides three separate classes
for each primitive type:

`StatevectorSampler`

and`StatevectorEstimator`

These classes are reference implementations of both primitives and use the simulator built in to Qiskit. They leverage the Qiskit `quantum_info`

module in the background, producing results based on ideal statevector simulations.

`BaseSampler`

and`BaseEstimator`

These are abstract base classes that define a common interface for implementing primitives. All other classes in the `qiskit.primitives`

module inherit from these base classes, and developers should use these if they are interested in developing their own primitives-based execution model for a specific system provider. These classes may also be useful for those who want to do highly customized processing and find the existing primitives implementations too simple for their needs.

`BackendSampler`

and`BackendEstimator`

If a provider does not support primitives natively, you can use these classes to “wrap” any system into a primitive. Users can write primitive-style code for providers that don’t yet have a primitives-based interface. These classes can be used just like the regular Sampler and Estimator, except they should be initialized with an additional `backend`

argument for selecting which system to run on.

The Qiskit Runtime primitives provide a more sophisticated implementation (for example, by including error mitigation) as a cloud-based service.

## Next steps

- Read Get started with primitives to implement primitives in your work.
- Review detailed primitives examples.
- Read Migrate to V2 primitives.
- Practice with primitives by working through the Cost function lesson(opens in a new tab) in IBM Quantum™ Learning.