Source code for sky130.pcells.mosfets

"""Magic-parity MOSFET generators for sky130.

Provides nfet_01v8 and pfet_01v8 parametric cells that match the geometry
produced by Magic's sky130 device generators, including multi-finger support,
S/D sharing, guard rings, and deep N-well options.

All geometry is centered at the origin to match Magic's output coordinate system.
"""

import gdsfactory as gf

from sky130.layers import LAYER


def _snap(val: float, grid: float = 0.005) -> float:
    """Snap a value to the nearest grid point (default 5nm)."""
    return round(val / grid) * grid


def _rect(c: gf.Component, layer, x0: float, y0: float, x1: float, y1: float):
    """Add a rectangle defined by two corners (x0,y0) to (x1,y1)."""
    x0, y0, x1, y1 = _snap(x0), _snap(y0), _snap(x1), _snap(y1)
    c.add_polygon(
        [(x0, y0), (x1, y0), (x1, y1), (x0, y1)],
        layer=layer,
    )


def _mosfet_core(
    c: gf.Component,
    gate_width: float,
    gate_length: float,
    nf: int,
    is_pmos: bool,
    suppress_nwell: bool = False,
) -> dict:
    """Build the core MOSFET geometry matching Magic's mos_device output.

    All geometry is centered at origin (0, 0).

    Returns a dict of key coordinates for port placement and enclosure calculations.
    """
    # ---- Magic design-rule constants ----
    contact_size = 0.17
    diff_surround = 0.06  # Diffusion enclosure of licon contact
    poly_surround = 0.08  # Poly enclosure of licon contact (on poly)
    gate_to_diffcont = 0.145  # Gate edge to diff contact center (edge contacts)
    gate_to_polycont_n = 0.275  # Gate edge to poly contact center (NFET)
    gate_to_polycont_p = 0.32  # Gate edge to poly contact center (PFET)
    diff_extension = 0.29  # Diffusion extension beyond gate (nf=1 edge)
    end_cap = 0.13  # Poly extension beyond diffusion (non-contact side)
    implant_enc = 0.125  # Implant enclosure beyond diffusion
    nwell_enc_x = 0.18  # Nwell enclosure of diff in x (PFET)
    npc_ext = 0.02  # NPC extension beyond poly pad
    met1_surround = 0.03  # Met1 surround of mcon contact
    li1_ext_y = 0.02  # Li1 extension beyond diffusion edge for S/D

    gate_to_polycont = gate_to_polycont_p if is_pmos else gate_to_polycont_n

    # ---- Derived dimensions ----
    W = gate_width
    L = gate_length
    hw = W / 2.0  # half-width (y direction)
    hl = L / 2.0  # half-length (x direction)

    # Poly contact pad dimensions
    pc_pad_size = contact_size + 2 * poly_surround  # 0.33

    # Gate extension on the contact side: poly extends just to the pad base
    gate_ext_contact = gate_to_polycont - (contact_size / 2 + poly_surround)
    # Gate extension on non-contact side (standard end_cap)
    gate_ext_nocont = end_cap

    # ---- Finger pitch and contact positions ----
    # For shared contacts between gates (nf > 1), the gate-to-contact spacing
    # depends on L: when L < contact_size, poly_surround clearance requires
    # wider spacing (0.165 vs 0.145).  A minimum pitch of 0.48 is enforced
    # so intermediate L values (e.g. L=0.18) maintain adequate clearance.
    if L < contact_size:
        shared_gate_to_diffcont = max(
            gate_to_diffcont, contact_size / 2 + poly_surround
        )
    else:
        shared_gate_to_diffcont = gate_to_diffcont

    if nf == 1:
        # Single finger: contacts on both sides at gate_to_diffcont from gate edge
        finger_pitch = 0.0  # not applicable
        # S/D contact centers
        sd_centers_x = [-(hl + gate_to_diffcont), hl + gate_to_diffcont]
        gate_centers_x = [0.0]
        # Diffusion half-width in x
        diff_half_x = hl + diff_extension
    else:
        # Multi-finger: regular grid with shared contacts
        finger_pitch = max(L + 2 * shared_gate_to_diffcont, 0.48)
        # Gate centers: symmetric about origin
        gate_centers_x = [
            _snap(-((nf - 1) / 2.0) * finger_pitch + i * finger_pitch)
            for i in range(nf)
        ]
        # S/D contact centers: nf+1 contacts
        sd_centers_x = [
            _snap(-(nf / 2.0) * finger_pitch + i * finger_pitch) for i in range(nf + 1)
        ]
        # Diffusion extends diff_surround + contact_size/2 beyond outermost contact
        diff_half_x = abs(sd_centers_x[-1]) + contact_size / 2 + diff_surround

    # ---- 1. Diffusion rectangle ----
    _rect(c, LAYER.diffdrawing, -diff_half_x, -hw, diff_half_x, hw)

    # ---- 2. Poly gates and contact pads ----
    # For nf=1: both top and bottom get contacts
    # For nf>1: gates alternate — even-indexed on bottom, odd-indexed on top
    #           (gate 0 has contact bottom, gate 1 has contact top, etc.)

    # Determine which side each gate gets a poly contact
    # "top" means positive y, "bottom" means negative y
    # When L >= 2*end_cap (0.26) but < pc_pad_size (0.33), the gate is long
    # enough that the poly body extends symmetrically to gate_ext_contact on
    # both sides, and the pad alternation reverses (even=top, odd=bottom).
    reversed_alternation = L >= 2 * end_cap and L < pc_pad_size and nf > 1
    for gi, gx in enumerate(gate_centers_x):
        if nf == 1 or L >= pc_pad_size or reversed_alternation:
            # Both sides get contacts when:
            # - nf=1 (single finger)
            # - gate is wide enough to serve as its own contact pad (L >= pc_pad_size)
            # - reversed alternation range (L >= 2*end_cap): gate body is symmetric
            #   and both sides have poly contact pads
            contact_sides = ["top", "bottom"]
        else:
            # Standard alternation (L < 2*end_cap): even gates get bottom, odd get top
            if gi % 2 == 0:
                contact_sides = ["bottom"]
            else:
                contact_sides = ["top"]

        # Poly gate rectangle
        if L >= pc_pad_size:
            # Gate is wide enough to serve as its own contact pad.
            # Poly extends symmetrically to full pad range on BOTH sides,
            # regardless of which side has the contact.
            poly_ext = hw + gate_to_polycont + contact_size / 2 + poly_surround
            _rect(c, LAYER.polydrawing, gx - hl, -poly_ext, gx + hl, poly_ext)
        else:
            # Gate is narrower than contact pad — need separate pad rectangles
            if nf == 1:
                # Gate rect connects top and bottom pads
                poly_top_gate = hw + gate_ext_contact
                poly_bot_gate = -(hw + gate_ext_contact)
                _rect(
                    c, LAYER.polydrawing, gx - hl, poly_bot_gate, gx + hl, poly_top_gate
                )

                # Top poly contact pad
                pad_half = pc_pad_size / 2.0
                pad_cy_top = hw + gate_to_polycont
                _rect(
                    c,
                    LAYER.polydrawing,
                    gx - pad_half,
                    pad_cy_top - pad_half,
                    gx + pad_half,
                    pad_cy_top + pad_half,
                )
                # Bottom poly contact pad
                pad_cy_bot = -(hw + gate_to_polycont)
                _rect(
                    c,
                    LAYER.polydrawing,
                    gx - pad_half,
                    pad_cy_bot - pad_half,
                    gx + pad_half,
                    pad_cy_bot + pad_half,
                )
            elif reversed_alternation:
                # Multi-finger with intermediate gate width (L >= 2*end_cap):
                # Gate body extends to gate_ext_contact on BOTH sides (symmetric).
                # Pad rectangles on BOTH sides (same as nf=1).
                pad_half = pc_pad_size / 2.0
                poly_ext_y = hw + gate_ext_contact
                _rect(
                    c,
                    LAYER.polydrawing,
                    gx - hl,
                    -poly_ext_y,
                    gx + hl,
                    poly_ext_y,
                )
                # Top poly contact pad
                pad_cy_top = hw + gate_to_polycont
                _rect(
                    c,
                    LAYER.polydrawing,
                    gx - pad_half,
                    pad_cy_top - pad_half,
                    gx + pad_half,
                    pad_cy_top + pad_half,
                )
                # Bottom poly contact pad
                pad_cy_bot = -(hw + gate_to_polycont)
                _rect(
                    c,
                    LAYER.polydrawing,
                    gx - pad_half,
                    pad_cy_bot - pad_half,
                    gx + pad_half,
                    pad_cy_bot + pad_half,
                )
            else:
                # Multi-finger with narrow gate (L < contact_size)
                pad_half = pc_pad_size / 2.0
                if "top" in contact_sides:
                    # Gate extends up to pad base, down to end_cap
                    poly_top_gate = hw + gate_ext_contact
                    poly_bot_gate = -(hw + gate_ext_nocont)
                    _rect(
                        c,
                        LAYER.polydrawing,
                        gx - hl,
                        poly_bot_gate,
                        gx + hl,
                        poly_top_gate,
                    )
                    # Top contact pad
                    pad_cy = hw + gate_to_polycont
                    _rect(
                        c,
                        LAYER.polydrawing,
                        gx - pad_half,
                        pad_cy - pad_half,
                        gx + pad_half,
                        pad_cy + pad_half,
                    )
                else:
                    # Gate extends down to pad base, up to end_cap
                    poly_top_gate = hw + gate_ext_nocont
                    poly_bot_gate = -(hw + gate_ext_contact)
                    _rect(
                        c,
                        LAYER.polydrawing,
                        gx - hl,
                        poly_bot_gate,
                        gx + hl,
                        poly_top_gate,
                    )
                    # Bottom contact pad
                    pad_cy = -(hw + gate_to_polycont)
                    _rect(
                        c,
                        LAYER.polydrawing,
                        gx - pad_half,
                        pad_cy - pad_half,
                        gx + pad_half,
                        pad_cy + pad_half,
                    )

        # ---- Licon contacts on poly pads ----
        # For wide gates (L > contact pad size), use a horizontal contact array
        cpl = gate_length - 2 * poly_surround  # poly contact width (coverage area)
        if cpl < contact_size:
            cpl = contact_size  # minimum one contact

        for side in contact_sides:
            if side == "top":
                pad_cy = hw + gate_to_polycont
            else:
                pad_cy = -(hw + gate_to_polycont)

            licon_half = contact_size / 2.0

            # Determine contact array dimensions for poly contacts
            # For narrow gates: single contact at gate center
            # For wide gates: array of contacts spanning gate width
            num_poly_contacts_x = 1
            if cpl > contact_size:
                from math import floor

                num_poly_contacts_x = 1 + floor(
                    (cpl - contact_size) / (contact_size + contact_size)
                )
            num_poly_contacts_x = max(1, num_poly_contacts_x)

            # Place contact array horizontally
            # Licon and mcon use different pitches for wide gates
            licon_poly_pitch = contact_size + contact_size  # 0.34
            mcon_poly_pitch = contact_size + 0.19  # 0.36
            if num_poly_contacts_x == 1:
                _rect(
                    c,
                    LAYER.licon1drawing,
                    gx - licon_half,
                    pad_cy - licon_half,
                    gx + licon_half,
                    pad_cy + licon_half,
                )
                _rect(
                    c,
                    LAYER.mcondrawing,
                    gx - licon_half,
                    pad_cy - licon_half,
                    gx + licon_half,
                    pad_cy + licon_half,
                )
            else:
                # Licon array
                licon_array_w = (
                    num_poly_contacts_x - 1
                ) * licon_poly_pitch + contact_size
                licon_start_x = gx - licon_array_w / 2.0
                for ci in range(num_poly_contacts_x):
                    cx = licon_start_x + ci * licon_poly_pitch
                    _rect(
                        c,
                        LAYER.licon1drawing,
                        cx,
                        pad_cy - licon_half,
                        cx + contact_size,
                        pad_cy + licon_half,
                    )
                # Mcon array (different pitch)
                mcon_array_w = (
                    num_poly_contacts_x - 1
                ) * mcon_poly_pitch + contact_size
                mcon_start_x = gx - mcon_array_w / 2.0
                for ci in range(num_poly_contacts_x):
                    cx = mcon_start_x + ci * mcon_poly_pitch
                    _rect(
                        c,
                        LAYER.mcondrawing,
                        cx,
                        pad_cy - licon_half,
                        cx + contact_size,
                        pad_cy + licon_half,
                    )

            # Li1 over poly contact: covers the larger of pad area or gate width
            li_half_x = max(pc_pad_size / 2.0, hl)
            _rect(
                c,
                LAYER.li1drawing,
                gx - li_half_x,
                pad_cy - licon_half,
                gx + li_half_x,
                pad_cy + licon_half,
            )
            # Met1 over poly contact
            pad_half_x = max(pc_pad_size / 2.0, hl)
            m1_half_x = pad_half_x - npc_ext
            m1_half_y = licon_half + met1_surround
            _rect(
                c,
                LAYER.met1drawing,
                gx - m1_half_x,
                pad_cy - m1_half_y,
                gx + m1_half_x,
                pad_cy + m1_half_y,
            )

            # NPC over poly contact region
            # For reversed alternation: defer NPC to after the loop (spans all gates)
            # For normal cases: per-gate NPC
            if not reversed_alternation:
                if num_poly_contacts_x > 1:
                    licon_array_w = (
                        num_poly_contacts_x - 1
                    ) * licon_poly_pitch + contact_size
                    npc_half_x = licon_array_w / 2.0 + poly_surround + npc_ext
                else:
                    npc_half_x = pc_pad_size / 2.0 + npc_ext
                npc_half_y = pc_pad_size / 2.0 + npc_ext
                _rect(
                    c,
                    LAYER.npcdrawing,
                    gx - npc_half_x,
                    pad_cy - npc_half_y,
                    gx + npc_half_x,
                    pad_cy + npc_half_y,
                )

    # ---- NPC for reversed alternation: span all gates on both top and bottom ----
    if reversed_alternation:
        npc_half_x = pc_pad_size / 2.0 + npc_ext
        npc_half_y = pc_pad_size / 2.0 + npc_ext
        # NPC left edge = leftmost gate center - npc_half_x
        # NPC right edge = rightmost gate center + npc_half_x
        npc_left = gate_centers_x[0] - npc_half_x
        npc_right = gate_centers_x[-1] + npc_half_x
        # Top NPC
        pad_cy_top = hw + gate_to_polycont
        _rect(
            c,
            LAYER.npcdrawing,
            npc_left,
            pad_cy_top - npc_half_y,
            npc_right,
            pad_cy_top + npc_half_y,
        )
        # Bottom NPC
        pad_cy_bot = -(hw + gate_to_polycont)
        _rect(
            c,
            LAYER.npcdrawing,
            npc_left,
            pad_cy_bot - npc_half_y,
            npc_right,
            pad_cy_bot + npc_half_y,
        )

    # ---- 3. S/D contacts (licon, li1, mcon, met1) ----
    # Licon: size=0.17, spacing=0.17, enclosure=0.06
    licon_spacing = 0.17
    licon_pitch = contact_size + licon_spacing  # 0.34
    # Mcon: size=0.17, spacing=0.19, enclosure=(0.03x, 0.06y)
    mcon_spacing = 0.19
    mcon_pitch = contact_size + mcon_spacing  # 0.36
    mcon_enc_y = 0.06

    for _si, sx in enumerate(sd_centers_x):
        ch = contact_size / 2.0

        # -- Licon contacts --
        avail_h_licon = W - 2 * diff_surround
        n_licon = max(1, int((avail_h_licon - contact_size) / licon_pitch) + 1)
        arr_h_licon = (n_licon - 1) * licon_pitch + contact_size
        for ci in range(n_licon):
            cy = -arr_h_licon / 2.0 + ci * licon_pitch + ch
            _rect(c, LAYER.licon1drawing, sx - ch, cy - ch, sx + ch, cy + ch)

        # -- Mcon contacts (different pitch and enclosure) --
        avail_h_mcon = W - 2 * mcon_enc_y
        n_mcon = max(1, int((avail_h_mcon - contact_size) / mcon_pitch) + 1)
        arr_h_mcon = (n_mcon - 1) * mcon_pitch + contact_size
        for ci in range(n_mcon):
            cy = -arr_h_mcon / 2.0 + ci * mcon_pitch + ch
            _rect(c, LAYER.mcondrawing, sx - ch, cy - ch, sx + ch, cy + ch)

        # Li1 strip over S/D: same x range as contact, y extends to ±(hw + li1_ext_y)
        li1_y_ext = hw + li1_ext_y
        _rect(c, LAYER.li1drawing, sx - ch, -li1_y_ext, sx + ch, li1_y_ext)

        # Met1 pad over S/D
        m1_half_x = ch + met1_surround
        m1_half_y = hw  # met1 extends to diff edge
        _rect(
            c, LAYER.met1drawing, sx - m1_half_x, -m1_half_y, sx + m1_half_x, m1_half_y
        )

    # ---- 4. Implant layer (NSDM or PSDM) ----
    impl_layer = LAYER.psdmdrawing if is_pmos else LAYER.nsdmdrawing
    _rect(
        c,
        impl_layer,
        -(diff_half_x + implant_enc),
        -(hw + implant_enc),
        diff_half_x + implant_enc,
        hw + implant_enc,
    )

    # ---- 5. N-well (PFET only, skipped when guard ring will provide it) ----
    if is_pmos and not suppress_nwell:
        # Nwell extends 0.18 beyond diff in x, and encompasses poly contact region in y
        nwell_x = diff_half_x + nwell_enc_x
        # In y: extends to poly contact region top/bottom + npc_ext + some clearance
        polycont_pad_top = hw + gate_to_polycont + contact_size / 2 + poly_surround
        nwell_y = polycont_pad_top + 0.015  # empirical from reference: pad_top + 0.015

        if nf > 1 and L < pc_pad_size and not reversed_alternation:
            # Multi-finger with alternating pads: L-shaped nwell.
            # (Only for standard alternation where each gate has a pad on one side.
            #  Reversed alternation has pads on both sides, so uses simple rect.)
            nwell_step_y = _snap(hw + gate_to_polycont - 0.01)
            step_x = _snap(nwell_x - finger_pitch)

            # Determine which y-side ("bottom" = -y, "top" = +y) the even gates use.
            # Even gates are at the outermost positions for odd nf.
            if reversed_alternation:
                even_side = "top"  # even gates get top contacts
            else:
                even_side = "bottom"  # even gates get bottom contacts

            if nf % 2 == 1:
                # Odd nf: both outermost gates are even, so the even-side needs
                # full width; the other side only needs narrow (center) width.
                if even_side == "bottom":
                    # Main rect: full width, from -nwell_y to +nwell_step_y
                    _rect(
                        c, LAYER.nwelldrawing, -nwell_x, -nwell_y, nwell_x, nwell_step_y
                    )
                    # Top bump: narrow, from +nwell_step_y to +nwell_y
                    _rect(c, LAYER.nwelldrawing, -step_x, nwell_step_y, step_x, nwell_y)
                else:
                    # Main rect: full width, from -nwell_step_y to +nwell_y
                    _rect(
                        c, LAYER.nwelldrawing, -nwell_x, -nwell_step_y, nwell_x, nwell_y
                    )
                    # Bottom bump: narrow, from -nwell_y to -nwell_step_y
                    _rect(
                        c, LAYER.nwelldrawing, -step_x, -nwell_y, step_x, -nwell_step_y
                    )
            else:
                # Even nf: outermost gates are 0 (even) and nf-1 (odd).
                # Each side extends in one diagonal direction.
                _rect(
                    c,
                    LAYER.nwelldrawing,
                    -nwell_x,
                    -nwell_step_y,
                    nwell_x,
                    nwell_step_y,
                )
                if even_side == "bottom":
                    _rect(
                        c, LAYER.nwelldrawing, -nwell_x, -nwell_y, step_x, -nwell_step_y
                    )
                    _rect(
                        c, LAYER.nwelldrawing, -step_x, nwell_step_y, nwell_x, nwell_y
                    )
                else:
                    _rect(
                        c, LAYER.nwelldrawing, -step_x, -nwell_y, nwell_x, -nwell_step_y
                    )
                    _rect(
                        c, LAYER.nwelldrawing, -nwell_x, nwell_step_y, step_x, nwell_y
                    )
        else:
            _rect(c, LAYER.nwelldrawing, -nwell_x, -nwell_y, nwell_x, nwell_y)

    # ---- 6. Labels ----
    # For nf=1: D (drain) on left/negative-x, S (source) on right/positive-x
    # For nf>1: D0, S1, D2, S3, ... alternating, with G labels at poly contact centers
    if nf == 1:
        # Drain on left (negative x), Source on right (positive x)
        c.add_label(
            text="D",
            position=(sd_centers_x[0], 0.0),
            layer=LAYER.li1label,
        )
        c.add_label(
            text="S",
            position=(sd_centers_x[1], 0.0),
            layer=LAYER.li1label,
        )
        # Gate label at poly contact center (top)
        c.add_label(
            text="G",
            position=(0.0, hw + gate_to_polycont),
            layer=LAYER.li1label,
        )
    else:
        # For nf>1: each gate i gets a label Gi.
        # Each gate's LEFT S/D contact gets Di (even i) or Si (odd i).
        # For odd nf: the last gate's RIGHT S/D also gets S(nf-1).
        # For even nf: the last S/D is unlabeled (same drain net as D0).
        for gi in range(nf):
            sx = sd_centers_x[gi]
            if gi % 2 == 0:
                c.add_label(text=f"D{gi}", position=(sx, 0.0), layer=LAYER.li1label)
            else:
                c.add_label(text=f"S{gi}", position=(sx, 0.0), layer=LAYER.li1label)
        # Last S/D contact: labeled only for odd nf
        if nf % 2 == 1:
            sx = sd_centers_x[nf]
            c.add_label(text=f"S{nf - 1}", position=(sx, 0.0), layer=LAYER.li1label)

        for gi, gx in enumerate(gate_centers_x):
            if L >= pc_pad_size or reversed_alternation:
                # Wide gate or reversed alternation: contacts on both sides, label on top
                gate_label_y = hw + gate_to_polycont
            elif gi % 2 == 0:
                gate_label_y = -(hw + gate_to_polycont)
            else:
                gate_label_y = hw + gate_to_polycont
            c.add_label(
                text=f"G{gi}",
                position=(gx, gate_label_y),
                layer=LAYER.li1label,
            )

    # ---- Compute outermost gate edge (for variant implant sizing) ----
    outermost_gate_edge_x = abs(gate_centers_x[-1]) + hl

    # ---- Compute nwell extent (for pfet variants) ----
    polycont_pad_top = hw + gate_to_polycont + contact_size / 2 + poly_surround
    nwell_x = diff_half_x + nwell_enc_x
    nwell_y = polycont_pad_top + 0.015  # empirical from reference

    # ---- Return coordinate info ----
    return {
        "diff_half_x": diff_half_x,
        "hw": hw,
        "gate_to_polycont": gate_to_polycont,
        "pc_pad_size": pc_pad_size,
        "sd_centers_x": sd_centers_x,
        "gate_centers_x": gate_centers_x,
        "finger_pitch": finger_pitch,
        "implant_enc": implant_enc,
        "nwell_enc_x": nwell_enc_x,
        "npc_ext": npc_ext,
        "outermost_gate_edge_x": outermost_gate_edge_x,
        "nwell_x": nwell_x,
        "nwell_y": nwell_y,
    }


