Day 7 — Visualization & QASM
Lesson
Syllabus coverage: §6 Visualize circuits, measurements, and states (11%) + §8 Operate with OpenQASM (6%) = 17% of the exam (~12 questions).
All code verified with qiskit 2.5.1 (MPLBACKEND=Agg). Optional deps used: matplotlib, pylatexenc (mpl/latex drawers), sympy + ipython (Statevector.draw('latex')), seaborn (qsphere).
1. circuit.draw() — the three outputs
from qiskit import QuantumCircuit
bell = QuantumCircuit(2, 2)
bell.h(0)
bell.cx(0, 1)
bell.measure([0, 1], [0, 1])
print(bell.draw('text')) # ASCII art (the DEFAULT)Verified output:
┌───┐ ┌─┐
q_0: ┤ H ├──■──┤M├───
└───┘┌─┴─┐└╥┘┌─┐
q_1: ─────┤ X ├─╫─┤M├
└───┘ ║ └╥┘
c: 2/═══════════╩══╩═
0 1| Output | Returns | Needs |
|---|---|---|
'text' (default) |
TextDrawing (prints as ASCII) |
nothing |
'mpl' |
matplotlib.figure.Figure |
matplotlib + pylatexenc |
'latex' |
PIL image rendered via LaTeX | LaTeX toolchain |
'latex_source' |
raw LaTeX string (\documentclass...) |
pylatexenc |
Key kwargs (all display-only — none modify the circuit):
reverse_bits=True— flips vertical qubit order so the most-significant qubit is on top (matches bitstring reading order). Verified:
┌───┐ ┌─┐
q_1: ─────┤ X ├───┤M├
┌───┐└─┬─┘┌─┐└╥┘
q_0: ┤ H ├──■──┤M├─╫─
└───┘ └╥┘ ║
c: 2/═══════════╩══╩═
0 1fold=N— for'text': max drawing width in characters before the diagram wraps to a new "row" (default 80,fold=-1disables). Continuation rows start with«. For'mpl'it is the number of layers per row. Verified with a 12-gate 1-qubit circuit andfold=20:
┌───┐┌───┐┌───┐»
q: ┤ H ├┤ T ├┤ H ├»
└───┘└───┘└───┘»
« ┌───┐┌───┐┌───┐»
«q: ┤ T ├┤ H ├┤ T ├»
...idle_wires=False— hides wires with no instructions. Verified (h(0); cx(0,2); measure(2,0)on 3 qubits): the default shows an emptyq_1wire; withidle_wires=Falsetheq_1line disappears — the circuit itself still has 3 qubits.
⚠️ Common exam trap:
reverse_bits=True(andidle_wires=False) change only the drawing. Counts, bitstrings, statevectors, and the circuit object are untouched. To actually reverse qubit order in the circuit usecircuit.reverse_bits()(a method that returns a new circuit).
2. Reading circuit diagrams
- Time flows left → right; leftmost gates execute first.
q_0is the TOP wire by default (opposite of most textbook bitstring layouts).- Control = solid dot
■; target = boxedX(⊕). The vertical line connects them. czis symmetric: two dots (─■─/─■─).swapis twoXmarks.ccxhas two dots + one boxed X.
Verified — direction matters:
cx(0, 1) cx(1, 0)
┌───┐
q_0: ──■── q_0: ┤ X ├
┌─┴─┐ └─┬─┘
q_1: ┤ X ├ q_1: ──■──
└───┘Verified — swap(0,1); cz(1,2); ccx(0,1,2):
q_0: ─X──────■──
│ │
q_1: ─X──■───■──
│ ┌─┴─┐
q_2: ────■─┤ X ├
└───┘Verified — GHZ (h(0); cx(0,1); cx(1,2)) gives (|000⟩+|111⟩)/√2:
┌───┐
q_0: ┤ H ├──■───────
└───┘┌─┴─┐
q_1: ─────┤ X ├──■──
└───┘┌─┴─┐
q_2: ──────────┤ X ├
└───┘measure_all() adds a barrier and its own meas classical register (verified: register line reads meas: 2/).
3. Counts visualization — plot_histogram / plot_distribution
from qiskit.visualization import plot_histogram, plot_distribution
counts = {'00': 505, '11': 495, '01': 12, '10': 10}
plot_histogram(counts) # bars labelled by bitstring
plot_histogram([c1, c2], legend=['ideal', 'noisy']) # multiple datasets side by side
plot_histogram(counts, sort='value_desc') # sort: 'asc','desc','value','value_desc','hamming'
plot_histogram(counts, number_to_keep=2) # top-2 bars + one aggregated 'rest' bar
plot_distribution(counts) # y-axis = quasi-probabilities, not raw countsAll calls above verified to run. number_to_keep=2 on {'00':40,'01':30,'10':20,'11':10} produces x-labels ['00', '01', 'rest'] (verified) — the N most frequent survive, everything else is lumped into rest.
legendlist length must match the number of datasets.sort='hamming'orders by Hamming distance fromtarget_string.plot_distributionshows probabilities / quasi-probabilities (bars sum to 1, can be negative for mitigated quasi-distributions);plot_histogramshows count-derived bars.
⚠️ Common exam trap — bitstring order: histogram labels are little-endian: the rightmost character is qubit 0. A bar labelled
'01'means q1=0, q0=1. Verified:x(0)on a 2-qubit circuit givesprobabilities_dict() == {'01': 1.0}.
Bell-signature check (verified): Statevector of H+CX gives {'00': 0.5, '11': 0.5} — two bars only, at 00 and 11.
4. State visualization
4.1 Bloch sphere positions (memorize!)
| State | Bloch position | Verified ⟨X⟩,⟨Y⟩,⟨Z⟩ |
|---|---|---|
| |0⟩ | +Z (north pole) | (0, 0, +1) |
| |1⟩ | −Z (south pole) | (0, 0, −1) |
| |+⟩ = (|0⟩+|1⟩)/√2 | +X | (+1, 0, 0) |
| |−⟩ = (|0⟩−|1⟩)/√2 | −X | (−1, 0, 0) |
| |i⟩ = |r⟩ = (|0⟩+i|1⟩)/√2 | +Y | (0, +1, 0) |
| |−i⟩ = |l⟩ = (|0⟩−i|1⟩)/√2 | −Y | (0, −1, 0) |
Every row verified with:
from qiskit.quantum_info import Statevector, Pauli
sv = Statevector.from_label('r') # 'r' = |+i>, 'l' = |-i>
[sv.expectation_value(Pauli(p)).real for p in ('X', 'Y', 'Z')] # -> [0.0, 1.0, 0.0]Useful gate walk: h(0) sends |0⟩→|+⟩ (+X); a following sdg(0) sends |+⟩→|−i⟩ (−Y); s(0) would send |+⟩→|+i⟩ (+Y).
4.2 The plot functions
from qiskit.visualization import (plot_bloch_vector, plot_bloch_multivector,
plot_state_qsphere, plot_state_city, plot_state_hinton, plot_state_paulivec)
plot_bloch_vector([0, 1, 0]) # cartesian [x, y, z] -> +Y
plot_bloch_vector([1, 3.14/2, 3.14/2], coord_type='spherical') # [r, theta, phi] (theta from +Z)
plot_bloch_multivector(sv) # ONE sphere PER QUBIT (accepts Statevector/DensityMatrix)
plot_state_qsphere(sv)
plot_state_city(sv) # 3D bars: real part + imaginary part of density matrix
plot_state_hinton(dm) # square sizes = matrix element magnitudes
plot_state_paulivec(sv) # bar chart of Pauli expectation valuesAll verified to run and return matplotlib figures.
What appears:
plot_bloch_vector— a single sphere with one arrow; takes a raw vector, not a state.plot_bloch_multivector— one Bloch sphere per qubit, each showing that qubit's reduced state. Entangled qubits ⇒ zero-length vectors (arrow tip at the sphere's center). Verified for the Bell state: partial trace of either qubit is I/2 and the per-qubit Bloch vector is (0, 0, 0) on both spheres.plot_state_qsphere— one sphere for the whole state; each basis state is a blob at a latitude set by Hamming weight (|0…0⟩ north, |1…1⟩ south). Blob size = probability (amplitude magnitude), blob color = relative PHASE. Same size + different colors ⇒ equal amplitudes with different phases (e.g. |+⟩ vs |−⟩).plot_state_city— two 3D "cityscapes": Re(ρ) and Im(ρ). Bell state: four equal towers at the corners of Re(ρ), flat Im(ρ).plot_state_paulivec— expectation value per Pauli string. Verified Bell values: II=+1, XX=+1, YY=−1, ZZ=+1, everything else 0.
4.3 Statevector.draw()
sv = Statevector.from_instruction(bell_no_meas)
sv.draw('latex') # pretty ket rendering (needs sympy + IPython)
sv.draw('latex_source') # the raw LaTeX string
sv.draw('text') # plain amplitude vectorVerified: Bell draw('latex_source') returns exactly
\frac{\sqrt{2}}{2} |00\rangle+\frac{\sqrt{2}}{2} |11\rangle.
Also available: qiskit.visualization.array_to_latex(matrix) for operators/arrays.
5. OpenQASM 3 (qiskit.qasm3)
from qiskit import qasm3
text = qasm3.dumps(circuit) # -> str
with open("prog.qasm", "w") as f:
qasm3.dump(circuit, f) # writes to an OPEN FILE OBJECT
qc = qasm3.loads(program_string) # str -> QuantumCircuit
qc = qasm3.load("prog.qasm") # filename -> QuantumCircuit⚠️ Common exam trap:
dumpsreturns a string ("dump-string");dumpwrites to a file object and returnsNone. Same convention inqasm3,qasm2, and Python'sjson.loadstakes a string;loadtakes a file/filename.
Verified qasm3.dumps(bell) output — anatomy of a QASM3 program:
OPENQASM 3.0; // version header
include "stdgates.inc"; // standard gate library (h, cx, rz, ...)
bit[2] c; // classical register (typed!)
qubit[2] q; // quantum register
h q[0];
cx q[0], q[1]; // first argument = control
c[0] = measure q[0]; // measurement is an ASSIGNMENT in QASM3
c[1] = measure q[1];Round-trip verified: qasm3.loads(qasm3.dumps(bell)) reproduces the same ops, and Operator(...).equiv(...) confirms unitary equivalence. Also verified dump to a StringIO produces the identical text as dumps.
QASM3 vs QASM2 differences (exam favorite)
| Feature | OpenQASM 2 | OpenQASM 3 |
|---|---|---|
| Header | OPENQASM 2.0; + include "qelib1.inc"; |
OPENQASM 3.0; + include "stdgates.inc"; |
| Registers | qreg q[2]; creg c[2]; |
qubit[2] q; bit[2] c; (typed declarations) |
| Measurement | measure q[0] -> c[0]; |
c[0] = measure q[0]; |
| Classical control | only if (creg==int) gate; (one gate, whole register) |
full block if (...) { ... } else { ... }, for, while |
| Runtime inputs | none | input float theta; (unbound circuit parameter) |
| Classical types | creg bits only | int, float, bool, bit, arrays, ... |
Verified: exporting a dynamic circuit (if_test on one clbit) with qasm3.dumps yields if (c[0]) { x q[1]; }, while qasm2.dumps on the same circuit raises QASM2ExportError ("OpenQASM 2 only supports register-equality conditions").
Verified: qasm3.loads of a program containing input float theta; + rx(theta) q[0]; produces a circuit with ParameterView([Parameter(theta)]) — a normal parameterized circuit you can bind or pass values for via a PUB.
6. OpenQASM 2 interoperability
from qiskit import qasm2, QuantumCircuit
s = qasm2.dumps(circuit) # -> str
qc = qasm2.loads(s) # str -> circuit
qc = QuantumCircuit.from_qasm_str(s) # classic alternative importer
qc = QuantumCircuit.from_qasm_file("f.qasm")Verified qasm2.dumps(bell):
OPENQASM 2.0;
include "qelib1.inc";
qreg q[2];
creg c[2];
h q[0];
cx q[0],q[1];
measure q[0] -> c[0];
measure q[1] -> c[1];Reading tip: measure q -> c; (whole register) expands to per-bit measurements q[i]→c[i].
7. Runtime REST API awareness (§8.3)
You do not need the Python SDK to run a job: the IBM Quantum Runtime REST API accepts job submissions by POST /v1/jobs with a JSON payload whose params.pubs contain the circuit as an OpenQASM 3 string, plus the program id (sampler/estimator) and backend name; auth is a bearer/API token. Know that this exists and that QASM3 is the interchange format — no endpoint memorization needed.
8. Trap recap (60-second review)
- Histogram/bitstring labels are little-endian — rightmost char = q0.
reverse_bits=Trueindraw()changes display only;circuit.reverse_bits()(method) actually returns a reordered circuit.dumps→ string,dump→ file object;loads← string,load← file.- Entangled qubit in
plot_bloch_multivector⇒ zero-length arrow, not a random direction. - Qsphere: size = probability, color = phase (never the reverse).
q_0is drawn on top; time runs left → right.- QASM3 measurement is an assignment
c = measure q;— QASM2 usesmeasure q -> c;. plot_bloch_vectortakes a vector[x,y,z];plot_bloch_multivectortakes a state.
9. Coding Labs — predict, then run
Lab 1 — Hand-written QASM3, verified with Operator
- Without touching Python, hand-write a QASM3 program that prepares the "signed GHZ" state (|000⟩ − |111⟩)/√2.
- Predict the exact ket
Statevector.draw('latex_source')will print. - Load it with
qasm3.loads, build the same circuit natively (h,cx,cx,z), and checkOperator(loaded).equiv(Operator(reference)).
Lab 2 — Predict the ASCII art
For the circuit below, write down on paper the full draw('text') output — box positions, the P(π/2) label, swap crosses, measure arrows and clbit indices — for BOTH reverse_bits=False and True. Then run and diff against your prediction.
import numpy as np
qc = QuantumCircuit(3, 2)
qc.h(0)
qc.cp(np.pi/2, 0, 2)
qc.swap(0, 2)
qc.measure(2, 0)
qc.measure(0, 1)Lab 3 — Bloch detective
Two mystery circuits:
A = QuantumCircuit(2); A.h(0); A.sdg(0); A.x(1)
B = QuantumCircuit(2); B.h(0); B.cx(0, 1)Predict the Bloch arrow (x, y, z) each of the four spheres of plot_bloch_multivector would show, then verify numerically with Statevector.from_instruction(...).expectation_value(Pauli(p), [qubit]) for p ∈ {X, Y, Z}.
Solutions in solution-bank.md.