jitx-substrate-modeler/SKILL.md
This skill should be used when the user asks to "create a substrate", "define a stackup", "add via definitions", "set up routing structures", "configure impedance control", "define differential pairs", "set fabrication rules", "ring a shape with fence vias", "fence a pour outline", "fence an antipad", or "model a PCB layer structure". Ask the user which fabrication house they are targeting — if they confirm JLCPCB, predefined substrates from jitxlib.jlcpcb (JLC04161H_1080, JLC04161H_7628, JLC06161H_7628) are available with 4/6-layer FR-4, 50/90/100 ohm routing structures, vias, and fab rules. Otherwise, create a custom substrate. Covers Stackup, Symmetric, Conductor, Dielectric, Via (laser, mechanical, backdrilled, blind, buried, stacked), RoutingStructure, DifferentialRoutingStructure, NeckDown, via fencing along traces, fenced pour outlines (Tag + fence_via rule paired with a Pour + optional same-shape KeepOut — covers antipads, RF cavities, BGA breakouts), geometry, reference planes, and FabricationConstraints.
npx skillsauth add jitx-inc/jitx-skills jitx-substrate-modelerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generate complete JITX Python substrate definitions — stackups, materials, vias, routing structures, and fabrication constraints — all in a single file.
If the user has confirmed they are targeting JLCPCB as their fabrication house, predefined substrates from jitxlib.jlcpcb are available. These are production-validated with correct materials, vias, fab rules, and impedance-matched routing structures:
| Class | Layers | Prepreg | Routing Structures | Import |
|-------|--------|---------|-------------------|--------|
| JLC04161H_1080 | 4 | 1080 | RS_50, DRS_90, DRS_100 | from jitxlib.jlcpcb import JLC04161H_1080 |
| JLC04161H_7628 | 4 | 7628 | RS_50, DRS_90, DRS_100 | from jitxlib.jlcpcb import JLC04161H_7628 |
| JLC06161H_7628 | 6 | 7628 | RS_50, DRS_100 | from jitxlib.jlcpcb import JLC06161H_7628 |
Each includes: Symmetric stackup, JLCPCBRules (FabricationConstraints), 11 JLCPCB via definitions (StdVia, StdViaPreferred, MultiLayerVia1-3 + Preferred variants, StdViaTentedFilled for via-in-pad), and routing structures for 50/90/100 ohm impedance targets.
Use directly — no substrate file needed:
from jitxlib.jlcpcb import JLC04161H_1080
substrate = JLC04161H_1080()
# Access routing structures for SI constraints:
# substrate.RS_50, substrate.DRS_90, substrate.DRS_100
When to use predefined: User has explicitly confirmed JLCPCB as fab house + 4 or 6 layer FR-4 + standard impedance targets (50/90/100 ohm). This covers USB, Ethernet, I2C, SPI, I2S, and most common protocols.
When to create custom (use the rest of this skill): User has not confirmed JLCPCB, non-FR-4 materials (Rogers, Megtron), unusual layer count, non-standard impedance, or additional routing structures needed. This is the default path — always create a custom substrate unless the user opts in to a predefined one.
Environment setup is handled by the base jitx skill. Ensure it has been invoked first.
# Core imports — use these exactly
from jitx.stackup import Stackup, Symmetric, Conductor, Dielectric
from jitx.substrate import Substrate, FabricationConstraints
from jitx.via import Via, ViaType, ViaDiameter, Backdrill, BackdrillSet
from jitx.si import RoutingStructure, DifferentialRoutingStructure, symmetric_routing_layers
from jitx.layerindex import Side, LayerSet
from jitx.units import ohm
from jitx.constraints import ViaFencePattern
from jitx.feature import KeepOut, Soldermask
from jitxlib.physics import phase_velocity
from jitx.container import inline
These DO NOT EXIST — never import:
jitx.material, jitx.layer, jitx.routing, jitx.impedance, jitx.pcb,
jitx.dielectric, jitx.conductor, jitxlib.stackup, jitxlib.substrate
Substrate-shaped data (layer-to-via maps, layer-pair tables, per-layer trace widths) belongs on the substrate, queried by the design — not duplicated as design-level constants. The design should write self.substrate.via[(a, b)], not maintain its own _SIGNAL_LAYER_TO_VIA dict. See jitx/references/architectural-patterns.md § "Substrate-shaped tables live on the substrate" before adding per-layer constant tables. Also: instantiate generic substrates (stackup = Generic_Stackup()), don't inline-subclass them (@inline class stackup(Generic_Stackup): pass) — § "Instantiate, don't inline-subclass".
A "generic" substrate must be reusable across designs. Design-specific tags (AntipadFenceTag named after a particular escape design), design-specific trace widths (DESKEW_TRACE_WIDTH), or design-specific fence definitions do not belong in generic_*.py — push them into the consuming design. Comments and docstrings are part of this surface too: a generic substrate must not claim it's tuned for one downstream tool's extraction/export flow (jitx-ansys / HFSS, odb++) or state a fact the code doesn't back — that couples the reusable artifact to one consumer and asserts facts not in evidence. A neutral, evidenced mention of a tool isn't the problem; an unbacked tool-specific suitability claim is.
For a same-model self-critique pass on the substrate after writing (catches what these rules don't), invoke jitx-skills:jitx-code-review. Optional for single-task use.
Everything goes in one Python file per substrate:
# 1. Material definitions (Dielectric, Conductor subclasses)
# 2. Stackup class (Stackup or Symmetric)
# 3. FabricationConstraints class
# 4. Substrate class containing:
# - stackup instance
# - constraints instance
# - Via nested classes (all types needed)
# - RoutingStructure instances
# - DifferentialRoutingStructure instances
Set properties as class attributes, instantiate with thickness.
Soldermask is a Dielectric — define it like any other dielectric material:
class SoldermaskLayer(Dielectric):
"""Soldermask — typically Er ≈ 3.8"""
dielectric_coefficient = 3.8
loss_tangent = 0.02
class FR4_Prepreg(Dielectric):
dielectric_coefficient = 4.4 # Dk (dielectric constant / relative permittivity)
loss_tangent = 0.0168 # Df (dissipation factor)
class FR4_Core(Dielectric):
dielectric_coefficient = 4.6 # Dk
loss_tangent = 0.0168 # Df
class Copper1oz(Conductor):
thickness = 0.035 # mm (can also be set at instantiation)
class CopperHalfOz(Conductor):
thickness = 0.0175 # mm (can also be set at instantiation)
class SoldermaskLayer(Dielectric):
thickness = 0.020
Terminology: dielectric_coefficient is the JITX attribute name for Dk (dielectric constant, also called relative permittivity or Er). loss_tangent is the JITX attribute name for Df (dissipation factor). Datasheets typically specify Dk and Df at a given frequency (e.g., 1 GHz or 10 GHz).
Reference table of common PCB dielectric materials. Values are typical at 10 GHz unless noted. Always confirm with the manufacturer's datasheet for your specific construction.
| Material | Manufacturer | Family | Dk | Df | Notes | |----------|-------------|--------|----|----|-------| | Standard FR-4 | | FR408HR | Isola | High-Tg epoxy | 3.68 | 0.0092 | Workhorse high-Tg FR-4; widely available | | I-Speed | Isola | Low-loss epoxy | 3.64 | 0.0060 | Step down in loss vs standard FR-4 | | N4000-13 EP | AGC/Nelco | High-speed epoxy | 3.60 | 0.0090 | High-speed digital backplanes | | N7000-2HT | AGC/Nelco | High-speed laminate | 3.50 | 0.0090 | Dk/Df available at 2.5 and 10 GHz | | Low-Loss | | I-Tera MT40 | Isola | Very low-loss epoxy | 3.45 | 0.0031 | High-speed digital/RF | | Megtron 6 | Panasonic | Low-loss multilayer | 3.34 | 0.0037 | Common in high-speed digital (at 13 GHz) | | RO4350B | Rogers | Hydrocarbon/ceramic | 3.48 | 0.0037 | Popular RF laminate; FR-4 processable | | RO4003C | Rogers | Hydrocarbon/ceramic | 3.38 | 0.0027 | Standard RF laminate | | 25N | Arlon | Ceramic-filled woven glass | 3.38 | 0.0025 | Low loss with standard FR-4 processes | | Ultra-Low-Loss | | Astra MT77 | Isola | Ultra-low-loss | 3.00 | 0.0017 | RF/microwave and very-high-speed | | Tachyon 100G | Isola | Ultra-low-loss | ~3.05 | ~0.0017 | Values vary by construction | | Megtron 7 | Panasonic | Ultra-low-loss | varies | varies | Capture exact row for glass style/resin | | PTFE / RF | | RT/duroid 5880 | Rogers | Glass microfiber PTFE | 2.20 | 0.0009 | Ultra-low loss; microwave/RF | | RT/duroid 5870 | Rogers | Glass microfiber PTFE | 2.33 | 0.0012 | Low Dk/loss; antennas/stripline | | RO3003 | Rogers | Ceramic-filled PTFE | 3.00 | 0.0010 | Low loss PTFE; common RF choice | | RO3035 | Rogers | Ceramic-filled PTFE | 3.50 | 0.0015 | PTFE with Dk ~3.5 | | TLY-5A | Taconic/AGC | Low-loss PTFE | 2.17–2.40 | ~0.0009 | Selectable Dk range | | TLX-0 | Taconic/AGC | Fiberglass PTFE | 2.45 | 0.0012 | Lowest Dk in TLX series | | High-Dk (miniaturization) | | RO3006 | Rogers | Ceramic-filled PTFE | 6.15 | 0.0020 | Higher Dk for size reduction | | RO3010 | Rogers | Ceramic-filled PTFE | 10.20 | 0.0022 | High Dk for compact RF | | CER-10 | Taconic/AGC | Organic-ceramic | 10.0 | 0.0035 | High Dk; check tolerances per lot |
Copper surface roughness affects insertion loss at high frequencies. Choose foil type based on your frequency range.
Rz values below are for the matte/bonding side (the side laminated to the dielectric core), which is the surface that dominates conductor loss. The drum/resist side is typically 2–5× smoother; use its Ra value when modelling the top surface of a trace.
| Copper Type | Rz — Matte/Bonding Side | Rz — Drum/Resist Side | Use Case | |-------------|-------------------------|-----------------------|----------| | Standard HTE (STD) | 5–10 μm | 3–5 μm | <1 GHz, general FR-4 inner layers | | Reverse Treated Foil (RTF) | 5–10 μm | 3–5 μm | <5 GHz; adhesion treatment moves to drum side | | Low Profile (LP / LoPro) | 2–4 μm | 1–2 μm | 1–10 GHz signal layers | | Very Low Profile (VLP) | 2.5–5 μm | 1–2 μm | 5–25 Gbps; Megtron 6, Isola IS415/FR408HR | | Hyper VLP (HVLP / SVLP) | 1–3 μm | 0.5–1 μm | 25–56 Gbps; high-speed SerDes | | Ultra Low Profile (ULP) | 0.5–1.5 μm | 0.3–0.5 μm | >56 Gbps, mmWave (>24 GHz) | | Rolled Annealed (RA) | 0.3–0.8 μm | 0.3–0.8 μm | RF/microwave, flex circuits; both sides smooth |
Rule of thumb: For signals above 5 GHz, use LP or smoother. Above 10 GHz, use VLP. For 25 Gbps+, use HVLP. For mmWave (>24 GHz) or >56 Gbps, use ULP or RA.
Cannonball-Huray parameters (for HFSS/EM simulation using the average HCPES+SCPES model):
a = 0.0573 × Rz (µm)Sr = 5.117 (constant, independent of foil type)| Copper Type | Representative Rz (µm) | Nodule radius a (µm) | |-------------|------------------------|----------------------| | STD HTE | 8.0 | 0.458 | | RTF | 6.0 | 0.344 | | LP / LoPro | 3.0 | 0.172 | | VLP | 3.5 | 0.201 | | HVLP | 2.0 | 0.115 | | ULP | 1.0 | 0.057 | | RA | 0.5 | 0.029 |
Define top half only — bottom auto-mirrors. Last layer MUST be dielectric (symmetry plane):
class My4LayerStackup(Symmetric):
soldermask = SoldermaskLayer(thickness=0.015)
top = Copper1oz()
prepreg = FR4_Prepreg(thickness=0.076)
inner = CopperHalfOz()
core = FR4_Core(thickness=1.265) # center — MUST be dielectric
Top-to-bottom order. Named attributes or list. Give copper layers informative names describing their function (signal, ground, power) — these appear in the JITX UI and help users navigate the design:
class My8LayerStackup(Stackup):
top_mask = SoldermaskLayer(thickness=0.02)
L8 = ThinCopper(name="L8-Patch")
sub7 = Prepreg326(thickness=0.068)
L7 = ThinCopper(name="L7-GND3")
sub6 = Prepreg322(thickness=0.104)
L6 = ThinCopper(name="L6-Signal")
# ... all layers top to bottom ...
L2 = ThinCopper(name="L2-GND1")
sub1 = Prepreg325(thickness=0.068)
L1 = ThickCopper(name="L1-Signal")
bottom_mask = SoldermaskLayer(thickness=0.02)
class MySubstrate(Substrate):
@inline
class stackup(Symmetric):
soldermask = SoldermaskLayer(thickness=0.015)
top = Copper1oz()
prepreg = FR4_Prepreg(thickness=0.076)
inner = CopperHalfOz()
core = FR4_Core(thickness=1.265)
Define as nested classes inside Substrate. All properties are ClassVar.
class THVia(Via):
type = ViaType.MechanicalDrill
start_layer = 0 # Side.Top also works
stop_layer = -1 # Side.Bottom also works
diameter = 0.45 # pad diameter (mm)
hole_diameter = 0.3 # drill hole (mm)
class THViaFilled(Via):
type = ViaType.MechanicalDrill
start_layer = Side.Top
stop_layer = Side.Bottom
diameter = 0.45
hole_diameter = 0.3
tented = True
filled = True
via_in_pad = True
class MicroVia_L1_L2(Via):
type = ViaType.LaserDrill
start_layer = 0
stop_layer = 1
diameter = 0.356
hole_diameter = 0.178
filled = True
via_in_pad = True
class StackedVia_L1_L3(Via):
type = ViaType.LaserDrill
start_layer = 0
stop_layer = 2
diameter = 0.356
hole_diameter = 0.178
filled = True
via_in_pad = True
class BuriedVia_L3_L12(Via):
type = ViaType.MechanicalDrill
start_layer = 2
stop_layer = 11
diameter = 0.356
hole_diameter = 0.178
filled = True
Backdrill depth is set via stop_layer — set it to the target signal layer, then use BackdrillSet to remove the stub. The backdrill side is opposite to the signal entry:
bd = Backdrill(
diameter=0.5, startpad_diameter=0.7,
solder_mask_opening=0.8, copper_clearance=0.6,
)
class BackdrilledVia_L3(Via):
"""Signal enters from top, connects at L3 — backdrill from bottom removes stub"""
type = ViaType.MechanicalDrill
start_layer = Side.Top
stop_layer = 3 # target signal layer controls backdrill depth
diameter = 0.6
hole_diameter = 0.3
filled = True
via_in_pad = True
backdrill = BackdrillSet(bottom=bd) # backdrill from opposite side
Dual backdrill (both sides) — incredibly uncommon, almost never needed:
backdrill = BackdrillSet(
top=Backdrill(diameter=0.5, startpad_diameter=0.7,
solder_mask_opening=0.8, copper_clearance=0.6),
bottom=Backdrill(diameter=0.5, startpad_diameter=0.7,
solder_mask_opening=0.8, copper_clearance=0.6),
)
class AdvancedVia(Via):
type = ViaType.MechanicalDrill
start_layer = 0
stop_layer = -1
diameter = 0.6
hole_diameter = 0.3
diameters = {
0: 0.5,
1: ViaDiameter(0.5, nfp=0.2), # non-functional pad on layer 1
}
from jitx.si import PinModel
class ModeledVia(Via):
# ... standard attributes ...
models = {
(0, -1): PinModel(5e-12, 0.05), # top-to-bottom: 5ps delay, 0.05dB loss
(0, 1): PinModel(2e-12, 0.02),
}
RS_50 = RoutingStructure(
impedance=50 * ohm,
layers=symmetric_routing_layers({
0: RoutingStructure.Layer(
trace_width=0.12, # mm
clearance=0.2, # mm
velocity=phase_velocity((4.4 + 1) / 2), # mm/s — microstrip effective Dk
insertion_loss=0.018, # dB/mm
)
}),
)
from jitxlib.physics import phase_velocity
vel_microstrip = phase_velocity((Dk + 1) / 2) # microstrip effective Dk
vel_stripline = phase_velocity(Dk) # stripline uses full Dk
vel_mixed = phase_velocity((Dk_pp + Dk_core) / 2) # mixed dielectric
velocity must be in mm/s, NOT m/s. phase_velocity() returns mm/s. Passing a raw m/s value will be 1000x too small, producing wrong timing constraints.
# WRONG — velocity in m/s (1000x too small, timing constraints will be wrong)
velocity = 1.5e8 # m/s — DO NOT USE
# CORRECT — always use phase_velocity() which returns mm/s
velocity = phase_velocity(4.2) # returns ~1.46e11 mm/s
Define top half only — mirrors to bottom using -layer - 1 index:
layers = symmetric_routing_layers({
0: RoutingStructure.Layer(...), # → also creates layer -1
2: RoutingStructure.Layer(...), # → also creates layer -3
})
RoutingStructure.Layer(
trace_width=0.15, clearance=0.1,
velocity=vel, insertion_loss=0.05,
neck_down=RoutingStructure.NeckDown(
trace_width=0.09, clearance=0.075,
),
)
RoutingStructure.Layer(
trace_width=0.203, clearance=0.076,
velocity=phase_velocity(1.99), insertion_loss=0.05,
).fence(
MicroVia_L1_L2, # via class
ViaFencePattern(
pitch=0.4, # via-to-via spacing along route
offset=0.43, # trace center to via center
num_rows=1,
),
reference_layer=1, # ground reference for fence net
)
Offset formula: offset = trace_width/2 + gap + via_pad_radius
RoutingStructure.Layer(
trace_width=0.12, clearance=0.08,
velocity=phase_velocity(3.26), insertion_loss=0.08,
)
.geometry(Soldermask, 0.25, side=Side.Top) # soldermask opening
.geometry(KeepOut, 1.2, layers=LayerSet(1), pour=True) # keepout on layer 1
.reference(2, 1.0) # reference plane on layer 2
.fence(FenceViaClass, ViaFencePattern(pitch=0.5, offset=0.35, num_rows=1),
reference_layer=2)
DRS_100 = DifferentialRoutingStructure(
name="100 Ohm Differential",
impedance=100 * ohm,
layers=symmetric_routing_layers({
0: DifferentialRoutingStructure.Layer(
trace_width=0.09,
pair_spacing=0.137, # edge-to-edge between P and N
clearance=0.2,
velocity=vel,
insertion_loss=0.018,
)
}),
uncoupled_region=RoutingStructure(
name="50 Ohm SingleEnded, Uncoupled",
impedance=50 * ohm,
layers=symmetric_routing_layers({
0: RoutingStructure.Layer(
trace_width=0.09, clearance=0.2,
velocity=vel, insertion_loss=0.018,
)
}),
),
)
Differential with NeckDown (for BGA escape or constrained areas):
DRS_100_ND = DifferentialRoutingStructure(
name="100 Ohm Differential w/ NeckDown",
impedance=100 * ohm,
layers=symmetric_routing_layers({
0: DifferentialRoutingStructure.Layer(
trace_width=0.09,
pair_spacing=0.137,
clearance=0.2,
velocity=vel,
insertion_loss=0.018,
neck_down=DifferentialRoutingStructure.NeckDown(
trace_width=0.075,
pair_spacing=0.1,
clearance=0.15,
),
)
}),
uncoupled_region=RoutingStructure(
name="100 Ohm Differential w/ NeckDown, Uncoupled",
impedance=50 * ohm, # half of differential impedance
layers=symmetric_routing_layers({
0: RoutingStructure.Layer(
trace_width=0.09, clearance=0.2,
velocity=vel, insertion_loss=0.018,
neck_down=RoutingStructure.NeckDown(
trace_width=0.075, clearance=0.15,
),
)
}),
),
)
Multi-layer differential (different trace widths per layer):
DRS_82 = DifferentialRoutingStructure(
impedance=82 * ohm,
layers=symmetric_routing_layers({
0: DifferentialRoutingStructure.Layer(
trace_width=0.154, pair_spacing=0.2,
clearance=0.23, velocity=VEL, insertion_loss=0.018,
),
2: DifferentialRoutingStructure.Layer(
trace_width=0.137, pair_spacing=0.15,
clearance=0.21, velocity=VEL, insertion_loss=0.018,
),
}),
uncoupled_region=RoutingStructure(
impedance=41 * ohm,
layers=symmetric_routing_layers({
0: RoutingStructure.Layer(trace_width=0.154, clearance=0.15,
velocity=VEL, insertion_loss=0.018),
2: RoutingStructure.Layer(trace_width=0.137, clearance=0.15,
velocity=VEL, insertion_loss=0.018),
}),
),
)
All values in mm.
class MyFabRules(FabricationConstraints):
min_copper_width = 0.09 # minimum trace width
min_copper_copper_space = 0.09 # minimum copper spacing
min_copper_hole_space = 0.254 # copper-to-hole spacing
min_copper_edge_space = 0.3 # copper-to-board-edge
min_annular_ring = 0.13 # via annular ring
min_drill_diameter = 0.3 # minimum drill hole
min_hole_to_hole = 0.5 # hole-to-hole spacing
min_pitch_leaded = 0.217 # leaded package pitch
min_pitch_bga = 0.377 # BGA pitch
max_board_width = 500
max_board_height = 400
min_silkscreen_width = 0.153
min_silk_solder_mask_space = 0.15
min_silkscreen_text_height = 1.0
solder_mask_registration = 0.05
min_soldermask_opening = 0.0
min_soldermask_bridge = 0.08
min_th_pad_expand_outer = 0.2
min_pth_pin_solder_clearance = 0.0
Custom attributes are allowed for fab-house-specific rules (not engine-enforced).
This section defines the rules (design_constraint(...)) a tag triggers. Choosing
which layout objects to tag and why — fanout/escape tags on package escapes,
direct-connect on high-current pads, tagging a code-based Route — is covered in the
jitx-physical-layout subskill.
For net-to-net clearances and via stitching rules:
from jitx.constraints import Tag, design_constraint
class RFSignalTag(Tag): pass
class GNDTag(Tag): pass
# Trace width for tagged nets (unary constraint — single tag)
self.rule1 = design_constraint(RFSignalTag(), priority=1).trace_width(0.102)
# Net-to-net clearance (binary constraint — two tags)
self.rule2 = design_constraint(RFSignalTag(), RFSignalTag()).clearance(1.05)
self.rule3 = design_constraint(RFSignalTag(), GNDTag()).clearance(0.15)
Board-wide defaults belong on the Design class, not the substrate. The four canonical defaults — trace width, copper clearance, thermal relief, wider power/ground — go in self.rules on the top-level Design via UnaryDesignConstraint(IsTrace) / BinaryDesignConstraint(IsCopper, IsCopper) / UnaryDesignConstraint(IsPad) / UnaryDesignConstraint(PowerTag() | GroundTag(), priority=1). See jitx/references/project-builder-flow.md "Default design rules" for the full pattern. The substrate's FabricationConstraints are the fab-minimum floor; the Design rules are the production-friendly defaults that sit above the floor.
design_constraint(...) and UnaryDesignConstraint(...) / BinaryDesignConstraint(...) are equivalent — the lowercase form is a factory that returns the right subtype based on arity. Use either.
Tags form a hierarchy through class inheritance, and a rule on a base tag applies to every subclass tag. This is a first-class JITX feature, not a trick — a tag can subclass another tag, not just Tag:
class FenceTag(Tag): pass
class AntipadFenceTag(FenceTag): pass # subclass of FenceTag
class DeskewAntipadFenceTag(AntipadFenceTag): pass # subclass of AntipadFenceTag
# Applies to ALL fence tags — antipad, deskew, and any future FenceTag subclass:
self.fence_clearance = design_constraint(FenceTag(), GNDTag()).clearance(0.15)
# Applies only to the deskew variant; give it higher priority to override the base
# rule where they overlap (higher priority wins when multiple rules match):
self.deskew_fence = design_constraint(DeskewAntipadFenceTag(), priority=10).fence_via(...)
A net/pour/object tagged DeskewAntipadFenceTag() matches rules written against DeskewAntipadFenceTag, AntipadFenceTag, and FenceTag. Where two matching rules conflict, the higher priority= wins — that's how a specific subtag rule overrides the general base-tag rule. (Tags also combine with & / | / ~ and Tag.any(...) when a hierarchy isn't the right shape.)
Flat tag proliferation is a smell. A row of near-identical sibling tags that all inherit straight from Tag and differ only by name — each wired to its own rule that mostly restates the others — usually wants one of:
NeckDownTag, plus a higher-priority rule for the one neckdown level that's special), ordesign_constraint(TagA() | TagB())…) when the tags aren't really distinct concepts.Reach for many flat tags only when the rule sets are genuinely distinct. Mapping a spreadsheet of per-combination rules into a flat tag-per-row table is the usual way this goes wrong — the hierarchy expresses the same intent with far fewer rules.
Rule conditions are not limited to tags you define:
IsCopper, IsTrace, IsPour, IsVia, IsPad,
IsBoardEdge, IsThroughHole, IsNeckdown, IsHole (import from
jitx.constraints or top-level jitx). The engine matches them by object
kind; they are conditions only — assign() on a builtin raises
TypeError. The four canonical Design defaults use these
(see jitx/references/project-builder-flow.md "Default design rules").OnLayer(index) — layer-scoped condition (import from jitx.constraints;
not re-exported top-level). OnLayer.external() matches the top and bottom
copper layers; OnLayer.internal() is its inverse.AnyObject — matches everything; useful as the second condition of a
binary rule.& / | / ~, and n-ary
Tag.any(*tags) / Tag.all(*tags).from jitx.constraints import design_constraint, AnyObject, OnLayer
# Wider high-speed traces on external layers only:
self.hs_outer = design_constraint(HighSpeedTag() & OnLayer.external()).trace_width(0.15)
# Keep everything 0.3 mm away from tagged power copper:
self.pwr_keepaway = design_constraint(PowerTag(), AnyObject).clearance(0.3)
Which objects can carry a tag (Net, TopologyNet, Copper, Pour, Route,
Component, Circuit, Landpattern, Pad, Via, ControlPoint), container
inheritance (tagging a landpattern tags its pads), and tagging self to tag all
instances of a class are covered in jitx-physical-layout "Layout-intent tags".
A rule's effects are chainable methods; one rule can set several. The arity
boundary: unary rules (one condition) chain any effect below except
clearance; binary rules (two conditions) support only .clearance().
Everything a design_constraint(...) can do (all dimensions in mm):
| Effect | Signature | Notes |
|---|---|---|
| Trace width | .trace_width(width) | example above |
| Clearance | .clearance(clearance) | binary rules only — design_constraint(cond1, cond2) |
| Via fencing | .fence_via(via_cls, ViaFencePattern(...)) | along traces/pour outlines — see "Fenced Pour Outlines" below |
| Via stitching | .stitch_via(via_cls, grid) | grid = SquareViaStitchGrid(pitch=, inset=) or TriangularViaStitchGrid(pitch=, inset=); inset = boundary-to-outermost-via-center distance |
| Thermal relief | .thermal_relief(gap_distance, spoke_width, num_spokes) | pad-to-pour connections |
| Serpentine params | .serpentine_params(min_radius=, min_pitch=) | bend radius / segment pitch of length-matching serpentines |
| Coupled-pair params | .coupled_pair_params(deskew_bump_radius=, skew_tolerance=, min_bump_spacing=, max_bump_length=, long_lookahead=) | deskew-bump geometry for diff pairs; skew_tolerance is in mm (distance, not time — the time-domain skew budget lives in jitx-interconnect-constraints) |
| Pour feature size | .pour_feature_size(min_width) | clips pour regions not coverable by a circle of min_width diameter fully inside the pour (sliver removal; thermal-relief spokes excluded) |
| Routing structure | .routing_structure(rs, ...) | see below |
from jitx.constraints import design_constraint, SquareViaStitchGrid, IsPour
# Stitch tagged ground pours on a 2 mm square grid:
self.gnd_stitch = design_constraint(GNDPourTag()).stitch_via(
GndVia, SquareViaStitchGrid(pitch=2.0, inset=0.5)
)
# Board-wide pour sliver removal:
self.no_slivers = design_constraint(IsPour).pour_feature_size(min_width=0.3)
.routing_structure(...) assigns an impedance-controlled structure (defined on
this substrate) to every trace matching the condition — including plain Nets
and code-based Routes that have no >> topology. Reference planes resolve one
of three ways:
# (a) one net references every reference layer the structure declares:
self.hs = design_constraint(HighSpeedTag()).routing_structure(self.RS_50, ref_net=gnd)
# (b) per-layer mapping:
self.hs = design_constraint(HighSpeedTag()).routing_structure(
self.DRS_100, ref_layer_nets={1: gnd, 4: gnd}
)
# (c) neither argument — requires an active jitx.si.ReferencePlanes context,
# else ValueError at rule-construction time.
The keyword names are ref_net / ref_layer_nets (not reference_*); passing
both raises ValueError. For ordered point-to-point paths where the structure
travels with timing/loss constraints, the topology-based
Constrain(...).structure(...) flow is usually the better fit — the choice is
covered in jitx-interconnect-constraints "Tag-based routing structures".
Trick for placing fence vias along an arbitrary closed shape — antipad rings around signal-via pairs, RF cavity perimeters, BGA breakout boundaries, deskew arc cutouts. Three pieces compose:
design_constraint(...).fence_via(...) declaring that any pour carrying the Tag gets fence vias of the given class placed along it.from jitx.constraints import Tag, design_constraint, ViaFencePattern
class FenceOutlineTag(Tag):
"""Pours with this tag get fence vias along their outline."""
class MySubstrate(Substrate):
# ... other vias ...
class uGndStitch(Via):
"""Example fence via — adjust to your fab's microvia capability."""
type = ViaType.LaserDrill
start_layer = 0
stop_layer = 6
diameter = 0.25
hole_diameter = 0.1
filled = True
_FENCE_PATTERN = ViaFencePattern(
pitch=0.35, # via-to-via spacing along each row
offset=0.15, # row-to-row spacing; also the default boundary-to-first-row offset
num_rows=1,
)
outline_fence_rule = design_constraint(
FenceOutlineTag(), priority=20
).fence_via(uGndStitch, _FENCE_PATTERN)
ViaFencePattern.input_shape_only (pour-only, defaults to True) controls which pour shape gets fenced — the pre-isolation input outline (default) or the post-isolation computed copper. Leaving it default is correct for nearly every fenced-outline case; set False only if downstream clearance rules will reshape the pour and the fence vias should track the reshaped boundary.
The tagged pour must sit on a conductor layer the fence via reaches — fence vias inherit the pour's net, so the pour has to be on a layer they can land on. Typically the pour goes on the reference/termination layer being fenced (here, the via's stop_layer = 6, so the pour goes on layer=6).
from jitx import Pour
from jitx.feature import KeepOut
from jitx.layerindex import LayerSet
# `shape` is the outline to fence — e.g. a capsule around a signal-via pair,
# an RF cavity perimeter, a BGA breakout boundary, or a deskew arc cutout.
fence_pour = Pour(shape, layer=6)
FenceOutlineTag().assign(fence_pour)
self.GND += fence_pour
# Add this ONLY when the pour copper itself is unwanted (cavity / antipad opening).
# Omit when the pour doubles as a real GND region.
self.fence_outline_keepout = KeepOut(shape, layers=LayerSet(6), pour=True, via=True)
Do not set isolate= on the fence Pour — it's legacy. Pour clearance is governed by FabricationConstraints + Tag-based design_constraint(...).clearance(...).
Reuse via definitions across substrates:
class MyVias:
class StdVia(Via): ...
class StdViaFilled(Via): ...
class SubstrateA(Substrate, MyVias):
stackup = StackupA()
constraints = RulesA()
class SubstrateB(Substrate, MyVias):
stackup = StackupB()
constraints = RulesB()
0 / Side.Top = top conductor1 = second conductor from top-1 / Side.Bottom = bottom conductor-2 = second from bottomsymmetric_routing_layers maps layer i to -i - 1Dielectric and Conductor subclasses with Dk/Df/roughnessSymmetric for symmetric boards, Stackup for asymmetricFabricationConstraints with all manufacturing rulesRoutingStructure and DifferentialRoutingStructure for each impedance targetdesign_constraint() for clearances if neededpyright type check, then jitx build with a test design (sequence builds — don't parallelize against the same project; see jitx/SKILL.md "Build Safety")For complete class definitions, all parameters, method signatures, and additional examples, see JITX Documentation.
ruff format path/to/substrate.py
development
This skill should be used when the user asks to author PCB physical layout from code — "draw copper from code", "create an antenna" / "filter copper" / "net tie" / "overlapping copper", build a "custom shape" or board outline with shapely, add a "custom pad shape", "soldermask/paste opening", or "thermal pad with vias", "place vias from code" / "attach a via to a pad", apply "fanout / escape tags" or "direct-connect / thermal-relief" to layout objects, or (advanced) add "control points" and "code-based routes" for escape routing or deskew. Covers shapely shape creation feeding ANY feature, Copper vs OverlappableCopper vs Pour, pad features (Soldermask/Paste/SMDPadConfig/thermal_pad), PortAttachment + explicit placement, layout-intent tags, and the Route / control-point API (RoutePoint / PairInsertion / PairPoint, stable as of JITX 4.2). For stackup, via definitions, routing structures, fence-via rules, and fenced pour outlines use jitx-substrate-modeler; for net wiring, passives, and basic pours use jitx-circuit-builder.
data-ai
Mechanical CAD interface for JITX designs. Use when the user asks to import DXF, EMN, IDF, IDX, or BDF mechanical data; set a board outline from mechanical CAD; export a JITX board to DXF; attach STEP models; export board STEP; or work with mechanical CAD data.
development
Same-model self-critique pass for JITX Python code just written in this workspace. Use when the user asks to "review my JITX code", "self-critique", "check JITX conventions", "find string-hacking", "check framework-boundary issues", "audit before merge", or any equivalent. Mandatory for complete-board tier at task acceptance (folds into Think Twice); user-invoked for single-task work. Catches the architectural failure modes that grep gates and static linters miss — parallel string-keyed models, reflection-as-iteration (regardless of whether on self), owner-shaped data misplaced in design code, build-spec-then-iterate, module-import-time parallel models, and framework-boundary-bypass (the "framework does it, therefore so can I" trap). Applies an ownership test to every banned-pattern hit or proposed exception. Produces severity-tagged findings (CRITICAL / WARNING / NOTE) that fold into the task acceptance block.
development
Base skill for JITX hardware design workflow. Use for JITX Python projects, PCB design, circuit creation, and build commands. Use when the user asks to "build my JITX design", "set up JITX environment", "create a circuit", "build a complete board", "design a PCB from requirements", or "create a full JITX project". For multi-component designs (3+ components, substrate, circuits), invoke the Project Builder workflow for orchestrated parallel agent execution with quality gates. CRITICAL - If user asks to create/model/generate a component or mentions a part number (NE555, LM1117, RP2040, etc.), immediately invoke jitx-component-modeler subskill. If user asks to create a substrate, stackup, via definitions, or routing structures, invoke jitx-substrate-modeler subskill. If user asks to author physical layout from code — draw copper, antennas, filters, or net-ties; build custom shapes with shapely; add pad/soldermask/paste/thermal-pad features; place vias or routes from code; or apply fanout/escape/direct-connect layout tags — invoke jitx-physical-layout subskill. If user asks to import DXF/EMN/IDF/IDX/BDF mechanical data, set board outline from mechanical, export STEP, add a 3D model, export JITX board XML to DXF, or work with mechanical CAD data, invoke jitx-mechanical subskill.