"""Phase B: stackup, via I, ESD mm, USB-PD, PoE isolation, antenna, PDN. Skip without evidence. No invented I/Z/mm/IPC via chart. Via ≠ pad ≠ track ≠ zone. """ from __future__ import annotations from backend.periscopex.antenna_layout_check import check_antenna_layout from backend.periscopex.esd_return_check import check_esd_distance from backend.periscopex.finding_engine import complete_finding, lookup_rule from backend.periscopex.interface_class_check import check_interface_classes from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, DesignGraph, LayoutDielectric, LayoutFootprint, LayoutGraph, LayoutPad, LayoutSegment, LayoutStackup, LayoutVia, LayoutZone, Net, NetType, Pin, PinConnection, SimpleComponentSpecs, ) from backend.periscopex.pcb_checks import run_pcb_checks from backend.periscopex.pcb_power_thermal import check_pcb_via_current from backend.periscopex.pdn_check import check_pdn from backend.periscopex.stackup_check import check_stackup from backend.periscopex.usb_pd_check import check_usb_pd def _ic(ref, pins, *, mpn="PART", specs=None, subtype=None): return Component( reference=ref, value=mpn, footprint="", component_type=ComponentType.IC, mpn=mpn, component_subtype=subtype, pins=pins, specs=specs, ) def _net(name, *pairs, ntype=NetType.SIGNAL): return Net( name=name, net_type=ntype, pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs], ) def test_stackup_skips_without_fab_spec(): layout = LayoutGraph( stackup=LayoutStackup( copper_layers=["F.Cu", "B.Cu"], dielectrics=[LayoutDielectric(name="d1", er=4.5, height_mm=0.2)], copper_thickness_mm=0.035, ), segments=[LayoutSegment(start=(0, 0), end=(1, 0), width=0.2, layer="F.Cu", net="GND")], ) g = DesignGraph(components={"U1": _ic("U1", {"1": "GND"})}, nets={"GND": _net("GND", ("U1", "1"), ntype=NetType.GROUND)}) assert check_stackup(g, {}, layout) == [] assert check_stackup(g, {}, None) == [] def test_stackup_vs_fab_spec_is_review_not_invented_oz(): layout = LayoutGraph( stackup=LayoutStackup( copper_layers=["F.Cu", "B.Cu"], dielectrics=[LayoutDielectric(name="d1", er=4.5, height_mm=0.2)], copper_thickness_mm=0.035, ), ) cons = ComponentConstraints( mpn="FAB", pintable=[Pin(number="1", name="GND")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "stackup", "copper_thickness_mm": 0.070, "note": "fab 2 oz", "source_page": 1, }], ) g = DesignGraph(components={"U1": _ic("U1", {"1": "GND"}, mpn="FAB")}, nets={}) findings = check_stackup(g, {"FAB": cons}, layout) assert len(findings) == 1 assert findings[0].rule_id == "PE-STK-001" assert findings[0].status == "WARNING" assert findings[0].finding_class == "REVIEW" assert "0.035" in findings[0].facts assert "0.07" in findings[0].facts.replace("070", "0.07") or "0.070" in findings[0].facts assert "1 oz" not in findings[0].finding.lower() complete_finding(findings[0]) assert findings[0].status != "ERROR" def test_via_current_skips_without_i_does_not_invent_ipc(): layout = LayoutGraph( segments=[LayoutSegment(start=(0, 0), end=(5, 0), width=0.5, layer="F.Cu", net="VOUT")], vias=[LayoutVia(x=1, y=0, net="VOUT", drill=0.3)], ) g = DesignGraph( components={"U1": _ic("U1", {"1": "VIN", "2": "VOUT"}, mpn="LDO1", specs=SimpleComponentSpecs(specs_type="discrete", values={}))}, nets={"VOUT": _net("VOUT", ("U1", "2"), ntype=NetType.POWER)}, ) cons = ComponentConstraints(mpn="LDO1", pintable=[Pin(number="2", name="VOUT")], absolute_maximum_ratings=[], rules=[]) assert check_pcb_via_current(g, {"LDO1": cons}, layout) == [] g.components["U1"].specs = SimpleComponentSpecs( specs_type="discrete", values={"i_load": 1.2}, ) findings = check_pcb_via_current(g, {"LDO1": cons}, layout) via2 = [f for f in findings if f.rule_id == "PE-VIA-002"] assert len(via2) == 1 assert via2[0].status == "INFO" assert via2[0].evidence_status == "INSUFFICIENT" blob = f"{via2[0].finding} {via2[0].requirement} {via2[0].inference} {via2[0].facts}" assert "1.2" in via2[0].facts assert "0.3" in via2[0].facts assert "IPC" not in via2[0].finding assert "k =" not in blob.lower() complete_finding(via2[0]) assert via2[0].status != "ERROR" def test_esd_distance_uses_pads_not_vias(): g = DesignGraph( components={ "J2": Component( reference="J2", value="USB_C", footprint="", component_type=ComponentType.CONNECTOR, pins={"A6": "USB_D+"}, ), "D1": _ic("D1", {"1": "USB_D+"}, mpn="USBLC6", subtype="ic.protection.esd"), }, nets={"USB_D+": _net("USB_D+", ("J2", "A6"), ("D1", "1"))}, ) layout = LayoutGraph( footprints={ "J2": LayoutFootprint(reference="J2", x=0, y=0, pads=[ LayoutPad(number="A6", x=0.0, y=0.0, net="USB_D+"), ]), "D1": LayoutFootprint(reference="D1", x=8.0, y=0.0, pads=[ LayoutPad(number="1", x=8.0, y=0.0, net="USB_D+"), ]), }, vias=[LayoutVia(x=1.0, y=0.0, net="USB_D+", drill=0.3)], segments=[LayoutSegment(start=(0, 0), end=(8, 0), width=0.2, layer="F.Cu", net="USB_D+")], ) findings = check_esd_distance(g, {}, layout) dist = [f for f in findings if f.rule_id == "PE-ESD-002"] assert len(dist) == 1 assert dist[0].status == "INFO" assert dist[0].evidence_status == "INSUFFICIENT" assert "8.000" in dist[0].facts or "8.00" in dist[0].facts assert "via" not in dist[0].facts.lower() or "pad" in dist[0].facts.lower() assert dist[0].calculation cons = ComponentConstraints( mpn="USBLC6", pintable=[Pin(number="1", name="IO")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "esd", "max_distance_mm": 3.0, "note": "TVS within 3 mm of connector", "source_page": 5, }], ) fail = check_esd_distance(g, {"USBLC6": cons}, layout) hit = [f for f in fail if f.rule_id == "PE-ESD-002"][0] assert hit.finding.startswith("FAIL:") complete_finding(hit) assert hit.status == "WARNING" assert hit.status != "ERROR" or hit.provenance == "MANDATORY" def test_usb_pd_skips_without_pd_controller(): g = DesignGraph( components={ "J2": Component( reference="J2", value="USB_C_Receptacle_USB2.0_16P", footprint="", component_type=ComponentType.CONNECTOR, pins={"A5": "USB_CC1"}, ), }, nets={"USB_CC1": _net("USB_CC1", ("J2", "A5"))}, ) assert check_usb_pd(g) == [] g.components["U8"] = _ic("U8", {"1": "USB_CC1", "2": "VBUS"}, mpn="FUSB302B") g.nets["VBUS"] = _net("VBUS", ("U8", "2"), ntype=NetType.POWER) found = check_usb_pd(g) assert [f.rule_id for f in found] == ["PE-PD-001"] assert found[0].status == "INFO" assert found[0].evidence_status == "SUFFICIENT" def test_poe_isolation_skips_without_voltage_fact(): g = DesignGraph( components={ "J1": Component( reference="J1", value="RJ45 PoE MagJack", footprint="", component_type=ComponentType.CONNECTOR, mpn="ARJP11A", pins={"1": "ETH_TX+"}, ), }, nets={"ETH_TX+": _net("ETH_TX+", ("J1", "1"))}, ) findings = check_interface_classes(g) assert not any(f.rule_id == "PE-POE-004" for f in findings) g.components["J1"].specs = SimpleComponentSpecs( specs_type="connector", values={"isolation_v": 1500.0}, ) with_v = check_interface_classes(g) iso = [f for f in with_v if f.rule_id == "PE-POE-004"] assert len(iso) == 1 assert iso[0].status == "INFO" assert "1500" in iso[0].facts assert iso[0].evidence_status == "SUFFICIENT" def test_antenna_skips_when_absent(): g = DesignGraph( components={"U1": _ic("U1", {"1": "GPIO1"})}, nets={"GPIO1": _net("GPIO1", ("U1", "1"))}, ) layout = LayoutGraph(segments=[ LayoutSegment(start=(0, 0), end=(2, 0), width=0.2, layer="F.Cu", net="GPIO1"), ]) assert check_antenna_layout(g, layout) == [] def test_antenna_keepout_and_match_are_facts(): g = DesignGraph( components={ "U1": _ic("U1", {"1": "ANT_FEED"}, mpn="ESP32"), "ANT1": Component( reference="ANT1", value="ANT", footprint="RF_Antenna:ANT", component_type=ComponentType.UNKNOWN, pins={"1": "ANT_FEED"}, ), "L1": Component( reference="L1", value="3.3nH", footprint="", component_type=ComponentType.INDUCTOR, pins={"1": "ANT_FEED", "2": "RF_OUT"}, ), }, nets={ "ANT_FEED": _net("ANT_FEED", ("U1", "1"), ("ANT1", "1"), ("L1", "1")), "RF_OUT": _net("RF_OUT", ("L1", "2")), }, ) layout = LayoutGraph( footprints={"ANT1": LayoutFootprint(reference="ANT1", x=10, y=10, pads=[ LayoutPad(number="1", x=10, y=10, net="ANT_FEED"), ])}, zones=[LayoutZone( net="antenna", layer="F.Cu", keepout=True, outlines=[[(8, 8), (14, 8), (14, 14), (8, 14)]], )], segments=[LayoutSegment(start=(0, 0), end=(10, 10), width=0.3, layer="F.Cu", net="ANT_FEED")], ) findings = check_antenna_layout(g, layout) ids = {f.rule_id for f in findings} assert "PE-ANT-001" in ids assert "PE-ANT-002" in ids keep = [f for f in findings if f.rule_id == "PE-ANT-001"][0] assert keep.status == "INFO" assert keep.evidence_status == "SUFFICIENT" assert "50" not in keep.requirement def test_pdn_skips_without_zf(): g = DesignGraph(components={"U1": _ic("U1", {"1": "3V3"})}, nets={ "3V3": _net("3V3", ("U1", "1"), ntype=NetType.POWER), }) layout = LayoutGraph(segments=[ LayoutSegment(start=(0, 0), end=(5, 0), width=0.5, layer="F.Cu", net="3V3"), ]) assert check_pdn(g, layout, None) == [] assert check_pdn(g, layout, {"nets": [{"net_name": "USB_D+", "z0_avg_ohms": 88}]}) == [] def test_run_pcb_checks_phase_b_absent_is_silent(): g = DesignGraph(components={"U1": _ic("U1", {"1": "GND"})}, nets={ "GND": _net("GND", ("U1", "1"), ntype=NetType.GROUND), }) ids = {f.rule_id for f in run_pcb_checks(g, {}, None)} assert "PE-STK-001" not in ids assert "PE-VIA-002" not in ids assert "PE-ESD-002" not in ids assert "PE-PD-001" not in ids assert "PE-POE-004" not in ids assert "PE-ANT-001" not in ids assert "PE-PDN-001" not in ids def test_phase_b_rule_catalog(): for rid in ( "PE-STK-001", "PE-VIA-002", "PE-ESD-002", "PE-PD-001", "PE-POE-004", "PE-ANT-001", "PE-ANT-002", "PE-PDN-001", ): rec = lookup_rule(rid) assert rec is not None, rid