Add schematic filter topology and LDO/resistor thermal checks.
Match RC/LC/π/T without inventing fc, compare to ADC rate only when specs list it, and skip ferrite DCR unless the IC gives a limit. LDO Tj uses I_load not Iout_max, and missing θJA stays INFO. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
"""Filter topology matcher — RC/LC/π/T fc, ADC compare only with specs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.pinscopex.filter_check import check_filters
|
||||
from backend.pinscopex.models import (
|
||||
CapacitorSpecs,
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
InductorSpecs,
|
||||
Net,
|
||||
NetType,
|
||||
Pin,
|
||||
PinConnection,
|
||||
ResistorSpecs,
|
||||
SimpleComponentSpecs,
|
||||
)
|
||||
|
||||
|
||||
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 _res(ref, ohms, n1, n2):
|
||||
return Component(
|
||||
reference=ref, value=str(ohms), footprint="",
|
||||
component_type=ComponentType.RESISTOR, mpn=ref,
|
||||
pins={"1": n1, "2": n2},
|
||||
specs=ResistorSpecs(value_ohms=ohms, value_formatted=str(ohms)),
|
||||
)
|
||||
|
||||
|
||||
def _cap(ref, farads, net):
|
||||
return Component(
|
||||
reference=ref, value="", footprint="",
|
||||
component_type=ComponentType.CAPACITOR, mpn=ref,
|
||||
pins={"1": net, "2": "GND"},
|
||||
specs=CapacitorSpecs(value_farads=farads, value_formatted="x"),
|
||||
)
|
||||
|
||||
|
||||
def _l(ref, henries, n1, n2, ferrite=False, dcr=None):
|
||||
sub = "passive.ferrite_bead" if ferrite else "passive.inductor"
|
||||
kwargs = dict(value_formatted="x", component_subtype=sub, dcr_ohms=dcr)
|
||||
if ferrite:
|
||||
specs = InductorSpecs(impedance_ohm=100.0, value_henries=henries, **kwargs)
|
||||
else:
|
||||
specs = InductorSpecs(value_henries=henries, **kwargs)
|
||||
return Component(
|
||||
reference=ref, value="", footprint="",
|
||||
component_type=ComponentType.INDUCTOR, mpn=ref,
|
||||
component_subtype=sub,
|
||||
pins={"1": n1, "2": n2}, specs=specs,
|
||||
)
|
||||
|
||||
|
||||
def _ic(ref="U1", pins=None, values=None, mpn="UTEST"):
|
||||
return Component(
|
||||
reference=ref, value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn=mpn,
|
||||
pins=pins or {"1": "AIN", "2": "GND"},
|
||||
specs=SimpleComponentSpecs(
|
||||
specs_type="ic", values=values or {},
|
||||
) if values is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def test_rc_reports_fc_info_without_adc_rate():
|
||||
# 1k * 100nF -> fc ≈ 1.59 kHz; no sample rate → INFO not WARNING.
|
||||
g = _graph(
|
||||
{
|
||||
"U1": _ic(),
|
||||
"R1": _res("R1", 1e3, "AIN", "FILT"),
|
||||
"C1": _cap("C1", 100e-9, "FILT"),
|
||||
},
|
||||
{
|
||||
"AIN": (NetType.SIGNAL, [("U1", "1"), ("R1", "1")]),
|
||||
"FILT": (NetType.SIGNAL, [("R1", "2"), ("C1", "1")]),
|
||||
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_filters(g)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-FLT-001"
|
||||
assert findings[0].status == "INFO"
|
||||
assert findings[0].source == "filter_check"
|
||||
|
||||
|
||||
def test_rc_vs_adc_rate_is_warning():
|
||||
g = _graph(
|
||||
{
|
||||
"U1": _ic(values={"adc_sample_rate": 1e6}),
|
||||
"R1": _res("R1", 1e3, "AIN", "FILT"),
|
||||
"C1": _cap("C1", 100e-9, "FILT"),
|
||||
},
|
||||
{
|
||||
"AIN": (NetType.SIGNAL, [("U1", "1"), ("R1", "1")]),
|
||||
"FILT": (NetType.SIGNAL, [("R1", "2"), ("C1", "1")]),
|
||||
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_filters(g)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-FLT-002"
|
||||
assert findings[0].status == "WARNING"
|
||||
|
||||
|
||||
def test_pullup_plus_decoupling_is_not_a_filter():
|
||||
g = _graph(
|
||||
{
|
||||
"U1": Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="UTEST",
|
||||
pins={"1": "SDA", "2": "3V3", "3": "GND"},
|
||||
),
|
||||
"R1": _res("R1", 4700, "SDA", "3V3"),
|
||||
"C1": _cap("C1", 100e-9, "3V3"),
|
||||
},
|
||||
{
|
||||
"SDA": (NetType.SIGNAL, [("U1", "1"), ("R1", "1")]),
|
||||
"3V3": (NetType.POWER, [("U1", "2"), ("R1", "2"), ("C1", "1")]),
|
||||
"GND": (NetType.GROUND, [("U1", "3"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
assert check_filters(g) == []
|
||||
|
||||
|
||||
def test_missing_c_value_does_not_invent_fc_warning():
|
||||
c = Component(
|
||||
reference="C1", value="", footprint="",
|
||||
component_type=ComponentType.CAPACITOR, mpn="C1",
|
||||
pins={"1": "FILT", "2": "GND"},
|
||||
)
|
||||
g = _graph(
|
||||
{
|
||||
"U1": _ic(values={"adc_sample_rate": 1e6}),
|
||||
"R1": _res("R1", 1e3, "AIN", "FILT"),
|
||||
"C1": c,
|
||||
},
|
||||
{
|
||||
"AIN": (NetType.SIGNAL, [("U1", "1"), ("R1", "1")]),
|
||||
"FILT": (NetType.SIGNAL, [("R1", "2"), ("C1", "1")]),
|
||||
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_filters(g)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-FLT-001"
|
||||
assert findings[0].status == "INFO"
|
||||
|
||||
|
||||
def test_pi_and_t_need_l_and_c():
|
||||
g_pi = _graph(
|
||||
{
|
||||
"L1": _l("L1", 10e-6, "A", "B"),
|
||||
"C1": _cap("C1", 100e-9, "A"),
|
||||
"C2": _cap("C2", 100e-9, "B"),
|
||||
},
|
||||
{
|
||||
"A": (NetType.SIGNAL, [("L1", "1"), ("C1", "1")]),
|
||||
"B": (NetType.SIGNAL, [("L1", "2"), ("C2", "1")]),
|
||||
"GND": (NetType.GROUND, [("C1", "2"), ("C2", "2")]),
|
||||
},
|
||||
)
|
||||
pi = check_filters(g_pi)
|
||||
assert len(pi) == 1 and pi[0].rule_id == "PS-FLT-001" and "π" in pi[0].finding
|
||||
|
||||
g_t = _graph(
|
||||
{
|
||||
"L1": _l("L1", 10e-6, "A", "MID"),
|
||||
"L2": _l("L2", 10e-6, "MID", "B"),
|
||||
"C1": _cap("C1", 100e-9, "MID"),
|
||||
},
|
||||
{
|
||||
"A": (NetType.SIGNAL, [("L1", "1")]),
|
||||
"MID": (NetType.SIGNAL, [("L1", "2"), ("L2", "1"), ("C1", "1")]),
|
||||
"B": (NetType.SIGNAL, [("L2", "2")]),
|
||||
"GND": (NetType.GROUND, [("C1", "2")]),
|
||||
},
|
||||
)
|
||||
t = check_filters(g_t)
|
||||
assert len(t) == 1 and "T" in t[0].finding
|
||||
|
||||
|
||||
def test_ferrite_dcr_warns_only_with_datasheet_limit():
|
||||
cons = {
|
||||
"UTEST": ComponentConstraints(
|
||||
mpn="UTEST",
|
||||
pintable=[Pin(number=1, name="VDDA"), Pin(number=2, name="GND")],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
fb = _l("FB1", None, "VDDA", "3V3", ferrite=True, dcr=2.0)
|
||||
g = _graph(
|
||||
{
|
||||
"U1": _ic(pins={"1": "VDDA", "2": "GND"}, values={}),
|
||||
"FB1": fb,
|
||||
"C1": _cap("C1", 100e-9, "VDDA"),
|
||||
},
|
||||
{
|
||||
"VDDA": (NetType.POWER, [("U1", "1"), ("FB1", "1"), ("C1", "1")]),
|
||||
"3V3": (NetType.POWER, [("FB1", "2")]),
|
||||
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
assert not any(f.rule_id == "PS-FLT-003" for f in check_filters(g, cons))
|
||||
|
||||
g2 = _graph(
|
||||
{
|
||||
"U1": _ic(pins={"1": "VDDA", "2": "GND"}, values={"max_ferrite_dcr_ohms": 0.5}),
|
||||
"FB1": fb,
|
||||
"C1": _cap("C1", 100e-9, "VDDA"),
|
||||
},
|
||||
{
|
||||
"VDDA": (NetType.POWER, [("U1", "1"), ("FB1", "1"), ("C1", "1")]),
|
||||
"3V3": (NetType.POWER, [("FB1", "2")]),
|
||||
"GND": (NetType.GROUND, [("U1", "2"), ("C1", "2")]),
|
||||
},
|
||||
)
|
||||
dcr = [f for f in check_filters(g2, cons) if f.rule_id == "PS-FLT-003"]
|
||||
assert len(dcr) == 1 and dcr[0].status == "WARNING"
|
||||
@@ -0,0 +1,144 @@
|
||||
"""LDO/resistor thermal — no invented I_load or θJA."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.pinscopex.models import (
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Net,
|
||||
NetType,
|
||||
Pin,
|
||||
PinConnection,
|
||||
ResistorSpecs,
|
||||
SimpleComponentSpecs,
|
||||
)
|
||||
from backend.pinscopex.thermal_check import check_thermal
|
||||
|
||||
|
||||
def _graph(components, nets):
|
||||
net_objs = {}
|
||||
for name, (ntype, volt, conns) in nets.items():
|
||||
net_objs[name] = Net(
|
||||
name=name, net_type=ntype, voltage=volt,
|
||||
pins=[PinConnection(component_ref=r, pin_number=str(p)) for r, p in conns],
|
||||
)
|
||||
return DesignGraph(components=components, nets=net_objs)
|
||||
|
||||
|
||||
def _ldo(values, subtype="ic.power.ldo"):
|
||||
return Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, component_subtype=subtype,
|
||||
mpn="LDOX",
|
||||
pins={"1": "VIN", "2": "VOUT", "3": "GND"},
|
||||
specs=SimpleComponentSpecs(specs_type="ic", component_subtype=subtype, values=values),
|
||||
)
|
||||
|
||||
|
||||
def _cons():
|
||||
return {
|
||||
"LDOX": ComponentConstraints(
|
||||
mpn="LDOX",
|
||||
component_subtype="ic.power.ldo",
|
||||
pintable=[
|
||||
Pin(number=1, name="VIN"),
|
||||
Pin(number=2, name="VOUT"),
|
||||
Pin(number=3, name="GND"),
|
||||
],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def test_ldo_without_theta_ja_is_info():
|
||||
g = _graph(
|
||||
{"U1": _ldo({"i_load_a": 0.2})},
|
||||
{
|
||||
"VIN": (NetType.POWER, 5.0, [("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, 3.3, [("U1", "2")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("U1", "3")]),
|
||||
},
|
||||
)
|
||||
findings = check_thermal(g, _cons())
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-TH-001"
|
||||
assert findings[0].status == "INFO"
|
||||
assert "theta_ja" in findings[0].finding.lower() or "theta_ja" in findings[0].why.lower()
|
||||
|
||||
|
||||
def test_iout_max_is_not_used_as_load():
|
||||
g = _graph(
|
||||
{"U1": _ldo({"iout_max_a": 0.5, "theta_ja": 160})},
|
||||
{
|
||||
"VIN": (NetType.POWER, 5.0, [("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, 3.3, [("U1", "2")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("U1", "3")]),
|
||||
},
|
||||
)
|
||||
assert check_thermal(g, _cons()) == []
|
||||
|
||||
|
||||
def test_ldo_hot_tj_is_warning():
|
||||
# 0.5 A * 1.7 V = 0.85 W * 160 °C/W + 25 = 161 °C
|
||||
g = _graph(
|
||||
{"U1": _ldo({"i_load_a": 0.5, "theta_ja": 160})},
|
||||
{
|
||||
"VIN": (NetType.POWER, 5.0, [("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, 3.3, [("U1", "2")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("U1", "3")]),
|
||||
},
|
||||
)
|
||||
findings = check_thermal(g, _cons())
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-TH-002"
|
||||
assert findings[0].status == "WARNING"
|
||||
|
||||
|
||||
def test_shunt_over_rating_is_warning():
|
||||
r = Component(
|
||||
reference="R1", value="1", footprint="",
|
||||
component_type=ComponentType.RESISTOR, mpn="R1",
|
||||
pins={"1": "A", "2": "B"},
|
||||
specs=ResistorSpecs(value_ohms=1.0, value_formatted="1", power_rating_w="0.125"),
|
||||
)
|
||||
g = _graph(
|
||||
{"R1": r},
|
||||
{
|
||||
"A": (NetType.POWER, 3.3, [("R1", "1")]),
|
||||
"B": (NetType.POWER, 0.0, [("R1", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_thermal(g)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-TH-003"
|
||||
assert findings[0].status == "WARNING"
|
||||
|
||||
|
||||
def test_led_resistor_within_rating_is_silent():
|
||||
led = Component(
|
||||
reference="D1", value="LED", footprint="",
|
||||
component_type=ComponentType.DISCRETE, component_subtype="discrete.led",
|
||||
mpn="LEDX",
|
||||
pins={"A": "+5V", "K": "NetK"},
|
||||
specs=SimpleComponentSpecs(
|
||||
specs_type="discrete", component_subtype="discrete.led",
|
||||
values={"forward_voltage_v": 2.0, "forward_current_a": "20mA"},
|
||||
),
|
||||
)
|
||||
r = Component(
|
||||
reference="R1", value="330", footprint="",
|
||||
component_type=ComponentType.RESISTOR, mpn="R1",
|
||||
pins={"1": "NetK", "2": "GND"},
|
||||
specs=ResistorSpecs(value_ohms=330.0, value_formatted="330", power_rating_w="0.125"),
|
||||
)
|
||||
g = _graph(
|
||||
{"D1": led, "R1": r},
|
||||
{
|
||||
"+5V": (NetType.POWER, 5.0, [("D1", "A")]),
|
||||
"NetK": (NetType.SIGNAL, None, [("D1", "K"), ("R1", "1")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("R1", "2")]),
|
||||
},
|
||||
)
|
||||
assert check_thermal(g) == []
|
||||
Reference in New Issue
Block a user