Files
periscope/backend/periscopex/emi_check.py
T
michele 1b3529501f Ship remaining PCB plan slices: BOM chain, SPOF, EMI FACT, gated Tj.
Package/pinout/ratings mismatches, SPOF as REVIEW, EMI only with a
datasheet quote, and Tj from copper+vias+θJA when every parameter exists.
Re-extract SI layout_rules from 1.12.0; ImpedenceFinder license stays UNKNOWN.
2026-09-20 12:29:27 +02:00

134 lines
4.6 KiB
Python

"""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