Source code for ihp.cells.fet_transistors

"""FET transistor components for IHP PDK.

Pure GDSFactory implementations that replicate the geometry from the IHP PyCell
library (cells2/ihp_pycell). The layout algorithm follows the PyCell construction
exactly: left-to-right sequential placement of source contacts, gate poly, drain
contacts, with design rules from the CNI tech parameters.

Shared helper functions and constants used by both FET and RF transistor modules
are defined here.
"""

import math

import gdsfactory as gf
from gdsfactory import Component
from gdsfactory.typings import LayerSpec

from ..tech import TECH


# ---------------------------------------------------------------------------
# Helper functions matching PyCell utility_functions.py
# ---------------------------------------------------------------------------
def _fix(value):
    """Floor for floats (matches PyCell fix())."""
    if isinstance(value, float):
        return int(math.floor(value))
    return value


def _grid_fix(x: float) -> float:
    """Snap to manufacturing grid (matches PyCell GridFix/tog/Snap)."""
    return _fix(x * (1.0 / TECH.grid) + TECH.epsilon) * TECH.grid


def _add_rect(
    c: Component, layer: LayerSpec, x1: float, y1: float, x2: float, y2: float
):
    """Add a rectangle directly as a polygon (no sub-cell hierarchy).

    Using add_polygon avoids sub-cell + transform indirection that can
    introduce 1-dbu rounding mismatches during hierarchy flattening.
    """
    if x1 > x2:
        x1, x2 = x2, x1
    if y1 > y2:
        y1, y2 = y2, y1
    if x2 - x1 <= 0 or y2 - y1 <= 0:
        return
    c.add_polygon([(x1, y1), (x2, y1), (x2, y2), (x1, y2)], layer=layer)


def _place_contacts(
    c: Component,
    layer_cont: LayerSpec,
    xl: float,
    yl: float,
    xh: float,
    yh: float,
    ox: float,
    oy: float,
    ws: float,
    ds: float,
):
    """Place contact array matching PyCell contactArray() from geometry.py.

    Args:
        c: Component to add contacts to.
        layer_cont: Contact layer.
        xl, yl: Lower-left of bounding box.
        xh, yh: Upper-right of bounding box.
        ox: X-direction enclosure (0 for transistor S/D contacts).
        oy: Y-direction enclosure (cont_Activ_overRec for transistor S/D).
        ws: Contact size.
        ds: Contact spacing.
    """
    w = xh - xl
    h = yh - yl

    nx = _fix((w - ox * 2 + ds) / (ws + ds) + TECH.epsilon)
    if nx <= 0:
        return

    if nx == 1:
        dsx = 0.0
    else:
        dsx = (w - ox * 2 - ws * nx) / (nx - 1)

    ny = _fix((h - oy * 2 + ds) / (ws + ds) + TECH.epsilon)
    if ny <= 0:
        return

    if ny == 1:
        dsy = 0.0
    else:
        dsy = (h - oy * 2 - ws * ny) / (ny - 1)

    if nx == 1:
        x_start = (w - ws) / 2
    else:
        x_start = ox

    for _i in range(int(nx)):
        if ny == 1:
            y = (h - ws) / 2
        else:
            y = oy

        for _j in range(int(ny)):
            cx = _grid_fix(xl + x_start)
            cy = _grid_fix(yl + y)
            _add_rect(
                c,
                layer_cont,
                cx,
                cy,
                _grid_fix(xl + x_start + ws),
                _grid_fix(yl + y + ws),
            )
            y += ws + dsy

        x_start += ws + dsx


def _even_dbu(w):
    """Round width to even multiples of dbu (0.002 um) per kfactory."""
    dbu_w = round(w / 0.001)
    return (dbu_w + dbu_w % 2) * 0.001


