Skip to main content

Cram sheet — Section 5: Use the sampler 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 sampler primitive options such as dynamical decoupling

The SamplerOptions tree — and the knob that is not in it

SamplerOptions is a validated pydantic dataclass, so every field name is checked — and SamplerV2 has no resilience_level. That knob belongs to EstimatorV2, where it drives expectation-value mitigation (TREX at level 1, ZNE at level 2). The exam blueprint's own wording, "set sampler primitive options such as resilience levels", is the bait.

LevelFields
topmax_execution_time, environment, simulator, default_shots, dynamical_decoupling, execution, twirling, experimental
dynamical_decouplingenable, sequence_type (XX default, XpXm, XY4), extra_slack_distribution, scheduling_method, skip_reset_qubits
twirlingenable_gates, enable_measure, num_randomizations, shots_per_randomization, strategy
executioninit_qubits, rep_delay, meas_type — no shots, no seed_simulator
simulatornoise_model, seed_simulator, coupling_map, basis_gates

The nested groups are where the questions probe, so learn the DEPTH of each knob and not just its name. Anything misspelled is rejected outright: experimental is an opt-in dict you fill yourself, not a catch-all for unknown names.

Sources: sampler-options · options-sampler-options

Setting options, and where the shot count comes from

Options you never touch stay Unset and take the server default, so your code's behaviour does not drift with the client version.

sampler = SamplerV2(mode=backend, options={"default_shots": 4096})
sampler.options.dynamical_decoupling.enable = True
sampler.options.update(default_shots=1024,
dynamical_decoupling={"sequence_type": "XpXm"})

All three forms are valid: a mapping or a SamplerOptions object in the constructor, per-attribute assignment afterwards, or a bulk update(). The options object is not read-only. What does NOT exist is the V1 habit sampler.set_options(...).

Shots then follow a documented precedence chain, and it is a favourite question.

RankWhere the shot count comes from
1the shot count carried in the PUB's third slot
2the shots= keyword on run()
3with twirling on, num_randomizations × shots_per_randomization
4options.default_shots

shots must be an integer — a one-element list raises TypeError. The default is environment-dependent too: a local fake backend falls through to 1024, while the Runtime service's own default is 4096.

Sources: runtime-options-overview · sampler-options

Must know:

  • What IS settable on a SamplerV2: options.default_shots, options.dynamical_decoupling.enable and options.twirling.enable_gatesresilience_level and optimization_level both fail validation. — ⚙️ proven in s5-q020
  • A shot count inside the PUB beats the option: with options.default_shots = 1000, the PUB (isa, None, 50) runs exactly 50 shots. — ⚙️ proven in s5-q026

Traps:

  • sampler.options.resilience_level = 1 raises a pydantic ValidationError ("Object has no attribute 'resilience_level'"), and passing it through the constructor's options={...} mapping is rejected too. — ⚙️ proven in s5-q014
  • Configuring is not enabling: dynamical decoupling needs options.dynamical_decoupling.enable = True, and setting sequence_type alone is accepted but leaves enable Unset. — ⚙️ proven in s5-q019
  • The simulator seed lives at options.simulator.seed_simulator; options.execution.seed_simulator, a top-level seed_simulator, run(seed=42) and run(options={...}) are all rejected. — ⚙️ proven in s5-q031
  • SamplerV2(mode=backend, options={"default_shots": 4096}) is the constructor form; a "shots" key is rejected, and SamplerV2(shots=...) or (default_shots=...) is a TypeError. — ⚙️ proven in s5-q033
  • twirling_level and readout are not fields of the twirling group — those near-miss names fail validation. — ⚙️ proven in s5-q027

Bypass runtime error mitigations and implement your own

Nothing to switch off — so roll your own

The Sampler is deliberately the low-level primitive: it hands back the sampled bitstrings, shot by shot, with no readout correction applied. You do not "bypass" Sampler mitigation by switching something off, because there is nothing on.

TechniqueWhere it livesWhat it does
dynamical decouplingSampler, options.dynamical_decouplingsuppression: pulse sequences cancel coherent errors on idling qubits
Pauli twirling of gates and/or measurementsSampler, options.twirlingsuppression: randomizes over an ensemble of equivalent circuits so arbitrary noise becomes Pauli noise
TREX, zero-noise extrapolation, PEC, error amplificationEstimator only, options.resilience.* or resilience_levelmitigation: corrects expectation values

Neither suppression option rewrites your counts into "mitigated" values, and mitigation has no Sampler counterpart because there is no single number to rescale — only a histogram. So code the correction yourself, in post-processing.

SchemeHow it worksTrade-off
post-selectiondrop the shots that violate a known symmetry of the ideal output — a Bell circuit can only produce 00 or 11 — then renormalize over what survivescheap and needs no calibration, but only works when the ideal support is known, and it throws information away
assignment-matrix (confusion-matrix) inversionprepare each computational basis state with calibration circuits, record what you read back, assemble the matrix mapping true outcomes to observed ones, and invert it (or least-squares invert it)rigorous; this is what packages such as M3/mthree automate, and the raw per-shot record is exactly their input

Work on the arrays rather than the dictionaries: a BitArray is faster than round-tripping through get_counts().

Sources: sampler-noise-management · error-mitigation-and-suppression-techniques · primitive-input-output

