Qiskit Runtime & the Primitives — SamplerV2 and EstimatorV2 in 2026

When we first seeded Quantum Genesis, we ran circuits the old way: build a circuit, call execute() or backend.run(), and wait for a Result object with raw counts. It worked, but every run felt like negotiating with the machine directly. Since then the Qiskit SDK has moved to a model built around the primitives — first Sampler/Estimator (V1), now SamplerV2/EstimatorV2 on Qiskit Runtime. This post is the mental model I wish I'd had, with code you can run today.

If you've been following old tutorials and finding that qiskit.execute no longer exists, or that IBMQ.save_account is gone, this is the update you need. The days of manually transpiling and submitting a single circuit to backend.run() for every experiment are over. Modern Qiskit wants you to think in terms of what you want to compute, not how to shuffle bits from the QPU.

Quantum Genesis NFT #29 — run through modern primitives

Why the primitives exist

The old flow forced you to become an expert in the transport layer: pick a backend, submit a job, poll for status, parse a counts dictionary, handle retries. Every experiment re-implemented the same plumbing. The primitives abstract that away and give you two verbs:

  • Sampler — given one or more circuits, return the probability (or the raw counts) of the measurement outcomes. This is your "run the circuit and give me the distribution" tool.
  • Estimator — given circuits plus operator expressions (like a Hamiltonian), return the expectation value. This is your "give me an energy / give me " tool for algorithms like VQE.

The V2 versions tighten this contract. EstimatorV2 accepts circuits with support for parameters and returns richer metadata. SamplerV2 lets you control the number of shots per circuit independently and exposes the results in a way that's easier to consume with qiskit_aer and runtime backends alike.

Installing what you need

pip install qiskit qiskit-ibm-runtime qiskit-aer

qiskit brings the circuit library and transpiler. qiskit-ibm-runtime is the package that talks to IBM's cloud backends through the primitives. qiskit-aer gives you a fast local simulator with the same primitive interface, which is perfect for iterating before you spend real quantum time.

The SamplerV2 workflow, step by step

The key difference from the old API: you pass a list of pub (programs with bindings) rather than a single job, and you get back a structured result rather than a raw counts.

from qiskit import QuantumCircuit
from qiskit_aer.primitives import SamplerV2

# A circuit that produces a simple superposition
qc = QuantumCircuit(1, 1)
qc.h(0)      # Hadamard -> equal superposition of |0> and |1>
qc.measure(0, 0)

# Build the primitive interface
sampler = SamplerV2()

# Run and inspect
result = sampler.run([qc], shots=1000).result()
counts = result[0].data.meas.get_counts()
print(counts)
# {'0': 496, '1': 504}   -- roughly 50/50, as superposition predicts

Notice a few things that changed from the old days:

  1. sampler.run([qc]) — you pass a list of circuits.
  2. result[0] — the result is indexed by pub, in the order you provided them.
  3. result[0].data.meas — each circuit can have multiple classical registers; you address the one you measured into.

For a circuit with a parameter, you use the .bind() argument to provide values:

from qiskit.circuit import Parameter

theta = Parameter("theta")
qc = QuantumCircuit(1, 1)
qc.ry(theta, 0)
qc.measure(0, 0)

from qiskit_aer.primitives import SamplerV2

sampler = SamplerV2()
result = sampler.run(
    [qc],
    parameter_values=[0.0, 3.14159],  # one set of bindings per shot group
).result()

For multiple parameter values you build a list where each entry is the value list for that pub. This is the "vectorized" batch mode that replaced the old parameter_binds loops.

EstimatorV2: getting an expectation value

Where the Estimator shines is when you want ⟨ψ|H|ψ⟩, the expectation value — the quantity every hybrid quantum algorithm needs.

from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer.primitives import EstimatorV2

# Prepare a simple state
qc = QuantumCircuit(2)
qc.h([0, 1])

# A toy Hamiltonian: Z on both qubits
H = SparsePauliOp(["ZZ"])

estimator = EstimatorV2()
job = estimator.run([(qc, H)])
result = job.result()
print(result[0].data.evs)

