Add DC-bias C_eff stima to derating and INFO when bulk C has no HF ceramic.

Keep both as labelled estimates: no Murata lot curve and no invented Z(f) target without f_sw.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 22:23:05 +02:00
co-authored by Cursor
parent 4fca789517
commit 4403c38d96
11 changed files with 412 additions and 2 deletions
+109
View File
@@ -0,0 +1,109 @@
"""HF decoupling coverage — bulk without a small ceramic.
Without a switching frequency this does not invent a Z(f) target.
INFO only: HF coverage depends on a ~100 nF close to the pin.
"""
from __future__ import annotations
from backend.pinscopex.models import ComponentType, DesignGraph, Finding, NetType
from backend.pinscopex.passive_rail_check import (
_cap_farads,
_is_ground_net,
_is_ic_supply_pin,
_is_nc_net,
_is_regulator_output_pin,
_pin_label,
)
from backend.pinscopex.validate import _match_constraints
_BULK_MIN_F = 1e-6
_HF_MAX_F = 1e-6
_HF_MIN_F = 1e-9
def _esl_hint(footprint: str) -> str:
fp = (footprint or "").upper()
if "0402" in fp:
return "typical ESL ~0.4 nH (0402 stima)"
if "0603" in fp:
return "typical ESL ~0.6 nH (0603 stima)"
if "0805" in fp:
return "typical ESL ~0.8 nH (0805 stima)"
return "ESL depends on package (stima)"
def _valued_gnd_caps(graph: DesignGraph, net_name: str) -> list[tuple[str, float]]:
out: list[tuple[str, float]] = []
unknown = False
for ref in graph.capacitors_on_net(net_name):
cap = graph.components[ref]
others = {n for n in cap.pins.values() if n != net_name}
if not any(_is_ground_net(graph, n) for n in others):
continue
farads = _cap_farads(cap)
if farads is None:
unknown = True
continue
out.append((ref, farads))
if unknown:
return []
return out
def check_hf_decoupling_coverage(
graph: DesignGraph,
constraints_map: dict,
) -> list[Finding]:
findings: 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, constraints_map)
for pin_num, net_name in sorted(comp.pins.items(), key=lambda x: str(x[0])):
if net_name in seen or _is_nc_net(net_name):
continue
is_rail = _is_ic_supply_pin(graph, cons, pin_num, net_name) or (
_is_regulator_output_pin(cons, pin_num)
)
if not is_rail:
continue
net = graph.nets.get(net_name)
if net and net.net_type == NetType.GROUND:
continue
seen.add(net_name)
caps = _valued_gnd_caps(graph, net_name)
if not caps:
continue
has_bulk = any(c >= _BULK_MIN_F for _, c in caps)
has_hf = any(_HF_MIN_F <= c < _HF_MAX_F for _, c in caps)
if not (has_bulk and not has_hf):
continue
bulk_ref = next(r for r, c in caps if c >= _BULK_MIN_F)
fp = graph.components[bulk_ref].footprint
pin_label = _pin_label(cons, pin_num, net_name)
findings.append(Finding(
designator=ref,
mpn=comp.mpn or "",
aspect="decoupling",
source="hf_coverage_check",
status="INFO",
finding=(
f"{ref} net '{net_name}' ({pin_label}) has bulk capacitance "
f"but no ~100 nF ceramic for HF."
),
why=(
f"Parallel Z(f) of large C is inductive above a few hundred "
f"kHz ({_esl_hint(fp)}). Without f_sw this is not an Ω target."
),
recommendation=(
f"Add a 10100 nF ceramic from '{net_name}' to ground near "
f"{ref}, in parallel with the bulk cap."
),
reference="netlist topology (stima)",
net=net_name,
pins=[f"{ref}.{pin_num}"],
rule_id="PS-ESR-001",
))
return findings