"""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)