"""
Landpattern and Pad definitions
===============================
This module provides classes for defining component landpatterns,
pads, and mappings between ports and pads.
"""
from __future__ import annotations
from collections.abc import Callable, Iterable, Sequence, Mapping
from dataclasses import dataclass, field, replace
from typing import Any, overload
from jitx._translate.lookup import ComputedNet
from jitx._utils import normalize_layer as _normalize_layer
from jitx.feature import Cutout
from jitx.inspect import extract
from jitx.shapes.primitive import Empty
from .net import Port
from .shapes import Shape
from .placement import Positionable
from .memo import memoize
from .context import Context
from .copper import Copper
from .decorators import early, late
from .query import transformer
from ._structural import Open, Ref, Structural, instantiation, InstanceField
[docs]
@memoize
class Landpattern(Open, Positionable):
"""Component landpattern definition, also known as a footprint, package, or land-pattern.
Defines the pads and associated geometry for the interface between an electrical component and the circuit board.
>>> # MyPad is a user-defined Pad subclass (see Pad class below)
>>> # Silkscreen, Courtyard are from jitx.feature
>>> # Polyline, rectangle are from jitx.shapes
>>> class ZigZagLandpattern(Landpattern):
... pad1 = MyPad().at(-0.5, 0)
... pad2 = MyPad().at(0.5, 0)
...
... silkscreen = Silkscreen(
... Polyline(
... 0.1,
... [
... (-0.4, 1), (-0.3, 1.2), (-0.2, 0.8),
... (-0.1, 1.2), (0, 0.8), (0.1, 1.2),
... (0.2, 0.8), (0.3, 1.2), (0.4, 1),
... ]
... )
... )
...
... courtyard = Courtyard(rectangle(2, 1))
"""
@early
def __early(self):
instantiation.push()
CurrentLandpattern(self).set()
@late
def __late(self):
for post in CurrentLandpattern.require().postprocessing:
post(self)
instantiation.pop()
__iliad: list[Any] = InstanceField(list)
def __iadd__(self, other: Any):
self.__iliad.append(other)
return self
def _layer_overrides(pad: Pad, n: int) -> dict[int, Shape | PadShape]:
"""Per-layer diameter overrides from ``via.diameters``, with layer keys
normalized against an ``n``-conductor stackup. Keys may be expressed
relative to either the top (non-negative) or the bottom (negative)."""
overrides: dict[int, Shape | PadShape] = {}
for key, diameter in pad.shapes.items():
layers = key if isinstance(key, tuple) else (key,)
for layer in layers:
overrides[_normalize_layer(layer, n)] = diameter
return overrides
[docs]
class PadShape:
"""Shapes of various features of a pad."""
shape: Shape
"""The geometric shape of the pad."""
nfp: Shape | None = None
"""The geometric shape of the pad when non-functional pads are removed.
When provided, it overrides the pad shape except on the top layer, bottom layer
and intermediate copper layers that have traces or pours connected to the pad."""
def __init__(
self,
shape: Shape,
*,
nfp: Shape | None = None,
):
self.shape = shape
self.nfp = nfp
[docs]
@staticmethod
def stack(pad: Pad, n_layers: int | None = None):
"""Produce a sequence representing the ViaDiameter stack for a Via,
from the start_layer of the via through the stop_layer. Each layer
will be represented once with the accompanying ViaDiameter object or
float."""
if n_layers is None:
import jitx
n_layers = len(jitx.current.substrate.stackup.conductors)
overrides = _layer_overrides(pad, n_layers)
if len(list(extract(pad, Cutout))):
layers = range(0, n_layers)
else:
layers = (0,)
for layer in layers:
yield layer, overrides.get(layer, pad.shape)
[docs]
@memoize
class Pad(Open, Positionable):
"""Class representing a pad, user code should overload this class in order
to create new pad definitions. If the pad contains a
:py:class:`~jitx.feature.Cutout` it will be interpreted as a through-hole
pad, otherwise it will be interpreted as a surface mount pad.
>>> # Circle, rectangle are from jitx.shapes
>>> # Cutout, Soldermask are from jitx.feature
>>> class MyPad(Pad):
... shape = Circle(diameter=0.8)
... shapes = {
... (0, 2): Circle(diameter=0.7),
... 1: PadShape(Circle(diameter=0.8), nfp=Circle(diameter=0.4)),
... -1: rectangle(2., 2.),
... }
...
... def __init__(self):
... self.cutout = Cutout(Circle(diameter=0.4))
... self.soldermask = Soldermask(self.shape)
"""
shape: Shape | PadShape
"""The geometric shape of the pad or a PadShape to specify the regular shape and the shape when non-functional pads are removed.
Can be overridden on a per-layer basis by :py:attr:`~jitx.landpattern.Pad.shapes`."""
shapes: Mapping[int | tuple[int, ...], Shape | PadShape] = {}
"""The geometric shapes of the pad for specific layers. Overrides :py:attr:`~jitx.landpattern.Pad.shape` for the given layers."""
@early
def __early(self):
instantiation.push()
CurrentPad(self).set()
@late
def __late(self):
for post in CurrentPad.require().postprocessing:
post(self)
instantiation.pop()
__iliad: list[Any] = InstanceField(list)
def __iadd__(self, other: Any):
self.__iliad.append(other)
return self
@transformer(Pad, Copper)
def _pad_to_copper(trace, pad: Pad):
from jitx import current
if pad.transform is None:
return ()
if isinstance(pad.shape, Shape) and isinstance(pad.shape.geometry, Empty):
return ()
if isinstance(pad.shape, PadShape) and isinstance(pad.shape.shape.geometry, Empty):
return ()
cn = ComputedNet.get(pad) or ComputedNet(None)
nlayers = len(current.substrate.stackup.conductors)
if list(extract(pad, Cutout)):
layerset = set(range(nlayers))
else:
layerset = {0}
trace = replace(
trace, transform=trace.transform * pad.transform, parent=pad, trace=trace
)
for key, shape_or_padshape in pad.shapes.items():
layers = key if isinstance(key, tuple) else (key,)
shape = (
shape_or_padshape.shape
if isinstance(shape_or_padshape, PadShape)
else shape_or_padshape
)
for layer in layers:
layerset.discard(layer)
yield trace, cn.assign(Copper(shape, layer))
for layer in layerset:
shape = pad.shape.shape if isinstance(pad.shape, PadShape) else pad.shape
yield trace, cn.assign(Copper(shape, layer))
[docs]
class PadMapping(Structural, Ref):
"""Mapping between component ports and landpattern pads.
If no pad mapping is provided, a default mapping will be created that maps ports to pads in declaration order.
If a port needs to be mapped to multiple pads, a PadMapping is required.
>>> class MyComponent(Component):
... GND = Power()
... VIN = Power()
... VOUT = Power()
...
... lp = MyLandpattern()
... mappings = PadMapping({
... GND: [lp.p[1], lp.p[4]],
... VIN: lp.p[3],
... VOUT: lp.p[3],
... })
>>> class MyComponent(Component):
... GND = Power()
... VIN = Power()
... VOUT = Power()
...
... lp = MyLandpattern()
... mappings = PadMapping({
... GND: lp.p[1],
... VIN: lp.p[2],
... VOUT: lp.p[3],
... })
"""
__entries: dict[Port, Pad | Sequence[Pad]]
__inverse: dict[Pad, Port]
def __init__(
self,
entries: Mapping[Port, Pad | Sequence[Pad]]
| Iterable[tuple[Port, Pad | Sequence[Pad]]],
):
"""Initialize a pad mapping.
Args:
entries: Mapping or iterable of (port, pad) or (port, pad sequence) pairs.
"""
self.__entries = dict(entries)
self.__inverse = {}
for port, pad in self.__entries.items():
if isinstance(pad, Sequence):
for p in pad:
self.__inverse[p] = port
else:
self.__inverse[pad] = port
@overload
def __setitem__(self, port: Port, pad: Pad | Sequence[Pad], /): ...
@overload
def __setitem__(self, pad: Pad, port: Port, /): ...
def __setitem__(self, port: Port | Pad, pad: Port | Pad | Sequence[Pad]):
if isinstance(port, Pad):
assert isinstance(pad, Port)
other = self.__entries.get(pad)
if isinstance(other, Sequence):
self.__entries[pad] = tuple(other) + (port,)
elif isinstance(other, Pad):
self.__entries[pad] = (port, other)
else:
self.__entries[pad] = port
self.__inverse[port] = pad
elif isinstance(pad, Sequence):
self.__entries[port] = pad
for p in pad:
self.__inverse[p] = port
else:
assert isinstance(pad, Pad)
self.__entries[port] = pad
self.__inverse[pad] = port
@overload
def __getitem__(self, port: Port) -> Pad | Sequence[Pad]: ...
@overload
def __getitem__(self, port: Pad) -> Port: ...
def __getitem__(self, port: Port | Pad):
if isinstance(port, Pad):
return self.__inverse[port]
return self.__entries[port]
@overload
def get[T](
self, port: Port, default: T = None, /
) -> Pad | Sequence[Pad] | Port | T: ...
@overload
def get[T](self, port: Pad, default: T = None, /) -> Port | T: ...
[docs]
def get[T](
self, port: Port | Pad, default: T = None, /
) -> Pad | Sequence[Pad] | Port | T:
"""Return the pad or port associated with the given port or pad."""
if isinstance(port, Pad):
return self.__inverse.get(port, default)
return self.__entries.get(port, default)
def __contains__(self, port: Port | Pad):
if isinstance(port, Pad):
return port in self.__inverse
return port in self.__entries
def __len__(self):
return len(self.__entries)
def __iter__(self):
return iter(self.__entries)
[docs]
def items(self):
return self.__entries.items()
[docs]
def values(self):
return self.__entries.values()
[docs]
def inverse(self) -> Mapping[Pad, Port]:
"""Return the inverse mapping from pads to ports."""
return self.__inverse
[docs]
@dataclass
class CurrentLandpattern(Context):
"""Context object representing the currently active landpattern during
processing. Should not be used directly, but rather accessed through
:py:data:`jitx.current`'s :py:attr:`~jitx.Current.landpattern` instead.
>>> def get_landpattern_pads() -> list[Pad]:
... landpattern = jitx.current.landpattern
... pads = extract(landpattern, Pad)
... return list(pads)
"""
landpattern: Landpattern
postprocessing: list[Callable[[Landpattern], None]] = field(default_factory=list)
[docs]
def postprocess(self, func: Callable[[Landpattern], Any]):
self.postprocessing.append(func)
[docs]
@dataclass
class CurrentPad(Context):
"""Context object representing the currently active pad during
processing. Should not be used directly, but rather accessed through
:py:data:`jitx.current`'s :py:attr:`~jitx.Current.pad` instead.
>>> def get_pad_shape() -> Shape:
... pad = jitx.current.pad
... return pad.shape
"""
pad: Pad
postprocessing: list[Callable[[Pad], None]] = field(default_factory=list)
[docs]
def postprocess(self, func: Callable[[Pad], Any]):
self.postprocessing.append(func)