Source code for jitxlib.landpatterns.grid_layout

from collections.abc import Iterable
from dataclasses import dataclass
from typing import Any, override
from jitx.landpattern import Pad
from jitx.transform import Transform
from jitx._structural import Structurable, Ref

from . import LandpatternProvider, LandpatternGenerator


[docs] @dataclass(frozen=True) class GridPosition: """Grid Position This class represents a position in a grid of pads as well as a pose for the pad at that position. This is used by the :py:class:`~GridLandpatternGenerator` classes during the construction of a landpattern. """ row: int """The row of the grid position, zero-indexed.""" column: int """The column of the grid position, zero-indexed.""" pose: Transform """The pose of the pad at the grid position.""" def __post_init__(self): assert self.row >= 0 assert self.column >= 0
[docs] class GridLayout: _num_rows: int = 0 _num_cols: int = 0
[docs] class GridLayoutInterface(GridLayout, LandpatternProvider): def _generate_layout(self) -> Iterable[GridPosition]: """Generate a sequence of all the grid positions in the landpattern in the order they should be traversed. Note that the numbering scheme will likely ignore the order of the sequence, but it may be used to determine pin 1, etc.""" raise NotImplementedError(f"{self.__class__.__name__}._generate_layout") def _transform_layout(self, pos: GridPosition) -> GridPosition: """Transform a grid position. This can be used to transform the landpattern to a different coordinate system, such as a different origin or orientation.""" return pos def _map_position(self, pos: GridPosition) -> tuple[int, int]: """Map a grid position to a row and column. This is primarily used to eliminate unused rows or columns or offset indexing. Care should be taken to not generate collisions. The resulting row and column will be used to determine the pad numbering and does not affect anything else. Subclasses should call ``super()._map_position`` to ensure that other mixins will be able to influence the position mapping as well.""" return pos.row, pos.column def _assign_pad(self, r: int, c: int, pad: Pad): """Assign a pad to the specified grid position.""" raise NotImplementedError( f"Missing {self.__class__.__name__}._assign_pad. The landpattern generator must specify a numbering scheme." ) def _get_pad(self, r: int, c: int) -> Pad: """Get the pad at the specified row and column.""" raise NotImplementedError( f"Missing {self.__class__.__name__}._get_pad. The landpattern generator must specify a numbering scheme." ) def _get_grid_position(self, pos: GridPosition) -> Pad: """Get the pad at the specified grid position.""" return self._get_pad(*self._map_position(pos)) def _active_pad(self, pos: GridPosition) -> bool: """Determine if a pad is active. Will be queried during construction of the landpattern to filter out grid locations that should not generate pads.""" return True def _create_pad(self, pos: GridPosition) -> Pad: """Create a pad at the specified grid position.""" raise ValueError("No applicable pad generator found") @override def _build(self): super()._build() for pos in self._generate_layout(): if self._active_pad(pos): self._assign_pad( *self._map_position(self._transform_layout(pos)), self._create_pad(pos), )
[docs] class A1(GridLayoutInterface): """Simple utility mixin to offset the minor index to start at 1 instead of 0, e.g. A[1]. Note that this offsets the minor index, which is typically but not always the column, and the order matters if using a numbering mixin that changes that, going before such mixins as :py:class:`~ColumnMajorOrder`.""" @override def _map_position(self, pos: GridPosition) -> tuple[int, int]: r, c = super()._map_position(pos) return r, c + 1
[docs] class ColumnMajorOrder(GridLayoutInterface): """Simple utility mixin to index the grid in column-major order instead of row-major order.""" @override def _map_position(self, pos: GridPosition) -> tuple[int, int]: r, c = super()._map_position(pos) return c, r
_ROW_LOOKUP = "ABCDEFGHJKLMNPRTUVWY" _ROW_RADIX = len(_ROW_LOOKUP)
[docs] def to_bga_row_ref(row: int) -> str: """Convert a row number into a alpha row reference This function converts a zero-indexed row ordinal to a BGA-style alpha row reference string. The letters I, O, Q, S, X, and Z are skipped. For example: 0 -> "A" 1 -> "B" 19 -> "Y" 20 -> "AA" 39 -> "AY" 40 -> "BA" 419 -> "YY" 420 -> "AAA" """ if row < 0: raise ValueError(f"Row index must be non-negative: {row}") ref = "" while row >= 0: rem = row % _ROW_RADIX row = row // _ROW_RADIX - 1 ref += _ROW_LOOKUP[rem] return ref[::-1]
[docs] class AlphaDictNumberingBase(GridLayoutInterface): """Base class to provide a BGA-style alpha-numeric dictionary numbering scheme. The row is referred to by a letter as a member field, and the column is referred to by a number inside the dictionary at that field. No predefined rows are provided, and the intent is that this class is subclassed with declarations for all used rows. Note that the row letters skip the letters I, O, Q, S, X, and Z. If the exact rows are not known, then the :py:class:`~AlphaDictNumbering` class can be used instead to at least provide some type safety for the first 20 rows. >>> class My4RowNumbering(AlphaDictNumberingBase): ... A: dict[int, Pad] ... B: dict[int, Pad] ... C: dict[int, Pad] ... D: dict[int, Pad] """ class _DictRefs(Ref): # keep track of dictionaries that needt beo cleaned up def __init__(self): self.dict: list[dict[int, Pad]] = [] __dicts = _DictRefs() def _build(self): for d in self.__dicts.dict: for pad in d.values(): Structurable._dispose(pad) d.clear() super()._build() @override def _assign_pad(self, r: int, c: int, pad: Pad): row = to_bga_row_ref(r) d = getattr(self, row, None) if d is None: d = {} setattr(self, row, d) d[c] = pad @override def _get_pad(self, r: int, c: int) -> Pad: row = to_bga_row_ref(r) d = getattr(self, row, None) if d is None: raise ValueError(f"Row {row} does not exist") return d[c]
[docs] class AlphaDictNumbering(AlphaDictNumberingBase): """Assign numbers to pads using an alpha-numeric dictionary. The row is referred to by a letter as a member field, and the column is referred to by a number inside the dictionary at that field. The row letters skip the letters I, O, Q, S, X, and Z. Note that the type checker is only aware of the first 20 rows, so will not provide type safety beyond that. From row 20 each row will be referred to by two letters, row 20 will be referred to as "AA", row 21 as "AB", and so on. .. note:: Only used rows will have their fields set, but please note that the type checker will not flag access to rows even though they may not be set at runtime, resulting in an attribute error. When in doubt, use :py:mod:`~jitx.inspect` functions instead. """ A: dict[int, Pad] B: dict[int, Pad] C: dict[int, Pad] D: dict[int, Pad] E: dict[int, Pad] F: dict[int, Pad] G: dict[int, Pad] H: dict[int, Pad] J: dict[int, Pad] K: dict[int, Pad] L: dict[int, Pad] M: dict[int, Pad] N: dict[int, Pad] P: dict[int, Pad] R: dict[int, Pad] T: dict[int, Pad] U: dict[int, Pad] V: dict[int, Pad] W: dict[int, Pad] Y: dict[int, Pad] # tell the type checker that there may be dynamic attributes def __setattr__(self, name: str, value: Any) -> None: super().__setattr__(name, value)
[docs] class AlphaNumericNumberingBase(GridLayoutInterface): """Base class to provide a BGA-style alpha-numeric numbering scheme. Pads are referred by a unique name consisting of the row letter and a number. It's advisable to create a subclass with declarations for all used pads. Note that the row letters skip the letters I, O, Q, S, X, and Z. The column indicies start at 0 unless offset using, for example, :py:class:`~A1`, and are not zero-padded. If pads are not known in advance, then introspection can be used to access pads with type safety, but without language server support. In this case it's recommended to use something like the :py:class:`~AlphaNumericNumbering` class instead. >>> class My9PadBGANumbering(AlphaNumericNumberingBase, A1): ... A1: Pad ... A2: Pad ... A3: Pad ... B1: Pad ... B2: Pad ... B3: Pad ... C1: Pad ... C2: Pad ... C3: Pad """ class _FieldRefs(Ref): # keep track of dictionaries that needt beo cleaned up def __init__(self): self.dict: list[dict[str, Pad]] = [] __fields = _FieldRefs() def _build(self): for d in self.__fields.dict: for field, pad in d.items(): setattr(self, field, None) Structurable._dispose(pad) super()._build() @override def _assign_pad(self, r: int, c: int, pad: Pad): row = to_bga_row_ref(r) field = f"{row}{c}" setattr(self, field, pad) @override def _get_pad(self, r: int, c: int) -> Pad: row = to_bga_row_ref(r) field = f"{row}{c}" d = getattr(self, field, None) if d is None: raise ValueError(f"Pad {field} does not exist") return d[c]
[docs] class LinearNumbering(GridLayoutInterface): """Assign numbers to pads using a linear numbering scheme, starting at pad number 1, accessed through ``p[1]``. Row and column indicies are ignored and pads are assigned in the generated order.""" # yes, these are mutable, but will be reset before use p: dict[int, Pad] = {} __pad_positions: dict[tuple[int, int], int] = {} @override def _assign_pad(self, r: int, c: int, pad: Pad): idx = len(self.p) + 1 self.p[idx] = pad self.__pad_positions[(r, c)] = idx @override def _get_pad(self, r: int, c: int) -> Pad: return self.p[self.__pad_positions[(r, c)]] @override def _build(self): for pad in self.p.values(): Structurable._dispose(pad) self.p = {} self.__pad_positions = {} super()._build()
[docs] class GridLandpatternGenerator(LandpatternGenerator, GridLayoutInterface): pass