PE-ESD-001 only on J* connector–IC nets (skip NC, unconnected, VSYS/GND/3V3). PE-PI-001 treats any capacitor on the rail, including KiCad slash prefixes, as decoupling. Sort findings ERROR then WARNING then INFO, then RULE/RISK/REVIEW/INFO. Report sidebar is a collapsed expand-on-click tree instead of an all-open list. GET /report and complete_findings fail-soft and sort the same way.
163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
"""Component → pin → net → block hierarchy (usable inventory, not a platform)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from backend.periscopex.functional_groups import FunctionalGroupsReport
|
|
from backend.periscopex.models import DesignGraph, Finding
|
|
|
|
|
|
class HierarchyPin(BaseModel):
|
|
number: str
|
|
net: str = ""
|
|
|
|
|
|
class HierarchyComponent(BaseModel):
|
|
ref: str
|
|
component_type: str
|
|
pins: list[HierarchyPin] = []
|
|
nets: list[str] = []
|
|
block_id: str | None = None
|
|
|
|
|
|
class HierarchyNet(BaseModel):
|
|
name: str
|
|
net_type: str
|
|
components: list[str] = []
|
|
block_id: str | None = None
|
|
|
|
|
|
class HierarchyBlock(BaseModel):
|
|
block_id: str
|
|
kind: str # domain | group | rail
|
|
ic_refs: list[str] = []
|
|
nets: list[str] = []
|
|
|
|
|
|
class HierarchyReport(BaseModel):
|
|
components: list[HierarchyComponent] = []
|
|
nets: list[HierarchyNet] = []
|
|
blocks: list[HierarchyBlock] = []
|
|
|
|
|
|
def build_hierarchy(
|
|
graph: DesignGraph,
|
|
plan: FunctionalGroupsReport | None = None,
|
|
) -> HierarchyReport:
|
|
"""Walk component.pins → nets, then group into domains / rail blocks."""
|
|
ref_block: dict[str, str] = {}
|
|
blocks: list[HierarchyBlock] = []
|
|
if plan is not None:
|
|
for d in plan.domains:
|
|
blocks.append(HierarchyBlock(
|
|
block_id=d.domain_id,
|
|
kind="domain",
|
|
ic_refs=list(d.ic_refs),
|
|
nets=list(d.power_nets),
|
|
))
|
|
for ref in d.ic_refs:
|
|
ref_block[ref] = d.domain_id
|
|
for g in plan.groups:
|
|
bid = ref_block.get(g.ref) or f"group:{g.ref}"
|
|
if g.ref not in ref_block:
|
|
blocks.append(HierarchyBlock(
|
|
block_id=bid, kind="group", ic_refs=[g.ref], nets=[],
|
|
))
|
|
ref_block[g.ref] = bid
|
|
else:
|
|
for net in graph.nets.values():
|
|
ntype = getattr(net.net_type, "value", net.net_type)
|
|
if str(ntype) != "power":
|
|
continue
|
|
ics = [
|
|
p.component_ref for p in net.pins
|
|
if (graph.components.get(p.component_ref)
|
|
and str(getattr(graph.components[p.component_ref].component_type, "value",
|
|
graph.components[p.component_ref].component_type)) == "ic")
|
|
]
|
|
if not ics:
|
|
continue
|
|
bid = f"rail:{net.name}"
|
|
blocks.append(HierarchyBlock(
|
|
block_id=bid, kind="rail", ic_refs=sorted(set(ics)), nets=[net.name],
|
|
))
|
|
for ref in ics:
|
|
ref_block.setdefault(ref, bid)
|
|
|
|
net_block: dict[str, str] = {}
|
|
for b in blocks:
|
|
for n in b.nets:
|
|
net_block[n] = b.block_id
|
|
|
|
comps: list[HierarchyComponent] = []
|
|
for ref, comp in sorted(graph.components.items()):
|
|
pins = [
|
|
HierarchyPin(number=str(num), net=n or "")
|
|
for num, n in sorted(comp.pins.items(), key=lambda x: str(x[0]))
|
|
]
|
|
nets = sorted({p.net for p in pins if p.net})
|
|
bid = ref_block.get(ref)
|
|
if bid is None and nets:
|
|
for n in nets:
|
|
if n in net_block:
|
|
bid = net_block[n]
|
|
break
|
|
comps.append(HierarchyComponent(
|
|
ref=ref,
|
|
component_type=str(getattr(comp.component_type, "value", comp.component_type)),
|
|
pins=pins,
|
|
nets=nets,
|
|
block_id=bid,
|
|
))
|
|
|
|
hnets: list[HierarchyNet] = []
|
|
for name, net in sorted(graph.nets.items()):
|
|
refs = sorted({p.component_ref for p in net.pins})
|
|
bid = net_block.get(name)
|
|
if bid is None:
|
|
for r in refs:
|
|
if r in ref_block:
|
|
bid = ref_block[r]
|
|
break
|
|
hnets.append(HierarchyNet(
|
|
name=name,
|
|
net_type=str(getattr(net.net_type, "value", net.net_type)),
|
|
components=refs,
|
|
block_id=bid,
|
|
))
|
|
return HierarchyReport(components=comps, nets=hnets, blocks=blocks)
|
|
|
|
|
|
def check_hierarchy(graph: DesignGraph, plan: FunctionalGroupsReport | None = None) -> list[Finding]:
|
|
"""FACT: IC pin with an empty net. Skip unnamed power-flags."""
|
|
out: list[Finding] = []
|
|
for ref, comp in sorted(graph.components.items()):
|
|
if str(getattr(comp.component_type, "value", comp.component_type)) != "ic":
|
|
continue
|
|
dangling = [str(n) for n, net in comp.pins.items() if not (net or "").strip()]
|
|
if not dangling:
|
|
continue
|
|
rec = f"Connect {ref} pin(s) {', '.join(dangling)} to a named net, or mark NC."
|
|
out.append(Finding(
|
|
designator=ref,
|
|
mpn=comp.mpn or "",
|
|
aspect="hierarchy",
|
|
finding=f"{ref} pin(s) {', '.join(dangling)} have no net.",
|
|
facts=f"{ref} pins {dangling} net=''.",
|
|
requirement="Every IC pin lands on a named net (or is explicitly NC).",
|
|
inference="Empty pin map is a graph FACT, not a layout millimetre claim.",
|
|
why="Component → pin → net hierarchy.",
|
|
status="INFO",
|
|
recommendation=rec,
|
|
action=rec,
|
|
source="hierarchy_check",
|
|
rule_id="PE-HIER-001",
|
|
finding_class="INFO",
|
|
provenance="TYPICAL",
|
|
evidence_status="SUFFICIENT",
|
|
pins=[f"{ref}.{p}" for p in dangling],
|
|
))
|
|
_ = plan
|
|
return out
|