Layout and AI findings always get action (fallback recommendation). The card renders that sentence in the body. PCB review context now includes vias under the footprint, copper thickness, nearby widths, courtyard, and keepout polygons.
133 lines
4.7 KiB
Python
133 lines
4.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.finding_engine import complete_findings
|
|
from backend.periscopex.models import DesignGraph, Finding, LayoutGraph
|
|
from backend.periscopex.pcb_net_match import check_pcb_net_match
|
|
from backend.periscopex.pcb_power_thermal import (
|
|
check_pcb_gnd_stitch,
|
|
check_pcb_kelvin,
|
|
check_pcb_power_traces,
|
|
check_pcb_thermal_copper,
|
|
check_pcb_via_current,
|
|
)
|
|
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}"
|
|
rec = (f.recommendation or "").strip()
|
|
act = (f.action or "").strip()
|
|
if not rec:
|
|
rec = act or _FALLBACK_FIX
|
|
f.recommendation = rec
|
|
if not act:
|
|
f.action = rec
|
|
complete_findings(findings)
|
|
|
|
|
|
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
|
|
schema_nr = list((schema or {}).get("not_reviewed") or [])
|
|
pcb_nr = list((pcb or {}).get("not_reviewed") or [])
|
|
if schema_nr or pcb_nr:
|
|
out["not_reviewed"] = schema_nr + pcb_nr
|
|
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)),
|
|
("pcb_power_traces", lambda: check_pcb_power_traces(graph, constraints_map, layout)),
|
|
("pcb_via_current", lambda: check_pcb_via_current(graph, constraints_map, layout)),
|
|
("pcb_thermal_copper", lambda: check_pcb_thermal_copper(graph, constraints_map, layout)),
|
|
("pcb_kelvin", lambda: check_pcb_kelvin(graph, constraints_map)),
|
|
("pcb_gnd_stitch", lambda: check_pcb_gnd_stitch(graph, layout)),
|
|
):
|
|
try:
|
|
out.extend(fn())
|
|
except Exception:
|
|
log.exception("PCB check %s failed — skipping", name)
|
|
assign_pcb_finding_ids(out)
|
|
return out
|