Files
periscope/backend/periscopex/spof_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

135 lines
4.7 KiB
Python

"""Single-point-of-failure on power rails — REVIEW, never RULE ERROR.
FACT: how many regulators drive a rail and how many ICs sit on it.
REQUIREMENT: redundancy is not a datasheet shall unless stated.
INFERENCE: this rail is a SPOF — review, do not invent IEC SIL.
"""
from __future__ import annotations
from collections import defaultdict
from backend.periscopex.models import (
ComponentConstraints,
ComponentType,
DesignGraph,
Finding,
NetType,
)
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
from backend.periscopex.power_margin_check import _regulators
_SKIP_RAIL = frozenset({"GND", "AGND", "DGND", "PGND", "VSS"})
def check_spof(
graph: DesignGraph,
constraints_map: dict[str, ComponentConstraints] | None = None,
) -> list[Finding]:
cmap = constraints_map or {}
producers: dict[str, list[str]] = defaultdict(list)
for ref, _comp, _cons, _vin, vout in _regulators(graph, cmap):
key = normalize_kicad_hierarchy_net(vout)
if key.upper() in _SKIP_RAIL:
continue
producers[key].append(ref)
out: list[Finding] = []
seen: set[str] = set()
for net_name, net in sorted(graph.nets.items()):
key = normalize_kicad_hierarchy_net(net_name)
if key in seen or key.upper() in _SKIP_RAIL:
continue
if net.net_type != NetType.POWER and not producers.get(key):
continue
regs = producers.get(key) or []
if len(regs) != 1:
continue
ics = [
r for r in graph.components_on_net(net_name)
if r in graph.components
and graph.components[r].component_type == ComponentType.IC
and r not in regs
]
if len(ics) < 1:
continue
seen.add(key)
reg = regs[0]
rcomp = graph.components[reg]
rec = (
f"Review whether '{net_name}' needs a second source; {reg} is the "
f"only regulator feeding {len(ics)} IC(s). This is not a design-rule ERROR."
)
out.append(Finding(
designator=reg,
mpn=rcomp.mpn or "",
aspect="spof",
finding=(
f"Rail '{net_name}' has a single regulator ({reg}) and "
f"{len(ics)} downstream IC(s) ({', '.join(ics[:8])}"
f"{'…' if len(ics) > 8 else ''})."
),
facts=(
f"producers={regs}; downstream_ics={ics}; "
f"net_type={net.net_type.value}."
),
requirement=(
"Redundancy / dual-supply is REVIEW unless a datasheet shall "
"names a second source (none assumed)."
),
inference="Single point of failure on this rail — engineering review.",
why="SPOF is not a measured RULE; no IEC SIL invented.",
status="INFO",
recommendation=rec,
action=rec,
source="spof_check",
rule_id="PE-SPOF-001",
evidence_status="SUFFICIENT",
net=net_name,
pins=[],
))
# Crystal / clock: one XTAL feeding one MCU is normal — only flag when
# several ICs share one crystal net with no second oscillator FACT.
for ref, comp in sorted(graph.components.items()):
if comp.component_type != ComponentType.CRYSTAL:
continue
nets = [n for n in set(comp.pins.values()) if n]
ics: list[str] = []
for n in nets:
for r in graph.components_on_net(n):
c = graph.components.get(r)
if c and c.component_type == ComponentType.IC and r not in ics:
ics.append(r)
xtals = [
r for r, c in graph.components.items()
if c.component_type == ComponentType.CRYSTAL
]
if len(ics) < 2 or len(xtals) > 1:
continue
rec = (
f"Review clock redundancy: {ref} is the only crystal and feeds "
f"{len(ics)} ICs. Not a RULE."
)
out.append(Finding(
designator=ref,
mpn=comp.mpn or "",
aspect="spof",
finding=(
f"{ref} is the only crystal and clocks {len(ics)} ICs "
f"({', '.join(ics)})."
),
facts=f"xtals={xtals}; ics_on_crystal_nets={ics}.",
requirement="A second oscillator is not assumed from IEC or folklore.",
inference="Clock SPOF — REVIEW.",
why="Counted crystals and IC pins on those nets.",
status="INFO",
recommendation=rec,
action=rec,
source="spof_check",
rule_id="PE-SPOF-001",
evidence_status="SUFFICIENT",
pins=[],
))
return out