Qiskit primitives standardize two core tasks:
- Sampler executes measured circuits and returns shot-resolved classical data.
- Estimator computes expectation values of observables for states prepared by circuits.
Use V2 interfaces. Their unit of work is a Primitive Unified Bloc (PUB).
| Implementation | Use |
|---|---|
StatevectorSampler |
Exact statevector evolution plus finite-shot sampling on the local CPU |
StatevectorEstimator |
Local statevector expectation values |
Aer SamplerV2 / EstimatorV2 |
High-performance and noisy local simulation |
Runtime SamplerV2 / EstimatorV2 |
IBM QPUs and IBM Runtime services |
BackendSamplerV2 / BackendEstimatorV2 |
Adapt a BackendV2 that lacks native primitives |
The V2 run() structure is shared, but options are implementation-specific. Do not pass Runtime resilience options to statevector or Aer primitives.
A Sampler PUB contains:
- One circuit with measurements.
- Optional parameter values.
Pass shots at the run() level unless a specific current API requires otherwise.
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()
sampler = StatevectorSampler(seed=11)
primitive_result = sampler.run([circuit], shots=1024).result()
pub_result = primitive_result[0]
counts = pub_result.data.meas.get_counts()
bitstrings = pub_result.data.meas.get_bitstrings()
metadata = pub_result.metadatameas is the name of the classical register created by measure_all().
circuit_x = QuantumCircuit(1)
circuit_x.x(0)
circuit_x.measure_all()
circuit_h = QuantumCircuit(1)
circuit_h.h(0)
circuit_h.measure_all()
result = sampler.run([circuit_x, circuit_h], shots=512).result()
counts_x = result[0].data.meas.get_counts()
counts_h = result[1].data.meas.get_counts()Each input PUB produces one PubResult.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.primitives import StatevectorSampler
theta = Parameter("theta")
circuit = QuantumCircuit(1)
circuit.ry(theta, 0)
circuit.measure_all()
values = [[0.0], [np.pi / 2], [np.pi]]
sampler = StatevectorSampler(seed=11)
pub_result = sampler.run(
[(circuit, values)],
shots=256,
).result()[0]
for index, value in enumerate(values):
counts = pub_result.data.meas.get_counts(index)
print(value[0], counts)For a shaped BitArray, pass an index to get_counts() when results must remain separated by parameter point. Calling it without an index can aggregate over axes.
Sampler data fields use register names:
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
from qiskit.primitives import StatevectorSampler
qubits = QuantumRegister(2, "q")
left = ClassicalRegister(1, "left")
right = ClassicalRegister(1, "right")
circuit = QuantumCircuit(qubits, left, right)
circuit.h(qubits[0])
circuit.cx(qubits[0], qubits[1])
circuit.measure(qubits[0], left[0])
circuit.measure(qubits[1], right[0])
pub_result = StatevectorSampler(seed=11).run(
[circuit],
shots=256,
).result()[0]
left_counts = pub_result.data.left.get_counts()
right_counts = pub_result.data.right.get_counts()Do not assume every result has .data.meas. Inspect circuit.cregs or pub_result.data.
An Estimator PUB contains:
- One circuit, normally without final measurements.
- One observable or an array of observables.
- Optional parameter values.
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
observable = SparsePauliOp.from_list(
[
("ZZ", 1.0),
("XX", 0.5),
]
)
estimator = StatevectorEstimator()
pub_result = estimator.run(
[(circuit, observable)]
).result()[0]
expectation_values = pub_result.data.evs
standard_deviations = pub_result.data.stdsSparsePauliOp labels are little-endian with respect to qubit indices: the rightmost label character acts on qubit 0.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
theta = Parameter("theta")
circuit = QuantumCircuit(2)
circuit.ry(theta, 0)
circuit.cx(0, 1)
observable = SparsePauliOp.from_list([("ZI", 1.0), ("XX", 0.5)])
values = [[0.0], [np.pi / 4], [np.pi / 2]]
pub_result = StatevectorEstimator().run(
[(circuit, observable, values)]
).result()[0]
print(pub_result.data.evs)The final axis of values corresponds to list(circuit.parameters).
observables = [
[SparsePauliOp.from_list([("ZZ", 1.0)])],
[SparsePauliOp.from_list([("XX", 1.0)])],
]
pub_result = StatevectorEstimator().run(
[(circuit, observables, values)]
).result()[0]
assert pub_result.data.evs.shape == (2, len(values))Estimator V2 broadcasts observable and parameter arrays. For nontrivial shapes, build a small test first and assert the output shape rather than relying on intuition.
- Sampler controls finite sampling with
shots. - Estimator controls target accuracy with
precision. StatevectorEstimatoris exact at its default precision of zero for supported circuits and Pauli observables.- Runtime may translate requested precision into a shot and randomization budget.
- Runtime twirling settings can affect how shots are allocated.
pub_result = runtime_estimator.run(
[(isa_circuit, isa_observable)],
precision=0.02,
).result()[0]Record requested precision, realized metadata, primitive options, and usage. Do not compare two experiments solely by nominal shot count when mitigation or twirling differs.
Runtime circuits must already satisfy the selected backend's ISA:
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True,
simulator=False,
min_num_qubits=2,
)
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=1,
seed_transpiler=11,
)
isa_circuit = pass_manager.run(circuit)
sampler = Sampler(
mode=backend,
options={"default_shots": 1024},
)
job = sampler.run([isa_circuit])
print(job.job_id())
counts = job.result()[0].data.meas.get_counts()Sampler noise-management options include dynamical decoupling, twirling, execution, environment, and simulator settings. Sampler does not expose Estimator resilience levels.
Map observables through the final circuit layout:
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime import EstimatorV2 as Estimator
observable = SparsePauliOp.from_list([("ZZ", 1.0)])
isa_observable = observable.apply_layout(isa_circuit.layout)
estimator = Estimator(
mode=backend,
options={"resilience_level": 1},
)
job = estimator.run(
[(isa_circuit, isa_observable)],
precision=0.02,
)
print(job.job_id())
pub_result = job.result()[0]
print(pub_result.data.evs, pub_result.data.stds)For a parameterized circuit, transpile once and include values in the PUB:
pub = (isa_circuit, isa_observable, parameter_values)
pub_result = estimator.run([pub], precision=0.02).result()[0]Set options with a dictionary, an options dataclass, direct attributes, or .update():
from qiskit_ibm_runtime import EstimatorOptions, EstimatorV2 as Estimator
options = EstimatorOptions(
resilience_level=2,
resilience={
"zne_mitigation": True,
"zne": {"noise_factors": [1, 3, 5]},
},
)
estimator = Estimator(mode=backend, options=options)
estimator.options.default_precision = 0.02
estimator.options.update(
dynamical_decoupling={
"enable": True,
"sequence_type": "XpXm",
}
)Current Estimator resilience levels are 0, 1, and 2; there is no level 3. Advanced features can be incompatible with each other, especially fractional gates, gate twirling, PEA, PEC, and gate-folding ZNE. Consult the current options guide before combining them.
Do not use the old shared Options() object or .set_options().
from qiskit_ibm_runtime import (
Batch,
EstimatorV2 as Estimator,
SamplerV2 as Sampler,
Session,
)
# Job mode
sampler = Sampler(mode=backend)
job = sampler.run([isa_circuit], shots=1024)
# Batch mode: independent jobs
with Batch(backend=backend, max_time="10m") as batch:
sampler = Sampler(mode=batch)
batch_jobs = [
sampler.run([circuit], shots=1024)
for circuit in isa_circuits
]
# Session mode: iterative jobs; unavailable on the Open Plan
with Session(backend=backend, max_time="20m") as session:
estimator = Estimator(mode=session)
session_jobs = [
estimator.run([pub], precision=0.03)
for pub in iterative_pubs
]Create the primitive with mode=batch or mode=session. Passing the backend instead runs in job mode even inside a context.
Use backend primitives when a provider exposes BackendV2 but no native V2 primitive:
from qiskit.primitives import BackendEstimatorV2, BackendSamplerV2
sampler = BackendSamplerV2(backend=backend)
estimator = BackendEstimatorV2(backend=backend)Provider behavior, result quality, and options differ. Transpile for the backend target and read the provider documentation.
For every result:
- Match each
PubResultto its input PUB. - Use the actual classical-register field for Sampler output.
- Preserve array dimensions for parameter and observable sweeps.
- Store metadata alongside values or counts.
- Store the job ID before blocking on
result(). - Record package versions, backend name, seeds, precision or shots, and all non-default options.
- Treat mitigated expectation values as estimates with method-dependent bias and overhead.
| Old pattern | Current pattern |
|---|---|
Sampler() or Estimator() V1 |
Explicit V2 implementation |
.quasi_dists |
Shot-resolved BitArray, such as .data.meas.get_counts() |
.values |
.result()[i].data.evs |
| Parallel circuit/observable/value lists | One or more PUB tuples |
Shared Options() |
SamplerOptions, EstimatorOptions, dictionaries, or .options.update() |
backend= / session= primitive arguments |
mode= |
| Runtime auto-transpilation | Explicit ISA circuit preparation |
| Unmapped observables | observable.apply_layout(isa_circuit.layout) |
- No
measfield: the circuit has a differently named register or no measurements. - Shape/broadcast error: check
circuit.parameters, the last parameter-value axis, and observable-array shape. - Circuit not ISA-compatible: transpile against the exact backend target before Runtime execution.
- Observable qubit mismatch: apply the circuit layout and confirm the resulting width.
- Unsupported option: options are implementation- and version-specific.
- Session rejected: Open Plan workloads must use job or batch mode.
- Unexpected cost: mitigation, twirling, and precision settings can multiply circuit and shot counts.