37 min read

> *"The first program you run on a new machine teaches you more about the machine than the next

Prerequisites

  • 1

Learning Objectives

  • Create an isolated Python environment and install Qiskit, Aer, and the IBM Runtime client, and verify each version from the interpreter.
  • Create a free IBM Quantum account, obtain an API token, and save credentials to disk without ever writing the token into a source file.
  • Build, draw, and read a two-qubit Bell state circuit in Qiskit.
  • Execute a circuit on the Aer simulator and interpret the resulting counts dictionary.
  • Select a real quantum backend, transpile a circuit for it with a preset pass manager, submit it with the SamplerV2 primitive, and retrieve the result.
  • Compare simulator and hardware histograms and explain, in physical terms, why the hardware histogram contains outcomes the ideal circuit forbids.
  • Diagnose the six most common setup and first-run failures from their error messages.

Chapter 2: Setting Up

"The first program you run on a new machine teaches you more about the machine than the next hundred, because it is the only one you run before you have any expectations."

Overview

By the end of this chapter you will have run a program on a physical quantum processor.

Not a simulation of one. An actual device — a chip of superconducting circuits held at roughly fifteen millikelvin, colder than interstellar space, in a data center you will never visit — which will accept your four-line Python program, execute it a thousand times, and send you back the results. It is free. It takes about an hour to set up. People find this genuinely startling the first time, and they should.

The result you get back will be wrong. Not wildly wrong — recognizably, frustratingly almost right. Your circuit will produce a state that should yield only two possible measurement outcomes, and you will get four. That discrepancy is the most valuable thing in this chapter, and the reason we run on hardware in Chapter 2 rather than in Chapter 20. Everything in Parts II and V — noise models, error mitigation, hardware-aware programming, benchmarking — exists to deal with what you are about to see.

This chapter is deliberately slow about setup. The ninety minutes you spend getting the environment right, understanding what each package does, and learning to read the two error messages that account for most first-run failures will save you days. A reader who does not know the difference between a transpilation error and an authentication error will lose hours to problems that thirty minutes of foundation would have prevented.

In this chapter, you will learn to:

  • Create an isolated virtual environment and install qiskit, qiskit-aer, and qiskit-ibm-runtime — and explain why isolation matters more here than in most Python work.
  • Create a free IBM Quantum account, obtain an API token, and save it to disk without ever putting it in a source file.
  • Build and draw a Bell state circuit.
  • Run it on the Aer simulator and read the counts dictionary it returns.
  • Select a real backend, transpile for it, submit with the SamplerV2 primitive, and retrieve results.
  • Compare the two histograms and explain, physically, where the impossible outcomes came from.
  • Recognize and fix the six failures that account for most first runs going wrong.

Learning Paths

How to read this chapter by track. - 🔰 Beginner — all of it, in order, at a keyboard. Do not skip §2.1; environment problems are the single most common reason people abandon this subject in week one. - 🔬 Researcher — §2.6 and §2.7 are the ones that matter. Note carefully in §2.6 that the circuit submitted to hardware is not the circuit you wrote; the ISA-circuit distinction will shape how you report methods in a paper. - 🤖 Quantum ML — the same setup serves you; add PennyLane now if you like (Appendix C §C.4), though nothing needs it until Chapter 16. - 🏗️ Quantum Engineer — §2.1's version-pinning discussion and §2.8's error taxonomy are worth real attention. You will be the person other people bring their environment problems to. - 🔐 Security — §2.3 is unusually relevant: an IBM Quantum API token is a credential with real value, and the section's handling rules are the ones you would want a team to follow.


2.1 The Environment

Before any quantum code, a boring and load-bearing decision: install into a virtual environment, not into your system Python.

This advice is standard for all Python work and is unusually important here, for three reasons.

The dependency trees are deep and opinionated. Qiskit, Cirq, PennyLane, and the Braket SDK each pull in a substantial stack — NumPy, SciPy, symbolic math, plotting, serialization — and they do not always agree on versions. Installing several quantum frameworks into one environment is a reliable way to produce a broken NumPy.

The versions matter, and you will need to change them. As Chapter 1 §1.2 showed, Qiskit 1.0 removed functions that appear in most older tutorials. You will sometimes want to reproduce someone else's result under their version, and then go back to yours. That is trivial with environments and painful without.

Reproducibility is a scientific requirement here, not a nicety. A quantum result is a distribution produced by a specific circuit, compiled by a specific transpiler version, run on a specific device with a specific calibration. If you cannot say which version of Qiskit produced a number, you cannot defend the number.

Creating the environment

$ python --version          # need 3.10 or newer
Python 3.11.7

$ python -m venv .venv

$ source .venv/bin/activate         # macOS / Linux
$ .venv\Scripts\activate            # Windows (PowerShell or cmd)

(.venv) $ python -m pip install --upgrade pip

Your prompt should now show (.venv). If it does not, activation did not take, and everything you install next will go into the wrong place — this is the single most common environment failure and it is silent until much later.

⚠️ Common Pitfall — The pip that is not your pip.

On many systems, pip and python resolve to different installations. Installing with bare pip and then running with python produces the maddening ModuleNotFoundError: No module named 'qiskit' immediately after a successful install.

Always use python -m pip install ..., which guarantees the package lands in the environment belonging to the interpreter you are about to run. This one habit prevents an entire category of confusion.

To confirm which interpreter you are actually using:

python import sys print(sys.executable) # should be inside your .venv

Installing Qiskit

Three packages, and it is worth knowing what each does rather than pasting one line.

(.venv) $ python -m pip install qiskit qiskit-aer qiskit-ibm-runtime
Package What it gives you First used
qiskit The core: QuantumCircuit, gates, the transpiler, visualization, quantum info tools §2.4
qiskit-aer High-performance local simulators, including noisy ones §2.5
qiskit-ibm-runtime The client for IBM's hardware: authentication, backends, primitives, jobs §2.6

