Source code for reasitic.substrate.shunt

"""Shunt capacitance from metal traces to substrate ground.

For each polygon on metal layer ``m`` we model the capacitance to
the substrate's bulk ground as a parallel-plate cap

.. math::

    C_p = \\varepsilon_0 \\varepsilon_r \\, A / h

with ``A`` the polygon's footprint area, ``h`` the vertical distance
from the metal centreline to the bottom of the layer stack, and
``ε_r`` the layer-stack-averaged relative permittivity. This is a
textbook approximation; the binary's full Green's-function path
captures lateral coupling that this stub ignores.

The fringe correction adds the standard 0.5·ε₀·(perimeter) term
(Yuan & Trick 1982).

Mirrors the simpler half of ``coupled_microstrip_caps_hj``
(``asitic_kernel.c:0x0804df6c``).
"""

from __future__ import annotations

from reasitic.geometry import Point, Shape
from reasitic.tech import Tech
from reasitic.units import EPS_0, UM_TO_M


[docs] def parallel_plate_cap_per_area(eps_r: float, h_um: float) -> float: """C/A in F/μm² for a parallel-plate cap of dielectric ``eps_r`` and thickness ``h`` (μm).""" if h_um <= 0: return float("inf") return EPS_0 * eps_r / (h_um * UM_TO_M)
def _polygon_signed_area(vertices: list[Point]) -> float: """Shoelace area of a 2D polygon (xy plane) in μm².""" n = len(vertices) if n < 3: return 0.0 area = 0.0 for i in range(n - 1): v_i = vertices[i] v_j = vertices[i + 1] area += v_i.x * v_j.y - v_j.x * v_i.y return 0.5 * area def _polygon_perimeter(vertices: list[Point]) -> float: if len(vertices) < 2: return 0.0 total = 0.0 for i in range(len(vertices) - 1): total += vertices[i].distance_to(vertices[i + 1]) return total
[docs] def shape_shunt_capacitance(shape: Shape, tech: Tech) -> float: """Total shunt capacitance from ``shape`` to substrate ground, in F. For each polygon we model the path from its metal-layer centreline down to the substrate ground as a stack of series parallel-plate caps, one per dielectric layer between the metal and the ground: .. math:: \\frac{1}{C_\\text{path}} = \\sum_k \\frac{h_k}{\\varepsilon_0 \\varepsilon_{r,k} A} so the equivalent ``ε_eff = h_total / Σ (h_k / ε_{r,k})`` only averages layers actually in the path to ground (rather than every substrate layer in the tech file as the previous version did). The fringe term keeps the same Yuan–Trick form on ``ε_eff``. Mirrors the simpler half of ``coupled_microstrip_caps_hj`` (``asitic_kernel.c:0x0804df6c``). """ if not tech.layers: return 0.0 total_C = 0.0 for poly in shape.polygons: if poly.metal < 0 or poly.metal >= len(tech.metals): continue m = tech.metals[poly.metal] if m.layer >= len(tech.layers): continue # Stack of layers strictly below the metal's assigned layer below = tech.layers[: m.layer] # Plus the part of the metal's own layer between ground-side # of the layer and the metal centreline. own_thickness_below = max(0.0, m.d) # Total path height h_total = sum(layer.t for layer in below) + own_thickness_below if h_total <= 0: continue # Series-cap effective ε_r over the path-to-ground layers inv_eps_total = ( sum((layer.t / layer.eps) for layer in below if layer.eps > 0) + own_thickness_below / max(tech.layers[m.layer].eps, 1.0) ) eps_eff = h_total / inv_eps_total if inv_eps_total > 0 else 1.0 A_um2 = abs(_polygon_signed_area(poly.vertices)) C_per_area = parallel_plate_cap_per_area(eps_eff, h_total) total_C += C_per_area * A_um2 * (UM_TO_M**2) # Fringe term — unchanged per_um = _polygon_perimeter(poly.vertices) total_C += 0.5 * EPS_0 * eps_eff * per_um * UM_TO_M return total_C