"""Fase B close-out: BOM↔PCB↔datasheet, SPOF, EMI FACT, gated Tj, SI refresh.""" from __future__ import annotations from backend.periscopex.layout_rules import needs_layout_rules_refresh from backend.periscopex.models import ( AbsMaxRating, Component, ComponentConstraints, ComponentType, DesignGraph, InductorSpecs, LayoutFootprint, LayoutGraph, LayoutPad, LayoutVia, LayoutZone, Net, NetType, PackageInfo, Pin, PinConnection, SimpleComponentSpecs, ) def test_si_refresh_when_old_extract_has_only_decoupling(): assert needs_layout_rules_refresh( { "model_version": "1.10.0", "layout_rules": [{"kind": "decoupling_proximity", "max_distance_mm": 2}], }, min_scan_version="1.12.0", ) assert not needs_layout_rules_refresh( { "model_version": "1.12.0", "layout_rules": [], }, min_scan_version="1.12.0", ) assert not needs_layout_rules_refresh( { "model_version": "1.10.0", "layout_rules": [{"kind": "impedance", "zdiff_ohm": 90}], }, min_scan_version="1.12.0", ) assert not needs_layout_rules_refresh( { "model_version": "1.9.0", "layout_rules": [{"kind": "decoupling_proximity", "max_distance_mm": None}], }, min_scan_version="1.10.0", ) def test_package_family_mismatch_is_pe_bom_010(): from backend.periscopex.bom_pcb_check import check_bom_pcb_datasheet cons = ComponentConstraints( mpn="IC1", package_info=PackageInfo( base_family="MSP", package="LQFP-48", pin_count=48, ), pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 49)], absolute_maximum_ratings=[], rules=[], ) graph = DesignGraph( components={ "U1": Component( reference="U1", value="IC", footprint="Package_DFN_QFN:QFN-16-1EP", component_type=ComponentType.IC, mpn="IC1", pins={str(i): "GND" for i in range(1, 17)}, ), }, nets={"GND": Net(name="GND", net_type=NetType.GROUND, pins=[])}, schematic_fields={"U1": {"footprint": "Package_DFN_QFN:QFN-16-1EP"}}, bom_fields={"U1": {"footprint": "QFN-16"}}, ) layout = LayoutGraph( footprints={ "U1": LayoutFootprint( reference="U1", footprint="Package_DFN_QFN:QFN-16-1EP_3x3mm", x=0, y=0, layer="F.Cu", pads=[LayoutPad(number=str(i), x=0, y=0, net="GND") for i in range(1, 17)], ), }, ) findings = check_bom_pcb_datasheet(graph, {"IC1": cons}, layout) ids = {f.rule_id for f in findings} assert "PE-BOM-010" in ids assert "PE-BOM-011" in ids assert all(f.status == "ERROR" for f in findings if f.rule_id in {"PE-BOM-010", "PE-BOM-011"}) def test_abs_max_voltage_and_temp_and_current(): from backend.periscopex.bom_pcb_check import check_bom_pcb_datasheet cons = ComponentConstraints( mpn="LDO1", package_info=PackageInfo(base_family="SPX", package="SOT-23-5", pin_count=5), pintable=[ Pin(number="1", name="VIN"), Pin(number="2", name="GND"), Pin(number="5", name="VOUT"), ], absolute_maximum_ratings=[ AbsMaxRating(parameter="VIN", min=None, max=4.0, unit="V", source_page=3), AbsMaxRating(parameter="TJ", min=None, max=125, unit="C", source_page=3), AbsMaxRating(parameter="IOUT", min=None, max=0.1, unit="A", source_page=4), ], rules=[], ) graph = DesignGraph( components={ "U1": Component( reference="U1", value="LDO", footprint="Package_TO_SOT_SMD:SOT-23-5", component_type=ComponentType.IC, mpn="LDO1", pins={"1": "VIN", "2": "GND", "5": "VOUT"}, specs=SimpleComponentSpecs( specs_type="discrete", values={"i_load_a": 0.5, "ta_max_c": 150}, ), ), }, nets={ "VIN": Net( name="VIN", net_type=NetType.POWER, voltage=5.0, pins=[PinConnection(component_ref="U1", pin_number="1")], ), "GND": Net(name="GND", net_type=NetType.GROUND, pins=[]), "VOUT": Net(name="VOUT", net_type=NetType.POWER, voltage=3.3, pins=[]), }, ) findings = check_bom_pcb_datasheet(graph, {"LDO1": cons}, None) ids = {f.rule_id for f in findings} assert "PE-BOM-012" in ids assert "PE-BOM-013" in ids assert "PE-BOM-014" in ids def test_inductor_current_rating(): from backend.periscopex.bom_pcb_check import check_bom_pcb_datasheet graph = DesignGraph( components={ "L1": Component( reference="L1", value="2.2uH", footprint="", component_type=ComponentType.INDUCTOR, mpn="IND1", pins={"1": "SW", "2": "VOUT"}, specs=InductorSpecs( value_henries=2.2e-6, value_formatted="2.2uH", current_rating_a="0.2", ), ), }, nets={}, ) graph.components["L1"].specs # type: ignore # I_load on the inductor itself graph.components["L1"] = graph.components["L1"].model_copy( update={"specs": InductorSpecs( value_henries=2.2e-6, value_formatted="2.2uH", current_rating_a="0.2", )} ) # specs_values reads SimpleComponentSpecs.values — inductor uses current_rating_a findings = check_bom_pcb_datasheet(graph, {}, None) # Without i_load on inductor specs, skip. assert all(f.rule_id != "PE-BOM-014" for f in findings) def test_spof_review_single_ldo(): from backend.periscopex.finding_engine import complete_finding from backend.periscopex.spof_check import check_spof 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={"1": "VIN", "2": "+3V3"}, ), "U2": Component( reference="U2", value="MCU", footprint="", component_type=ComponentType.IC, mpn="MCU", pins={"1": "+3V3"}, ), "U3": Component( reference="U3", value="PHY", footprint="", component_type=ComponentType.IC, mpn="PHY", pins={"1": "+3V3"}, ), }, nets={ "VIN": Net(name="VIN", net_type=NetType.POWER, pins=[]), "+3V3": Net( name="+3V3", net_type=NetType.POWER, pins=[ PinConnection(component_ref="U1", pin_number="2"), PinConnection(component_ref="U2", pin_number="1"), PinConnection(component_ref="U3", pin_number="1"), ], ), }, ) findings = check_spof(graph, {"LDO1": cons}) assert findings assert findings[0].rule_id == "PE-SPOF-001" complete_finding(findings[0]) assert findings[0].finding_class == "REVIEW" assert findings[0].status != "ERROR" def test_emi_only_with_datasheet_fact(): from backend.periscopex.emi_check import check_emi cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="USB_DP")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "keepout", "note": "Place a common-mode choke on the USB pair", "source_page": 22, }], ) graph = DesignGraph( components={ "U2": Component( reference="U2", value="PHY", footprint="", component_type=ComponentType.IC, mpn="PHY", pins={"1": "USB_DP"}, ), }, nets={ "USB_DP": Net( name="USB_DP", net_type=NetType.SIGNAL, pins=[PinConnection(component_ref="U2", pin_number="1")], ), }, ) findings = check_emi(graph, {"PHY": cons}, None) assert findings and findings[0].rule_id == "PE-EMI-001" cons2 = cons.model_copy(update={"layout_rules": []}) assert check_emi(graph, {"PHY": cons2}, None) == [] def test_tj_when_theta_copper_vias_exist(): from backend.periscopex.pcb_power_thermal import check_pcb_junction_temp from backend.periscopex.models import LayoutSegment, LayoutStackup 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={"1": "VIN", "2": "VOUT"}, specs=SimpleComponentSpecs( specs_type="discrete", values={"i_load_a": 0.5, "theta_ja": 50, "tj_max": 150}, ), ), }, 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"], dielectrics=[], copper_thickness_mm=0.035), 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=(5, 0), width=0.5, layer="F.Cu", net="VOUT")], vias=[LayoutVia(x=0.1, y=0.1, net="GND", drill=0.3)], zones=[LayoutZone( net="GND", layer="F.Cu", outlines=[[(-3, -3), (3, -3), (3, 3), (-3, 3)]], )], ) findings = check_pcb_junction_temp(graph, {"LDO1": cons}, layout) assert findings and findings[0].rule_id == "PE-THM-002" assert findings[0].evidence_status == "SUFFICIENT" assert "Tj" in findings[0].finding assert findings[0].status == "INFO" def test_tj_insufficient_without_theta(): from backend.periscopex.pcb_power_thermal import check_pcb_junction_temp 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", pins={"1": "VIN", "2": "VOUT"}, specs=SimpleComponentSpecs( specs_type="discrete", values={"i_load_a": 0.5}, ), ), }, 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(footprints={}) findings = check_pcb_junction_temp(graph, {"LDO1": cons}, layout) assert findings and findings[0].rule_id == "PE-THM-002" assert findings[0].evidence_status == "INSUFFICIENT" assert findings[0].status == "INFO"