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 goneqiskit-experiments, Runtime resilience options
Aqua goneqiskit-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.

⚠️ precision is 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 ValueErrorloud, 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:

  1. Compare to an exact reference on the untranspiled circuit (StatevectorEstimator). Free. Ratio near 1 = noise; near 0.5 or near 0 = structural.
  2. 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 stds as 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 v1get_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.