Files
periscope/backend/services/pcb_validation.py
T
michele 16c606ae3d 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.
2026-09-19 22:54:17 +02:00

82 lines
3.2 KiB
Python

"""Per-IC PCB datasheet review — same agentic loop as schematic, layout context."""
from __future__ import annotations
import logging
from pathlib import Path
from backend.periscopex.cad_bridge import annotate_findings_cad, cad_index_from_graph
from backend.periscopex.functional_groups import FunctionalGroupsReport
from backend.periscopex.models import ComponentType, DesignGraph, Finding, LayoutGraph, ValidationReport
from backend.periscopex.pcb_inventory import PcbInventoryReport
from backend.periscopex.pcb_review import PCB_SYSTEM_PROMPT, build_pcb_layout_context
from backend.periscopex.validate import ReviewResult
from backend.services.api_logs import ApiLogger
from backend.services.validation import _find_pdf, review_ic_async
log = logging.getLogger(__name__)
_FIX = "Adjust the layout to match the datasheet recommendation, then re-run PCB review."
def _ensure_recs(findings: list[Finding]) -> None:
for f in findings:
if f.source in (None, "review"):
f.source = "pcb_review"
if not (f.recommendation or "").strip():
f.recommendation = _FIX
async def review_pcb_ics(
graph: DesignGraph,
constraints_map: dict,
layout: LayoutGraph | None,
plan: FunctionalGroupsReport | None,
inventory: PcbInventoryReport | None,
pdf_dir: Path,
storage=None,
api_logger: ApiLogger | None = None,
on_progress=None,
) -> tuple[list[Finding], dict[str, list[str]], list[dict]]:
"""Review each IC with a datasheet. Fail-soft per IC. No auto-place."""
findings: list[Finding] = []
coverage: dict[str, list[str]] = {}
skipped: list[dict] = []
cache: dict = {}
for ref, comp in sorted(graph.components.items()):
if comp.component_type != ComponentType.IC:
continue
mpn = (comp.mpn or "").strip() or (comp.value or "").strip()
if not mpn:
skipped.append({"designator": ref, "reason": "no MPN in BOM"})
continue
pdf = _find_pdf(mpn, pdf_dir, storage=storage)
if pdf is None:
skipped.append({"designator": ref, "reason": "no datasheet PDF"})
continue
extra = build_pcb_layout_context(ref, graph, layout, plan, inventory)
try:
result, _trace = await review_ic_async(
graph, constraints_map, ref, str(pdf),
on_progress=on_progress,
api_logger=api_logger,
pdf_dir=pdf_dir,
storage=storage,
excerpt_cache=cache,
extra_context=extra,
system_prompt=PCB_SYSTEM_PROMPT,
log_stage="pcb_review",
)
except Exception:
log.exception("PCB AI review failed for %s — skipping", ref)
skipped.append({"designator": ref, "reason": "pcb_review error"})
continue
if not isinstance(result, ReviewResult):
continue
_ensure_recs(result.findings)
annotate_findings_cad(result.findings, cad_index_from_graph(graph))
findings.extend(result.findings)
if result.checked_areas:
coverage[ref] = list(result.checked_areas)
return findings, coverage, skipped