# ---------------------------------------------------------------------------
# Core MOS layout engine
# ---------------------------------------------------------------------------
def _mos_core(
    width: float,
    length: float,
    nf: int,
    is_pmos: bool = False,
    is_hv: bool = False,
    layer_gatpoly: LayerSpec = "GatPolydrawing",
    layer_activ: LayerSpec = "Activdrawing",
    layer_cont: LayerSpec = "Contdrawing",
    layer_metal1: LayerSpec = "Metal1drawing",
    layer_psd: LayerSpec = "pSDdrawing",
    layer_nwell: LayerSpec = "NWelldrawing",
    layer_thickgateox: LayerSpec = "ThickGateOxdrawing",
    layer_heattrans: LayerSpec = "HeatTransdrawing",
    layer_substrate: LayerSpec = "Substratedrawing",
    layer_metal1_pin: LayerSpec = "Metal1pin",
    layer_gatpoly_pin: LayerSpec = "GatPolypin",
) -> Component:
    """Core MOS transistor layout matching IHP PyCell geometry.

    Constructs layout left-to-right: source contacts -> gate poly -> drain contacts.
    Exactly replicates nmos_code.py / pmos_code.py / nmosHV_code.py / pmosHV_code.py.
    """
    c = Component()

    # Tech params
    epsilon = TECH.epsilon
    endcap = TECH.m1_endcap
    cont_size = TECH.cont_size
    cont_dist = TECH.cont_spacing
    cont_Activ_overRec = TECH.cont_enc_active
    cont_metall_over = TECH.m1_over
    gatpoly_Activ_over = TECH.gatpoly_activ_over
    gatpoly_cont_dist = TECH.cont_gate_dist
    smallw_gatpoly_cont_dist = TECH.cont_enc_active + TECH.gat_d
    contActMin = 2 * TECH.cont_enc_active + TECH.cont_size

    # PMOS-specific params
    if is_pmos:
        psd_pActiv_over = TECH.psd_activ_over
        psd_PFET_over = TECH.psd_gate_over_hv if is_hv else TECH.psd_gate_over_lv
        nwell_pActiv_over = TECH.nw_activ_over_hv if is_hv else TECH.nw_activ_over_lv

    # HV params
    thGateOxGat = TECH.tgo_gatpoly
    thGateOxAct = TECH.tgo_activ

    # Endcap adjustment
    if endcap < cont_metall_over:
        endcap = cont_metall_over

    # Process dimensions
    ng = _fix(nf + epsilon)
    w = width / ng
    w = _grid_fix(w)
    gate_length = _grid_fix(length)

    # Narrow-width gate-contact spacing adjustment
    if w < contActMin - epsilon:
        gatpoly_cont_dist = smallw_gatpoly_cont_dist

    xdiff_beg = 0.0
    ydiff_beg = 0.0
    ydiff_end = w

    diffoffset = 0.0
    if w < contActMin:
        diffoffset = (contActMin - w) / 2
        diffoffset = _grid_fix(diffoffset)

    # Number of contacts (differs between nmos and pmos)
    distc = cont_size + cont_dist
    if is_pmos:
        # pmos formula: subtracts 2*endcap from lcon
        lcon = w - 2 * cont_Activ_overRec
        ncont = _fix((lcon + cont_dist - 2 * endcap) / distc + epsilon)
    else:
        # nmos formula
        ncont = _fix(
            (w - 2 * cont_Activ_overRec + cont_dist) / (cont_size + cont_dist) + epsilon
        )

    if ncont == 0:
        ncont = 1

    diff_cont_offset = _grid_fix(
        (w - 2 * cont_Activ_overRec - ncont * cont_size - (ncont - 1) * cont_dist) / 2
    )

    # -----------------------------------------------------------------------
    # Source contact column (first S/D region)
    # -----------------------------------------------------------------------
    xcont_beg = xdiff_beg + cont_Activ_overRec
    ycont_beg = ydiff_beg + cont_Activ_overRec
    ycont_cnt = ycont_beg + diffoffset + diff_cont_offset
    xcont_end = xcont_beg + cont_size

    # Metal1 Y extents (computed once, reused for all S/D columns)
    yMet1 = ycont_cnt - endcap
    yMet2 = ycont_cnt + cont_size + (ncont - 1) * distc + endcap
    yMet1 = min(yMet1, ydiff_beg + diffoffset)
    yMet2 = max(yMet2, ydiff_end + diffoffset)

    # Source Metal1
    _add_rect(
        c,
        layer_metal1,
        xcont_beg - cont_metall_over,
        yMet1,
        xcont_end + cont_metall_over,
        yMet2,
    )

    # Source contacts
    _place_contacts(
        c,
        layer_cont,
        xcont_beg,
        ydiff_beg,
        xcont_end,
        ydiff_end + diffoffset * 2,
        0,
        cont_Activ_overRec,
        cont_size,
        cont_dist,
    )

    # Pin sublayer for pin markers and ports.
    pin_layer_m1 = layer_metal1_pin
    pin_layer_poly = layer_gatpoly_pin

    # Source pin marker
    _add_rect(
        c,
        pin_layer_m1,
        xcont_beg - cont_metall_over,
        yMet1,
        xcont_end + cont_metall_over,
        yMet2,
    )

    # Save source port location
    src_x = (xcont_beg - cont_metall_over + xcont_end + cont_metall_over) / 2
    src_y = (yMet1 + yMet2) / 2
    port_height = yMet2 - yMet1

    # Source diffusion (Activ)
    _add_rect(
        c,
        layer_activ,
        xcont_beg - cont_Activ_overRec,
        ycont_beg - cont_Activ_overRec,
        xcont_end + cont_Activ_overRec,
        ycont_beg + cont_size + cont_Activ_overRec,
    )

    # -----------------------------------------------------------------------
    # Gate fingers loop
    # -----------------------------------------------------------------------
    gate_x = gate_y = drain_x = drain_y = gate_height = 0.0
    for i in range(1, ng + 1):
        # Poly gate
        xpoly_beg = xcont_end + gatpoly_cont_dist
        ypoly_beg = ydiff_beg - gatpoly_Activ_over
        xpoly_end = xpoly_beg + gate_length
        ypoly_end = ydiff_end + gatpoly_Activ_over

        _add_rect(
            c,
            layer_gatpoly,
            xpoly_beg,
            ypoly_beg + diffoffset,
            xpoly_end,
            ypoly_end + diffoffset,
        )

        # HeatTrans layer (thermal marker)
        _add_rect(
            c,
            layer_heattrans,
            xpoly_beg,
            ypoly_beg + diffoffset,
            xpoly_end,
            ypoly_end + diffoffset,
        )

        # Gate pin (first finger only, matching onep(i) check)
        if i == 1:
            _add_rect(
                c,
                pin_layer_poly,
                xpoly_beg,
                ypoly_beg + diffoffset,
                xpoly_end,
                ypoly_end + diffoffset,
            )
            gate_x = (xpoly_beg + xpoly_end) / 2
            gate_y = (ypoly_beg + ypoly_end) / 2 + diffoffset
            gate_height = ypoly_end - ypoly_beg

        # Drain/next-source contact column
        xcont_beg = xpoly_end + gatpoly_cont_dist
        ycont_beg = ydiff_beg + cont_Activ_overRec
        ycont_cnt = ycont_beg + diffoffset + diff_cont_offset
        xcont_end = xcont_beg + cont_size

        # Metal1 for this S/D column
        _add_rect(
            c,
            layer_metal1,
            xcont_beg - cont_metall_over,
            yMet1,
            xcont_end + cont_metall_over,
            yMet2,
        )

        # Contacts for this S/D column
        _place_contacts(
            c,
            layer_cont,
            xcont_beg,
            ydiff_beg,
            xcont_end,
            ydiff_end + diffoffset * 2,
            0,
            cont_Activ_overRec,
            cont_size,
            cont_dist,
        )

        # Drain pin (first finger only)
        if i == 1:
            _add_rect(
                c,
                pin_layer_m1,
                xcont_beg - cont_metall_over,
                yMet1,
                xcont_end + cont_metall_over,
                yMet2,
            )
            drain_x = (xcont_beg - cont_metall_over + xcont_end + cont_metall_over) / 2
            drain_y = src_y

        # Drain/source diffusion (Activ)
        _add_rect(
            c,
            layer_activ,
            xcont_beg - cont_Activ_overRec,
            ycont_beg - cont_Activ_overRec,
            xcont_end + cont_Activ_overRec,
            ycont_beg + cont_size + cont_Activ_overRec,
        )

    # -----------------------------------------------------------------------
    # Spanning diffusion rectangle
    # -----------------------------------------------------------------------
    xdiff_end = xcont_end + cont_Activ_overRec
    _add_rect(
        c,
        layer_activ,
        xdiff_beg,
        ydiff_beg + diffoffset,
        xdiff_end,
        ydiff_end + diffoffset,
    )

    # -----------------------------------------------------------------------
    # PMOS: pSD and NWell layers
    # -----------------------------------------------------------------------
    if is_pmos:
        # pSD layer
        _add_rect(
            c,
            layer_psd,
            xdiff_beg - psd_pActiv_over,
            ypoly_beg - psd_PFET_over + gatpoly_Activ_over + diffoffset,
            xdiff_end + psd_pActiv_over,
            ypoly_end + psd_PFET_over - gatpoly_Activ_over + diffoffset,
        )

        # NWell layer with minimum-width offset
        # PyCell uses self.grid = tech.getGridResolution() which is 0.0 for SG13
        _grid_res = 0.0  # tech.getGridResolution()
        nwell_offset = max(0, _grid_fix((contActMin - w) / 2 + 0.5 * _grid_res))
        _add_rect(
            c,
            layer_nwell,
            xdiff_beg - nwell_pActiv_over,
            ydiff_beg - nwell_pActiv_over + diffoffset - nwell_offset,
            xdiff_end + nwell_pActiv_over,
            ydiff_end + nwell_pActiv_over + diffoffset + nwell_offset,
        )

    # -----------------------------------------------------------------------
    # B-Pin on Substrate (pmos only)
    # -----------------------------------------------------------------------
    if is_pmos:
        _add_rect(
            c,
            layer_substrate,
            xcont_beg - cont_Activ_overRec,
            ycont_beg - cont_Activ_overRec,
            xcont_end + cont_Activ_overRec,
            ycont_beg + cont_size + cont_Activ_overRec,
        )

    # -----------------------------------------------------------------------
    # HV: ThickGateOx layer
    # -----------------------------------------------------------------------
    if is_hv:
        if is_pmos:
            # pmosHV: check if NWell is bigger than standard TGO enclosure
            x1 = xdiff_beg - thGateOxAct
            x2 = xdiff_end + thGateOxAct
            y1 = ydiff_beg - gatpoly_Activ_over - thGateOxGat
            y2 = ydiff_end + gatpoly_Activ_over + thGateOxGat

            nwell_offset_tgo = max(0, _grid_fix((contActMin - w) / 2 + 0.5 * _grid_res))
            if nwell_pActiv_over > thGateOxAct:
                x1 = xdiff_beg - nwell_pActiv_over
                x2 = xdiff_end + nwell_pActiv_over
            if (nwell_pActiv_over + diffoffset - nwell_offset_tgo) > (
                gatpoly_Activ_over - thGateOxGat
            ):
                y1 = ydiff_beg - nwell_pActiv_over + diffoffset - nwell_offset_tgo
                y2 = ydiff_end + nwell_pActiv_over + diffoffset + nwell_offset_tgo

            _add_rect(c, layer_thickgateox, x1, y1, x2, y2)
        else:
            # nmosHV: standard TGO enclosure
            _add_rect(
                c,
                layer_thickgateox,
                xdiff_beg - thGateOxAct,
                ydiff_beg - gatpoly_Activ_over - thGateOxGat,
                xdiff_end + thGateOxAct,
                ydiff_end + gatpoly_Activ_over + thGateOxGat,
            )

    # -----------------------------------------------------------------------
    # GDSFactory ports for netlisting (S, D, G)
    # Port widths must be even multiples of dbu (0.002 um) per kfactory.
    # -----------------------------------------------------------------------
    c.add_port(
        name="S",
        center=(src_x, src_y),
        width=_even_dbu(port_height),
        orientation=180,
        layer=layer_metal1_pin,
        port_type="electrical",
    )
    c.add_port(
        name="D",
        center=(drain_x, drain_y),
        width=_even_dbu(port_height),
        orientation=0,
        layer=layer_metal1_pin,
        port_type="electrical",
    )
    c.add_port(
        name="G",
        center=(gate_x, gate_y),
        width=_even_dbu(gate_height),
        orientation=270,
        layer=layer_gatpoly_pin,
        port_type="electrical",
    )

    return c


