Source code for circuitgenome.sizer.shared.gmid_lut

"""gm/Id lookup table for the procedural PTM sizer.

Loads a committed ``*_gmid.npz`` (produced by ``tools/extract_tech.py --gm-id``)
and provides bilinear interpolation of the canonical gm/Id quantities over a
``(gm/Id, L)`` grid, plus the inverse used at metric-evaluation time.

The table is the source of truth for device physics in the gm/Id path: it
replaces the square-law ``gm``/``gds``/``vds_sat`` and the ``25·Id``
weak-inversion ceiling heuristic with measured BSIM4 behaviour.

Axes (both per polarity, fields shaped ``(n_L, n_gmid)``):
    ``gm_id_axis``  uniform gm/Id grid in 1/V (weak → strong inversion)
    ``l_axis``      channel lengths in µm

Fields: ``id_w`` (A/µm), ``gm_gds`` (V/V), ``ft`` (Hz), ``vdsat`` (V),
``vgs`` (V, magnitude).
"""
from __future__ import annotations

from pathlib import Path

import numpy as np

_FIELDS = ("id_w", "gm_gds", "ft", "vdsat", "vgs")


[docs] class GmIdLut: """Interpolating gm/Id table for both NMOS and PMOS.""" def __init__(self, path: Path | str): """Load the committed ``*_gmid.npz`` table from ``path``.""" data = np.load(path) self.gm_id_axis: np.ndarray = data["gm_id_axis"] self.l_axis: np.ndarray = data["l_axis"] self._fields: dict[str, dict[str, np.ndarray]] = { dtype: {f: data[f"{dtype}_{f}"] for f in _FIELDS} for dtype in ("nmos", "pmos") } # -- internal helpers --------------------------------------------------- def _row(self, dtype: str, field: str, l_um: float) -> np.ndarray: """Field as a 1-D array over the gm/Id axis at length ``l_um``. Linear interpolation between the two bracketing length rows (clamped to the L-axis range). """ la = self.l_axis grid = self._fields[dtype][field] l = float(np.clip(l_um, la[0], la[-1])) j = int(np.searchsorted(la, l)) if j <= 0: return grid[0].copy() if j >= len(la): return grid[-1].copy() l0, l1 = la[j - 1], la[j] t = (l - l0) / (l1 - l0) if l1 > l0 else 0.0 return grid[j - 1] * (1.0 - t) + grid[j] * t def _lookup(self, dtype: str, field: str, gm_id: float, l_um: float) -> float: """Bilinear interpolation of ``field`` at ``(gm_id, l_um)`` (clamped).""" row = self._row(dtype, field, l_um) g = float(np.clip(gm_id, self.gm_id_axis[0], self.gm_id_axis[-1])) return float(np.interp(g, self.gm_id_axis, row)) # -- public reads -------------------------------------------------------
[docs] def id_per_w(self, dtype: str, gm_id: float, l_um: float) -> float: """Drain current per µm of width (A/µm) at the operating point.""" return self._lookup(dtype, "id_w", gm_id, l_um)
[docs] def gm_gds(self, dtype: str, gm_id: float, l_um: float) -> float: """Intrinsic-gain ratio gm/gds (V/V).""" return self._lookup(dtype, "gm_gds", gm_id, l_um)
[docs] def ft(self, dtype: str, gm_id: float, l_um: float) -> float: """Transition frequency gm/(2π·Cgg) in Hz.""" return self._lookup(dtype, "ft", gm_id, l_um)
[docs] def vdsat(self, dtype: str, gm_id: float, l_um: float) -> float: """Saturation overdrive ``VDS,sat`` in V.""" return self._lookup(dtype, "vdsat", gm_id, l_um)
[docs] def vgs(self, dtype: str, gm_id: float, l_um: float) -> float: """Gate-source voltage magnitude in V.""" return self._lookup(dtype, "vgs", gm_id, l_um)
[docs] def max_gm_id(self, dtype: str, l_um: float) -> float: """Largest gm/Id the table represents (weak-inversion ceiling), 1/V.""" return float(self.gm_id_axis[-1])
[docs] def gm_id_from_idw(self, dtype: str, id_w: float, l_um: float) -> float: """Recover gm/Id from a current density ``id_w`` (A/µm) at length ``l``. Inverse of :meth:`id_per_w`: ``id_w`` decreases monotonically with gm/Id, so we invert the per-length curve. Used to read back the operating point from a solved ``(W, L, Id)`` for accurate metric evaluation. """ curve = self._row(dtype, "id_w", l_um) # decreasing in gm/Id xs = curve[::-1] # ascending current density ys = self.gm_id_axis[::-1] return float(np.interp(id_w, xs, ys))