They are separate packages on purpose. qiskit alone is a complete circuit-construction and compilation toolkit that talks to nothing; the other two are the execution paths. This separation is why the Chapter 1 Version Note's from qiskit import Aer no longer works — Aer lives in its own package now.

You also want plotting, which the visualization functions need:

(.venv) $ python -m pip install matplotlib pylatexenc

pylatexenc is easy to forget and produces a confusing failure if missing: circuit.draw("mpl") raises an error about LaTeX rendering. Install it now.

Verifying the install

Do not skip this. Run it, and record the output — you will want it later, and every result you report should carry it.

# example-01-verify-install.py
import sys

import qiskit
import qiskit_aer
import qiskit_ibm_runtime

print(f"python              {sys.version.split()[0]}")
print(f"interpreter         {sys.executable}")
print(f"qiskit              {qiskit.__version__}")
print(f"qiskit-aer          {qiskit_aer.__version__}")
print(f"qiskit-ibm-runtime  {qiskit_ibm_runtime.__version__}")
python              3.13.12
interpreter         /home/you/quantum/.venv/bin/python
qiskit              2.5.1
qiskit-aer          0.17.2
qiskit-ibm-runtime  0.48.0

Your version numbers will differ and that is fine. What matters is that all three import, and that the interpreter path is inside your .venv.

🗝️ Version Note — Which versions this book was written against.

Every measured number in this book was produced on Qiskit 2.5.1, qiskit-aer 0.17.2, and qiskit-ibm-runtime 0.48.0, under Python 3.13. Most examples also run on 2.0+; almost none run on 1.x, because Qiskit 2.0 removed qiskit.pulse and the BackendV1 surface around it. Where a 2.0 change affects an example, a Version Note says so.

If you are reading this some time after publication and an example fails, check your version first. The failure modes to expect: a function moved to a different module (fix the import); a keyword argument was renamed (check the current API reference); or a whole subsystem was removed (Pulse, in 2.0 — see Chapter 31).

To reproduce this book's environment exactly:

console (.venv) $ python -m pip install "qiskit>=2.5,<3" "qiskit-aer>=0.17" "qiskit-ibm-runtime>=0.48"

What the version numbers actually buy you

The Version Note above gives a compatible range. Which exact versions produced the measured numbers in this book is a different question, and the answer belongs next to the numbers:

python              3.13.12
qiskit              2.5.1
qiskit-aer          0.17.2
qiskit-ibm-runtime  0.48.0
pennylane           0.45.1        (Chapter 16)
cirq                1.7.0         (Chapter 14)

That list is not decoration. Run the verification script above and you can tell whether a number in this book should reproduce on your machine — and if it does not, you know the first place to look.

The reason to care is that a minor release renames things, and a major release deletes subsystems. Renames cost you an afternoon. Deletions cost you a chapter.

Qiskit 2.0 removed the entire pulse layer — the API for shaped microwave envelopes, custom gate calibrations, and everything below the gate. Here is what that removal looks like from the interpreter, run against the environment above:

   qiskit 2.5.1

   import qiskit.pulse                       ModuleNotFoundError: No module named 'qiskit.pulse'
   from qiskit import pulse                  ImportError: cannot import name 'pulse' from 'qiskit'
   QuantumCircuit.add_calibration            AttributeError: no attribute 'add_calibration'
   QuantumCircuit.calibrations               AttributeError: no attribute 'calibrations'
   backend.defaults()                        AttributeError: no attribute 'defaults'
   backend.instruction_schedule_map          AttributeError: no attribute 'instruction_schedule_map'
   backend.drive_channel(0)                  AttributeError: no attribute 'drive_channel'
   target.instruction_schedule_map           AttributeError: 'Target' has no attribute ...

One module and five APIs, in one release. There is no import to fix and no keyword to rename; the capability is gone from the library.

This is not hypothetical for this book. Chapter 31 was planned as a tour of qiskit.pulse and its first probe script failed on line one. The chapter that exists instead — §31.6 explains why the API went away, and the rest of it covers what survives below the gate — is a different chapter, written because the version pinned at the top of this section is the version that exists.

🗝️ Version Note — What still works, and how to check without guessing.

Pulse went. The device data that the pulse layer used to expose did not: it moved into backend.target, which is the object Case Study 2 and Chapter 29 read calibrations from. One line tells you what a backend can actually do:

python from qiskit_ibm_runtime.fake_provider import FakeSherbrooke backend = FakeSherbrooke() print(backend.name, backend.num_qubits) print(sorted(backend.target.operation_names))

text fake_sherbrooke 127 ['delay', 'ecr', 'for_loop', 'id', 'if_else', 'measure', 'reset', 'rz', 'switch_case', 'sx', 'x']

That is the whole instruction set of a 127-qubit processor: four single-qubit instructions (rz, sx, x, and the identity), one entangler, measure, reset, delay, and three control-flow constructs. Every circuit in this book that runs on this device is built out of those eleven names. Print this list for any backend before you write a circuit for it — it takes a second and it is the ground truth that the transpiler in §2.6 is compiling toward.

Note what is not there: no h, no cx, no t. §2.6 shows what happens to yours.

2.2 What You Just Installed

A brief orientation, because "I installed a thing and it worked" is a weak foundation and you will be debugging this stack.

qiskit is the framework proper. Its central object is QuantumCircuit. It contains the gate library, the transpiler with all its passes and preset configurations, the quantum_info module (statevectors, operators, fidelity measures — heavily used from Chapter 3 onward), the visualization functions, and the OpenQASM importers and exporters. It knows nothing about hardware.

qiskit-aer is a set of simulators written in C++ with a Python interface. It simulates circuits exactly (statevector), with sampling (the default), with density matrices, with stabilizer methods, and — crucially — with noise, including noise models built from real device calibration data. Chapter 11 is about this package.

qiskit-ibm-runtime is the client for IBM Quantum. It handles authentication, lists backends, submits jobs, manages sessions, and provides the primitivesSamplerV2 and EstimatorV2 — which are the modern interface for asking a quantum computer a question. Chapter 7 covers it properly.

