Source code for jitxexamples.demos.si_bga_optimization.si_geometry

"""Per-lane SI escape geometry for the BGA escape design.

Signal-via antipad shapes and keepouts, deskew-arc cross-section and
per-lane knobs, GND-via stitching sites, the board-level SI cutout, and
the HFSS-instrumented-lane override. Helpers here build shapes and
``KeepOut``s only; JITX objects with Net membership (``Pour``s, Vias)
are constructed by the owning Circuit in ``bga_escape``.
"""

import math

import shapely
from jitx import KeepOut, LayerSet
from jitx.feature import Custom
from jitx.shapes.composites import rectangle
from jitx.shapes.primitive import Text
from jitx.shapes.shapely import ShapelyGeometry
from . import generic_bga as bga
from .deskew import is_overlappable_copper
from .substrate import PerSignalLayerSpec


# -----------------------------------------------------------------------------
# Per-lane geometry knobs and constants.
# -----------------------------------------------------------------------------

# Deskew-arc cross-section. Decoupled from ``substrate.DIFFPAIR_*``: the
# deskew arc routes its two legs as largely-uncoupled single-ended
# traces, so each leg's local Z is set by trace width vs. reference-
# plane spacing, not by the inner edge-to-edge gap. Widening the legs
# from 0.115 -> 0.150 mm pulls Z_se down toward 46 ohm -> Z_diff toward
# 85 ohm; inner gap preserved at the original 0.118 mm.
DESKEW_TRACE_WIDTH = 0.138
DESKEW_PAIR_SPACING = 0.118

# Signal-via antipad geometry. Two radii around each via: the keepout
# (antipad clearance from the pad edge) and the fence (larger; pour
# boundary). The instrumented lane swaps the keepout for a physics-
# sized split radius on the L1-L3 reference-plane crossings above
# Signal2.
SIGNAL_VIA_PAD_DIAMETER = 0.25
SIGNAL_VIA_KEEPOUT_RADIUS = SIGNAL_VIA_PAD_DIAMETER / 2 + 0.05
SIGNAL_VIA_FENCE_RADIUS = SIGNAL_VIA_PAD_DIAMETER / 2 + 0.17
_SIGNAL_VIA_ANTIPAD_OFFSET = 0.15  # buffer applied to deskew copper

# Deskew arc geometry (single shared input + clearance pair).
DESKEW_EXIT_ABOVE_LOWER_BALL = 0.25
DESKEW_VIA_PAD_TO_TRACE_GAP = 0.10


