"""Pad net on the PCB vs pin net on the schematic graph. Silent when either side has no name. Power-flag footprints (#PWR) skipped. KiCad PCB net names often keep a root-sheet ``/`` (and nested sheet path) that a flattened schematic netlist omits. Those are the same net: do not emit per-pad reconnect findings. """ from __future__ import annotations from backend.periscopex.models import DesignGraph, Finding, LayoutGraph def normalize_kicad_hierarchy_net(name: str) -> str: """Drop leading slashes; keep inner sheet path (``sheet/NET``).""" n = (name or "").strip().replace("\\", "/") while n.startswith("/"): n = n[1:] while n.endswith("/") and n: n = n[:-1] return n def kicad_nets_match(a: str, b: str) -> bool: """True if schematic and PCB names are the same KiCad net. Equal strings match. After stripping leading ``/``, equal names match (``/ESP32_EN`` vs ``ESP32_EN``). A flattened leaf also matches a nested path that ends with ``/{leaf}`` (``REGN`` vs ``/power/REGN``). Different sheets that only share a last segment (``sheet1/CLK`` vs ``sheet2/CLK``) do not match each other. """ if not a or not b: return False if a == b: return True na = normalize_kicad_hierarchy_net(a) nb = normalize_kicad_hierarchy_net(b) if not na or not nb: return False if na == nb: return True return na.endswith("/" + nb) or nb.endswith("/" + na) def _ensure_recommendation(f: Finding) -> Finding: if not (f.recommendation or "").strip(): f.recommendation = ( "Align the PCB pad net with the schematic pin, then re-run PCB review." ) return f def _hierarchy_only_mismatch(pcb_net: str, sch_net: str) -> bool: """PCB vs schematic differ only by KiCad sheet-path prefix.""" if pcb_net == sch_net: return False return kicad_nets_match(pcb_net, sch_net) def check_pcb_net_match( graph: DesignGraph, layout: LayoutGraph | None, ) -> list[Finding]: if layout is None or not layout.footprints: return [] findings: list[Finding] = [] sch_refs = { ref for ref in graph.components if not ref.startswith("#") } pcb_refs = { ref for ref in layout.footprints if not ref.startswith("#") } for ref in sorted(sch_refs - pcb_refs): comp = graph.components[ref] findings.append(_ensure_recommendation(Finding( designator=ref, mpn=comp.mpn or "", aspect="pcb_match", finding=f"{ref} is in the schematic but has no footprint on the PCB.", why="Layout review cannot check a part that is not placed.", status="WARNING", recommendation=f"Place {ref} on the board or remove it from the schematic/BOM.", source="pcb_net_match", rule_id="PE-LAY-002", pins=[], ))) real: list[tuple[str, str, str, str]] = [] # ref, pad, pcb_net, sch_net hierarchy: list[tuple[str, str, str, str]] = [] for ref, fp in sorted(layout.footprints.items()): if ref.startswith("#"): continue comp = graph.components.get(ref) if comp is None: continue for pad in fp.pads: if not pad.net or not pad.number: continue sch_net = graph.pin_net(ref, pad.number) if not sch_net: continue if pad.net == sch_net: continue if _hierarchy_only_mismatch(pad.net, sch_net): hierarchy.append((ref, pad.number, pad.net, sch_net)) continue real.append((ref, pad.number, pad.net, sch_net)) if hierarchy and not real: examples: list[str] = [] seen_pairs: set[tuple[str, str]] = set() for _ref, _pad, pcb_net, sch_net in hierarchy: pair = (pcb_net, sch_net) if pair in seen_pairs: continue seen_pairs.add(pair) examples.append(f"PCB '{pcb_net}' ↔ schematic '{sch_net}'") if len(examples) >= 8: break extra = len(seen_pairs) - len(examples) example_txt = "; ".join(examples) if extra > 0: example_txt += f" (+{extra} more)" first_ref = hierarchy[0][0] first_comp = graph.components.get(first_ref) findings.append(_ensure_recommendation(Finding( designator=first_ref, mpn=(first_comp.mpn if first_comp else "") or "", aspect="pcb_match", finding=( "Schematic and PCB net names differ only by KiCad hierarchy " f"prefix ({len(hierarchy)} pads). Examples: {example_txt}." ), why=( "KiCad stores board nets with a leading '/' and optional sheet " "path; a flattened schematic netlist usually omits that prefix. " "These pads are the same electrical nets." ), status="INFO", recommendation=( "Update the PCB from the schematic (or re-export a netlist with " "the same hierarchy flattening) so names match. Do not retouch " "individual pads." ), source="pcb_net_match", rule_id="PE-LAY-003", pins=[], ))) for ref, pad_no, pcb_net, sch_net in real: comp = graph.components[ref] findings.append(_ensure_recommendation(Finding( designator=ref, mpn=comp.mpn or "", aspect="pcb_match", finding=( f"{ref}.{pad_no} PCB net '{pcb_net}' does not match " f"schematic net '{sch_net}'." ), why=( "Datasheet and SI checks follow schematic net names. " "A pad on the wrong net is a layout error, not a BOM typo." ), status="ERROR", recommendation=( f"Reconnect pad {ref}.{pad_no} to '{sch_net}' " f"(or fix the schematic if the PCB is authoritative)." ), source="pcb_net_match", rule_id="PE-LAY-001", net=pcb_net, pins=[pad_no], ))) return findings