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.
111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""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
|