"""Trace / bus inventory from a LayoutGraph. Numbers only, no findings.""" from __future__ import annotations import re from collections import defaultdict from pydantic import BaseModel from backend.periscopex.models import DesignGraph, LayoutGraph, NetType from backend.periscopex.si_check import net_length_mm, partner_net _BUS_RE = re.compile(r"^(.*?)(\d+)$") class PcbNetInventory(BaseModel): name: str length_mm: float = 0.0 via_count: int = 0 layers: list[str] = [] width_min_mm: float | None = None width_max_mm: float | None = None pair: str | None = None bus: str | None = None net_type: str | None = None z0_ohm: float | None = None class PcbInventoryReport(BaseModel): nets: list[PcbNetInventory] = [] domains: list[str] = [] group_count: int = 0 def _bus_name(net: str) -> str | None: m = _BUS_RE.match(net.split("/")[-1].replace(".", "_")) if not m: return None prefix, digits = m.group(1), m.group(2) if not prefix or len(digits) > 3: return None return prefix.rstrip("_") or None def build_pcb_inventory( layout: LayoutGraph, graph: DesignGraph | None = None, *, domain_ids: list[str] | None = None, group_count: int = 0, z0_by_net: dict[str, float] | None = None, ) -> PcbInventoryReport: names: set[str] = set() layers: dict[str, set[str]] = defaultdict(set) widths: dict[str, list[float]] = defaultdict(list) vias: dict[str, int] = defaultdict(int) for s in layout.segments: if not s.net: continue names.add(s.net) if s.layer: layers[s.net].add(s.layer) if s.width > 0: widths[s.net].append(s.width) for v in layout.vias: if v.net: names.add(v.net) vias[v.net] += 1 z0_by_net = z0_by_net or {} rows: list[PcbNetInventory] = [] for name in sorted(names): w = widths.get(name) or [] sch = graph.nets.get(name) if graph else None nt = sch.net_type.value if sch and isinstance(sch.net_type, NetType) else ( str(sch.net_type) if sch and sch.net_type else None ) partner = partner_net(name) if partner and partner not in names: partner = None rows.append(PcbNetInventory( name=name, length_mm=round(net_length_mm(layout, name), 3), via_count=vias.get(name, 0), layers=sorted(layers.get(name, ())), width_min_mm=min(w) if w else None, width_max_mm=max(w) if w else None, pair=partner, bus=_bus_name(name), net_type=nt, z0_ohm=z0_by_net.get(name), )) return PcbInventoryReport( nets=rows, domains=list(domain_ids or []), group_count=group_count, )