Source code for circuitgenome.recognizer.netlist_parser
"""
Layer 0 — netlist parsing.
Parses :func:`circuitgenome.synthesizer.netlist.to_flat_spice` output into a
:class:`~circuitgenome.recognizer.models.ParsedNetlist`. This is the
structural inverse of ``to_flat_spice``'s ``_device_line``:
.. code-block:: text
{ref} {d} {g} {s} {b} {nmos|pmos} # MOSFET (6 tokens)
{ref} {t1} {t2} [1k] # resistor (3-4 tokens)
{ref} {p} {m} [1p] # capacitor (3-4 tokens)
wrapped in a ``.subckt <name> <port...>`` / ``.ends`` block (or ``.suckt``
for netlists with that common typo). Net and ref names are treated as
arbitrary strings -- the parser makes no assumptions about ``to_flat_spice``'s
own naming conventions (e.g. ``net_bias{N}`` or ``{ref}_{slot}``).
Device lines are dispatched by the first character of the ref (SPICE
convention): ``m`` → MOSFET (6 tokens), ``c`` → capacitor (3-4 tokens),
``r`` → resistor (3-4 tokens). The value token (e.g. ``1k`` / ``1p``) is
optional and ignored when present.
"""
from __future__ import annotations
from circuitgenome.synthesizer.models import Device
from .models import ParsedNetlist
_MOSFET_TYPES = {"nmos", "pmos"}
[docs]
def parse(spice: str) -> ParsedNetlist:
"""Parse a flat SPICE ``.subckt`` block into a :class:`ParsedNetlist`.
:param spice: A SPICE netlist string, typically produced by
:func:`circuitgenome.synthesizer.netlist.to_flat_spice`
or an external SPICE tool. The header keyword may be
``.subckt`` or ``.suckt`` (common typo). The value token
on capacitor/resistor lines (``1p`` / ``1k``) is optional.
:returns: A :class:`ParsedNetlist` with ``devices`` in the same order as
the input, and ``internal_nets`` = every net referenced by a
device terminal that is not in ``external_ports``.
:raises ValueError: If a MOSFET line's trailing type token is not
``"nmos"``/``"pmos"``, or a device line has an
unexpected token count for its prefix type.
Example::
from circuitgenome import synthesize
from circuitgenome.synthesizer import to_flat_spice
from circuitgenome.recognizer import parse
circuit = synthesize({"topology": "one_stage_opamp"})[0]
parsed = parse(to_flat_spice(circuit))
print(parsed.external_ports) # ["ibias", "in1", "in2", "out", "vdd!", "gnd!"]
print(len(parsed.devices))
"""
name = ""
external_ports: list[str] = []
devices: list[Device] = []
referenced_nets: set[str] = set()
for line in spice.splitlines():
tokens = line.split()
if not tokens:
continue
if tokens[0] in (".subckt", ".suckt"):
name = tokens[1]
external_ports = tokens[2:]
elif tokens[0] in (".ends", ".end"):
continue
else:
ref = tokens[0]
first = ref[0].lower()
if first == "m":
if len(tokens) != 6:
raise ValueError(f"Unsupported MOSFET line: {line!r}")
dev_type = tokens[5].lower()
if dev_type not in _MOSFET_TYPES:
raise ValueError(f"Unsupported device type {dev_type!r} on line: {line!r}")
terminals = {"d": tokens[1], "g": tokens[2], "s": tokens[3], "b": tokens[4]}
elif first == "c":
if len(tokens) not in (3, 4):
raise ValueError(f"Unsupported capacitor line: {line!r}")
terminals = {"p": tokens[1], "m": tokens[2]}
dev_type = "capacitor"
elif first == "r":
if len(tokens) not in (3, 4):
raise ValueError(f"Unsupported resistor line: {line!r}")
terminals = {"t1": tokens[1], "t2": tokens[2]}
dev_type = "resistor"
else:
raise ValueError(f"Unsupported device line: {line!r}")
devices.append(Device(ref=ref, type=dev_type, terminals=terminals))
referenced_nets.update(terminals.values())
internal_nets = referenced_nets - set(external_ports)
return ParsedNetlist(
name=name,
external_ports=external_ports,
devices=devices,
internal_nets=internal_nets,
)