Source code for circuitgenome.synthesizer.netlist

"""
SPICE netlist serializers.

Two output formats are supported:

- **Flat** (:func:`to_flat_spice`) — all devices inlined into a single
  ``.subckt`` block.  Maximally portable; every SPICE simulator can read it.
- **Hierarchical** (:func:`to_hierarchical_spice`) — one ``.subckt`` definition
  per module variant, instantiated via ``X`` calls in the top-level block.
  Easier to read and avoids repeating shared subcircuits across many circuits.
"""
from __future__ import annotations
from .models import Device, SynthesizedCircuit


def _device_line(ref: str, dev: Device) -> str:
    t = dev.type.lower()
    if t in ("nmos", "pmos"):
        d = dev.terminals.get("d", "?")
        g = dev.terminals.get("g", "?")
        s = dev.terminals.get("s", "?")
        b = dev.terminals.get("b", "?")
        return f"{ref} {d} {g} {s} {b} {t}"
    if t == "resistor":
        t1 = dev.terminals.get("t1", "?")
        t2 = dev.terminals.get("t2", "?")
        return f"{ref} {t1} {t2} 1k"
    if t == "capacitor":
        p = dev.terminals.get("p", "?")
        m = dev.terminals.get("m", "?")
        return f"{ref} {p} {m} 1p"
    terms = " ".join(dev.terminals.values())
    return f"{ref} {terms} {t}"


[docs] def to_flat_spice(circuit: SynthesizedCircuit, name: str | None = None) -> str: """Serialize *circuit* as a flat SPICE subcircuit. All devices from every module slot are inlined into a single ``.subckt`` block. Device reference designators are suffixed with the slot name (e.g. ``m1_input_pair``) so the leading character still identifies the SPICE primitive type; internal nets are prefixed with the slot name (e.g. ``load_internal_0``). :param circuit: A :class:`~circuitgenome.synthesizer.models.SynthesizedCircuit` returned by :func:`~circuitgenome.synthesizer.synthesizer.synthesize` or :func:`~circuitgenome.synthesizer.synthesizer.enumerate_circuits`. :param name: Override the subcircuit name. Defaults to ``circuit.name``. :returns: A SPICE netlist string starting with ``.subckt`` and ending with ``.ends``. Example:: from circuitgenome import synthesize from circuitgenome.synthesizer import to_flat_spice circuit = synthesize({"topology": "one_stage_opamp"})[0] print(to_flat_spice(circuit, name="my_ota")) """ subckt_name = name or circuit.name ports = " ".join(circuit.external_ports) lines = [f".subckt {subckt_name} {ports}"] for ref, dev in circuit.devices: lines.append(_device_line(ref, dev)) lines.append(".ends") return "\n".join(lines)
[docs] def to_hierarchical_spice(circuit: SynthesizedCircuit, name: str | None = None) -> str: """Serialize *circuit* as a hierarchical SPICE netlist. Emits one ``.subckt`` definition per distinct module variant used, followed by a top-level ``.subckt`` that instantiates them with ``X`` calls. Duplicate module variants (same name) are defined only once. :param circuit: A :class:`~circuitgenome.synthesizer.models.SynthesizedCircuit`. :param name: Override the top-level subcircuit name. :returns: A multi-block SPICE string. Example:: from circuitgenome import synthesize from circuitgenome.synthesizer import to_hierarchical_spice circuit = synthesize({"topology": "one_stage_opamp"})[0] print(to_hierarchical_spice(circuit, name="my_ota_hier")) """ lines: list[str] = [] seen_variants: set[str] = set() for slot_name, variant in circuit.variant_map.items(): if variant.name in seen_variants: continue seen_variants.add(variant.name) port_names = [p.name for p in variant.ports if p.role != "optional"] ports_str = " ".join(port_names) lines.append(f".subckt {variant.name} {ports_str}") for dev in variant.devices: lines.append(_device_line(dev.ref, dev)) lines.append(".ends") lines.append("") top_name = name or circuit.name ext_ports = " ".join(circuit.external_ports) lines.append(f".subckt {top_name} {ext_ports}") for slot_name, variant in circuit.variant_map.items(): non_optional_ports = [p.name for p in variant.ports if p.role != "optional"] nets_for_slot = _recover_port_nets(slot_name, variant, circuit) net_vals = [nets_for_slot.get(p, f"<{p}>") for p in non_optional_ports] net_str = " ".join(net_vals) lines.append(f"X{slot_name} {net_str} {variant.name}") lines.append(".ends") return "\n".join(lines)
def _recover_port_nets( slot_name: str, variant, circuit: SynthesizedCircuit, ) -> dict[str, str]: """ Recover the port→global_net mapping for a slot by scanning the flat device list and reversing the terminal substitution applied during synthesis. """ port_names = {p.name for p in variant.ports} slot_suffix = f"_{slot_name}" port_to_global: dict[str, str] = {} for ref, dev in circuit.devices: if not ref.endswith(slot_suffix): continue local_ref = ref[: -len(slot_suffix)] orig_dev = next((d for d in variant.devices if d.ref == local_ref), None) if orig_dev is None: continue for term, local_net in orig_dev.terminals.items(): if local_net in port_names: global_net = dev.terminals.get(term) if global_net: port_to_global[local_net] = global_net for p in variant.ports: if p.name not in port_to_global: if p.name == "vdd": port_to_global["vdd"] = "vdd!" elif p.name == "gnd": port_to_global["gnd"] = "gnd!" # Ports that alias another port (e.g. a non-cascode load's out1/out2 # aliasing in1/in2) are never referenced directly by a device terminal, # so recover their net from the aliased port instead. for p in variant.ports: if p.name not in port_to_global and p.alias_of and p.alias_of in port_to_global: port_to_global[p.name] = port_to_global[p.alias_of] return port_to_global