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 is EstimatorV2's expectation-value mitigation (TREX at level 1, ZNE at level 2). The blueprint's 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

Learn each knob's DEPTH, not just its name. Misspelled names are rejected; 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​

Untouched options stay Unset and take the server default, so 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 SamplerOptions object in the constructor, attribute assignment, or a bulk update(); the options object is not read-only. The V1 sampler.set_options(...) does NOT exist.

The documented shots precedence:

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. A local fake backend falls through to 1024; the Runtime service's default is 4096.

Sources: runtime-options-overview · sampler-options

Must know:

  • Settable on SamplerV2: options.default_shots, options.dynamical_decoupling.enable, options.twirling.enable_gates; resilience_level and optimization_level both fail validation. — ⚙️ proven in s5-q020
  • A PUB shot count 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'"); the constructor's options={...} mapping rejects it too. — ⚙️ proven in s5-q014
  • Configuring is not enabling: dynamical decoupling needs options.dynamical_decoupling.enable = True; setting only sequence_type is accepted but leaves enable Unset. — ⚙️ proven in s5-q019
  • The simulator seed lives at options.simulator.seed_simulator; options.execution.seed_simulator, 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 twirling fields; 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 the low-level primitive: it returns the sampled bitstrings shot by shot, with no readout correction. There is no Sampler mitigation to "bypass" by switching it off.

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 counts into "mitigated" values, and mitigation has no Sampler counterpart: there is no single number to rescale, only a histogram. 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

Post-process the BitArray: faster than round-tripping through get_counts().

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

Must know:

  • Post-selection is code you write: a Bell run's raw counts {'00': 1941, '11': 1822, '01': 90, '10': 147} become {'00': 1941, '11': 1822} by dropping odd-parity shots and renormalizing. — ⚙️ proven in s5-q028
  • 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

Traps:

  • BitArray.postselect(indices, selection) takes BIT indices (0 is the rightmost character, as in slice_bits) and selection lists values to KEEP: on 000/111/100/011, postselect([2], [0]) keeps only 000 and 011, still 3 bits. — ⚙️ proven in s5-q045

Understand the theoretical background behind the sampler primitive​

PUBs, shots, and broadcasting​

The Sampler runs a circuit many times, recording the measured register each shot — draws from the circuit's distribution over computational-basis states. It never returns amplitudes or expectation values; probabilities are frequencies you compute, with statistical error shrinking like 1/√N_shots, so halving the error costs four times the shots.

Input is a PUB (Primitive Unified Bloc): a tuple of at most three entries, (circuit, parameter_values, shots), the last two optional. The circuit must contain measurements; 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

Binding follows NumPy broadcasting plus one rule: the parameter array's LAST axis holds the circuit's free parameters and is consumed; the shape before 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 with one entry per PUB, 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)

Only measure_all() creates a register called meas. The per-shot view lets you correlate outcomes across registers before aggregating; a counts dict cannot.

Sources: primitive-input-output

Must know:

  • Shots are per parameter set, not per PUB: 6 parameter values with shots=200 make 6 × 200 = 1200 circuit executions. — ⚙️ proven in s5-q035
  • get_counts() values sum to the shot count (shots=1000 → 1000) however 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 transpiling with a preset pass manager for the backend, not changing the run call. — ⚙️ proven in s5-q013
  • The reference StatevectorSampler in qiskit.primitives accepts abstract (non-ISA) instructions, since it simulates a local statevector — the ISA requirement is a Runtime-primitive rule. — 📖 simulate-with-qiskit-sdk-primitives
  • join_data() puts the FIRST-declared register at the low-index, rightmost end: out (reads 10) declared before anc (reads 1) joins to '110'; join_data(['anc', 'out']) gives '101'. — ⚙️ proven in s5-q044

Traps:

  • result[0].data.meas is a BitArray of per-shot outcomes, not a counts dict; get_counts() gives the summary as a plain dict. — ⚙️ proven in s5-q011
  • QuantumCircuit(2, 2) names its register c, so outcomes are in data.c; meas exists only after measure_all(). — ⚙️ proven in s5-q025
  • get_counts() lists only outcomes that occurred: a 40-shot two-qubit run that never produced 10 gives a three-key dict, so plot_histogram draws three bars — no zero-height bar for the missing outcome. — ⚙️ proven in s5-q041
  • A fake backend is not an exact simulator: a seeded 2000-shot h(0); cx(0,1); x(2) run on FakeManilaV2 hits all eight outcomes, the ideal peaks 100 and 111 holding about 43% and 40% of shots. — ⚙️ proven in s5-q040
  • StatevectorSampler rejects dynamic circuits: an if_test raises QiskitError: 'StatevectorSampler cannot handle ControlFlowOp', a reset or gate after a measurement '... cannot handle mid-circuit measurements'. Use SamplerV2(mode=AerSimulator()) instead. — ⚙️ proven in s5-q052
Exam checklist
  • Place each SamplerV2 knob at its level (dynamical_decoupling, twirling, execution, simulator); SamplerV2 has no resilience_level.
  • Enable a technique explicitly; setting only sequence_type leaves enable Unset.
  • Recite the shots precedence: PUB slot, run(shots=), twirling randomizations, options.default_shots.
  • Hand run() a LIST of PUBs, each at most (circuit, parameter_values, shots), circuits measured.
  • Derive the result shape: the parameter array's last axis binds parameters, 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 (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).