"""Reset RC and strap dividers — only when R, C, and datasheet times/levels exist.""" from __future__ import annotations from backend.periscopex.models import ( CapacitorSpecs, ComponentConstraints, ComponentType, DesignGraph, Finding, ResistorSpecs, ) from backend.periscopex.passive_rail_check import ( _is_reset_pin, _pin_name_tokens, _resistor_to_ground, _resistor_to_power, ) from backend.periscopex.validate import _match_constraints _T_RESET_KEYS = ("t_reset_min_s", "t_reset_min_ms", "reset_delay_ms", "t_por_ms") _VIH_KEYS = ("vih", "vih_min_v", "v_ih_min", "vih_min") _VIL_KEYS = ("vil", "vil_max_v", "v_il_max", "vil_max") _STRAP_RE = __import__("re").compile( r"(?:PSEL|BOOT|STRAP|VSENSE|VSET|CFG)", __import__("re").I, ) def _specs_map(comp) -> dict: specs = getattr(comp, "specs", None) if specs is None: return {} vals = getattr(specs, "values", None) return dict(vals) if isinstance(vals, dict) else {} def _first_num(values: dict, keys: tuple[str, ...]) -> float | None: for k in keys: v = values.get(k) if isinstance(v, bool) or v is None: continue try: n = float(v) except (TypeError, ValueError): continue if k.endswith("_ms"): n = n / 1000.0 return n return None def _r_on_net(graph: DesignGraph, net: str) -> list[float]: out: list[float] = [] for ref in graph.components_on_net(net): comp = graph.components.get(ref) if not comp or comp.component_type != ComponentType.RESISTOR: continue if isinstance(comp.specs, ResistorSpecs) and comp.specs.value_ohms > 0: out.append(float(comp.specs.value_ohms)) return out def _c_on_net(graph: DesignGraph, net: str) -> list[float]: out: list[float] = [] for ref in graph.components_on_net(net): comp = graph.components.get(ref) if not comp or comp.component_type != ComponentType.CAPACITOR: continue if isinstance(comp.specs, CapacitorSpecs) and comp.specs.value_farads > 0: out.append(float(comp.specs.value_farads)) return out def check_timing( graph: DesignGraph, constraints_map: dict[str, ComponentConstraints] | None = None, ) -> list[Finding]: """PE-TIM-001 RC vs t_reset; PE-TIM-002 strap vs Vih/Vil. Skip without numbers.""" cmap = constraints_map or {} out: list[Finding] = [] seen: set[str] = set() for ref, comp in sorted(graph.components.items()): if comp.component_type != ComponentType.IC: continue cons = _match_constraints(comp.mpn or comp.value, cmap) values = _specs_map(comp) t_min = _first_num(values, _T_RESET_KEYS) vih = _first_num(values, _VIH_KEYS) vil = _first_num(values, _VIL_KEYS) for pin_num, net in sorted(comp.pins.items(), key=lambda x: str(x[0])): if not net or net in seen: continue if _is_reset_pin(graph, cons, pin_num, net) and t_min is not None: rs = _r_on_net(graph, net) cs = _c_on_net(graph, net) if not rs or not cs: continue tau = min(rs) * min(cs) seen.add(net) if tau + 1e-18 >= t_min: continue rec = ( f"Increase R or C on {net} so τ=RC ≥ {t_min:g} s " f"(measured τ={tau:g} s)." ) out.append(Finding( designator=ref, mpn=comp.mpn or "", aspect="timing", finding=( f"{ref} reset net {net} has τ=RC={tau:g} s " f"below t_reset={t_min:g} s." ), facts=f"R={min(rs):g} Ω, C={min(cs):g} F, τ={tau:g} s.", requirement=f"Datasheet t_reset_min={t_min:g} s.", inference="τ=R·C compared to t_reset; no 10 ms default.", why="Reset delay only when R, C, and t_reset are numeric.", status="ERROR", recommendation=rec, action=rec, source="timing_check", rule_id="PE-TIM-001", calculation=f"τ=R·C={tau:g}; t_min={t_min:g}", evidence_status="SUFFICIENT", net=net, pins=[f"{ref}.{pin_num}"], )) continue tokens = _pin_name_tokens(cons, pin_num) or [net, str(pin_num)] if not any(_STRAP_RE.search(t or "") for t in tokens): continue if vih is None and vil is None: continue if not (_resistor_to_power(graph, net) and _resistor_to_ground(graph, net)): continue rs = _r_on_net(graph, net) if len(rs) < 2: continue rhi, rlo = max(rs), min(rs) vrail = None nobj = graph.nets.get(net) # Strap mid-point: need rail voltage on the pull-up net. for rref in graph.components_on_net(net): rcomp = graph.components.get(rref) if not rcomp or rcomp.component_type != ComponentType.RESISTOR: continue for pnet in rcomp.pins.values(): other = graph.nets.get(pnet) if other and other.voltage and other.voltage > 0: vrail = float(other.voltage) if vrail is None: continue vstrap = vrail * rlo / (rhi + rlo) seen.add(net) fail = (vih is not None and vstrap < vih) or (vil is not None and vstrap > vil and vih is None) if not fail: continue rec = f"Adjust the strap divider on {net} so Vstrap meets Vih/Vil." out.append(Finding( designator=ref, mpn=comp.mpn or "", aspect="timing", finding=( f"{ref} strap {net} V={vstrap:g} V from {rhi:g}/{rlo:g} Ω " f"on {vrail:g} V rail." ), facts=f"Vstrap={vstrap:g} V; Rhi={rhi:g}; Rlo={rlo:g}; Vrail={vrail:g}.", requirement=( f"Vih={vih if vih is not None else '—'} V, " f"Vil={vil if vil is not None else '—'} V from specs." ), inference="Divider ratio vs Vih/Vil; no 50% default.", why="Strap level only when divider ohms and Vih/Vil exist.", status="ERROR", recommendation=rec, action=rec, source="timing_check", rule_id="PE-TIM-002", calculation=f"V=Vrail·Rlo/(Rhi+Rlo)={vstrap:g}", evidence_status="SUFFICIENT", net=net, pins=[f"{ref}.{pin_num}"], )) return out