Source code for jitx.si

"""
Signal integrity constraints and modeling
=========================================

This module provides classes for defining signal integrity constraints,
routing structures, and pin models.
"""

from __future__ import annotations

from abc import abstractmethod
from contextlib import contextmanager
from dataclasses import dataclass, replace
from collections.abc import Iterable, Mapping, Sequence
from typing import Any, Self, cast, overload

from jitx._structural import Critical, Structural, Structurable
from jitx.circuit import CurrentCircuit
from jitx.context import Context
from jitx.feature import (
    Custom,
    Cutout,
    Feature,
    KeepOut,
    MultiLayerFeature,
    SurfaceFeature,
)
from jitx.inspect import extract
from jitx.layerindex import Layers, LayerSet, Side
from jitx.net import Net, DiffPair, Port, TopologyNet, Topology, _PORT, _match_port_type
from jitx.constraints import FenceVia, ViaFencePattern

from logging import getLogger

from jitx.shapes.primitive import Empty
from jitx.toleranced import Toleranced
from jitx.units import PlainQuantity, ohm
from jitx.via import Via


logger = getLogger(__name__)


[docs] class SignalConstraint[T: Port](Structural): """ SignalConstraint is the primary way to apply signal integrity constraints in a design. They are meant to encapsulate a set of conditions that generate the constraints for a given signal or set of signals. While it is possible to apply constraints directly to a signal topology, it is recommended to define a reusable ``SignalConstraint`` from specifications instead. Various examples of SignalConstraint implementations can be found in :py:mod:`JITX Standard Library protocols <jitxlib.protocols>`. This is a base class, create a subclass and override :py:meth:`constrain` to implement your own signal constraint. """ def __init__(self): self.constrains: list[BaseConstrain] = []
[docs] def add(self, *constrain: BaseConstrain) -> None: self.constrains.extend(constrain)
[docs] @abstractmethod def constrain(self, src: T, dst: T): """Called to apply implementation specific constraints. Note that all constraints should be added to the container by calling :py:meth:`~SignalConstraint.add` on self. It is valid to just set member attributes as well, as with everything in JITX, but be aware that your constraint may be used multiple times with different topologies, and thus be careful with overwriting values (e.g. some constraints may use a single :py:class:`DiffPairConstraint` to constrain all its diff pairs). """ pass
def __find_signal_end(self, p: T, q: T) -> T: # XXX finding the signal end is useless for pin assigned topologies - # does it even make sense to find it for any case if we need to handle # the pin-assigned case anyway? # XXX should check that we aren't actually already starting at a component port # which would perhaps mean we need to delay this until the end? # TODO traverse BridgingPinModels visited: set[TopologyNet] = set() if cc := CurrentCircuit.get(): for tn in extract(cc.circuit, TopologyNet): if tn not in visited: visited.add(tn) # if both p and q are in the same sequence, it's connecting the two, # so look for something else. if p in tn.sequence and q not in tn.sequence: # p in sequence means tn must be [T] tn = cast(TopologyNet[T], tn) if p == tn.sequence[0]: p = tn.sequence[-1] elif p == tn.sequence[-1]: p = tn.sequence[0] else: logger.warning( "Port is in the middle of a topology while looking for signal end." ) return p else: logger.warning("No active circuit while applying topology constraint") return p
[docs] def constrain_topology(self, src: T, dst: T): """Construct topology and apply the constraint end-to-end. >>> class MyCircuit(Circuit): ... a = ComponentCircuit() ... b = ComponentCircuit() ... cst = MyDiffPairConstraint() ... def __init__(self): ... with self.cst.constrain_topology(self.a.require(DiffPair), self.b.require(DiffPair)) as (src, dst): ... # insert circuitry between src and dst here. ... self += src >> dst """ # ensure this runs even if the context manager is never entered. srcex = self.__find_signal_end(src, dst) dstex = self.__find_signal_end(dst, src) self.constrain(srcex, dstex) @contextmanager def constrain_topology(): yield (src, dst) return constrain_topology()
[docs] class DiffPairConstraint(SignalConstraint[DiffPair]): """Basic signal constraint for a differential pair signal topology. Provides a simple declration of skew and insertion loss requirements for a diff pair signal. Often used inside a more complex protocol signal constraint.""" def __init__( self, skew: Toleranced, loss: float, structure: DifferentialRoutingStructure | None = None, ): super().__init__() self.skew = skew self.loss = loss self.structure = structure
[docs] def constrain(self, src: DiffPair, dst: DiffPair): super().constrain(src, dst) self.add( cdp := ConstrainDiffPair(Topology(src, dst)) .timing_difference(self.skew) .insertion_loss(self.loss) ) if self.structure: cdp.structure(self.structure)
[docs] class PinModel(Critical): """Simple model base class to describe signal propagation behavior as fixed delay and loss.""" delay: Toleranced loss: Toleranced def __init__(self, delay: float | Toleranced, loss: float | Toleranced): if isinstance(delay, float | int): delay = Toleranced.exact(delay) if isinstance(loss, float | int): loss = Toleranced.exact(loss) self.delay = Toleranced.exact(delay) self.loss = Toleranced.exact(loss)
[docs] class BridgingPinModel[T: Port](PinModel, Structural): """A pin model the describes the signal propagation through a component from one port to another at the frequency of interest. Ideally this is included in the component definition, but can also be added to the circuit when used, if it doesn't already have one. Args: portA: The first port of the pin model. portB: The second port of the pin model. delay: The propagation delay through the component in seconds. loss: The insertion loss through the component in dB. >>> class MyComponent(Component): ... a = Port() ... b = Port() ... pin_model = BridgingPinModel(a, b, delay=6e-12, loss=3) >>> class MyCircuit(Circuit): ... r = Resistor() ... def __init__(self): ... self.aux_pin_model = BridgingPinModel(self.r.a, self.r.b, delay=6e-12, loss=3) """ ports: tuple[T, T] @overload def __init__( self, portA: T, portB: T, /, *, delay: float | Toleranced, loss: float | Toleranced, ): ... @overload def __init__( self, ports: tuple[T, T], /, *, delay: float | Toleranced, loss: float | Toleranced, ): ... def __init__( self, portA: T | tuple[T, T], portB: T | None = None, /, *, delay: float | Toleranced, loss: float | Toleranced, ): super().__init__(delay=delay, loss=loss) if isinstance(portA, tuple): assert portB is None self.ports = portA else: assert portB is not None self.ports = portA, portB
[docs] class TerminatingPinModel[T: Port](PinModel, Structural): """A pin model the describes the signal propagation from a component port into the relevant part of the component at frequency of interest. Ideally this is included in the component definition, but can also be added to the circuit when used, if it doesn't already have one. Args: port: The port of the pin model. delay: The propagation delay into the component in seconds. loss: The insertion loss into the component in dB. >>> class MyComponent(Component): ... a = Port() ... pin_model = TerminatingPinModel(a, delay=6e-12, loss=3) >>> class MyCircuit(Circuit): ... ic = MyComponent() ... def __init__(self): ... self.aux_pin_model = TerminatingPinModel(self.ic.a, delay=6e-12, loss=3) """ port: T def __init__(self, port: T, *, delay: float, loss: float): super().__init__(delay=delay, loss=loss) self.port = port
[docs] @dataclass class Constraint(Critical): """Base class for all signal integrity constraints.""" pass
[docs] class BaseConstrain(Structural): """Base class for applying constraints to signal topologies.""" _constraints: list[Constraint | RoutingStructureConstraint] """List of constraints applied to this topology.""" def __init__(self): super().__init__() self._constraints = [] def _individual(self) -> Iterable[Topology]: raise NotImplementedError( "Please use a concrete constraint strategy, e.g. Constrain" ) @overload def insertion_loss(self, high: float, low: float = 0): ... @overload def insertion_loss(self, window: Toleranced, /): ...
[docs] def insertion_loss(self, high: float | Toleranced, low: float = 0): """Apply an insertion loss constraint to the signal topology. Parameters: high: Maximum insertion loss allowed. low: Minimum insertion loss allowed. Default is 0. """ if isinstance(high, Toleranced): low = high.min_value high = high.max_value self._constraints.append(InsertionLossConstraint(low, high)) return self
@overload def timing(self, high: float, low: float = 0): ... @overload def timing(self, window: Toleranced, /): ...
[docs] def timing(self, high: Toleranced | float, low: float = 0): """Apply a timing constraint to the signal topology. Parameters: high: Maximum delay allowed. low: Minimum delay allowed. Default is 0. """ if isinstance(high, Toleranced): low = high.min_value high = high.max_value self._constraints.append(TimingConstraint(low, high)) return self
[docs] class Constrain(BaseConstrain): """Constrain a single signal topology. This is most commonly used inside a :py:class:`SignalConstraint` (which encapsulates more complex constraint configurations), and not to constrain a signal topology directly.""" topologies: Sequence[Topology] """Signal topologies to constrain.""" _structure: RoutingStructureConstraint | None = None """Optional routing structure constraint.""" def __init__(self, topologies: Topology | Iterable[Topology]): super().__init__() self.topologies = ( (topologies,) if isinstance(topologies, Topology) else tuple(topologies) ) def _individual(self) -> Iterable[Topology]: return self.topologies
[docs] def structure( self, structure: RoutingStructure, *, ref_layers: Mapping[int, RefLayerType] | None = None, ): """Apply a routing structure constraint to the signal topology. Parameters: structure: The routing structure to apply. ref_layers: A mapping of layers to reference plane nets. """ self._structure = RoutingStructureConstraint(structure, ref_layers=ref_layers) return self
[docs] class BaseConstrainPairwise(BaseConstrain): """Constrain signal topologies pairwise. This is a base class that declares the :py:meth:`_pairwise` method.""" _pairwise_constraints: list[Constraint | DifferentialRoutingStructureConstraint] def __init__(self): super().__init__() self._pairwise_constraints = [] def _individual(self) -> Iterable[Topology]: # redefined to make a better error message raise NotImplementedError( "Please use a concrete difference constraint strategy, e.g. ConstrainReferenceDifference" ) def _pairwise(self) -> Iterable[tuple[Topology, Topology]]: raise NotImplementedError( "Please use a concrete difference constraint strategy, e.g. ConstrainReferenceDifference" ) @overload def timing_difference(self, high: float, low: float | None = None) -> Self: ... @overload def timing_difference(self, window: Toleranced, /) -> Self: ...
[docs] def timing_difference( self, high: Toleranced | float, low: float | None = None ) -> Self: """Apply a timing difference constraint between two signal topologies. Parameters: high: Maximum timing difference allowed. low: Minimum timing difference allowed. If not provided, it is set to -high. """ if isinstance(high, Toleranced): low = high.min_value high = high.max_value elif low is None: low = -high self._pairwise_constraints.append(TimingDifferenceConstraint(low, high)) return self
[docs] class ConstrainReferenceDifference(BaseConstrainPairwise): """Constrain multiple signals to a single reference signal. Timing difference constraints will be applied to all signals, using the guide as reference. Timing and loss constraints apply to all, including the guide.""" guide: Topology[Port] topologies: Sequence[Topology] def __init__( self, guide: Topology[Port], topologies: Topology | Iterable[Topology] ): super().__init__() self.guide = guide if not guide.begin.is_single_pin(): # XXX should diff-pairs be explicitly allowed? raise ValueError("Guide must be a single pin type") self.topologies = ( (topologies,) if isinstance(topologies, Topology) else tuple(topologies) ) def _individual(self) -> Iterable[Topology]: yield self.guide yield from self.topologies def _pairwise(self) -> Iterable[tuple[Topology, Topology]]: guide = self.guide for topo in self.topologies: yield guide, topo
[docs] class ConstrainDiffPair(BaseConstrainPairwise): """Apply constraints within diff-pair signals. Timing difference constraints will effectively be skew constraints. Timing and loss apply to all P and N individually.""" topologies: Sequence[Topology[DiffPair]] _structure: DifferentialRoutingStructureConstraint | None = None def __init__(self, topologies: Topology[DiffPair] | Iterable[Topology[DiffPair]]): super().__init__() self.topologies = ( (topologies,) if isinstance(topologies, Topology) else tuple(topologies) ) def _individual(self) -> Iterable[Topology]: for dp in self.topologies: yield Topology(dp.begin.p, dp.end.p) yield Topology(dp.begin.n, dp.end.n) def _pairwise(self) -> Iterable[tuple[Topology, Topology]]: for dp in self.topologies: yield Topology(dp.begin.p, dp.end.p), Topology(dp.begin.n, dp.end.n)
[docs] def structure( self, structure: DifferentialRoutingStructure, *, ref_layers: Mapping[int, RefLayerType] | None = None, ): """Apply a differential routing structure constraint to the signal topology. Parameters: structure: The differential routing structure to apply. ref_layers: A mapping of layers to reference plane nets. """ self._structure = DifferentialRoutingStructureConstraint( structure, ref_layers=ref_layers ) return self
[docs] @dataclass class TimingConstraint(Constraint): """ A timing constraint that can be applied to a signal topology. The `min_delay` and `max_delay` are relative delays limits in units of seconds. """ min_delay: float """Minimum delay in seconds.""" max_delay: float """Maximum delay in seconds."""
[docs] @dataclass class InsertionLossConstraint(Constraint): """ An insertion loss constraint that can be applied to a signal topology. The `min_loss` and `max_loss` are relative attenuation limits in units of dB. """ min_loss: float """Minimum insertion loss in decibels.""" max_loss: float """Maximum insertion loss in decibels."""
# TODO: Ensure max_loss > min_loss? (And likewise check for other constraints?)
[docs] @dataclass class TimingDifferenceConstraint(Constraint): """ A timing difference constraint that can be applied between two signal topologies. The `min_delta` and `max_delta` are relative skew limits in units of seconds between the two signal routes to be constrained. It is typical in most applications for `min-delta` to be negative and `max-delta` to be positive. """ min_delta: float """Minimum timing difference in seconds.""" max_delta: float """Maximum timing difference in seconds."""
[docs] class RoutingStructure: """Routing structure definition. A :py:func:`symmetric_routing_layers` function is available to create a symmetric routing structure by specifying just half of the layers, assuming that it's valid to use symmetric values. >>> class MySubstrate(Substrate): ... RS_50 = RoutingStructure( ... impedance=50, ... layers={ ... 0: RoutingStructure.Layer(trace_width=0.2, velocity=150e6, insertion_loss=0.01), ... 1: RoutingStructure.Layer(trace_width=0.2, velocity=150e6, insertion_loss=0.01), ... -2: RoutingStructure.Layer(trace_width=0.2, velocity=150e6, insertion_loss=0.01), ... -1: RoutingStructure.Layer(trace_width=0.2, velocity=150e6, insertion_loss=0.01), ... } ... ) >>> class MySubstrate(Substrate): ... RS_50 = RoutingStructure( ... impedance=50, ... layers=symmetric_routing_layers({ ... 0: RoutingStructure.Layer(trace_width=0.2, velocity=150e6, insertion_loss=0.01), ... 1: RoutingStructure.Layer(trace_width=0.2, velocity=150e6, insertion_loss=0.01), ... }) ... ) """
[docs] @dataclass(kw_only=True) class NeckDown: """Neck down parameters for routing layer.""" trace_width: float | None = None clearance: float | None = None insertion_loss: float | None = None velocity: float | None = None
[docs] @dataclass(kw_only=True) class Layer(Critical): """Routing layer definition.""" trace_width: float """Width of traces on this layer.""" velocity: float """Velocity of signal for purposes of timing constraints, in millimeters per second.""" insertion_loss: float """Insertion loss of traces on this layer, in decibels per millimeter.""" clearance: float | None = None """Minimum clearance to other objects on this layer, in millimeters.""" neck_down: RoutingStructure.NeckDown | None = None """Routing parameters to apply in neckdown regions.""" def __post_init__(self): self.__reference: list[_RefLayer] = [] self.__geometry: list[_AddGeom] = [] self.__fence: _StructureViaFence | None = None def _invert(self): ob = replace(self) ob.__reference = [rl._invert() for rl in self.__reference] ob.__geometry = [g._invert() for g in self.__geometry] if self.__fence: ob.__fence = self.__fence._invert() return ob @overload def geometry[T: SurfaceFeature]( self, feature: type[T], width: float, *, side: Side ) -> Self: ... @overload def geometry[T: MultiLayerFeature]( self, feature: type[T], width: float, *, layers: Layers ) -> Self: ... @overload def geometry( self, feature: type[KeepOut], width: float, *, layers: Layers, pour=False ) -> Self: ... @overload def geometry( self, feature: type[Custom], width: float, *, side: Side, name: str ) -> Self: ... @overload def geometry(self, feature: type[Cutout], width: float) -> Self: ...
[docs] def geometry(self, feature: type[Feature], width: float, **kwargs) -> Self: """Add routing structure geometry for this routing layer. The generated feature will follow the shape if the created route, with the desired width, and can be on layers other than the current one. This is particularly useful if generating KeepOut voids in between this layer and a reference layer. .. note:: It's currently not supported to generate route KeepOuts. Args: feature: The feature type to add. desired_width: The desired width of the feature that will follow the generated route. """ if "layers" in kwargs: # allow construction of LayerSet from Layers. The only "layers" # argument in Feature is LayerSet; this is valid as long as # that's true. It would be cleaner to add this as input # preprocessing in MultiLayerFeature, but dataclasses don't # have good support for that. kwargs["layers"] = LayerSet(kwargs["layers"]) self.__geometry.append(_AddGeom(feature(Empty(), **kwargs), width)) return self
# Can be done with a paramspec (without overloads), which will provide # correct type checking, but won't provide language server suggestions, # so let's go with overloads instead. # def geometry[**P](self, feature: Callable[Concatenate[Shape, P], Feature], desired_width: float, *args: P.args, **kwargs: P.kwargs) -> Self: # self.__geometry.append(AddGeom(feature(Empty(), *args, **kwargs), desired_width)) # return self @overload def reference(self, layer: int, /, desired_width: float) -> Self: ... @overload def reference(self, layers: Mapping[int, float], /) -> Self: ...
[docs] def reference( self, layer: int | Mapping[int, float], desired_width: float | None = None ) -> Self: """Specify that there should be a reference plane on the given layer, with the desired width surrounding the route. An attempt will be made to ensure there is a contiguous reference plane on that layer, but it is not guaranteed. Args: layer: The layer number of the reference plane. desired_width: The desired width of the reference plane surrounding the route. layers: Alternatively, a mapping of layer numbers to desired widths can be used to declare multiple reference planes at once. """ if isinstance(layer, Mapping): for layeridx, width in layer.items(): self.__reference.append(_RefLayer(layeridx, width)) elif desired_width is not None: self.__reference.append(_RefLayer(layer, desired_width)) else: raise TypeError("Must specify desired_width if layer is not a mapping") return self
@overload def fence( self, fence: FenceVia, /, *, reference_layer: int | None = None ) -> Self: ... @overload def fence( self, via: type[Via], pattern: ViaFencePattern, *, reference_layer: int | None = None, ) -> Self: ...
[docs] def fence( self, via: type[Via] | FenceVia, pattern: ViaFencePattern | None = None, *, reference_layer: int | None = None, ) -> Self: """Specify that there should be a via fence around the perimeter of the route. Unless otherwise specified, the vias will be given the same net as the first reference plane specified in the routing structure. Args: via: The Via class to use for fencing. pattern: The geometric pattern for fence placement. fence: Alternatively, a prepared FenceVia can be used to specify the via and pattern in one go. reference_layer: An optional layer index to use as reference for the via fence. The generated vias will be given the same net as the reference plane on that layer. Note that this does not imply that a this layer is a reference plane for the signal. Note that the via fence will be generated regardless whether there actually is a reference plane on this layer. This can parameter can also be used, for example, to generate a via fence using planes far away from the signal (e.g. using through-hole vias), but are not actually reference planes, should that ever be necessary. """ if reference_layer is None: if not self.__reference: raise ValueError( "No reference plane specified in the routing" " structure, and none provided, cannot specify a via" " fence without a reference plane." ) reference_layer = self.__reference[0].layer if isinstance(via, FenceVia): pattern = via.pattern via = via.definition elif pattern is None: raise TypeError("Must specify pattern if no FenceVia specified") self.__fence = _StructureViaFence(via, pattern, reference_layer) return self
layers: Mapping[int, Layer] impedance: PlainQuantity __name: str | None = None def __init__( self, layers: Mapping[int, Layer] | None = None, *, impedance: PlainQuantity | float, name: str | None = None, ): if layers is not None: self.layers = layers else: # assume these have been set by a subclass if not hasattr(self, "layers"): raise TypeError( "RoutingStructure must be constructed with a mapping of layers, either directly or via a subclass field." ) self.impedance = ohm.from_(impedance, strict=False, name="impedance") self.__name = name @property def name(self) -> str: return self.__name or self.__class__.__name__ def __repr__(self): if self.__name is None: return f"{self.__class__.__name__}(impedance={self.impedance:g~P})" else: return f"RoutingStructure(name={self.name}, impedance={self.impedance:g~P})" def __str__(self): return self.name
[docs] class DifferentialRoutingStructure: """Differential routing structure definition."""
[docs] @dataclass(kw_only=True) class NeckDown(RoutingStructure.NeckDown): """Neck down parameters for differential routing layer.""" pair_spacing: float | None = None
[docs] @dataclass(kw_only=True) class Layer(RoutingStructure.Layer): """Differential routing layer definition.""" pair_spacing: float """Internal spacing within the differential pair, in millimeters."""
layers: Mapping[int, Layer] uncoupled_region: RoutingStructure | None = None impedance: PlainQuantity __name: str | None = None def __init__( self, layers: Mapping[int, Layer] | None = None, uncoupled_region: RoutingStructure | None = None, *, impedance: PlainQuantity | float, name: str | None = None, ): if layers is not None: self.layers = layers else: # assume these have been set by a subclass if not hasattr(self, "layers"): raise TypeError( "DifferentialRoutingStructure must be constructed with a collection of layers," " either as constructor argument or using a subclass field." ) if uncoupled_region is not None: self.uncoupled_region = uncoupled_region self.impedance = ohm.from_(impedance, strict=False, name="impedance") self.__name = name @property def name(self) -> str: return self.__name or self.__class__.__name__ def __repr__(self): return f"DifferentialRoutingStructure(name={self.name}, impedance={self.impedance:g~P})" def __str__(self): return self.name
@dataclass class _StructureViaFence(FenceVia): """A via fence that is generated around a signal topology.""" reference_layer: int | None = None """The layer index to use as reference for the via fence. The generated vias will be given the same net as the reference layer. Note that this does not imply that a this layer is a refernce plane for the signal. While likely the case, the via fence will be generated regardless of whether there is a reference plane on this layer. If not specified, the reference layer will be the first reference plane specified in the routing structure. If no such reference plane is speicified, an error will be generated.""" _inverted: bool = False def _invert(self): start = self.definition.start_layer end = self.definition.stop_layer if start >= 0 != end >= 0: if -start - 1 != end: raise ValueError( "Can only symmetrize via fences that use symmetric vias" ) copy = replace( self, reference_layer=None if self.reference_layer is None else -self.reference_layer - 1, ) copy._inverted = not self._inverted return copy type RefLayerType = Net[Port] def _reference_layers( ref_layers: Mapping[int, RefLayerType] | RefLayerType | None, structure: RoutingStructure | DifferentialRoutingStructure, container: Any, ) -> Mapping[int, RefLayerType]: # container is just for error reporting. all_nets: RefLayerType | None = None modifiable = {} if ref_layers: if isinstance(ref_layers, Mapping): for net in ref_layers.values(): _match_port_type([net, _PORT], container) ref_layers = dict(ref_layers) else: _match_port_type([ref_layers, _PORT], container) all_nets = ref_layers ref_layers = modifiable else: planes = ReferencePlanes.get() if not planes: ref_layers = {} elif planes.all: all_nets = planes.all ref_layers = modifiable else: ref_layers = planes.layers for slayer in structure.layers.values(): for rl in extract(slayer, _RefLayer): if all_nets: modifiable[rl.layer] = all_nets elif rl.layer not in ref_layers: raise ValueError( f"Reference plane layers do not declare the required layer {rl.layer} from {structure}" ) for svf in extract(slayer, _StructureViaFence): layer = svf.reference_layer if layer is None: raise ValueError("Unexpected missing reference layer for via fence") if all_nets: modifiable[layer] = all_nets elif layer not in ref_layers: raise ValueError( f"Reference plane layers do not declare the required via fence reference layer {layer} from {structure}" ) return ref_layers
[docs] class ReferencePlanes(Context, Structurable): """A context for specifying net assignments for reference layers used by routing structures. Nets of reference layers can also be specified directly when applying the routing structure, but this context allows a simple mechanism for declaring reference planes higher up in the design hierarchy. >>> class MyCircuit(Circuit): ... GND = Net() ... planes = ReferencePlanes({0: GND, 1: GND}) ... c = MyComponent() ... def __init__(self): ... self += self.c.p[0] >> self.c.p[1] Or if different reference planes are required for different parts of the design, the context can be activated separately. Note that this form can only be used inside the constructor function and will have no effect if used in the class variable context due to how JITX structural elements are instantiated. >>> class MyCircuit(Circuit): ... GND1 = Net() ... GND2 = Net() ... c = MyComponent() ... protocol = MyProtocolConstraint() ... def __init__(self): ... with ReferencePlanes(self.GND1): ... with self.protocol.constrain_topology(selc.c.p[0].to(self.c.p[1])) as src, dst: ... self += src >> dst ... with ReferencePlanes(self.GND2): ... with self.protocol.constrain_topology(selc.c.p[0].to(self.c.p[1])) as src, dst: ... self += src >> dst Args: layers: A mapping of layer indices to nets. The nets will be assigned to the reference layers of the routing structure. all: Optionally a single net can be specified which will used for all reference layers. """ all: RefLayerType | None = None layers: Mapping[int, RefLayerType] @overload def __init__(self, layers: Mapping[int, RefLayerType], /): ... @overload def __init__(self, all: RefLayerType, /): ... def __init__(self, layers: Mapping[int, RefLayerType] | RefLayerType, /): if isinstance(layers, Mapping): for net in layers.values(): _match_port_type([net, _PORT], self) self.layers = dict(layers) else: _match_port_type([layers, _PORT], self) self.all = layers self.layers = {}
[docs] class RoutingStructureConstraint(Structurable): """A constraint to apply a routing structure to a signal topology.""" structure: RoutingStructure ref_layers: Mapping[int, RefLayerType] def __init__( self, structure: RoutingStructure, *, ref_layers: Mapping[int, RefLayerType] | None = None, ): self.structure = structure self.ref_layers = _reference_layers(ref_layers, structure, self)
[docs] class DifferentialRoutingStructureConstraint(Structurable): """A constraint to apply a differential routing structure to a signal topology.""" structure: DifferentialRoutingStructure ref_layers: Mapping[int, RefLayerType] def __init__( self, structure: DifferentialRoutingStructure, *, ref_layers: Mapping[int, RefLayerType] | None = None, ): self.structure = structure self.ref_layers = _reference_layers(ref_layers, structure, self)
[docs] def symmetric_routing_layers[T: RoutingStructure.Layer]( layers: Mapping[int, T], invert_geometry=True ) -> dict[int, T]: """Create a symmetric routing structure from a dictionary of routing layers. Args: layers: Half of a routing structure layer set. invert_geometry: If True, the geometry of the inverted layers will be flipped appropriayle. If not, the geometry will be left as is. Note that via fences cannot currently be inverted unless they're using a via that's symmetric around the center of the board, since there's no practical way to specify a via for fencing on the other side of the board. """ ret = dict(layers) for layer in layers: if invert_geometry: ret[-layer - 1] = layers[layer]._invert() else: ret[-layer - 1] = layers[layer] return ret
@dataclass class _RefLayer(Critical): layer: int desired_width: float # required width is not yet supported # required_width: float | None = None def _invert(self): return replace(self, layer=-self.layer - 1) @dataclass class _AddGeom(Critical): feature: Feature width: float def _invert(self): return replace(self, feature=self.feature.invert())