This is the first post in a series working through the modern Qiskit stack (Qiskit 2.4.2, qiskit-aer 0.17.2, qiskit-ibm-runtime 0.47.0, Python 3.12). Qiskit’s 0.x → 1.0 → 2.x transition removed most of what older tutorials teach — execute(), opflow, qiskit.algorithms are all gone — so the series sticks strictly to the current model: every program is map → optimize → execute → analyze, and execution goes through the V2 primitives. Everything below runs locally on AerSimulator through the runtime primitives; the same code targets real hardware by swapping the backend, which is the point of the primitives design.

Full notebooks are on GitHub.

Sampler: distributions over bitstrings#

SamplerV2 answers “what did I measure, and how often?” — so its circuits must contain measurements. The hello-world case is a Bell state:

bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
bell.measure_all()

sampler = Sampler(mode=AerSimulator())
result = sampler.run([bell], shots=4096).result()
result[0].data.meas.get_counts()   # {'00': 2026, '11': 2070}
Bell state counts: only 00 and 11, roughly 50/50 over 4096 shots

Only 00 and 11 ever appear, split 2026/2070 over 4096 shots — the qubits are perfectly correlated, never independent.

Prepending an X gate before the entangler prepares |Ψ⁺⟩ = (|01⟩ + |10⟩)/√2 instead, and the histogram flips to the anti-correlated outcomes:

Ψ⁺ state counts: only 01 and 10

Estimator: expectation values, and why it’s a separate primitive#

EstimatorV2 answers “what is ⟨ψ|O|ψ⟩?” directly. You hand it an observable as a SparsePauliOp and a circuit without measurements; it handles the basis rotations and averaging internally. On the Bell state it returns ⟨ZZ⟩ = 1.0. The state |Φ⁺⟩ is a +1 eigenstate of ZZ.

The more interesting check is on |Ψ⁺⟩, in two bases:

for label in ["ZZ", "XX"]:
    obs = SparsePauliOp(label)
    result = estimator.run([(psiplus, obs)]).result()
    print(label, float(result[0].data.evs))
# ZZ -1.0
# XX  1.0

That pair of numbers is the difference between entanglement and mere classical correlation. A classical 50/50 mixture of 01 and 10 also gives ⟨ZZ⟩ = −1, but its ⟨XX⟩ is 0. Getting ⟨XX⟩ = +1 as well means the correlations survive a change of basis — only the coherent superposition does that.

It also explains why Sampler and Estimator coexist. You could compute ⟨ZZ⟩ from Sampler counts by hand, but X- and Y-basis observables need basis-change circuits, and Estimator does all of that for you, with proper error bars. The variational algorithms later in this series (VQE, QAOA) are “minimize an expectation value,” so Estimator is their native interface; Sampler is for when the bitstrings themselves are the answer, like reading out a QAOA solution. Different questions, different primitives.

GHZ, and a first look at the compiler#

Extending to a 3-qubit GHZ state gives the expected two-outcome histogram — 2051/2045 over 4096 shots:

GHZ counts: only 000 and 111

Before running, I also pushed the GHZ circuit through generate_preset_pass_manager against FakeManilaV2, a snapshot of a real 5-qubit IBM device. The depth grew from 3 to 5 while the 2-qubit gate count stayed at 2, and the layout mapped virtual qubits straight onto physical qubits 0–2 — the circuit already fit Manila’s line topology, so the growth came purely from translating into the device’s native gate set. Circuits that don’t fit the topology pay a much steeper price, which will be tested next in part 1 of this series.