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:
@@ -0,0 +1,110 @@
|
||||
"""Deterministic PCB review checks (no LLM). Fail-soft at the caller."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import Counter
|
||||
|
||||
from backend.periscopex.derating import build_derating_table
|
||||
from backend.periscopex.models import DesignGraph, Finding, LayoutGraph
|
||||
from backend.periscopex.pcb_net_match import check_pcb_net_match
|
||||
from backend.periscopex.placement_check import check_placement
|
||||
from backend.periscopex.si_check import check_si
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_FALLBACK_FIX = "Confirm the layout against the datasheet and adjust the board."
|
||||
|
||||
|
||||
def assign_pcb_finding_ids(findings: list[Finding]) -> None:
|
||||
"""IDs ``PCB-{designator}-{001}`` so they never collide with schema review."""
|
||||
counter: Counter[str] = Counter()
|
||||
for f in findings:
|
||||
counter[f.designator] += 1
|
||||
f.finding_id = f"PCB-{f.designator}-{counter[f.designator]:03d}"
|
||||
if not (f.recommendation or "").strip():
|
||||
f.recommendation = _FALLBACK_FIX
|
||||
|
||||
|
||||
def check_pcb_derating(graph: DesignGraph) -> list[Finding]:
|
||||
"""ERROR only when both Vop and Vrated exist and Vop exceeds Vrated."""
|
||||
out: list[Finding] = []
|
||||
for row in build_derating_table(graph):
|
||||
rated = row.get("rated_voltage_v")
|
||||
op = row.get("operating_voltage_v")
|
||||
if rated is None or op is None:
|
||||
continue
|
||||
if float(op) <= float(rated):
|
||||
continue
|
||||
ref = str(row.get("designator") or "")
|
||||
out.append(Finding(
|
||||
designator=ref,
|
||||
mpn=str(row.get("mpn") or ""),
|
||||
aspect="derating",
|
||||
finding=(
|
||||
f"{ref} operates at {op:g} V with a {rated:g} V rating."
|
||||
),
|
||||
why="Capacitor voltage rating must exceed the rail (no invented derating %).",
|
||||
status="ERROR",
|
||||
recommendation=(
|
||||
f"Replace {ref} with a capacitor rated above {op:g} V, "
|
||||
"or lower the rail."
|
||||
),
|
||||
source="pcb_derating",
|
||||
rule_id="PE-DRT-001",
|
||||
net=row.get("net_plus"),
|
||||
pins=[],
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def merge_schema_pcb_reports(
|
||||
schema: dict | None, pcb: dict | None,
|
||||
) -> dict | None:
|
||||
"""Combine schematic report.json with pcb_report.json for GET /report."""
|
||||
if not schema and not pcb:
|
||||
return None
|
||||
if not pcb:
|
||||
out = dict(schema or {})
|
||||
out["findings"] = [
|
||||
f for f in (out.get("findings") or [])
|
||||
if not str(f.get("finding_id") or "").startswith("PCB-")
|
||||
]
|
||||
return out
|
||||
if not schema:
|
||||
return dict(pcb)
|
||||
findings = [
|
||||
f for f in (schema.get("findings") or [])
|
||||
if not str(f.get("finding_id") or "").startswith("PCB-")
|
||||
]
|
||||
findings.extend(pcb.get("findings") or [])
|
||||
out = dict(schema)
|
||||
out["findings"] = findings
|
||||
summary: dict[str, int] = {"ERROR": 0, "WARNING": 0, "INFO": 0}
|
||||
for f in findings:
|
||||
st = f.get("status")
|
||||
if st in summary:
|
||||
summary[st] = summary.get(st, 0) + 1
|
||||
out["summary"] = summary
|
||||
return out
|
||||
|
||||
|
||||
def run_pcb_checks(
|
||||
graph: DesignGraph,
|
||||
constraints_map: dict,
|
||||
layout: LayoutGraph | None,
|
||||
) -> list[Finding]:
|
||||
"""Placement, SI, pad-net match, derating — skip individually on error."""
|
||||
out: list[Finding] = []
|
||||
for name, fn in (
|
||||
("pcb_net_match", lambda: check_pcb_net_match(graph, layout)),
|
||||
("placement_check", lambda: check_placement(graph, constraints_map, layout)),
|
||||
("si_check", lambda: check_si(graph, constraints_map, layout)),
|
||||
("pcb_derating", lambda: check_pcb_derating(graph)),
|
||||
):
|
||||
try:
|
||||
out.extend(fn())
|
||||
except Exception:
|
||||
log.exception("PCB check %s failed — skipping", name)
|
||||
assign_pcb_finding_ids(out)
|
||||
return out
|
||||
Reference in New Issue
Block a user