"""
Via definitions
===============
This module provides classes for defining vias, including via types,
backdrill specifications, and via properties.
"""
from __future__ import annotations
from collections.abc import Callable, Mapping
from enum import Enum
from dataclasses import dataclass, field, replace
from typing import Any, ClassVar, TYPE_CHECKING
from jitx._translate.lookup import ComputedNet
from jitx.net import Port, TopologyNet
from .copper import Copper
from .decorators import early, late
from ._structural import InstanceField, Ref, dispose, instantiation
from .context import Context
from .feature import Cutout, Soldermask
from .memo import memoize
from .query import transformer
from .shapes.primitive import Circle
from .layerindex import Side
from .placement import Positionable
from ._utils import normalize_layer as _normalize_layer
if TYPE_CHECKING:
from jitx.si import PinModel
from jitx.controlpoint import ControlPoint
[docs]
@memoize
class Via(Positionable, Ref):
"""Via definition that can be instantiated in the board.
Layer indexes referenced in this definition are absolute in the board: 0 is always the top layer.
They are not relative to any component / circuit. Flipping via instances in the UI will not flip those layers,
contrary to landpattern pads.
>>> class StandardVia(Via):
... start = Side.Top
... stop = Side.Bottom
... diameter = 0.6
... diameters = {0: 0.5, 1: ViaDiameter(0.5, nfp=0.2)}
... hole_diameter = 0.3
... type = ViaType.MechanicalDrill
"""
type: ClassVar[ViaType]
"""Type of via drilling method: MechanicalDrill or LaserDrill."""
name: str | None = None
"""Name of the via. If not specified, the name of the class will be used. This name must be unique."""
start_layer: int
"""Starting layer for the via. Setting this to a layer index other than the top layer allows for creating buried or blind vias."""
stop_layer: int
"""Ending layer for the via."""
diameter: float | ViaDiameter
"""Pad diameter of the via, in mm. Can be overridden on a per-layer basis by :py:attr:`~jitx.via.Via.diameters`."""
diameters: Mapping[int | tuple[int, ...], float | ViaDiameter] = {}
"""Pad diameters of the via for specific layers. Overrides :py:attr:`~jitx.via.Via.diameter` for the given layers."""
hole_diameter: float
"""Drilled or laser-cut hole diameter for the via, in mm."""
filled: bool = False
"""Whether the via is filled."""
tented: set[Side] | Side | None | bool = True
"""Whether the via is tented on Top, Bottom, or both sides. Untented sides will have a solder mask opening."""
via_in_pad: bool = False
"""Whether the via is allowed to be placed inside a component's pads."""
backdrill: BackdrillSet | Backdrill | None = None
"""Backdrill specifications. If a :py:class:`Backdrill` is used, it will be
assumed to be drilled from bottom."""
models: Mapping[tuple[int, int], PinModel] = {}
"""Specifies delay and loss models for the via, used by signal integrity constraints.
Each entry defines a model for a pair of layers. Layers are specified as layer indices.
Models are assumed to be symmetric, so the order of layer indices is arbitrary. It is an
error to specify multiple models for the same pair of layers.
It is recommended to not specify models with the same start and end layer. They will
currently be ignored by the constraint solver (since path does not travel through the via)
but this behavior may change in a future version of JITX. The models are given in terms of
the same :py:class:`jitx.si.PinModel` object used by components.
>>> class MyVia(Via):
>>> # Other via parameters...
>>> models = {
>>> # Top to bottom layer (through-hole via)
>>> (0, -1): PinModel(5e-12, 0.05),
>>> # Top to inner layer 1 (blind via)
>>> (0, 1): PinModel(2e-12, 0.02),
>>> # Inner layer 1 to inner layer 2 (buried via)
>>> (1, 2): PinModel(2e-12, 0.02),
>>> }
Not all layer pairs need to be specified. If you know that, for example, all usages of a via
will go between the top and bottom of the board, then only that model needs to be provided.
Via models can also be generated programmatically using loops. For example, to define models
for all layer pairs in a multi-layer board where delay and loss scale linearly with the
number of layers traversed:
>>> class MultiModelVia(Via):
>>> # Other via parameters...
>>>
>>> # Generate models for all layer pairs
>>> total_layers = 4
>>> models = {}
>>> for i in range(total_layers):
>>> for j in range(i + 1, total_layers):
>>> # Model that scales with number of layers
>>> n_layers = j - i
>>> models[(i, j)] = PinModel(
>>> 2e-12 * n_layers,
>>> 0.05 * n_layers
>>> )
When models are not provided for a via, placeholder models are automatically inserted.
When using placeholder models (models with ideal delay and loss), any timing or loss
constraints involving signals through these vias will be flagged as unsatisfied. This is
because the placeholder model assumes some arbitrary transmission delay and loss values,
which are not set by the user. For accurate signal integrity analysis, provide calculated,
measured, or simulated via models for all layer pairs used in your design.
For maximum accuracy, it is recommended to simulate or measure each specific pair of
layers rather than using a simplified linear model. The loop approach is most useful
for initial designs or when detailed measurements are not yet available.
"""
@early
def __early(self):
instantiation.push()
CurrentVia(self).set()
@late
def __late(self):
for post in CurrentVia.require().postprocessing:
post(self)
instantiation.pop()
port = Port()
"""Port for connectivity. It's mainly used internally."""
__iliad: list[Any] = InstanceField(list)
# Avoiding and __iadd__ or __add__ because vias aren't containers, and the
# signature is confusing with nets.
def _apply_tag(self, other: Any):
self.__iliad.append(other)
def __rshift__(
self, other: Port | TopologyNet[Port] | Via | ControlPoint
) -> TopologyNet[Port]:
from .via import Via
from .controlpoint import ControlPoint
if isinstance(other, TopologyNet):
dispose(other)
return TopologyNet((self,) + other.sequence)
elif isinstance(other, Port | Via | ControlPoint):
return TopologyNet(self, other)
return NotImplemented
def _layer_overrides(via: Via, n: int) -> dict[int, float | ViaDiameter]:
"""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, float | ViaDiameter] = {}
for key, diameter in via.diameters.items():
layers = key if isinstance(key, tuple) else (key,)
for layer in layers:
overrides[_normalize_layer(layer, n)] = diameter
return overrides
def _pad_diameter(d: float | ViaDiameter) -> float:
return d.pad if isinstance(d, ViaDiameter) else d
def _tented_sides(tented: set[Side] | Side | None | bool) -> set[Side]:
"""Sides covered by solder mask. Untented sides get an opening instead.
Mirrors the interpretation in :func:`jitx._translate.via.translate_tented`."""
if tented is True or tented is None:
return {Side.Top, Side.Bottom}
if tented is False:
return set()
if isinstance(tented, Side):
return {tented}
if isinstance(tented, set):
return set(tented)
raise ValueError(f"Bad via tented value: {tented!r}")
@transformer(Via, Copper)
def _via_to_copper(trace, via: Via):
"""Yield one Copper per conducting layer between ``via.start_layer`` and
``via.stop_layer`` (inclusive). Per-layer overrides from ``via.diameters``
take precedence; the dict keys may be expressed relative to either the
top (non-negative) or the bottom (negative) of the stackup.
Resolves the conductor count via ``jitx.current.substrate.stackup.conductors``,
so this transformer only fires inside a design context.
"""
from jitx import current
if not trace.transform:
# no transform, placement would be inaccurate.
return
n = len(current.substrate.stackup.conductors)
if via.transform:
trace = replace(trace, transform=trace.transform * via.transform)
cn = ComputedNet.get(via) or ComputedNet(None)
for layer, d in ViaDiameter.stack(via, n):
yield trace, cn.assign(Copper(Circle(diameter=_pad_diameter(d)), layer))
@transformer(Via, Cutout)
def _via_to_cutout(trace, via: Via):
"""Yield a single Cutout for the via's drilled hole. Only through-hole vias are considered.
If there's a backdrill, assume that side is drilled all the way to the surface."""
from jitx import current
if not trace.transform:
# no transform, placement would be inaccurate.
return
n = len(current.substrate.stackup.conductors)
start = _normalize_layer(via.start_layer, n)
stop = _normalize_layer(via.stop_layer, n)
if via.backdrill:
# if back/frontdrill, assume via drill goes all the way to the surface
if isinstance(via.backdrill, Backdrill):
stop = n - 1
elif isinstance(via.backdrill, BackdrillSet):
if via.backdrill.top:
start = 0
if via.backdrill.bottom:
stop = n - 1
if via.transform:
trace = replace(trace, transform=trace.transform * via.transform)
if start == 0 and stop == n - 1:
yield trace, Cutout(Circle(diameter=via.hole_diameter))
@transformer(Via, Soldermask)
def _via_to_soldermask(trace, via: Via):
"""Yield solder mask openings (solder mask geometry is negative) for the via.
A surface side the via reaches gets an opening sized to the pad diameter on
that layer when the side is untented. Independently, a backdrilled side gets
an opening sized to the backdrill's ``solder_mask_opening`` when that opening
is larger than the backdrill diameter. A bare :py:class:`Backdrill` is
assumed to be drilled from the bottom.
"""
from jitx import current
if not trace.transform:
# no transform, placement would be inaccurate.
return
n = len(current.substrate.stackup.conductors)
overrides = _layer_overrides(via, n)
start = _normalize_layer(via.start_layer, n)
stop = _normalize_layer(via.stop_layer, n)
surfaces: list[tuple[Side, int]] = []
if start == 0:
surfaces.append((Side.Top, 0))
if stop == n - 1:
surfaces.append((Side.Bottom, n - 1))
# At most one opening per side: when both rules apply, the larger diameter
# subsumes the smaller.
openings: dict[Side, float] = {}
def widen(side: Side, diameter: float):
openings[side] = max(openings.get(side, 0.0), diameter)
tented = _tented_sides(via.tented)
for side, layer in surfaces:
if side not in tented:
widen(side, _pad_diameter(overrides.get(layer, via.diameter)))
if isinstance(via.backdrill, Backdrill):
backdrills = [(via.backdrill, Side.Bottom)]
elif isinstance(via.backdrill, BackdrillSet):
backdrills = [
(via.backdrill.top, Side.Top),
(via.backdrill.bottom, Side.Bottom),
]
else:
backdrills = []
for bd, side in backdrills:
if bd is not None and bd.solder_mask_opening > bd.diameter:
widen(side, bd.solder_mask_opening)
if via.transform:
trace = replace(trace, transform=trace.transform * via.transform)
for side, diameter in openings.items():
yield trace, Soldermask(Circle(diameter=diameter), side=side)
[docs]
class ViaDiameter:
"""Diameters of various features of a via."""
pad: float
"""Pad diameter for the via, in mm."""
nfp: float | None = None
"""Pad diameter for the via when non-functional pads are removed, in mm.
When provided, it overrides the pad diameter except on the start layer if there is no top backdrill,
stop layer if there is no bottom backdrill and intermediate copper layers that have traces or pours connected to the via."""
def __init__(
self,
pad: float,
*,
nfp: float | None = None,
):
self.pad = pad
self.nfp = nfp
def __float__(self):
return self.pad
[docs]
@staticmethod
def stack(via: Via, 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(via, n_layers)
for layer in range(
_normalize_layer(via.start_layer, n_layers),
_normalize_layer(via.stop_layer, n_layers) + 1,
):
yield layer, overrides.get(layer, via.diameter)
[docs]
class ViaType(Enum):
"""Type of via drilling method."""
MechanicalDrill = 1
LaserDrill = 2
[docs]
@dataclass
class Backdrill:
"""Backdrill specification for a via."""
diameter: float
"""Diameter of the backdrill in mm."""
startpad_diameter: float
"""Diameter of the starting pad in mm."""
solder_mask_opening: float
"""Solder mask opening size in mm."""
copper_clearance: float
"""Copper clearance diameter in mm."""
[docs]
@dataclass
class BackdrillSet:
"""Set of backdrill specifications for top and bottom sides."""
top: Backdrill | None = None
"""Backdrill specification for the top side."""
bottom: Backdrill | None = None
"""Backdrill specification for the bottom side."""
[docs]
@dataclass
class CurrentVia(Context):
"""Context object representing the currently active via during
processing. Should not be used directly, but rather accessed through
:py:data:`jitx.current`'s :py:attr:`~jitx.Current.via` instead.
>>> def get_via_diameter() -> float | ViaDiameter:
... via = jitx.current.via
... return via.diameter
"""
via: Via
postprocessing: list[Callable[[Via], None]] = field(default_factory=list)
[docs]
def postprocess(self, func: Callable[[Via], Any]):
self.postprocessing.append(func)