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 @@
"""DC-bias C_eff stima — not a Murata lot curve.
Favor: C0G stays at C; X7R at 50% Vr loses ~30%; C_eff formatted.
Against: tantalum uses C_nom (no MLCC model); missing Vr or C skips C_eff;
C0G is not treated as X7R.
"""
from __future__ import annotations
from backend.pinscopex.derating import (
build_derating_table,
dc_bias_remaining,
)
from backend.pinscopex.models import (
CapacitorSpecs,
Component,
ComponentType,
DesignGraph,
Net,
NetType,
PinConnection,
)
def test_c0g_keeps_full_capacitance():
assert dc_bias_remaining("C0G", v_op=16.0, rated_v=16.0) == 1.0
assert dc_bias_remaining("NP0", v_op=10.0, rated_v=16.0) == 1.0
def test_x7r_at_half_rated_is_about_70_percent():
f = dc_bias_remaining("X7R", v_op=8.0, rated_v=16.0)
assert f is not None
assert 0.65 <= f <= 0.75
def test_x7r_at_zero_bias_is_nominal():
assert dc_bias_remaining("X7R", v_op=0.0, rated_v=16.0) == 1.0
def test_tantalum_has_no_mlcc_bias_model():
assert dc_bias_remaining("tantalum", v_op=8.0, rated_v=16.0) is None
def test_missing_voltage_or_value_skips_c_eff():
assert dc_bias_remaining("X7R", v_op=None, rated_v=16.0) is None
assert dc_bias_remaining("X7R", v_op=8.0, rated_v=None) is None
def _cap(ref, dielectric, farads, rated, net="3V3"):
return Component(
reference=ref, value="", footprint="",
component_type=ComponentType.CAPACITOR,
component_subtype="passive.capacitor.ceramic",
mpn=ref,
pins={"1": net, "2": "GND"},
specs=CapacitorSpecs(
value_farads=farads,
value_formatted="10uF",
voltage_rating_v=f"{rated}V",
dielectric=dielectric,
),
)
def test_derating_row_includes_c_eff_stima():
c1 = _cap("C1", "X7R", 10e-6, 16)
g = DesignGraph(
components={
"C1": c1,
},
nets={
"3V3": Net(
name="3V3", net_type=NetType.POWER, voltage=8.0,
pins=[PinConnection(component_ref="C1", pin_number="1")],
),
"GND": Net(
name="GND", net_type=NetType.GROUND, voltage=0.0,
pins=[PinConnection(component_ref="C1", pin_number="2")],
),
},
)
rows = build_derating_table(g)
assert len(rows) == 1
row = rows[0]
assert row["dc_bias_model"] == "stima"
assert row["c_nominal_f"] == 10e-6
assert row["c_eff_f"] is not None
assert 6.5e-6 <= row["c_eff_f"] <= 7.5e-6
assert "uF" in (row["c_eff_formatted"] or "")
def test_c0g_row_c_eff_equals_nominal():
c1 = _cap("C9", "C0G", 18e-12, 50)
g = DesignGraph(
components={"C9": c1},
nets={
"3V3": Net(
name="3V3", net_type=NetType.POWER, voltage=3.3,
pins=[PinConnection(component_ref="C9", pin_number="1")],
),
"GND": Net(
name="GND", net_type=NetType.GROUND,
pins=[PinConnection(component_ref="C9", pin_number="2")],
),
},
)
row = build_derating_table(g)[0]
assert row["c_eff_f"] == 18e-12
assert row["dc_bias_factor"] == 1.0
+2 -1
View File
@@ -85,8 +85,9 @@ def test_simple_project_eval_matches_committed_golden():
assert scores.graph_ok, scores.graph_errors
assert scores.precision == 1.0
assert scores.recall == 1.0
assert scores.finding_count == 2
assert scores.finding_count == 3
assert scores.by_status["WARNING"] == 2
assert scores.by_status["INFO"] == 1
def test_simple_project_eval_rejects_truncated_graph(tmp_path: Path):
+93
View File
@@ -0,0 +1,93 @@
"""HF coverage INFO when bulk C exists without a 100 nF-class ceramic."""
from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage
from backend.pinscopex.models import (
CapacitorSpecs,
Component,
ComponentConstraints,
ComponentType,
DesignGraph,
Net,
NetType,
Pin,
PinConnection,
)
def _graph(components, nets):
net_objs = {
name: Net(
name=name, net_type=ntype,
pins=[PinConnection(component_ref=r, pin_number=str(p)) for r, p in conns],
)
for name, (ntype, conns) in nets.items()
}
return DesignGraph(components=components, nets=net_objs)
def _ic():
return Component(
reference="U1", value="", footprint="",
component_type=ComponentType.IC, mpn="UTEST",
pins={"1": "3V3", "2": "GND"},
)
def _cmap():
return {
"UTEST": ComponentConstraints(
mpn="UTEST",
pintable=[Pin(number=1, name="VDD"), Pin(number=2, name="GND")],
absolute_maximum_ratings=[], rules=[],
)
}
def _cap(ref, farads, net="3V3"):
return Component(
reference=ref, value="", footprint="C_0603",
component_type=ComponentType.CAPACITOR, mpn=ref,
pins={"1": net, "2": "GND"},
specs=CapacitorSpecs(value_farads=farads, value_formatted="x"),
)
def test_bulk_only_is_info_ps_esr_001():
g = _graph(
{"U1": _ic(), "C1": _cap("C1", 10e-6)},
{
"3V3": (NetType.POWER, [("U1", "1"), ("C1", "1")]),
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
},
)
findings = check_hf_decoupling_coverage(g, _cmap())
assert len(findings) == 1
assert findings[0].rule_id == "PS-ESR-001"
assert findings[0].status == "INFO"
def test_bulk_plus_100n_is_silent():
g = _graph(
{"U1": _ic(), "C1": _cap("C1", 10e-6), "C2": _cap("C2", 100e-9)},
{
"3V3": (NetType.POWER, [("U1", "1"), ("C1", "1"), ("C2", "1")]),
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2"), ("C2", "2")]),
},
)
assert check_hf_decoupling_coverage(g, _cmap()) == []
def test_unknown_cap_value_is_not_guessed():
c = Component(
reference="C1", value="", footprint="",
component_type=ComponentType.CAPACITOR, mpn="C1",
pins={"1": "3V3", "2": "GND"},
)
g = _graph(
{"U1": _ic(), "C1": c},
{
"3V3": (NetType.POWER, [("U1", "1"), ("C1", "1")]),
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
},
)
assert check_hf_decoupling_coverage(g, _cmap()) == []