| name | jitx-physical-layout |
| description | Use when the user asks to author PCB physical layout from code: draw copper, antennas, filters, net ties, custom shapes, board outlines, custom pads, soldermask or paste openings, thermal pads with vias, code-placed vias, fanout or escape tags, direct-connect or thermal-relief tags, control points, code-based routes, escape routing, or deskew. Covers shapely geometry, Copper, OverlappableCopper, Pour, pad features, PortAttachment, explicit placement, layout-intent tags, and Route control-point APIs. Use jitx-substrate-modeler for stackups, vias, routing structures, fence-via rules, and fenced pours; use jitx-circuit-builder for net wiring, passives, and basic pours. |
JITX Physical Layout
Author physical layout geometry — copper, custom shapes, pad features, explicit
placement, code-driven vias/routes, and layout-intent tags — directly in Python.
This is the layer between schematic-level wiring (jitx-circuit-builder) and
stackup/fab definition (jitx-substrate-modeler).
JITX is a moving target — APIs on this page have been renamed across releases
(most recently the control-point classes in 4.2.0). Do not rely on prior JITX
knowledge — verify every import and signature with pyright against the
installed package, and build-test control-point/route geometry before
trusting it.
Scope — what this skill owns vs neighbors
| You want to… | Skill |
|---|
| Draw copper shapes, antennas, filters, net-ties, custom board/pad geometry | this skill |
| Build a shape with shapely and feed it to any feature | this skill |
| Add soldermask/paste/thermal-pad features, place vias/components from code | this skill |
| Tag layout objects (fanout, escape, direct-connect) for selection | this skill (rule mechanics → substrate-modeler) |
| Code-based routes & control points (escape lanes, deskew) | this skill (advanced — see reference) |
| Wire nets, add passives, voltage dividers, basic pours | jitx-circuit-builder |
| Define the stackup, vias, routing structures, fence-via rules, fenced pour outlines | jitx-substrate-modeler |
| Author a component's package/landpattern from a datasheet | jitx-component-modeler |
Topology (>>), timing/skew/impedance constraints | jitx-interconnect-constraints |
This skill covers the design-side geometry and placement; the substrate owns the
rule definitions (design_constraint(...), via classes, routing structures) that
act on it. Where they meet, this skill cross-references rather than restating.
Environment
Environment setup is handled by the base jitx skill — invoke it first.
Imports
from jitx import Copper, Pour, current
from jitx.feature import OverlappableCopper
from jitx.feature import Soldermask, Paste, Silkscreen, Courtyard, Custom, Cutout, KeepOut
from jitx.shapes import Shape
from jitx.shapes.shapely import ShapelyGeometry
from jitx.shapes.composites import rectangle, capsule, notch_rectangle, chipped_circle
from jitx.shapes.primitive import Circle, Polygon, Text
from jitx.anchor import Anchor
from jitx.layerindex import Side, LayerSet
import shapely
from jitx.net import Port, PortAttachment
from jitx.constraints import Tag, Tags
from jitxlib.landpatterns.pads import SMDPadConfig
from jitx.circuit import Route
from jitx.controlpoint import RoutePoint, PairInsertion, PairPoint
Do NOT import (these do not exist): jitx.copper.OverlappableCopper
(it lives in jitx.feature), jitx.shapes.Shapely, jitx.geometry,
jitx.layout, jitx.routes. When unsure, search the installed source with your Grep tool (pattern class OverlappableCopper|class Route|class PortAttachment, path .venv, glob *.py); it recurses and is OS-agnostic. Shell fallback: bash grep -rn "class OverlappableCopper\|class Route\|class PortAttachment" .venv/lib/python*/site-packages/jitx/ (macOS/Linux); on Windows use the Grep tool, or Select-String over .venv\Lib\site-packages\jitx.
Custom shapes with shapely (general)
Shapely is general shape creation for any JITX feature that takes a Shape —
copper, pours, keepouts, the board outline, courtyards, and pad soldermask/paste.
It is not pad-specific.
Reach for a built-in composite first for common shapes — they stay exact
(rectangle, capsule, Circle, notch_rectangle, chipped_circle, bullseye,
equilateral_triangle, …). Use shapely when you need CSG (union / difference /
intersection), buffering, fillets, or arbitrary polygons.
import shapely
from jitx.shapes.shapely import ShapelyGeometry
ring = shapely.box(-5, -5, 5, 5).difference(shapely.box(-4, -4, 4, 4))
shape = ShapelyGeometry(ring)
from jitx.shapes.composites import rectangle
expanded = rectangle(2, 1).to_shapely().buffer(0.1, cap_style="square", join_style="mitre")
ShapelyGeometry supports set operators & (intersection) | (union) -
(difference) ^ (symmetric difference) and .buffer(). A morphological open
(buffer(-r).buffer(r)) rounds sharp inside corners — useful for paste cells.
Validity caveat — guard before feeding a fab feature. JITX serializes only
non-empty Polygon / MultiPolygon geometries. Shapely operations can produce
empty geometries, LineStrings, or GeometryCollections — those raise at build
time. After CSG, confirm the result is a non-empty polygon:
g = ring
assert not g.is_empty and g.geom_type in ("Polygon", "MultiPolygon"), g.geom_type
Arcs are polygonized at a tolerance when converted to/from shapely; keep an eye on
vertex counts for large numbers of circular features.
Copper: Pour vs Copper vs OverlappableCopper
Three ways to put copper on a layer — pick by net membership and whether the
copper is allowed to overlap other copper:
| Construct | On a net? | Overlap-exempt? | Use for |
|---|
Pour(shape, layer, *, rank=0, orphans=True) | yes (net += Pour(...)) | no | filled planes / shaped fills |
Copper(shape, layer) | yes (net += Copper(...) or a + Copper(...)) | no | an explicit copper shape on one net |
OverlappableCopper(shape, layer) | no (netless) | yes | net-tie copper bridging two nets' pads, antenna radiators, filter copper — ignored by the router and overlap checks |
Copper(..., exempt=True) was removed in 4.2.0 — there is no on-net,
overlap-exempt copper anymore. Overlap-tolerant copper is OverlappableCopper,
which is netless: its connectivity comes from the pads it overlaps.
Copper lives in jitx (top-level / jitx.copper); OverlappableCopper lives in
jitx.feature.
from jitx import Copper, Pour
from jitx.feature import OverlappableCopper
self.GND += Pour(current.design.board.shape, layer=0)
self.SIG += Copper(rectangle(10, 0.5).at(0, 5), layer=0)
OverlappableCopper is netless. Its electrical connection comes from the pads it
overlaps, not from the copper itself. A net-tie is the minimal case: the bridging
shape is OverlappableCopper drawn across pads on the two nets it ties. The
antenna pattern is the same idea at full size: give the structure a
small Component with anchor pads that are on the nets (so the router has
something to land on), then draw the radiating shape as OverlappableCopper
overlapping those pads:
self.ant = AntennaIFA().at(0, 0)
self.ANT_FEED += self.ant.feed
self.GND += self.ant.short
self.copper_radiator = OverlappableCopper(radiator_shape, layer=0)
Note OverlappableCopper is not in the set of tag-able objects (see Tags below).
The full antenna example is in references/layout-examples.md.
Pad features (soldermask / paste / thermal pad)
Custom Pad subclasses (KiCad-converted footprints, mechanical pads) get no
default soldermask or paste — the pad is unsolderable until you add them. Two
mechanisms, kept distinct:
(a) Feature objects added to a Pad. Surface features take (shape, side=Side.Top):
from jitx.feature import Soldermask, Paste
mask = Soldermask(expanded_copper_shape, side=Side.Top)
paste = Paste(copper_shape)
(b) SMDPadConfig driving a landpattern generator. Each of copper /
soldermask / paste takes a Shape (or a float expansion, None to skip,
or a ShapeAdjustment like paste subdivision). Omit a field (default ...) for
standard behavior. Pass the config to landpattern.thermal_pad(shape, config=):
from jitxlib.landpatterns.pads import SMDPadConfig
config = SMDPadConfig(soldermask=opening_shape, paste=opening_shape)
landpattern.thermal_pad(shape=rectangle(3.45, 3.45), config=config)
A soldermask-defined thermal pad (shapely CSG webs + via dams, a cheap-fab
alternative to filled via-in-pad) is a complete worked example in
references/layout-examples.md. Authoring the package/landpattern itself from a
datasheet belongs to jitx-component-modeler; this skill is the feature mechanics.
Explicit placement & via attachment
Scope rule — PortAttachment is for signal topologies only: binding a signal
port to a control point, or to a signal via in an escape/deskew path. For
ground/power stitching vias, thermal vias, and anything else that just joins a
net, add the placed via (or copper) to the net — Net accepts Copper | Via
members directly. PortAttachment use is deliberately being limited and is
expected to be deprecated; default to the net form whenever the connection is
plain net membership.
via_cls = substrate.signal_via[layer]
self.thermal_vias = [via_cls().at(x, y) for (x, y) in via_positions]
for via in self.thermal_vias:
self.GND += via
For the signal-topology cases, PortAttachment(port_or_ports, attachment) connects
a port (or a sequence of ports) to a placed Copper | Via | ControlPoint
at a fixed location:
from jitx.net import PortAttachment
self.attachments = [PortAttachment(self.serdes.TX.p, via_cls().at(x, y))]
Vias are defined in the substrate, not here (see jitx-substrate-modeler). This
skill places instances of them. Define a custom module-scope Via
subclass only with a fab-verified reason — e.g. the tented-unfilled thermal via in
the thermal-pad example, where JLCPCB charges nothing for tented vias inside a pad.
Placement — place a direct descendant with .at(); use Circuit.place() sparingly.
self.led = LED().at(10.0, 5.0, rotate=90)
self.led_b = LED().at(10.0, 5.0, on=Side.Bottom)
self.subckt = MySub().at(floating=True)
self.x = MyChip()
self.place(self.x, (1.0, 0.0), relative_to=self.led)
.at() mutates the instance's own transform, so the placement is readable on the instance —
visible to introspection before the design is built. For a direct descendant placed in the
parent's frame, set the position with .at() (chained when you create it, self.x = Comp().at(x, y), or self.x.at(x, y) if it already exists) — not self.place(self.x, (x, y)).
When a placed component and the geometry attached to it share the same local
frame (pin the anchor with .at(0, 0), give attachments offsets in that frame), they
move together under interactive placement. Use .at(0, 0) here, not place(): place() records a
deferred placement request (and force-floats a placed subcircuit), so the anchor is not pinned in
the parent frame alongside its copper. Store attachments/vias/lanes as list or
dataclass attributes — never getattr(self, f"via_{i}") (see Anti-string-hacking).
Keepouts that shape pours
KeepOut(shape, layers=LayerSet(...), pour=, via=, route=) — at least one of
pour / via / route must be True or it is a no-op (the constructor warns):
pour=True — keep pours out of this area
via=True — block the auto-router from dropping vias here
route=True — disallow auto-router traces here
A higher-rank Pour of the same shape fills back over a keepout — the pattern
for a deliberately-shaped local ground island (e.g. around an antenna) that the
board-wide pour is otherwise cleared from.
Local vs global pours. Board-wide return-path pours belong in the top-level
design (the convention in jitx-circuit-builder). The exception: a local
pour/keepout that must track a placed sub-circuit — like an antenna's ground
island — lives inside that circuit so it follows the circuit wherever it is
placed. To ring an arbitrary shape with fence vias (antipads, RF cavities, BGA
breakouts), see jitx-substrate-modeler "Fenced Pour Outlines".
Layout-intent tags (object selection)
Use tags to mark which physical objects get special layout treatment; the
rule that defines the treatment (width, clearance, thermal relief, fence vias,
routing structure) is declared in the substrate or top-level design via
design_constraint(...) — see jitx-substrate-modeler.
Apply with MyLayoutTag().assign(obj) or Tags(tag_a, tag_b).assign(obj1, obj2) —
always a Tag subclass you define, never the bare Tag base — inside a
design/circuit context. Supported object types: Net, TopologyNet, Copper,
Pour, Route, Component, Circuit, Landpattern, Pad, Via,
ControlPoint — note OverlappableCopper is not taggable.
Tagging a container tags the copper objects inside it — tag a Landpattern
and every pad in it inherits the tag; same for a Component or Circuit. Tag
self in a class __init__ to tag all instances of that class. Assignment
outside a design-relevant context emits a RuntimeWarning and has no effect, and
assigning a BuiltinTag raises TypeError (builtins are rule conditions only —
see jitx-substrate-modeler).
Common layout-intent tag roles (define the Tag subclasses in your design):
- Fanout / escape (
PinFanoutTag, PowerPinFanoutTag, BootstrapFanoutTag) —
local neckdown/escape for fine-pitch package pins, applied to short route segments
or individual pads, overriding the board-wide width/clearance for the escape.
FanoutPourKeepoutTag — ask pours to stay back from dense local fanout copper.
DirectConnectTag — solid pour connection (no thermal-relief spokes) for
high-current/high-dissipation pads; tag the component to tag all its pads.
The "route two pads, then tag the route" workflow — create a code-based route and
mark it for the escape rule:
r = Route(self.u1.SCL, self.header.SCL, layer=0)
self.routes = [r]
Tags(PinFanoutTag()).assign(r)
Route and the control-point types are detailed in
references/control-points.md.
Control points & code-based routes (ADVANCED)
Stable as of JITX 4.2.0. The module is jitx.controlpoint (the three
classes are also re-exported from top-level jitx). The classes were renamed in
4.2.0 — pre-release alphas called them SingleControl / InsertionControl /
PairControl; those names no longer import.
Route(source, destination, layer, sketch=None) — a code-based route between two
Port/Pad/Via endpoints (not directional); sketch is an optional list of
points hinting the routing engine. No per-route width/clearance overrides — tag
the route and write a design_constraint(...) rule instead.
RoutePoint(layer=..., shape=None, bundle=Port) — the single-ended control
point; its .port is the routable endpoint.
PairInsertion(layer=..., bundle=DiffPair) — differential-pair insertion
point (uncoupled legs on one side via .uncoupled.{n,p}, coupled pair on the
other via .coupled); PairPoint(layer=..., bundle=DiffPair) — joins two
coupled segments via .pair. Both are placed with .at(point, rotate=) and
wired to ports via PortAttachment([n, p], control) — port order sets
chirality for PairInsertion.
Full pattern, chirality rules + the real BGA escape/deskew example:
references/control-points.md.
Anti-string-hacking
Geometry-heavy layout code tempts you into building a parallel string-keyed model
(vias[f"r{r}c{c}"] = ...) and walking it to emit JITX calls. Don't. Construct the
JITX objects directly; batch parameters with a @dataclass(frozen=True) or a plain
list; key dicts by Port/structural objects, never by an assembled string. If the
only key you have is a runtime-built string, the structural object you need is
missing. See the base jitx skill's references/architectural-patterns.md, and run
jitx-code-review as a self-critique pass on layout code.
Verification
pyright path/to/layout.py
ruff format path/to/layout.py
Then build-test with a SampleDesign harness (see jitx-circuit-builder
"Verification Process"); sequence builds — don't parallelize against the same
design. Validate shapely outputs (non-empty Polygon/MultiPolygon) before they
reach a fab feature.
API Reference
Complete class definitions and parameters: JITX Documentation.
Worked examples: references/layout-examples.md (thermal-pad CSG, antenna) and
references/control-points.md (Route / control points).