Back to Claude Scientific Skills

V2 Primitives and PUBs

skills/qiskit/references/primitives.md

2.55.011.6 KB
Original Source

V2 Primitives and PUBs

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).

Implementations

ImplementationUse
StatevectorSamplerExact statevector evolution plus finite-shot sampling on the local CPU
StatevectorEstimatorLocal statevector expectation values
Aer SamplerV2 / EstimatorV2High-performance and noisy local simulation
Runtime SamplerV2 / EstimatorV2IBM QPUs and IBM Runtime services
BackendSamplerV2 / BackendEstimatorV2Adapt 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:

  1. One circuit with measurements.
  2. Optional parameter values.

Pass shots at the run() level unless a specific current API requires otherwise.

One Circuit

python
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().

Multiple Circuits

python
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

python
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:

python
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.

Estimator PUBs

An Estimator PUB contains:

  1. One circuit, normally without final measurements.
  2. One observable or an array of observables.
  3. Optional parameter values.

One Circuit and Observable

python
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.

Parameter Sweep

python
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

python
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.

Precision and Shots

  • Sampler controls finite sampling with shots.
  • Estimator controls target accuracy with precision.
  • StatevectorEstimator is 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.
python
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 Sampler V2

Runtime circuits must already satisfy the selected backend's ISA:

python
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.

Runtime Estimator V2

Map observables through the final circuit layout:

python
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:

python
pub = (isa_circuit, isa_observable, parameter_values)
pub_result = estimator.run([pub], precision=0.02).result()[0]

Runtime Options

Set options with a dictionary, an options dataclass, direct attributes, or .update():

python
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().

Job, Batch, and Session Modes

python
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.

Adapting a Backend

Use backend primitives when a provider exposes BackendV2 but no native V2 primitive:

python
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.

Result Handling Checklist

For every result:

  1. Match each PubResult to its input PUB.
  2. Use the actual classical-register field for Sampler output.
  3. Preserve array dimensions for parameter and observable sweeps.
  4. Store metadata alongside values or counts.
  5. Store the job ID before blocking on result().
  6. Record package versions, backend name, seeds, precision or shots, and all non-default options.
  7. Treat mitigated expectation values as estimates with method-dependent bias and overhead.

Migration Traps

Old patternCurrent pattern
Sampler() or Estimator() V1Explicit V2 implementation
.quasi_distsShot-resolved BitArray, such as .data.meas.get_counts()
.values.result()[i].data.evs
Parallel circuit/observable/value listsOne or more PUB tuples
Shared Options()SamplerOptions, EstimatorOptions, dictionaries, or .options.update()
backend= / session= primitive argumentsmode=
Runtime auto-transpilationExplicit ISA circuit preparation
Unmapped observablesobservable.apply_layout(isa_circuit.layout)

Common Errors

  • No meas field: 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.