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
+63
View File
@@ -5,11 +5,62 @@ from __future__ import annotations
import re
from backend.pinscopex.models import ComponentType, DesignGraph, NetType
from backend.pinscopex.resolve_passives import _format_value
from backend.pinscopex.utils import natural_sort_key
# Dielectric strings that indicate ceramic capacitors
_CERAMIC_DIELECTRICS = {"X7R", "X5R", "C0G", "NP0", "Y5V", "X7S", "X6S", "X8R", "C0G (NP0)"}
# Remaining C/C0 vs V/Vrated. Empirical stima, not a vendor lot curve.
_BIAS_CURVES: dict[str, list[tuple[float, float]]] = {
"c0g": [(0.0, 1.0), (1.2, 1.0)],
"x7r": [(0.0, 1.0), (0.25, 0.90), (0.50, 0.70), (0.75, 0.45), (1.0, 0.30), (1.2, 0.22)],
"x5r": [(0.0, 1.0), (0.25, 0.82), (0.50, 0.55), (0.75, 0.32), (1.0, 0.18), (1.2, 0.12)],
"y5v": [(0.0, 1.0), (0.25, 0.50), (0.50, 0.20), (0.80, 0.12), (1.0, 0.10)],
}
def _lerp(curve: list[tuple[float, float]], x: float) -> float:
if x <= curve[0][0]:
return curve[0][1]
for (x0, y0), (x1, y1) in zip(curve, curve[1:]):
if x <= x1:
if x1 == x0:
return y1
t = (x - x0) / (x1 - x0)
return y0 + t * (y1 - y0)
return curve[-1][1]
def _bias_family(dielectric: str | None) -> str | None:
if not dielectric:
return None
u = dielectric.upper()
if "C0G" in u or "NP0" in u or "NPO" in u:
return "c0g"
if "Y5V" in u:
return "y5v"
if "X5R" in u or "X6S" in u:
return "x5r"
if "X7R" in u or "X7S" in u or "X8R" in u:
return "x7r"
return None
def dc_bias_remaining(
dielectric: str | None,
v_op: float | None,
rated_v: float | None,
) -> float | None:
"""Fraction of nominal C remaining under DC bias, or None if not modelled.
Labelled a *stima*: class-2 MLCC curves vary by lot, thickness and vendor.
"""
family = _bias_family(dielectric)
if family is None or v_op is None or rated_v is None or rated_v <= 0:
return None
return _lerp(_BIAS_CURVES[family], max(0.0, v_op) / rated_v)
def _parse_voltage_rating(s: str | None) -> float | None:
"""Extract numeric voltage from a rating string like '16V', '25V', '2.5V'."""
@@ -64,10 +115,12 @@ def build_derating_table(graph: DesignGraph) -> list[dict]:
rated_v: float | None = None
value_fmt: str | None = None
dielectric: str | None = None
c_nom: float | None = None
if comp.specs and hasattr(comp.specs, "voltage_rating_v"):
rated_v = _parse_voltage_rating(comp.specs.voltage_rating_v)
value_fmt = getattr(comp.specs, "value_formatted", None)
dielectric = getattr(comp.specs, "dielectric", None)
c_nom = getattr(comp.specs, "value_farads", None)
# Operating voltage: max non-zero voltage among connected nets
op_voltage: float | None = None
@@ -107,6 +160,10 @@ def build_derating_table(graph: DesignGraph) -> list[dict]:
net_minus = by_v[0][0]
net_plus = by_v[-1][0]
factor = dc_bias_remaining(dielectric, op_voltage, rated_v)
c_eff = (c_nom * factor) if (c_nom is not None and factor is not None) else None
c_eff_fmt = _format_value(c_eff, "F") if c_eff is not None else None
rows.append({
"designator": comp.reference,
"mpn": comp.mpn,
@@ -117,6 +174,12 @@ def build_derating_table(graph: DesignGraph) -> list[dict]:
"net_plus": net_plus,
"net_minus": net_minus,
"dielectric_category": _dielectric_category(comp.component_subtype, dielectric),
"dielectric": dielectric,
"c_nominal_f": c_nom,
"dc_bias_factor": factor,
"c_eff_f": c_eff,
"c_eff_formatted": c_eff_fmt,
"dc_bias_model": "stima" if factor is not None else None,
})
rows.sort(key=lambda r: natural_sort_key(r["designator"]))