That three-way split maps onto the stack from Chapter 1 §1.3: qiskit owns the circuit and transpiler layers, qiskit-aer substitutes for the hardware layer locally, and qiskit-ibm-runtime is the path to the actual hardware layer.

2.3 Your IBM Quantum Account

Now the credential.

  1. Go to the IBM Quantum platform site and create an account. It is free and does not require a credit card.
  2. After signing in, your dashboard shows an API token. Copy it.
  3. Save it to disk once, from a Python shell, using the code below.
# Run this ONCE, interactively. Do not put your token in a script you commit.
from qiskit_ibm_runtime import QiskitRuntimeService

QiskitRuntimeService.save_account(
    token="PASTE_YOUR_TOKEN_HERE",
    channel="ibm_quantum_platform",
    set_as_default=True,
    overwrite=True,
)

This writes the token to a configuration file in your home directory (~/.qiskit/). From then on, every script authenticates with a bare constructor:

from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()      # reads the saved credentials
print(f"connected; {len(service.backends())} backends visible")

🗝️ Version Note — The channel and instance arguments have changed, and will again.

Older code and tutorials use channel="ibm_quantum". IBM has migrated to a new platform, and the current channel name and the way instances are specified have changed with it. Some accounts also need an instance argument identifying which allocation to use.

The reliable move: copy the exact save_account(...) snippet shown on your own IBM Quantum dashboard. It is generated for your account, it reflects the current API, and it will be right when a book published months earlier is not. This is one of the few places where "check the vendor's page" beats any printed reference, and it is worth building the habit here.

If you get IBMNotAuthorizedError or an error mentioning an unknown channel, this is why.

⚠️ Common Pitfall — Treat the token as a real credential.

An API token is not a convenience string. It authorizes compute against your allocation, and if your account is ever attached to paid resources, it authorizes spend.

Rules: - Never write it into a .py file, a notebook cell you will commit, or a Docker image layer. - Never paste it into a chat, an issue report, or a screenshot. Check screenshots before posting; tokens hide in the corners of terminal captures. - save_account is fine — it writes to your home directory outside the repository. This book's .gitignore excludes ~/.qiskit/ and .env anyway, belt and braces. - If you must pass a token in CI, use the platform's secret store and inject it as an environment variable: python import os from qiskit_ibm_runtime import QiskitRuntimeService service = QiskitRuntimeService(token=os.environ["IBM_QUANTUM_TOKEN"], channel="ibm_quantum_platform") - If a token leaks, revoke and regenerate it from the dashboard immediately. It takes seconds.

What the free tier gets you

Free access to real quantum processors, with a monthly quota measured in minutes of QPU time.

That quota sounds tiny and is more than you need for this book. QPU time is not wall-clock time — it is the actual execution time of your circuits, and a 1,024-shot run of a small circuit consumes well under a second of it. What you actually wait for is the queue, which is shared and can run from minutes to hours depending on demand.

💰 Cost and Queue — Budget your attention, not your money.

Exact free-tier allowances change; check your dashboard for current numbers. The practical shape of it:

  • A small circuit at 1,024 shots costs a fraction of a second of QPU time. You could run hundreds of the exercises in this book and stay inside a typical monthly allowance.
  • Queue time dominates your experience. Submit, then go do something else. Do not sit watching a progress bar; that is the single biggest waste of a learner's time in this subject.
  • Use service.least_busy() (§2.6) — it routinely turns an hour of waiting into a few minutes.
  • Debug on the simulator. Every hardware submission of a circuit you have not simulated first is a waste of queue time and of your afternoon. This is theme four of the book: the simulator is your lab; hardware is your exam.

2.4 Your First Circuit

Now the fun part. Four lines.

# example-02-bell-state.py
from qiskit import QuantumCircuit

qc = QuantumCircuit(2, 2)     # 2 qubits, 2 classical bits
qc.h(0)                       # Hadamard on qubit 0
qc.cx(0, 1)                   # CNOT: control 0, target 1
qc.measure([0, 1], [0, 1])    # measure q0 -> c0, q1 -> c1

print(qc.draw())
     ┌───┐     ┌─┐
q_0: ┤ H ├──■──┤M├───
     └───┘┌─┴─┐└╥┘┌─┐
q_1: ─────┤ X ├─╫─┤M├
          └───┘ ║ └╥┘
c: 2/═══════════╩══╩═
                0  1

Read the diagram. Time runs left to right. Each horizontal line is a qubit's worth of history. q_0 gets an H gate, then acts as the control (the ) of a CNOT whose target (the , drawn as an X in a box) is q_1. Then both are measured (M) into the double-line classical register c.

QuantumCircuit(2, 2) creates two qubits and two classical bits. All qubits start in $|0\rangle$ — always, in every framework, with no exception. There is no uninitialized quantum memory.

⚛️ The Physics Underneath — What those two gates do.

Both qubits begin in $|00\rangle$: qubit 0 is definitely 0, qubit 1 is definitely 0.

H on qubit 0 creates an equal superposition of that qubit's two values:

$$|00\rangle \;\longrightarrow\; \tfrac{1}{\sqrt{2}}\bigl(|00\rangle + |01\rangle\bigr)$$

(Reading right to left in each ket, since Qiskit puts qubit 0 rightmost — §2.7 and Chapter 5 return to this.)

CNOT flips the target when the control is 1. Applied to a superposition, it acts on both parts at once:

$$\tfrac{1}{\sqrt{2}}\bigl(|00\rangle + |01\rangle\bigr) \;\longrightarrow\; > \tfrac{1}{\sqrt{2}}\bigl(|00\rangle + |11\rangle\bigr)$$

That is a Bell state. Measuring it gives 00 with probability $\lvert 1/\sqrt{2}\rvert^2 = 0.5$, `11` with probability $0.5$, and 01 or 10 with probability exactly zero.

The two qubits are now entangled: neither has a state of its own, and their outcomes are perfectly correlated no matter which you measure first. Chapter 4 does this properly. For now, hold on to the prediction: 50/50 between 00 and 11, and nothing else.

