Add power-margin, PG-to-EN sequencing, and DNP enable checks.
Compare only specified IQ plus I_load to Iout_max and series-R drop, flag sequencing only when power_sequence is in specs, and treat DNP as a fitted-variant graph so a missing enable pull is ERROR only when the BOM actually marks DNP. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""DNP variant: fitted enable without pull/driver is ERROR."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from backend.pinscopex.dnp_check import check_dnp_enables
|
||||
from backend.pinscopex.models import (
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Net,
|
||||
NetType,
|
||||
Pin,
|
||||
PinConnection,
|
||||
ResistorSpecs,
|
||||
)
|
||||
from backend.pinscopex.parsers import parse_bom
|
||||
|
||||
|
||||
def _graph(components, nets, bom_fields=None):
|
||||
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, bom_fields=bom_fields or {})
|
||||
|
||||
|
||||
def _cons():
|
||||
return {
|
||||
"LDOX": ComponentConstraints(
|
||||
mpn="LDOX",
|
||||
pintable=[
|
||||
Pin(number=1, name="VIN"),
|
||||
Pin(number=2, name="VOUT"),
|
||||
Pin(number=3, name="EN"),
|
||||
Pin(number=4, name="GND"),
|
||||
],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _ldo():
|
||||
return Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="LDOX",
|
||||
pins={"1": "VIN", "2": "VOUT", "3": "EN_NET", "4": "GND"},
|
||||
)
|
||||
|
||||
|
||||
def _r(dnp_net="EN_NET"):
|
||||
return Component(
|
||||
reference="R1", value="10k", footprint="",
|
||||
component_type=ComponentType.RESISTOR, mpn="R1",
|
||||
pins={"1": dnp_net, "2": "VIN"},
|
||||
specs=ResistorSpecs(value_ohms=10000, value_formatted="10k"),
|
||||
)
|
||||
|
||||
|
||||
def test_dnp_pull_leaves_enable_floating():
|
||||
g = _graph(
|
||||
{"U1": _ldo(), "R1": _r()},
|
||||
{
|
||||
"VIN": (NetType.POWER, [("U1", "1"), ("R1", "2")]),
|
||||
"VOUT": (NetType.POWER, [("U1", "2")]),
|
||||
"EN_NET": (NetType.SIGNAL, [("U1", "3"), ("R1", "1")]),
|
||||
"GND": (NetType.GROUND, [("U1", "4")]),
|
||||
},
|
||||
bom_fields={
|
||||
"U1": {"mpn": "LDOX", "value": "", "dnp": False},
|
||||
"R1": {"mpn": "R1", "value": "10k", "dnp": True},
|
||||
},
|
||||
)
|
||||
findings = check_dnp_enables(g, _cons())
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-DNP-001"
|
||||
assert findings[0].status == "ERROR"
|
||||
|
||||
|
||||
def test_fitted_pull_is_silent():
|
||||
g = _graph(
|
||||
{"U1": _ldo(), "R1": _r()},
|
||||
{
|
||||
"VIN": (NetType.POWER, [("U1", "1"), ("R1", "2")]),
|
||||
"VOUT": (NetType.POWER, [("U1", "2")]),
|
||||
"EN_NET": (NetType.SIGNAL, [("U1", "3"), ("R1", "1")]),
|
||||
"GND": (NetType.GROUND, [("U1", "4")]),
|
||||
},
|
||||
bom_fields={
|
||||
"U1": {"mpn": "LDOX", "value": "", "dnp": False},
|
||||
"R1": {"mpn": "R1", "value": "10k", "dnp": False},
|
||||
},
|
||||
)
|
||||
assert check_dnp_enables(g, _cons()) == []
|
||||
|
||||
|
||||
def test_no_dnp_column_skips_floating_enable():
|
||||
g = _graph(
|
||||
{"U1": _ldo()},
|
||||
{
|
||||
"VIN": (NetType.POWER, [("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, [("U1", "2")]),
|
||||
"EN_NET": (NetType.SIGNAL, [("U1", "3")]),
|
||||
"GND": (NetType.GROUND, [("U1", "4")]),
|
||||
},
|
||||
bom_fields={"U1": {"mpn": "LDOX", "value": ""}},
|
||||
)
|
||||
assert check_dnp_enables(g, _cons()) == []
|
||||
|
||||
|
||||
def test_parse_bom_reads_dnp_and_skips_when_column_absent(tmp_path: Path):
|
||||
with_dnp = tmp_path / "dnp.csv"
|
||||
with_dnp.write_text(
|
||||
"Reference,Value,DNP,Manufacturer Part Number\n"
|
||||
"U1,LDO,,LDOX\n"
|
||||
"R1,10k,1,Rpull\n"
|
||||
)
|
||||
bom = parse_bom(with_dnp)
|
||||
assert bom["U1"]["dnp"] is False
|
||||
assert bom["R1"]["dnp"] is True
|
||||
|
||||
no_col = tmp_path / "plain.csv"
|
||||
no_col.write_text("Reference,Value,Manufacturer Part Number\nU1,LDO,LDOX\n")
|
||||
bom2 = parse_bom(no_col)
|
||||
assert "dnp" not in bom2["U1"]
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Regulator Iout margin and series-R IR drop — no invented IQ or traces."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.pinscopex.models import (
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Net,
|
||||
NetType,
|
||||
Pin,
|
||||
PinConnection,
|
||||
ResistorSpecs,
|
||||
SimpleComponentSpecs,
|
||||
)
|
||||
from backend.pinscopex.power_margin_check import check_power_margin
|
||||
|
||||
|
||||
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 _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 _ldo(values):
|
||||
return Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, component_subtype="ic.power.ldo",
|
||||
mpn="LDOX",
|
||||
pins={"1": "VIN", "2": "VOUT", "3": "GND"},
|
||||
specs=SimpleComponentSpecs(specs_type="ic", component_subtype="ic.power.ldo", values=values),
|
||||
)
|
||||
|
||||
|
||||
def _mcu(iq=None):
|
||||
values = {} if iq is None else {"iq_a": iq}
|
||||
return Component(
|
||||
reference="U2", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="MCU",
|
||||
pins={"1": "VOUT", "2": "GND"},
|
||||
specs=SimpleComponentSpecs(specs_type="ic", values=values) if values else None,
|
||||
)
|
||||
|
||||
|
||||
def test_load_over_iout_max_is_ps_pwr_001():
|
||||
g = _graph(
|
||||
{
|
||||
"U1": _ldo({"i_load_a": 0.4, "iout_max_a": 0.5}),
|
||||
"U2": _mcu(0.2),
|
||||
},
|
||||
{
|
||||
"VIN": (NetType.POWER, 5.0, [("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, 3.3, [("U1", "2"), ("U2", "1")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("U1", "3"), ("U2", "2")]),
|
||||
},
|
||||
)
|
||||
findings = check_power_margin(g, _cons())
|
||||
assert any(f.rule_id == "PS-PWR-001" and f.designator == "U1" for f in findings)
|
||||
|
||||
|
||||
def test_missing_iq_is_not_guessed_into_margin_fail():
|
||||
g = _graph(
|
||||
{
|
||||
"U1": _ldo({"iout_max_a": 0.1}),
|
||||
"U2": _mcu(None),
|
||||
},
|
||||
{
|
||||
"VIN": (NetType.POWER, 5.0, [("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, 3.3, [("U1", "2"), ("U2", "1")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("U1", "3"), ("U2", "2")]),
|
||||
},
|
||||
)
|
||||
assert check_power_margin(g, _cons()) == []
|
||||
|
||||
|
||||
def test_series_r_ir_drop_uses_i_load_not_trace():
|
||||
r = Component(
|
||||
reference="R1", value="1", footprint="",
|
||||
component_type=ComponentType.RESISTOR, mpn="R1",
|
||||
pins={"1": "USB", "2": "VIN"},
|
||||
specs=ResistorSpecs(value_ohms=1.0, value_formatted="1"),
|
||||
)
|
||||
g = _graph(
|
||||
{"U1": _ldo({"i_load_a": 0.5, "iout_max_a": 1.0}), "R1": r},
|
||||
{
|
||||
"USB": (NetType.POWER, 5.0, [("R1", "1")]),
|
||||
"VIN": (NetType.POWER, 5.0, [("R1", "2"), ("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, 3.3, [("U1", "2")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("U1", "3")]),
|
||||
},
|
||||
)
|
||||
findings = check_power_margin(g, _cons())
|
||||
assert any(f.designator == "R1" and f.rule_id == "PS-PWR-001" for f in findings)
|
||||
|
||||
|
||||
def test_no_series_r_does_not_invent_trace_drop():
|
||||
g = _graph(
|
||||
{"U1": _ldo({"i_load_a": 0.5, "iout_max_a": 1.0})},
|
||||
{
|
||||
"VIN": (NetType.POWER, 5.0, [("U1", "1")]),
|
||||
"VOUT": (NetType.POWER, 3.3, [("U1", "2")]),
|
||||
"GND": (NetType.GROUND, 0.0, [("U1", "3")]),
|
||||
},
|
||||
)
|
||||
assert check_power_margin(g, _cons()) == []
|
||||
@@ -0,0 +1,91 @@
|
||||
"""PG→EN sequencing only when power_sequence is in IC specs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.pinscopex.models import (
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Net,
|
||||
NetType,
|
||||
Pin,
|
||||
PinConnection,
|
||||
SimpleComponentSpecs,
|
||||
)
|
||||
from backend.pinscopex.sequencing_check import check_power_sequencing
|
||||
|
||||
|
||||
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 _cons():
|
||||
return {
|
||||
"L1": ComponentConstraints(
|
||||
mpn="L1", component_subtype="ic.power.ldo",
|
||||
pintable=[
|
||||
Pin(number=1, name="VIN"), Pin(number=2, name="VOUT"),
|
||||
Pin(number=3, name="PG"), Pin(number=4, name="GND"),
|
||||
],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
),
|
||||
"L2": ComponentConstraints(
|
||||
mpn="L2", component_subtype="ic.power.ldo",
|
||||
pintable=[
|
||||
Pin(number=1, name="VIN"), Pin(number=2, name="VOUT"),
|
||||
Pin(number=3, name="EN"), Pin(number=4, name="GND"),
|
||||
],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _ldo(ref, mpn, pins, values=None):
|
||||
return Component(
|
||||
reference=ref, value="", footprint="",
|
||||
component_type=ComponentType.IC, component_subtype="ic.power.ldo",
|
||||
mpn=mpn, pins=pins,
|
||||
specs=SimpleComponentSpecs(
|
||||
specs_type="ic", component_subtype="ic.power.ldo", values=values or {},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _dual(pg_net, en_net, sequence=True):
|
||||
u1 = _ldo("U1", "L1", {"1": "5V", "2": "3V3", "3": pg_net, "4": "GND"})
|
||||
vals = {"power_sequence": "pg_before_en"} if sequence else {}
|
||||
u2 = _ldo("U2", "L2", {"1": "3V3", "2": "1V8", "3": en_net, "4": "GND"}, vals)
|
||||
return _graph(
|
||||
{"U1": u1, "U2": u2},
|
||||
{
|
||||
"5V": (NetType.POWER, [("U1", "1")]),
|
||||
"3V3": (NetType.POWER, [("U1", "2"), ("U2", "1")]),
|
||||
"1V8": (NetType.POWER, [("U2", "2")]),
|
||||
pg_net: (NetType.SIGNAL, [("U1", "3")] + ([("U2", "3")] if pg_net == en_net else [])),
|
||||
**({en_net: (NetType.SIGNAL, [("U2", "3")])} if pg_net != en_net else {}),
|
||||
"GND": (NetType.GROUND, [("U1", "4"), ("U2", "4")]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_pg_not_tied_to_en_is_warning():
|
||||
findings = check_power_sequencing(_dual("PGOOD", "EN_1V8"), _cons())
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-SEQ-001"
|
||||
assert findings[0].status == "WARNING"
|
||||
|
||||
|
||||
def test_pg_tied_to_en_is_silent():
|
||||
assert check_power_sequencing(_dual("SEQ", "SEQ"), _cons()) == []
|
||||
|
||||
|
||||
def test_no_power_sequence_spec_skips_even_if_pg_open():
|
||||
assert check_power_sequencing(_dual("PGOOD", "EN_1V8", sequence=False), _cons()) == []
|
||||
Reference in New Issue
Block a user