Tutorial › reference
- Two design decisions
- Instruction types
- Calling the decoder
- The movement template
- Execution model
- The importer
- Round-trip
- Text
- Modules
TSIR — the control IR#
Layer 2 of the stack (PLAN §4). A TSIR program is a list of instructions over an architecture, plus the metrics it claims. Two backends emit it — the importer that brings in the shipped schedule, and (later) the compiler — and the same verifier checks both.
Two design decisions#
One instruction is one concurrent cycle#
A simd instruction carries many participants and a gate instruction carries many pairs, because a broadcast-wired machine really does drive them together — that is the entire content of R4's "a class fixes (type, direction); participation is variadic". Cost sums across participants; depth and duration take the maximum. That is also the deck's own rule ("one rigid hop takes the maximum primitive edge depth used by any moving ion in that hop"), and it is what makes "9.1 % batch utilization" a number the IR can express at all.
Cost, duration and quanta are annotations, not content#
An instruction says what moves where. qccd.verify.replay computes what it costs under a given qccd.cost model. Any annotation already present is a claim, and R9 is exactly the check that the claims equal the replay.
That separation is why M1 and M2 run the identical program — under DeckModel and under CorrectedModel — and every difference is attributable to the model rather than to the schedule. One test asserts it, and says so in its name: same moves, same contacts, same junction transits; different cost.
Claims are model-scoped. meta.metrics_model records which cost model produced them; replaying under a different one reports R9 as skipped, with the reason, rather than as a red failure that means nothing.
Instruction types#
| type | carries | notes |
|---|---|---|
init | placement, quanta | must be the first instruction |
simd | class, mode, template or participants, holds | one transport cycle |
gate | gate, pairs, sites | many co-located pairs driven together |
measure / reset | ions | |
cool | broadcast or ions | global by default: one op cools every ion |
barrier | — | explicit synchronization, costs nothing |
decode | ions | call the decoder on those ions' outcomes; classical, costs no machine time |
{ "type": "simd", "id": 1, "class": "rotate_cw", "mode": "inter",
"template": { "kind": "loop_shift", "loop": "L0", "delta": 13 },
"holds": ["L0", "E0", "E1", ...],
"cost": 1924, "steps": 39,
"meta": { "batch": 0, "kind": "rotate", "hops": 13, "direction": "cw" } }
{ "type": "simd", "id": 2, "class": "dock", "mode": "inter",
"participants": [ { "ion": "d108", "from": "S120", "to": "A120", "via": ["V120"] } ] }
{ "type": "gate", "id": 3, "gate": "CX", "mode": "intra",
"pairs": [["d108", "a120"]], "sites": ["A120"] }
Calling the decoder#
decode is the instruction that says where a measurement's outcome goes: down the wire from the site where each named ion was measured to the decoder, and the decoder's frame update on to the classical memory. Before it existed a compiled error-correction programme measured its ancillas and stopped, and the studio's QEC-cycle layer drew a decoder that nothing in the programme ever used.
{ "type": "decode", "id": 5382, "ions": ["q144", "q149", ...],
"meta": { "kind": "decode", "window": 0, "reads": [565, 580, ...] } }
It is classical: it moves no ion, heats none, holds no rail and takes no machine time -- the replay records it at t0 == t1 with zero cost, and no rule sees it, because the decoder works alongside the ions and only something that reads its answer would wait. Its latency is real and is budgeted by the QEC-cycle layer (qccd.analysis.feedback), not charged to the ions. That is what lets qccd.compile.decode.insert_decodes add one to every leaderboard programme without moving a single ranking: every board's replayed time is identical to the microsecond, under both cost models, with and without it.
Authoring: p.decode() sends every ion measured since the last decode, in the order they were last measured; p.decode(["a0"]) names them; with nothing measured it refuses ("nothing to decode..."). The browser's twin is pinned by the programme lane of .
The pass (qccd.compile.decode) calls the decoder once per window: a window ends where an ion would be measured a second time, or where the programme ends. That is a whole syndrome round when every check has its own ancilla, and a slice of one when ancillas are re-used -- ring144_24v a24 measures 144 checks with 24 ancillas, so it decodes six windows of 24. New decodes take ids above every existing one (id_seq), so the certificate and the circuit join, which key on instruction id, are untouched.
On the page, a decode frame lights the wires from every site where one of its ions was measured, down to the bus, along it to the decoder and on to the memory, with the outcomes moving down them as the frame plays (; measures it in pixels). The stage frames the classical floor too (fit() reads STAGE_EXTENT), so the outcomes are seen arriving rather than leaving the picture.
The movement template#
"template": { "kind": "loop_shift", "loop": "L0", "delta": 1 }
This is not a compression trick. PLAN §1's thesis is that rigid rotation needs exactly one movement template where an odd–even sort needs many, so the IR has to be able to say "one template" in one object — counting them is how M4 will measure the claim.
TSIR.templates() returns distinct templates → machine cycles issued. A shift by k is not k templates; it is the same unit-hop template driven k times, which is exactly the property that makes one broadcast waveform enough. So the key normalizes to (loop, direction) and the value counts unit cycles:
prog.templates()
# {'loop_shift:L0:+1': 1159, 'loop_shift:L0:-1': 1513,
# 'class:dock': 396, 'class:undock': 396}
Four templates for the whole shipped schedule, independent of code size. It is four rather than three because the schedule rotates in both directions, which is not obviously free under broadcast wiring — see Knowledge: q_bidirectional_rotation_template_count.
The replay decomposes |delta| > 1 into |delta| unit sub-cycles, each a real machine cycle with its own depth, duration and rule check.
Execution model#
Instructions execute in list order, each atomically; t0 of the next is t1 of the previous. That is honest for an imported barrier-scheduled artifact, and holds plus explicit t0/t1 leave room for the event scheduler of PLAN §7 pass 7 to overlap them later without an IR change.
The replay owns no cost knowledge and no schedule knowledge. It carries per-ion n̄ by component — shuttle, junction, split_merge, gate, anomalous — because a single opaque total cannot be checked against a published budget, and PLAN §0.4's budget is stated as three named components.
from qccd.verify import replay
res = replay(prog, arch, corrected_model())
res.quanta_components # {'shuttle': 38649.6, 'junction': 194976.0, ...}
res.per_ion_quanta["d17"] # {'shuttle': 268.4, 'junction': 1354.0, ...}
res.junction_transits["d17"] # 453
res.us_by_class # {'rotate_cw': 115900.0, 'dock': 65340.0, ...}
A probe(instruction, current_quanta) callback fires before each instruction; the cooling pass uses it to read the n̄ a cool is about to remove without the replay storing a trace.
The importer#
INLINE_DATA is one JSON object on line 344 of visualizer_24_ancillas_24_junctions_standalone.html. The importer locates it by prefix, not by line number, so an edit above it cannot silently change what gets imported.
It does not trust the artifact. Ion positions are recomputed by the replay from the initial order and the rotation history, and the replay raises if a docking participant is not where its instruction says it is — so every one of the 864 contacts is an assertion. Totals can agree by coincidence in two places; 864 recomputed positions cannot.
Before importing anything it cross-checks the artifact's geometry against the architecture: capacity, dock slots, ancilla sites, and every slot's side label and position. A mismatch is an error, not a warning.
completeness_report(prog) reports what the artifact omits: it carries no ancilla measurement and no reset, so the imported program is a transport-and-contact schedule rather than a complete ESM round. The missing SPAM is ~1.0 ms and moves none of the M1 or M2 numbers, but it is named rather than papered over.
Round-trip#
prog.save("deck24.tsir.json")
again = TSIR.load("deck24.tsir.json")
validate_program(prog) shape-checks ids, types and required fields without needing an architecture.
Text#
The JSON is what passes read; the text is what a person reads, and what the gadget page lights line by line. They are the same program: parse_program(program_text(p)) == p, held for every leaf program of the gadget library .
tsir msf15.produce+cooling the program's name
arch msf15 its architecture
id_seq 412 the identity allocator's high-water mark
meta {"derived_from": ...} its own metadata (JSON), if any
0 init f0@S0 f1@S1 | f0=0.1 f1=0.1 placement | n̄
1 simd rotate_cw ~L0 +3 ; op=ring kind=rotate
2 simd dock f3:S1->A1 ; op=ring
3 gate CX f3:f12 @A1 ; src=rm15.circ:41
4 gate H f0 f1 @A0 @A1 ; src=rm15.circ:32
5 measure f4 ; src=rm15.circ:131
6 cool (alone: the broadcast cool)
A line starts with the instruction's id (an identity, not a position). After ; come key=value pairs: the instruction's metadata, and, with a leading dot, one of its own fields where it differs from what the line implies (.mode=inter, .t0=12.5). A value is a bare word or JSON. A simd line is either the rigid rotation ~LOOP ±k or one ion:from->to[via,...] per participant; a gate line is pairs a:b or a batch of single-qubit gates on bare ions.
Modules#
A whole run is too large to be one TSIR program, and most of it repeats. TSIR modules write it once, on the architecture it runs on. Every hardware instruction is then in exactly one gadget:
tsir modules 1
architecture fullstack_d3 the floor, instanced
place bench1 ppm_d3 48.0 12.0 16 a place: its master, where it is, its first resident ion
road /door_bench1_in_a j0_6.s -> bench1.in_a chan9
wire /wire_bench1_syn bench1.syn -> decoder.syn 5.0
program shor15 the program's own body
block q0 126..134 ; born=1265.0 born_at=prep code=surface_d3 n=9 k=1
ins 3 17 "zzy q0 q1" ; kind=call op=call blocks=["q0","q1"] gate=zzy
call c0 89875.0 m0 zzy q0 q1 ; gid=153 ... a composite gadget, called
gadget m0 zzy a composite gadget: one gate in one context
ins 1 3 "ppm Z arg0 Z arg1 Z zzy1_l0 -> zzy1_o1" ; kind=ppm op=ppm.ZZZ ...
run 45 42135.0 102850.0 ppm_d3.ppm.ZZZ @bench1 d0:arg0.0 ... f0:126 ...
carry 12 41105.0 chan9.carry /door_bench1_in_a j0_6.s arg0 ; dep=[...] ...
msg 3 121035.0 121040.0 /wire_bench1_syn bench1.syn -> decoder.syn 3 syndrome
hold 0 1300.0 413230.0 x4 mem_d3.refresh @mem13 arg1 aX0:18 ...
leaf ppm_d3.ppm.ZZZ a leaf gadget: one op of one master, its TSIR
tsir ppm_d3.ppm.ZZZ+cooling
89 measure p0 ; op=ppm round=0 checks=["P0"]
- A
runrow names a leaf gadget (master.op) and the place it runs at. Its instruction#89is line89 ...ofleaf ppm_d3.ppm.ZZZ. Acarrynames its road's gadget, aholdthe memory'srefresh. Only the decoder's and the archive's rows are classical: a modeled latency, named, with no TSIR. - Rows sit under the logical instruction they carry out (
ins), in time order. A gadget's parameter blocks are its caller's, so their ions are symbols (arg0.3). A list that is exactly one block's ions is written as the block's name. - Modular: a leaf gadget is written once per master op, a composite once per context (the folded schedule's macro), and a call is one line. Scalable: FormalRV's three-hour Shor is 827 files.
- The same program:
read_modules(write_modules(g)) == g, exactly (; FormalRV's Shor too: 687,690 rows).python -m qccd.gadget modules DIRreads modules and checks what they say: every leaf module must be the library's characterized op, and the place checks must hold per view (and on the expansion when it is small). - A folded page writes its run to
hw/beside it. The Stack's Module row cites the row and the instruction,file:line, and two boxes show those lines lit.