skills/qiskit/references/primitives.md
Qiskit primitives standardize two core tasks:
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:
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.metadata
meas 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:
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.stds
SparsePauliOp 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.
shots.precision.StatevectorEstimator is exact at its default precision of zero for supported circuits and Pauli observables.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:
PubResult to its input PUB.result().| 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) |
meas field: the circuit has a differently named register or no measurements.circuit.parameters, the last parameter-value axis, and observable-array shape.