Cram sheet — Section 6: Use the estimator primitive
One-screen review layer for this section: short background primers per objective, then the key facts — each backed by an official source or by an executed proof from the question bank.
Set estimator primitive options such as resilience levels
The resilience_level dial: what 0, 1 and 2 buy
resilience_level is a single coarse dial: it abstracts away the detailed choice of method so you can reason about the cost/accuracy trade instead. The validator accepts only 0, 1 and 2, though a string "2" or 2.0 is silently coerced.
| Level | What it applies | Cost |
|---|---|---|
| 0 | no mitigation at all | none |
| 1 (the default) | readout mitigation via Twirled Readout Error eXtinction (TREX): measurement twirling plus an inversion of the diagonalized readout-error transfer matrix | minimal |
| 2 | everything level 1 does, PLUS zero-noise extrapolation and gate twirling | medium; typically reduces bias, but "not guaranteed to be zero-bias" |
There is no level 3, and no level turns on PEC — that is only the individual option resilience.pec_mitigation. The trade never reverses: a higher level post-processes a larger ensemble of related circuits, buying more accurate, less biased expectation values at the cost of longer processing time and more QPU usage. It never makes a job cheaper, faster or shallower, and no level changes which physical qubits you land on.
Options you set yourself are applied IN ADDITION to the level's base set. Level 0 turns zne_mitigation off, yet an explicit resilience.zne_mitigation = True re-enables ZNE. A level is a starting point, never a lock.
Keep the two families straight:
| Family | When it acts | Examples | How you switch it on |
|---|---|---|---|
| error suppression | before or during execution, to stop errors happening | dynamical decoupling pulses idling qubits so their idle-time errors approximately cancel | options.dynamical_decoupling.enable |
| error mitigation | after execution, post-processing an ensemble of circuits to remove bias from expectation values | TREX, ZNE, PEA, PEC | resilience_level or resilience.*_mitigation |
| neither | — | a higher transpiler optimization level; more shots, which shrink the STATISTICAL error, not the systematic bias | — |
Sources: estimator-noise-management · error-mitigation-and-suppression-techniques
The EstimatorOptions tree and its defaults
EstimatorOptions is a nested dataclass, and most option questions are really questions about nesting DEPTH.
| Level | Fields |
|---|---|
| top | max_execution_time, environment, simulator, default_precision, default_shots, resilience_level, seed_estimator, dynamical_decoupling, resilience, execution, twirling, experimental |
resilience | measure_mitigation, measure_noise_learning, zne_mitigation, zne, pec_mitigation, pec, layer_noise_learning, layer_noise_model |
resilience.zne | amplifier, noise_factors, extrapolator, extrapolated_noise_factors |
Three shapes of mistake follow: the WRONG NAME (zne.factors, the Mitiq-flavoured zne.scale_factors), the right name at the WRONG LEVEL, and — nastiest — CONFIGURING WITHOUT ENABLING. Setting resilience.zne.noise_factors is accepted but leaves zne_mitigation at Unset, so the job still runs without ZNE; worse, resilience.zne_mitigation.enable = True raises nothing at all — Unset is a plain singleton, so the write lands harmlessly on it. Only resilience.zne_mitigation = True (or resilience_level = 2) turns ZNE on; TREX is resilience.measure_mitigation = True and PEC is resilience.pec_mitigation = True.
Memorize the documented defaults, since "what if I touch nothing" is a question shape.
| Option | Default |
|---|---|
resilience_level | 1 |
resilience.measure_mitigation | True |
resilience.zne_mitigation, resilience.pec_mitigation | False |
resilience.zne.noise_factors | (1, 3, 5) |
resilience.zne.amplifier | gate_folding |
resilience.zne.extrapolator | (exponential, linear) |
dynamical_decoupling.enable, sequence_type | False, XX |
twirling.enable_gates, twirling.enable_measure | False, True |
default_precision | 0.015625 — exactly 1/sqrt(4096) |
max_execution_time | 10800 seconds |
An option you never touch sits at Unset, meaning the server default applies. Set values three ways: an EstimatorOptions instance or nested dict at construction, assignment down the attribute path, or a bulk options.update(...).
Sources: estimator-options · runtime-options-overview
Must know:
- ZNE runs the circuit at several amplified noise rates (digital gate folding) and extrapolates back to the zero-noise limit — but it "is not guaranteed to produce an unbiased result". — 📖 error-mitigation-and-suppression-techniques
- PEC writes the ideal circuit as a quasi-probability combination of noisy ones: unbiased, but with a sampling overhead scaling quadratically in gamma = sum of |quasi-probabilities|, which itself grows exponentially with circuit depth. — 📖 error-mitigation-and-suppression-techniques
- PEC is documented as incompatible with gate-folding ZNE, with PEA and with fractional gates — enabling
pec_mitigationandzne_mitigationtogether raises a validation error. — 📖 estimator-options - Error-bar length is bought with
default_precision: 0.1 yieldsstdsaround 0.04–0.10 on a fake backend, 0.01 around 0.002–0.009.options.execution.shotsis no route to it —executionholds onlyinit_qubitsandrep_delay. — ⚙️ proven ins6-q041
Traps:
resilience_levelaccepts only 0, 1 and 2 —3,4,5,-1and1.5all raise a pydanticValidationErrorabout the 0..2 range. — ⚙️ proven ins6-q014- The ZNE scale factors live at
options.resilience.zne.noise_factors;zne.factors(wrong name) andresilience.noise_factors(right name, wrong level) are bothno_such_attributeerrors. — ⚙️ proven ins6-q020 default_precisionmust be strictly positive:0raisesValidationErrorwhether assigned or passed toEstimatorOptions(default_precision=0)— the constructor is not an escape hatch. — ⚙️ proven ins6-q031EstimatorV2.run()'s only execution keyword isprecision=;shots=,default_precision=andresilience_level=all raiseTypeError: unexpected keyword argument, andoptions.precisionis aValidationError. — ⚙️ proven ins6-q016
Understand the theoretical background behind the estimator primitive
The Estimator PUB: four positional slots
An Estimator PUB is a tuple of AT MOST FOUR values. The slots are positional, never searched by content, and even a single task must be wrapped in a list.
| Slot | Contents | Note |
|---|---|---|
| 1 | one QuantumCircuit | may contain Parameter objects |
| 2 | an array of one or more observables | anything ObservablesArrayLike: Pauli, SparsePauliOp, PauliList or a bare string |
| 3 | parameter values to bind | its LAST index runs over the circuit's parameters; omit it or pass None when the circuit has none |
| 4 | a target precision (optional) | a float dropped into slot 3 gives ValueError: Length of () inconsistent with last dimension of [0.01] |
result = est.run([(isa, isa_obs, params, 0.01)]) # not est.run((isa, isa_obs))
The output shape is decided entirely by NumPy broadcasting between the observables array and the parameter-value array, and the Estimator returns one expectation value per element of the broadcast shape.
| Observables | Parameter values | evs.shape |
|---|---|---|
| one observable | none | () |
flat list [o1, o2, o3] | none | (3,) |
column [[o1], [o2], [o3]] | none | (3, 1) |
row [[o1, o2, o3]] | none | (1, 3) |
| one observable | (4, 1) | (4,) |
a (4, 1) column | (1, 6) | (4, 6) |
A SparsePauliOp counts as a SINGLE element however many Pauli terms it holds: it returns one number, the coefficient-weighted sum — never a per-term breakdown. Commuting observables share one measurement only when grouped in the SAME PUB; two PUBs mean two measurement bases.
Back comes a PrimitiveResult with one PubResult per PUB. result[0].data is a DataBin whose keys are exactly evs and stds, both real float64; metadata carries only run bookkeeping (target_precision, circuit_metadata).
Sources: primitive-input-output · get-started-with-estimator
Observables, layout alignment, and precision
An n-qubit observable must be written as a weighted sum of tensor products of Paulis — that is what SparsePauliOp stores. Only I and Z Paulis are diagonal in the measurement basis; X and Y terms need a basis change (H for X, S-dagger then H for Y), which the Estimator performs automatically.
| Constructor | Takes | Watch for |
|---|---|---|
SparsePauliOp(['ZZ', 'XX'], coeffs=[1.0, 0.5]) | labels plus coeffs, width inferred from the labels | there is no num_qubits= keyword here |
SparsePauliOp.from_list([('ZZ', 1.0), ('XX', 0.5)]) | (label, coefficient) pairs on FULL-WIDTH labels | mixing widths raises ValueError: The Nth Pauli is defined over k qubits |
SparsePauliOp.from_sparse_list([('Z', [0], 1.0)], num_qubits=3) | (label, qubit_indices, coefficient) triples plus num_qubits | num_qubits is REQUIRED here, and the result is padded with identities to 'IIZ' |
Transpiling pads the circuit out to the backend's full width and may permute your qubits. The observable does not follow along, so align it: isa_observable = observable.apply_layout(isa_circuit.layout). Nothing else rescues it — measure_all(), a string observable, or pinning initial_layout=[0, 1] all leave the circuit at full backend width.
precision is a target STANDARD ERROR on the expectation value, not a shot count, which is why it is a float. Statistical error falls as 1/sqrt(N), so the shot cost goes as 1/precision^2.
| Rank | Where the precision comes from |
|---|---|
| 1 | a precision inside the PUB |
| 2 | run(precision=...) |
| 3 | twirling being on by default, num_randomizations × shots_per_randomization |
| 4 | default_shots |
| 5 | default_precision, documented as 0.015625 = 1/sqrt(4096) |
Two guardrails: the result is not GUARANTEED to reach the requested precision, and lower precision means more QPU time.
Sources: specify-observables-pauli · estimator-options · primitive-input-output
Must know:
- Width mismatch is the signature Estimator bug: a 2-qubit observable against a 5-qubit ISA circuit raises
ValueError: The number of qubits of the circuit (5) does not match the number of qubits of the ()-th observable (2)— onlyobs.apply_layout(isa.layout)fixes it. — ⚙️ proven ins6-q013 SparsePauliOp('ZZ').apply_layout([2, 0], num_qubits=4)returns'IZIZ'— label characters pair with the index list left-to-right while the printed label stays little-endian. — ⚙️ proven ins6-q033- A multi-term
SparsePauliOpis ONE observable:from_list([('X', 3.0)])on |+⟩ returns a 0-dfloat64array holding 3.0 — never a length-1 array, never one value per Pauli term. — ⚙️ proven ins6-q022 - Halving the target precision quadruples the shot budget: the measured standard error falls from 0.0195 at 2500 shots to 0.0104 at 4x the shots, while 2x only reaches 0.0137. — ⚙️ proven in
s6-q017
Traps:
result[0].data.evsholds the estimate(s) andresult[0].data.stdstheir standard errors;data.expectation_valuesis anAttributeErrorandmetadata['evs']aKeyError. — ⚙️ proven ins6-q010- Even a single task must be wrapped in a list:
est.run([(isa, iobs)]). A bare circuit raisesValueError: An invalid Estimator pub-like was given, and the V1 habitrun(circuits=..., observables=...)raisesTypeError. — ⚙️ proven ins6-q012 - The reference
StatevectorEstimatorreportsstdsof exactly0.0— an exact calculation has no sampling uncertainty — even whenrun()asks for a tighter precision. — ⚙️ proven ins6-q037 - Sweeping
ry(theta)over 0 to 2π againstSparsePauliOp('Z')traces ONE full cosine period; cos(theta/2) gives half a period and cos(2*theta) two — the period tells the three apart. — ⚙️ proven ins6-q040
- Recite the level table: 0 no mitigation, 1 the default with TREX, 2 adds ZNE and gate twirling.
- Expect a higher resilience level to cost more processing time and QPU usage, never less.
- Remember an option you set is applied in addition to the level's base set, overriding it.
- Put each option at its depth —
resilience.zne.noise_factorsconfigures ZNE,resilience.zne_mitigation = Trueenables it. - Order an Estimator PUB positionally: circuit, observables, parameter values, precision — wrapped in a list.
- Predict
evs.shapeby broadcasting observables against parameter values; aSparsePauliOpcounts as one element. - Map every observable through
obs.apply_layout(isa.layout)before running on a transpiled circuit. - Treat
precisionas a target standard error: halving it quadruples the shots.
Every fact above is sourced: 📖 links go to official documentation, ⚙️ marks facts observed by executing code against the pinned Qiskit stack (the linked section page shows the proof evidence on its practice questions).