Chapter 7 — Key Takeaways (Qiskit Architecture)
Where things live, which question you are asking, and the line that must never be missing.
The package map
| Was | Is now |
|---|---|
| Terra | qiskit itself |
| Aer | qiskit-aer (separate package) |
| Ignis | gone → qiskit-experiments, Runtime resilience options |
| Aqua | gone → qiskit-nature, -optimization, -algorithms |
qiskit. module |
Owns |
|---|---|
circuit |
QuantumCircuit, gates, Parameter, circuit library |
quantum_info |
Statevector, Operator, SparsePauliOp, fidelity, partial trace |
transpiler |
passes, pass managers, layout, routing |
primitives |
the reference Sampler/Estimator (local, exact) |
dagcircuit |
the graph the transpiler works on |
qasm2 / qasm3 / qpy |
serialization |
Date old code by its imports: Aqua/Ignis → pre-2021. from qiskit import Aer or execute
→ pre-1.0.
The circuit model
qc.data is a list of CircuitInstruction — operation + qubits + clbits. Bits are objects,
not indices; qc.find_bit(q).index converts. Composite gates carry their own definitions and expand
during transpilation, not construction.
The transpiler works on a DAG. Depth is the longest path through the graph, not a count:
4 operations, depth 3 -- the two measures share a layer (neither depends on the other)
Aer is several simulators
| Method | Scales to | Use when |
|---|---|---|
statevector |
~30 qubits | default; exact |
density_matrix |
~15 | noise, decoherence |
stabilizer |
thousands | Clifford only — QEC (Ch. 25), benchmarking (Ch. 30) |
matrix_product_state |
hundreds | low entanglement |
unitary / superop |
~15 / ~7 | verifying an operator / a channel |
Measured on a GHZ chain (Clifford):
| n | statevector | stabilizer | ratio |
|---|---|---|---|
| 24 | 147.2 ms | 6.7 ms | 22× |
| 26 | 598.5 ms | 7.2 ms | 83× |
| 1000 | ~10²⁷⁸ YiB of memory | 4.1 s | — |
A 1000-qubit entangled state, exact, in four seconds. The flip side: anything simulable that easily cannot give a quantum advantage. T gates break it — hence T-count as the fault-tolerant currency (Ch. 23).
★ The two primitives
| Sampler | Estimator | |
|---|---|---|
| Question | what outcomes? | what is $\langle\psi\lvert H\rvert\psi\rangle$? |
| Circuit | with measure |
without measure |
| Output | counts per register | one real number + stds |
| Shots for precision $\epsilon$ | grows like $2^n$ | $O(1/\epsilon^2)$, independent of $n$ |
| Use for | Grover, Shor, BV, Simon, tomography | VQE, QAOA, all QML, any energy or cost |
If you would summarize the result as a single real number → Estimator. As "the answer is
01101" → Sampler.
Measured impact at 20 qubits: ~1000× fewer shots. At 30 qubits: ~10⁶×.
Why: estimating a mean never requires resolving the whole distribution. $\mathrm{Var}(C)$ depends on the cost function's range, not the outcome count.
Pubs:
sampler.run([(circuit,)])
estimator.run([(circuit, observable)])
estimator.run([(circuit, observable, parameter_values)]) # bind at submission
Precision, not shots
result = estimator.run([(qc, H)], precision=0.01).result()[0]
value, error = float(result.data.evs), float(result.data.stds)
stds matches the requested precision exactly. Values differ between runs — an Estimator value
is a sample, not a constant. For bit-reproducibility use StatevectorEstimator, which is exact.
⚠️
precisionis statistical only. Requesting $10^{-4}$ on noisy hardware returns a confident $10^{-4}$ error bar on a number that may be 0.2 from the truth. Match precision to systematic error (Ch. 5 §5.9 step 5).
★★ The layout trap
isa = pm.run(qc)
observable = observable.apply_layout(isa.layout) # ← NEVER OMIT THIS
Measured, circuit on physical qubits [60, 61]:
reference (exact) +2.0000
observable laid out correctly +1.8135 ratio 0.907 ← noise
observable placed on qubits 0,1 +0.8838 ratio 0.442 ← SILENTLY WRONG
| Failure | Behavior |
|---|---|
| widths disagree | clean ValueError — loud, therefore harmless |
| widths match, placement wrong | plausible, stable, wrong, no exception |
Why every check misses it: the optimizer converges (it minimizes whatever you give it); error bars are correct (they are statistical); runs agree (wrong answers are stable); simulator matches hardware (same wrong observable in both).
The two checks that work — both step outside the failing pipeline:
- Compare to an exact reference on the untranspiled circuit (
StatevectorEstimator). Free. Ratio near 1 = noise; near 0.5 or near 0 = structural. - Print the layout and the observable's support.
A check that shares a code path with the bug cannot find it.
Best defense: make it unrepresentable. Return the transpiled circuit and laid-out observable as one object. Encode rules in types, not comments.
Transpile once, bind many
isa = pm.run(ansatz) # ONCE -- parameters still free
laid_out = observable.apply_layout(isa.layout) # ONCE
for values in optimizer_suggestions():
estimator.run([(isa, laid_out, values)], precision=1e-2)
| iterations | fast | slow | speedup |
|---|---|---|---|
| 5 | 2.1 ms | 8.3 ms | 4.0× |
| 20 | 2.3 ms | 33.2 ms | 14.6× |
| 50 | 3.5 ms | 82.5 ms | 23.3× |
| 100 | 5.5 ms | 172.0 ms | 31.4× |
Roughly linear in $N$ — the transpilation cost is paid once instead of $N$ times.
Sessions
| Mode | Use for |
|---|---|
| Job | one-off circuits |
| Batch | many independent circuits |
| Session | iterative workloads — keeps your queue position between iterations |
Without a session, a 200-iteration VQE queues 200 times. Sessions are metered while open, so keep classical work fast or outside, and batch what the optimizer allows.
Common pitfalls
- Passing a measured circuit to an Estimator, or an unmeasured one to a Sampler.
- Forgetting
apply_layout— the chapter's headline failure. - Reading
stdsas a total uncertainty. - Re-transpiling inside an optimizer loop.
- Using the Sampler for a quantity that is a single number.
- Opening a session to run one circuit.
Project piece added this chapter
vqelab/backends.py v1 — get_estimator, get_sampler, prepare, reference_value,
check_against_reference, and a frozen Prepared dataclass.
prepare() returns the transpiled circuit and its laid-out observable as one object, so the trap
is designed out. reference_value() computes the exact noiseless answer, and
check_against_reference() turns the silent failure into a loud one — with an error message that
names the likely cause rather than the symptom.