Files
periscope/backend/periscopex/hf_coverage_check.py
T
micheleandCursor 8d2b85600f Rebrand Pinscope to Periscope across product and codebase.
Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 20:02:04 +02:00

110 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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.periscopex.models import ComponentType, DesignGraph, Finding, NetType
from backend.periscopex.passive_rail_check import (
_cap_farads,
_is_ground_net,
_is_ic_supply_pin,
_is_nc_net,
_is_regulator_output_pin,
_pin_label,
)
from backend.periscopex.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="PE-ESR-001",
))
return findings