Add MODE=pcb layout exam job with AI and deterministic checks.

Parallel pcb_status pipeline: parse board, classify domains/groups,
inventory traces, PE-LAY/PLC/SI/DRT checks, per-IC datasheet review.
Findings merge into the report UI. No auto-place or pcbnew write-back.
This commit is contained in:
2026-09-19 22:54:17 +02:00
parent a2011dad91
commit 16c606ae3d
28 changed files with 1906 additions and 32 deletions
+97
View File
@@ -0,0 +1,97 @@
"""Trace / bus inventory from a LayoutGraph. Numbers only, no findings."""
from __future__ import annotations
import re
from collections import defaultdict
from pydantic import BaseModel
from backend.periscopex.models import DesignGraph, LayoutGraph, NetType
from backend.periscopex.si_check import net_length_mm, partner_net
_BUS_RE = re.compile(r"^(.*?)(\d+)$")
class PcbNetInventory(BaseModel):
name: str
length_mm: float = 0.0
via_count: int = 0
layers: list[str] = []
width_min_mm: float | None = None
width_max_mm: float | None = None
pair: str | None = None
bus: str | None = None
net_type: str | None = None
z0_ohm: float | None = None
class PcbInventoryReport(BaseModel):
nets: list[PcbNetInventory] = []
domains: list[str] = []
group_count: int = 0
def _bus_name(net: str) -> str | None:
m = _BUS_RE.match(net.split("/")[-1].replace(".", "_"))
if not m:
return None
prefix, digits = m.group(1), m.group(2)
if not prefix or len(digits) > 3:
return None
return prefix.rstrip("_") or None
def build_pcb_inventory(
layout: LayoutGraph,
graph: DesignGraph | None = None,
*,
domain_ids: list[str] | None = None,
group_count: int = 0,
z0_by_net: dict[str, float] | None = None,
) -> PcbInventoryReport:
names: set[str] = set()
layers: dict[str, set[str]] = defaultdict(set)
widths: dict[str, list[float]] = defaultdict(list)
vias: dict[str, int] = defaultdict(int)
for s in layout.segments:
if not s.net:
continue
names.add(s.net)
if s.layer:
layers[s.net].add(s.layer)
if s.width > 0:
widths[s.net].append(s.width)
for v in layout.vias:
if v.net:
names.add(v.net)
vias[v.net] += 1
z0_by_net = z0_by_net or {}
rows: list[PcbNetInventory] = []
for name in sorted(names):
w = widths.get(name) or []
sch = graph.nets.get(name) if graph else None
nt = sch.net_type.value if sch and isinstance(sch.net_type, NetType) else (
str(sch.net_type) if sch and sch.net_type else None
)
partner = partner_net(name)
if partner and partner not in names:
partner = None
rows.append(PcbNetInventory(
name=name,
length_mm=round(net_length_mm(layout, name), 3),
via_count=vias.get(name, 0),
layers=sorted(layers.get(name, ())),
width_min_mm=min(w) if w else None,
width_max_mm=max(w) if w else None,
pair=partner,
bus=_bus_name(name),
net_type=nt,
z0_ohm=z0_by_net.get(name),
))
return PcbInventoryReport(
nets=rows,
domains=list(domain_ids or []),
group_count=group_count,
)