Case Study: Taking a Circuit from Simulator to Hardware
Executive Summary
The circuit works. It has worked for weeks on AerSimulator. You submit it to a real backend and get an error about ISA circuits, then a distribution that looks nothing like the simulation, then a queue time longer than your afternoon.
This case study is the checklist nobody writes down: every difference between a simulated run and a hardware run, in the order you will encounter them. Working through it once converts hardware from mysterious to merely noisy.
Skills applied
- Transpiling to a backend's ISA and understanding why it is mandatory (§8.9).
- Choosing between Sampler and Estimator primitives (§8.11).
- Reading backend properties to select qubits (§8.13).
- Interpreting hardware results against a noisy simulation rather than an ideal one.
Phase 1: The circuit that works locally
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
qc = QuantumCircuit(3, 3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.measure([0, 1, 2], [0, 1, 2])
sim = AerSimulator()
result = sim.run(qc, shots=4096).result()
print(result.get_counts()) # {'000': ~2048, '111': ~2048}
Clean GHZ state, two outcomes, nothing else. This is the baseline against which everything below is a degradation.
Phase 2: Difference one — the circuit must be rewritten
Submit this circuit to hardware and it is rejected. Hardware runs only its native instruction set on its actual coupling map; h and cx between arbitrary qubits are abstractions.
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit import transpile
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
isa_circuit = transpile(qc, backend, optimization_level=3, seed_transpiler=42)
print(f"depth {qc.depth()} -> {isa_circuit.depth()}")
print(f"gates {len(qc.data)} -> {len(isa_circuit.data)}")
Typical result: depth 4 → 11, gates 6 → 18. The circuit tripled in size before executing a single shot. On a device where qubits 0, 1, 2 are not mutually adjacent, SWAPs appear here.
Rule. Always inspect the transpiled circuit, not the one you wrote. The transpiled version is what runs and what determines fidelity.
Phase 3: Difference two — primitives, not run
Modern hardware access goes through primitives, which handle batching, error mitigation options, and session management:
from qiskit_ibm_runtime import SamplerV2 as Sampler
sampler = Sampler(mode=backend)
job = sampler.run([isa_circuit], shots=4096)
result = job.result()
counts = result[0].data.c.get_counts()
Note result[0].data.c — the classical register's name. If you created the circuit with QuantumCircuit(3, 3) the register is named c by default; a custom ClassicalRegister name changes this path. This trips people up constantly.
Use Sampler when you want a distribution over bitstrings. Use Estimator when you want $\langle O \rangle$ for an observable — it handles the basis rotations for you and returns a number with an error estimate, which is almost always what a variational algorithm wants.
Phase 4: Difference three — the results
Hardware, 4,096 shots:
| Outcome | Simulator | Hardware |
|---|---|---|
000 |
2,043 | 1,721 |
111 |
2,053 | 1,606 |
001 |
0 | 178 |
010 |
0 | 149 |
100 |
0 | 166 |
011 |
0 | 92 |
101 |
0 | 84 |
110 |
0 | 100 |
81% in the two correct outcomes, 19% scattered. Is the device broken?
Compare against the right baseline — a noisy simulation using the backend's own properties:
from qiskit_aer import AerSimulator
noisy = AerSimulator.from_backend(backend)
noisy_counts = noisy.run(isa_circuit, shots=4096).result().get_counts()
The noisy simulation gives roughly 78–83% in the correct outcomes. The hardware is behaving exactly as its published error rates predict. Nothing is broken; the expectation was wrong.
Rule. The ideal simulator is not the right comparison for a hardware run. Compare against a noise model built from the backend's current properties, and treat a match as success.
Phase 5: Difference four — qubit selection matters
The transpiler chose a qubit triple. You can do better by reading the calibration data:
props = backend.properties()
for edge in backend.coupling_map:
err = props.gate_error('cx', edge)
print(edge, f"{err:.4f}")
Error rates across pairs on the same device commonly vary by 3–5×, and one or two qubits are often markedly worse than the rest. Constraining the layout to a good triple:
isa_circuit = transpile(qc, backend, initial_layout=[12, 13, 14],
optimization_level=3, seed_transpiler=42)
On a typical device this moves the correct-outcome fraction from ~81% to ~88% with no change to the algorithm whatsoever.
Phase 6: Difference five — everything else
Four practical realities with no simulator analogue:
- Queue time. Jobs wait. Batch related circuits into one job, or use a Session to hold an allocation across a variational loop, or your optimizer will spend most of its wall-clock time in a queue.
- Calibration drift. Results from Tuesday do not necessarily reproduce on Wednesday. Record
backend.properties().last_update_datewith every result. - Shot cost. Shots are metered. Estimate the precision you need first: $1/\sqrt N$ means 10,000 shots buys 1% and 1,000,000 buys 0.1%. Deciding you need 0.1% is a 100× budget decision.
- Non-determinism. Even at a fixed seed, the device is stochastic. Report error bars.
Discussion Questions
- Why is the noisy simulator the correct baseline, and what does it mean when hardware underperforms it?
- Qubit selection bought 7 percentage points with no algorithmic change. What does that imply for comparing results between research groups?
- Sampler and Estimator both exist; the Estimator could be built from the Sampler. Why is it provided separately?
- Shot noise scales as $1/\sqrt N$. For a variational algorithm evaluating an energy 500 times per optimization run, work out the shot budget for 1% precision.
Your Turn: Extensions
- Run the GHZ circuit on a fake backend, then on real hardware, and tabulate all three distributions.
- Write a helper that ranks all connected qubit triples by summed CNOT error and picks the best.
- Repeat the same job on three different days and quantify the drift.
- Redo the experiment with
Estimatorand $\langle ZZZ \rangle$; compare its error bar against your own from the Sampler counts.
Key Takeaways
- Hardware executes ISA circuits only: transpile, then inspect what transpilation produced.
- Primitives (Sampler for distributions, Estimator for expectation values) are the modern access path;
execute()is gone. - Compare hardware to a noise-model simulation, never to the ideal one — a match means success.
- Qubit selection is a free and substantial fidelity lever; error rates vary several-fold across one chip.
- Queue time, calibration drift, and metered shots are real constraints with no simulator counterpart. Plan for them.