# ---------------------------------------------------------------------------
# Public cell functions
# ---------------------------------------------------------------------------
[docs] @gf.cell def nmos( width: float = 0.15, length: float = 0.13, nf: int = 1, m: int = 1, model: str = "sg13_lv_nmos", ) -> Component: """Create an NMOS transistor. Args: width: Total width of the transistor in micrometers. length: Gate length in micrometers. nf: Number of fingers. m: Multiplier (number of parallel devices). model: Device model name. Returns: Component with NMOS transistor layout. Raises: ValueError: If width, length, or nf is outside allowed range. """ if width < TECH.nmos_min_width or width > TECH.nmos_max_width: raise ValueError( f"nmos width={width} out of range [{TECH.nmos_min_width}, {TECH.nmos_max_width}]" ) if length < TECH.nmos_min_length or length > TECH.nmos_max_length: raise ValueError( f"nmos length={length} out of range [{TECH.nmos_min_length}, {TECH.nmos_max_length}]" ) if nf < 1 or nf > TECH.nmos_max_nf: raise ValueError(f"nmos nf={nf} out of range [1, {TECH.nmos_max_nf}]") c = _mos_core(width, length, nf, is_pmos=False, is_hv=False) # VLSIR simulation metadata c.info["vlsir"] = { "model": "sg13_lv_nmos", "spice_type": "SUBCKT", "spice_lib": "sg13g2_moslv_mod.lib", "port_order": ["d", "g", "s", "b"], "port_map": {"D": "d", "G": "g", "S": "s"}, "params": { "w": width * 1e-6, "l": length * 1e-6, "ng": nf, "m": m, }, } return c
[docs] @gf.cell def pmos( width: float = 0.15, length: float = 0.13, nf: int = 1, m: int = 1, model: str = "sg13_lv_pmos", ) -> Component: """Create a PMOS transistor. Args: width: Total width of the transistor in micrometers. length: Gate length in micrometers. nf: Number of fingers. m: Multiplier (number of parallel devices). model: Device model name. Returns: Component with PMOS transistor layout. Raises: ValueError: If width, length, or nf is outside allowed range. """ if width < TECH.pmos_min_width or width > TECH.pmos_max_width: raise ValueError( f"pmos width={width} out of range [{TECH.pmos_min_width}, {TECH.pmos_max_width}]" ) if length < TECH.pmos_min_length or length > TECH.pmos_max_length: raise ValueError( f"pmos length={length} out of range [{TECH.pmos_min_length}, {TECH.pmos_max_length}]" ) if nf < 1 or nf > TECH.pmos_max_nf: raise ValueError(f"pmos nf={nf} out of range [1, {TECH.pmos_max_nf}]") c = _mos_core(width, length, nf, is_pmos=True, is_hv=False) # VLSIR simulation metadata c.info["vlsir"] = { "model": "sg13_lv_pmos", "spice_type": "SUBCKT", "spice_lib": "sg13g2_moslv_mod.lib", "port_order": ["d", "g", "s", "b"], "port_map": {"D": "d", "G": "g", "S": "s"}, "params": { "w": width * 1e-6, "l": length * 1e-6, "ng": nf, "m": m, }, } return c
[docs] @gf.cell def nmos_hv( width: float = 0.60, length: float = 0.45, nf: int = 1, m: int = 1, model: str = "sg13_hv_nmos", ) -> Component: """Create a high-voltage NMOS transistor. Args: width: Total width of the transistor in micrometers. length: Gate length in micrometers. nf: Number of fingers. m: Multiplier (number of parallel devices). model: Device model name. Returns: Component with HV NMOS transistor layout. Raises: ValueError: If width, length, or nf is outside allowed range. """ if width < TECH.nmos_hv_min_width or width > TECH.nmos_hv_max_width: raise ValueError( f"nmos_hv width={width} out of range [{TECH.nmos_hv_min_width}, {TECH.nmos_hv_max_width}]" ) if length < TECH.nmos_hv_min_length or length > TECH.nmos_hv_max_length: raise ValueError( f"nmos_hv length={length} out of range [{TECH.nmos_hv_min_length}, {TECH.nmos_hv_max_length}]" ) if nf < 1 or nf > TECH.nmos_hv_max_nf: raise ValueError(f"nmos_hv nf={nf} out of range [1, {TECH.nmos_hv_max_nf}]") c = _mos_core(width, length, nf, is_pmos=False, is_hv=True) # VLSIR simulation metadata c.info["vlsir"] = { "model": "sg13_hv_nmos", "spice_type": "SUBCKT", "spice_lib": "sg13g2_moshv_mod.lib", "port_order": ["d", "g", "s", "b"], "port_map": {"D": "d", "G": "g", "S": "s"}, "params": { "w": width * 1e-6, "l": length * 1e-6, "ng": nf, "m": m, }, } return c
[docs] @gf.cell def pmos_hv( width: float = 0.30, length: float = 0.40, nf: int = 1, m: int = 1, model: str = "sg13_hv_pmos", ) -> Component: """Create a high-voltage PMOS transistor. Args: width: Total width of the transistor in micrometers. length: Gate length in micrometers. nf: Number of fingers. m: Multiplier (number of parallel devices). model: Device model name. Returns: Component with HV PMOS transistor layout. Raises: ValueError: If width, length, or nf is outside allowed range. """ if width < TECH.pmos_hv_min_width or width > TECH.pmos_hv_max_width: raise ValueError( f"pmos_hv width={width} out of range [{TECH.pmos_hv_min_width}, {TECH.pmos_hv_max_width}]" ) if length < TECH.pmos_hv_min_length or length > TECH.pmos_hv_max_length: raise ValueError( f"pmos_hv length={length} out of range [{TECH.pmos_hv_min_length}, {TECH.pmos_hv_max_length}]" ) if nf < 1 or nf > TECH.pmos_hv_max_nf: raise ValueError(f"pmos_hv nf={nf} out of range [1, {TECH.pmos_hv_max_nf}]") c = _mos_core(width, length, nf, is_pmos=True, is_hv=True) # VLSIR simulation metadata c.info["vlsir"] = { "model": "sg13_hv_pmos", "spice_type": "SUBCKT", "spice_lib": "sg13g2_moshv_mod.lib", "port_order": ["d", "g", "s", "b"], "port_map": {"D": "d", "G": "g", "S": "s"}, "params": { "w": width * 1e-6, "l": length * 1e-6, "ng": nf, "m": m, }, } return c
if __name__ == "__main__": from gdsfactory.difftest import xor from ihp import PDK from ihp import cells2 as pycell PDK.activate() c0 = pycell.nmos() # PyCell reference c1 = nmos() # Pure GDSFactory c = xor(c0, c1) c.show()