Source code for circuitgenome.recognizer.models

"""
Core data models for the subcircuit and functional block recognizers.

All structures are plain dataclasses — they carry no logic and can be freely
inspected or passed between pipeline stages. They mirror the 3-layer
pipeline described in the design doc
(``docs/plans/2026_06_15_subcircuit_and_functional_block_recognizer.md``):

- :class:`ParsedNetlist` is the output of **Layer 0** (netlist parsing).
- :class:`SubcircuitRecognitionResult` (made up of
  :class:`RecognizedStructure` instances) is the output of **Layer 1**, the
  Subcircuit Recognizer (SR).
- :class:`FunctionalBlockRecognitionResult` (made up of
  :class:`SlotAssignment` instances) is the output of **Layer 2**, the
  Functional Block Recognizer (FBR).

:class:`PatternDevice`, :class:`PatternDef`, and :class:`HookMatch` describe
the SR pattern-library schema used by
:mod:`circuitgenome.recognizer.subcircuit_recognizer`.
"""
from __future__ import annotations
from dataclasses import dataclass, field

from circuitgenome.synthesizer.models import Device


[docs] @dataclass class ParsedNetlist: """A flat SPICE subcircuit, parsed into devices and net names. Output of :func:`~circuitgenome.recognizer.netlist_parser.parse` (Layer 0) -- the structural inverse of :func:`~circuitgenome.synthesizer.netlist.to_flat_spice`. :param name: The ``.subckt`` name. :param external_ports: Ordered list of port names from the ``.subckt`` header line. :param devices: Every device line in the subcircuit, as :class:`~circuitgenome.synthesizer.models.Device` instances. :param internal_nets: Net names referenced by ``devices`` that are not in ``external_ports``. """ name: str external_ports: list[str] devices: list[Device] internal_nets: set[str]
[docs] @dataclass class RecognizedStructure: """A single recognized structure -- one SR pattern match. Produced by :func:`~circuitgenome.recognizer.subcircuit_recognizer.recognize` (Layer 1). :data:`SubcircuitRecognitionResult.structures` may contain multiple overlapping ``RecognizedStructure`` instances covering the same devices (see :class:`SubcircuitRecognitionResult`); FBR (Layer 2) is responsible for picking, per topology slot, the one whose ``pins`` line up with that slot's expected connectivity. :param name: The matching :class:`PatternDef`'s name, e.g. ``"differential_pair_nmos"``. For composite patterns (see :class:`PatternDef`) this is also the corresponding ``opamp_modules.yaml`` variant name. :param category: The matching pattern's category, e.g. ``"input_pair"`` (one of the ``opamp_modules.yaml`` module categories). ``None`` for level-0 primitives and structural composites that have no FBR counterpart. :param circuit_block: High-level functional block, e.g. ``"gain_stage_1"``, ``"gain_stage_2"``, ``"bias"``, ``"compensation"``, or ``"cmfb"``. ``None`` for primitives and structural composites. Used by :func:`~circuitgenome.recognizer.functional_block_recognizer.group_by_category` to group structures without a topology template. :param index: 0-based instance number, distinguishing repeated matches of the same pattern (e.g. a second ``differential_pair_nmos`` candidate elsewhere in the netlist). :param tech_type: ``"n"``, ``"p"``, or ``None`` -- the technology of the pattern's :attr:`PatternDef.tech_type_from` template device, or ``None`` if the pattern doesn't declare one. :param pins: Maps pin names (from :attr:`PatternDef.pins`, plus any ``extra_pins`` contributed by a :class:`HookMatch`) to the actual net names they resolve to in this netlist. :param devices: The actual :class:`~circuitgenome.synthesizer.models.Device` instances matched by this structure (the base template assignment, plus any ``extra_devices`` from a :class:`HookMatch`). :param children: Sub-structures from multi-level composition. Empty for level-0 primitives and MVP composite-only patterns; populated for level-1+ patterns. """ name: str category: str | None circuit_block: str | None index: int tech_type: str | None pins: dict[str, str] devices: list[Device] children: list["RecognizedStructure"] = field(default_factory=list)
[docs] @dataclass class SubcircuitRecognitionResult: """Output of Layer 1, the Subcircuit Recognizer (SR). :param structures: Every :class:`RecognizedStructure` matched by any pattern in the library. **May contain multiple overlapping candidates for the same device-set** -- e.g. two patterns with structurally identical templates but different categories/names, or a composite pattern's reference device that also satisfies a smaller, unrelated pattern. SR reports every candidate; it does not pick a winner. Layer 2 (FBR) resolves overlaps using topology context. :param unrecognized_devices: Devices matched by *no* pattern. For a netlist produced by :func:`~circuitgenome.synthesizer.netlist.to_flat_spice` from a known :class:`~circuitgenome.synthesizer.models.SynthesizedCircuit`, this should be empty -- a non-empty list indicates a pattern-library gap. """ structures: list[RecognizedStructure] unrecognized_devices: list[Device] = field(default_factory=list)
[docs] @dataclass class PatternDevice: """One template device within a :class:`PatternDef`. :param ref: Template-local reference used by :attr:`PatternDef.same_net` and :attr:`PatternDef.pins` to refer to this device, e.g. ``"m1"``. Scoped to the enclosing pattern -- unrelated to the actual netlist's device refs. :param type: The device type this template slot matches: ``"nmos"``, ``"pmos"``, ``"resistor"``, or ``"capacitor"``. Matches :attr:`~circuitgenome.synthesizer.models.Device.type`. """ ref: str type: str
[docs] @dataclass class ChildDef: """One required child in a multi-level composite pattern. :param pattern: Name of the expected child :class:`PatternDef`, e.g. ``"diode_connected_nmos"``. :param devices: Template device refs (from the enclosing :class:`PatternDef`) that together form this child. E.g. ``["m_ref"]`` for a single-device child or ``["m_dr", "m_dl"]`` for a two-device child. """ pattern: str devices: list[str]
[docs] @dataclass class PatternDef: """A single SR pattern definition, loaded from ``config/primitives.yaml``, ``config/structural_patterns.yaml``, or ``config/opamp_patterns.yaml``. A pattern is a small template graph: a handful of typed :class:`PatternDevice` slots, ``same_net`` equality constraints between their terminals, and a ``pins`` map exporting named nets. See :func:`~circuitgenome.recognizer.subcircuit_recognizer.recognize` for the matching algorithm. **Level-0 primitive patterns** describe single-device topological facts (e.g. ``diode_connected_nmos``). Exclusive primitives claim a device for exactly one pattern (higher :attr:`priority` wins). **Level-1+ composite patterns** declare :attr:`children` referencing lower-level patterns they compose; the SR verifies those children are present before accepting a match. **MVP composite patterns** (no ``children``, not ``exclusive``) correspond 1:1 to an ``opamp_modules.yaml`` module variant and run in the same single-pass loop as before. :param name: Unique pattern name, e.g. ``"differential_pair_nmos"``. For composite patterns, also the ``opamp_modules.yaml`` variant name -- this is the value that ends up in :attr:`RecognizedStructure.name` and :attr:`SlotAssignment.pattern_name`. :param category: The ``opamp_modules.yaml`` module category this pattern represents, e.g. ``"input_pair"``. ``None`` for primitives and structural composites without an FBR counterpart. Copied into :attr:`RecognizedStructure.category`. :param circuit_block: High-level functional block this pattern belongs to, e.g. ``"gain_stage_1"``, ``"gain_stage_2"``, ``"bias"``, ``"compensation"``, or ``"cmfb"``. Uses ``gain_stage_N`` naming (never ``second_stage``) to avoid clashing with :attr:`category` values. ``None`` for primitives and structural composites. Copied into :attr:`RecognizedStructure.circuit_block`. :param devices: The pattern's template devices. The matcher searches for an injective assignment from these to actual netlist devices of matching :attr:`PatternDevice.type`. :param same_net: Equality constraints on resolved terminals, each written as a list of ``"template_ref.terminal"`` strings, e.g. ``[["m1.s", "m2.s"]]`` means "``m1``'s source and ``m2``'s source must be the same net". These are the *only* required equalities -- terminals not listed are unconstrained, so a more-connected real circuit can still match. :param pins: Maps an exported pin name to a ``"template_ref.terminal"`` string. Resolved through the matched assignment to produce :attr:`RecognizedStructure.pins`. :param tech_type_from: The template device ref (e.g. ``"m1"``) whose matched :attr:`~circuitgenome.synthesizer.models.Device.type` (first character, ``"n"`` or ``"p"``) becomes :attr:`RecognizedStructure.tech_type`. ``None`` if the pattern declares no dominant technology. :param hook: Optional ``"module:function"`` path to an extra-check function (see :class:`HookMatch`), resolved dynamically by :func:`~circuitgenome.recognizer.subcircuit_recognizer.recognize`. Most patterns have no hook. :param children: Required sub-structures for multi-level composition. Empty for level-0 patterns and MVP composites. When non-empty, ``recognize`` verifies each child is present as a matched structure at the previous level before accepting this pattern's match. :param exclusive: If ``True``, this pattern claims its matched device(s) exclusively in Pass 0 -- each device is assigned to at most one exclusive pattern. Only level-0 primitives use this. :param priority: Tie-breaking priority among ``exclusive`` patterns that match the same device; higher wins. E.g. ``diode_connected_nmos`` (priority 10) beats ``nmos`` (priority 0). """ name: str category: str | None circuit_block: str | None devices: list[PatternDevice] same_net: list[list[str]] pins: dict[str, str] tech_type_from: str | None = None hook: str | None = None children: list[ChildDef] = field(default_factory=list) exclusive: bool = False priority: int = 0
[docs] @dataclass class HookMatch: """Accept-and-extend result of a :attr:`PatternDef.hook` function. A hook is called once per base-template match (:func:`~circuitgenome.recognizer.subcircuit_recognizer.recognize`'s ``(assignment, pins, netlist)``) and returns either: - ``None`` -- reject this match entirely (it is dropped, as if the pattern hadn't matched). - a ``HookMatch`` -- accept the match, merging ``extra_devices`` into :attr:`RecognizedStructure.devices` and ``extra_pins`` into :attr:`RecognizedStructure.pins` (in addition to the base template's devices/pins). The motivating example is ``diode_connected_mosfet_bias`` (:func:`~circuitgenome.recognizer.hooks.diode_connected_mosfet_bias_legs`): its base template is just the 1-device diode-connected bias reference, and the hook discovers however many output "legs" (1-7, one per needed bias rail) are actually present in the netlist and appends their devices and ``legN_out`` pins. :param extra_devices: Additional :class:`~circuitgenome.synthesizer.models.Device` instances to append to :attr:`RecognizedStructure.devices`, beyond the base template assignment. :param extra_pins: Additional ``pin name -> net name`` entries to merge into :attr:`RecognizedStructure.pins`, beyond the base template's :attr:`PatternDef.pins`. """ extra_devices: list[Device] = field(default_factory=list) extra_pins: dict[str, str] = field(default_factory=dict)
[docs] @dataclass class SlotAssignment: """One FBR decision: a recognized structure assigned to a topology slot. :param slot_name: The :attr:`~circuitgenome.synthesizer.models.Slot.name` of the topology slot this assignment fills, e.g. ``"input_pair"``. :param pattern_name: The assigned structure's :attr:`RecognizedStructure.name`. For a correct round trip, this equals the :attr:`~circuitgenome.synthesizer.models.ModuleVariant.name` of the variant that was synthesized into this slot. :param structure: The assigned :class:`RecognizedStructure` itself. """ slot_name: str pattern_name: str structure: RecognizedStructure
[docs] @dataclass class FunctionalBlockRecognitionResult: """Output of Layer 2, the Functional Block Recognizer (FBR). MVP taxonomy: the ``opamp_modules.yaml`` categories plus the matched :class:`~circuitgenome.synthesizer.models.TopologyTemplate` (design doc section 6.2). Produced by :func:`~circuitgenome.recognizer.functional_block_recognizer.assign_slots`. :param slot_assignments: Maps each :attr:`~circuitgenome.synthesizer.models.Slot.name` that received a candidate to its :class:`SlotAssignment`. Shaped like :attr:`~circuitgenome.synthesizer.models.SynthesizedCircuit.variant_map` (``{slot_name: ...}``), so the two can be compared directly in round-trip tests. :param unassigned_structures: SR candidates that were not assigned to any slot (e.g. overlapping/spurious pattern matches that lost to a better-connected candidate). :param unrecognized_devices: Passed through unchanged from :attr:`SubcircuitRecognitionResult.unrecognized_devices`. """ slot_assignments: dict[str, SlotAssignment] unassigned_structures: list[RecognizedStructure] = field(default_factory=list) unrecognized_devices: list[Device] = field(default_factory=list)
[docs] @dataclass class CategoryGroupResult: """Layer 2 output when no :class:`~circuitgenome.synthesizer.models.TopologyTemplate` is available. Produced by :func:`~circuitgenome.recognizer.functional_block_recognizer.group_by_category`. Structures are grouped first by :attr:`~RecognizedStructure.circuit_block` (e.g. ``"gain_stage_1"``, ``"bias"``) then by :attr:`~RecognizedStructure.category` (e.g. ``"input_pair"``, ``"load"``). Within each category list, candidates are ranked by descending external-port adjacency — how many of the structure's pins connect directly to a subcircuit external port. :param groups: ``circuit_block → category → candidates`` (ranked, best-guess first). Structures with ``circuit_block=None`` are excluded. :param unrecognized_devices: Passed through unchanged from :attr:`SubcircuitRecognitionResult.unrecognized_devices`. """ groups: dict[str, dict[str, list[RecognizedStructure]]] unrecognized_devices: list[Device] = field(default_factory=list)