Day 6 — Estimator & Error Mitigation
Lesson
Syllabus coverage: Section 5 — Use the Estimator Primitive (12%)
Verified against: qiskit 2.5.1, qiskit-ibm-runtime 0.48.0 (all runnable snippets executed locally; account-only snippets marked # not run - requires account and cross-checked against IBM docs guides/error-mitigation-and-suppression-techniques and guides/estimator-noise-management).
1. Estimator concept: expectation values, not counts
The Estimator primitive computes expectation values ⟨ψ|O|ψ⟩ of observables O with respect to the state |ψ⟩ prepared by a circuit.
Contrast with Sampler (Day 5):
| Sampler | Estimator | |
|---|---|---|
| Returns | bitstring samples / counts | expectation values (floats) |
| Circuit must contain measurements? | YES | NO (must NOT) |
| PUB shape | (circuit, param_values, shots) |
(circuit, observables, param_values, precision) |
| Accuracy knob | shots / default_shots |
precision / default_precision |
| Result data | result[0].data.<creg>.get_counts() |
result[0].data.evs and .stds |
Error mitigation options (resilience) |
none | TREX, ZNE, PEC |
Why no measurements? The observable defines what gets measured. The Estimator
internally rotates into the right basis and estimates ⟨O⟩. If you pass a circuit that
contains a measure instruction, it is an error. Verified locally:
# StatevectorEstimator raises:
# QiskitError: 'Cannot apply instruction with classical bits: measure'Minimal working example (verified, runs locally)
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import StatevectorEstimator
qc = QuantumCircuit(2) # NO ClassicalRegister, NO measure
qc.h(0)
qc.cx(0, 1) # Bell state (|00> + |11>)/sqrt(2)
obs = SparsePauliOp("ZZ")
estimator = StatevectorEstimator()
job = estimator.run([(qc, obs)]) # a list of PUBs
result = job.result()
print(result[0].data.evs) # 1.0 (ideal <ZZ> of a Bell state)
print(result[0].data.stds) # 0.0 (exact simulation)
print(result[0].metadata) # {'target_precision': 0.0, 'circuit_metadata': {}}2. Estimator PUB shapes
A Primitive Unified Bloc (PUB) for the Estimator is a tuple:
(circuit, observables) # non-parameterized
(circuit, observables, parameter_values) # parameterized
(circuit, observables, parameter_values, precision) # per-PUB precision
estimator.run(pubs)takes a list of PUBs — even a single PUB must be wrapped:run([(qc, obs)]).precisioncan also be given for the whole run:estimator.run(pubs, precision=0.01).- Precision = target standard error on the expectation value. Smaller precision = more shots.
Observables: SparsePauliOp
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("ZZ") # single Pauli string
H = SparsePauliOp.from_list([("ZZ", 0.5), # weighted sum (Hamiltonian)
("XI", 0.3),
("IX", 0.3)])Pauli strings read left-to-right = highest-to-lowest qubit ("ZI" = Z on qubit 1, I on qubit 0 — the usual Qiskit little-endian convention). Verified: for the Bell state, ⟨ZZ⟩ = 1, ⟨XX⟩ = 1, ⟨ZI⟩ = 0, and ⟨H⟩ above = 0.5.
Plain strings, Pauli objects, and dicts like {"ZZ": 0.5} are also accepted where observables go; SparsePauliOp is the canonical exam answer.
3. THE #1 EXAM TRAP: apply_layout
After transpiling to ISA form, the circuit typically has more qubits (backend width) and your logical qubits sit on different physical qubits. A 2-qubit observable no longer matches a 127-qubit ISA circuit. You must remap the observable:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import EstimatorV2 as Estimator
backend = FakeManilaV2() # 5-qubit fake backend, runs locally
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_circuit = pm.run(qc) # now 5 qubits, has .layout
isa_obs = obs.apply_layout(isa_circuit.layout) # <-- THE step people forget
estimator = Estimator(mode=backend)
result = estimator.run([(isa_circuit, isa_obs)]).result()
print(result[0].data.evs) # ~0.88 (noisy fake backend)Verified failure mode if you skip it:
ValueError: The number of qubits of the circuit (5) does not match the
number of qubits of the ()-th observable (2).
Common exam traps
apply_layoutis trap #1. Transpiled circuit + original observable → qubit-count mismatchValueError. Fix:isa_obs = observable.apply_layout(isa_circuit.layout). It is a method on the observable, taking the circuit's.layout.- Estimator circuits must NOT contain measurements (Sampler circuits MUST).
- Precision vs shots: shots ≈ 1/precision². Verified locally: precision 0.1 → 100 shots, 0.05 → 400 shots, 0.01 → 10,000 shots. Halving precision quadruples shots. Estimator uses
precision/default_precision; Sampler usesshots/default_shots(Sampler options have nodefault_precisionand noresilience— verified by attribute inspection).
4. Broadcasting rules (observables × parameters)
Observables and parameter values inside one PUB broadcast like NumPy arrays.
The result evs array has the broadcast shape.
| observables shape | parameter_values shape | evs shape |
|---|---|---|
| scalar (one obs) | (4, k) → 4 sets of k params |
(4,) |
(3,) (list of 3 obs) |
none | (3,) |
(3, 1) |
(4,) sets |
(3, 4) |
(n,) |
(n,) sets |
(n,) (paired element-wise) |
Verified example — 3 observables × 4 parameter sets:
import numpy as np
from qiskit.circuit import Parameter
theta = Parameter("theta")
pqc = QuantumCircuit(1)
pqc.ry(theta, 0)
params = [[0.0], [np.pi/2], [np.pi], [3*np.pi/2]] # 4 sets of 1 param
obs = [[SparsePauliOp("Z")], [SparsePauliOp("X")], [SparsePauliOp("Y")]] # shape (3,1)
result = StatevectorEstimator().run([(pqc, obs, params)]).result()
print(result[0].data.evs.shape) # (3, 4)
# [[ 1. 0. -1. -0.] <Z> = cos(theta)
# [ 0. 1. 0. -1.] <X> = sin(theta)
# [ 0. 0. 0. 0.]] <Y> = 0 for RY rotationsPassing a flat list of 3 observables (shape (3,)) with 4 parameter sets (shape (4,))
does not produce a 3×4 grid — it fails to broadcast. You need shapes (3,1) and (4,).
5. Results
result = job.result() # PrimitiveResult, indexable per PUB
pub_result = result[0] # PubResult for PUB 0
pub_result.data.evs # ndarray of expectation values
pub_result.data.stds # ndarray of standard errors
pub_result.metadata # dict: target_precision, shots, circuit_metadata...
result.metadata # job-level metadata, e.g. {'version': 2}Verified metadata from a fake-backend run: {'target_precision': 0.05, 'shots': 400, 'circuit_metadata': {}}.
evs/stds are stored in data (a DataBin), one PubResult per input PUB — same
container pattern as Sampler, but with evs/stds instead of a classical-register field.
6. EstimatorV2 options (qiskit-ibm-runtime)
All option paths below verified by instantiating EstimatorV2(mode=FakeManilaV2())
against runtime 0.48.0 and setting/reading each attribute.
from qiskit_ibm_runtime import EstimatorV2 as Estimator
est = Estimator(mode=backend)
# Accuracy
est.options.default_precision = 0.01 # Estimator's native knob
est.options.default_shots = 4096 # alternative; overrides precision if set
# Mitigation preset
est.options.resilience_level = 2 # 0, 1 (default), or 2. Value 3 -> ValidationError
# Fine-grained mitigation (options.resilience.*)
est.options.resilience.measure_mitigation = True # TREX
est.options.resilience.measure_noise_learning.num_randomizations = 32
est.options.resilience.zne_mitigation = True # ZNE
est.options.resilience.zne.noise_factors = (1, 3, 5)
est.options.resilience.zne.extrapolator = "exponential"
est.options.resilience.pec_mitigation = False # PEC (awareness)
# Suppression (also available on Sampler)
est.options.dynamical_decoupling.enable = True
est.options.dynamical_decoupling.sequence_type = "XY4" # e.g. "XX", "XpXm", "XY4"
est.options.twirling.enable_gates = True # Pauli (gate) twirling
est.options.twirling.enable_measure = True # measurement twirling
est.options.twirling.num_randomizations = 32
est.options.twirling.shots_per_randomization = 100Options can also be passed as a dict at construction (verified):
est = Estimator(mode=backend, options={"resilience_level": 1, "default_shots": 4096})Unset options report the sentinel Unset (not None) when inspected.
Resilience levels (Estimator only)
| Level | Name | What it enables | Cost |
|---|---|---|---|
| 0 | No mitigation | Nothing — raw results | cheapest |
| 1 | Minimal mitigation (default) | Measurement (readout) error mitigation via TREX + measurement twirling | small overhead (calibration circuits) |
| 2 | Medium mitigation | Level 1 plus ZNE (and gate twirling) | ~3× overhead (default 3 noise factors) |
- Sampler has no resilience levels or
resilienceoptions at all. - PEC is not part of any resilience level in current runtime — enable it explicitly
with
options.resilience.pec_mitigation = True. resilience_levelis a preset; individualoptions.resilience.*flags give fine-grained control and can be combined.
ZneOptions (verified field names)
ZneOptions fields: amplifier, noise_factors, extrapolator, extrapolated_noise_factors.
noise_factors: sequence ≥ 1, default(1, 3, 5)— circuits are re-run at amplified noise (digital gate folding: replace U with U U†U for factor 3).extrapolator:"linear","exponential"(default behavior uses exponential+linear fallback),"polynomial_degree_k"(e.g."polynomial_degree_2"),"fallback". An invalid string like"cubic"raisesValidationError(verified).amplifier="pea"switches noise amplification to Probabilistic Error Amplification.
Suppression vs Mitigation — know this table cold
| Error SUPPRESSION | Error MITIGATION | |
|---|---|---|
| When it acts | Before/during execution (modifies the circuits/pulses sent to hardware) | Post-processing (classical processing of results; may need extra circuit executions) |
| Techniques | Dynamical decoupling (DD), Pauli twirling | TREX (measurement/readout), ZNE, PEC |
| Option paths | options.dynamical_decoupling.*, options.twirling.* |
options.resilience.* |
| Applies to | Sampler and Estimator | Estimator only |
| Result bias | reduces coherent errors on idle qubits (DD); tailors noise to Pauli channel (twirling) | ZNE: biased estimate possible; PEC: unbiased but exponential sampling overhead |
- DD inserts pulse sequences (net identity) on idle qubits; can hurt dense circuits.
- Twirling sandwiches (two-qubit) gates with random Paulis; converts arbitrary noise into Pauli noise.
- TREX = Twirled Readout Error eXtinction: twirls measurements (X gate + measure + classical bit flip) to diagonalize the readout-error matrix, then inverts it.
- ZNE = run at noise factors 1, 3, 5, ... then extrapolate to zero noise.
- PEC = express ideal circuit as quasi-probability mix of noisy circuits; unbiased, overhead scales with γ² (exponential in depth).
options.resilience.pec.max_overheadcaps it.
Real-hardware pattern (for the exam)
# not run - requires account
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2 as Estimator
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_circuit = pm.run(qc)
isa_obs = obs.apply_layout(isa_circuit.layout)
estimator = Estimator(mode=backend, options={"resilience_level": 2})
job = estimator.run([(isa_circuit, isa_obs)])
print(job.result()[0].data.evs)Note: in local testing mode (fake backends / AerSimulator) resilience_level is accepted
but ignored with a UserWarning: The resilience_level option has no effect in local testing mode.
Shots/precision still work locally.
7. Quick recall list
- Estimator = ⟨ψ|O|ψ⟩; circuit has no measurements; observable =
SparsePauliOp. - PUB:
(circuit, observables[, param_values[, precision]]); run takes a list of PUBs. - ISA workflow: transpile circuit, then
obs.apply_layout(isa_circuit.layout). - Results:
result[0].data.evs,result[0].data.stds,result[0].metadata. - shots ≈ 1/precision².
- Resilience: 0 = none, 1 = TREX (default), 2 = +ZNE. PEC is opt-in only.
- Suppression (DD, twirling) = during execution; Mitigation (TREX, ZNE, PEC) = post-processing.
- ZNE:
noise_factors=(1,3,5),extrapolator∈ linear / exponential / polynomial_degree_k.
8. CODING LABS — predict, then run
Use /home/kartikey_purohit/development/ibm-qiskit/.venv/bin/python. Write your prediction
down before running. Solutions in solution-bank.md.
Lab 1 — Bell-state observable safari
Build the Bell state (H on q0, CX 0→1, no measurements). Using one PUB with the observable
list [ZZ, XX, YY, ZI, IZ, XY], predict all six expectation values, then verify with
StatevectorEstimator. Bonus prediction: what is result[0].data.evs.shape and what does
result[0].data.stds contain for an exact simulator?
Lab 2 — Broadcasting grid
Circuit: single qubit, ry(theta, 0). Observables shaped (3, 1): [[Z], [X], [Y]].
Parameter values: theta ∈ {0, π/2, π, 3π/2} (4 sets). Predict the shape of evs and
the full 3×4 matrix of values (hint: ⟨Z⟩ = cos θ, ⟨X⟩ = sin θ for RY). Then predict what
happens if you pass the observables as a flat list of 3 instead of shape (3,1). Run both.
Lab 3 — The apply_layout gauntlet + precision economics
Transpile the Bell circuit for FakeManilaV2() (opt level 1). (a) Run runtime EstimatorV2
in local mode with the untranspiled observable — predict the exact exception type.
(b) Fix it with apply_layout and predict roughly what ⟨ZZ⟩ will be (noisy!). (c) Run with
precision=0.1, 0.05, 0.01 and predict the shots value reported in
result[0].metadata for each before looking.