primitives.md
422 lines · skills/qiskit/references/primitives.md at 5f2bc03
V2 Primitives and PUBs
Cloud authentication, QPU, session, and batch snippets are illustrative and were not executed in this refresh. Local evidence is listed in sources.md. 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).
Raw lines 1 to 10Implementations
| 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 client-side Sampler / Estimator (V2 API) | 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.
Sampler PUBs
A Sampler PUB contains:
- One circuit with measurements.
- Optional parameter values.
- Optional per-PUB shots:
(circuit, values, shots).
The core SDK supports per-PUB shots. Runtime 0.50 client-side primitives require equal shot budgets across PUBs in one call; split unequal budgets into separate jobs.
Pass shots at the run() level unless a specific current API requires otherwise.
One Circuit
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().
Multiple Circuits
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.
Parameter Sweep
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.
Multiple Classical Registers
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. Register counts are marginals. Preserve their paired shots with:
joint_counts = pub_result.join_data(["left", "right"]).get_counts()
# Qiskit 2.5.2 BitArray: first listed register occupies least-significant bits.
assert set(joint_counts) <= {"00", "11"}Do not multiply marginal frequencies to estimate cross-register correlations. In executed Qiskit 2.5.2, left=1, right=0 with join_data(["left", "right"]) yields "01". The current API docstring claims the opposite displayed order; the released implementation calls BitArray.concatenate_bits, which puts the first input in the least-significant bits. Keep an asymmetric known-state test when upgrading; Bell 00/11 alone cannot detect reversal.
Estimator PUBs
An Estimator PUB contains:
- One circuit, normally without final measurements.
- One observable or an array of observables.
- Optional parameter values.
- Optional per-PUB precision:
(circuit, observables, values, precision).
Use one precision per Runtime call; heterogeneous budgets are implementation-specific.
Raw lines 138 to 148One Circuit and Observable
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.
Parameter Sweep
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).
Multiple Observables
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.
Raw lines 204 to 219Precision and Shots
- Sampler controls finite sampling with
shots. - Estimator controls target accuracy with
precision. StatevectorEstimatoris exact at precision zero for unitary circuits and Pauli observables; reset can make trajectories stochastic. Positive precision injects synthetic Gaussian noise, not device or shot-level measurement noise. Itsstdsremain zero and are not hardware confidence intervals.- 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.
Raw lines 220 to 236Runtime Sampler V2
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
from qiskit_ibm_runtime.executor_sampler import 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.
Raw lines 237 to 276Runtime Estimator V2
Use an unmeasured preparation circuit; do not reuse the preceding measured Sampler circuit. Map observables through its final layout:
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.executor_estimator import Estimator
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
isa_circuit = generate_preset_pass_manager(backend=backend, optimization_level=1, seed_transpiler=11).run(circuit)
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]Raw lines 277 to 313Runtime Options
Set options with a dictionary, an options_models Pydantic model, direct attributes, or .update():
from qiskit_ibm_runtime.options_models import EstimatorOptions
from qiskit_ibm_runtime.executor_estimator import 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.
Use qiskit_ibm_runtime.options_models with the new client-side primitives. Top-level EstimatorOptions/SamplerOptions belong to the deprecated implementation. Record estimator.finalize_options().model_dump() to include resolved resilience/twirling defaults. Put max_execution_time at the top level; environment.max_execution_time is deprecated.
Do not use the old shared Options() object or .set_options().
Job, Batch, and Session Modes
from qiskit_ibm_runtime import Batch, Session
from qiskit_ibm_runtime.executor_estimator import Estimator
from qiskit_ibm_runtime.executor_sampler import Sampler
# 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.
Adapting a Backend
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)BackendSamplerV2 requires a backend supporting memory=True for shot-resolved output. Provider behavior, result quality, and options differ. Transpile for the backend target and read the provider documentation.
Result Handling Checklist
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.
Migration Traps
| 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() | options_models.SamplerOptions, options_models.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) |
Common Errors
- **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.