Skip to content

library ¤

Scoped model-card loading and hierarchical circuit elaboration.

Classes:

Name Description
FileDigests

sha256 of file bytes, re-read only when (mtime_ns, size) change.

Library

Load existing model libraries and elaborate their public subcircuits.

ResolvedCircuit

Evaluated model cards awaiting conversion to kfnetlist and native models.

ResolvedInstance

A leaf compact model with evaluated model and instance parameters.

FileDigests ¤

FileDigests()

sha256 of file bytes, re-read only when (mtime_ns, size) change.

A stamp is trusted only for files unmodified for a settling period before they were read, so an edit that keeps the size within one timestamp tick is still hashed. Hashing reads bytes only; nothing is loaded or validated.

Methods:

Name Description
digest

Return the content digest, or None when the file is missing.

Source code in circulax/netlist_io/library.py
def __init__(self) -> None:
    """Start with no remembered stamps."""
    self._entries: dict[Path, tuple[tuple[int, int], str]] = {}

digest ¤

digest(path: Path) -> str | None

Return the content digest, or None when the file is missing.

Source code in circulax/netlist_io/library.py
def digest(self, path: Path) -> str | None:
    """Return the content digest, or ``None`` when the file is missing."""
    try:
        status = path.stat()
    except OSError:
        self._entries.pop(path, None)
        return None
    stamp = (status.st_mtime_ns, status.st_size)
    entry = self._entries.get(path)
    if entry is not None and entry[0] == stamp:
        return entry[1]
    read_at = time.time_ns()
    digest = hashlib.sha256(path.read_bytes()).hexdigest()
    if read_at - status.st_mtime_ns > self._SETTLE_NS:
        self._entries[path] = (stamp, digest)
    else:
        self._entries.pop(path, None)
    return digest

Library ¤

Library(
    *,
    temperature_c: float = 27.0,
    include_paths: tuple[Path, ...] = (),
    dialect: str = "ngspice",
    statistical_mode: str = "reject"
)

Load existing model libraries and elaborate their public subcircuits.

Parameters and model cards are evaluated separately for each instance. Libraries retain their lexical scopes, local nodes and conditional branches.

Methods:

Name Description
from_file

Read a library and select a named corner section when requested.

include

Add a library to the same root scope, preserving declaration order.

inspection_sources

Fingerprint actual parsed source dependencies without native loading.

instantiate

Expand a library subcircuit without loading executable OSDI modules.

refreshed

Reuse unchanged parsed cards or reparse edited, removed or shadowed source dependencies.

resolve

Expand instances declared at the top level of a loaded circuit file.

with_temperature

Reparse source declarations at a new temperature without mutation.

Source code in circulax/netlist_io/library.py
def __init__(
    self,
    *,
    temperature_c: float = 27.0,
    include_paths: tuple[Path, ...] = (),
    dialect: str = "ngspice",
    statistical_mode: str = "reject",
) -> None:
    """Create an empty model library at the requested card temperature."""
    if not math.isfinite(temperature_c) or temperature_c <= -273.15:
        msg = "temperature_c must be finite and above absolute zero"
        raise NetlistError(msg)
    if statistical_mode not in {"reject", "nominal"}:
        msg = "statistical_mode must be 'reject' or 'nominal'"
        raise NetlistError(msg)
    self.temperature_c = temperature_c
    self.include_paths = tuple(Path(p) for p in include_paths)
    self.dialect = dialect
    self.loads: list[tuple[str, Path]] = []
    self.globals = {"0"}
    self.grounds = {"0"}
    self.scope = Scope()
    self.scope.dialect = dialect
    self.scope.statistical_mode = statistical_mode
    self.scope.bindings["$temp"] = temperature_c
    self.frame = _Frame(self.scope)
    self._parsed: dict[Path, list[Statement]] = {}
    self._parsed_fingerprints: dict[Path, str] = {}
    self._digests = FileDigests()
    # Include search results: candidate paths in order, and how many precede the one found.
    self._lookups: dict[tuple[Path, ...], int] = {}
    self._sources: list[tuple[Path, str | None]] = []
    self.revision = next(_REVISIONS)

