"""Phase A: HF pair integrity + CPU/FPGA/DDR class — only if present. Via ≠ pad ≠ track ≠ zone. No invented Z/I/mm. Skip when evidence is missing. """ from __future__ import annotations from backend.periscopex.finding_engine import complete_finding, lookup_rule from backend.periscopex.hf_bus_class import check_memory_fpga_classes from backend.periscopex.hf_line_check import check_hf_lines from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, DesignGraph, LayoutDielectric, LayoutFootprint, LayoutGraph, LayoutPad, LayoutSegment, LayoutStackup, LayoutVia, LayoutZone, Net, NetType, Pin, PinConnection, ResistorSpecs, ) from backend.periscopex.pcb_checks import run_pcb_checks from backend.periscopex.si_check import bus_class, check_si, skip_si_net def _ic(ref: str, pins: dict[str, str], *, mpn: str = "PHY", subtype: str | None = None) -> Component: return Component( reference=ref, value=mpn, footprint="", component_type=ComponentType.IC, mpn=mpn, component_subtype=subtype, pins=pins, ) def _net(name: str, *pairs: tuple[str, str], ntype: NetType = NetType.SIGNAL) -> Net: return Net( name=name, net_type=ntype, pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs], ) def _usb_graph() -> DesignGraph: return DesignGraph( components={ "U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}), "J2": Component( reference="J2", value="USB_C", footprint="", component_type=ComponentType.CONNECTOR, pins={"A6": "USB_D+", "A7": "USB_D-"}, ), }, nets={ "USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "A6")), "USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "A7")), }, ) def _fp_with_pads(ref: str, pads: list[LayoutPad], x: float = 0.0, y: float = 0.0) -> LayoutFootprint: return LayoutFootprint(reference=ref, footprint="P", x=x, y=y, pads=pads) def test_bus_class_mipi_hf_clk_not_xtal(): assert bus_class("MIPI_D0_P") == "mipi" assert bus_class("CSI_CLK_N") == "mipi" assert bus_class("DSI_D1_P") == "mipi" assert bus_class("GTX_CLK") == "hf_clk" assert bus_class("PCIE_REFCLK") == "pcie" assert bus_class("CLK_P") == "hf_clk" assert bus_class("XTAL_IN") is None assert bus_class("OSCIN") is None assert bus_class("HFXIN") is None assert skip_si_net("USB_CC1") assert bus_class("USB_D+") == "usb2" def test_no_layout_skips_hf_geometry(): assert check_hf_lines(_usb_graph(), {}, None) == [] assert check_hf_lines(_usb_graph(), {}, LayoutGraph()) == [] def test_pad_to_pad_usb_has_no_stub(): layout = LayoutGraph( footprints={ "U1": _fp_with_pads("U1", [ LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+"), LayoutPad(number="2", x=0.0, y=0.4, net="USB_D-"), ]), "J2": _fp_with_pads("J2", [ LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+"), LayoutPad(number="A7", x=10.0, y=0.4, net="USB_D-"), ], x=10.0), }, segments=[ LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"), LayoutSegment(start=(0, 0.4), end=(10, 0.4), width=0.2, layer="F.Cu", net="USB_D-"), ], ) findings = check_hf_lines(_usb_graph(), {}, layout) assert [f for f in findings if f.rule_id == "PE-SI-007"] == [] def test_via_is_not_a_stub(): layout = LayoutGraph( footprints={ "U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]), "J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0), }, segments=[ LayoutSegment(start=(0, 0), end=(5, 0), width=0.2, layer="F.Cu", net="USB_D+"), LayoutSegment(start=(5, 0), end=(10, 0), width=0.2, layer="B.Cu", net="USB_D+"), ], vias=[LayoutVia(x=5.0, y=0.0, net="USB_D+", drill=0.3)], ) findings = check_hf_lines(_usb_graph(), {}, layout) assert [f for f in findings if f.rule_id == "PE-SI-007"] == [] assert all(not isinstance(v, LayoutPad) for v in layout.vias) def test_zone_is_not_a_track_stub(): layout = LayoutGraph( footprints={ "U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]), }, segments=[ LayoutSegment(start=(0, 0), end=(4, 0), width=0.2, layer="F.Cu", net="USB_D+"), ], zones=[LayoutZone( net="USB_D+", layer="F.Cu", outlines=[[(3.5, -1.0), (8.0, -1.0), (8.0, 1.0), (3.5, 1.0)]], )], ) findings = check_hf_lines(_usb_graph(), {}, layout) assert [f for f in findings if f.rule_id == "PE-SI-007"] == [] def test_dangling_track_stub_vs_datasheet_mm_is_fail(): layout = LayoutGraph( footprints={ "U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]), "J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0), }, segments=[ LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"), LayoutSegment(start=(10, 0), end=(10, 3), width=0.2, layer="F.Cu", net="USB_D+"), ], ) cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "stub", "net_class": "usb2", "max_distance_mm": 1.0, "note": "USB stub < 1 mm", "source_page": 12, }], ) findings = check_hf_lines(_usb_graph(), {"PHY": cons}, layout) stubs = [f for f in findings if f.rule_id == "PE-SI-007"] assert len(stubs) == 1 assert stubs[0].finding.startswith("FAIL:") assert stubs[0].facts.startswith("stub_mm=") assert "1" in stubs[0].requirement complete_finding(stubs[0]) assert stubs[0].status == "WARNING" assert stubs[0].finding_class != "RULE" or stubs[0].provenance != "MANDATORY" assert stubs[0].evidence_status == "SUFFICIENT" def test_measured_stub_without_datasheet_mm_is_insufficient(): layout = LayoutGraph( footprints={ "U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]), "J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0), }, segments=[ LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"), LayoutSegment(start=(5, 0), end=(5, 4), width=0.2, layer="F.Cu", net="USB_D+"), ], ) findings = check_hf_lines(_usb_graph(), {}, layout) stubs = [f for f in findings if f.rule_id == "PE-SI-007"] assert len(stubs) == 1 assert stubs[0].status == "INFO" assert stubs[0].evidence_status == "INSUFFICIENT" assert "90" not in (stubs[0].requirement or "") assert "FAIL" not in stubs[0].finding def test_split_under_pair_needs_stackup_and_zone(): segs = [ LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="USB_D+"), ] bare = LayoutGraph(segments=segs) assert [f for f in check_hf_lines(_usb_graph(), {}, bare) if f.rule_id == "PE-SI-011"] == [] stack = LayoutStackup( copper_layers=["F.Cu", "B.Cu"], dielectrics=[LayoutDielectric(name="dielectric_1", er=4.5, height_mm=0.2)], copper_thickness_mm=0.035, ) no_zone = LayoutGraph(segments=segs, stackup=stack) assert [f for f in check_hf_lines(_usb_graph(), {}, no_zone) if f.rule_id == "PE-SI-011"] == [] covered = LayoutGraph( segments=segs, stackup=stack, zones=[LayoutZone( net="GND", layer="B.Cu", outlines=[[(-1, -2), (21, -2), (21, 2), (-1, 2)]], )], ) assert [f for f in check_hf_lines(_usb_graph(), {}, covered) if f.rule_id == "PE-SI-011"] == [] split = LayoutGraph( segments=segs, stackup=stack, zones=[LayoutZone( net="GND", layer="B.Cu", outlines=[[(0, -2), (5, -2), (5, 2), (0, 2)]], )], ) hits = [f for f in check_hf_lines(_usb_graph(), {}, split) if f.rule_id == "PE-SI-011"] assert len(hits) == 1 assert hits[0].status == "WARNING" assert hits[0].finding_class == "REVIEW" assert hits[0].facts assert hits[0].requirement assert hits[0].inference complete_finding(hits[0]) assert hits[0].status != "ERROR" def test_bom_termination_is_fact_missing_is_skip(): layout = LayoutGraph( segments=[ LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"), ], ) g = _usb_graph() assert [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"] == [] g.components["R22"] = Component( reference="R22", value="22R", footprint="", component_type=ComponentType.RESISTOR, pins={"1": "USB_D+", "2": "USB_D+_PHY"}, specs=ResistorSpecs(value_ohms=22.0, value_formatted="22"), ) g.nets["USB_D+_PHY"] = _net("USB_D+_PHY", ("R22", "2")) g.nets["USB_D+"].pins.append(PinConnection(component_ref="R22", pin_number="1")) found = [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"] assert len(found) == 1 assert found[0].status == "INFO" assert found[0].evidence_status == "SUFFICIENT" assert "R22" in found[0].facts assert "22" in found[0].facts assert "50 Ω" not in found[0].requirement def test_no_ddr_cpu_fpga_on_usb_only_graph(): findings = check_memory_fpga_classes(_usb_graph()) assert findings == [] ids = {f.rule_id for f in run_pcb_checks(_usb_graph(), {}, None)} assert not any(str(i).startswith(("PE-DDR", "PE-CPU", "PE-FPGA")) for i in ids) def test_ddr_present_class_and_nets_skip_invented_vtt(): pins = {str(i + 1): f"DDR3_DQ{i}" for i in range(8)} pins["20"] = "DDR3_DQS0_P" pins["21"] = "DDR3_DQS0_N" pins["22"] = "DDR3_CK_P" pins["23"] = "DDR3_CK_N" pins["24"] = "DDR3_A0" comps = {"U10": _ic("U10", pins, mpn="MT41K256M16TW", subtype="ic.memory.ddr")} nets = {n: _net(n, ("U10", p)) for p, n in pins.items()} g = DesignGraph(components=comps, nets=nets) findings = check_memory_fpga_classes(g) for f in findings: complete_finding(f) assert [f.rule_id for f in findings if f.rule_id == "PE-DDR-001"] assert [f for f in findings if f.rule_id == "PE-DDR-001"][0].status == "INFO" assert [f for f in findings if f.rule_id == "PE-DDR-002"][0].status == "INFO" assert not any(f.rule_id == "PE-DDR-003" for f in findings) assert not any((f.rule_id or "").startswith("PE-CPU") for f in findings) assert not any((f.rule_id or "").startswith("PE-FPGA") for f in findings) g.nets["DDR_VTT"] = _net("DDR_VTT", ("U10", "30"), ntype=NetType.POWER) g.components["U10"].pins["30"] = "DDR_VTT" with_vtt = check_memory_fpga_classes(g) assert [f for f in with_vtt if f.rule_id == "PE-DDR-003"][0].status == "INFO" def test_cpu_parallel_bus_only_when_data_addr_control_exist(): mcu_pins = {str(i + 1): f"MCU_D{i}" for i in range(8)} for i in range(8): mcu_pins[str(20 + i)] = f"MCU_A{i}" mcu_pins["40"] = "MCU_NWE" mcu_pins["41"] = "MCU_NOE" mcu_pins["42"] = "MCU_NCS" sram_pins = dict(mcu_pins) comps = { "U1": _ic("U1", mcu_pins, mpn="STM32F407", subtype="ic.mcu"), "U2": _ic("U2", sram_pins, mpn="IS61WV51216", subtype="ic.memory.sram"), } nets = {} for p, n in mcu_pins.items(): nets[n] = _net(n, ("U1", p), ("U2", p)) g = DesignGraph(components=comps, nets=nets) findings = check_memory_fpga_classes(g) ids = {f.rule_id for f in findings} assert "PE-CPU-001" in ids assert "PE-CPU-002" in ids assert "PE-CPU-003" in ids assert "PE-CPU-004" in ids assert not any(str(i).startswith("PE-DDR") for i in ids) assert not any(str(i).startswith("PE-FPGA") for i in ids) gpio = DesignGraph( components={"U1": _ic("U1", {"1": "GPIO0", "2": "GPIO1"}, mpn="MSPM0", subtype="ic.mcu")}, nets={ "GPIO0": _net("GPIO0", ("U1", "1")), "GPIO1": _net("GPIO1", ("U1", "2")), }, ) assert check_memory_fpga_classes(gpio) == [] def test_fpga_only_when_present_config_flash_optional(): fpga_pins = {"1": "IO_L1P", "2": "IO_L1N", "3": "VCCINT", "4": "VCCIO"} g = DesignGraph( components={"U5": _ic("U5", fpga_pins, mpn="XC7A35T", subtype="ic.fpga")}, nets={ "IO_L1P": _net("IO_L1P", ("U5", "1")), "IO_L1N": _net("IO_L1N", ("U5", "2")), "VCCINT": _net("VCCINT", ("U5", "3"), ntype=NetType.POWER), "VCCIO": _net("VCCIO", ("U5", "4"), ntype=NetType.POWER), }, ) findings = check_memory_fpga_classes(g) ids = {f.rule_id for f in findings} assert "PE-FPGA-001" in ids assert "PE-FPGA-003" in ids assert "PE-FPGA-002" not in ids assert not any(str(i).startswith("PE-DDR") for i in ids) assert not any(str(i).startswith("PE-CPU") for i in ids) g.components["U6"] = _ic("U6", {"1": "FPGA_CS", "2": "FPGA_MOSI"}, mpn="W25Q64", subtype="ic.memory.flash") g.nets["FPGA_CS"] = _net("FPGA_CS", ("U6", "1"), ("U5", "10")) g.components["U5"].pins["10"] = "FPGA_CS" with_flash = check_memory_fpga_classes(g) assert any(f.rule_id == "PE-FPGA-002" for f in with_flash) def test_rule_catalog_hf_ids(): for rid, domain in ( ("PE-SI-007", "pcb"), ("PE-SI-011", "pcb"), ("PE-SI-012", "pcb"), ("PE-DDR-001", "shared"), ("PE-DDR-002", "shared"), ("PE-DDR-003", "shared"), ("PE-CPU-001", "shared"), ("PE-CPU-002", "shared"), ("PE-CPU-003", "shared"), ("PE-CPU-004", "shared"), ("PE-FPGA-001", "shared"), ("PE-FPGA-002", "shared"), ("PE-FPGA-003", "shared"), ): rec = lookup_rule(rid) assert rec is not None, rid assert rec.domain == domain def test_existing_si_usb_not_polluted_by_hf_geometry(): layout = LayoutGraph( segments=[ LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"), LayoutSegment(start=(0, 0.4), end=(12.5, 0.4), width=0.2, layer="F.Cu", net="USB_D-"), ], vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)], ) findings = check_si(_usb_graph(), {}, layout, [ { "net_name": "USB_D+", "partner_net_name": "USB_D-", "z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0, "length_mm": 10.0, "topologies": ["MICROSTRIP"], }, { "net_name": "USB_D-", "partner_net_name": "USB_D+", "z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0, "length_mm": 12.5, "topologies": ["MICROSTRIP"], }, ]) assert any(f.rule_id == "PE-SI-010" for f in findings) assert all(f.rule_id != "PE-SI-002" for f in findings)