Subcircuit Recognizer¶
The Subcircuit Recognizer (SR) is the first structural half of the recognizer pipeline — the inverse of the synthesizer. Given a flat SPICE netlist, it recovers the building blocks that produced it in two layers:
Layer 0 — netlist parsing (
parse()) turns flat SPICE text back into aParsedNetlist(devices plus external ports and internal nets).Layer 1 — subcircuit recognition (
recognize()) matches a library of small structural patterns — differential pairs, current mirrors, cascode loads, bias legs — against the parsed devices, producing aSubcircuitRecognitionResult.
The recognizer is deliberately split into two modules. SR is structural and circuit-agnostic — a differential pair or a current mirror is the same shape whether it sits in an op-amp, a comparator, or a DAC — so it matches building blocks and reports all candidates (including overlapping ones) without picking a winner. The Functional Block Recognizer (FBR) then adds the circuit-specific interpretation, assigning each block its functional role and resolving the overlaps; that semantic layer is op-amp-only in this version. Keeping the structural layer separate lets it be reused for other circuit families as they are added. Constraints that resist a declarative pattern are handled by hooks.
Deep dive: the recognizer walkthrough is a figure-rich tour of the recognizer code itself (see Code Walkthroughs).
Entry points¶
parse()— flat SPICE → parsed netlist.recognize()— parsed netlist → recognized structures.
Netlist parsing (Layer 0)¶
parse() is the structural
inverse of to_flat_spice(): it reads
a .subckt <name> <port...> / .ends block with one MOSFET device line
per device,
{ref} {d} {g} {s} {b} {nmos|pmos}
and produces a ParsedNetlist. Net
and ref names are treated as arbitrary strings – the parser makes no
assumptions about to_flat_spice’s own naming conventions. Resistor lines
(r<ref> <t1> <t2> <value>) and capacitor lines (c<ref> <p> <m>
<value>) are also handled; the leading character of ref determines the
device type (r → resistor, c → capacitor).
Subcircuit recognition (Layer 1)¶
The SR pattern library
(circuitgenome/recognizer/config/subcircuit_patterns.yaml, loaded by
load_patterns()) is a
list of small template graphs. Each pattern declares:
devices– typed template slots (nmos/pmos), e.g.m1,m2.same_net– terminal-equality constraints between slots, e.g.[m1.s, m2.s](”m1’s source andm2’s source must be the same net”); unlisted terminals are unconstrained.pins– named nets exported by the pattern, e.g.in1: m1.g.tech_type_from– which template device’s matched type ("n"/"p") becomes the recognized structure’stech_type.circuit_block– a coarse functional-role label one level abovecategory(gain_stage_1,gain_stage_2,bias,compensation,cmfb,output_stage_block), used by the topology-free FBR pass to group structures by where they sit in the op-amp.an optional
hook– a"module:function"extra-check for constraints too awkward to express declaratively.
Composite patterns correspond 1:1 to an opamp_modules.yaml module variant
and reuse its name, so a successful match’s
name is directly
comparable to a
variant_map
entry’s variant name. The library covers every module variant across the
templates the synthesizer produces – 43 patterns across eight categories:
Category |
Patterns (count) |
Notes |
|---|---|---|
|
5 |
|
|
14 |
|
|
8 |
|
|
4 |
All four use hooks (below) to discover however many output legs are present. |
|
2 |
Both use |
|
3 |
Connectivity scoring naturally disambiguates the overlapping 1-device subsets without hooks. |
|
5 |
|
|
2 |
Fills the |
recognize() matches
every pattern against the netlist’s devices via a small backtracking search
(patterns are 1-4 devices, so no graph library is needed), filtering
candidates by device type and checking same_net. A pattern’s optional
hook can further constrain or extend each match — see Hooks below.
Hooks¶
A hook is a Python callback attached to a pattern for constraints too
awkward to express in the declarative YAML. It runs once per base-template
match and either rejects the match (returns None) or accepts it,
optionally merging in extra devices and pins (a
HookMatch). This lets a small fixed
template match a variable-size structure — e.g. a bias generator with any
number of output legs — or apply a guard a purely declarative pattern cannot
express.
Six hooks are implemented in circuitgenome.recognizer.hooks:
constructed_bias_legs()discovers the constructed generator’s legs (NMOS-referenced pairs, theprefbranch, gnd-referenced / current / resistor legs off the PMOS-side reference), rejecting purely NMOS-referenced shapes so they resolve to the historical pattern below.diode_connected_mosfet_bias_legs(),magic_battery_bias_legs(), andresistor_bias_legs()each handle a historical single-flavor generator: the shared reference device is always present, but the number of output “legs” (0-7) varies per netlist. The base template matches only the reference device; the hook walks the netlist to find however many legs are actually present and appends their devices andlegN_outpins toHookMatch.resistor_tail_vdd_check()andresistor_tail_gnd_check()each accept a single-resistortail_currentmatch only if the resistor’s supply-side terminal is the globalvdd!/gnd!rail, preventing the unconstrained 1-device template from spuriously matching every resistor in the netlist.
Recognition result¶
The result, a
SubcircuitRecognitionResult, may
contain multiple overlapping candidates for the same device(s) – SR does
not pick a winner. For example, current_mirror_tail_nmos and
diode_connected_mosfet_bias share the same 2-terminal diode-connected
shape, so a single diode-connected NMOS may match the base template of both
patterns; disambiguation is FBR’s job.
unrecognized_devices lists any device matched by no pattern – for a
netlist produced from a known SynthesizedCircuit with full pattern
coverage, this should be empty.
That diode-connected-NMOS overlap surfaces as two structures that both claim
the same device (fields abbreviated for illustration; devices is really a
list of Device objects, shown here as
their refs m5/m6):
SubcircuitRecognitionResult(
structures=[
...,
RecognizedStructure(name="current_mirror_tail_nmos",
category="tail_current", tech_type="n",
pins={"out": "net_tail", "bias": "net_bias7"},
devices=[m5, m6]),
RecognizedStructure(name="diode_connected_mosfet_bias",
category="bias_generation", tech_type="n",
pins={"ref": "net_bias7"},
devices=[m5]), # m5 also claimed above
],
unrecognized_devices=[], # full coverage
)
SR pattern coverage¶
Coverage is measured against the synthesizer, not asserted by inspection.
Every module variant the synthesizer can emit has a matching SR pattern, and the
synthesizer emits circuits per topology template — so the template is the natural
unit for checking coverage: for each one, round-trip test cases synthesize
representative circuits, flatten them to SPICE, and run them back through SR +
FBR, asserting the original variant_map is recovered with no unrecognized
devices. The table breaks the library down by template — the patterns each first
introduces, and the round-trip combos that exercise it:
Patterns introduced — the SR patterns this template is the first to need. The counts total all 43, so the column doubles as a coverage checklist (every pattern is introduced by some template).
Round-trip combos — the number of round-trip test cases targeting that template.
Template |
Patterns introduced |
Round-trip combos |
|---|---|---|
|
27 — 5 |
11 |
|
+6 — 3 |
11 |
|
+6 — 4 differential |
13 |
|
+2 — |
10 |
|
none — reuses existing patterns per output path |
8 |
|
+2 — |
within the 2-/3-stage rows |
The table’s 53 combos plus 2 opt-in stacked-diode cascode-tail round-trips
(include_infeasible) total 55, all asserting
unrecognized_devices == [] and full variant_map recovery. Combos are
hand-picked to cover every variant and to avoid a few known bias-pattern
ambiguities that would otherwise need extra disambiguation code.
Primitive/multi-level pattern composition and topology identification from an arbitrary netlist are deferred to later milestones.