Part 1 · Why does Layer 2 exist?
The one job: "Layer 1 gave me many overlapping candidates for the same devices. Which one is the real input pair, the real load, the real tail?"
Remember Layer 1's deliberate cowardice: it reports every pattern that fits and never picks a winner.
So a single differential pair might show up as an input_pair candidate and get re-matched by a bias-mirror pattern, and a cascode pattern, all overlapping.
Someone has to be the judge. That's this file.
The analogy: casting a play from a huge audition pile
Layer 1 is an open audition — everyone who vaguely fits a role gets a callback slip. Layer 2 is the casting director: exactly one actor per role, chosen by how well they actually fit the part. And the "part" is defined by wiring — which nets a real input pair must connect to.
Part 2 · Two modes, because sometimes you know the script
The casting director works in two very different situations:
Topology mode is the easy, precise case — you have the blueprint. Topology-free mode is the hard, heuristic case — most of this file's complexity lives there.
Part 3 · The two scorers — tiny, and everything hinges on them
_connectivity_score — used by topology mode
"How many of this candidate's pins land on the exact nets the slot expects?"
return sum(1 for pin, net in structure.pins.items()
if slot_connections.get(pin) == net)
A candidate whose in1/in2/tail pins all match the slot's expected nets scores 3; a spurious overlap scores 0. Highest score wins the slot.
_external_port_score — used by topology-free mode
"How many of this candidate's pins touch a subcircuit external port?"
return sum(1 for net in structure.pins.values() if net in external_ports)
This is the port-vs-internal distinction from Layer 0, finally cashing in. A real input pair's in1/in2 hit the subcircuit's actual input ports; an internal bias mirror touches no ports. Port-adjacency becomes the proxy for "is this on the real signal path?"
Two five-line functions carry the entire judgement. Everything else in the file is orchestration around these two scores.
Part 4 · assign_slots() — topology mode, traced
The one job: "For each named slot in the topology, pick the candidate whose wiring fits best, and don't reuse a candidate twice."
for slot in topology.slots:
candidates = [s for s in sr_result.structures
if s.category == slot.category and id(s) not in assigned_ids]
if not candidates: continue
slot_connections = topology.slot_connections(slot.name)
best = max(candidates, key=lambda s: _connectivity_score(s, slot_connections))
slot_assignments[slot.name] = SlotAssignment(...)
assigned_ids.add(id(best))
Two guards do the real work:
- category filter — only candidates of the slot's category are even considered;
assigned_ids— once a structure fills a slot, itsid()is banked so it can't fill a second slot of the same category (crucial when a fully-differential topology has twocompensationslots).
Why score even when a category has only one slot? Because SR overlaps mean a category can have several candidates regardless of slot count. Connectivity scoring picks the one actually wired into that slot's nets. A slot with no candidates is silently omitted — an honest "I didn't find this."
Part 5 · group_by_category() — the topology-free funnel
The one job: "With no blueprint, throw out the obvious noise and rank what's left into a best-guess block → category → candidate structure."
This is the file's centerpiece: a multi-pass funnel that narrows a noisy candidate pile into clean groups.
The initial grouping sorts every category's candidates by _external_port_score descending. Then the passes correct that ranking where it's misleading — which is the interesting part.
Part 6 · The three classes of spurious matches
The filter pass exists because Layer 1's over-reporting produces predictable false positives. The code names three and kills each:
| Class | What re-matched | Tell-tale sign |
|---|---|---|
| A | An input-pair / bias-ref nmos re-matched as a gain stage | 'in' pin on an external port |
| B | A bias mirror's pmos leg re-matched as a gain stage | 'bias' pin on an external port |
| C | Cascode load devices re-matched as a gain stage | an nmos whose source ≠ gnd! |
# Class A & B: pin lands on an external port → not a real stage input
if s.pins.get("in") in ext or s.pins.get("bias") in ext:
continue
# Class C: (single-category blocks only) a floating-source nmos = cascode
if single_cat and any(
d.type == "nmos" and d.terminals.get("s") != _GND
for d in s.devices):
continue
Why is Class C gated on single_cat?
Because a genuine input pair's nmos sources connect to net_tail, not gnd! — applying Class C to a multi-category gain_stage_1 block would wrongly delete the real input pair. Restricting it to single-category (second-stage) blocks avoids that. This is exactly the kind of subtle context-dependence that makes topology-free mode hard.
The signal-chain following
The multi-category pass is the cleverest bit. After re-ranking input_pair by how many distinct external ports its {in1, in2} hit (a real diff pair hits two; a bias mirror hits one; a spurious pair hits zero), it uses the winning input pair to chain-follow:
In plain English: the real load is the one whose inputs are wired to the input pair's outputs; the real tail is the one whose output is wired to the input pair's tail node. The circuit's own connectivity resolves the ambiguity — no topology needed.
Part 7 · The split pass — separating stacked stages
The one job: "A three-stage opamp has several gain stages of the same category. Order them from first to last."
If a gain_stage_* block has exactly one category but more than one candidate, those candidates are almost certainly consecutive stages. Sort them by ascending _external_port_score and fan them out into numbered blocks:
structs.sort(key=lambda s: _external_port_score(s, ext))
for i, s in enumerate(structs):
to_add.append((f"gain_stage_{base + i}", cat, [s]))
The intuition: the later a stage sits in the chain, the closer it is to the external output port, so the more external ports it touches. Ascending port-score = first-to-last stage order.
Part 8 · The hidden idea
Underneath all three passes is one principle, applied over and over:
Connectivity is identity. A structure's role in the circuit is determined entirely by which nets its pins touch — external ports vs. internal nets, and whether those internal nets chain to a neighbor's pins.
Topology mode states this exactly: score against known expected nets. Topology-free mode approximates it with proxies: external-port adjacency stands in for "on the signal path," and pin-net equality between neighbors stands in for "wired together." Every filter, every re-sort, every split is just a different way of asking where does this thing connect?
And notice the payoff of Layer 1's earlier restraint: because SR over-reported and never disambiguated, Layer 2 has all the candidates it needs to make these connectivity comparisons. Had SR committed early, the true match might already have been thrown away. The two layers' contracts fit together like a lock and key.
In one sentence: functional_block_recognizer.py is the casting director that turns Layer 1's overlapping audition pile into one actor per role — by exact wiring when a topology blueprint exists, and by a funnel of connectivity heuristics (port-adjacency, signal-chain following, stage-splitting) when it doesn't.
Looking ahead → Part 6 · __init__.py is the last stop: the tiny public façade that re-exports everything you've now met into one clean, importable API.