"""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(): if net.net_type.value != "power": continue ics = [ p.component_ref for p in net.pins if (graph.components.get(p.component_ref) and graph.components[p.component_ref].component_type.value == "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=comp.component_type.value, 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=net.net_type.value, 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 comp.component_type.value != "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