Part 1 · Why does a file of pure nouns exist?
The one job: "What are the exact shapes of data that flow between the three recognition stages — and nothing else?"
Imagine an assembly line with three stations.
Station 1 takes raw SPICE text and hands the next station a tidy tray of parts.
Station 2 takes that tray and hands over a tray of recognized shapes.
Station 3 turns those into named functional blocks.
models.py is not a station. It's the catalog of trays — the exact molds every station's output must fit into.
Every class here is a @dataclass that carries no logic. The docstring says it plainly:
"All structures are plain dataclasses — they carry no logic and can be freely inspected or passed between pipeline stages."
That's a deliberate design choice, and a good one. When the data has no behavior, you can print it, diff it, pickle it, or compare two runs field-by-field in a test — without ever worrying that reading it changes it.
Why is this the first page?
Because every other file in the package is a verb acting on these nouns. You can't understand parse() until you know what a ParsedNetlist is; you can't understand recognize() until you know what a RecognizedStructure is. Master the vocabulary and the rest reads like plain English.
Part 2 · The one picture to hold in your head
There are nine classes, but they line up perfectly with the three pipeline layers. This is the map to keep open while reading everything else.
Notice the one arrow coming in from the side: PatternDef and its friends are not pipeline data — they're the rulebook Layer 1 matches against. We'll treat them separately in Part 5.
Part 3 · Device — the atom the whole package is built on
The one job: "What is the smallest thing a circuit is made of, and how do we describe just one of them?"
Device is imported from circuitgenome.synthesizer.models — it lives outside this package — but you'll see it everywhere, so meet it now. A device is a single circuit element: a transistor, a resistor, a capacitor.
Three fields do all the work:
ref— its name in the netlist, e.g."m1". The first letter follows SPICE convention:m=MOSFET,r=resistor,c=capacitor.type—"nmos","pmos","resistor", or"capacitor".terminals— a dict mapping terminal names to the net (wire) each one touches.
This is the single most important idea in the whole package:
A circuit's structure is nothing more than which devices share which nets. Recognition never looks at transistor sizes — only at this connectivity graph.
Part 4 · ParsedNetlist — the Layer-0 tray
The one job: "After we've read the SPICE text, how do we hold the whole subcircuit in one object?"
Four fields, and they answer four questions:
| Field | Answers |
|---|---|
name | What is this subcircuit called? (the .subckt name, e.g. "ota") |
external_ports | Which wires poke out to the outside world? (ordered: in1 in2 out vdd! gnd!) |
devices | Every element inside, as Device objects, in file order. |
internal_nets | Every wire a device touches that isn't an external port. |
The distinction between external_ports and internal_nets looks minor but it does real work later. In Layer 2, "does this pin land on an external port?" is the key test that separates a real gain stage from a bias mirror. So Layer 0 computes it once, up front:
# netlist_parser.py, last lines
internal_nets = referenced_nets - set(external_ports)
Set subtraction: "everything a device touched, minus the ports." Clean.
Part 5 · The pattern schema — the rulebook, not the data
The one job: "How do we write down 'a differential pair looks like this' in a way a matcher can check?"
These four classes describe the template library loaded from the YAML files in config/. They're the shape Layer 1 hunts for — think of them as the cookie-cutter, while RecognizedStructure is the cookie.
A staircase of four classes
The two fields inside PatternDef that carry the real matching logic:
same_net— equality constraints like[["m1.s", "m2.s"]], read as "m1's source and m2's source must be the same net." These are the only required equalities; anything not listed is free to differ, so a busier real circuit can still match.pins—{"in1": "m1.g"}exports named wires out of the match, so later stages can ask "what net is this structure'sin1?"
The exclusive/priority pair (only on level-0 primitives) resolves fights: when two primitives both match one device, higher priority wins. diode_connected_nmos (priority 10) beats bare nmos (priority 0).
Part 6 · RecognizedStructure — the Layer-1 cookie
The one job: "We found a match. How do we record what we found, where, and which real devices it covers?"
When a PatternDef (the cutter) matches part of the netlist, the result is a RecognizedStructure (the cookie). Its most-used fields:
| Field | Meaning |
|---|---|
name | Which pattern matched, e.g. "differential_pair_nmos". |
category / circuit_block | The FBR bucket ("input_pair") and the block it belongs to ("gain_stage_1", "bias"). None for primitives. |
index | 0-based counter distinguishing repeated matches of the same pattern. |
pins | Resolved pin name → actual net name — the pattern's pins map after substitution. |
devices | The actual Device objects this match covers. |
children | Sub-structures, for multi-level composites. Empty for leaves. |
The critical honesty in the docstring:
"
SubcircuitRecognitionResult.structuresmay contain multiple overlappingRecognizedStructureinstances covering the same devices."
Layer 1 is deliberately greedy and non-committal: it reports every candidate and never picks a winner. Resolving overlaps is Layer 2's job. Hold that thought — it's the whole reason FBR exists.
Part 7 · The Layer-2 results — two shapes for two situations
Layer 2 has two exits, and therefore two result types.
SlotAssignment is the atom of topology mode: it records "this recognized structure fills the input_pair slot." For a correct round-trip, its pattern_name equals the variant name that was originally synthesized into that slot — which is exactly what the tests check.
Part 8 · The hidden idea
Although the file never says so, models.py encodes a small type-level theorem about the whole system:
Each arrow is one file. Each box is one dataclass here. The reason recognition can be tested so cleanly — synthesize a circuit, flatten it to SPICE, recognize it, and check you got the original slots back — is that every intermediate shape is a plain, comparable, logic-free value. That design decision lives entirely in this file.
And the deepest choice of all: SubcircuitRecognitionResult allows overlaps on purpose. By refusing to disambiguate early, Layer 1 stays simple and total; all the messy, context-dependent judgement is pushed into Layer 2 where the topology (or lack of it) is finally known.
In one sentence: models.py is the logic-free catalog of trays for a three-station assembly line — nine dataclasses that let raw SPICE flow, one comparable value at a time, into named functional blocks.
Looking ahead → Part 2 · netlist_parser.py fills the very first tray: it turns SPICE text into a
ParsedNetlist. It's the simplest transform in the package — a perfect warm-up.