from_file classmethod ¤

from_file(
    path: str | Path,
    *,
    section: str | None = None,
    temperature_c: float = 27.0,
    include_paths: tuple[Path, ...] = (),
    dialect: str = "ngspice",
    statistical_mode: str = "reject"
) -> Library

Read a library and select a named corner section when requested.

statistical_mode="nominal" explicitly evaluates agauss at its nominal value. The default rejects random functions; sampling is unsupported.

Source code in circulax/netlist_io/library.py
@classmethod
def from_file(
    cls,
    path: str | Path,
    *,
    section: str | None = None,
    temperature_c: float = 27.0,
    include_paths: tuple[Path, ...] = (),
    dialect: str = "ngspice",
    statistical_mode: str = "reject",
) -> Library:
    """Read a library and select a named corner section when requested.

    statistical_mode="nominal" explicitly evaluates agauss at its nominal
    value. The default rejects random functions; sampling is unsupported.
    """
    library = cls(temperature_c=temperature_c, include_paths=include_paths, dialect=dialect, statistical_mode=statistical_mode)
    library.include(path, section=section)
    return library

include ¤

include(path: str | Path, *, section: str | None = None) -> None

Add a library to the same root scope, preserving declaration order.

Source code in circulax/netlist_io/library.py
def include(self, path: str | Path, *, section: str | None = None) -> None:
    """Add a library to the same root scope, preserving declaration order."""
    source = Path(path).resolve()
    statements = self._read(source, section, (), set())
    self._populate(self.frame, statements)
    self._sources.append((source, section))

inspection_sources ¤

inspection_sources() -> list[dict[str, str]]

Fingerprint actual parsed source dependencies without native loading.

Source code in circulax/netlist_io/library.py
def inspection_sources(self) -> list[dict[str, str]]:
    """Fingerprint actual parsed source dependencies without native loading."""
    records = []
    for path in sorted(self._parsed, key=str):
        digest = self._digests.digest(path)
        if digest is None:
            msg = f"{path}: library source not found"
            raise NetlistError(msg)
        records.append({"path": str(path), "sha256": digest})
    return records

instantiate ¤

instantiate(
    subcircuit: str,
    nodes: tuple[str, ...] | None = None,
    settings: dict[str, float] | None = None,
    *,
    name: str = "X1"
) -> ResolvedCircuit

Expand a library subcircuit without loading executable OSDI modules.

Source code in circulax/netlist_io/library.py
def instantiate(
    self, subcircuit: str, nodes: tuple[str, ...] | None = None, settings: dict[str, float] | None = None, *, name: str = "X1"
) -> ResolvedCircuit:
    """Expand a library subcircuit without loading executable OSDI modules."""
    found = self.frame.find(subcircuit.lower(), "subcircuits")
    if found is None:
        msg = f"unknown subcircuit {subcircuit!r}"
        raise NetlistError(msg)
    _, definition = found
    ports = tuple(_nodes(definition[0].node))
    actual_nodes = nodes if nodes is not None else ports
    instances: list[ResolvedInstance] = []
    self._instantiate(definition, actual_nodes, settings or {}, name, instances, ())
    return ResolvedCircuit(instances, list(self.loads), dict(zip(ports, actual_nodes, strict=True)), self.temperature_c)

refreshed ¤

refreshed() -> Library

Reuse unchanged parsed cards or reparse edited, removed or shadowed source dependencies.

Source code in circulax/netlist_io/library.py
def refreshed(self) -> Library:
    """Reuse unchanged parsed cards or reparse edited, removed or shadowed source dependencies."""
    unchanged = all(self._digests.digest(path) == fingerprint for path, fingerprint in self._parsed_fingerprints.items())
    if unchanged and not any(c.is_file() for candidates, found in self._lookups.items() for c in candidates[:found]):
        return self
    return self.with_temperature(self.temperature_c)