The pub interface here is (circuit, operator) — you pass the circuit and the observable you want measured. With EstimatorV2 the default variance/overlap metadata is richer than V1, so it's straightforward to extract the expectation value as a float.

Jumping to real IBM hardware

The local Aer simulators share the primitive API with the real thing, so the only change to go to the cloud is the backend you attach to.

from qiskit_ibm_runtime import IBMBackend, SamplerV2 as RuntimeSampler

# Initialize with your IBM Quantum API token
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum", token="YOUR_API_TOKEN")
backend = service.backend("ibm_fez")  # or whatever is available today

sampler = RuntimeSampler(backend)
result = sampler.run([qc], shots=1024).result()

The circuit goes through runtime's built-in transpilation, and the sampler handles the sessions, caching, and error mitigation options under the hood. You no longer pass the backend into transpile(..., backend=...) by hand unless you want fine-grained control.

Sessions: keeping state across calls

For iterative algorithms like VQE, repeatedly spinning up jobs is wasteful. Sessions give you a shared context where multiple primitive calls reuse the same runtime slot:

from qiskit_ibm_runtime import Session

with Session(service=service, backend=backend) as session:
    sampler = RuntimeSampler(session=session)
    e1 = estimator.run([(qc, H)]).result()
    # ... iterate ...
    e2 = estimator.run([(qc2, H)]).result()

Sessions matter for any loop that talks to the backend more than once — the scheduling overhead and latency of cold-starts vanish after the first call. This is the change that quietly made hybrid loops practical on real hardware.

Error mitigation: improving noisy results

The real hardware is noisy, and the primitives make error mitigation a one-line option rather than a research project.

from qiskit_ibm_runtime import SamplerV2 as RuntimeSampler
from qiskit_ibm_runtime.options import SamplerOptions

sampler = RuntimeSampler(backend)
# Several mitigation levels are available to trade cost vs. accuracy
result = sampler.run([qc], shots=4096).result()

Options like measurement-error mitigation, dynamical decoupling, and Pauli twirling are toggled through the sampler's options object. You don't have to hand-roll a mitigation loop, but you do need to understand the trade-offs: more mitigation means more shots and more classical post-processing time.

Migrating old code to the primitives

The single biggest trap in upgrading is reaching for functions that no longer exist. Quick map that covers 95% of real code:

Old (deprecated/removed)New
qiskit.execute(circ)SamplerV2 (or EstimatorV2)
backend.run(circ)a runtime primitive bound to that backend
transpile(circ, backend) before runusually implicit in runtime primitives
IBMQ.save_account() / IBMQ.load_account()QiskitRuntimeService(..., token=...)
result.get_counts(circ)result[n].data.meas.get_counts()
parameter_bindsparameter_values in the pub interface
assemble(...)gone — runtime builds it for you

If a tutorial references qiskit.execute, IBMQ.get_provider, or backend.run with a manually assembled Qobj, it's for a version that has been superseded. The primitives are the present and the near future.

What this means for reproducible art

Back when Quantum Genesis still ran the old way, each of our 82 IBM seeds went through backend.run(). The pipeline worked, but it was brittle: if the backend name changed or the SDK bumped, the whole run script needed surgery. If we were building that same collection today, I'd wrap the seed generation in a SamplerV2 bound to a runtime backend and let the primitive handle transpilation, sessions, and the counts parsing. The art code would only ever touch a clean interface: "give me 4096 shots of this circuit."

Quantum Genesis NFT #57 — Seeded the modern way, on Qiskit Runtime primitives

The primitives are less about new physics and more about a sane engineering boundary between your code and the hardware. Once you're on that boundary, switching between a laptop simulator and a real quantum processor is a one-line change — which is exactly the flexibility a generative-art pipeline wants when it's iterating on tens of circuits at a time.

Comments

Popular posts from this blog

Getting Your Collection Visible on OpenSea, Step by Step

Polygon versus Ethereum for an NFT contract, from the gas bills up

Quantum Error Correction, or Why Your Qubits Forget What They Were Doing