PCB AI skips a second PDF exam (pintable cache, pdf_path=None) and the report gains a PCB exam section plus power-trace, thermal, via, Kelvin, and GND-stitch checks grounded in board geometry and extracted specs.
100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
"""Per-IC PCB layout review — consumes shared library extraction, no second PDF exam."""
|
|
|
|
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
|
|
from backend.periscopex.pcb_inventory import PcbInventoryReport
|
|
from backend.periscopex.pcb_review import (
|
|
PCB_SYSTEM_PROMPT,
|
|
build_pcb_layout_context,
|
|
format_library_extraction,
|
|
)
|
|
from backend.periscopex.validate import ReviewResult, _match_constraints
|
|
from backend.services.api_logs import ApiLogger
|
|
from backend.services.validation import 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
|
|
|
|
|
|
def _has_library_extraction(cons) -> bool:
|
|
return cons is not None and bool(cons.pintable)
|
|
|
|
|
|
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]]:
|
|
"""Layout-only AI exam using ``library/extracted`` (or project extracted/).
|
|
|
|
Does not attach a datasheet PDF. ICs without a library pintable are skipped
|
|
with a reason to run schematic review first.
|
|
"""
|
|
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
|
|
cons = _match_constraints(mpn, constraints_map)
|
|
if not _has_library_extraction(cons):
|
|
skipped.append({
|
|
"designator": ref,
|
|
"reason": "no library extraction — run schematic review first",
|
|
})
|
|
continue
|
|
extra = "\n\n".join([
|
|
format_library_extraction(cons),
|
|
build_pcb_layout_context(ref, graph, layout, plan, inventory),
|
|
])
|
|
try:
|
|
result, _trace = await review_ic_async(
|
|
graph, constraints_map, ref, None,
|
|
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
|