Tutorial › reference
- The two derived quantities everything hangs off
- Loops
- Generators
- Compact and expanded forms
- Zone types
- Primitives are curves, not scalars
- Control
- The reference architectures
- Validation
- Geometry limits (R19-R21)
- Sketching a shape (the studio's Sketch mode)
The architecture description language#
Layer 1 of the stack (PLAN §3). One declarative document describes a QCCD machine: its trap graph, its zone capabilities, its control wiring, and the (duration, quanta) curves of its primitives. Everything downstream — compiler, verifier, cost model, viz — consumes the expanded graph, never the generator parameters.
from qccd.arch import load
arch = load("ring144_24v.arch.json")
arch.device.summary()
# {'n_nodes': 168, 'degree_histogram': {1: 24, 2: 120, 3: 24},
# 'n_junction_nodes': 24, 'n_corners': 4, 'n_docks': 24, 'n_dock_corners': 2, ...}
The two derived quantities everything hangs off#
Degree#
Device.degree(node) counts segment incidences in the expanded graph. Rule R18 then reads straight off it: a node is a junction iff its degree is ≥ 3, and junction_cross is charged at that degree.
Nothing declares "this is a junction". Attaching a spur to a rail node makes it degree 3, and it becomes one. That is the point — the shipped design's 24 vertical shuttling lines are expensive because of what they do to the graph, and the cost model finds that out by counting, not by being told.
arch.device.junction_nodes # ('S0', 'S102', 'S108', ...) 24 of them
arch.device.degree("S0") # 3 -- two rail segments plus the dock spur
arch.device.degree("S71") # 2 -- a corner, but only a bend
Corners#
A corner is a property of a transport loop, not of a node in isolation.
In the shipped ring the two corner docks sit exactly on the end-caps: the ancilla for the dock at S0 = (0, 0) is at (0, 0.5), which lies on the segment from S0 to S143 = (0, 1). A purely geometric "did the direction change here?" test over all incident segments would call every T-junction a corner. So corners are found by walking the loop's cyclic node order and comparing in- and out-directions there (Device.corners(loop_id)), and the geometric test is used only at degree 2, where "the two directions" is unambiguous (Device.geometric_bends) — which is what catches the degree-2 corners of a grid, where the turn happens between two different paths.
Device.corner_endpoints[segment] counts how many of a segment's endpoints are corners. A value of 2 means the segment contains a whole turn. In a height-2 ring exactly the two end-caps qualify, which is what the deck charges 3 primitive hops for:
142 straight x 1 + 2 end-cap x 3 = 148 cost, depth 3 per rigid hop
Both numbers fall out of the graph. See the rules reference for why this reading of the deck's rule is the only one consistent with its own totals.
Loops#
"loops": [ { "id": "L0", "kind": "ring", "closed": true, "nodes": ["S0", "S1", ...] } ]
A closed loop is the domain of exactly one movement template — "shift every ion on L0 by k" — which is the whole reason rigid rotation needs one SIMD class where an odd–even sort needs many (PLAN §1). Device.shift_map(loop, delta) is that template. Keeping loops in the geometry rather than rediscovering cycles at compile time is what makes the thesis's quantity expressible.
An open loop (closed: false) is a linear register; asking it for a rigid shift raises.
Generators#
| generator | parameters | produces |
|---|---|---|
ring | width, height, verticals, dock_offset | 2W + 2H − 4 rail slots in one closed loop, plus V dock spurs to mid-line ancilla sites, the first at slot dock_offset |
grid | a, b | an a × b lattice of junctions with one trap in the middle of every wire: 2ab − a − b traps, interior junctions degree 4, boundary 3, lattice corners 2 |
chain | n | n sites in a line; no loop, no junction, two degree-1 ends. chain(1) with a large capacity is the stationary-chain baseline |
Ring slot numbering reproduces the shipped visualizer's slot_coordinates exactly — top row left to right, down the right end, bottom row right to left, up the left end — so slot ids in an imported schedule mean the same thing here.
ring(72, 2, 24) puts docks at {0, 6, …, 138}; verticals must divide the slot count. dock_offset (default 0, less than one dock spacing) shifts them all: slot 0 is a corner, and on a loop of height 3 or more a corner dock's spur lies along the side rail and fails DRC, while a top and a bottom dock at the same x put two ancilla sites on one point. The offset costs nothing in the transport model; the dock sweep --rings` searches it together with the width for the layout the DRC accepts.
Compact and expanded forms#
A document may carry geometry.generator + geometry.params (compact) or the explicit nodes / segments / loops (expanded). load() always returns the expanded graph. arch.to_json(expanded=…) writes either.
nodes is ONE ordered array (schema 0.3). It replaced a sites + junctions split, which was a lossy encoding of an ordered dict: from_json rebuilt as sites + junctions, so any generator that interleaves the two — grid, and therefore grid9x9 and deck_unit_cell — came back reordered. Node order is load-bearing twice over: an explicit-mode architecture listing emits its statements in it, and the layout's _bows sums a centroid over pos.values() in it, where two orders differ by a few ulp after a drag — enough to flip a collinear tie-break and a segment's bow sign. The two legacy keys are still READ, so an older expanded file still loads; it simply cannot promise an order it never recorded.
The compact form is only emitted when re-running the generator on those params reproduces the graph exactly (Device.reproducible_from_generator()); otherwise the expanded form is written instead. Writing generator + params for a device that has since been edited would throw the real graph away and hand back a file that loads as something else.
The expanded form caches the derived degree and corner on each node and corner_endpoints on each segment — and re-checks them on load, so a hand-edited file cannot quietly disagree with its own graph:
ExpansionError: node 'S0': file says degree 10, graph gives 3
Zone types#
"zone_types": {
"data": { "capacity": 2, "gate": false, "spam": false, "cool": true },
"ancilla": { "capacity": 2, "gate": true, "spam": true, "cool": true }
}
capacity is R1's bound; the flags are R6's capabilities. Motion and gate capability are decoupled: an ion may traverse a site it cannot be gated in. A site with no explicit capacity inherits its zone type's.
Primitives are curves, not scalars#
Slowing a transport buys motional quanta, so the compiler has to be allowed to make that trade (PLAN §3.2, §7 pass 5). Every point is tagged with the table it was measured in:
"shuttle_segment": { "curve": [
{ "us": 5, "quanta": 0.10, "table": "qccdsim_jones", "source": "2510.23519" },
{ "us": 12, "quanta": 1.00, "table": "transport_excitation", "source": "2605.25118" },
{ "us": 14, "quanta": 0.10, "table": "transport_excitation", "source": "2605.25118" }
]},
"junction_cross": { "curve_by_degree": {
"3": [ { "us": 100, "quanta": 3.0, "table": "qccdsim_jones" },
{ "us": 200, "quanta": 3.0, "table": "measured", "source": "1210.3655" } ],
"4": [ { "us": 120, "quanta": 3.0, "table": "cyclone" } ]
}}
The corpus supplies two mutually incompatible tables differing by 2–3× in time (Knowledge: q_heating_rate_measurement). Rather than pick one, the whole stack re-runs against either, so ranking stability under parameter uncertainty is itself measurable.
from qccd.arch import OperatingPointPolicy
curve = arch.primitives.curve("shuttle_segment")
curve.pick(OperatingPointPolicy("qccdsim_jones", "fastest")) # 5 us / 0.1 quanta
curve.pick(OperatingPointPolicy("transport_excitation", "coolest")) # 14 us / 0.1
A policy whose table is silent about a primitive falls back to the union of tables so a partial table is usable at all. That fallback is reported, not hidden — primitives.table_coverage(table) says which primitives the table actually covers, because an unnoticed fallback turns a two-table comparison into a comparison of a table with itself.
junction_cross carries a degree-2 entry only because Cyclone's table has one. PLAN §0.5 rejects it — a two-arm bend has a continuous RF null and therefore no barrier — and CorrectedModel ignores it.
Control#
"control": {
"model": "simd_classes",
"max_simd_classes_per_cycle": 1, // WISE = 1; C2LR = 4
"intra_inter_exclusive": true, // R4b
"classes": { "generator": "x_junction_grid", "count": 18, "extra": [
{ "id": "rotate_cw", "type": "shift", "orbit": "L0", "delta": 1 },
{ "id": "dock", "type": "shift", "orbit": "spurs", "entails": ["split", "merge"] }
]},
"wiring": { "scheme": "wise", "dacs_dynamic": 100, "electrodes_per_junction": 48 }
}
entails is load-bearing. A rigid rotation is a conveyor-belt transport of a whole chain along a moving potential — nothing splits (H2 drives 20 wells per side from three broadcast signals, arXiv:2305.03828). A dock lifts one ion out of the rail's potential and inserts it into a separate trap, so it costs a split at the source and a merge at the destination, each way. That difference is a property of the movement class, not of occupancy, so the architecture declares it and the cost model reads it (arch.entails("dock") == ("split", "merge")).
The reference architectures#
| file | shape | degrees | note |
|---|---|---|---|
ring144_24v | ring(72, 2, 24) | {1:24, 2:120, 3:24} | the shipped design. 24 degree-3 docks, 4 corners, 2 of them docks |
cyclone_base | ring(36, 2, 0) | {2:72} | base Cyclone, m/2 = 72 traps, ancillas in-line, no junction on the rotation path |
chain | chain(72) | {1:2, 2:70} | the unrolled control: same 144 ion slots, no loop, no spur, no junction |
grid9x9 | grid(9, 9) | {2:148, 3:28, 4:49} | baseline grid QCCD; 2ab − a − b = 144 traps, one per data qubit |
Validation#
validate_document(doc) shape-checks and returns messages with a JSON path; check(doc) raises. Structural checks that need the expanded graph — dangling endpoints, duplicate ids, self-loops, parallel segments, a junction declared with non-zero capacity, declared-vs-derived mismatches — live in Device.check_structure and run on every load().
One further check is program-level and lives in the verifier: every node the graph makes a junction must be priceable at that degree, or the architecture cannot charge for what it built. It is reported under R11.
Geometry limits (R19-R21)#
Two optional fields under budget bound the shape of the rail graph; the verifier's geometry rules read them (qccd.verify.rules.geometry_limits) and fall back to the surface-trap defaults when they are absent:
| field | default | rule |
|---|---|---|
budget.max_junction_degree | 4 | R19: no node joins more rails than this. A 2D surface trap has T- and X-junctions and nothing denser; a 3D trap may declare 6. |
budget.min_rail_angle_deg | 60 | R20: at every node with two or more rails, any two of them subtend at least this angle, measured from the declared node positions. A symmetric X-junction is 90 degrees. |
R21 has no parameter: rails are planar, so a rail passes through no node it does not end at, and two rails cross only at a node they share. All three are judged from pos alone, which is why pos must be geometry and not a sketch: the ring generator's corner docks were drawn on the end-cap rail until 2026-09 and failed R20 and R21 on every ring; they now run outward. A periodic grid cannot pass R21 on a plane (its wrap wires are chords across the die); cylinder is the planar drawing of the same torus.
Sketching a shape (the studio's Sketch mode)#
The studio has two design modes, remembered under qccd.studio.mode. Parts is the element-at-a-time tool: place a site, place a junction, shift-drag a rail. Sketch is the default on a canvas with no device on it, and it is the one that speaks this language directly: arm a shape — rectangle, ellipse, line, polyline — drag it, and the release emits builder statements.
A shape is turned into ADL by exactly the rule the generators use:
- a trapping site every one lattice unit along the drawn path (
L.ux/L.uy, the step the layout measured, or 1 on an empty canvas), the first at the shape's start. The last spacing on an edge may be short: the drawn extent is the authority and the shape is never resized to make the arithmetic come out even; - every vertex is a site, so a 6 × 4 rectangle is 20 sites and 20 rails — the same count
ring(7, 5)gives for the lattice its corners span; - a closed shape also emits
d.loop(id, walk, closed=True, kind="ring")and everyd.segmentcarriesloop=, sop.rotateworks on it the moment it lands; an open shape emits thekind="path",closed=Falseloopchain()declares for a linear register; - the ellipse is polygonised at one side per unit of its drawn circumference (Ramanujan), which makes R20 true by construction: a regular n-gon's rails meet at
180 − 360/ndegrees.
Everything lands in ONE transaction, namespaced by the component instantiator, so the whole shape is one selectable unit and one Ctrl+Z.
A corner is a bend, not a junction#
The obvious reading — "the shape has a corner, so put a junction there" — is wrong, and the geometry above says why. A corner of a drawn rail is a degree-2 node, and R18 prices a degree-2 node as ordinary transport. The shipped ring has four corners and pays no junction charge at any of them; declaring them kind: "junction" would make the verifier charge junction_cross on every rigid hop through a turn the loop does contain as a bend, which is the single most expensive structural mistake this file can express.
So a corner gets a site, and the studio says so in the drag readout: corner: a bend (R18), not a junction.
A junction is added automatically, at the place a junction is — a real meeting of three or four rails:
| what the shape meets | what is emitted |
|---|---|
| an existing node, at a vertex or along an edge | that node is reused; its zone and capacity are kept, and R18 makes it a junction by degree |
| an existing rail, crossed or touched | a kind: "junction" node at the meeting point, and the crossed segment replaced by its two halves |
Both keep R21 true (no rail crosses another without a shared node, and no rail passes through a node it does not end at), and both are checked against R19 and R20 before the release commits: a crossing that would push a node past budget.max_junction_degree, or a corner tighter than budget.min_rail_angle_deg, is refused under the pointer with the rule and the number in the readout. The commit re-runs the geometry pass afterwards and takes the whole shape back if it added a finding, so a sketched device is clean of R19/R20/R21 by construction.
Two limits are worth stating rather than discovering:
- A sketch needs a builder.
d.siteis hoisted above the seal, and a generator device (from_template,blank) has none — nor is there any post-seal verb that declares a loop (the edit op table isadd_site,add_junction,add_segment,remove_node,remove_segment). Sketching on one is refused with the message that names the button which makes it explicit. - A ring may not cross a ring. Splitting a rail that already carries an orbit has to go through
add_site(on=...), which is the only verb that re-walks a loop — and it runs after the seal, where the new shape's ownd.loophas already been written. One junction can fix one of the two orbits, so a closed shape crossing a loop edge is refused under R21 with what to do instead (meet the loop at one of its own sites).