def _add_guard_ring(
    c: gf.Component,
    info: dict,
    is_pmos: bool,
    is_hvi: bool = False,
    is_nvt: bool = False,
) -> dict:
    """Add a guard ring around the device matching Magic's exact geometry.

    For standard devices: ring_width=0.17, spacing from nsdm/npc.
    For HVI devices: ring_width=0.29, larger spacing, different nwell enclosure.

    Returns a dict with guard ring extents for HVI layer placement.
    """

    hw = info["hw"]
    diff_half_x = info["diff_half_x"]
    gate_to_polycont = info["gate_to_polycont"]
    pc_pad_size = info["pc_pad_size"]
    npc_ext = info["npc_ext"]

    # Guard ring constants — differ for HVI devices
    contact_size = 0.17
    contact_pitch = 0.34  # contact_size + contact_spacing
    impl_enc = 0.125

    if is_hvi:
        ring_width = 0.29
        # HVI devices use larger spacing from device boundary
        if is_nvt:
            inner_spacing_x = 0.255
        else:
            # nfet_g5v0d10v5 uses 0.265, pfet_g5v0d10v5 uses 0.255
            inner_spacing_x = 0.265 if not is_pmos else 0.255
        inner_spacing_y = 0.36
    else:
        ring_width = 0.17
        inner_spacing_x = 0.215
        inner_spacing_y = 0.24

    # Device extents for spacing calculations
    nsdm_x = diff_half_x + info["implant_enc"]
    npc_y = hw + gate_to_polycont + pc_pad_size / 2.0 + npc_ext

    # Guard ring inner/outer edges
    gr_inner_x = _snap(nsdm_x + inner_spacing_x)
    gr_inner_y = _snap(npc_y + inner_spacing_y)
    gr_outer_x = gr_inner_x + ring_width
    gr_outer_y = gr_inner_y + ring_width

    # ---- Tap segments ----
    # Top and bottom span full width; left and right span inner_y range only
    # (corners formed by overlap of horizontal and vertical segments)
    _rect(c, LAYER.tapdrawing, -gr_outer_x, gr_inner_y, gr_outer_x, gr_outer_y)  # top
    _rect(
        c, LAYER.tapdrawing, -gr_outer_x, -gr_outer_y, gr_outer_x, -gr_inner_y
    )  # bottom
    _rect(
        c, LAYER.tapdrawing, -gr_outer_x, -gr_inner_y, -gr_inner_x, gr_inner_y
    )  # left
    _rect(c, LAYER.tapdrawing, gr_inner_x, -gr_inner_y, gr_outer_x, gr_inner_y)  # right

    # ---- Li1 on guard ring ----
    # For HVI devices, Li1 is 0.17 wide (standard) centered in the wider 0.29 tap.
    # For standard devices, Li1 matches the tap footprint.
    if is_hvi:
        li1_width = 0.17
        li1_margin = (ring_width - li1_width) / 2.0
        li1_inner_x = gr_inner_x + li1_margin
        li1_outer_x = gr_outer_x - li1_margin
        li1_inner_y = gr_inner_y + li1_margin
        li1_outer_y = gr_outer_y - li1_margin
    else:
        li1_inner_x = gr_inner_x
        li1_outer_x = gr_outer_x
        li1_inner_y = gr_inner_y
        li1_outer_y = gr_outer_y
    _rect(c, LAYER.li1drawing, -li1_outer_x, li1_inner_y, li1_outer_x, li1_outer_y)
    _rect(c, LAYER.li1drawing, -li1_outer_x, -li1_outer_y, li1_outer_x, -li1_inner_y)
    _rect(c, LAYER.li1drawing, -li1_outer_x, -li1_inner_y, -li1_inner_x, li1_inner_y)
    _rect(c, LAYER.li1drawing, li1_inner_x, -li1_inner_y, li1_outer_x, li1_inner_y)

    # ---- Implant on guard ring ----
    # PSDM for NFET body tap (P+ substrate), NSDM for PFET body tap (N+ well)
    # Implant is 4 separate rectangles matching tap corners (with overlap)
    gr_impl_layer = LAYER.nsdmdrawing if is_pmos else LAYER.psdmdrawing
    imp_x0 = -(gr_outer_x + impl_enc)
    imp_x1 = gr_outer_x + impl_enc
    imp_y0 = -(gr_outer_y + impl_enc)
    imp_y1 = gr_outer_y + impl_enc
    imp_inner_x0 = -(gr_inner_x - impl_enc)
    imp_inner_x1 = gr_inner_x - impl_enc
    imp_inner_y0 = -(gr_inner_y - impl_enc)
    imp_inner_y1 = gr_inner_y - impl_enc
    # Top horizontal
    _rect(c, gr_impl_layer, imp_x0, imp_inner_y1, imp_x1, imp_y1)
    # Bottom horizontal
    _rect(c, gr_impl_layer, imp_x0, imp_y0, imp_x1, imp_inner_y0)
    # Left vertical
    _rect(c, gr_impl_layer, imp_x0, imp_inner_y0, imp_inner_x0, imp_inner_y1)
    # Right vertical
    _rect(c, gr_impl_layer, imp_inner_x1, imp_inner_y0, imp_x1, imp_inner_y1)

    # ---- Licon contacts on guard ring ----
    # Ring center positions
    gr_cx = (gr_inner_x + gr_outer_x) / 2.0  # x center of left/right segments
    gr_cy = (gr_inner_y + gr_outer_y) / 2.0  # y center of top/bottom segments

    # For HVI devices, licon contacts use the li1 inner edge for available region,
    # not the tap inner edge (since li1 is narrower than tap in HVI).
    if is_hvi:
        contact_region_inner_x = li1_inner_x
        contact_region_inner_y = li1_inner_y
    else:
        contact_region_inner_x = gr_inner_x
        contact_region_inner_y = gr_inner_y

    # Horizontal segment contacts (top/bottom)
    non_corner_x = 2 * contact_region_inner_x
    # Contact count uses enclosure in the non-corner region.
    # Use integer nanometer arithmetic for exact boundary handling.
    horiz_enc = 0.325 if is_hvi else 0.31
    _ncx_nm = round(non_corner_x * 1000)
    _henc_nm = round(horiz_enc * 1000)
    _cs_nm = round(contact_size * 1000)
    _cp_nm = round(contact_pitch * 1000)
    n_horiz = max(1, 1 + (_ncx_nm - 2 * _henc_nm - _cs_nm) // _cp_nm)
    arr_w = (n_horiz - 1) * contact_pitch + contact_size
    ch = contact_size / 2.0
    for sign_y in [1, -1]:
        cy = sign_y * gr_cy
        for i in range(n_horiz):
            cx = -arr_w / 2.0 + i * contact_pitch + ch
            _rect(c, LAYER.licon1drawing, cx - ch, cy - ch, cx + ch, cy + ch)

    # Vertical segment contacts (left/right)
    non_corner_y = 2 * contact_region_inner_y
    # Contact count — also in integer nanometers.
    # Subtract 1 before division for strict less-than at exact boundaries
    # (e.g. PFET W=5.0 where the available height is exactly N*pitch).
    vert_enc = 0.29 if is_hvi else 0.30
    _ncy_nm = round(non_corner_y * 1000)
    _venc_nm = round(vert_enc * 1000)
    _vert_avail_nm = _ncy_nm - 2 * _venc_nm - _cs_nm
    n_vert = max(1, 1 + (_vert_avail_nm - 1) // _cp_nm)
    arr_h = (n_vert - 1) * contact_pitch + contact_size
    for sign_x in [-1, 1]:
        cx = sign_x * gr_cx
        for i in range(n_vert):
            cy = -arr_h / 2.0 + i * contact_pitch + ch
            _rect(c, LAYER.licon1drawing, cx - ch, cy - ch, cx + ch, cy + ch)

    # ---- N-well for PFET guard ring ----
    if is_pmos:
        # HVI PFET uses larger nwell enclosure (0.33 vs 0.18)
        nw_ext = 0.33 if is_hvi else 0.18
        _rect(
            c,
            LAYER.nwelldrawing,
            -(gr_outer_x + nw_ext),
            -(gr_outer_y + nw_ext),
            gr_outer_x + nw_ext,
            gr_outer_y + nw_ext,
        )

    # ---- PR boundary at ring center ----
    pr_x = (gr_inner_x + gr_outer_x) / 2.0
    pr_y = (gr_inner_y + gr_outer_y) / 2.0
    _rect(c, LAYER.prBoundaryboundary, -pr_x, -pr_y, pr_x, pr_y)

    # ---- Body label at bottom of PR boundary ----
    c.add_label(
        text="B",
        position=(0.0, -pr_y),
        layer=LAYER.li1label,
    )

    return {
        "gr_outer_x": gr_outer_x,
        "gr_outer_y": gr_outer_y,
        "gr_inner_x": gr_inner_x,
        "gr_inner_y": gr_inner_y,
    }


[docs] @gf.cell def sky130_fd_pr__nfet_01v8( gate_width: float = 0.42, gate_length: float = 0.15, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """1.8V N-channel MOSFET (sky130_fd_pr__nfet_01v8). Generates a multi-finger NMOS transistor with Magic-parity geometry, including source/drain contacts, gate contacts, implants, optional guard ring, and optional deep N-well. Args: gate_width: transistor width (um), i.e. the diffusion extent in y. gate_length: gate poly length (um) in the direction of current flow. sd_width: source/drain contact region width (um) — reserved, not used directly. nf: number of gate fingers. guard_ring: if True, add a pwell (P+ substrate) guard ring. dnwell: if True, add a deep N-well under the device. end_cap: poly extension beyond diffusion edge (um) — reserved, not used directly. mult: multiplier (currently generates one device; reserved for future use). """ c = gf.Component() info = _mosfet_core(c, gate_width, gate_length, nf, is_pmos=False) if guard_ring: _add_guard_ring(c, info, is_pmos=False) _add_ports(c, info, gate_width) return c
[docs] @gf.cell def sky130_fd_pr__pfet_01v8( gate_width: float = 0.42, gate_length: float = 0.15, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """1.8V P-channel MOSFET (sky130_fd_pr__pfet_01v8). Generates a multi-finger PMOS transistor with Magic-parity geometry, including source/drain contacts, gate contacts, N-well, implants, optional nwell guard ring, and optional deep N-well. Args: gate_width: transistor width (um), i.e. the diffusion extent in y. gate_length: gate poly length (um) in the direction of current flow. sd_width: source/drain contact region width (um) — reserved, not used directly. nf: number of gate fingers. guard_ring: if True, add an nwell (N+ tap) guard ring. dnwell: if True, add a deep N-well under the device. end_cap: poly extension beyond diffusion edge (um) — reserved, not used directly. mult: multiplier (currently generates one device; reserved for future use). """ c = gf.Component() info = _mosfet_core( c, gate_width, gate_length, nf, is_pmos=True, suppress_nwell=guard_ring ) if guard_ring: _add_guard_ring(c, info, is_pmos=True) _add_ports(c, info, gate_width) return c
def _add_ports( c: gf.Component, info: dict, gate_width: float, ) -> None: """Add standard MOSFET ports to a component.""" sd = info["sd_centers_x"] hw = info["hw"] gpc = info["gate_to_polycont"] c.add_port( name="GATE", center=(info["gate_centers_x"][0], 0.0), width=gate_width, orientation=0, layer=LAYER.polydrawing, port_type="electrical", ) c.add_port( name="SOURCE", center=(sd[-1], 0.0), width=gate_width, orientation=0, layer=LAYER.li1drawing, port_type="electrical", ) c.add_port( name="DRAIN", center=(sd[0], 0.0), width=gate_width, orientation=180, layer=LAYER.li1drawing, port_type="electrical", ) c.add_port( name="BODY", center=(0.0, -(hw + gpc)), width=gate_width, orientation=270, layer=LAYER.li1drawing, port_type="electrical", ) def _add_lvtn_or_hvtp(c, info, layer): """Add LVTN (125/44) or HVTP (78/44) implant layer. Sizing: outermost_gate_edge + 0.18 in x, hw + 0.18 in y. """ enc = 0.18 ge = info["outermost_gate_edge_x"] hw = info["hw"] _rect(c, layer, -(ge + enc), -(hw + enc), ge + enc, hw + enc) def _add_hvntm(c, info): """Add HVNTM (125/20) layer. Sizing: diff_half_x + 0.185 in x, hw + 0.185 in y. """ enc = 0.185 dhx = info["diff_half_x"] hw = info["hw"] _rect(c, LAYER.hvntmdrawing, -(dhx + enc), -(hw + enc), dhx + enc, hw + enc) def _add_hvi_nfet(c, info, guard_ring, gr_info=None): """Add HVI (75/20) layer for NFET devices. Without guard ring: diff_half_x + 0.185 in x, hw + 0.185 in y (nf>1) or hw + 0.21 in y (nf=1). With guard ring: gr_outer + 0.185 in both x and y. """ hvi_enc = 0.185 if guard_ring and gr_info is not None: hvi_x = gr_info["gr_outer_x"] + hvi_enc hvi_y = gr_info["gr_outer_y"] + hvi_enc else: dhx = info["diff_half_x"] hw = info["hw"] hvi_x = dhx + hvi_enc # Magic uses minimum HVI y-extent of 0.42 (matching nf=1 W=0.42 case), # or hw + hvi_enc for larger W where hw already exceeds the minimum. hvi_y = max(hw + hvi_enc, 0.42) _rect(c, LAYER.hvidrawing, -hvi_x, -hvi_y, hvi_x, hvi_y) def _add_hvi_pfet(c, info, guard_ring, gr_info=None): """Add HVI (75/20) layer for PFET devices. With guard ring: same as nwell extent (gr_outer + nw_ext). Without guard ring: three rectangles covering nwell + extra horizontal band. """ if guard_ring and gr_info is not None: # HVI = nwell extent = gr_outer + 0.33 nw_ext = 0.33 hvi_x = gr_info["gr_outer_x"] + nw_ext hvi_y = gr_info["gr_outer_y"] + nw_ext _rect(c, LAYER.hvidrawing, -hvi_x, -hvi_y, hvi_x, hvi_y) else: # Three rectangles forming a cross/plus shape around nwell nwell_x = info["nwell_x"] nwell_y = info["nwell_y"] hw = info["hw"] hvi_enc = 0.185 center_y = max(hw + hvi_enc, 0.42) if center_y > 0.42: # Large W: minimal protrusion beyond nwell center_x = nwell_x + (hvi_enc - 0.18) # nwell_x + 0.005 else: # Small W: wider horizontal band for HVI clearance center_x = nwell_x + 0.425 # Center horizontal band _rect(c, LAYER.hvidrawing, -center_x, -center_y, center_x, center_y) # Top fill to nwell boundary _rect(c, LAYER.hvidrawing, -nwell_x, center_y, nwell_x, nwell_y) # Bottom fill to nwell boundary _rect(c, LAYER.hvidrawing, -nwell_x, -nwell_y, nwell_x, -center_y) def _add_areaid_native(c, info): """Add areaidlvNative (81/60) marker for native NMOS devices. For nf=1: single rect covering gate_edge + 0.10 in x, hw + 0.10 in y. For nf>1: separate per-gate rects, each gate_center ± (hl + 0.10) in x. """ enc = 0.10 hw = info["hw"] gate_centers = info["gate_centers_x"] # hl is the half gate length (outermost_gate_edge minus last gate center) if len(gate_centers) == 1: hl = info["outermost_gate_edge_x"] else: hl = info["outermost_gate_edge_x"] - abs(gate_centers[-1]) for gx in gate_centers: _rect( c, LAYER.areaidlvNative, gx - hl - enc, -(hw + enc), gx + hl + enc, hw + enc ) # --------------------------------------------------------------------------- # Variant: Low-Vt NMOS 1.8V # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__nfet_01v8_lvt( gate_width: float = 0.42, gate_length: float = 0.15, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """Low-Vt 1.8V NMOS (sky130_fd_pr__nfet_01v8_lvt).""" c = gf.Component() info = _mosfet_core(c, gate_width, gate_length, nf, is_pmos=False) # LVTN implant: gate_edge + 0.18 enclosure _add_lvtn_or_hvtp(c, info, LAYER.lvtndrawing) if guard_ring: _add_guard_ring(c, info, is_pmos=False) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: Low-Vt PMOS 1.8V # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__pfet_01v8_lvt( gate_width: float = 0.42, gate_length: float = 0.35, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """Low-Vt 1.8V PMOS (sky130_fd_pr__pfet_01v8_lvt).""" c = gf.Component() info = _mosfet_core( c, gate_width, gate_length, nf, is_pmos=True, suppress_nwell=guard_ring ) # LVTN implant: gate_edge + 0.18 enclosure _add_lvtn_or_hvtp(c, info, LAYER.lvtndrawing) if guard_ring: _add_guard_ring(c, info, is_pmos=True) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: High-Vt PMOS 1.8V # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__pfet_01v8_hvt( gate_width: float = 0.42, gate_length: float = 0.15, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """High-Vt 1.8V PMOS (sky130_fd_pr__pfet_01v8_hvt).""" c = gf.Component() info = _mosfet_core( c, gate_width, gate_length, nf, is_pmos=True, suppress_nwell=guard_ring ) # HVTP implant: gate_edge + 0.18 enclosure _add_lvtn_or_hvtp(c, info, LAYER.hvtpdrawing) if guard_ring: _add_guard_ring(c, info, is_pmos=True) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: Thick-oxide NMOS 5V/10V # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__nfet_g5v0d10v5( gate_width: float = 0.42, gate_length: float = 0.50, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """Thick-oxide 5V/10V NMOS (sky130_fd_pr__nfet_g5v0d10v5).""" c = gf.Component() info = _mosfet_core(c, gate_width, gate_length, nf, is_pmos=False) # HVNTM layer _add_hvntm(c, info) gr_info = None if guard_ring: gr_info = _add_guard_ring(c, info, is_pmos=False, is_hvi=True) # HVI layer (sizing depends on guard ring) _add_hvi_nfet(c, info, guard_ring, gr_info) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: Thick-oxide PMOS 5V/10V # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__pfet_g5v0d10v5( gate_width: float = 0.42, gate_length: float = 0.50, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """Thick-oxide 5V/10V PMOS (sky130_fd_pr__pfet_g5v0d10v5).""" c = gf.Component() info = _mosfet_core( c, gate_width, gate_length, nf, is_pmos=True, suppress_nwell=guard_ring ) gr_info = None if guard_ring: gr_info = _add_guard_ring(c, info, is_pmos=True, is_hvi=True) # HVI layer (sizing depends on guard ring and nwell) _add_hvi_pfet(c, info, guard_ring, gr_info) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: 20V LDNMOS # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__nfet_20v0( gate_width: float = 0.42, gate_length: float = 2.0, sd_width: float = 0.50, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """20V LDNMOS (sky130_fd_pr__nfet_20v0) — simplified.""" c = gf.Component() info = _mosfet_core(c, gate_width, gate_length, nf, is_pmos=False) _add_hvntm(c, info) gr_info = None if guard_ring: gr_info = _add_guard_ring(c, info, is_pmos=False, is_hvi=True) _add_hvi_nfet(c, info, guard_ring, gr_info) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: 20V LDPMOS # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__pfet_20v0( gate_width: float = 0.42, gate_length: float = 2.0, sd_width: float = 0.50, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """20V LDPMOS (sky130_fd_pr__pfet_20v0) — simplified.""" c = gf.Component() info = _mosfet_core( c, gate_width, gate_length, nf, is_pmos=True, suppress_nwell=guard_ring ) gr_info = None if guard_ring: gr_info = _add_guard_ring(c, info, is_pmos=True, is_hvi=True) _add_hvi_pfet(c, info, guard_ring, gr_info) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: Native NMOS 3.3V # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__nfet_03v3_nvt( gate_width: float = 0.42, gate_length: float = 0.50, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """Native NMOS 3.3V (sky130_fd_pr__nfet_03v3_nvt).""" c = gf.Component() info = _mosfet_core(c, gate_width, gate_length, nf, is_pmos=False) # areaidlvNative marker _add_areaid_native(c, info) # LVTN implant _add_lvtn_or_hvtp(c, info, LAYER.lvtndrawing) # HVNTM layer _add_hvntm(c, info) gr_info = None if guard_ring: gr_info = _add_guard_ring(c, info, is_pmos=False, is_hvi=True, is_nvt=True) # HVI layer _add_hvi_nfet(c, info, guard_ring, gr_info) _add_ports(c, info, gate_width) return c
# --------------------------------------------------------------------------- # Variant: Native NMOS 5V # ---------------------------------------------------------------------------
[docs] @gf.cell def sky130_fd_pr__nfet_05v0_nvt( gate_width: float = 0.42, gate_length: float = 0.90, sd_width: float = 0.28, nf: int = 1, guard_ring: bool = True, dnwell: bool = False, end_cap: float = 0.13, mult: int = 1, ) -> gf.Component: """Native NMOS 5V (sky130_fd_pr__nfet_05v0_nvt).""" c = gf.Component() info = _mosfet_core(c, gate_width, gate_length, nf, is_pmos=False) # LVTN implant _add_lvtn_or_hvtp(c, info, LAYER.lvtndrawing) # HVNTM layer _add_hvntm(c, info) gr_info = None if guard_ring: gr_info = _add_guard_ring(c, info, is_pmos=False, is_hvi=True, is_nvt=True) # HVI layer _add_hvi_nfet(c, info, guard_ring, gr_info) _add_ports(c, info, gate_width) return c
if __name__ == "__main__": c = sky130_fd_pr__nfet_01v8() c.show()