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.
| Level | Fields |
|---|---|
| top | max_execution_time, environment, simulator, default_shots, dynamical_decoupling, execution, twirling, experimental |
dynamical_decoupling | enable, sequence_type (XX default, XpXm, XY4), extra_slack_distribution, scheduling_method, skip_reset_qubits |
twirling | enable_gates, enable_measure, num_randomizations, shots_per_randomization, strategy |
execution | init_qubits, rep_delay, meas_type — no shots, no seed_simulator |
simulator | noise_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.
| Rank | Where the shot count comes from |
|---|---|
| 1 | the shot count carried in the PUB's third slot |
| 2 | the shots= keyword on run() |
| 3 | with twirling on, num_randomizations × shots_per_randomization |
| 4 | options.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.enableandoptions.twirling.enable_gates—resilience_levelandoptimization_levelboth fail validation. — ⚙️ proven ins5-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 ins5-q026
Traps:
sampler.options.resilience_level = 1raises a pydanticValidationError("Object has no attribute 'resilience_level'"), and passing it through the constructor'soptions={...}mapping is rejected too. — ⚙️ proven ins5-q014- Configuring is not enabling: dynamical decoupling needs
options.dynamical_decoupling.enable = True, and settingsequence_typealone is accepted but leavesenableUnset. — ⚙️ proven ins5-q019 - The simulator seed lives at
options.simulator.seed_simulator;options.execution.seed_simulator, a top-levelseed_simulator,run(seed=42)andrun(options={...})are all rejected. — ⚙️ proven ins5-q031 SamplerV2(mode=backend, options={"default_shots": 4096})is the constructor form; a"shots"key is rejected, andSamplerV2(shots=...)or(default_shots=...)is aTypeError. — ⚙️ proven ins5-q033twirling_levelandreadoutare not fields of thetwirlinggroup — those near-miss names fail validation. — ⚙️ proven ins5-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.
| Technique | Where it lives | What it does |
|---|---|---|
| dynamical decoupling | Sampler, options.dynamical_decoupling | suppression: pulse sequences cancel coherent errors on idling qubits |
| Pauli twirling of gates and/or measurements | Sampler, options.twirling | suppression: randomizes over an ensemble of equivalent circuits so arbitrary noise becomes Pauli noise |
| TREX, zero-noise extrapolation, PEC, error amplification | Estimator only, options.resilience.* or resilience_level | mitigation: 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.
| Scheme | How it works | Trade-off |
|---|---|---|
| post-selection | drop the shots that violate a known symmetry of the ideal output — a Bell circuit can only produce 00 or 11 — then renormalize over what survives | cheap and needs no calibration, but only works when the ideal support is known, and it throws information away |
| assignment-matrix (confusion-matrix) inversion | prepare 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 ins5-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 ins5-q029- A
BitArraysupports boolean-mask post-selection on another register,slice_bits,slice_shots,expectation_valuesfor diagonal observables, andconcatenate_bitsto 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 path | What you get |
|---|---|
len(result) | the number of PUBs submitted — three circuits in one run() give three PUB results |
result[0].data | a 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 result | that 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=200performs 6 × 200 = 1200 circuit executions. — ⚙️ proven ins5-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 ins5-q010- Bitstrings are little-endian: qubit 0 is the LAST character, so read
b[-1], notb[0]— afterx(0)on two qubits the dominant outcome is'01'. — ⚙️ proven ins5-q032 - Runtime
SamplerV2rejects a non-ISA circuit withIBMInputValueError; the fix is a preset pass manager transpiled for the backend, not a change to the run call. — ⚙️ proven ins5-q013 - The reference
StatevectorSamplerinqiskit.primitivesstill 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.measis aBitArrayof per-shot outcomes, not a counts dict;get_counts()is the summarized view and returns a plaindict. — ⚙️ proven ins5-q011QuantumCircuit(2, 2)names its registerc, so the outcomes are indata.c; the namemeasexists only when the circuit usedmeasure_all(). — ⚙️ proven ins5-q025get_counts()lists only the outcomes that actually occurred: a 40-shot two-qubit run in which10never appeared returns a three-key dict, soplot_histogramdraws three bars — nothing pads the missing outcome to a zero-height bar. — ⚙️ proven ins5-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 peaks100and111holding about 43% and 40% of the shots. — ⚙️ proven ins5-q040
- Place each SamplerV2 knob at its level:
dynamical_decoupling,twirling,execution,simulator— and know SamplerV2 has noresilience_level. - Enable a technique explicitly; setting a sub-option such as
sequence_typeleavesenableUnset. - Recite the shots precedence: PUB slot,
run(shots=), twirling randomizations, thenoptions.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.measexists only aftermeasure_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).