import math
from typing import NamedTuple, Sequence
from dataclasses import field
from jitx import PairInsertion, Route
from jitx.decorators import identityclass
from jitx.via import Via
from jitx.transform import Transform
from jitx.net import Net, Port, DiffPair, TopologyNet
from jitx.circuit import Circuit
from jitx.feature import KeepOut
from jitx.layerindex import LayerSet
from jitx.shapes import Shape
from jitx.container import Container
[docs]
@identityclass
class ViaGroundCage(Container):
"""Base class for defining the ground cage for a Via Structure"""
via_def: type[Via]
""" Via Definition for the ground cage vias. This definition will be used to
instantiate each of the needed vias.
"""
[docs]
def place_via_cage(self, n: Net):
"""User is expected to override this method and generate the
via instances needed for the cage.
Args:
n: Net to which the vias of the cage will be connected. This would
typically be ground in most applications.
"""
raise NotImplementedError("Missing 'place_via_cage' method for ViaGroundCage")
[docs]
@identityclass
class PolarViaGroundCage(ViaGroundCage):
"""Polar Via Ground Cage
This type implements a polar-coordinate system for defining the vias of the
ground cage.
"""
via_def: type[Via]
"""Via Definition to use for all of the via placements.
"""
count: int
"""Total number of via placements. Must be positive.
"""
radius: float
"""Radius in mm for the circular pattern of via placements. Must be positive.
"""
theta: float = 0.0
"""Starting angle for the pattern. Value in degrees.
Default value is 0.0 degrees which points to the right along the X axis.
"""
skips: Sequence[int] = ()
""" Skipped indices in the via pattern. Each value in this collection
must be in the range `[0, count-1]`
"""
pose: Transform = field(default_factory=Transform.identity)
""" Change the location and placement of the ground cage
with respect to the origin of the via structure. The
default value is the `IDENTITY` transformation.
"""
vias: Sequence[Via] = ()
def __post_init__(self):
assert self.count > 0
assert self.radius > 0.0
for i, skip in enumerate(self.skips):
assert skip < self.count, (
f"Skip[{i}]={skip} Not less than count '{self.count}'"
)
[docs]
def place_via_cage(self, n: Net):
def compute_loc(i: int) -> Transform:
phase = 2.0 * math.pi * i / self.count
phase += math.radians(self.theta)
return Transform.rotate(math.degrees(phase)) * Transform.translate(
self.radius, 0.0
)
valid_pos = [x for x in range(self.count) if x not in self.skips]
via_set = []
for i in valid_pos:
tx = self.pose * compute_loc(i)
v = self.via_def().at(tx)
via_set.append(v)
n += v
self.vias = via_set
[docs]
class AntiPad(Container):
"""Base class for Anti-Pad constructors"""
[docs]
def place_anti_pad(self):
raise NotImplementedError("Missing 'place_anti_pad' method for Antipad")
[docs]
@identityclass
class SimpleAntiPad(AntiPad):
"""Trivial Anti-Pad Generator
This Anti-Pad type generates `KeepOut` shapes on the passed layers
and then positions them according the the `pose` argument.
"""
shape: Shape
""" KeepOut shape to be applied to all requested layers.
"""
layers: LayerSet
""" Set of layers where keepout will be applied.
"""
pose: Transform = field(default_factory=Transform.identity)
""" Optional transform to apply to the antipads so that they can be
positioned with respect to the via-structure's origin.
This value is `Transform.identity` by default.
"""
[docs]
def place_anti_pad(self):
self.KO = KeepOut(self.pose * self.shape, self.layers, pour=True)
[docs]
class ViaStructure(Circuit):
"""Base class for ViaStructure definitions"""
def __init__(
self,
ground_cages: Sequence[ViaGroundCage],
antipads: Sequence[AntiPad],
):
"""Constructor for base class
Args:
ground_cages - Set of zero or more ground cage structures.
antipads - Set of zero or more antipad definitions to apply to the via structure.
insertion_points - Set of zero or more insertion point locators.
"""
# via structures are floating, the vias are in a fixed position inside
self.at(floating=True)
self.ground_cages = ground_cages
self.antipads = antipads
[docs]
def generate_common_structures(self, common: Net):
for gndCage in self.ground_cages:
gndCage.place_via_cage(common)
for antipad in self.antipads:
antipad.place_anti_pad()
[docs]
class SingleViaStructure(ViaStructure):
"""Single-Ended Signal Via structure
This object constructs a Circuit definition that can generate
via structure instances for single-ended signals (ie single `Port` nets).
User must instantiate an via structure instance and net/topo it in the
circuit like a normal component.
"""
sig_in = Port()
sig_out = Port()
COMMON = Port()
def __init__(
self,
via: type[Via] | Via,
*,
ground_cages: Sequence[ViaGroundCage],
antipads: Sequence[AntiPad],
):
"""Construct a single-ended via structure instance
Args:
via: Via or via type that will be become the signal via for the structure.
ground_cages - Set of zero or more ground cage structures.
antipads - Set of zero or more antipad definitions to apply to the via structure.
insertion_points - Set of zero or more insertion point locators.
"""
super().__init__(ground_cages, antipads)
self.GND = Net([self.COMMON])
if isinstance(via, type):
via = via()
self.signal_via = via
self._topo = self.sig_in >> self.signal_via >> self.sig_out
self.generate_common_structures(self.GND)
[docs]
class InsertionPlacement(NamedTuple):
layer: int
offset: float
[docs]
class DifferentialViaStructure(ViaStructure):
"""Differential Pair Via Structure
This object constructs a Circuit definition that can generate a
via structure instance for support a `DiffPair` port net.
User must instantiate an instance of this via structure type and
net/topo it into the circuit like a normal component instance.
"""
sig_in = DiffPair()
sig_out = DiffPair()
COMMON = Port()
def __init__(
self,
vias: type[Via] | tuple[type[Via] | Via, type[Via] | Via],
pitch: float,
*,
ground_cages: Sequence[ViaGroundCage],
antipads: Sequence[AntiPad],
enterOn: int | InsertionPlacement | None = None,
exitOn: int | InsertionPlacement | None = None,
):
"""Construct a new Differential Via Structure instance.
Args:
via_defs: Via Type that will be instantiated to construct
the signal vias for the structure. If this value is a tuple
of 2 Via definitions, then we will use separate via definitions
for the P and N signals, respectively.
pitch: Distance between the P and N signal vias in mm.
ground_cages - Set of zero or more ground cage structures.
antipads - Set of zero or more antipad definitions to apply to the via structure.
"""
super().__init__(ground_cages, antipads)
if not isinstance(vias, tuple):
vias = (vias, vias)
assert len(vias) == 2
assert pitch > 0
def construct(via: type[Via] | Via):
if isinstance(via, type):
return via()
return via
self.via_p = construct(vias[0]).at(0, pitch / 2)
self.via_n = construct(vias[0]).at(0, -pitch / 2)
self._routes: list[Route] = []
topo_p = TopologyNet([self.via_p])
topo_n = TopologyNet([self.via_n])
if enterOn is not None:
if isinstance(enterOn, InsertionPlacement):
layer, offset = enterOn
else:
layer = enterOn
offset = pitch / 2
self.inbound = PairInsertion(layer=layer, invert=True).at(
-offset, 0, rotate=180
)
topo_p = self.inbound.port.p >> topo_p
topo_n = self.inbound.port.n >> topo_n
self._routes.extend(
(
Route(self.inbound.uncoupled.p, self.via_p, layer=layer),
Route(self.inbound.uncoupled.n, self.via_n, layer=layer),
)
)
if exitOn is not None:
if isinstance(exitOn, InsertionPlacement):
layer, offset = exitOn
else:
layer = exitOn
offset = pitch / 2
self.outbound = PairInsertion(layer=layer).at(offset, 0)
topo_p = topo_p >> self.outbound.port.p
topo_n = topo_n >> self.outbound.port.n
self._routes.extend(
(
Route(self.via_p, self.outbound.uncoupled.p, layer=layer),
Route(self.via_n, self.outbound.uncoupled.n, layer=layer),
)
)
self._topo_p = self.sig_in.p >> topo_p >> self.sig_out.p
self._topo_n = self.sig_in.n >> topo_n >> self.sig_out.n
self.GND = Net([self.COMMON])
self.generate_common_structures(self.GND)