Tighten PCB findings: actions, stitch gating, real copper.
Fill action on every finding and show it in the report. Stitch/return only on large HS/power nets. Parse stackup thickness, arcs, and vias from the board. IC courtyard pad copper is not PE-PLC-004 RULE. Recommended RC/cap stay REVIEW.
This commit is contained in:
@@ -133,3 +133,26 @@ def test_same_object_pcb_and_schema():
|
||||
assert obj.finding_class
|
||||
assert obj.provenance
|
||||
assert obj.evidence_status
|
||||
assert (obj.action or "").strip()
|
||||
|
||||
|
||||
def test_empty_recommendation_gets_action():
|
||||
f = Finding(designator="U1", finding="note", status="INFO", source="review")
|
||||
complete_finding(f)
|
||||
assert f.action.strip()
|
||||
assert f.recommendation.strip() == f.action.strip()
|
||||
|
||||
|
||||
def test_recommended_rc_cap_is_review_not_rule():
|
||||
f = Finding(
|
||||
designator="C1",
|
||||
finding="10n vs 100n",
|
||||
why="datasheet typical 100n",
|
||||
status="WARNING",
|
||||
rule_id="PE-DEC-002",
|
||||
source="passive_rail_check",
|
||||
)
|
||||
complete_finding(f)
|
||||
assert f.finding_class == "REVIEW"
|
||||
assert f.provenance == "RECOMMENDED"
|
||||
assert f.status != "ERROR"
|
||||
|
||||
@@ -237,3 +237,25 @@ def test_layout_graph_written_from_valid_pcb(tmp_path: Path):
|
||||
assert out.is_file()
|
||||
data = __import__("json").loads(out.read_text())
|
||||
assert "R1" in data["footprints"]
|
||||
|
||||
|
||||
def test_stackup_thickness_without_full_dielectrics(tmp_path: Path):
|
||||
p = tmp_path / "t.kicad_pcb"
|
||||
p.write_text("""(kicad_pcb (version 20240108)
|
||||
(net 0 "")
|
||||
(net 1 "VOUT")
|
||||
(setup
|
||||
(stackup
|
||||
(layer "F.Cu" (type "copper") (thickness 0.035mm))
|
||||
(layer "B.Cu" (type "copper") (thickness 0.035))
|
||||
)
|
||||
)
|
||||
(via (at 1 1) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net 1))
|
||||
(arc (start 0 0) (end 2 0) (width 0.4) (layer "F.Cu") (net 1))
|
||||
)
|
||||
""")
|
||||
g = parse_kicad_pcb(p)
|
||||
assert g.stackup is not None
|
||||
assert g.stackup.copper_thickness_mm == pytest.approx(0.035)
|
||||
assert len(g.vias) == 1
|
||||
assert any(s.width == pytest.approx(0.4) and s.net == "VOUT" for s in g.segments)
|
||||
|
||||
@@ -394,12 +394,19 @@ def test_power_trace_ipc2221_errors_when_load_exceeds_ampacity():
|
||||
assert findings[0].recommendation
|
||||
|
||||
|
||||
def test_power_trace_uses_parsed_width_and_thickness():
|
||||
from backend.periscopex.pcb_power_thermal import check_pcb_power_traces
|
||||
|
||||
graph, cmap, layout = _ldo_graph_layout(i_load=0.05, width=1.0, thickness=0.035)
|
||||
findings = check_pcb_power_traces(graph, cmap, layout)
|
||||
assert findings == []
|
||||
|
||||
|
||||
def test_power_trace_skips_without_i_load():
|
||||
from backend.periscopex.pcb_power_thermal import check_pcb_power_traces
|
||||
|
||||
graph, cmap, layout = _ldo_graph_layout(i_load=10)
|
||||
graph.components["U1"].specs.values["i_load_a"] = None # type: ignore[union-attr]
|
||||
# drop i_load
|
||||
graph.components["U1"].specs = None
|
||||
assert check_pcb_power_traces(graph, cmap, layout) == []
|
||||
|
||||
@@ -502,6 +509,38 @@ def test_thermal_copper_warns_without_pour_or_vias():
|
||||
assert findings[0].recommendation
|
||||
|
||||
|
||||
def test_gnd_stitch_skips_tiny_gpio():
|
||||
from backend.periscopex.models import LayoutZone, PinConnection
|
||||
from backend.periscopex.pcb_power_thermal import check_pcb_gnd_stitch
|
||||
|
||||
graph = DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="X",
|
||||
pins={"1": "GPIO4"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"GPIO4": Net(
|
||||
name="GPIO4", net_type=NetType.SIGNAL,
|
||||
pins=[PinConnection(component_ref="U1", pin_number="1")],
|
||||
),
|
||||
},
|
||||
)
|
||||
layout = LayoutGraph(
|
||||
footprints={},
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(0.1, 3), width=0.2, layer="F.Cu", net="GPIO4"),
|
||||
],
|
||||
zones=[
|
||||
LayoutZone(net="GND", layer="B.Cu", outlines=[[(0, -5), (20, -5), (20, 5), (0, 5)]]),
|
||||
],
|
||||
vias=[],
|
||||
)
|
||||
assert check_pcb_gnd_stitch(graph, layout) == []
|
||||
|
||||
|
||||
def test_gnd_stitch_info_when_signal_has_pour_but_no_via():
|
||||
from backend.periscopex.models import LayoutVia, LayoutZone, PinConnection
|
||||
from backend.periscopex.pcb_power_thermal import check_pcb_gnd_stitch
|
||||
@@ -511,12 +550,12 @@ def test_gnd_stitch_info_when_signal_has_pour_but_no_via():
|
||||
"U1": Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="X",
|
||||
pins={"1": "SDA"},
|
||||
pins={"1": "USB_DP"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"SDA": Net(
|
||||
name="SDA", net_type=NetType.SIGNAL,
|
||||
"USB_DP": Net(
|
||||
name="USB_DP", net_type=NetType.SIGNAL,
|
||||
pins=[PinConnection(component_ref="U1", pin_number="1")],
|
||||
),
|
||||
},
|
||||
@@ -524,17 +563,18 @@ def test_gnd_stitch_info_when_signal_has_pour_but_no_via():
|
||||
layout = LayoutGraph(
|
||||
footprints={},
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="SDA"),
|
||||
LayoutSegment(start=(0, 0), end=(20, 8), width=0.2, layer="F.Cu", net="USB_DP"),
|
||||
],
|
||||
zones=[
|
||||
LayoutZone(net="GND", layer="B.Cu", outlines=[[(0, -5), (20, -5), (20, 5), (0, 5)]]),
|
||||
LayoutZone(net="GND", layer="B.Cu", outlines=[[(0, -5), (20, -5), (20, 10), (0, 10)]]),
|
||||
],
|
||||
vias=[],
|
||||
)
|
||||
findings = check_pcb_gnd_stitch(graph, layout)
|
||||
assert findings and findings[0].rule_id == "PE-STCH-001"
|
||||
assert findings[0].status == "INFO"
|
||||
layout.vias = [LayoutVia(x=10, y=0, net="GND", drill=0.3)]
|
||||
assert findings[0].action
|
||||
layout.vias = [LayoutVia(x=10, y=4, net="GND", drill=0.3)]
|
||||
assert check_pcb_gnd_stitch(graph, layout) == []
|
||||
|
||||
|
||||
|
||||
@@ -262,6 +262,60 @@ def test_keepout_foreign_track_in_courtyard_is_ps_plc_004():
|
||||
assert len(plc) == 1
|
||||
assert plc[0].designator == "X1"
|
||||
assert plc[0].net == "GND"
|
||||
assert plc[0].finding_class == "REVIEW"
|
||||
assert plc[0].action
|
||||
|
||||
|
||||
def test_ic_courtyard_pad_nets_are_not_keepout_violations():
|
||||
"""GND/I2C into an IC courtyard is normal copper to that part's pads."""
|
||||
from backend.periscopex.models import Component, ComponentType, Net, NetType, PinConnection
|
||||
|
||||
cons = {
|
||||
"MCU": ComponentConstraints(
|
||||
mpn="MCU",
|
||||
pintable=[Pin(number="1", name="SDA")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
layout_rules=[{"kind": "keepout", "pin": "1"}],
|
||||
)
|
||||
}
|
||||
graph = DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="", footprint="",
|
||||
component_type=ComponentType.IC, mpn="MCU",
|
||||
pins={"1": "SDA", "2": "GND"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"SDA": Net(
|
||||
name="SDA", net_type=NetType.SIGNAL,
|
||||
pins=[PinConnection(component_ref="U1", pin_number="1")],
|
||||
),
|
||||
"GND": Net(
|
||||
name="GND", net_type=NetType.GROUND,
|
||||
pins=[PinConnection(component_ref="U1", pin_number="2")],
|
||||
),
|
||||
},
|
||||
)
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": LayoutFootprint(
|
||||
reference="U1", x=0, y=0, layer="F.Cu",
|
||||
pads=[
|
||||
LayoutPad(number="1", x=0.2, y=0.2, net="SDA"),
|
||||
LayoutPad(number="2", x=0.8, y=0.2, net="GND"),
|
||||
],
|
||||
courtyard=[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)],
|
||||
),
|
||||
},
|
||||
segments=[
|
||||
LayoutSegment(start=(0.4, 0.4), end=(0.6, 0.4), width=0.2, layer="F.Cu", net="GND"),
|
||||
LayoutSegment(start=(0.3, 0.5), end=(0.5, 0.5), width=0.2, layer="F.Cu", net="SDA"),
|
||||
],
|
||||
)
|
||||
findings = check_placement(graph, cons, layout)
|
||||
assert [f for f in findings if f.rule_id == "PE-PLC-004"] == []
|
||||
|
||||
|
||||
def test_keepout_own_net_in_courtyard_is_silent():
|
||||
|
||||
Reference in New Issue
Block a user