That prediction — made before running anything — is what makes the next two sections informative. Write it down.

2.5 Running on the Simulator

The simulator first. It is free, instant, and noiseless, which makes it the reference against which hardware is judged.

# example-03-run-simulator.py
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

sim = AerSimulator()
qc_compiled = transpile(qc, sim)                  # even simulators want compiling
result = sim.run(qc_compiled, shots=1024, seed_simulator=1234).result()
counts = result.get_counts()

print(counts)
{'00': 529, '11': 495}

Exactly as predicted. Two outcomes, roughly half the shots each, and no 01 or 10 at all — not "very few," but literally none, because the amplitude for those outcomes is zero and the simulator computes the ideal physics.

Three details in that snippet are worth pausing on.

shots=1024. The circuit ran 1,024 times. Each run produced two classical bits. The counts dictionary is a tally. Change this number and the counts change proportionally, but the ratio converges to 50/50 — that convergence, and how fast it happens, is Chapter 5's subject.

seed_simulator=1234. This makes the sampling reproducible. Run it again and you get exactly {'00': 529, '11': 495} again. Every quoted simulator number in this book fixes a seed, and your numbers will match this book's when you use the same seed and a comparable Aer version. (The output above was produced with Qiskit 2.5 and qiskit-aer 0.17; the sampler's internal RNG can change between major versions, so if your counts differ slightly with the same seed, check your version before suspecting anything else.) Remove the seed and you will get something near 512/512 but different every time — correct behavior, and the reason unseeded results should never be quoted as if they were exact.

transpile(qc, sim). Yes, even for a simulator. AerSimulator supports a broad gate set, so transpilation here changes little, but doing it always builds the right habit: the circuit you write is not the circuit that runs. On hardware that gap is enormous, and §2.6 shows it.

🧪 Run It — Watch the ratio converge.

Change shots to 10, then 100, then 10,000, and remove the seed. Run each several times.

At 10 shots you will see things like {'00': 7, '11': 3} — nowhere near 50/50. At 10,000 you will see splits within a percent of even.

The distribution was always 50/50. What changed is how well your sample estimates it. This is the most important statistical fact in quantum programming and it is worth feeling in your hands rather than reading. Exercise 2.7 makes it quantitative.

📐 Math Aside — "Zero" and "too small to see" are different claims, and this run only supports one of them.

The simulator printed {'00': 529, '11': 495}. No 01, no 10. It is tempting to read that as proof that those outcomes have probability zero. It is not, and the distinction is worth getting right the first time you meet it, because you will meet it constantly.

An outcome with true probability $p$ appears $Np$ times on average in $N$ shots. At $N = 1{,}024$, an outcome needs $p \gtrsim 1/1024 \approx 0.098\%$ before you expect to see it even once. Anything rarer than that will usually print nothing at all — indistinguishable, on the page, from impossible.

Seeing zero events in $N$ trials does bound $p$, by the standard rule of three: the 95% upper confidence bound is $3/N$.

$$N = 1{,}024:\quad p \le 3/1024 = 0.29\% > \qquad\qquad N = 4{,}096:\quad p \le 3/4096 = 0.073\%$$

So the run establishes $p < 0.29\%$. It does not establish $p = 0$. What establishes $p = 0$ is the algebra in §2.4 — the amplitude of $|01\rangle$ in $\tfrac{1}{\sqrt2}(|00\rangle + |11\rangle)$ is exactly zero, and no number of shots is needed to know that. The derivation is the strong claim; the run is the weak one. Keep track of which is doing the work.

This also tells you why the hardware result in §2.6 is unmissable. Its impossible-outcome fraction is 4.39%, roughly 15× the tightest bound a 1,024-shot run could have set. A device defect at the 0.1% level, by contrast, would hide completely at these shot counts — which is exactly the trap Chapter 27 §27.5 documents, where a false-failure rate quoted as 1.0% from 2 events in 200 runs turned out to be 0.150% once 2,000 runs were done. Two events is not a rate.

2.6 Running on Real Hardware

Now the part that is genuinely remarkable.

# example-04-run-hardware.py
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler

# 1. The circuit -- identical to the simulator version.
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

# 2. Connect, and pick the least busy real device.
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
print(f"backend: {backend.name}  ({backend.num_qubits} qubits)")

# 3. Transpile FOR THAT DEVICE. This step is mandatory on hardware.
pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_circuit = pm.run(qc)
print(f"depth: {qc.depth()} -> {isa_circuit.depth()} after transpilation")

# 4. Submit.
sampler = Sampler(mode=backend)
job = sampler.run([isa_circuit], shots=1024)
print(f"job id: {job.job_id()}    (save this -- you can retrieve results later)")

# 5. Wait, then read.
result = job.result()
counts = result[0].data.c.get_counts()
print(counts)

A representative result:

backend: ibm_brisbane  (127 qubits)
depth: 3 -> 8 after transpilation
job id: cx1a2b3c4d5e6f7g8h9i0j
{'00': 481, '11': 500, '01': 18, '10': 25}

There they are. Eighteen 01s and twenty-five 10s — outcomes with amplitude exactly zero in the ideal circuit, which the ideal simulator produced exactly zero of, and which the hardware produced forty-three of.

Your own numbers will differ, and this is worth being precise about rather than waving at. The device differs, the day differs, the calibration differs, and above all which physical qubits the transpiler picked differs. For this circuit on current superconducting hardware, the fraction of shots in the two impossible states typically runs from around 2% on a well-calibrated, well-chosen pair to well over 10% on a poor one — a fivefold spread on the same chip on the same afternoon. Exercise 2.22 has you demonstrate that spread deliberately.

What will not differ is the shape: two dominant peaks a little below 50% each, and a stubborn residue in the two states that should be impossible.

🧪 Run It — No account? Run the noise anyway.