resolve ¤

resolve() -> ResolvedCircuit

Expand instances declared at the top level of a loaded circuit file.

Source code in circulax/netlist_io/library.py
def resolve(self) -> ResolvedCircuit:
    """Expand instances declared at the top level of a loaded circuit file."""
    instances: list[ResolvedInstance] = []
    self._calls(self.frame, {}, "", instances, ())
    return ResolvedCircuit(instances, list(self.loads), temperature_c=self.temperature_c)

with_temperature ¤

with_temperature(temperature_c: float) -> Library

Reparse source declarations at a new temperature without mutation.

Source code in circulax/netlist_io/library.py
def with_temperature(self, temperature_c: float) -> Library:
    """Reparse source declarations at a new temperature without mutation."""
    if not self._sources:
        msg = "Temperature override requires reparsable library source declarations"
        raise ValueError(msg)
    library = type(self)(
        temperature_c=temperature_c,
        include_paths=self.include_paths,
        dialect=self.dialect,
        statistical_mode=self.scope.statistical_mode,
    )
    for path, section in self._sources:
        library.include(path, section=section)
    return library

ResolvedCircuit dataclass ¤

ResolvedCircuit(
    instances: list[ResolvedInstance],
    loads: list[tuple[str, Path]],
    ports: dict[str, str] = dict(),
    temperature_c: float = 27.0,
)

Evaluated model cards awaiting conversion to kfnetlist and native models.

This intermediate retains ordered terminals and load declarations needed for OSDI provisioning. Compilation builds the canonical kfnetlist.Netlist.

Methods:

Name Description
compile

Compile with immutable native analysis mode: dc (default), ac, or tran.

compile ¤

compile(
    *,
    module_paths: tuple[Path, ...] = (),
    osdi_modules: tuple[Path, ...] = (),
    compiler: str | None = None,
    cache_dir: Path | None = None,
    backend: str = "dense",
    analysis: str = "dc",
    state_policy: str = "reject",
    simparams: Mapping[str, float] | None = None
) -> Any

Compile with immutable native analysis mode: dc (default), ac, or tran.

simparams supplies numeric $simparam settings to all native devices. Separate registrations are required to change mode. For AC, retain DC conductance and obtain capacitance from an AC registration.

osdi_modules registers compiled .osdi/.va files by their own descriptor name directly, for libraries (e.g. plain ngspice model cards) that reference a module by name without a load statement.

Source code in circulax/netlist_io/library.py
def compile(
    self,
    *,
    module_paths: tuple[Path, ...] = (),
    osdi_modules: tuple[Path, ...] = (),
    compiler: str | None = None,
    cache_dir: Path | None = None,
    backend: str = "dense",
    analysis: str = "dc",
    state_policy: str = "reject",
    simparams: Mapping[str, float] | None = None,
) -> Any:
    """Compile with immutable native analysis mode: dc (default), ac, or tran.

    simparams supplies numeric $simparam settings to all native devices.
    Separate registrations are required to change mode. For AC, retain
    DC conductance and obtain capacitance from an AC registration.

    osdi_modules registers compiled `.osdi`/`.va` files by their own
    descriptor name directly, for libraries (e.g. plain ngspice model
    cards) that reference a module by name without a `load` statement.
    """
    from circulax.netlist_io.osdi import compile_resolved

    return compile_resolved(
        self,
        module_paths=module_paths,
        osdi_modules=osdi_modules,
        compiler=compiler,
        cache_dir=cache_dir,
        backend=backend,
        analysis=analysis,
        state_policy=state_policy,
        simparams=simparams,
    )

ResolvedInstance dataclass ¤

ResolvedInstance(
    name: str, module: str, nodes: tuple[str, ...], parameters: dict[str, float]
)

A leaf compact model with evaluated model and instance parameters.