Skip to content
Kartikey Purohit
← Course overview

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  1
  • fold=N — for 'text': max drawing width in characters before the diagram wraps to a new "row" (default 80, fold=-1 disables). Continuation rows start with «. For 'mpl' it is the number of layers per row. Verified with a 12-gate 1-qubit circuit and fold=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 empty q_1 wire; with idle_wires=False the q_1 line disappears — the circuit itself still has 3 qubits.

⚠️ Common exam trap: reverse_bits=True (and idle_wires=False) change only the drawing. Counts, bitstrings, statevectors, and the circuit object are untouched. To actually reverse qubit order in the circuit use circuit.reverse_bits() (a method that returns a new circuit).


2. Reading circuit diagrams

  • Time flows left → right; leftmost gates execute first.
  • q_0 is the TOP wire by default (opposite of most textbook bitstring layouts).
  • Control = solid dot ; target = boxed X (⊕). The vertical line connects them.
  • cz is symmetric: two dots (─■─ / ─■─). swap is two X marks. ccx has 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 counts

All 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.

  • legend list length must match the number of datasets.
  • sort='hamming' orders by Hamming distance from target_string.
  • plot_distribution shows probabilities / quasi-probabilities (bars sum to 1, can be negative for mitigated quasi-distributions); plot_histogram shows 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 gives probabilities_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 values

All 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 vector

Verified: 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: dumps returns a string ("dump-string"); dump writes to a file object and returns None. Same convention in qasm3, qasm2, and Python's json. loads takes a string; load takes 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)

  1. Histogram/bitstring labels are little-endian — rightmost char = q0.
  2. reverse_bits=True in draw() changes display only; circuit.reverse_bits() (method) actually returns a reordered circuit.
  3. dumps → string, dump → file object; loads ← string, load ← file.
  4. Entangled qubit in plot_bloch_multivectorzero-length arrow, not a random direction.
  5. Qsphere: size = probability, color = phase (never the reverse).
  6. q_0 is drawn on top; time runs left → right.
  7. QASM3 measurement is an assignment c = measure q; — QASM2 uses measure q -> c;.
  8. plot_bloch_vector takes a vector [x,y,z]; plot_bloch_multivector takes a state.

9. Coding Labs — predict, then run

Lab 1 — Hand-written QASM3, verified with Operator

  1. Without touching Python, hand-write a QASM3 program that prepares the "signed GHZ" state (|000⟩ − |111⟩)/√2.
  2. Predict the exact ket Statevector.draw('latex_source') will print.
  3. Load it with qasm3.loads, build the same circuit natively (h, cx, cx, z), and check Operator(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.