> *"Learning a framework is mostly learning where things live, and which of its questions you are
Prerequisites
- 1
- 2
- 3
- 4
- 5
- 6
Learning Objectives
- Name Qiskit's major subsystems and say what each one owns, and explain what happened to Terra, Aer, Ignis, and Aqua.
- Describe the circuit data model: instructions, registers, and the DAG the transpiler actually operates on.
- Choose an Aer simulation method appropriate to a circuit, and state each method's scaling limit.
- Decide between the Sampler and Estimator primitives from the question being asked, and justify the choice in terms of shot cost.
- Use EstimatorV2's precision parameter instead of a shot count, and read the returned standard deviation.
- Apply a transpiled circuit's layout to an observable, and explain why omitting it produces a wrong answer rather than an error.
- Structure a variational workload as transpile-once/bind-many, and measure the speedup.
In This Chapter
Chapter 7: Qiskit Architecture
"Learning a framework is mostly learning where things live, and which of its questions you are actually asking."
Overview
Part I used Qiskit. Part II learns it, and this chapter is the map.
Two ideas carry the chapter, and both change how you write code.
The first is the primitives split. Qiskit's modern interface to a quantum computer offers exactly two ways to ask a question: Sampler ("what outcomes do I get?") and Estimator ("what is the expectation value of this observable?"). Chapter 5 §5.5 showed why this matters — an expectation value costs $O(1/\epsilon^2)$ shots regardless of qubit count, while a full distribution costs exponentially more. Choosing the wrong primitive is the most expensive mistake in this book, and it is easy to make because both of them run and both return plausible numbers.
The second is that transpilation is a fixed cost you should pay once. A variational algorithm evaluates the same circuit structure hundreds of times with different parameter values. Transpiling each time is the natural way to write it and is roughly an order of magnitude slower than the alternative. §7.7 measures the difference and turns it into the pattern that Chapter 24's optimizer depends on.
There is also a trap in this chapter that produces wrong answers rather than errors, and it catches essentially everyone once: when you transpile a circuit, you must apply its layout to your observable, or the Estimator will happily measure the wrong qubits and return a confident, wrong number. §7.6 shows it.
In this chapter, you will learn to:
- Name Qiskit's subsystems and what each owns, and understand the Terra/Aer/Ignis/Aqua history.
- Describe the circuit data model and the DAG the transpiler works on.
- Choose an Aer simulation method, and state its scaling limit.
- Decide between Sampler and Estimator from the question, not from habit.
- Use precision rather than shots, and read the returned standard deviation.
- Apply a layout to an observable, and understand why omitting it fails silently.
- Structure work as transpile once, bind many, and measure the speedup.
Learning Paths
How to read this chapter by track. - 🔰 Beginner — §7.1, §7.2, §7.5, and §7.6. The rest is reference until you need it. - 🔬 Researcher — §7.6 and §7.7. The precision parameter and the layout trap are the two things most likely to silently corrupt a published number. - 🤖 Quantum ML — §7.6 and §7.7 are your chapter. Every training loop is an Estimator call inside a bind-many pattern, and getting this structure wrong costs an order of magnitude. - 🏗️ Quantum Engineer — all of it. §7.3's simulator methods and §7.8's session model are operational knowledge you will be asked for. - 🔐 Security — skim §7.1 and §7.5 for vocabulary; the rest is not on your path.
7.1 What "Qiskit" Names Today
Ask what Qiskit is and you get different answers depending on when the person last used it. The history is worth two paragraphs because you will encounter all of it in older material.
The old structure (roughly 2018–2021) divided Qiskit into four named elements:
| Element | What it was | Where it went |
|---|---|---|
| Terra | circuits, transpiler, core | became qiskit itself |
| Aer | simulators | separate package qiskit-aer |
| Ignis | noise characterization and mitigation | deprecated; work moved to qiskit-experiments and into Runtime |
| Aqua | algorithms and applications | split up into qiskit-nature, qiskit-optimization, qiskit-algorithms, etc. |
The current structure is simpler and it is what §2.2 introduced: qiskit is the core, and
execution paths are separate packages.
import pkgutil, qiskit
print(sorted(m.name for m in pkgutil.iter_modules(qiskit.__path__)
if not m.name.startswith("_")))
['capi', 'circuit', 'compiler', 'converters', 'dagcircuit', 'exceptions',
'passmanager', 'primitives', 'providers', 'qasm2', 'qasm3', 'qpy',
'quantum_info', 'result', 'synthesis', 'transpiler', 'user_config',
'utils', 'version', 'visualization']
The ones you will actually use:
| Module | What it owns | First met |
|---|---|---|
qiskit.circuit |
QuantumCircuit, gates, Parameter, the circuit library |
Ch. 2 |
qiskit.quantum_info |
Statevector, Operator, SparsePauliOp, fidelity, partial trace |
Ch. 3, 4, 5 |
qiskit.transpiler |
passes, pass managers, layout, routing | Ch. 2, and Ch. 10 in depth |
qiskit.primitives |
the reference Sampler and Estimator (local, exact) | Ch. 5 |
qiskit.qasm2 / qasm3 |
serialization | Ch. 6 |
qiskit.qpy |
binary serialization | Ch. 6 |
qiskit.visualization |
plot_histogram, plot_bloch_multivector, drawers |
Ch. 2, 3 |
qiskit.dagcircuit |
the DAG the transpiler operates on | §7.2 |
qiskit.synthesis |
unitary decomposition | Ch. 3 CS2, Ch. 28 |
And the separate packages:
| Package | Role |
|---|---|
qiskit-aer |
high-performance local simulation, including noise models |
qiskit-ibm-runtime |
IBM hardware: auth, backends, primitives, jobs, sessions |
qiskit-nature, qiskit-optimization, qiskit-algorithms |
application and algorithm layers (Ch. 24, 36, 37) |
qiskit-experiments |
characterization and calibration routines (Ch. 30) |
🗝️ Version Note — Why old tutorials import things that do not exist.
Four import patterns from the old structure, and their fates:
python from qiskit import Aer # ✗ -> from qiskit_aer import AerSimulator from qiskit import execute # ✗ -> backend.run(transpile(...)) or a primitive from qiskit.aqua.algorithms import VQE # ✗ -> from qiskit_algorithms import VQE from qiskit.ignis.mitigation import ... # ✗ -> Runtime resilience options, or qiskit-experimentsIf a tutorial imports
AquaorIgnis, it predates 2021 and essentially none of its code will run. If it importsAerfromqiskit, it predates 1.0. Check the imports before you check your environment — the import line dates the material more reliably than anything else on the page.
7.2 The Circuit Data Model
A QuantumCircuit is not a list of gates, quite. Understanding what it actually is explains several
behaviors that otherwise look arbitrary.
from qiskit import QuantumCircuit
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
for instruction in qc.data:
print(f" {instruction.operation.name:<8} qubits={[qc.find_bit(q).index for q in instruction.qubits]}"
f" clbits={[qc.find_bit(c).index for c in instruction.clbits]}")
h qubits=[0] clbits=[]
cx qubits=[0, 1] clbits=[]
measure qubits=[0] clbits=[0]
measure qubits=[1] clbits=[1]
Each entry is a CircuitInstruction: an operation plus the qubits and classical bits it acts on.
The operation itself (instruction.operation) carries the name, the parameters, and — for composite
gates — a definition circuit.
Three consequences:
Bits are objects, not indices. qc.find_bit(q).index converts back. This is why composing
circuits with different registers works, and why the transpiled circuits in Chapter 6 could refer to
physical qubits — the bit objects were remapped.
Composite gates carry their own definitions. A Toffoli is one instruction with a definition
that expands to six CNOTs (Chapter 4 §4.6). The expansion happens during transpilation, not at
construction — which is why qc.count_ops() on your circuit and on the transpiled circuit report
such different things.
The transpiler does not work on this structure. It converts to a DAG first.
The DAG
from qiskit.converters import circuit_to_dag
dag = circuit_to_dag(qc)
print(f" operation nodes: {len(list(dag.op_nodes()))}")
print(f" depth: {dag.depth()}")
for layer_index, layer in enumerate(dag.layers()):
names = [node.op.name for node in layer["graph"].op_nodes()]
print(f" layer {layer_index}: {names}")
operation nodes: 4
depth: 3
layer 0: ['h']
layer 1: ['cx']
layer 2: ['measure', 'measure']
A directed acyclic graph whose nodes are operations and whose edges are the qubits and bits
flowing between them. This is the right representation for a compiler because it makes the real
dependencies explicit: the two measure operations are in the same layer because neither depends on
the other, so they can execute simultaneously.
That is also the definition of depth: the length of the longest path through the DAG, which is the number of time steps the circuit needs. Depth, not gate count, is what your coherence budget is spent on.
You will rarely manipulate a DAG directly until you write a custom transpiler pass (Chapter 10 §10.8). Knowing it exists explains why the transpiler can reorder commuting operations and why "depth" is a graph property rather than a count.
Why a graph and not a list
The conversion looks like bookkeeping. It is not. A list of instructions asserts dependencies that do not exist, and an optimizer that believes them cannot optimize.
qc.data is a total order: instruction 3 follows instruction 2 follows instruction 1, whether or
not any of them touch the same qubit. A DAG is a partial order — an edge exists only where one
operation genuinely consumes a wire that another produced. Anything with no directed path between it
is free to move.
The Bell circuit above already shows the gap. Four instructions in qc.data, in one fixed sequence,
against a DAG depth of 3, because the two measure operations are unordered with respect to each
other. The list says four steps; the graph says three; the graph is right.
The gap widens with width. §7.7's four-qubit ansatz is 25 operations:
as written ry 16 + cx 9 = 25 operations
DAG depth 11 layers
Twenty-five operations, eleven time steps. The other fourteen overlap with something else, and only the graph can say which. Against a coherence budget measured in microseconds (Chapter 12 §12.3), that factor of 2.3 is the difference between a circuit that fits and one that does not.
Commutation has to be looked for
Independence is the easy case: two gates on disjoint qubits obviously do not interact, and the wire structure says so. Commutation is the hard case, and it is where the real optimization lives. Two gates can share a qubit and still be reorderable, and nothing about the wiring reveals it — you have to know the algebra.
The smallest example that matters: $Z$ on a CNOT's control commutes through it, and $X$ on the
target commutes through it, so cx; z; cx is just z. Measured, running CommutationAnalysis
followed by CommutativeCancellation:
cx-z-cx before {'cx': 2, 'z': 1} depth 3
after {'z': 1} depth 1
cx-x-cx before {'cx': 2, 'x': 1} depth 3
after {'x': 1} depth 1
Both CNOTs vanish in both cases — and nothing in the wire structure suggested they could. All three operations touch the shared qubit, so the dependency graph is a chain of length 3, and a pass that cancels only adjacent inverses finds nothing to do. The two CNOTs survive an optimizer that does not know the identity.
That is why Qiskit ships a dedicated CommutationAnalysis pass whose entire job is to work out,
pairwise, which neighbouring operations commute — and to record the answer somewhere a later pass can
find it. §7.4's property set is that recording mechanism.
Chapter 28
§28.5 derives the underlying identity and measures what the mechanism is worth on real circuits.
⚙️ Under the Transpiler — commutation is an analysis result, not a rewrite.
CommutationAnalysischanges nothing about the circuit. It walks the DAG, tests neighbouring operations for commutativity, and deposits its findings under the keycommutation_set. On the three-gate circuit above that set has 11 entries — far more than the circuit has gates, because it is keyed both by wire (2 entries, one list of commuting sets per qubit) and by(node, wire)pair (9 entries, counting the DAG's input and output boundary nodes).CommutativeCancellationthen reads that set and does the rewriting.The split is deliberate, and it is the transpiler's central design decision: anything expensive enough to be worth computing once is computed by a pass that only computes, and is stored where every later pass can reach it. Cancellation, gate reordering, and scheduling can all consume the same commutation analysis without recomputing it.
The cost of not having it is the measurement above. A CNOT is the most expensive thing you can leave lying around on hardware, and here the naive pipeline leaves two in a circuit that needs zero.
📐 Math Aside — why depth is the longest path, and why it is the number that matters.
Give each operation node $v$ a start time $s(v)$. The only constraint is that an operation cannot start until every operation feeding one of its wires has finished:
$$s(v) \;=\; \max_{u \to v}\big(s(u) + 1\big), \qquad s(v) = 0 \text{ for sources}$$
On a DAG this recursion has a unique solution — that is exactly what "acyclic" buys you — and its maximum over all $v$ is by construction the number of edges on the longest directed path. Depth is not a naming convention; it is the earliest possible finishing time of the circuit, assuming anything unordered runs at once.
Two consequences follow. First, depth is a lower bound on execution time that hardware need not achieve, because real gates have unequal durations — Chapter 31 measures the gap. Second, the only way to reduce depth is to remove an operation from the critical path. Deleting a gate that was already running in parallel with something longer reduces the gate count and changes the depth by nothing. That is why "we removed 30% of the gates" and "we removed 30% of the depth" are different claims, and why Chapter 28 always reports both.
7.3 Aer
qiskit-aer is not one simulator. It is several, with very different scaling.
from qiskit_aer import AerSimulator
print(sorted(AerSimulator().available_methods()))
['automatic', 'density_matrix', 'extended_stabilizer', 'matrix_product_state',
'stabilizer', 'statevector', 'superop', 'unitary']
| Method | Represents | Scales to | Use when |
|---|---|---|---|
statevector |
the exact pure state, $2^n$ amplitudes | ~30 qubits | default; exact results on small circuits |
density_matrix |
a mixed state, $4^n$ entries | ~15 qubits | noise, decoherence, partial traces |
stabilizer |
Clifford circuits only | thousands | error correction (Ch. 25), benchmarking (Ch. 30) |
extended_stabilizer |
Clifford + a few T gates | hundreds, with cost growing in T count | near-Clifford circuits |
matrix_product_state |
entanglement-limited states | hundreds, if entanglement stays low | 1-D circuits, shallow ansätze |
unitary |
the full $2^n \times 2^n$ matrix | ~15 qubits | verifying a circuit's operator |
superop |
the full quantum channel | ~7 qubits | noisy-channel analysis |
automatic picks for you, and picks reasonably.
The stabilizer entry is the surprising one. Chapter 1 §1.4 said 50 qubits is out of reach for exact simulation — and that is true for general circuits. Circuits built only from Clifford gates (H, S, CNOT, and the Paulis) are efficiently simulable at thousands of qubits, by the Gottesman–Knill theorem.
A GHZ chain is Clifford, so it makes the contrast measurable:
n | statevector | stabilizer | ratio
---|-------------|-------------|---------
16 | 14.4 ms | 5.1 ms | 2.8x
20 | 9.3 ms | 5.6 ms | 1.7x
24 | 147.2 ms | 6.7 ms | 22.1x
26 | 598.5 ms | 7.2 ms | 83.1x
Statevector time doubles with every qubit; stabilizer time barely moves. And past the point where statevector stops being an option at all:
n= 100: stabilizer 17.6 ms statevector would need ~2e+07 YiB
n= 500: stabilizer 570.6 ms statevector would need ~4e+127 YiB
n= 1000: stabilizer 4094.5 ms statevector would need ~1e+278 YiB
A thousand-qubit entangled state, simulated exactly, in four seconds. Both outcomes
(000…0 and 111…1) appear, correctly, with no approximation.
This is not a contradiction of Chapter 1. Clifford circuits generate entanglement but not the kind that requires exponential resources — and the flip side is that they cannot achieve quantum advantage on their own. Anything you can simulate this easily, you can also just simulate. Adding T gates is what breaks the classical simulation, which is exactly why T-count is the currency of fault-tolerant resource estimation (Chapter 23).
The practical consequence is large: error-correction simulation is tractable because stabilizer codes are Clifford, which is why Chapter 25 can run codes on hundreds of qubits on your laptop.
Chapter 11 is entirely about these methods and
their limits. For now: know that statevector is not your only option, and that reaching for
stabilizer or matrix_product_state when the circuit structure allows it is the difference between
simulating 25 qubits and simulating 250.
7.4 The Transpiler, in Outline
Chapter 10 covers this properly. The outline you need to read §7.6 and §7.7:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(optimization_level=1, backend=backend, seed_transpiler=42)
isa_circuit = pm.run(qc)
A pass manager is an ordered sequence of passes, grouped into stages:
| Stage | What it decides |
|---|---|
| init | preliminary normalization |
| layout | which physical qubit each logical qubit maps to |
| routing | where to insert SWAPs so two-qubit gates act on connected qubits |
| translation | rewriting into the device's basis gates |
| optimization | cancelling, merging, and simplifying |
| scheduling | inserting delays and timing information (optional) |
The output is an ISA circuit — "Instruction Set Architecture," meaning a circuit expressed entirely in operations the target device can execute, on physical qubits it actually has.
The primitives require ISA circuits. Submitting an untranspiled circuit to a hardware primitive
is the TranspilerError from Chapter 2 §2.8.
Two kinds of pass, and the blackboard between them
Every pass in Qiskit is one of exactly two things, and the distinction is enforced by the base class it inherits from.
AnalysisPass |
TransformationPass |
|
|---|---|---|
| Returns | nothing | a new DAG |
| May change the circuit | no | yes |
| Communicates by | writing to the property set | rewriting the DAG |
| Examples | VF2Layout, CheckMap, Depth, CommutationAnalysis |
SabreSwap, BasisTranslator, Optimize1qGatesDecomposition |
The property set is a plain dictionary that lives on the pass manager and persists across the
whole run. It is the only channel a pass has to tell a later pass something. VF2Layout computes a
layout and writes it; ApplyLayout reads it and rewrites the DAG. Nothing is passed as an argument;
everything goes on the blackboard.
Counting the passes in the preset pipelines makes the architecture concrete. Against
FakeSherbrooke on Qiskit 2.5.1:
level | analysis | transform | total
------|----------|-----------|-------
0 | 10 | 14 | 24
1 | 23 | 27 | 50
2 | 21 | 35 | 56
3 | 18 | 38 | 56
Two things worth reading here. Levels 2 and 3 run the same number of passes — 56 each — and differ in the mix, not the length: level 3 trades three analysis passes for three more transformations. That the pipeline stops growing after level 2 is consistent with Chapter 28's headline result: levels 2 and 3 differ in only 14 of 40 circuit-seed pairs.
And level 1 is the most analysis-heavy pipeline of the four, at 23 analysis passes against level
3's 18. That is not an accident of counting: level 1's strategy is to try cheap things and check
whether they were good enough, and checking is analysis. Its layout stage alone runs five analysis
passes — SetLayout, TrivialLayout, CheckMap, VF2Layout, FullAncillaAllocation — before any
rewriting happens, and its optimization loop terminates on four FixedPoint analysis passes where
level 3 uses the transformation pass MinimumPoint. Cheap pipelines are not pipelines that do less
thinking; they are pipelines that think in order to avoid work.
★ Level 0 is not "no optimization" — it is 24 passes. Basis translation, layout, and routing are correctness work, not optimization, and every level pays for them.
Here is the property set after a level-3 run on the two-qubit Bell circuit — 19 keys, and worth reading as a description of what the pipeline decided:
layout Layout {60: q[0], 61: q[1]}
original_qubit_indices dict the inverse map
VF2Layout_stop_reason enum SOLUTION_FOUND
VF2PostLayout_stop_reason enum SOLUTION_FOUND
routing_not_needed bool True
all_gates_in_basis bool True
is_direction_mapped bool False
size int 14
depth int 9
num_input_qubits int 2
contains_delay / _if_else / _for_loop / _while_loop / _switch_case / _box
optimization_loop_minimum_point (+ its state object)
reschedule_required bool False
routing_not_needed = True is the pipeline telling you it found a layout so good that no SWAP was
required, which is the outcome Chapter 29's hardware-aware work is chasing. size and depth are
there because MinimumPoint uses them to decide when the optimization loop has stopped improving —
an analysis pass measuring the thing a control-flow construct is watching.
⚙️ Under the Transpiler — the property set is why a pass manager is not just a list of functions.
If passes only transformed circuits, a pass manager would be
reduce(compose, passes)and no architecture would be needed. The property set is what makes it a pipeline rather than a chain: passes cooperate through shared state, so expensive analyses are computed once and consumed many times, and control-flow constructs likeDoWhileControllercan look at a property (size,depth,optimization_loop_minimum_point) to decide whether to run the loop body again.It is also the main hazard when you write your own pass (Chapter 10 §10.8). An analysis pass that mutates the DAG, or a transformation pass that forgets to invalidate a property it made stale, produces a pipeline that works on your test circuit and fails on someone else's. The base class you inherit from is the contract; honour it.
What an ISA circuit actually is
"Expressed in operations the target can execute" understates how much changes. Three things do, and all three matter later.
as written ISA (FakeSherbrooke, level 3)
Bell h 1, cx 1 depth 2, 2 qubits rz 8, sx 5, ecr 1 depth 9, 127 qubits
Toffoli ccx 1 depth 1, 3 qubits rz 37, sx 25, ecr 9, x 1 depth 48, 127 qubits
ansatz ry 16, cx 9 depth 11, 4 qubits rz 84, sx 56, ecr 9 depth 56, 127 qubits
The gate set changes. There is no h, no cx, no ry in the output — this device's basis is
rz, sx, x, and ecr. A Hadamard is not one gate here; it is a sequence of them.
The width changes. A two-qubit Bell circuit becomes a 127-qubit circuit, because an ISA circuit is addressed in physical qubits and the device has 127 of them. The other 125 are idle wires. This is the fact §7.6's trap is built on: your observable is two qubits wide and your circuit is 127.
The depth changes, sometimes enormously. One ccx instruction becomes depth 48. Chapter 4 §4.6's
"six CNOTs" was the count in an idealized basis; on this device the same gate costs 9 two-qubit
operations and 63 single-qubit ones. The cost of a gate is a property of the device, not of the
gate.
And one thing that does not change: the ansatz's 16 free parameters survive transpilation intact. That is not incidental — it is the entire mechanism behind §7.7. The transpiler compiles structure; parameters are values, and values are bound afterwards.
📊 What the Numbers Say — more optimization made this circuit deeper.
The same ansatz, same backend, same seed, at two optimization levels:
text level 1 rz 62, sx 32, ecr 9, x 3 depth 40 layout [0, 1, 2, 3] level 3 rz 84, sx 56, ecr 9 depth 56 layout [124, 125, 126, 112]Level 3 produced 40% more depth and 43 more single-qubit gates (97 against 140), and kept the two-qubit count identical at 9. It is not malfunctioning. Both layouts are connected 3-edge chains, but they are not equally good chains — the mean
ecrerror along each, straight from theTarget:
text level 1 [0, 1, 2, 3] 0.007494 0.008788 0.008695 mean 0.008326 level 3 [124, 125, 126, 112] 0.006303 0.004630 0.004596 mean 0.005177Level 3's path is 1.61× cleaner on the gates that dominate the error budget. The extra single-qubit gates are the price of its heavier synthesis passes, and single-qubit errors on this device run two orders of magnitude below
ecrerrors (1.07e-04 against 3.47e-03 at best).Depth is not the objective; fidelity is, and the two disagree here. Do not read a depth increase as a regression without checking the metric that actually matters — Chapter 29 measured a hardware-aware level-1 layout at 0.9116 against a naive level-3 layout at 0.7720, which is the same lesson pointing the other way. Chapter 28 §28.4 is the full treatment.
Target: the device model everything compiles against
The pass manager needs to know what the device can do. That knowledge lives in one object,
backend.target, and it is worth opening because most of the surprises in Part V are visible in it.
target = backend.target
print(target.num_qubits, sorted(target.operation_names))
print(target.instruction_supported("cx"), target.instruction_supported("ecr"))
For FakeSherbrooke:
num_qubits 127
operation names delay, ecr, for_loop, id, if_else, measure,
reset, rz, switch_case, sx, x
instruction entries 1,036
coupling-map edges 144
dt 2.2222e-10 s
granularity 16
h, cx, and t are all absent, and target.instruction_supported() returns False for each.
This is the whole reason a translation stage exists, and it is why the ISA table above looks the way
it does.
The 1,036 instruction entries are the interesting number. A Target is not a list of gate names;
it is a mapping from (operation, qubit tuple) to calibrated properties — a duration and an error
rate for that gate on those specific qubits:
operation entries error min error max duration
ecr 144 3.470e-03 1.000e+00 341.3 - 881.8 ns
sx / x / id 127 1.074e-04 1.333e-02 56.9 ns
rz 127 0.000e+00 0.000e+00 0.0 ns
measure 127 2.930e-03 5.000e-01 1,216.0 ns
reset 127 (no error data) 1,272.9 - 1,400.9 ns
Three readings, each of which a later chapter turns into a result.
rz costs nothing — zero error, zero duration. It is a virtual Z: the control electronics
implement it by shifting the phase of every subsequent pulse rather than emitting one. Chapter 31
measures this directly and it explains why the ISA circuits above are full of rz and nobody minds.
Nine of the 144 ecr links have error exactly 1.000. Those are dead couplers, and they are the
reason a device-wide average error rate is a misleading summary. Excluding them, the live links run
min 0.00347, median 0.00750, max 0.11736 — the numbers Chapter 28 §28.3 uses to predict circuit
fidelity and Chapter 29 uses to pick qubits.
Measurement is the slowest thing on the chip by a factor of more than twenty — 1,216 ns against 56.9 ns for a single-qubit gate — and readout error runs from 0.293% to a coin flip. Chapter 13's readout mitigation exists because of that second column.
📊 What the Numbers Say — a
Targetis a snapshot, not a specification.Every error and duration in that table came from a calibration run, and IBM recalibrates on a schedule.
FakeSherbrookefreezes one particular day, which is exactly what makes it useful for a book: the numbers above are reproducible on your machine and will still be reproducible in a year.The corollary is that a real device's
Targetis different tomorrow, and a layout you chose from yesterday's snapshot may sit on a coupler that has since drifted or died. Chapter 12 §12.1.1 turns that into a procedure; Chapter 29 measures what it is worth (a hardware-aware layout at optimization level 1 scored 0.9116 against a naive level-3 layout's 0.7720).Read the Target before you read the docs. A device page tells you what a machine is; the
Targettells you what it was when it was last measured, which is the thing your circuit will actually run on.
7.5 The Primitives
Here is the modern interface to a quantum computer, and it has exactly two shapes.
Sampler — "what outcomes do I get?"
from qiskit.primitives import StatevectorSampler
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
sampler = StatevectorSampler(seed=1234)
result = sampler.run([(qc,)], shots=4096).result()
print(dict(sorted(result[0].data.meas.get_counts().items())))
{'00': 2012, '11': 2084}
Input: circuits with measurements. Output: counts, per classical register.
Estimator — "what is $\langle\psi|H|\psi\rangle$?"
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1) # NO measurements
H = SparsePauliOp.from_list([("ZZ", 1.0), ("XX", 1.0)])
estimator = StatevectorEstimator(seed=1234)
result = estimator.run([(qc, H)]).result()
print(float(result[0].data.evs))
1.9999999999999996
Input: circuits without measurements, plus observables. Output: one real number per observable.
That $2.0$ is Chapter 5's $\langle ZZ\rangle + \langle XX\rangle = 1 + 1$ — the entanglement witness from Chapter 4's Case Study 2, computed in three lines instead of twelve.
⚠️ Common Pitfall — Measurements belong to the Sampler, not the Estimator.
An Estimator circuit must not contain measurements. The Estimator adds the basis rotations and measurements itself, per observable, because different Pauli terms need different bases (Chapter 5 §5.6).
Passing a measured circuit to an Estimator raises. Passing an unmeasured circuit to a Sampler gives you the "no counts" error from Chapter 2 §2.8. The presence or absence of
measureis how you tell which primitive a circuit was written for, and it is worth naming your builder functions accordingly.
The Pub
Both primitives take a list of Pubs — "Primitive Unified Blocs" — which are tuples:
sampler.run([(circuit,), (circuit2,)]) # Sampler pub: (circuit,)
estimator.run([(circuit, observable), (circuit, obs2)]) # Estimator pub: (circuit, observable)
estimator.run([(circuit, observable, parameter_values)]) # with parameters bound
The extra tuple slot for parameter values is what makes §7.7's pattern work: one transpiled circuit, many parameter sets, one submission.
7.6 Which Question Are You Asking?
The most consequential decision in this chapter, and the reasoning is Chapter 5 §5.5's.
| Sampler | Estimator | |
|---|---|---|
| Question | what outcomes? | what expectation value? |
| Output | counts over $2^n$ bitstrings | one real number per observable |
| Shots for precision $\epsilon$ | grows with $2^n$ | $O(1/\epsilon^2)$, independent of $n$ |
Circuit contains measure |
yes | no |
| Use for | Grover, Shor, sampling problems, debugging | VQE, QAOA, all of QML, any energy or cost |
Ask the Sampler for a distribution and you pay exponentially. Ask the Estimator for one number and you pay $O(1/\epsilon^2)$. If your algorithm's answer is a number rather than a bitstring, use the Estimator.
What the Estimator owns
The table makes the Estimator look like a Sampler with post-processing bolted on. It is closer to the reverse. The Estimator owns three jobs you would otherwise do by hand, and each is a place where hand-rolled code quietly goes wrong.
It owns term grouping. A SparsePauliOp with $k$ terms does not need $k$ measurements. Terms
that commute qubit-wise — same Pauli letter on every qubit where both are non-identity — can be
read from the same shots. You can see the arithmetic yourself:
from qiskit.quantum_info import SparsePauliOp
H = SparsePauliOp.from_list([("ZZ", 1.0), ("XX", 1.0)])
print([g.paulis.to_labels() for g in H.group_commuting(qubit_wise=True)])
print([g.paulis.to_labels() for g in H.group_commuting(qubit_wise=False)])
qubit-wise [['ZZ'], ['XX']] 2 groups
general commuting [['ZZ', 'XX']] 1 group
The chapter's own two-term Hamiltonian already exhibits the distinction. $ZZ$ and $XX$ commute as operators — they share an eigenbasis, the Bell basis — but they do not commute qubit-wise, so reading them from one computational-basis measurement is impossible. Getting them into one circuit needs a Clifford basis change, which is more machinery than most implementations carry. Qubit-wise grouping is the cheap version, and it is the one in general use.
On Chapter 24's five-term H₂ Hamiltonian the split is:
qubit-wise [['XX', 'II'], ['IZ', 'ZI', 'ZZ']] 2 groups
Five terms, two circuits. The identity is free — $\langle II\rangle = 1$ always — so the real count is four terms in two measurement settings, which is the "2 settings, 4 term-by-term" that Chapter 24 §24.3.2 prices.
It owns basis scheduling. Once grouped, each group needs its own circuit: the ansatz, plus the
single-qubit rotations that turn that group's measurement basis into the computational basis
(Chapter 5 §5.6). This is why the Estimator refuses a circuit that already contains measure — it
has to append its own, and it has to append a different set per group.
It owns mitigation. resilience_level, readout mitigation, zero-noise extrapolation, and gate
twirling are all Estimator options (Chapter 13 §13.4). That is not an arbitrary API choice: ZNE is
defined on expectation values. You extrapolate a number to zero noise; there is no meaningful
extrapolation of a bitstring distribution to zero noise. Choosing the Sampler does not merely cost you
shots — it puts the whole mitigation stack out of reach.
Scale that up and the grouping question stops being cosmetic. Chapter 36's molecules, at the term counts it measured:
molecule qubits Pauli terms
H2 4 15
LiH 12 631
BeH2 14 666
H2O 14 1,086
Six hundred and thirty-one terms for LiH. Term-by-term that is 631 circuits per energy evaluation, and an energy evaluation is one point in an optimization that will want hundreds. The grouping the Estimator performs is what stands between that number and something submittable. Chapter 39 §39.7 measured a LiH VQE at 27 tasks per iteration, exactly — one per Hamiltonian term in a small active space, which is what term-by-term looks like when you can still afford it. The structure of the bill is the structure of the Hamiltonian, and grouping is the only lever that changes it without changing the chemistry.
⚠️ Common Pitfall — quoting the reduction in measurement settings as if it were the reduction in shots.
Grouping $k$ terms into $g$ groups is a $k/g$ reduction in circuits. It is not a $k/g$ reduction in the shots needed for a given precision, because terms inside a group are read from the same shots and their fluctuations are therefore correlated.
Chapter 24 §24.3.2 measured the gap on H₂: the settings ratio is 2 and the shot ratio is exactly 1.500. The discrepancy always runs the same direction — correlations within a group can only make the group noisier than an independent-terms bound suggests, never quieter — so the settings count is always the flattering figure.
Which is this book's recurring warning in miniature: the easy number is almost always the flattering one, because it stops the search.
📐 Math Aside — where $O(1/\epsilon^2)$ comes from, and why it does not know about $n$.
Measure a Pauli observable $P$ with $P^2 = I$. Its eigenvalues are $\pm 1$, so a single shot is a random variable $X \in \{-1, +1\}$ with $\mathbb{E}[X] = \langle P\rangle$ and
$$\mathrm{Var}(X) = \mathbb{E}[X^2] - \mathbb{E}[X]^2 = 1 - \langle P\rangle^2 \;\le\; 1$$
because $X^2 = 1$ identically. Average $N$ shots and the standard error is $\sqrt{\mathrm{Var}(X)/N} \le 1/\sqrt{N}$, so reaching precision $\epsilon$ needs
$$N \;\le\; \frac{1}{\epsilon^2}$$
Nothing in that derivation mentions the qubit count. $\mathrm{Var}(X) \le 1$ holds for a two-qubit Pauli and a two-hundred-qubit Pauli alike, because the eigenvalues are $\pm1$ either way. The full distribution's cost grows with $2^n$ for the opposite reason: you are estimating $2^n$ probabilities, and each needs its own shots.
The bound is worth reading in the other direction too. $\mathrm{Var}(X) = 1 - \langle P\rangle^2$ means the estimate is cheapest exactly where the answer is most definite: a $\langle P\rangle$ near $\pm 1$ costs almost nothing, and a $\langle P\rangle$ near 0 costs the full $1/\epsilon^2$. For a weighted sum $H = \sum_j c_j P_j$ the coefficients enter squared, which is why Chapter 24 can compute a shot budget from the Hamiltonian before running anything.
Working the numbers for the precisions §7.6 uses: $1/\epsilon^2$ is 400 at $\epsilon = 0.05$, 10,000 at $0.01$, and 1,000,000 at $0.001$. Three decimal places costs 2,500 times as much as one and a half. That factor is why Chapter 36 §36.7 reaches $1.91\times10^{20}$ shots at 50 orbitals and calls it $6.06\times10^{8}$ QPU-years.
Precision instead of shots
The Estimator's interface reflects this. You do not give it a shot count; you give it a precision, and it works out the shots.
from qiskit_aer.primitives import EstimatorV2
estimator = EstimatorV2()
for precision in (0.05, 0.01, 0.001):
result = estimator.run([(qc, H)], precision=precision).result()
print(f" precision={precision:<7} value {float(result[0].data.evs):+.4f} "
f"stds {float(result[0].data.stds):.4f}")
Two consecutive runs, which is the point:
run 1 precision=0.05 value +2.0161 stds 0.0500
precision=0.01 value +2.0011 stds 0.0100
precision=0.001 value +2.0001 stds 0.0010
run 2 precision=0.05 value +2.0137 stds 0.0500
precision=0.01 value +1.9982 stds 0.0100
precision=0.001 value +2.0008 stds 0.0010
Three things to read here.
The returned stds is the error bar, and it matches the requested precision exactly. Chapter 5
§5.9 insisted that an estimate without an uncertainty is not a result; the Estimator hands you one
automatically. Use it.
The value converges as the precision tightens, and it lands within its stated uncertainty of the true value of 2.0 in all six cases above. Do not read that as a guarantee — a one-sigma interval misses roughly a third of the time by construction. The error bar is a distribution statement, not a bracket the truth is promised to sit inside.
The numbers differ between runs, and this is worth noticing rather than working around. Unlike
AerSimulator, whose seed_simulator gave you bit-for-bit reproducibility in Part I, the Estimator's
sampling is not straightforwardly seedable through this interface. A quoted Estimator value is a
sample, not a constant — so quote it with its stds, and if you need bit-reproducibility for a
regression test, use StatevectorEstimator, which is exact.
⚠️ Common Pitfall — Precision is a request, not a promise about accuracy.
precision=0.001asks for a statistical uncertainty of 0.001. It says nothing about systematic error. On noisy hardware you can request a precision of $10^{-4}$ and receive a number that is 0.2 away from the truth, with a confident-looking error bar of $10^{-4}$ attached.That is Chapter 5 §5.9's step 5 wearing a new hat, and the interface makes it easier to get wrong, because the returned
stdslooks like a total uncertainty and is not. Match the requested precision to your systematic error, and report both.
Specifying precision is better than specifying shots, because precision is what the problem cares about. Chapter 5's Case Study 1 derived a shot budget by starting from chemical accuracy and working backwards; the Estimator interface lets you state the requirement directly and lets the library do the arithmetic.
⚠️ The layout trap
Now the trap that produces wrong numbers rather than errors.
When you transpile a circuit, the transpiler remaps your logical qubits onto physical ones. Your observable still refers to the logical qubits. If you hand the Estimator a transpiled circuit and an untranspiled observable, the two disagree about which qubit is which.
isa = pm.run(qc) # logical 0,1 -> some physical pair
observable = SparsePauliOp("ZZ")
# WRONG -- the observable does not know about the layout
estimator.run([(isa, observable)])
# RIGHT
estimator.run([(isa, observable.apply_layout(isa.layout))])
apply_layout rewrites the observable onto the physical qubits, padding with identities for the
qubits the circuit does not use. Without it you are measuring an observable on the wrong wires — and
depending on the device size, that is either an error about mismatched qubit counts or, worse, a
perfectly well-formed calculation of something you did not ask for.
🐛 Debug This — The Estimator that returned a plausible wrong number.
Symptom: a VQE runs, converges, and reports an energy that is stable, reproducible, and completely wrong — often suspiciously close to zero.
Cause: the observable was laid out onto the wrong physical qubits. Here is the failure, constructed deliberately so you can see it. The circuit transpiles onto physical qubits
[60, 61], and the observable is padded onto qubits0and1instead:
python isa = pm.run(qc) # layout: [60, 61] right = H.apply_layout(isa.layout) # onto 60, 61 wrong = H.apply_layout([0, 1], num_qubits=127) # onto 0, 1 -- same WIDTH, wrong PLACE
text correct -> 1.8135 wrong -> 0.8838 <- no exceptionThe Estimator computed $\langle ZZ\rangle + \langle XX\rangle$ on two idle qubits that were never touched by the circuit. The answer is not zero, not obviously broken, and not flagged.
Two failure modes, and only one is loud. If the widths disagree — an unpadded 2-qubit observable against a 127-qubit circuit — you get a clean
ValueErrorabout mismatched qubit counts, which is the friendly case. If the widths happen to match and only the placement is wrong, you get 0.8838.Why it is nasty: no exception, no warning, and the number is stable across runs, which is exactly what a correct result looks like. It survives every consistency check you would think to apply.
The fix:
observable.apply_layout(isa.layout), always, immediately after transpilation — taking the layout from the circuit rather than writing one out by hand.The test that catches it: compare against a noiseless reference computed with
StatevectorEstimatoron the untranspiled circuit. If the two disagree by more than noise, suspect the layout before you suspect the physics. The project checkpoint below builds exactly this comparison in.
The measured effect of getting it right, on a device-derived noise model with the circuit on
physical qubits [60, 61]:
StatevectorEstimator, ideal 2.0000
EstimatorV2, layout applied 1.8135 <- noise, correctly measured
EstimatorV2, observable placed on 0,1 0.8838 <- silently wrong
The correct answer and the wrong one differ by a factor of two, and only one line of code distinguishes them.
Why the wrong answer is 0.8838 and not zero
The wrong number is not noise and it is not garbage. It is a correct measurement of a different question, and working out which question explains everything about why the bug survives inspection.
The misplaced observable acts on physical qubits 0 and 1. The circuit never touched them, so they are still in $|0\rangle$. Compute the two terms exactly:
$$\langle 00 | Z\!\otimes\! Z | 00\rangle = (+1)(+1) = +1 \qquad\qquad \langle 00 | X\!\otimes\! X | 00\rangle = 0$$
<ZZ> on |00> +1.0000
<XX> on |00> +0.0000
sum +1.0000
The ideal wrong answer is 1.0. The measured wrong answer is 0.8838, which is 1.0 attenuated by a factor of 0.884 — and the measured right answer, 1.8135, is 2.0 attenuated by 0.907. The two attenuations are the same size. The wrong number carries exactly the amount of noise damage a correct number would carry, because it is a correct number about two real, noisy qubits.
Split the misplaced observable into its two terms and the situation gets worse:
on the idle pair (0,1) on the circuit's pair (60,61)
<ZZ> +0.9048 +0.9082
<XX> -0.0200 +0.9077
★★ The $ZZ$ term cannot tell the difference. +0.9048 against +0.9082 — a gap of 0.0034, well inside the run-to-run scatter of §7.6's own precision table. And the reason is structural, not statistical: $|00\rangle$ and the Bell state $(|00\rangle + |11\rangle)/\sqrt{2}$ have the same $ZZ$ correlation, exactly $+1$. Two states as different as a product state and a maximally entangled one are indistinguishable to this observable.
Only the $XX$ term catches the bug, and it catches it decisively: $+0.9077$ against $-0.0200$.
Now carry that forward to a Hamiltonian that has no $XX$ term. An Ising cost function — every QAOA problem, every portfolio model, Case Study 2's twelve-variable prototype — is built entirely from $Z$ and $ZZ$ terms. On idle qubits, every one of those has expectation $+1$, so the misplaced observable returns
$$\sum_{i which is the cost of the all-zeros bitstring: a perfectly legitimate value of the objective
function, of exactly the right magnitude, for a candidate solution that was never proposed. There
is no term left to catch it. This is the book's "a measurement that cannot detect the thing being asked about" arriving early,
and in its purest form. Part V finds six more. There is a defense that does not require a reference value, and it falls straight out of the analysis
above. The idle qubits are in $|0\rangle$ regardless of what the ansatz parameters are. So the
misplaced observable's expectation is a constant function of the parameters, exactly. Measure it. A one-parameter ansatz — The correct observable sweeps through a full unit; the misplaced one moves by 0.0093, which is
shot noise at the requested precision and nothing else. A hundredfold difference in a quantity that
takes seven circuit submissions to obtain. With The test: sweep one parameter, take the range, compare it to your requested precision. If the
range is the size of your error bar, your observable is not on your circuit's qubits. It costs seven
runs, it needs no reference implementation, and unlike the optimizer's convergence it is looking at
the one thing the bug cannot fake. 🔬 Honest Assessment — what this failure does and does not prove. It does establish that a stable, reproducible, error-barred result can be wrong for structural
reasons, and that the usual consistency checks are blind to it. Every symptom Case Study 1 lists —
smooth convergence, small It does not establish that the Estimator is badly designed, or that And it does not generalize into "always compare against a simulator." The comparison works here
because the reference is computed on the untranspiled circuit, i.e. outside the failing code path.
Run the same wrong observable against a noiseless simulator and it agrees with itself perfectly.
A check that shares a code path with the bug cannot find it — which is the criterion, not the
tool. The performance pattern that variational algorithms live or die by. The natural way to write a parameter sweep is the slow way: Transpilation is expensive — layout search, routing, and optimization are real algorithms — and the
circuit's structure does not change when a rotation angle does. So transpile the parameterized
circuit once, and bind values into the already-transpiled result: Measured, on a 4-qubit depth-3 ansatz with 16 parameters, 20 parameter sets: And the advantage grows with the iteration count, because the transpilation cost is paid once rather
than $N$ times: 🐛 Debug This: the same function, the same arguments, 7.0× and 14.6×. The 20-iteration row above says 14.6×. The block before it says 7.0×. They are the identical
call — The Which number is right depends on the question. 7.0× is the honest answer if your program
transpiles once and exits — a script, a CI job, a single submission. 14.6× is the honest answer
inside a VQE loop, where the process is already warm by the time the optimizer starts and the
one-time cost has been amortized across everything that came before. Neither is a benchmarking
error. Reporting one of them without saying which is. The general form of this trap is that a first-call measurement is a measurement of your imports,
and it is why §7.7's scaling table — which discards nothing and simply calls the same function four
more times — is the more useful of the two. Roughly linear in $N$, as the arithmetic predicts. A 200-iteration VQE — Chapter 5's Case Study 1
budget — is well past a factor of thirty, and on a larger circuit where transpilation takes seconds
rather than milliseconds, this is the difference between a workable optimization and an unusable one. 🧱 Project Checkpoint — The project's backend module grows up. Chapter 2's ```python def get_estimator(kind="sim", **options):
"""An EstimatorV2 for any backend kind, with a consistent interface."""
if kind == "sim":
from qiskit_aer.primitives import EstimatorV2
return EstimatorV2()
from qiskit_ibm_runtime import EstimatorV2
return EstimatorV2(mode=get_backend(kind), options=options)
``` Plus the two things that make it safe: That second function is the one that will save you an afternoon. It costs nothing on small
circuits and it turns the silent failure above into a loud one. Operational knowledge for real hardware. Three execution modes: The distinction that matters: a session keeps your place in the queue between iterations. Without one, a 200-iteration VQE queues 200 separate times, and the queue wait — not the QPU time —
dominates completely. Chapter 5's Case Study 1 budgeted 47 minutes of QPU time; without a session
the wall-clock could be days. 💰 Cost and Queue — Sessions are metered. A session reserves capacity, so the clock generally runs while it is open — including while your
classical optimizer is thinking. That has a direct consequence for how you write the loop: Exact session semantics, limits, and pricing differ by plan and change; check current
documentation before planning a long run.
Chapter 39
compares the platforms on exactly this. The question is how many times do you pay the queue? Everything else about job, batch, and session
follows from it, and the reason it dominates is arithmetic rather than opinion. Chapter 39 measured a 4,096-shot Bell job occupying an IBM device for 6.92 ms. Put that beside a
queue of any realistic length and the ratio is absurd: at a five-minute queue the device spends
$2.31\times10^{-5}$ of your job's lifetime on it, so you wait 43,340 times longer than you compute. That reframes the mode choice completely. ★ Batching 100 circuits is worth about 99×, and it is not a quantum technique. It beats every
transpiler flag in Chapter 10 and every mitigation option in Chapter 13, and it is a scheduling
change you make in one line. The shape of the win is worth knowing because it tells you when to stop. With $t_q$ the queue and
$t_d$ the device time, $n$ separate jobs cost $n(t_q + t_d)$ and one batch costs $t_q + n t_d$: $$S(n) = \frac{n(t_q + t_d)}{t_q + n\,t_d}
\qquad\xrightarrow{\;n\,t_d \ll t_q\;}\qquad S(n) \approx n$$ Linear in the batch size — which is why 100 circuits is 99.8× and not something more interesting.
The ceiling is $S(\infty) = 1 + t_q/t_d$, the same 43,340× as the wall-clock multiple, and linearity
holds until $n \approx t_q/t_d \approx 43{,}300$ circuits. For any batch a working scientist will
ever submit, batching returns its full payoff and has not begun to saturate. What binds in practice
is your provider's cap on circuits per job; look that up, because the formula will not stop you. Sessions exist because batching requires knowing the circuits in advance, and a variational loop
does not. Iteration $k+1$'s parameters depend on iteration $k$'s result, so the circuits cannot be
submitted together. Chapter 39 §39.7 priced a 120-iteration LiH VQE both ways: The session speedup is essentially the iteration count, for the same reason batching's is
essentially the batch size — it is the same formula with $i$ in place of $n$. And the two compose. That run was 27 tasks per iteration, and those 27 are independent of one
another, so they batch; the 120 iterations are dependent, so they need the session. Batch within
the iteration, session across them. A loop that does neither pays $120 \times 27 = 3{,}240$ queue
waits for 31 seconds of computation. Chapter 12 §12.5.1 works the operational detail;
§7.8's job here is only to make you notice that the choice exists before you write the loop. 🔬 Honest Assessment — the queue numbers above are representative, not measured. The 6.92 ms device time, the 31.19 s of VQE computation, and the 27 tasks per iteration are all
measured. The five-minute queue is an assumption, because this book has no credentials that
would let it sample real queue depth, and inventing one would be exactly the failure the book keeps
documenting in other people's work. What survives the assumption is the shape: $S(n) \approx n$ holds for any $t_q \gg n\,t_d$, and
the four-orders-of-magnitude gap between device time and queue time is not sensitive to whether the
queue is thirty seconds or eight hours. The conclusion is robust; the constant is not. Quote the
mechanism, not the 99.8×, unless you measured your own queue. Qiskit today is a core package plus separate execution packages. Terra became A circuit is a list of Aer is several simulators. The primitives are the modern interface, and there are exactly two. Sampler answers "what
outcomes?" and takes measured circuits. Estimator answers "what expectation value?", takes
unmeasured circuits plus observables, and costs $O(1/\epsilon^2)$ shots independent of qubit
count — against exponential cost for a full distribution. If your answer is a number, use the
Estimator. The Estimator takes a precision, not a shot count, and returns a standard deviation with
every value. Precision is what the problem cares about; let the library do the shot arithmetic. ⚠️ Apply the layout to your observable. Transpile once, bind many. Circuit structure does not change when a parameter does. Measured:
14.6× faster at 20 iterations once the transpiler is warm — 7.0× on the very first call, which is
the same measurement paying its warm-up — and the advantage grows with the iteration count. Sessions keep your queue position between iterations — essential for any classical loop, and
metered while open, so keep the classical work fast or outside. Next: Chapter 8 — parameters and parameter
vectors, composition, custom gates, the circuit library, and barriers. This is where the project's
ansatz gets the shape it will keep all the way to Chapter 36.The diagnostic that costs one sweep
ry(θ, 0) then cx(0, 1) — swept over $[0, \pi]$ at seven
points, precision=0.005, transpiled to physical qubits [60, 61]: theta | correct | misplaced
---------|-----------|-----------
0.0000 | +0.8709 | +0.9111
0.5236 | +1.3204 | +0.9075
1.0472 | +1.6822 | +0.9103
1.5708 | +1.8254 | +0.9062
2.0944 | +1.7315 | +0.9147
2.6180 | +1.4215 | +0.9113
3.1416 | +0.9630 | +0.9054
range 0.9545 0.0093 ratio 102x
seed_simulator fixed the misplaced column's range is exactly 0.0000 — but that is a
seeding artefact and 0.0093 is the honest figure, so 0.0093 is the one quoted.
stds, agreement across runs, simulator matching hardware — is caused
by the bug rather than merely surviving it.apply_layout is a wart.
The observable genuinely is a property of the problem and the layout genuinely is a property of the
run; something has to join them, and the API cannot know which qubits you meant. What it does
establish is that the joining must be structural rather than remembered — which is why §7.7's
prepare() returns the pair as one object instead of documenting a rule.7.7 Transpile Once, Bind Many
# SLOW -- re-transpiles on every iteration
for values in parameter_sets:
bound = ansatz.assign_parameters(values)
isa = pm.run(bound) # <-- full transpilation, every time
estimator.run([(isa, observable)])
# FAST -- transpile the parameterized circuit ONCE
isa = pm.run(ansatz) # parameters still free
laid_out = observable.apply_layout(isa.layout)
for values in parameter_sets:
estimator.run([(isa, laid_out, values)]) # bind at submission
transpile once : 3.9 ms
bind 20 times : 1.0 ms total 4.9 ms
re-transpile 20x : 33.9 ms
speedup : 7.0x
iterations | fast (ms) | slow (ms) | speedup
---------- | ---------- | ---------- | --------
5 | 2.1 | 8.3 | 4.0x
20 | 2.3 | 33.2 | 14.6x
50 | 3.5 | 82.5 | 23.3x
100 | 5.5 | 172.0 | 31.4x
benchmark(n=4, depth=3, iterations=20) — run twice in one process, and the discrepancy is
not noise. Calling it five times in a row:text
call transpile bind fast slow speedup
1 3.4 0.9 4.3 35.1 8.1x
2 1.6 0.8 2.4 33.3 13.9x
3 1.7 0.8 2.5 33.2 13.1x
4 1.6 0.7 2.3 32.4 13.9x
5 1.9 0.9 2.8 33.3 11.8xslow column is flat. All of the movement is in transpile, which costs 3.4 ms the first
time and 1.6–1.9 ms every time after — pass-manager construction, import machinery, and cache
population, paid once per process. That cost lands entirely in the numerator's denominator: it is
a fixed 1.8 ms added to a 2.4 ms quantity, so it nearly halves the ratio.
backends.py v1: primitives behind one interface.get_backend() returned a backend object;
now the project needs primitives, and it needs them to work identically on a simulator and on
hardware.vqelab/backends.py -- v1 additions
prepare(circuit, observable, backend) returns the transpiled circuit and the laid-out
observable together, as a pair — so it is impossible to use one without the other. The layout trap
is designed out rather than documented around.reference_value(circuit, observable) computes the exact noiseless expectation with
StatevectorEstimator. Every hardware result the project ever produces gets compared to it, and a
disagreement larger than noise means "check the layout" before "blame the physics."7.8 Sessions, Batches, and Jobs
Mode
What it does
Use for
Job (default)
one submission, queued independently
one-off circuits
Batch
many circuits submitted together, run as a group
independent circuits you want run efficiently
Session
a reserved window; your jobs get priority within it
iterative workloads — VQE, QAOA, anything with a classical loop
from qiskit_ibm_runtime import Session, EstimatorV2
with Session(backend=backend) as session:
estimator = EstimatorV2(mode=session)
for values in parameter_sets:
result = estimator.run([(isa, laid_out, values)]).result()
# ... classical optimizer decides the next values ...
The three modes answer one question
100 independent circuits, 5-minute queue
mode queue waits wall clock speedup
Job 100 100 x 300.007 s = 8.33 h 1.0x
Batch 1 300 s + 0.692 s = 5.01 min 99.8x
as 120 jobs, 5-min queue 120 x (300 + 0.26) s = 10.01 hours, 31.19 s computing
in one session 300 s + 31.19 s = 5.52 minutes
ratio 108.8x
7.9 Summary
qiskit; Aer is
qiskit-aer; Ignis and Aqua are gone, their work redistributed into qiskit-experiments,
Runtime's resilience options, and the domain packages. An old tutorial's import lines date it more
reliably than anything else on the page.CircuitInstructions — operation plus bits — where bits are objects
rather than indices. Composite gates carry their own definitions and expand during transpilation.
The transpiler converts to a DAG, which is why it can reorder commuting operations and why
depth is the longest path through a graph rather than a count.statevector reaches ~30 qubits; density_matrix ~15;
matrix_product_state hundreds if entanglement stays low; and stabilizer reaches thousands for
Clifford circuits, which is why error-correction simulation is tractable at all.observable.apply_layout(isa.layout), every time.
Omitting it does not raise — it measures the wrong qubits and returns a stable, reproducible, wrong
number, often near zero. The defense is a noiseless reference value computed on the untranspiled
circuit, compared against every hardware result.