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.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user