Source code for pdkmaster.io.spice.pyspice

# SPDX-License-Identifier: GPL-2.0-or-later OR AGPL-3.0-or-later OR CERN-OHL-S-2.0+
from typing import Tuple, Dict, Iterable, Optional, Any

from PySpice.Spice.Netlist import Circuit, SubCircuit
from PySpice.Unit import u_µm, u_Ω # type: ignore

from .typing import CornerSpec
from ... import _util
from ...technology import primitive as _prm
from ...design import circuit as _ckt


__all__ = ["PySpiceFactory"]


def _sanitize_name(name):
    return name.replace("(", "[").replace(")", "]")


class _SubCircuit(SubCircuit):
    def __init__(self, *, circuit: _ckt._Circuit, lvs: bool):
        ports = tuple(_sanitize_name(port.name) for port in circuit.ports)
        name = _sanitize_name(circuit.name)

        super().__init__(name, *ports)
        self._circuit = circuit

        netlookup = {}
        for net in circuit.nets:
            lookup = {port: net for port in net.childports}
            double = tuple(filter(lambda n: n in netlookup, lookup))
            if double:
                doublenames = tuple(net.full_name for net in double)
                raise ValueError(
                    f"Ports {doublenames} are on more than one net in circuit "
                    f"{circuit.name}"
                )
            netlookup.update(lookup)

        for inst in circuit.instances:
            if isinstance(inst, _ckt._PrimitiveInstance):
                if isinstance(inst.prim, _prm.MOSFET):
                    sgdb = []
                    for portname in (
                        "sourcedrain1", "gate", "sourcedrain2", "bulk",
                    ):
                        port = inst.ports[portname]
                        try:
                            net = netlookup[port]
                        except KeyError:
                            raise ValueError(
                                f"Port '{port.full_name}' not on any net in circuit "
                                f"'{name}'"
                            )
                        else:
                            sgdb.append(_sanitize_name(net.name))
                    # TODO: support more instance parameters
                    self.M(inst.name, *sgdb,
                        model=inst.prim.model,
                        l=u_µm(round(inst.params["l"],6)), w=u_µm(round(inst.params["w"],6)),
                    )
                elif isinstance(inst.prim, _prm.Bipolar):
                    cbe = []
                    for portname in ("collector", "base", "emitter"):
                        port = inst.ports[portname]
                        try:
                            net= netlookup[port]
                        except KeyError:
                            raise ValueError(
                                f"Port '{port.full_name}' not on any net in circuit "
                                f"'{name}'"
                            )
                        else:
                            cbe.append(_sanitize_name(net.name))
                    if not inst.prim.is_subcircuit:
                        self.Q(inst.name, *cbe, model=inst.prim.model)
                    else:
                        self.X(inst.name, inst.prim.model, *cbe)
                elif isinstance(inst.prim, _prm.Resistor):
                    has_model = inst.prim.model is not None
                    has_sheetres = inst.prim.sheetres is not None
                    if not (has_model or has_sheetres):
                        raise NotImplementedError(
                            "Resistor circuit generation without a model or sheet resistance"
                        )

                    if (not has_model) or lvs:
                        l = inst.params["height"]
                        w = inst.params["width"]
                        assert inst.prim.sheetres is not None
                        self.R(
                            inst.name,
                            _sanitize_name(netlookup[inst.ports.port1].name),
                            _sanitize_name(netlookup[inst.ports.port2].name),
                            u_Ω(round(inst.prim.sheetres*l/w, 10)),
                        )
                    else:
                        assert has_model
                        params = inst.prim.subckt_params
                        w = round(inst.params["width"], 6)
                        l = round(inst.params["height"], 6)
                        if params is None:
                            assert inst.prim.sheetres is not None
                            self.SemiconductorResistor(
                                inst.name,
                                _sanitize_name(netlookup[inst.ports.port1].name),
                                _sanitize_name(netlookup[inst.ports.port2].name),
                                u_Ω(round(inst.prim.sheetres*l/w, 10)),
                                model=inst.prim.model, w=u_µm(w), l=u_µm(l),
                            )
                        else:
                            model_args = {
                                params["width"]: w,
                                params["height"]: l,
                            }
                            self.X(
                                inst.name, inst.prim.model,
                                _sanitize_name(netlookup[inst.ports.port1].name),
                                _sanitize_name(netlookup[inst.ports.port2].name),
                                **model_args,
                            )
                elif isinstance(inst.prim, _prm.MIMCapacitor):
                    # TODO: Implement lvs
                    w = inst.params["width"]
                    h = inst.params["height"]
                    if inst.prim.subckt_params is not None:
                        param_names = inst.prim.subckt_params
                    else:
                        param_names = {"width": "w", "height": "l"}
                    model_args = {param_names["width"]: w, param_names["height"]: h}
                    # TODO: Make port order configurable
                    self.X(
                        inst.name, inst.prim.model,
                        _sanitize_name(netlookup[inst.ports.top].name),
                        _sanitize_name(netlookup[inst.ports.bottom].name),
                        **model_args,
                    )
                elif isinstance(inst.prim, _prm.Diode):
                    if inst.prim.model is None:
                        raise NotImplementedError("Diode generation without a model")
                    w = inst.params["width"]
                    h = inst.params["height"]
                    self.D(
                        inst.name,
                        _sanitize_name(netlookup[inst.ports.anode].name),
                        _sanitize_name(netlookup[inst.ports.cathode].name),
                        model=inst.prim.model, area=round(w*h, 6)*1e-12, pj=u_µm(round(2*(w + h), 6)),
                    )
            elif isinstance(inst, _ckt._CellInstance):
                pin_args = tuple()
                for port in inst.ports:
                    try:
                        net = netlookup[port]
                    except KeyError:
                        raise ValueError(
                            f"Port '{port.full_name}' not on any net in circuit "
                            f"'{name}'"
                        )
                    else:
                        pin_args += (net.name,)
                pin_args = tuple(
                    _sanitize_name(netlookup[port].name) for port in inst.ports
                )
                self.X(inst.name, _sanitize_name(inst.circuit.name), *pin_args)
            else:
                raise AssertionError("Internal error")


