"""EMI beyond ESD — only when a datasheet FACT names a filter. No IEC 61000 invention. Common-mode / ferrite / shield from layout_rules notes or kind, checked against parts on the same HS connector net. """ from __future__ import annotations import re from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, Finding, LayoutGraph, ) from backend.periscopex.pcb_net_match import ( normalize_kicad_hierarchy_net, refs_on_matched_net, ) from backend.periscopex.si_check import bus_class, skip_si_net from backend.periscopex.validate import _match_constraints _EMI_RE = re.compile( r"(emi|emc|common[-\s]?mode|cmc|choke|ferrite\s*bead|shield(?:ing)?|" r"pi\s*filter|lc\s*filter)", re.I, ) _EMI_KINDS = frozenset({"emi", "common_mode", "shield"}) def _is_filter_part(comp) -> bool: sub = (comp.component_subtype or "").lower() val = (comp.value or "").lower() blob = f"{sub} {val} {comp.mpn or ''}" if "esd" in sub or "protection" in sub: return False if "ferrite" in blob or "common" in blob or "choke" in blob or "cmc" in blob: return True if comp.component_type == ComponentType.INDUCTOR and _EMI_RE.search(blob): return True if (comp.reference or "").upper().startswith(("FB", "L")) and _EMI_RE.search(blob): return True return False def _emi_rules(cons: ComponentConstraints | None) -> list[dict]: if not cons: return [] rows: list[dict] = [] for rule in cons.layout_rules or []: if not isinstance(rule, dict): continue kind = str(rule.get("kind") or "") note = str(rule.get("note") or "") if kind in _EMI_KINDS or _EMI_RE.search(note) or _EMI_RE.search(kind): rows.append(rule) return rows def check_emi( graph: DesignGraph, constraints_map: dict[str, ComponentConstraints] | None = None, layout: LayoutGraph | None = None, ) -> list[Finding]: """REVIEW when a sourced EMI/filter FACT has no matching part on the net.""" _ = layout cmap = constraints_map or {} out: list[Finding] = [] seen: set[tuple[str, str]] = set() for ref, comp in sorted(graph.components.items()): if comp.component_type not in (ComponentType.IC, ComponentType.CONNECTOR): continue cons = _match_constraints(comp.mpn or comp.value, cmap) rules = _emi_rules(cons) if not rules: continue for pin, net in sorted(comp.pins.items(), key=lambda x: str(x[0])): if not net or skip_si_net(net): continue if not bus_class(net) and comp.component_type != ComponentType.CONNECTOR: continue key = (ref, normalize_kicad_hierarchy_net(net)) if key in seen: continue on_net = refs_on_matched_net(graph, net) if any( r in graph.components and _is_filter_part(graph.components[r]) for r in on_net ): continue quote = next( (str(r.get("note") or "") for r in rules if r.get("note")), "", ) page = next( (r.get("source_page") for r in rules if r.get("source_page")), None, ) rec = ( f"Add the datasheet EMI/filter part on '{net}' " f"(common-mode / ferrite / shield as quoted). Not IEC 61000." ) seen.add(key) out.append(Finding( designator=ref, mpn=comp.mpn or "", aspect="emi", finding=( f"{net} has a datasheet EMI/filter FACT and no ferrite/" f"common-mode/choke part on the net." ), facts=( f"net={net}; pin={pin}; filter_parts=none; " f"quote={quote!r}; page={page}." ), requirement=quote or "Datasheet layout_rules EMI/filter note.", inference="Filter missing vs sourced note — REVIEW, not IEC.", why="EMI only when the library quotes a filter; ESD is PE-ESD-001.", status="WARNING", recommendation=rec, action=rec, source="emi_check", source_page=int(page) if isinstance(page, int) else None, source_quote=quote, rule_id="PE-EMI-001", evidence_status="SUFFICIENT", net=net, pins=[str(pin)], )) return out