Part III. Circuit Model · Chapter 16
Circuit Notation as a Programming Language
A circuit diagram is a program: wires are registers, boxes are instructions, and time flows left to right. Read it as ordered operations and you can trace any small circuit by hand before a simulator finishes warming up.
In this chapter 8 sections
Reader question. How should a quantum circuit diagram be read as an executable program with explicit time, scope, and measurement semantics?
Read wires as ordered state carriers, columns as operations that may run concurrently, multi-wire symbols as one joint gate, and measurements as classical-output boundaries; then translate the diagram into an ordered instruction sequence with declared qubit and bit mappings.
- This chapter does not define every vendor's circuit syntax or imply visual left-to-right spacing is physical duration.
- It does not treat equivalent drawings as equal-cost compiled programs.
The grammar carried by a wire
Define wire identity, state flow, and qubit order.
A circuit diagram is a compact syntax for state evolution. Time runs left to right. Each horizontal wire is a qubit register. A box on one wire applies a one-qubit unitary there while the identity acts on every other wire. A vertical controlled operation applies one joint multi-qubit unitary whose action depends on the control's basis value.
The habit that makes circuits readable is to expand them into a sequence:
- Initialize every wire, usually to .
- Apply each one-qubit gate in left-to-right order.
- Apply each controlled gate as one joint operation on the full state.
- Measure the selected wires.
- Record the classical bits.
That sequence can be traced by hand, simulated with a statevector, or compiled through an SDK; three executions of the same program at three levels of fidelity.
Evidence boundary. Circuit diagrams encode ordered quantum operations and measurements on named wires. [Michael A. Nielsen] [John Preskill]
Columns express dependencies, not typography
Recover a partial execution order from gate placement.
Every closed-system gate in the diagram must satisfy . A drawn box whose matrix fails that test is not a valid isolated quantum gate, whatever the documentation says. Measurement plays by different rules: it emits a classical outcome and changes the state description, so it is not a unitary box and cannot be undone by drawing another box.
Two numbers belong to every circuit reading. Width is the number of wires the program needs. Depth is the number of sequential gate layers once dependencies are respected. A clean diagram may have modest abstract depth while compilation raises the physical depth sharply; a device with a different native gate set or sparse connectivity forces decompositions and swaps. Depth and width are part of what the diagram says, not merely part of the performance postmortem.
Evidence boundary. OpenQASM 3 provides an explicit textual representation for quantum/classical instructions and timing/control constructs. [openqasm3]
One symbol may own several wires
Translate controls, targets, swaps, and measurement.
Evidence boundary. Circuit semantics and physical execution cost are distinct because compilation can alter gates and schedule. [Lieven M. K. Vandersypen et al.] [openqasm3]
Notation contract: Wires q[0..n−1], classical bits c[...]; diagram time left-to-right; matrix composition convention cross-referenced; measurement basis labeled.
A Bell diagram becomes instructions
Produce an exact instruction list and state trace.
The closest classical analog is a typed intermediate representation with unusual semantics. Registers have width. Instructions have arity. Some instructions commute; others must stay ordered. Measurement writes classical bits, and later operations may be classically controlled on those bits.
When a circuit gives the wrong answer, debug by layer:
- Diagram syntax; did you draw what you meant?
- Gate sequence; does the expansion match the drawing, with control and target in the right places?
- Ideal statevector trace; does the math produce the intended state?
- Compiled circuit; did transpilation blow up the depth or reorder what must stay ordered?
- Hardware result; do the counts match the compiled circuit within sampling noise?
A syntax mistake is a different failure from a swapped control-target convention; a correct ideal trace can still fail after transpilation if depth grows too large; a correct compiled circuit can still produce poor counts on noisy hardware. Naming the layer is half the fix.
Ambiguities a serializer must reject
Expose missing basis, reversed order, and unlabeled classical bits.
Reject an instruction when a control is unlabeled, a measurement lacks a classical destination, a multi-qubit gate repeats a wire, a parameter lacks units or expression grammar, or two operations claim the same wire in one scheduled moment without defined ordering. A parser should not guess from geometry. The source diagram, intermediate representation, and hardware schedule are distinct objects with explicit conversion rules.
Circuit diagram parser fixture
| Field | Reader-visible record |
|---|---|
| Format | OpenQASM 3 snippet, canonical instruction JSON, and state trace |
| Verification | Round-trip test parses/serializes without changing operations, qubit mapping, dependencies, or expected Bell counts. |
| Availability | Published companion and deterministic command |
Reference state-vector simulator is the executable companion used by this page.
cd labs && python -m unittest tests.test_simulator -vExecutable reference fixture
Run with Python 3.11 or later. The final assertion is the chapter-level pass condition for this small instance.
import json
import math
SOURCE = """OPENQASM 3;
qubit[2] q;
bit[2] c;
h q[0];
cx q[0], q[1];
c[0] = measure q[0];
c[1] = measure q[1];
"""
def indexed(token, register, size):
prefix = register + "["
if not token.startswith(prefix) or not token.endswith("]"):
raise ValueError("wire must name a declared register")
index = int(token[len(prefix):-1])
if not 0 <= index < size:
raise ValueError("wire index out of range")
return index
def parse(source):
qubits = classical = None
operations = []
classical_writes = set()
for raw in source.splitlines():
line = raw.strip()
if not line:
continue
if not line.endswith(";"):
raise ValueError("every statement must end with a semicolon")
line = line[:-1].strip()
if line == "OPENQASM 3":
continue
if line.startswith("qubit["):
left, name = line.split()
qubits = int(left[6:-1])
if name != "q" or qubits < 1:
raise ValueError("unsupported qubit declaration")
elif line.startswith("bit["):
left, name = line.split()
classical = int(left[4:-1])
if name != "c" or classical < 1:
raise ValueError("unsupported bit declaration")
elif line.startswith("h "):
if qubits is None:
raise ValueError("qubits must be declared before gates")
operations.append(("H", indexed(line[2:].strip(), "q", qubits)))
elif line.startswith("cx "):
if qubits is None:
raise ValueError("qubits must be declared before gates")
operands = [part.strip() for part in line[3:].split(",")]
if len(operands) != 2:
raise ValueError("cx needs a control and target")
control = indexed(operands[0], "q", qubits)
target = indexed(operands[1], "q", qubits)
if control == target:
raise ValueError("control and target must differ")
operations.append(("CNOT", control, target))
elif "= measure " in line:
if qubits is None or classical is None:
raise ValueError("measurement registers must be declared")
destination, source_wire = [part.strip() for part in line.split("= measure ")]
cbit = indexed(destination, "c", classical)
qubit = indexed(source_wire, "q", qubits)
if cbit in classical_writes:
raise ValueError("classical destination written twice")
classical_writes.add(cbit)
operations.append(("MEASURE", qubit, cbit))
else:
raise ValueError("unknown or ambiguous instruction")
if qubits is None or classical is None or not operations:
raise ValueError("incomplete circuit")
return {"qubits": qubits, "bits": classical, "operations": operations}
def serialize(ir):
lines = ["OPENQASM 3;", f"qubit[{ir['qubits']}] q;", f"bit[{ir['bits']}] c;"]
for operation in ir["operations"]:
if operation[0] == "H":
lines.append(f"h q[{operation[1]}];")
elif operation[0] == "CNOT":
lines.append(f"cx q[{operation[1]}], q[{operation[2]}];")
else:
lines.append(f"c[{operation[2]}] = measure q[{operation[1]}];")
return "\n".join(lines) + "\n"
def dependency_edges(operations):
last_on_wire = {}
edges = set()
for index, operation in enumerate(operations):
wires = operation[1:3] if operation[0] == "CNOT" else operation[1:2]
for wire in wires:
if wire in last_on_wire:
edges.add((last_on_wire[wire], index))
last_on_wire[wire] = index
return edges
def apply_h(state, qubit, count):
mask = 1 << (count - 1 - qubit)
output = state[:]
scale = math.sqrt(0.5)
for basis in range(len(state)):
if basis & mask == 0:
paired = basis | mask
output[basis] = (state[basis] + state[paired]) * scale
output[paired] = (state[basis] - state[paired]) * scale
return output
def apply_cnot(state, control, target, count):
control_mask = 1 << (count - 1 - control)
target_mask = 1 << (count - 1 - target)
output = [0j] * len(state)
for basis, amplitude in enumerate(state):
destination = basis ^ target_mask if basis & control_mask else basis
output[destination] += amplitude
return output
def execute(ir):
state = [0j] * (1 << ir["qubits"])
state[0] = 1.0
measurement_map = {}
for operation in ir["operations"]:
if operation[0] == "H":
state = apply_h(state, operation[1], ir["qubits"])
elif operation[0] == "CNOT":
state = apply_cnot(state, operation[1], operation[2], ir["qubits"])
else:
measurement_map[operation[2]] = operation[1]
if set(measurement_map) != set(range(ir["bits"])):
raise ValueError("fixture requires every classical bit to be assigned")
probabilities = {}
for basis, amplitude in enumerate(state):
bits = []
for cbit in range(ir["bits"]):
qubit = measurement_map[cbit]
bits.append(str((basis >> (ir["qubits"] - 1 - qubit)) & 1))
outcome = "".join(bits)
probabilities[outcome] = probabilities.get(outcome, 0.0) + abs(amplitude) ** 2
return state, {key: value for key, value in probabilities.items() if value > 1e-12}
ir = parse(SOURCE)
assert parse(serialize(ir)) == ir
assert dependency_edges(ir["operations"]) == {(0, 1), (1, 2), (1, 3)}
canonical_json = json.dumps(ir, sort_keys=True)
assert '"operations": [["H", 0], ["CNOT", 0, 1]' in canonical_json
state, counts = execute(ir)
assert all(abs(actual - expected) < 1e-12 for actual, expected in zip(state, [math.sqrt(0.5), 0, 0, math.sqrt(0.5)]))
assert set(counts) == {"00", "11"} and all(abs(value - 0.5) < 1e-12 for value in counts.values())
mutated = {**ir, "operations": [ir["operations"][1], ir["operations"][0], *ir["operations"][2:]]}
mutated_counts = execute(mutated)[1]
assert set(mutated_counts) == {"00", "10"} and all(abs(value - 0.5) < 1e-12 for value in mutated_counts.values())
def with_line(index, replacement):
lines = SOURCE.splitlines()
lines[index] = replacement
return "\n".join(lines) + "\n"
for invalid in (
with_line(4, "cx q[0], q[0];"),
with_line(6, "measure q[1];"),
with_line(6, "c[0] = measure q[1];"),
):
rejected = False
try:
parse(invalid)
except ValueError:
rejected = True
assert rejected
print(f"PASS: 16 circuit parser round-trips {len(ir['operations'])} operations with {len(dependency_edges(ir['operations']))} dependencies; Bell support={sorted(counts)}, mutation support={sorted(mutated_counts)}")
Scope boundary
- This chapter does not define every vendor's circuit syntax or imply visual left-to-right spacing is physical duration.
- It does not treat equivalent drawings as equal-cost compiled programs.
Depth commitment. One grammar, one Bell translation, one ambiguity set, and one parser exercise.
Practice problem
Translate a supplied three-wire diagram with one control, one swap, and two measurements into canonical instruction JSON.
Wire order top-to-bottom: q0,q1,q2. Classical bits: c0,c1.
At moment 1 apply a controlled-X q0 -> q2.
At moment 2 swap q1 and q2.
At moment 3 measure q0 -> c0 and q2 -> c1.
- Deliverable
- Instruction list, dependency edges, qubit/classical-bit map, and predicted basis output.
- Pass condition
- The parser ingests the reference OpenQASM and produces the same canonical JSON and output.
Verification record
Expected solution form. Annotated translation plus parser-generated diff.
Model answer. The canonical operation list is CX(control=q0,target=q2), SWAP(q1,q2), MEASURE(q0,c0), MEASURE(q2,c1), retaining the two simultaneous measurements in declared wire order. The JSON must preserve qubit and classical-bit identities rather than infer them from drawing position later.
Model result and check. CI compares normalized instruction sequences and simulates the deterministic test input.
Acceptance test. The parser ingests the reference OpenQASM and produces the same canonical JSON and output.
Companion work
Artifacts for this chapter
These entries resolve to checked-in local source. Commands are reproduced exactly from the chapter manifest, and source-embedded fixtures are exported as direct downloads.
Reproduce or test
cd labs && python -m unittest tests.test_simulator -v
Provenance
Sources and review
- Michael A. Nielsen and Isaac L. Chuang. Quantum Computation and Quantum Information. Cambridge University Press. 2010textbook
- John Preskill. Lecture Notes for Physics 219: Quantum Computation. California Institute of Technology. 2018graduate lecture notes
- OpenQASM Technical Steering Committee. OpenQASM 3 specification. Linux Foundation Joint Development Foundation. 2026official technical specification
- Lieven M. K. Vandersypen et al.. A look at the full stack. Nature Reviews Physics. 2021peer-reviewed perspective
The load-bearing claims in the chapter are mapped inline to this registered source set. A citation supports only the bounded claim beside it.