Qiskit ships fake backends: noise models built from snapshots of real IBM devices, including the full coupling map, basis gates, and per-qubit calibration. They run locally, need no credentials, no queue, and no quota — and they reproduce the character of hardware results faithfully enough that most of this book's hardware discussions can be followed without ever submitting a job.

```python from qiskit_ibm_runtime.fake_provider import FakeSherbrooke from qiskit_ibm_runtime import SamplerV2 as Sampler from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

backend = FakeSherbrooke() # 127 qubits, from a real device snapshot pm = generate_preset_pass_manager(optimization_level=1, backend=backend, seed_transpiler=42) isa = pm.run(qc)

sampler = Sampler(mode=backend) sampler.options.simulator.seed_simulator = 1234 # reproducible; drop it for realism counts = sampler.run([isa], shots=4096).result()[0].data.c.get_counts() print(counts) ```

text {'00': 1927, '11': 1989, '01': 91, '10': 89}

That is 180 shots out of 4,096 — 4.39% — in states that cannot occur. Locally, in about a second, and with both seeds fixed it is bit-for-bit reproducible. Every hardware number in this book that you can check was produced this way, and Case Study 2 audits exactly this result against the device's calibration data.

Use fake backends for development and real hardware for the truth. They are not the same thing — a fake backend's noise model is a static snapshot and misses drift, crosstalk correlations, and the day's calibration — but they are far closer to hardware than an ideal simulator, and they cost you nothing.

📉 Noise Report — Where those hundred impossible results came from.

Four physical mechanisms, in rough order of contribution for this circuit:

Readout error. Measuring a qubit is a physical process with a nonzero misclassification rate — typically around 1–3% per qubit on current superconducting devices, and often asymmetric between the two outcomes. A perfect 00 misread on one qubit becomes 01 or 10. This is usually the largest single term for a circuit this shallow.

The asymmetry deserves a warning. It is frequently explained by saying that reading a true 1 as 0 is the more likely direction, because an excited qubit can decay during the measurement window — which is a real mechanism. But readout also involves discriminating two noisy signals, and on some devices and some qubits the bias runs the other way. Case Study 2 measures it on a real device snapshot and finds it running against the usual story. Do not assume the direction. Two circuits measure it, and Case Study 2 §Step 4 shows you both.

Two-qubit gate error. The CNOT is not implemented directly; it is compiled into the device's native entangling gate plus single-qubit corrections, and that composite operation has an error rate of roughly a few parts per thousand on good hardware, higher on a bad pair of qubits.

Decoherence. Between preparation and measurement, qubits lose energy to their environment ($T_1$ relaxation, driving 1 toward 0) and lose phase coherence ($T_2$ dephasing, destroying the superposition). Coherence times are tens to hundreds of microseconds; your circuit takes a few microseconds, so this is a small effect here — and a dominant one for the deeper circuits of Part IV.

Crosstalk. Operations on nearby qubits perturb yours. Small for a two-qubit circuit on a 127-qubit chip, real at scale.

None of this is a bug in your code. It is the physical behavior of the device. That distinction — noise versus bug — is the one you will be making constantly, and Chapter 12 §12.7 gives you a decision procedure for it.

What that job actually cost

§2.3 claimed that QPU time is execution time rather than wall-clock time, and that a small circuit consumes very little of it. Here is the number, because a claim with a number attached is worth more than the same claim without one.

Chapter 39 §39.2 schedules exactly this circuit against a 133-qubit device's calibrated instruction durations — not an estimate, the device's own timing data — and reads off the answer:

   circuit    2q gates   depth   duration/shot   @ 4,096 shots
   Bell              2       8         1.69 us         6.93 ms
   QFT-8           137     252        10.55 us        43.20 ms

Your Bell job occupies the processor for about seven milliseconds. Two consequences follow directly.

A minute of QPU allowance is 8,658 of these jobs. $60{,}000 \text{ ms} / 6.93 \text{ ms} = 8{,}658$. You are not going to exhaust a free-tier quota running the exercises in this book. Stop worrying about the meter.

Almost all of that time is readout. On Chapter 39's device a measurement takes 1,560 ns, so it is 92% of the 1.69 μs shot — the two gates are a rounding error against it. Case Study 2 finds the same shape on a different chip: readout 1,216 ns against 533 ns for the entangling gate. The slowest operation on a quantum computer is looking at the answer.

Now the comparison that reframes the whole experience. Chapter 39 also timed the same jobs on a local statevector simulator: the Bell job took 21.7 ms, about three times longer than the quantum processor's 6.93 ms. Neither number is the bottleneck. What you actually waited for was the queue — Chapter 39 measures device utilization at a five-minute queue as $2.31\times10^{-5}$, a factor of 43,340 between execution time and wall clock.

So: submit and walk away. Not as etiquette, but because the arithmetic says the thing you are watching is 0.002% of what you are waiting for.

What the transpiler did

Look again at that depth: 3 -> 8 line. Your three-layer circuit became eight layers before executing. Let us see why.

print(isa_circuit.draw(idle_wires=False))
print(f"\ngate counts: {dict(isa_circuit.count_ops())}")
gate counts: {'rz': 7, 'sx': 4, 'ecr': 1, 'measure': 2}

There is no h and no cx in that output, because the hardware does not have those gates.

Current IBM processors implement a small native basis — on recent devices typically rz, sx, x, and the two-qubit ecr (echoed cross-resonance); older generations used cx directly. Your H was rewritten as a short sequence of rz and sx. Your CNOT became ecr plus single-qubit corrections. The transpiler also chose which two physical qubits to use out of 127, since the two must be physically connected for a two-qubit gate.

⚙️ Under the Transpiler — The gap, and why it grows.

Your circuit On hardware
Gates h, cx, 2 × measure rz×7, sx×4, ecr×1, 2 × measure
Depth 3 8
Qubits logical 0, 1 two specific, physically adjacent, physical qubits

A 2-gate circuit became 12 operations. That expansion factor is normal, it compounds with circuit size, and it means the gate count you designed is not the gate count that determines your error rate. When Chapter 1 §1.5 said a few hundred two-qubit gates is your budget, that is a budget in transpiled gates.

