PS-SI-001 and decoupling skip without a numeric layout_rule. Tests use simple_project nets, not invented USBPHY boards. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
2.8 KiB
Python
92 lines
2.8 KiB
Python
"""G1 SI: intra-pair skew only when the datasheet gives millimetres.
|
|
|
|
Pair names (_DP/_DM, _P/_N) only identify which nets to compare. The
|
|
limit is never 3W, USB spec folklore, or a default millimetre.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from backend.pinscopex.models import DesignGraph, Finding, LayoutGraph, LayoutSegment
|
|
from backend.pinscopex.validate import _match_constraints
|
|
|
|
_PAIR_SUFFIXES = (("_DP", "_DM"), ("_P", "_N"), ("+", "-"))
|
|
|
|
|
|
def _seg_len(seg: LayoutSegment) -> float:
|
|
return math.hypot(seg.end[0] - seg.start[0], seg.end[1] - seg.start[1])
|
|
|
|
|
|
def net_length_mm(layout: LayoutGraph, net: str) -> float:
|
|
return sum(_seg_len(s) for s in layout.segments if s.net == net)
|
|
|
|
|
|
def partner_net(name: str) -> str | None:
|
|
for a, b in _PAIR_SUFFIXES:
|
|
if name.endswith(a):
|
|
return name[: -len(a)] + b
|
|
if name.endswith(b):
|
|
return name[: -len(b)] + a
|
|
return None
|
|
|
|
|
|
def _length_match_limit_mm(constraints_map: dict, graph: DesignGraph) -> tuple[float, int | None] | None:
|
|
for comp in graph.components.values():
|
|
cons = _match_constraints(comp.mpn, constraints_map)
|
|
if not cons:
|
|
continue
|
|
for rule in cons.layout_rules or []:
|
|
if rule.get("kind") != "length_match":
|
|
continue
|
|
mm = rule.get("max_distance_mm")
|
|
if mm is None:
|
|
continue
|
|
return float(mm), rule.get("source_page")
|
|
return None
|
|
|
|
|
|
def check_si(
|
|
graph: DesignGraph,
|
|
constraints_map: dict,
|
|
layout: LayoutGraph | None,
|
|
) -> list[Finding]:
|
|
if layout is None or not layout.segments:
|
|
return []
|
|
limit = _length_match_limit_mm(constraints_map, graph)
|
|
if limit is None:
|
|
return []
|
|
max_mm, page = limit
|
|
seen: set[tuple[str, str]] = set()
|
|
findings: list[Finding] = []
|
|
names = {s.net for s in layout.segments if s.net}
|
|
for net in names:
|
|
partner = partner_net(net)
|
|
if not partner or partner not in names:
|
|
continue
|
|
key = tuple(sorted((net, partner)))
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
skew = abs(net_length_mm(layout, net) - net_length_mm(layout, partner))
|
|
if skew <= max_mm:
|
|
continue
|
|
findings.append(Finding(
|
|
designator="layout",
|
|
mpn="",
|
|
aspect="si",
|
|
finding=(
|
|
f"Intra-pair skew {skew:.1f} mm on {key[0]}/{key[1]} "
|
|
f"(datasheet max {max_mm:g} mm)."
|
|
),
|
|
why=f"length_match max_distance_mm={max_mm:g}.",
|
|
status="ERROR",
|
|
recommendation="Length-match the differential pair.",
|
|
source="si_check",
|
|
rule_id="PS-SI-001",
|
|
net=net,
|
|
pins=[],
|
|
source_page=page,
|
|
))
|
|
return findings
|