Treat KiCad /net vs schematic net as one hierarchy finding.

Pad-net compare strips leading slash and nested sheet path so /ESP32_EN
matches ESP32_EN. If every mismatch is only that prefix, emit PE-LAY-003
once instead of per-pad reconnect errors.
This commit is contained in:
2026-09-20 08:10:34 +02:00
parent 89a484830e
commit f3f96ea4d1
4 changed files with 208 additions and 26 deletions
+117 -23
View File
@@ -1,6 +1,10 @@
"""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
@@ -8,6 +12,38 @@ 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 = (
@@ -16,6 +52,13 @@ def _ensure_recommendation(f: Finding) -> Finding:
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,
@@ -47,6 +90,9 @@ def check_pcb_net_match(
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
@@ -59,28 +105,76 @@ def check_pcb_net_match(
sch_net = graph.pin_net(ref, pad.number)
if not sch_net:
continue
if sch_net == pad.net:
if pad.net == sch_net:
continue
findings.append(_ensure_recommendation(Finding(
designator=ref,
mpn=comp.mpn or "",
aspect="pcb_match",
finding=(
f"{ref}.{pad.number} PCB net '{pad.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.number} to '{sch_net}' "
f"(or fix the schematic if the PCB is authoritative)."
),
source="pcb_net_match",
rule_id="PE-LAY-001",
net=pad.net,
pins=[pad.number],
)))
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