Share library/extracted for schematic and PCB review.

PCB AI skips a second PDF exam (pintable cache, pdf_path=None) and the report
gains a PCB exam section plus power-trace, thermal, via, Kelvin, and GND-stitch
checks grounded in board geometry and extracted specs.
This commit is contained in:
2026-09-20 08:32:23 +02:00
parent f3f96ea4d1
commit 8f4ebc645c
15 changed files with 1014 additions and 107 deletions
+220
View File
@@ -317,3 +317,223 @@ def test_update_project_writes_storage_prefix_not_stale_user_id(tmp_path: Path):
assert jwt_meta is not None
assert jwt_meta.pcb_status == "queued"
assert proj_svc.get_project(storage, "local", pid) is None
def _ldo_graph_layout(*, i_load=10.0, tj_max=150.0, width=0.15, thickness=0.035):
from backend.periscopex.models import (
ComponentConstraints,
LayoutStackup,
Pin,
PinConnection,
SimpleComponentSpecs,
)
pins = {"1": "VIN", "2": "VOUT"}
cons = ComponentConstraints(
mpn="LDO1",
pintable=[
Pin(number="1", name="VIN"),
Pin(number="2", name="VOUT"),
],
absolute_maximum_ratings=[],
rules=[],
)
graph = DesignGraph(
components={
"U1": Component(
reference="U1", value="LDO", footprint="",
component_type=ComponentType.IC, mpn="LDO1",
component_subtype="ic.power.ldo",
pins=pins,
specs=SimpleComponentSpecs(
specs_type="discrete",
values={"i_load_a": i_load, "tj_max": tj_max, "theta_ja": 50},
),
),
},
nets={
"VIN": Net(
name="VIN", net_type=NetType.POWER, voltage=5.0,
pins=[PinConnection(component_ref="U1", pin_number="1")],
),
"VOUT": Net(
name="VOUT", net_type=NetType.POWER, voltage=3.3,
pins=[PinConnection(component_ref="U1", pin_number="2")],
),
},
)
layout = LayoutGraph(
stackup=LayoutStackup(
copper_layers=["F.Cu", "B.Cu"],
dielectrics=[],
copper_thickness_mm=thickness,
),
footprints={
"U1": LayoutFootprint(
reference="U1", x=0, y=0, layer="F.Cu",
courtyard=[(-2, -2), (2, -2), (2, 2), (-2, 2)],
pads=[LayoutPad(number="2", x=0, y=0, net="VOUT")],
),
},
segments=[
LayoutSegment(start=(0, 0), end=(20, 0), width=width, layer="F.Cu", net="VOUT"),
],
)
return graph, {"LDO1": cons}, layout
def test_power_trace_ipc2221_errors_when_load_exceeds_ampacity():
from backend.periscopex.pcb_power_thermal import check_pcb_power_traces
graph, cmap, layout = _ldo_graph_layout()
findings = check_pcb_power_traces(graph, cmap, layout)
assert findings
assert findings[0].rule_id == "PE-PWR-001"
assert findings[0].status == "ERROR"
assert "IPC-2221" in findings[0].finding
assert findings[0].recommendation
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) == []
def test_kelvin_sense_pin_on_shared_net():
from backend.periscopex.models import ComponentConstraints, Pin, PinConnection
from backend.periscopex.pcb_power_thermal import check_pcb_kelvin
cons = ComponentConstraints(
mpn="AMP",
pintable=[Pin(number="4", name="ISNS", description="current sense")],
absolute_maximum_ratings=[],
rules=[],
)
graph = DesignGraph(
components={
"U2": Component(
reference="U2", value="", footprint="",
component_type=ComponentType.IC, mpn="AMP",
pins={"4": "ISNS_NET"},
),
"R1": Component(
reference="R1", value="10m", footprint="",
component_type=ComponentType.RESISTOR, mpn="",
pins={"1": "ISNS_NET", "2": "GND"},
),
"U9": Component(
reference="U9", value="", footprint="",
component_type=ComponentType.IC, mpn="LOAD",
pins={"1": "ISNS_NET"},
),
},
nets={
"ISNS_NET": Net(
name="ISNS_NET", net_type=NetType.SIGNAL,
pins=[
PinConnection(component_ref="U2", pin_number="4"),
PinConnection(component_ref="R1", pin_number="1"),
PinConnection(component_ref="U9", pin_number="1"),
],
),
},
)
findings = check_pcb_kelvin(graph, {"AMP": cons})
assert findings and findings[0].rule_id == "PE-KEL-001"
assert findings[0].status == "WARNING"
def test_pcb_ai_skips_without_library_extraction():
import asyncio
from backend.services.pcb_validation import review_pcb_ics
graph = DesignGraph(
components={
"U1": Component(
reference="U1", value="X", footprint="",
component_type=ComponentType.IC, mpn="NOEXT",
pins={"1": "GND"},
),
},
nets={},
)
async def _run():
return await review_pcb_ics(
graph, {}, None, None, None, Path("/tmp"),
)
_f, _c, skipped = asyncio.run(_run())
assert skipped
assert "library extraction" in skipped[0]["reason"]
def test_library_constraints_fill_from_storage(tmp_path: Path):
from backend.periscopex.models import ComponentConstraints, Pin
from backend.services.pcb_pipeline import _load_constraints_map
from backend.services.storage import LocalStorageBackend
storage = LocalStorageBackend(tmp_path)
cons = ComponentConstraints(
mpn="LIBIC",
pintable=[Pin(number="1", name="VCC")],
absolute_maximum_ratings=[],
rules=[],
)
storage.write_json("library/extracted/LIBIC.json", cons.model_dump())
cmap = _load_constraints_map(tmp_path / "missing", storage)
assert "LIBIC" in cmap
assert cmap["LIBIC"].pintable[0].name == "VCC"
def test_thermal_copper_warns_without_pour_or_vias():
from backend.periscopex.pcb_power_thermal import check_pcb_thermal_copper
graph, cmap, layout = _ldo_graph_layout(i_load=0.5, width=1.0)
findings = check_pcb_thermal_copper(graph, cmap, layout)
assert findings
assert findings[0].rule_id == "PE-THM-001"
assert findings[0].status == "WARNING"
assert findings[0].recommendation
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
graph = DesignGraph(
components={
"U1": Component(
reference="U1", value="", footprint="",
component_type=ComponentType.IC, mpn="X",
pins={"1": "SDA"},
),
},
nets={
"SDA": Net(
name="SDA", net_type=NetType.SIGNAL,
pins=[PinConnection(component_ref="U1", pin_number="1")],
),
},
)
layout = LayoutGraph(
footprints={},
segments=[
LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="SDA"),
],
zones=[
LayoutZone(net="GND", layer="B.Cu", outlines=[[(0, -5), (20, -5), (20, 5), (0, 5)]]),
],
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 check_pcb_gnd_stitch(graph, layout) == []