Skip to main content

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.

LevelWhat it appliesCost
0no mitigation at allnone
1 (the default)readout mitigation via Twirled Readout Error eXtinction (TREX): measurement twirling plus an inversion of the diagonalized readout-error transfer matrixminimal
2everything level 1 does, PLUS zero-noise extrapolation and gate twirlingmedium; 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:

FamilyWhen it actsExamplesHow you switch it on
error suppressionbefore or during execution, to stop errors happeningdynamical decoupling pulses idling qubits so their idle-time errors approximately canceloptions.dynamical_decoupling.enable
error mitigationafter execution, post-processing an ensemble of circuits to remove bias from expectation valuesTREX, ZNE, PEA, PECresilience_level or resilience.*_mitigation
neithera 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.

LevelFields
topmax_execution_time, environment, simulator, default_precision, default_shots, resilience_level, seed_estimator, dynamical_decoupling, resilience, execution, twirling, experimental
resiliencemeasure_mitigation, measure_noise_learning, zne_mitigation, zne, pec_mitigation, pec, layer_noise_learning, layer_noise_model
resilience.zneamplifier, 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.

OptionDefault
resilience_level1
resilience.measure_mitigationTrue
resilience.zne_mitigation, resilience.pec_mitigationFalse
resilience.zne.noise_factors(1, 3, 5)
resilience.zne.amplifiergate_folding
resilience.zne.extrapolator(exponential, linear)
dynamical_decoupling.enable, sequence_typeFalse, XX
twirling.enable_gates, twirling.enable_measureFalse, True
default_precision0.015625 — exactly 1/sqrt(4096)
max_execution_time10800 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_mitigation and zne_mitigation together raises a validation error. — 📖 estimator-options
  • Error-bar length is bought with default_precision: 0.1 yields stds around 0.04–0.10 on a fake backend, 0.01 around 0.002–0.009. options.execution.shots is no route to it — execution holds only init_qubits and rep_delay. — ⚙️ proven in s6-q041

Traps:

  • resilience_level accepts only 0, 1 and 2 — 3, 4, 5, -1 and 1.5 all raise a pydantic ValidationError about the 0..2 range. — ⚙️ proven in s6-q014
  • The ZNE scale factors live at options.resilience.zne.noise_factors; zne.factors (wrong name) and resilience.noise_factors (right name, wrong level) are both no_such_attribute errors. — ⚙️ proven in s6-q020
  • default_precision must be strictly positive: 0 raises ValidationError whether assigned or passed to EstimatorOptions(default_precision=0) — the constructor is not an escape hatch. — ⚙️ proven in s6-q031
  • EstimatorV2.run()'s only execution keyword is precision=; shots=, default_precision= and resilience_level= all raise TypeError: unexpected keyword argument, and options.precision is a ValidationError. — ⚙️ proven in s6-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.

SlotContentsNote
1one QuantumCircuitmay contain Parameter objects
2an array of one or more observablesanything ObservablesArrayLike: Pauli, SparsePauliOp, PauliList or a bare string
3parameter values to bindits LAST index runs over the circuit's parameters; omit it or pass None when the circuit has none
4a 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.

ObservablesParameter valuesevs.shape
one observablenone()
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.

ConstructorTakesWatch for
SparsePauliOp(['ZZ', 'XX'], coeffs=[1.0, 0.5])labels plus coeffs, width inferred from the labelsthere is no num_qubits= keyword here
SparsePauliOp.from_list([('ZZ', 1.0), ('XX', 0.5)])(label, coefficient) pairs on FULL-WIDTH labelsmixing 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_qubitsnum_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.

RankWhere the precision comes from
1a precision inside the PUB
2run(precision=...)
3twirling being on by default, num_randomizations × shots_per_randomization
4default_shots
5default_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) — only obs.apply_layout(isa.layout) fixes it. — ⚙️ proven in s6-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 in s6-q033
  • A multi-term SparsePauliOp is ONE observable: from_list([('X', 3.0)]) on |+⟩ returns a 0-d float64 array holding 3.0 — never a length-1 array, never one value per Pauli term. — ⚙️ proven in s6-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.evs holds the estimate(s) and result[0].data.stds their standard errors; data.expectation_values is an AttributeError and metadata['evs'] a KeyError. — ⚙️ proven in s6-q010
  • Even a single task must be wrapped in a list: est.run([(isa, iobs)]). A bare circuit raises ValueError: An invalid Estimator pub-like was given, and the V1 habit run(circuits=..., observables=...) raises TypeError. — ⚙️ proven in s6-q012
  • The reference StatevectorEstimator reports stds of exactly 0.0 — an exact calculation has no sampling uncertainty — even when run() asks for a tighter precision. — ⚙️ proven in s6-q037
  • Sweeping ry(theta) over 0 to 2π against SparsePauliOp('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 in s6-q040
Exam checklist
  • 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_factors configures ZNE, resilience.zne_mitigation = True enables it.
  • Order an Estimator PUB positionally: circuit, observables, parameter values, precision — wrapped in a list.
  • Predict evs.shape by broadcasting observables against parameter values; a SparsePauliOp counts as one element.
  • Map every observable through obs.apply_layout(isa.layout) before running on a transpiled circuit.
  • Treat precision as 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).