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:
@@ -5,11 +5,62 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from backend.pinscopex.models import ComponentType, DesignGraph, NetType
|
from backend.pinscopex.models import ComponentType, DesignGraph, NetType
|
||||||
|
from backend.pinscopex.resolve_passives import _format_value
|
||||||
from backend.pinscopex.utils import natural_sort_key
|
from backend.pinscopex.utils import natural_sort_key
|
||||||
|
|
||||||
# Dielectric strings that indicate ceramic capacitors
|
# Dielectric strings that indicate ceramic capacitors
|
||||||
_CERAMIC_DIELECTRICS = {"X7R", "X5R", "C0G", "NP0", "Y5V", "X7S", "X6S", "X8R", "C0G (NP0)"}
|
_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:
|
def _parse_voltage_rating(s: str | None) -> float | None:
|
||||||
"""Extract numeric voltage from a rating string like '16V', '25V', '2.5V'."""
|
"""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
|
rated_v: float | None = None
|
||||||
value_fmt: str | None = None
|
value_fmt: str | None = None
|
||||||
dielectric: str | None = None
|
dielectric: str | None = None
|
||||||
|
c_nom: float | None = None
|
||||||
if comp.specs and hasattr(comp.specs, "voltage_rating_v"):
|
if comp.specs and hasattr(comp.specs, "voltage_rating_v"):
|
||||||
rated_v = _parse_voltage_rating(comp.specs.voltage_rating_v)
|
rated_v = _parse_voltage_rating(comp.specs.voltage_rating_v)
|
||||||
value_fmt = getattr(comp.specs, "value_formatted", None)
|
value_fmt = getattr(comp.specs, "value_formatted", None)
|
||||||
dielectric = getattr(comp.specs, "dielectric", None)
|
dielectric = getattr(comp.specs, "dielectric", None)
|
||||||
|
c_nom = getattr(comp.specs, "value_farads", None)
|
||||||
|
|
||||||
# Operating voltage: max non-zero voltage among connected nets
|
# Operating voltage: max non-zero voltage among connected nets
|
||||||
op_voltage: float | None = None
|
op_voltage: float | None = None
|
||||||
@@ -107,6 +160,10 @@ def build_derating_table(graph: DesignGraph) -> list[dict]:
|
|||||||
net_minus = by_v[0][0]
|
net_minus = by_v[0][0]
|
||||||
net_plus = by_v[-1][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({
|
rows.append({
|
||||||
"designator": comp.reference,
|
"designator": comp.reference,
|
||||||
"mpn": comp.mpn,
|
"mpn": comp.mpn,
|
||||||
@@ -117,6 +174,12 @@ def build_derating_table(graph: DesignGraph) -> list[dict]:
|
|||||||
"net_plus": net_plus,
|
"net_plus": net_plus,
|
||||||
"net_minus": net_minus,
|
"net_minus": net_minus,
|
||||||
"dielectric_category": _dielectric_category(comp.component_subtype, dielectric),
|
"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"]))
|
rows.sort(key=lambda r: natural_sort_key(r["designator"]))
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from backend.pinscopex.passive_rail_check import (
|
|||||||
check_supply_decoupling,
|
check_supply_decoupling,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.bom_match_check import check_bom_schematic_match
|
from backend.pinscopex.bom_match_check import check_bom_schematic_match
|
||||||
|
from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage
|
||||||
|
|
||||||
|
|
||||||
class EvalScores(BaseModel):
|
class EvalScores(BaseModel):
|
||||||
@@ -81,6 +82,7 @@ def run_deterministic_on_graph(graph: DesignGraph) -> list[Finding]:
|
|||||||
out.extend(check_i2c_pullups(graph, cmap))
|
out.extend(check_i2c_pullups(graph, cmap))
|
||||||
out.extend(check_reset_pullups(graph, cmap))
|
out.extend(check_reset_pullups(graph, cmap))
|
||||||
out.extend(check_bom_schematic_match(graph.schematic_fields, graph.bom_fields))
|
out.extend(check_bom_schematic_match(graph.schematic_fields, graph.bom_fields))
|
||||||
|
out.extend(check_hf_decoupling_coverage(graph, cmap))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 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="PS-ESR-001",
|
||||||
|
))
|
||||||
|
return findings
|
||||||
@@ -50,6 +50,7 @@ from backend.pinscopex.passive_rail_check import (
|
|||||||
check_supply_decoupling,
|
check_supply_decoupling,
|
||||||
)
|
)
|
||||||
from backend.pinscopex.bom_match_check import check_bom_schematic_match
|
from backend.pinscopex.bom_match_check import check_bom_schematic_match
|
||||||
|
from backend.pinscopex.hf_coverage_check import check_hf_decoupling_coverage
|
||||||
|
|
||||||
TRACE_VERSION = 1
|
TRACE_VERSION = 1
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ def _run_deterministic_checks(
|
|||||||
("bom_match_check", lambda: check_bom_schematic_match(
|
("bom_match_check", lambda: check_bom_schematic_match(
|
||||||
graph.schematic_fields, graph.bom_fields,
|
graph.schematic_fields, graph.bom_fields,
|
||||||
)),
|
)),
|
||||||
|
("hf_coverage_check", lambda: check_hf_decoupling_coverage(graph, constraints_map)),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
out.extend(fn())
|
out.extend(fn())
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
What's new in Pinscope.
|
What's new in Pinscope.
|
||||||
|
|
||||||
|
## 2.13.0 — 2026-09-10 — DC-bias C_eff stima
|
||||||
|
|
||||||
|
The derating table now shows an effective capacitance under DC bias for C0G/X7R/X5R ceramics. It is labelled *stima* — not a vendor lot curve.
|
||||||
|
|
||||||
|
- [New] `C_eff` column from an empirical V/Vrated table. Tantalum/electrolytic and unknown dielectrics are left blank.
|
||||||
|
- [Improved] C0G/NP0 stays at nominal C; X7R at 50% of rated V is about 70% of C.
|
||||||
|
- [New] Bulk C without a ~100 nF ceramic is `PS-ESR-001` INFO (no invented Z(f) target).
|
||||||
|
|
||||||
## 2.12.0 — 2026-09-10 — Pull-up sizing and LDO Cout
|
## 2.12.0 — 2026-09-10 — Pull-up sizing and LDO Cout
|
||||||
|
|
||||||
Deterministic schema checks now size I2C pull-ups, flag NRST pull-downs, and look at LDO VOUT capacitance — still WARNING, never a invented datasheet µF ERROR.
|
Deterministic schema checks now size I2C pull-ups, flag NRST pull-downs, and look at LDO VOUT capacitance — still WARNING, never a invented datasheet µF ERROR.
|
||||||
|
|||||||
@@ -751,6 +751,9 @@ function DeratingTable({
|
|||||||
<span className="text-[10px] text-rose-600 dark:text-rose-400">{failCount} fail</span>
|
<span className="text-[10px] text-rose-600 dark:text-rose-400">{failCount} fail</span>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<p className="text-[11px] text-muted-foreground pt-1">
|
||||||
|
C_eff is an empirical DC-bias stima (C0G/X7R/X5R), not a Murata lot curve.
|
||||||
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -760,6 +763,7 @@ function DeratingTable({
|
|||||||
<th className="pb-2 pr-4 font-medium">Designator</th>
|
<th className="pb-2 pr-4 font-medium">Designator</th>
|
||||||
<th className="pb-2 pr-4 font-medium">MPN</th>
|
<th className="pb-2 pr-4 font-medium">MPN</th>
|
||||||
<th className="pb-2 pr-4 font-medium">Value</th>
|
<th className="pb-2 pr-4 font-medium">Value</th>
|
||||||
|
<th className="pb-2 pr-4 font-medium">C_eff</th>
|
||||||
<th className="pb-2 pr-4 font-medium">Type</th>
|
<th className="pb-2 pr-4 font-medium">Type</th>
|
||||||
<th className="pb-2 pr-4 font-medium">Net+</th>
|
<th className="pb-2 pr-4 font-medium">Net+</th>
|
||||||
<th className="pb-2 pr-4 font-medium">Net−</th>
|
<th className="pb-2 pr-4 font-medium">Net−</th>
|
||||||
@@ -800,6 +804,18 @@ function DeratingTable({
|
|||||||
<td className="py-2 pr-4 font-mono text-xs">
|
<td className="py-2 pr-4 font-mono text-xs">
|
||||||
{row.value_formatted ?? <span className="text-muted-foreground">—</span>}
|
{row.value_formatted ?? <span className="text-muted-foreground">—</span>}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="py-2 pr-4 font-mono text-xs">
|
||||||
|
{row.c_eff_formatted ? (
|
||||||
|
<span title="Empirical DC-bias stima, not a vendor lot curve">
|
||||||
|
{row.c_eff_formatted}
|
||||||
|
{row.dc_bias_model === "stima" && (
|
||||||
|
<span className="ml-1 text-[10px] text-muted-foreground">stima</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="py-2 pr-4 text-xs">
|
<td className="py-2 pr-4 text-xs">
|
||||||
{row.dielectric_category ? (
|
{row.dielectric_category ? (
|
||||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 capitalize">
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0 capitalize">
|
||||||
|
|||||||
@@ -305,6 +305,12 @@ export interface DeratingRow {
|
|||||||
net_plus: string | null;
|
net_plus: string | null;
|
||||||
net_minus: string | null;
|
net_minus: string | null;
|
||||||
dielectric_category: "ceramic" | "tantalum" | "electrolytic" | null;
|
dielectric_category: "ceramic" | "tantalum" | "electrolytic" | null;
|
||||||
|
dielectric?: string | null;
|
||||||
|
c_nominal_f?: number | null;
|
||||||
|
dc_bias_factor?: number | null;
|
||||||
|
c_eff_f?: number | null;
|
||||||
|
c_eff_formatted?: string | null;
|
||||||
|
dc_bias_model?: "stima" | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeratingSettings {
|
export interface DeratingSettings {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"min_nets": 30,
|
"min_nets": 30,
|
||||||
"deterministic_keys": [
|
"deterministic_keys": [
|
||||||
"PS-I2C-001|U3|/I2C0.SDA",
|
"PS-I2C-001|U3|/I2C0.SDA",
|
||||||
"PS-I2C-001|U3|/I2C0.SCL"
|
"PS-I2C-001|U3|/I2C0.SCL",
|
||||||
|
"PS-ESR-001|U1|+3V3"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -85,8 +85,9 @@ def test_simple_project_eval_matches_committed_golden():
|
|||||||
assert scores.graph_ok, scores.graph_errors
|
assert scores.graph_ok, scores.graph_errors
|
||||||
assert scores.precision == 1.0
|
assert scores.precision == 1.0
|
||||||
assert scores.recall == 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["WARNING"] == 2
|
||||||
|
assert scores.by_status["INFO"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_simple_project_eval_rejects_truncated_graph(tmp_path: Path):
|
def test_simple_project_eval_rejects_truncated_graph(tmp_path: Path):
|
||||||
|
|||||||
@@ -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()) == []
|
||||||
Reference in New Issue
Block a user