[docs] def deskew_knobs(lane_index: int) -> tuple[float, float]: """Hand-tuned ``(theta_exit_deg, right_r_wrap)`` per lane. Smaller ``theta_exit_deg`` lengthens the P-leg wrap. ``right_r_wrap`` is the wrap radius in mm. Lane 1 carries the HFSS instrumentation. """ return ((92.7, 0.45), (99.6, 0.45), (45.0, 0.50))[lane_index]
# ----------------------------------------------------------------------------- # Geometry helpers (board-level features, antipads, deskew shapes). # -----------------------------------------------------------------------------
[docs] def make_si_cutout(): # Cutout edges sit just inside the board-edge stitch-via row. The # previous -13.05 caused TXb6 Signal1 coupled-strip ports to touch # the top-to-Ground2 ground vias at the port plane. +0.10 mm "edge # capture" so boundary GND balls aren't dropped by this helper. edge_capture = 0.10 ymin = -12.95 ymax = -4.75 + edge_capture xmin = -(bga.BGA_LAYOUT_CENTER_COL * bga.BGA_VPITCH) - (bga.BGA_BALL_DIAMETER / 2) xmax = ( ((bga.BGA_COLS - 1) - bga.BGA_LAYOUT_CENTER_COL) * bga.BGA_VPITCH + bga.BGA_BALL_DIAMETER / 2 + edge_capture ) w = xmax - xmin h = ymax - ymin cx = (xmax + xmin) / 2 cy = (ymax + ymin) / 2 return [ Custom(rectangle(w, h).at(cx, cy), name="SI CUTOUT"), Custom(Text("SI Cutout", 0.3).at(cx, cy), name="SI CUTOUT"), ]
def _occupied_signal_cols_by_row() -> dict[int, set[int]]: occupied: dict[int, set[int]] = {} for pair_index, (top_row, bottom_row) in enumerate(bga.SIGNAL_ROW_PAIRS): cols = bga.signal_cols_for_pair(pair_index) for row_index in (top_row, bottom_row): occupied.setdefault(row_index, set()).update(cols) return occupied
[docs] def gnd_pad_via_sites() -> list[tuple[int, int, int]]: """GND-via stitching plan around every BGA GND ball. Pure data: returns ``(row_index, col, signal_layer)`` triples in canonical order. ``BGALink`` constructs the actual Via instances directly (each site maps to ``substrate.gnd_via[signal_layer]()`` at the ball's coordinates), keeping JITX object emission inside the owning Circuit rather than in a free helper. For each signal row pair, walks the rows above and below, and at every (row, col) that isn't itself a signal ball records the ``gnd_via`` layer that matches the row pair's signal layer (so the via spans L1 to the signal's lower reference plane). If two row pairs both reach the same (row, col), the deeper signal layer wins. """ row_col_layers: dict[tuple[int, int], int] = {} occupied_signal_cols = _occupied_signal_cols_by_row() def add_gnd_row(row_index: int, ground_layer: int) -> None: if row_index < 0 or row_index >= bga.BGA_ROWS: return for col in range(1, bga.BGA_COLS + 1): if col in occupied_signal_cols.get(row_index, set()): continue key = (row_index, col) row_col_layers[key] = max(ground_layer, row_col_layers.get(key, 0)) for pair_index, row_pair in enumerate(bga.SIGNAL_ROW_PAIRS): ground_layer = bga.signal_layer_for_pair(pair_index) for signal_row in row_pair: add_gnd_row(signal_row - 1, ground_layer) add_gnd_row(signal_row, ground_layer) add_gnd_row(signal_row + 1, ground_layer) return [(row, col, layer) for (row, col), layer in sorted(row_col_layers.items())]
[docs] def signal_via_pair_capsule( p_pad: tuple[float, float], n_pad: tuple[float, float], radius: float, ): """Capsule with circular caps centered at the two signal via pads.""" return ShapelyGeometry(shapely.LineString([p_pad, n_pad]).buffer(radius))
[docs] def signal_via_pair_antipad_keepouts( keepout_shape, spec: PerSignalLayerSpec, p_pad: tuple[float, float] | None = None, n_pad: tuple[float, float] | None = None, split_keepout_radius: float = SIGNAL_VIA_KEEPOUT_RADIUS, split_antipad_layers: LayerSet | None = None, ) -> list[KeepOut]: """Antipad keepouts for one signal-via pair. Always returns at least one element (the per-pair capsule cut on ``spec.antipad_layers``); additional elements appear when ``split_antipad_layers`` (defaulting to ``spec.split_antipad_layers``) adds per-via circular cuts on those layers. The matching upper-reference fence Pour is *not* returned here: Pours must live on the same Circuit as the Net they belong to (the board-wide ``GND`` Net lives on ``BGALink``), so ``BGALink`` constructs the Pour itself from the ``fence_shape`` exposed by each ``EscapeLane``. """ keepouts: list[KeepOut] = [ KeepOut(keepout_shape, layers=spec.antipad_layers, pour=True, via=True) ] active_split_layers = ( spec.split_antipad_layers if split_antipad_layers is None else split_antipad_layers ) if active_split_layers is not None: if p_pad is None or n_pad is None: raise ValueError( "split_antipad_layers requires p_pad/n_pad to build per-via " "circular keepouts" ) for pad in (p_pad, n_pad): circle = ShapelyGeometry(shapely.Point(pad).buffer(split_keepout_radius)) keepouts.append( KeepOut( circle, layers=active_split_layers, pour=True, via=True, ) ) return keepouts
def _signal_via_d_keepout( pad: tuple[float, float], partner_pad: tuple[float, float], inward_radius: float, outward_radius: float, ) -> ShapelyGeometry: """Per-via D cut: flat side faces the paired via.""" px, py = pad dx = partner_pad[0] - px dy = partner_pad[1] - py length = math.hypot(dx, dy) if length == 0: raise ValueError("D-shaped keepout requires distinct signal-via pads") ux = dx / length uy = dy / length vx = -uy vy = ux lateral = outward_radius * 4.0 back = outward_radius * 2.0 boundary_x = px + ux * inward_radius boundary_y = py + uy * inward_radius back_x = px - ux * back back_y = py - uy * back half_plane = shapely.Polygon( [ (boundary_x + vx * lateral, boundary_y + vy * lateral), (boundary_x - vx * lateral, boundary_y - vy * lateral), (back_x - vx * lateral, back_y - vy * lateral), (back_x + vx * lateral, back_y + vy * lateral), ] ) disk = shapely.Point(pad).buffer(outward_radius) return ShapelyGeometry(disk.intersection(half_plane).buffer(0)) def _shape_to_shapely(shape): if hasattr(shape, "to_shapely"): return shape.to_shapely().g return ShapelyGeometry.from_shapegeometry(shape).g def _deskew_antipad_shape(coppers, antipad_shape) -> ShapelyGeometry: geoms = [ _shape_to_shapely(copper.shape).buffer(_SIGNAL_VIA_ANTIPAD_OFFSET) for copper in coppers if is_overlappable_copper(copper) ] geoms.append(_shape_to_shapely(antipad_shape)) if not geoms: raise ValueError("No overlappable deskew copper was provided") union = shapely.unary_union(geoms).buffer(0) if union.geom_type != "Polygon": union = union.convex_hull return ShapelyGeometry(union)
[docs] def deskew_antipad_keepout_and_pour_shape( right_copper, left_copper, antipad_shape, fence_pour_shape, spec: PerSignalLayerSpec, ) -> tuple[KeepOut, ShapelyGeometry]: """Build the deskew-layer keepout and the deskew fence Pour's shape. ``antipad_shape`` and ``fence_pour_shape`` are usually the same (the fence-pour-sized capsule unioned with buffered deskew copper). The instrumented lane decouples them: the keepout shrinks to the per-via keepout-sized capsule so the L4 cut stays small even when the L1 D-cut envelope grows. The Pour itself is constructed by ``BGALink`` (it must live on the same Circuit as the ``GND`` Net it belongs to), so we return the keepout (no Net membership, lives on ``EscapeLane``) and the *shape* the Pour needs. """ coppers = (right_copper, left_copper) keepout_shape = _deskew_antipad_shape(coppers, antipad_shape) pour_shape = _deskew_antipad_shape(coppers, fence_pour_shape) keepout = KeepOut( keepout_shape, layers=spec.deskew_antipad_layers, pour=True, via=True ) return keepout, pour_shape
# ----------------------------------------------------------------------------- # HFSS-instrumented lane. # ----------------------------------------------------------------------------- INSTRUMENTED_SIGNAL2_SPLIT_ANTIPAD_RADIUS = 0.38 # Outer radius of the L1 D-cut keepouts; the fence pour also lands here # (no extra margin past the D-cut — pushing further hit neighbours). INSTRUMENTED_SIGNAL2_L1_D_OUTWARD_RADIUS = ( INSTRUMENTED_SIGNAL2_SPLIT_ANTIPAD_RADIUS + SIGNAL_VIA_FENCE_RADIUS - SIGNAL_VIA_KEEPOUT_RADIUS ) # Instrumented row pair (the lane the HFSS sim points at). Changing # this re-points instrumentation; the design itself populates every # row pair equally. _INSTRUMENTED_ROW_PAIR = (16, 17) _INSTRUMENTED_PAIR_INDEX = bga.SIGNAL_ROW_PAIRS.index(_INSTRUMENTED_ROW_PAIR) _INSTRUMENTED_LANE_INDEX = 1 # Mirrored D-cut keepout for the instrumented lane on L1 only (zero- # based layer index 0). The all-layer D cut expansion to L1/L2/L3 was # the dominant Scd21 regression source; reverted to the L1-D + L2/L3- # circular hybrid. _INSTRUMENTED_D_KEEPOUT_LAYERS = LayerSet(0)
[docs] def instrumented_l1_d_keepouts( p_pad: tuple[float, float], n_pad: tuple[float, float], ) -> list[KeepOut]: return [ KeepOut( _signal_via_d_keepout( p_pad, n_pad, INSTRUMENTED_SIGNAL2_SPLIT_ANTIPAD_RADIUS, INSTRUMENTED_SIGNAL2_L1_D_OUTWARD_RADIUS, ), layers=_INSTRUMENTED_D_KEEPOUT_LAYERS, pour=True, via=True, ), KeepOut( _signal_via_d_keepout( n_pad, p_pad, INSTRUMENTED_SIGNAL2_SPLIT_ANTIPAD_RADIUS, INSTRUMENTED_SIGNAL2_L1_D_OUTWARD_RADIUS, ), layers=_INSTRUMENTED_D_KEEPOUT_LAYERS, pour=True, via=True, ), ]
[docs] def is_instrumented_lane(pair_index: int, lane_index: int) -> bool: """The HFSS-instrumented lane lives on a specific Signal2 row pair. The signal-layer guard is defensive: if ``_INSTRUMENTED_ROW_PAIR`` is ever retargeted off Signal2, the Signal2-specific override (L1 D-cuts + enlarged L2/L3 circles) silently no-ops instead of misapplying to a different layer's antipad stack. """ return ( pair_index == _INSTRUMENTED_PAIR_INDEX and lane_index == _INSTRUMENTED_LANE_INDEX and bga.signal_layer_for_pair(pair_index) == 2 )