Rebrand Pinscope to Periscope across product and codebase.

Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 20:02:04 +02:00
co-authored by Cursor
parent 556306bf4d
commit 8d2b85600f
177 changed files with 869 additions and 766 deletions
+91
View File
@@ -0,0 +1,91 @@
"""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.periscopex.models import DesignGraph, Finding, LayoutGraph, LayoutSegment
from backend.periscopex.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="PE-SI-001",
net=net,
pins=[],
source_page=page,
))
return findings