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.
126 lines
5.0 KiB
Python
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)
|