Worse, if your circuit needs a two-qubit gate between qubits that are not physically connected, the transpiler inserts SWAP gates to move the information — and each SWAP costs three CNOTs. On a device with sparse connectivity this is frequently the dominant cost.

Chapter 10 is entirely about controlling this, and Chapter 28 about fighting it.

The pieces of the hardware path

Five things in that script deserve a name, because you will use all of them constantly.

QiskitRuntimeService() — your authenticated connection, reading the credentials you saved in §2.3.

service.least_busy(operational=True, simulator=False) — backend selection. operational=True excludes devices under maintenance; simulator=False insists on real hardware. This one call is the difference between a five-minute wait and a two-hour one. Chapter 12 covers choosing a backend on quality rather than availability, which matters once your circuits get real.

generate_preset_pass_manager(optimization_level=1, backend=backend) — builds a transpilation pipeline configured for that specific device's gate set and connectivity. Levels run 0 (none) to 3 (aggressive); level 1 is a sensible default and level 3 is worth trying when a circuit is close to the noise floor.

Sampler(mode=backend) — the primitive. SamplerV2 answers the question "what measurement outcomes do I get?" Its sibling EstimatorV2 answers "what is the expectation value of this observable?" — which is the question the whole variational program of Chapter 24 asks. Chapter 7 §7.6 explains why this split exists and why choosing wrongly is expensive.

job.job_id() — save it. Jobs are asynchronous and persistent. You can close your laptop and retrieve the result tomorrow:

job = service.job("cx1a2b3c4d5e6f7g8h9i0j")
print(job.status())
counts = job.result()[0].data.c.get_counts()

⚠️ Common Pitfallresult[0].data.c and where that c comes from.

SamplerV2 returns results per classical register, and c is the default name of the register created by QuantumCircuit(2, 2). If you build the register explicitly:

python from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit qr = QuantumRegister(2, "q") cr = ClassicalRegister(2, "meas") # <- named "meas" qc = QuantumCircuit(qr, cr)

then the results live at result[0].data.meas, not .c, and using .c raises an AttributeError that reads as if the result were empty.

To find the name when you are unsure:

python print(result[0].data.keys())

This trips up almost everyone once. It is not a deep problem; it just looks like one.

2.7 Reading the Two Histograms

Put them side by side. This comparison is the intellectual payload of the chapter.

# example-05-compare.py  (excerpt; see the code/ directory for the full script)
from qiskit.visualization import plot_histogram
import matplotlib.pyplot as plt

# both at 4096 shots so the bars are directly comparable
sim_counts = {"00": 2074, "11": 2022}
hw_counts = {"00": 1927, "11": 1989, "01": 91, "10": 89}

plot_histogram([sim_counts, hw_counts], legend=["simulator", "hardware"])
plt.savefig("bell-comparison.png", dpi=150, bbox_inches="tight")
        SIMULATOR (ideal)                    HARDWARE (real)

  0.50 ┤ ███         ███            0.50 ┤ ███         ███
       │ ███         ███                 │ ███         ███
  0.25 ┤ ███         ███            0.25 ┤ ███         ███
       │ ███         ███                 │ ███         ███
  0.00 ┼─███──┬───┬──███─           0.00 ┼─███──▌───▌──███─
        00    01  10   11                  00    01  10   11
                                                 2.2% 2.2%

  exactly 0 for 01 and 10           4.39% total in "impossible" states

The ideal side

Two bars, near half each, nothing else. The deviation from exactly 2048/2048 is sampling noise — you took 4,096 samples from a fair coin, and 2074/2022 is an entirely ordinary result. Not a defect; the expected fluctuation in each count is on the order of $\sqrt{4096}/2 = 32$, and each count sits 26 away from 2048. Chapter 5 §5.4 makes this precise.

The hardware side

Four bars. The two you expected, slightly diminished, plus 4.39% of the shots in states the physics says cannot occur.

Two different things are going on and it is important to keep them separate:

Simulator Hardware
00 and 11 not exactly equal sampling noise — finite shots sampling noise and device asymmetry
01 and 10 appear at all never happens device noise — readout, gates, decoherence
More shots fixes it? yes — the ratio converges to 50/50 no — the 01/10 fraction converges to a nonzero value

That last row is the one to internalize. Sampling noise shrinks with more shots. Device noise does not. Running a million shots on hardware gives you a beautifully precise measurement of a wrong distribution. This is why error mitigation (Chapter 13) is a separate discipline from simply taking more data, and why the two must never be conflated.

What the four numbers can and cannot tell you

{'00': 1927, '11': 1989, '01': 91, '10': 89}. Four numbers, and three obvious questions. It is worth working out which of the three this run can actually answer, because the answer is one — and the habit of asking is the difference between reading a result and merely looking at it.

Question 1: is 11 really more likely than 00? The two differ by 62 counts, which looks like something. Restrict attention to the $1927 + 1989 = 3{,}916$ shots that landed in an allowed state. Under a fair split each count has mean 1,958 and the difference between them has standard deviation $\sqrt{3916} = 62.6$. The observed difference of 62 is 0.99 standard deviations. On its own, entirely ordinary.

But there is a reason to expect a real difference here, and Case Study 2 supplies it. Its two gate-free calibration circuits measure the loss out of each state directly: preparing $|00\rangle$ and immediately measuring it is wrong 4.79% of the time, preparing $|11\rangle$ only 2.81%. A Bell state is half of each, so the prediction is

$$\text{pred}(\texttt{00}) = 2048 \times 0.9521 = 1950 \qquad \text{pred}(\texttt{11}) = 2048 \times 0.9719 = 1990$$

— a genuine device asymmetry of about 40 counts, in the observed direction. Compare it to the data honestly:

   predicted difference (11 - 00)          40
   observed  difference (11 - 00)          62
   sampling sd of that difference          63