Key facts:

  • Post-selection is something you code: a Bell run's raw counts {'00': 1941, '11': 1822, '01': 90, '10': 147} become {'00': 1941, '11': 1822} by dropping the odd-parity shots and renormalizing. — ⚙️ proven in s5-q028
  • get_bitstrings() returns one entry per shot in the order taken (777 shots → a list of length 777) — the un-aggregated record an assignment-matrix or M3-style correction needs as input. — ⚙️ proven in s5-q029
  • A BitArray supports boolean-mask post-selection on another register, slice_bits, slice_shots, expectation_values for diagonal observables, and concatenate_bits to merge registers. — 📖 primitive-input-output

Understand the theoretical background behind the sampler primitive

PUBs, shots, and broadcasting

The Sampler executes a circuit many times and records the measured register each shot — draws from the distribution the circuit defines over computational-basis states. It never returns amplitudes or expectation values; probabilities are frequencies you compute yourself, and their statistical error shrinks like 1/√N_shots, so halving the error costs four times the shots.

Input is a PUB, a Primitive Unified Bloc: a tuple of at most three entries, (circuit, parameter_values, shots), the last two optional. The circuit must contain measurements, and run() takes a LIST of PUBs.

result = sampler.run([(isa, params, 200)]) # never run(isa)
# (isa, 256, vals) -> TypeError: shots must be an integer
# (isa, vals, 200, 1) -> ValueError: The length of pub must be 1, 2 or 3

Parameter binding follows NumPy broadcasting with one rule bolted on: the LAST axis of the parameter array is reserved for the circuit's free parameters and is consumed during binding, and whatever shape precedes it becomes the result's shape.

Parameter array (circuit has 2 parameters)data.meas.shape
(6, 2)(6,)
(3, 4, 2)(3, 4), indexable as data.meas[i, j]
(5, 3)ValueError: Length of ('p[0]', 'p[1]') inconsistent with last dimension

Transposing, flattening or splitting into one PUB per row all raise that same error; reshaping to (5, 2) is the fix.

Sources: primitive-input-output · primitives · multi-product-formula

Reading a SamplerPubResult

run() returns a PrimitiveResult whose length is the number of PUBs you submitted, in submission order.

Access pathWhat you get
len(result)the number of PUBs submitted — three circuits in one run() give three PUB results
result[0].dataa DataBin holding one BitArray per classical register, named after the register (data.alice, data.bob)
.get_counts()the histogram, as a plain dict
.get_int_counts()the same histogram with the bitstrings decoded as integers ('11' becomes the key 3)
.get_bitstrings()the raw per-shot list, in the order the shots were taken
.get_counts(0) on a broadcast resultthat one parameter set's 500 shots; with no index, get_counts() POOLS every set (4 × 500 = 2000)

measure_all() is what creates a register called meas, which is the only reason data.meas usually works. The per-shot view is the one that lets you correlate outcomes across registers before aggregating, which a counts dict cannot do.

Sources: primitive-input-output

Must know:

  • Shots are per parameter set, not per PUB: broadcasting over 6 values with shots=200 performs 6 × 200 = 1200 circuit executions. — ⚙️ proven in s5-q035
  • get_counts() tallies every shot, so its values sum to the shot count used (shots=1000 → 1000) no matter how many distinct bitstrings appear. — ⚙️ proven in s5-q010
  • Bitstrings are little-endian: qubit 0 is the LAST character, so read b[-1], not b[0] — after x(0) on two qubits the dominant outcome is '01'. — ⚙️ proven in s5-q032
  • Runtime SamplerV2 rejects a non-ISA circuit with IBMInputValueError; the fix is a preset pass manager transpiled for the backend, not a change to the run call. — ⚙️ proven in s5-q013
  • The reference StatevectorSampler in qiskit.primitives still accepts abstract (non-ISA) instructions because it runs a local statevector simulation — the ISA requirement is a Runtime-primitive rule. — 📖 simulate-with-qiskit-sdk-primitives

Traps:

  • result[0].data.meas is a BitArray of per-shot outcomes, not a counts dict; get_counts() is the summarized view and returns a plain dict. — ⚙️ proven in s5-q011
  • QuantumCircuit(2, 2) names its register c, so the outcomes are in data.c; the name meas exists only when the circuit used measure_all(). — ⚙️ proven in s5-q025
  • get_counts() lists only the outcomes that actually occurred: a 40-shot two-qubit run in which 10 never appeared returns a three-key dict, so plot_histogram draws three bars — nothing pads the missing outcome to a zero-height bar. — ⚙️ proven in s5-q041
  • A fake backend is not an exact simulator: a seeded 2000-shot run of h(0); cx(0,1); x(2) on FakeManilaV2 puts weight on all eight outcomes, the two ideal peaks 100 and 111 holding about 43% and 40% of the shots. — ⚙️ proven in s5-q040
Exam checklist
  • Place each SamplerV2 knob at its level: dynamical_decoupling, twirling, execution, simulator — and know SamplerV2 has no resilience_level.
  • Enable a technique explicitly; setting a sub-option such as sequence_type leaves enable Unset.
  • Recite the shots precedence: PUB slot, run(shots=), twirling randomizations, then options.default_shots.
  • Hand run() a LIST of PUBs, each at most (circuit, parameter_values, shots), with measurements in the circuit.
  • Derive the result shape: the parameter array's last axis binds parameters, the leading axes survive.
  • Read outcomes from the register-named BitArray; data.meas exists only after measure_all().
  • Read every bitstring right-to-left — qubit 0 is the last character.
  • Build readout correction yourself, by post-selection or assignment-matrix inversion; the Sampler ships none.

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).