Chapter 2 — Key Takeaways (Setting Up)
The toolchain page. Keep the two run recipes and the little-endian rule where you can see them.
Environment
$ python --version # need 3.10+
$ python -m venv .venv
$ source .venv/bin/activate # Windows: .venv\Scripts\activate
(.venv) $ python -m pip install --upgrade pip
(.venv) $ python -m pip install qiskit qiskit-aer qiskit-ibm-runtime matplotlib pylatexenc
python -m pip, always. Plain pip may belong to a different interpreter — the cause of
"installed but not importable."
| Package | Owns |
|---|---|
qiskit |
QuantumCircuit, gates, transpiler, quantum_info, visualization. Knows nothing about hardware |
qiskit-aer |
Local simulators, including noise models built from real devices |
qiskit-ibm-runtime |
Auth, backends, primitives, jobs — the path to real hardware |
Verify and record:
import sys, qiskit, qiskit_aer, qiskit_ibm_runtime
print(sys.executable, qiskit.__version__, qiskit_aer.__version__, qiskit_ibm_runtime.__version__)
Credentials
# ONCE, interactively. Never in a committed file.
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(token="...", channel="ibm_quantum_platform",
set_as_default=True, overwrite=True)
# Thereafter:
service = QiskitRuntimeService()
Copy the exact save_account snippet from your own dashboard — channel and instance arguments
have changed and will again. Leaked token → revoke and regenerate first, clean up second.
The Bell circuit
from qiskit import QuantumCircuit
qc = QuantumCircuit(2, 2) # 2 qubits, 2 classical bits; qubits always start in |0⟩
qc.h(0) # superposition on q0
qc.cx(0, 1) # entangle: control 0, target 1
qc.measure([0, 1], [0, 1])
$$|00\rangle \xrightarrow{\ H_0\ } \tfrac{1}{\sqrt2}(|00\rangle + |01\rangle) \xrightarrow{\ \mathrm{CNOT}\ } \tfrac{1}{\sqrt2}(|00\rangle + |11\rangle)$$
Ideal output: 50% 00, 50% 11, and exactly zero 01 and 10.
The two run recipes
# ---- simulator ----
from qiskit import transpile
from qiskit_aer import AerSimulator
sim = AerSimulator()
counts = sim.run(transpile(qc, sim), shots=1024, seed_simulator=1234).result().get_counts()
# ---- real hardware ----
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
backend = QiskitRuntimeService().least_busy(operational=True, simulator=False)
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa = pm.run(qc) # MANDATORY, and for THIS backend
job = Sampler(mode=backend).run([isa], shots=1024)
print(job.job_id()) # save it — jobs are persistent
counts = job.result()[0].data.c.get_counts() # 'c' = classical register NAME
least_busy()turns hours of queue into minutes.- Jobs are asynchronous:
service.job(job_id)retrieves later. Submit and walk away. result[0].data.<register_name>— find it withresult[0].data.keys().
Sampling noise vs. device noise — the chapter's core idea
| Simulator | Hardware | |
|---|---|---|
00/11 not exactly equal |
sampling noise | sampling noise + device asymmetry |
01/10 appear at all |
never | device noise |
| More shots fixes it? | yes, ratio → 50/50 | no, converges to a nonzero value |
A million shots gives you a very precise measurement of a wrong distribution. This is why error mitigation (Ch. 13) is a separate discipline from taking more data.
The four noise mechanisms (shallow circuit, superconducting)
| Mechanism | Typical scale | Notes |
|---|---|---|
| Readout error | 1–3% per qubit | Usually dominant for shallow circuits; often asymmetric, in a direction you must measure, not assume |
| Two-qubit gate error | a few parts per thousand | Varies several-fold across pairs on one chip |
| Decoherence | $T_1$ ~200–400 μs, $T_2$ ~100–250 μs | Small for μs circuits, dominant for deep ones. $T_1$ and $T_2$ are independent |
| Crosstalk | small at 2 qubits | Real at scale |
Two more facts that surprise people, both verified in Case Study 2:
rzis virtual on superconducting hardware — zero duration, zero error. Seven of the transpiled Bell circuit's fourteen operations are free.- Readout is the slowest operation: ~1,200 ns against ~530 ns for the entangling gate.
Diagnostic: a circuit with no gates that prepares and measures $|00\rangle$ measures readout error directly; the same for $|11\rangle$ gives you the other direction. Two circuits, one second, and it is the first half of readout mitigation (Ch. 13). Run it whenever a hardware result surprises you.
Fake backends (from qiskit_ibm_runtime.fake_provider import FakeSherbrooke) carry a real
device's coupling map, basis gates, and calibration in a local noise model — no account, no queue,
deterministic. Develop against them; validate on hardware.
Little-endian — the rule that causes the most bugs
bitstring: 0 1
│ └── qubit 0 ← rightmost
└──── qubit 1
'01' means qubit 0 measured 1. Cirq and Q# order differently — see Ch. 18.
The transpiler gap
| You wrote | Hardware ran | |
|---|---|---|
| Gates | h, cx |
rz×7, sx×4, ecr, measure×2 |
| Depth | 3 | 8 |
| Qubits | logical 0, 1 | two specific connected physical qubits |
Neither h nor cx exists on current IBM devices. A SWAP (needed when qubits are not connected)
costs three CNOTs. Your gate budget is in transpiled gates.
Six errors and their fixes
| Message | Cause | Fix |
|---|---|---|
ModuleNotFoundError: qiskit |
wrong interpreter/env | check sys.executable; python -m pip |
IBMNotAuthorizedError |
credential/channel | re-run save_account with the dashboard snippet |
AttributeError on .data.c |
register named differently | result[0].data.keys() |
stuck in QUEUED |
busy device | least_busy(); submit and walk away; do not resubmit |
TranspilerError re: basis |
not transpiled, or for the wrong backend | generate_preset_pass_manager(backend=backend) |
LaTeX error on draw("mpl") |
missing pylatexenc |
install it; qc.draw() always works |
No counts for experiment |
no measurement in the circuit | add measure / measure_all() |
Common pitfalls
- Quoting an unseeded simulator result as if it were exact.
- Submitting to hardware a circuit you have not simulated first (wastes queue time and your day).
- Transpiling for backend A and submitting to backend B.
- Forgetting
backend.namein your recorded results — then not knowing whether it was hardware. - Treating one hardware run as a measurement. Run it several times.
Project piece added this chapter
vqelab/backends.py v0 — get_backend("sim" | "hardware"), nine lines, so no other module ever
knows whether it is on a simulator or a QPU. By Chapter 17 the same call hides five platforms and no
caller has changed. Plus check_credentials(), which fails early with a useful message instead of
late with a stack trace.