Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
"""Connectivity-hygiene checks (net-level, format-agnostic).
``run(sch, cfg) -> list[Finding]`` flags the classic "looks fine, isn't
connected" traps that graphical review misses:
* **NET_SINGLE_PIN** — a net with exactly one pin: either a floating label,
a power port driving nothing, or a missing second connection. This is the
net-level twin of KiCad's single-pin-net ERC.
* **NET_OFF_GRID** — component pins whose world coordinates are off the
schematic grid. Off-grid pins are the canonical cause of wires that *touch*
a pin on screen without ever joining its net.
Grid and coincidence policy
---------------------------
All comparisons are **exact integer nanometres** (``units.mil_to_nm``), never
rounded floats: two points COINCIDE only when their nm coordinates are equal
after the mil→nm rounding, which is the same quantisation the readers and the
writer gate use — so this check can neither claim a coincidence netbuild
rejects nor miss one it accepts. The grid comes from ``Config.grid_nm``
(``[project] grid`` — ``"50mil"`` default, metric values like ``"1.27mm"`` /
``"0.5mm"`` are exact in nm). A pin is off-grid when its nm residue exceeds
``_TOL_NM`` (0.5 mil): unit conversion noise is far below it, while a
genuinely misplaced pin (KiCad snaps exactly) is far above it.
Everything works on the normalized model, so Altium inputs are checked with
the same rules as KiCad ones.
"""
from __future__ import annotations
from .. import units
from ..config import DEFAULT_GRID_NM, Config
from ..model import Schematic
from ..report import Finding, Severity, anchor
NET_SINGLE_PIN = "NET_SINGLE_PIN" # WARNING: 1-member net (floating / undriven)
NET_OFF_GRID = "NET_OFF_GRID" # WARNING: pins off the schematic grid
_TOL_NM = units.mil_to_nm(0.5) # past rounding noise = genuinely off-grid
def _off_grid(nm: int, grid_nm: int) -> bool:
r = nm % grid_nm
return min(r, grid_nm - r) > _TOL_NM
def _pt_nm(x_mil: float, y_mil: float) -> tuple[int, int]:
return (units.mil_to_nm(x_mil), units.mil_to_nm(y_mil))
def run(sch: Schematic, cfg: Config | None = None) -> list[Finding]:
findings: list[Finding] = []
grid_nm = cfg.grid_nm if cfg is not None else DEFAULT_GRID_NM
# pins carrying an explicit no-connect marker are intentionally single
nc_points = {_pt_nm(x, y) for x, y in (sch.no_erc_points or [])}
pin_at = {(c.designator, p.number): _pt_nm(p.x_mil, p.y_mil)
for c in sch.components for p in c.pins}
pin_mil = {(c.designator, p.number): (p.x_mil, p.y_mil)
for c in sch.components for p in c.pins}
for net in sch.nets:
if len(net.members) != 1:
continue
d, p = net.members[0]
if pin_at.get((d, p)) in nc_points:
continue
what = ("power port drives nothing"
if d.startswith("#") else "floating or missing connection")
pos = pin_mil.get((d, p))
findings.append(Finding(
NET_SINGLE_PIN, Severity.WARNING,
f"net '{net.name}' has a single pin {d}.{p} — {what}",
refs=[f"{d}.{p}"],
pos=pos,
anchors=[anchor("pin", f"{d}.{p}", pos), anchor("net", net.name or "")],
))
grid_mil = grid_nm / units.NM_PER_MIL
for comp in sch.components:
off = [p for p in comp.pins
if _off_grid(units.mil_to_nm(p.x_mil), grid_nm)
or _off_grid(units.mil_to_nm(p.y_mil), grid_nm)]
if not off:
continue
sample = ", ".join(
f"{p.number}@({p.x_mil:g},{p.y_mil:g})" for p in off[:4])
more = f" (+{len(off) - 4} more)" if len(off) > 4 else ""
anchors = [anchor("component", comp.designator, (comp.x_mil, comp.y_mil))]
anchors += [anchor("pin", f"{comp.designator}.{p.number}", (p.x_mil, p.y_mil))
for p in off[:4]]
findings.append(Finding(
NET_OFF_GRID, Severity.WARNING,
f"{comp.designator}: {len(off)} pin(s) off the {grid_mil:g}-mil "
f"grid: {sample}{more} — wires may touch without connecting",
refs=[comp.designator],
pos=(off[0].x_mil, off[0].y_mil),
anchors=anchors,
))
return findings
2126c3284This file is pinned to the selected release. Download the original to open it in your design tool.