Both the prediction and zero sit within one standard deviation of what you measured. The asymmetry is real — Case Study 2 established it with circuits designed to isolate it — but you could not have discovered it from this run, and if you had reported the 62 as evidence for it you would have been right by luck.

Question 2: is 01 really more common than 10? They differ by 2 counts. The difference of two counts totalling 180 has standard deviation $\sqrt{180} = 13.4$, so 2 is 0.15 standard deviations — nothing.

The useful move is to turn the question around and ask what this run could have detected. A two-sigma difference needs 27 counts, meaning a split of about 103 versus 77:

   shots      impossible counts    smallest 01:10 imbalance detectable at 2 sigma
     4,096            ~180                          35%
    16,384            ~720                          16%
    65,536          ~2,880                           8%
   262,144         ~11,520                           4%

At 4,096 shots this experiment cannot see an imbalance below about 35%. Case Study 2's four directional readout rates span 0.61% to 3.17% — a factor of five between the extremes. A tool with a 35% floor is not going to resolve structure like that, and the near-equality of 91 and 89 is therefore consistent with Case Study 2's model rather than evidence for it. Case Study 2 gets its result by measuring the four rates separately, on circuits with no gates in them at all.

Question 3: what fraction landed in states that cannot occur? $180/4096 = 4.39\%$. The count has standard deviation 13, so this is $4.39\% \pm 0.32$ percentage points.

This is the question the run answers, to two significant figures, and it is exactly the number Case Study 2 predicts from the device's published calibration before looking. The other two questions needed a different experiment.

📊 What the Numbers Say — Three questions, one run, one answer.

Question Statistic Verdict
Is 11 favored over 00? 62 ± 63 counts Cannot tell. Real effect ≈ 40; noise ≈ 63
Is 01 favored over 10? 2 ± 13 counts Cannot tell. Floor is a 35% imbalance
What fraction is impossible? 4.39% ± 0.32 pp Answered. Two significant figures

The four counts came out of one job in one second and every one of them is real. Two of the three questions people ask of them are unanswerable from this data, and nothing in the output says so.

This is one of the book's recurring themes in its smallest form: the number that is easy to get is not the number that answers the question. The easy number here is the 62-count gap between 00 and 11, and it is the flattering one, because it looks like a discovery and it stops the search. The number that answers a question is 4.39%, and the way to learn something about the other two is to run a different circuit — which is precisely what Case Study 2 does, and why it uses two circuits containing no gates rather than more shots of this one.

More shots of the wrong circuit is not more information. Chapter 5 §5.4 gives you the arithmetic for deciding how many shots a given question needs; Chapter 27 §27.5 shows what it costs to skip that step.

Reading the bitstring

One convention, stated now and repeated throughout Part I because it causes more bugs than anything else in quantum programming:

Qiskit is little-endian. In the bitstring 01, the LEFT character is qubit 1 and the RIGHT character is qubit 0.

   bitstring:    0 1
                 │ └── qubit 0   (the rightmost character)
                 └──── qubit 1

So '01' means qubit 1 measured 0 and qubit 0 measured 1. For the Bell state this does not bite, because 00 and 11 are palindromes. The moment your circuit has an asymmetric answer — the first Grover implementation, say — it bites hard, and Chapter 26 is largely about the aftermath.

Not every framework agrees on this. Cirq and Q# order things differently. Chapter 18 puts the conventions side by side, and it is the most-consulted page in that chapter.

🔬 Honest Assessment — What you just proved, and what you did not.

You did: run a real quantum circuit on real quantum hardware, produce genuine entanglement, and measure it. The correlation in your results is real. The two-qubit state your device prepared could not be produced by any classical device with the same interaction pattern. That is a real quantum experiment, and forty years ago it would have been a publishable one.

You did not: compute anything a classical computer could not. Your laptop simulated this circuit exactly, in microseconds, more accurately than the quantum processor did. The quantum computer was slower, less accurate, and required a queue.

Both statements are true, and holding both is the disposition this book is trying to build. The hardware run is valuable pedagogically — it teaches you what noise is in a way no description can. It is not valuable computationally at this scale, and it will not be for two qubits ever.

If someone tells you they ran a Bell state on a quantum computer and therefore quantum advantage is here, you now know exactly why that does not follow.

2.8 When It Goes Wrong

Six failures account for most first-run problems. Learn to recognize them by their message.

ModuleNotFoundError: No module named 'qiskit'

The environment is not the one you installed into. Check:

import sys; print(sys.executable)

If that path is not inside your .venv, either the environment is not activated or you installed with a different pip. Reactivate and reinstall with python -m pip install.

IBMNotAuthorizedError / IBMInputValueError on the service constructor

Credentials. In order of likelihood: you have not run save_account; you pasted the token with stray whitespace; the token was regenerated on the dashboard (which invalidates the old one); or the channel/instance arguments no longer match the current platform (see the Version Note in §2.3).

Check what is actually saved:

from qiskit_ibm_runtime import QiskitRuntimeService
print(QiskitRuntimeService.saved_accounts().keys())

Re-run save_account(..., overwrite=True) with the snippet from your dashboard.

AttributeError on result[0].data.c

Wrong classical register name. See the pitfall in §2.6. Print result[0].data.keys().

The job sits in QUEUED for a very long time

Normal. You picked a busy device, or it is a busy time of day. Options: use least_busy(); submit and walk away, retrieving by job ID later; or check the dashboard's queue depths and choose manually. Do not cancel and resubmit — you lose your queue position.

TranspilerError mentioning basis gates or coupling

You skipped transpilation, or transpiled for the wrong backend. Every hardware submission needs a circuit already lowered to that device's instruction set:

pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_circuit = pm.run(qc)         # <- this, for THIS backend

Transpiling for backend A and submitting to backend B produces exactly this error, and it is a common mistake once you start iterating.

circuit.draw("mpl") raises an error about LaTeX

Missing pylatexenc. python -m pip install pylatexenc. The plain-text drawer, qc.draw(), always works and needs nothing.

Read the exception type before you read the message

