| name | jitx-interconnect-constraints |
| description | Use when the user asks about topology using the double-greater-than operator, timing constraints, skew matching, insertion loss limits, differential pairs, DiffPair bundles, protocol constraints for PCIe, USB, DisplayPort, RGMII, Ethernet, DDR, pin models, reference planes, length matching, impedance-controlled routing, or SignalConstraint definitions. Covers TopologyNet, Constrain, ConstrainDiffPair, ConstrainReferenceDifference, DiffPairConstraint, ReferencePlanes, and protocol-specific constraint patterns. |
JITX Interconnect Constraints
Apply signal integrity constraints to signal topologies in JITX Python designs. This skill bridges jitx-circuit-builder (wiring with +) and jitx-substrate-modeler (routing structures) by teaching how to create SI-aware topologies with >> and constrain them.
Environment
Environment setup is handled by the base jitx skill. Ensure it has been invoked first.
Package Architecture
from jitx.si import (
Constrain,
ConstrainDiffPair,
ConstrainReferenceDifference,
DiffPairConstraint,
SignalConstraint,
PinModel,
BridgingPinModel,
TerminatingPinModel,
ReferencePlanes,
RoutingStructure,
DifferentialRoutingStructure,
symmetric_routing_layers,
)
from jitx.net import DiffPair, Port, TopologyNet, Topology
from jitx.common import LanePair
from jitx import Circuit, Net, current
from jitx.toleranced import Toleranced
from jitx.units import ohm
These DO NOT EXIST — never import:
jitx.topology, jitx.constraints.si, jitx.diffpair, jitx.signal,
jitx.si.TopologyNet (it is in jitx.net), jitxlib.si, jitxlib.constraints
Key locations:
DiffPair and TopologyNet are in jitx.net
Topology is in jitx.net (re-exported from jitx.si)
- All constraint classes are in
jitx.si
- Routing structures are in
jitx.si (also used in substrate definitions)
For complete API signatures, see jitx.si API docs.
Anti-string-hacking — read before applying per-lane / per-pair constraint metadata
When applying constraints across N parallel diff-pairs or N parallel lanes, iterate over a typed collection (self.lanes: list[DiffPair]), not a string-keyed dict (self.pairs["TX_b0"]) or sibling attributes plus getattr. The constraints themselves are objects; assemble them inside a typed dataclass or list, never dict[str, Any]. See jitx/references/architectural-patterns.md §§ "String-keyed dicts → structural objects" and "Sibling attributes → array attributes" before writing per-lane constraint application code.
For a same-model self-critique pass on the constraint code after writing (catches what these rules don't), invoke jitx-code-review. Optional for single-task use.
The Critical Distinction: + vs >>
The most important concept for SI-constrained designs:
self.power_net = self.ic.VCC + self.cap.p1
self += self.driver.out >> self.receiver.inp
Storage rules (same as for + nets):
self += self.src >> self.dst
self.my_topo = self.src >> self.dst
self.topos = [self.src >> self.dst]
self.src >> self.dst
When to use which:
+ — Power rails, ground connections, control signals without SI requirements
>> — Any signal with timing, skew, loss, or impedance constraints (clocks, data buses, differential pairs, RF signals)
Basic Topology and Constraints
Step 1: Create topology with >>
self += self.driver.out >> self.receiver.inp
Step 2: Identify topology for constraint application
topo = Topology(self.driver.out, self.receiver.inp)
Step 3: Apply constraints
rs50 = current.substrate.routing_structure(50.0)
self.GND = Net(name="GND")
with ReferencePlanes(self.GND):
self.cst = Constrain(topo).insertion_loss(1.0).structure(rs50)
Constraint methods on Constrain
Constrain(topo).insertion_loss(3.0)
Constrain(topo).timing(500e-12)
Constrain(topo).timing(high=500e-12, low=100e-12)
Constrain(topo).structure(rs50)
self.cst = (
Constrain(topo)
.insertion_loss(1.0)
.timing(500e-12)
.structure(rs50)
)
Multiple signals with same constraint
topos = [Topology(s, d) for s, d in zip(src_pins, dst_pins)]
self.cst = Constrain(topos).insertion_loss(3.0).structure(rs50)
Differential Pairs
The DiffPair bundle
from jitx.net import DiffPair
class MyComponent(jitx.Component):
data = DiffPair()
Constraining differential pairs
self += self.src.data >> self.dst.data
topo = Topology(self.src.data, self.dst.data)
drs100 = current.substrate.differential_routing_structure(100.0)
self.dp_cst = (
ConstrainDiffPair(topo)
.timing_difference(5e-12)
.insertion_loss(3.0)
.structure(drs100)
)
DiffPairConstraint — reusable helper
For applying the same constraint to multiple differential pairs:
from jitx.si import DiffPairConstraint
dp_cst = DiffPairConstraint(
skew=Toleranced(0, 5e-12),
loss=3.0,
structure=drs100,
)
self += self.src.tx >> self.dst.rx
dp_cst.constrain(self.src.tx, self.dst.rx)
self += self.src.clk >> self.dst.clk
dp_cst.constrain(self.src.clk, self.dst.clk)
LanePair — TX + RX bundle
from jitx.common import LanePair
class MySerialPort(Port):
lane = LanePair()
Bus-Level Matching (ConstrainReferenceDifference)
Match multiple data signals to a reference clock or strobe:
self += self.src.clk >> self.dst.clk
clk_topo = Topology(self.src.clk, self.dst.clk)
data_topos = []
for i in range(8):
self += self.src.data[i] >> self.dst.data[i]
data_topos.append(Topology(self.src.data[i], self.dst.data[i]))
self.bus_skew = (
ConstrainReferenceDifference(
guide=clk_topo,
topologies=data_topos,
).timing_difference(Toleranced(0, 11e-12))
)
Key concept: The guide is the reference signal. All topologies are constrained to arrive within the specified timing window relative to the guide.
This pattern is used for:
- RGMII: data-to-clock matching
- DDR: DQ-to-DQS matching, CMD/ADDR-to-CK matching
- PCIe/SFP: inter-lane skew (first lane as reference)
Pin Models
Pin models characterize signal propagation through components for SI analysis.
TerminatingPinModel — IC endpoints
Applied to active component pins where the signal terminates:
class MyIC(jitx.Component):
TX_P = Port()
TX_N = Port()
pm_txp = TerminatingPinModel(TX_P, delay=15e-12, loss=0.2)
pm_txn = TerminatingPinModel(TX_N, delay=15e-12, loss=0.2)
BridgingPinModel — pass-through components
Applied to passive components that a signal passes through (AC coupling caps, series resistors, common-mode chokes):
class BlockingCapacitor(jitx.Component):
p1 = Port()
p2 = Port()
pin_model = BridgingPinModel(p1, p2, delay=6e-12, loss=0.5)
Using BridgingPinModel in a subcircuit
Define a subcircuit with ports that a topology chains through:
class ACCoupler(Circuit):
"""Single-ended AC coupling subcircuit."""
A = Port()
B = Port()
def __init__(self):
self.cap = BlockingCapacitor()
self += self.A >> self.cap.p1
self += self.cap.p2 >> self.B
Differential pair version:
class DiffPairCoupler(Circuit):
"""AC coupling for a differential pair."""
A = DiffPair()
B = DiffPair()
def __init__(self, capacitance=100e-9):
self.cap_p = BlockingCapacitor(capacitance)
self.cap_n = BlockingCapacitor(capacitance)
self.topo_p1 = self.A.p >> self.cap_p.p1
self.topo_p2 = self.cap_p.p2 >> self.B.p
self.topo_n1 = self.A.n >> self.cap_n.p1
self.topo_n2 = self.cap_n.p2 >> self.B.n
Chaining topology through a subcircuit
The outer circuit connects to the subcircuit's ports with >>. The topology chains through the subcircuit's internal >> connections automatically:
class MyDesign(Circuit):
def __init__(self):
self.driver = MyDriver()
self.recv = MyReceiver()
self.coupler = ACCoupler()
self += self.driver.out >> self.coupler.A
self += self.coupler.B >> self.recv.inp
topo = Topology(self.driver.out, self.recv.inp)
with ReferencePlanes(self.GND):
self.cst = Constrain(topo).insertion_loss(3.0)
Topology with bridging components (no subcircuit)
When a topology passes through a component that does not have an embedded BridgingPinModel, define one at the circuit level:
self += self.driver.out >> self.cap.p1
self += self.cap.p2 >> self.receiver.inp
self.bridge = BridgingPinModel(self.cap.p1, self.cap.p2, delay=6e-12, loss=0.5)
topo = Topology(self.driver.out, self.receiver.inp)
self.cst = Constrain(topo).insertion_loss(3.0)
Reference Planes
Reference planes specify which net serves as the return path for each routing layer.
Context manager form (most common)
self.GND = Net(name="GND")
with ReferencePlanes(self.GND):
self.cst = Constrain(topo).structure(rs50)
Per-layer form
with ReferencePlanes({0: self.GND, 1: self.GND, 2: self.PWR}):
self.cst = Constrain(topo).structure(rs50)
On Constrain directly (via structure ref_layers)
self.cst = Constrain(topo).structure(rs50, ref_layers={0: self.GND})
Important: If a routing structure has .reference() definitions (from substrate-modeler), you MUST provide ReferencePlanes. Without them, the constraint will error at build time.
Tag-based routing structures (alternative to Constrain)
Since 4.2, a routing structure can also be applied through the design-rule
system: tag the nets/routes, then attach the structure as a rule effect.
from jitx.constraints import Tag, design_constraint
class HighSpeedTag(Tag):
"Nets that route on the 50-ohm structure."
HighSpeedTag().assign(self.clk_net)
self.hs_rule = design_constraint(HighSpeedTag()).routing_structure(rs50, ref_net=self.GND)
When to choose which:
- Topology
Constrain(...).structure(...) — an ordered >> path exists and
SI constraints (timing, skew, insertion loss) travel with the structure. The
structure applies to the topology paths you enumerate (Constrain takes one
topology or a list — see "Multiple signals with same constraint").
- Tag-based
design_constraint(...).routing_structure(...) — class-of-net
rules: every net or code-based Route carrying the tag gets the structure,
with no per-signal topology authoring. This is also the only way to put a
structure on a plain Net or a code-based Route, neither of which has a
>> topology.
Reference planes for the rule effect resolve through ref_net= (one net for all
reference layers), ref_layer_nets={layer: net}, or an active ReferencePlanes
context — note the kwarg names differ from Constrain.structure(..., ref_layers=). Full signature and the three resolution modes:
jitx-substrate-modeler "Routing structures as a rule effect".
Building Protocol Constraints (SignalConstraint)
For reusable protocol-specific constraint bundles, subclass SignalConstraint[T]:
Pattern: Port bundle + Standard + Constraint
from dataclasses import dataclass
from jitx.si import SignalConstraint, DiffPairConstraint
from jitx.net import DiffPair, Port
from jitx.toleranced import Toleranced
from jitx import current
class MySerialLink(Port):
tx = DiffPair()
rx = DiffPair()
@dataclass(frozen=True)
class MyStandard:
skew: Toleranced = Toleranced(0, 1e-12)
loss: float = 12.0
impedance: Toleranced = Toleranced(100, 10)
class MyConstraint(SignalConstraint["MySerialLink"]):
def __init__(self, standard: MyStandard,
structure: DifferentialRoutingStructure | None = None):
if structure is None:
structure = current.substrate.differential_routing_structure(
standard.impedance
)
self._dp_cst = DiffPairConstraint(
skew=standard.skew, loss=standard.loss, structure=structure
)
def constrain(self, src: MySerialLink, dst: MySerialLink):
self._dp_cst.constrain(src.tx, dst.rx)
self._dp_cst.constrain(dst.tx, src.rx)
Using constrain_topology (context manager)
The constrain_topology method handles topology creation and constraint application together. Use it when inserting components (like AC coupling caps) into the topology:
class MyDesignCircuit(Circuit):
def __init__(self):
self.fpga = FPGA()
self.phy = PHY()
std = MyStandard()
cst = MyConstraint(std)
with cst.constrain_topology(
self.fpga.serial, self.phy.serial
) as (src, dst):
self.coupler = DiffPairCoupler()
self.topos = [
src.tx >> self.coupler.A,
self.coupler.B >> dst.rx,
dst.tx >> src.rx,
]
Hierarchical constraints (DDR4 pattern)
For complex protocols with multiple signal groups, compose constraints:
class DDR4Constraint(SignalConstraint["DDR4"]):
def __init__(self, standard):
self._data_cst = DDR4DataConstraint(standard)
self._acc_cst = DDR4AccConstraint(standard)
def constrain(self, src: DDR4, dst: DDR4):
self._data_cst.constrain(src.data, dst.data)
self._acc_cst.constrain(src.acc, dst.acc)
...
See jitx_protocols_ext for complete protocol implementations including PCIe, SATA, SFP, DDR4, LPDDR4, LPDDR5, and GDDR7.
Working with Built-in Protocols
JITX provides built-in protocol constraints in jitxlib. Check your installed version for available protocols with your Grep tool (pattern class.*Constraint.*SignalConstraint, path .venv, glob *.py); it recurses and is OS-agnostic. Shell fallback: bash grep -rn "class.*Constraint.*SignalConstraint" .venv/lib/python*/site-packages/jitxlib/protocols/ (macOS/Linux); on Windows use the Grep tool, or Select-String over .venv\Lib\site-packages\jitxlib\protocols.
Common built-in protocols:
| Protocol | Import Path | Port Bundle |
|---|
| USB | jitxlib.protocols.usb | USB2, USB3 |
| DisplayPort | jitxlib.protocols.displayport | DisplayPort |
| RGMII | jitxlib.protocols.ethernet.mii.rgmii | RGMII |
| 100Base-TX | jitxlib.protocols.ethernet.mdi.mdi100base_tx | MDI100BaseTX |
| 1000Base-T | jitxlib.protocols.ethernet.mdi.mdi1000base_t | MDI1000BaseT |
For protocol-specific timing parameters (skew, loss, impedance per standard version), see references/protocol-standards.md.
Via Structures in Topologies
Via structures let the constraint engine track signal transitions between layers. They are Circuit subclasses with sig_in, sig_out, and COMMON ports.
from jitxlib.via_structures import SingleViaStructure, PolarViaGroundCage, SimpleAntiPad
These DO NOT EXIST — never import:
jitx.via_structures, jitx.si.ViaStructure, jitxlib.vias
Simple via (bare signal transition)
from jitxlib.via_structures import SingleViaStructure
self.via = SingleViaStructure(
MySubstrate.MicroVia,
ground_cages=[],
antipads=[],
insertion_points=[],
)
self.GND += self.via.COMMON
self += self.driver.out >> self.via.sig_in
self += self.via.sig_out >> self.receiver.inp
topo = Topology(self.driver.out, self.receiver.inp)
rs50 = current.substrate.routing_structure(50.0)
with ReferencePlanes(self.GND):
self.cst = Constrain(topo).structure(rs50).insertion_loss(3.0)
RF via with ground cage
For controlled-impedance via transitions, add a PolarViaGroundCage and optional SimpleAntiPad:
from jitxlib.via_structures import SingleViaStructure, PolarViaGroundCage, SimpleAntiPad
self.rf_via = SingleViaStructure(
MySubstrate.MicroviaL1L2,
ground_cages=[
PolarViaGroundCage(
MySubstrate.BlindViaL1L4,
count=12,
radius=0.6,
)
],
antipads=[SimpleAntiPad(shape, layers)],
insertion_points=[],
)
self.GND += self.rf_via.COMMON
self += self.driver.out >> self.rf_via.sig_in
self += self.rf_via.sig_out >> self.receiver.inp
topo = Topology(self.driver.out, self.receiver.inp)
rs50 = current.substrate.routing_structure(50.0)
with ReferencePlanes(self.GND):
self.cst = Constrain(topo).insertion_loss(1.0).structure(rs50)
Key points
sig_in / sig_out — Signal enters and exits the via structure. Chain these with >> into your topology.
COMMON — Ground connection for the cage vias. Always connect to your GND net with +=.
insertion_points=[] — Required parameter. Pass empty list unless using custom insertion point geometry.
- Positioning — Via structures support
.at(x, y, rotate=angle) for placement.
DifferentialViaStructure — Same pattern but with DiffPair ports (sig_in.p, sig_in.n, etc.) and a pitch parameter for P/N spacing.
For RoutingStructure, DifferentialRoutingStructure, and substrate via definitions, see the jitx-substrate-modeler skill.
Common Mistakes
self.data_net = self.src.data + self.dst.data
self += self.src.data >> self.dst.data
self.src.data >> self.dst.data
self += self.src.data >> self.dst.data
self.src.data = self.dst.data
Constrain(topo).structure(rs50_with_reference)
with ReferencePlanes(self.GND):
Constrain(topo).structure(rs50_with_reference)
topo = Topology(self.src, self.dst)
self += self.src >> self.dst
self += self.src >> self.dst
topo = Topology(self.src, self.dst)
Verification
Step 1: Type Check
pyright path/to/circuit.py
Step 2: Build Test
jitx build <module.path.DesignClass>
Don't run parallel JITX builds against the same project — sequence them. See jitx/SKILL.md "Build Safety".
SI constraint violations appear in the Issues List under "Unsatisfied Signal Constraints" in the JITX UI.
API Reference
For complete class definitions, all parameters, and method signatures:
Formatting
ruff format path/to/circuit.py