class _Circuit(Circuit):
    def __init__(self, *,
        fab: "PySpiceFactory", corner: CornerSpec, top: _ckt._Circuit,
        subckts: Optional[Iterable[_ckt._Circuit]], title: Optional[str], gnd: Optional[str],
    ):
        if title is None:
            title = f"{top.name} testbench"
        super().__init__(title)

        corner = _util.v2t(corner)
        invalid = tuple(filter(lambda c: c not in fab.corners, corner))
        if invalid:
            raise ValueError(f"Invalid corners(s) {invalid}")
        for c in corner:
            try:
                conflicts = fab.conflicts[c]
            except KeyError:
                pass
            else:
                for c2 in conflicts:
                    if c2 in corner:
                        raise ValueError(
                            f"Corner '{c}' conflicts with corner '{c2}'"
                        )
            self.lib(fab.libfile, c)

        self._fab = fab
        self._corner = corner

        if subckts is None:
            scan = [top]
            scanned = []

            while scan:
                circuit = scan.pop()
                try:
                    # If it is in the scanned list put the circuit at the end
                    scanned.remove(circuit)
                except ValueError:
                    # If it is not in the scanned list, add subcircuits in the scan list
                    for inst in circuit.instances.__iter_type__(_ckt._CellInstance):
                        circuit2 = inst.cell.circuit
                        try:
                            scan.remove(circuit2)
                        except ValueError:
                            pass
                        scan.append(circuit2)
                scanned.append(circuit)
            scanned.reverse()
            subckts = scanned
        psubckts = tuple(
            fab.new_pyspicesubcircuit(circuit=c) for c in (*subckts, top)
        )
        for subckt in psubckts:
            self.subcircuit(subckt)
        self.X(
            "top", top.name,
            *(self.gnd if node==gnd else node for node in psubckts[-1]._external_nodes),
        )

    # Stop pylance from deriving types from PySpice code
    def simulator(self, *args, **kwargs) -> Any:
        return super().simulator(*args, **kwargs)


[docs]class PySpiceFactory: def __init__(self, *, libfile: str, corners: Iterable[str], conflicts: Dict[str, Tuple[str, ...]], ): s = ( "conflicts has to be a dict where the element value is a list of corners\n" "that conflict with the key" ) for key, value in conflicts.items(): if (key not in corners) or any(c not in corners for c in value): raise ValueError(s) self.libfile = libfile self.corners = set(corners) self.conflicts = conflicts
[docs] def new_pyspicecircuit(self, *, corner: CornerSpec, top: _ckt._Circuit, subckts: Optional[Iterable[_ckt._Circuit]]=None, title: Optional[str]=None, gnd: Optional[str]=None, ): return _Circuit( fab=self, corner=corner, top=top, subckts=subckts, title=title, gnd=gnd, )
[docs] def new_pyspicesubcircuit(self, *, circuit: _ckt._Circuit, lvs: bool=False): return _SubCircuit(circuit=circuit, lvs=lvs)