Files
periscope/backend/periscopex/pcb_review.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

126 lines
5.0 KiB
Python

"""PCB datasheet review prompt and layout neighborhood context (no auto-place)."""
from __future__ import annotations
import math
from backend.periscopex.functional_groups import FunctionalGroupsReport
from backend.periscopex.models import DesignGraph, LayoutGraph
from backend.periscopex.pcb_inventory import PcbInventoryReport
from backend.periscopex.si_check import net_length_mm
PCB_SYSTEM_PROMPT = """\
You are an electrical engineer reviewing a PCB layout against the IC \
datasheet. This is an EXAM of the existing board — do not propose a new \
floorplan, do not invent millimetres, and do not write Gerbers.
### Coverage checklist (layout)
- Domains (power-rail islands) and functional groups (IC + satellites: \
decoupling, bulk, filter, crystal, pullup).
- Placement: decoupling and crystal load caps vs datasheet layout notes \
and any numeric layout_rules (max_distance_mm, same_layer, thermal vias).
- Routing: net lengths, differential-pair skew, obvious stubs; length \
match only when the datasheet gives millimetres.
- Impedance: comment on Z0 only when stackup + width numbers are in the \
context. Never assume 50 Ω.
- Derating: flag a capacitor only when both operating and rated voltages \
are present and Vop exceeds Vrated.
- Filters: topology already on the schematic — check whether filter parts \
sit with the IC they serve when coordinates exist.
- Datasheet layout pages (typical application / PCB layout) vs this \
neighborhood.
### Findings
Every finding MUST have status ERROR, WARNING, or INFO and a non-empty \
recommendation (what to change on the board). INFO still needs a next \
step (e.g. "Measure Z0 after stackup is filled in").
Call submit_review. Empty findings with checked_areas is valid when the \
layout matches the datasheet.
"""
def build_pcb_layout_context(
ic_ref: str,
graph: DesignGraph,
layout: LayoutGraph | None,
plan: FunctionalGroupsReport | None,
inventory: PcbInventoryReport | None = None,
) -> str:
"""Plain-text block appended to the schematic neighborhood for PCB review."""
lines = ["### PCB layout context (existing board — do not move parts)"]
domain_id = None
group = None
if plan is not None:
for g in plan.groups:
if g.ref == ic_ref:
group = g
break
for d in plan.domains:
if ic_ref in d.ic_refs:
domain_id = d.domain_id
lines.append(
f"Domain: {d.domain_id} (power nets: {', '.join(d.power_nets) or '—'})"
)
break
if group is None:
lines.append("Functional group: (this IC is not in a classified group)")
else:
lines.append(
f"Functional group: {group.ref} subtype={group.component_subtype or '—'} "
f"rank={group.rank}"
)
sats = group.satellites or []
if sats:
lines.append("Satellites:")
for s in sats:
xy = ""
if layout and s.ref in layout.footprints:
fp = layout.footprints[s.ref]
xy = f" @ ({fp.x:.2f},{fp.y:.2f}) {fp.layer}"
lines.append(
f" {s.ref} role={s.role_hint} nets={','.join(s.nets or [])}{xy}"
)
else:
lines.append("Satellites: none")
if layout and ic_ref in layout.footprints:
fp = layout.footprints[ic_ref]
lines.append(
f"Footprint {ic_ref}: ({fp.x:.2f},{fp.y:.2f}) layer={fp.layer} "
f"pads={len(fp.pads)}"
)
near: list[str] = []
for other, ofp in layout.footprints.items():
if other == ic_ref:
continue
dist = math.hypot(ofp.x - fp.x, ofp.y - fp.y)
if dist <= 15.0:
near.append(f"{other} {dist:.1f}mm {ofp.layer}")
if near:
lines.append("Within 15 mm: " + "; ".join(sorted(near)[:24]))
pin_nets = graph.components.get(ic_ref)
if pin_nets:
seen: set[str] = set()
lines.append("Connected net lengths (PCB copper):")
for net_name in pin_nets.pins.values():
if not net_name or net_name in seen:
continue
seen.add(net_name)
mm = net_length_mm(layout, net_name)
lines.append(f" {net_name}: {mm:.2f} mm")
elif layout is None:
lines.append("No LayoutGraph — skip millimetre claims.")
else:
lines.append(f"No footprint for {ic_ref} on the PCB.")
if inventory and inventory.nets:
sample = inventory.nets[:12]
lines.append("Inventory sample (name length_mm pair):")
for n in sample:
lines.append(
f" {n.name}: {n.length_mm:.2f} mm pair={n.pair or '—'} "
f"Z0={n.z0_ohm if n.z0_ohm is not None else '—'}"
)
if domain_id:
lines.append(f"(domain_id={domain_id})")
return "\n".join(lines)