Chapter 6 — Key Takeaways (OpenQASM)

The serialization page. Consult it whenever a circuit crosses a boundary.

What QASM is for

APPLICATION → ALGORITHM → CIRCUIT → [OpenQASM] → TRANSPILER → PULSE → QPU
  • A common target. Turns $N$ frameworks × $M$ devices into $N + M$. Same argument as LLVM IR.
  • A stable boundary. A specification moves slower than a framework, so it is what you write to disk.
  • Where the truth is written down. A QuantumCircuit is a Python object; QASM has a grammar.
  • Readable at the right level — low enough to show what runs, high enough to read. Pulse schedules are neither.

OpenQASM 3, minimal

OPENQASM 3.0;
include "stdgates.inc";
bit[2] c;
qubit[2] q;
h q[0];
cx q[0], q[1];
c[0] = measure q[0];      // measurement is an ASSIGNMENT
c = measure q;            // whole-register form

// and /* */ comments · free-form whitespace · semicolons · lowercase gate names.

The API

from qiskit import qasm2, qasm3

qasm3.dumps(qc)                 # -> str          ✓
qasm3.loads(text)               # str -> circuit  ✓
qasm2.dumps(qc) / qasm2.loads(text)

qc.qasm()                       # ✗ REMOVED in Qiskit 1.0

⚠️ dump and load are not symmetric. qasm3.dump(circuit, stream) takes a file object; qasm3.load(filename) takes a path. Passing an open file to load raises a TypeError that reads like a broken handle.

Habit that sidesteps it: use dumps/loads with strings and do your own file I/O. You also control the encoding and can prepend a comment line.

★ Reading transpiled QASM

gate ecr _gate_q_0, _gate_q_1 { s _gate_q_0; sx _gate_q_1; cx _gate_q_0, _gate_q_1; x _gate_q_0; }
bit[2] c;
rz(-pi) $0;  sx $0;  rz(-pi/2) $1;  sx $1;  rz(-pi) $1;
ecr $1, $0;
rz(-pi/2) $0; sx $0; rz(pi/2) $0;  rz(pi/2) $1; sx $1; rz(pi/2) $1;
c[0] = measure $0;  c[1] = measure $1;
Feature Meaning
$0`, `$1 physical qubits — the layout decision, written down. Record it with every result
gate ecr ... { } ecr is not in stdgates.inc, so it is defined in terms of gates you know
no h, no cx they do not exist on this device
7 × rz virtual — zero duration, zero error

Real cost: 4 sx + 1 ecr = five operations out of twelve instructions. Instruction count is not cost.

Extract four numbers from any transpiled circuit:

Number From Tells you
physical qubits the $n identifiers which layout
two-qubit gate count count ecr/cz/cx whether SWAPs were inserted (compare to what you wrote)
real pulses count sx + x true single-qubit cost
depth isa.depth() coherence budget consumed

Diff the QASM at two optimization levels — a precise record of every compiler decision:

difflib.unified_diff(qasm_at(qc, 0), qasm_at(qc, 3), "level 0", "level 3", lineterm="")

An empty diff is informative: it rules out compilation as the cause of a problem.

★ What a round trip loses

Preserved?
unitary (up to phase)
custom gates, as named gates
gate counts
global phase ✗ — silently
parameter names ✗ — theta[0]_theta_0_
gate names ✗ — a custom h returns as h_0
layout ✗ — a 127-qubit ISA circuit returns as 2 qubits

All four losses share one property (§6.6): none is visible to count_ops, and none is visible to Operator.equiv. Each is a property of the circuit that is not a gate, and both checks are gate-level. Enumerate them; do not test for them.

Global phase. Harmless except under control, where it becomes a relative phase — phase kickback. Measured: controlled-original → {'1': 1757}, controlled-archived → {'0': 1758}. The answer inverted.

Why every plausible test misses it:

Test Result Why blind
Operator.equiv passes compares up to global phase, by design
gate counts passes a phase is not a gate
measurement statistics passes the distributions are genuinely identical
QASM round-trips to a fixed point passes idempotent ≠ lossless

A property unobservable in isolation can be load-bearing in composition. Test the composition.

Parameter names. [ and ] are illegal in an OpenQASM identifier. Fails loudly with a KeyError, and a ParameterVector no longer round-trips as a vector.

Do not reproduce the exporter's mangling rule — it is an implementation detail. Normalize both sides by stripping non-alphanumerics: theta[0] and _theta_0_ both → theta0.

The fix — record out-of-band in comments (parsers ignore them; the file stays portable):

PHASE_COMMENT = "// vqelab: global_phase = "
PARAMS_COMMENT = "// vqelab: parameters = "

Or better, fail loudly at the boundary: raise rather than silently archive information the format cannot carry.

QASM 2 vs QASM 3

QASM 2 QASM 3
registers qreg q[2]; creg c[2]; qubit[2] q; bit[2] c;
measurement measure q[0] -> c[0]; c[0] = measure q[0];
include qelib1.inc stdgates.inc
free parameters QASM2ExportError input float[64] theta;
classical control register equality only ✓ full if/else, loops
physical qubits $0
timing duration, delay

QASM 2 is a circuit format; QASM 3 is a program format. Use QASM 2 for maximum interchange compatibility on static bound circuits; QASM 3 for anything with parameters, control flow, or timing.

QASM vs QPY

QASM 3 QPY
cross-framework
human-readable
stable across versions ✓ (a spec) ✗ (Qiskit-versioned)
preserves global phase
preserves every Qiskit detail

Use QASM to communicate and archive. Use QPY to checkpoint within one Qiskit version. Save both.

Common pitfalls

  • Using qc.qasm() from an old tutorial.
  • Passing an open file to qasm3.load().
  • Trusting equiv in a round-trip test when the phase matters.
  • Assuming specification-valid QASM 3 is accepted by every tool that claims QASM 3 support — test against the tool, not the spec.
  • Editing QASM by hand as your primary workflow. Keep the source of truth in the framework; regenerate.
  • Counting rz as cost.

Project piece added this chapter

vqelab/qasm.pyto_qasm, from_qasm, archive, restore, check_round_trip.

Records both the global phase and the original parameter names as comments, so an archived ansatz restores with theta[0]theta[3] intact and its phase recovered. Chapter 22's QPE circuits will be used as controlled operations, and the habit has to be in place before it is needed.