Those six failures fall into three classes, and the class is given away by the exception type alone — before you have read a word of the message. Learning to sort them takes thirty seconds and saves the afternoon, because the three have nothing to do with each other.

   ModuleNotFoundError: No module named 'qiskit'
        -> the package is NOT INSTALLED in the interpreter you are running.   ENVIRONMENT problem.

   ImportError: cannot import name 'Aer' from 'qiskit'
        -> the package IS installed and importable; the NAME is gone.         VERSION problem.

   AttributeError: 'QuantumCircuit' object has no attribute 'add_calibration'
        -> the object exists and has lost a method.                           VERSION problem, late.

ModuleNotFoundError means Python never found the package at all. Go to §2.1: print sys.executable, check it is inside your .venv, reinstall with python -m pip. Nothing about Qiskit is involved and no amount of reading the release notes will help.

ImportError: cannot import name X from 'qiskit' means the opposite. Python found qiskit, opened it, and did not find X inside. Your environment is fine; your code is written against a different version. Every one of these, run against Qiskit 2.5.1, is a real line from a real tutorial:

   from qiskit import Aer          ImportError: cannot import name 'Aer' from 'qiskit'
   from qiskit import execute      ImportError: cannot import name 'execute' from 'qiskit'
   from qiskit import BasicAer     ImportError: cannot import name 'BasicAer' from 'qiskit'
   from qiskit import pulse        ImportError: cannot import name 'pulse' from 'qiskit'

Note the message names the package it searched — from 'qiskit' — which is your confirmation that the install worked. An ImportError naming a package is good news about your environment.

AttributeError is the same disease caught late. The import succeeded, the object was constructed, and the removal only surfaced when you called the method — possibly deep inside a run, possibly after a queue wait. This is why §2.1 insists on recording versions: an AttributeError on a Qiskit object at minute forty of a session is a version problem wearing a disguise.

The practical consequence, and the reason Case Study 1's colleague lost a week: almost every "Qiskit tutorial doesn't work" report is class two or three, not class one. Pre-1.0 code fails on its import line; 1.x code that touched pulse fails on an attribute. Neither is fixed by reinstalling, and both are fixed by checking the date on the code before you check your understanding.

🐛 Debug This — A circuit that runs and returns nonsense.

```python qc = QuantumCircuit(2, 2) qc.h(0) qc.cx(0, 1) qc.measure([0, 1], [0, 1])

sim = AerSimulator() result = sim.run(qc, shots=1024).result() # <-- no transpile print(result.get_counts()) ```

Symptom: on some versions this raises; on others it silently works. Either way it is wrong as a habit.

The real bug is more insidious. Consider:

```python qc = QuantumCircuit(2, 2) qc.h(0) qc.cx(0, 1)

forgot to measure

result = sim.run(transpile(qc, sim), shots=1024).result() print(result.get_counts()) ```

Symptom: QiskitError: 'No counts for experiment ...'

Cause: no measurement, so no classical bits, so nothing to count. The circuit ran perfectly and produced a quantum state that was then thrown away.

Fix: add qc.measure_all() or explicit measure calls.

This is the most common beginner bug in Qiskit, and it teaches something real: the quantum state is not the output. Only measurement produces output. Everything before it is setup.

2.9 Summary

You installed qiskit, qiskit-aer, and qiskit-ibm-runtime into an isolated virtual environment, verified the versions, and recorded them — because in this field an undated result is an unreproducible one.

You created a free IBM Quantum account, saved an API token to your home directory with save_account, and never put it in a source file.

You built a Bell state: h(0), cx(0, 1), measure. You predicted its output before running anything — 50/50 between 00 and 11, nothing else.

On the Aer simulator, with a fixed seed, you got exactly that. Deviations from an even split are sampling noise and shrink as shots grow.

On real hardware — or on a fake backend built from a real device snapshot — you got the same two peaks plus a few percent of shots in 01 and 10, states whose amplitude is exactly zero. On a well-chosen qubit pair that residue runs around 2–5%; on a poor pair it can exceed 10%. It is device noise: readout misclassification, two-qubit gate error, decoherence, and crosstalk. It does not shrink with more shots, which is why error mitigation is a separate discipline from taking more data.

You saw that the circuit you wrote is not the circuit that ran. H and CNOT do not exist on the device; the transpiler rewrote them into rz, sx, and ecr, chose physical qubits, and took the depth from 3 to 8. That expansion is normal and it compounds.

You learned the convention that will cause you the most trouble: Qiskit is little-endian, so the rightmost character of a bitstring is qubit 0.

And you learned the six error messages that account for most first-run failures.

The most important thing you now have is a reference. You know what the ideal answer looks like and what the real answer looks like, and you have seen the gap with your own data. Everything in Parts II and V is about that gap.

🧱 Project Checkpointbackends.py v0.

Add the first real module to vqelab: a single function that returns a backend, so that nothing else in the project ever has to know whether it is running on a simulator or a QPU.

```python

vqelab/backends.py -- v0

def get_backend(kind: str = "sim"): """Return a backend. kind is "sim" or "hardware".""" if kind == "sim": from qiskit_aer import AerSimulator return AerSimulator() if kind == "hardware": from qiskit_ibm_runtime import QiskitRuntimeService return QiskitRuntimeService().least_busy(operational=True, simulator=False) raise ValueError(f"unknown backend kind: {kind!r}") ```

Nine lines, and it is the most important architectural decision in the project. Case Study 1 in Chapter 1 identified "isolate framework-specific code behind an interface" as the thing that converts a catastrophic migration into an inconvenient one. This is that interface. By Chapter 17 it will hide five different platforms behind the same call, and no other module in vqelab will have changed a line.

The checkpoint file also adds a credential check that fails loudly and early with a useful message, rather than at submission time with a stack trace. See code/project-checkpoint.py.


Next: Chapter 3 — what those gates actually did. You will apply every single-qubit gate, watch the state move on the Bloch sphere, inspect the statevector directly, and build the bridge between "a gate" and "a $2\times2$ matrix."