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 with result[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:

  • rz is 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.name in 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 v0get_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.