Files
periscope/backend/pinscopex/nc_pin_check.py
T
micheleandCursor 2950446e12 Add Layout F1 functional groups and crystal/NC checks.
Write routing-first domain/satellite topology after graph_build, and
flag crystal CL mismatches and NC pins on active nets without inventing
millimetres.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 16:22:56 +02:00

110 lines
3.5 KiB
Python

"""NC pintable pins must not sit on an active net with other parts."""
from __future__ import annotations
import re
from backend.pinscopex.models import (
ComponentConstraints,
ComponentType,
DesignGraph,
Finding,
)
_NC_NAME_RE = re.compile(
r"^(?:n/?c|n\.c\.|nc|unconnected|no[_-]?connect|not[_-]?connected)$",
re.IGNORECASE,
)
_NC_NET_RE = re.compile(
r"^(?:n/?c|n\.c\.|nc|unconnected|no[_-]?connect|not[_-]?connected)$",
re.IGNORECASE,
)
def check_nc_pins(
graph: DesignGraph,
constraints_map: dict[str, ComponentConstraints],
) -> list[Finding]:
findings: list[Finding] = []
for ref, comp in sorted(graph.components.items()):
if comp.component_type != ComponentType.IC:
continue
cons = _match(comp.mpn or comp.value, constraints_map)
if not cons or not cons.pintable:
continue
for pin in cons.pintable:
if not _is_nc_pin_name(pin.name or ""):
continue
net_name = comp.pins.get(str(pin.number))
if not net_name:
continue
if _NC_NET_RE.match(net_name.strip()):
continue
others = [
r for r in graph.components_on_net(net_name)
if r != ref
]
if not others:
# Lone net named oddly but empty of other parts — still flag if
# the net name looks like a real signal (not floating placeholder).
if _looks_active_net(net_name):
findings.append(_finding(ref, comp.mpn or "", pin.number, pin.name, net_name, []))
continue
findings.append(_finding(ref, comp.mpn or "", pin.number, pin.name, net_name, others))
return findings
def _finding(ref, mpn, pin_num, pin_name, net, others) -> Finding:
other_s = ", ".join(others[:6]) if others else "(no other refs)"
return Finding(
designator=ref,
mpn=mpn,
aspect="connectivity",
source="nc_pin_check",
status="WARNING",
finding=(
f"{ref} pin {pin_num} ({pin_name or 'NC'}) is marked NC in the "
f"pintable but connects to net '{net}'"
+ (f" with {other_s}." if others else ".")
),
why="No-connect pins should remain unconnected or on an explicit NC net.",
recommendation="Leave the NC pin floating or disconnect the net.",
reference="pintable",
rule_id="PS-NC-001",
net=net,
pins=[f"{ref}.{pin_num}"],
)
def _is_nc_pin_name(name: str) -> bool:
t = (name or "").strip()
if not t:
return False
if _NC_NAME_RE.match(t):
return True
# Slash-separated alts: "NC/GPIO" still counts as NC-capable; only pure NC.
parts = [p.strip() for p in re.split(r"[/,]", t) if p.strip()]
return bool(parts) and all(_NC_NAME_RE.match(p) or p.upper() == "NC" for p in parts)
def _looks_active_net(name: str) -> bool:
u = (name or "").strip()
if not u or u.startswith("unconnected"):
return False
return not _NC_NET_RE.match(u)
def _match(
mpn: str | None,
datasheets: dict[str, ComponentConstraints],
) -> ComponentConstraints | None:
if not mpn:
return None
if mpn in datasheets:
return datasheets[mpn]
norm = re.sub(r"[/_\-\s]", "", mpn).upper()
for ds_mpn, constraints in datasheets.items():
if re.sub(r"[/_\-\s]", "", ds_mpn).upper() == norm:
return constraints
return None