models.py — the vocabulary

Nine plain dataclasses that name everything the recognizer produces and consumes. Learn these nouns first; every other file just moves them around.

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.

LAYER 0 — parse LAYER 1 — Subcircuit Recognizer (SR) LAYER 2 — Functional Block Recognizer (FBR) ParsedNetlist holds a list of Device (from synthesizer) SubcircuitRecognitionResult = list of ↓ RecognizedStructure PatternDef PatternDevice·ChildDef·HookMatch the "template" a structure is matched against FunctionalBlockRecognitionResult (topology mode) = dict of SlotAssignment CategoryGroupResult (topology-free mode)
Each layer's output is the next layer's input. Nine classes, three layers — hover a box to see its group.

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.
g d s m1 (nmos) terminals = { "d": "net1", "g": "in1", "s": "tail", "b": "vdd!" }
A device is just "which named wire touches each of my legs." Recognition is entirely about matching these wire-sharing patterns.

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:

FieldAnswers
nameWhat is this subcircuit called? (the .subckt name, e.g. "ota")
external_portsWhich wires poke out to the outside world? (ordered: in1 in2 out vdd! gnd!)
devicesEvery element inside, as Device objects, in file order.
internal_netsEvery 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

PatternDef one whole template: name, category, same_net constraints, pins map devices: [PatternDevice] one typed slot: ref + type children: [ChildDef] required sub-patterns (composites) HookMatch an "accept-and-extend" result a hook returns (see hooks.py)
A PatternDef is a small typed graph. HookMatch is how a hook bolts extra devices onto a match at runtime.

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's in1?"

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:

FieldMeaning
nameWhich pattern matched, e.g. "differential_pair_nmos".
category / circuit_blockThe FBR bucket ("input_pair") and the block it belongs to ("gain_stage_1", "bias"). None for primitives.
index0-based counter distinguishing repeated matches of the same pattern.
pinsResolved pin name → actual net name — the pattern's pins map after substitution.
devicesThe actual Device objects this match covers.
childrenSub-structures, for multi-level composites. Empty for leaves.

The critical honesty in the docstring:

"SubcircuitRecognitionResult.structures may contain multiple overlapping RecognizedStructure instances 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.

have a topology template? yes no FunctionalBlockRecognitionResult {slot_name → SlotAssignment} CategoryGroupResult {block → category → [ranked]}
Know the topology → fill named slots precisely. Don't → group by category and rank a best guess.

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:

text → ParsedNetlist → [RecognizedStructure] → {slot: assignment}

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.