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>
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""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 10–100 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
|