"""G1 SI vs simple_project — no invented millimetres or USBPHY boards. Favor: real /USB.D+ and /USB.D- pair by suffix; eval has no PE-SI keys. Against: no .kicad_pcb → no PE-SI-001; 3W is not invented. I2C/GPIO/CC are not 50/90 Ω. ImpedenceFinder numbers vs layout_rules only. """ from __future__ import annotations from tests.paths import SIMPLE_PROJECT, TAXONOMY from pathlib import Path from backend.periscopex.eval_report import eval_simple_project from backend.periscopex.finding_engine import complete_finding from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, LayoutSegment, LayoutVia, Net, NetType, Pin, PinConnection, ) from backend.periscopex.si_check import bus_class, check_si, partner_net, skip_si_net SIMPLE = SIMPLE_PROJECT def _graph() -> DesignGraph: return DesignGraph.model_validate_json( (SIMPLE / "design_graph.json").read_text() ) def test_simple_project_usb_dp_dm_are_a_named_pair(): g = _graph() assert "/USB.D+" in g.nets assert "/USB.D-" in g.nets assert partner_net("/USB.D+") == "/USB.D-" assert partner_net("/USB.D-") == "/USB.D+" assert partner_net("/USBC.D+") == "/USBC.D-" def test_simple_project_without_pcb_has_no_ps_si_001(): findings = check_si(_graph(), {}, None) assert findings == [] assert all(f.rule_id != "PE-3W-001" for f in findings) def test_simple_project_eval_has_no_si_keys(): scores = eval_simple_project(SIMPLE) assert scores.finding_count == 8 assert scores.precision == 1.0 assert scores.recall == 1.0 assert not any( k.startswith("PE-SI-") or k.startswith("PE-3W-") for k in scores.extra_keys ) def test_skip_i2c_gpio_cc_regn(): assert skip_si_net("I2C_SDA") assert skip_si_net("GPIO9") assert skip_si_net("USB_CC1") assert skip_si_net("/power/REGN") assert skip_si_net("ESP32_EN") assert not skip_si_net("USB_D+") assert not skip_si_net("USB_DP") assert bus_class("USB_D+") == "usb2" assert bus_class("USB_SSTX_P") == "usb3" assert bus_class("ETH_TX+") == "eth_mdi" assert bus_class("RGMII_TXD0") == "rgmii" assert bus_class("SGMII_TX_P") == "sgmii" assert bus_class("DDR3_DQ0") == "ddr3_dq" assert bus_class("DDR3_DQS0_P") == "ddr3_dqs" assert bus_class("MIPI_D0_P") == "mipi" assert bus_class("GTX_CLK") == "hf_clk" assert bus_class("XTAL_IN") is None assert bus_class("USB_CC1") is None assert bus_class("I2C_SCL") is None def _usb_graph() -> DesignGraph: return DesignGraph( components={ "U1": Component( reference="U1", value="PHY", footprint="", component_type=ComponentType.IC, mpn="PHY", pins={"1": "USB_D+", "2": "USB_D-", "3": "USB_CC1", "4": "I2C_SDA"}, ), }, nets={ "USB_D+": Net(name="USB_D+", net_type=NetType.SIGNAL, pins=[ PinConnection(component_ref="U1", pin_number="1"), ]), "USB_D-": Net(name="USB_D-", net_type=NetType.SIGNAL, pins=[ PinConnection(component_ref="U1", pin_number="2"), ]), "USB_CC1": Net(name="USB_CC1", net_type=NetType.SIGNAL, pins=[]), "I2C_SDA": Net(name="I2C_SDA", net_type=NetType.SIGNAL, pins=[]), }, ) def _usb_layout() -> LayoutGraph: return 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-"), LayoutSegment(start=(0, 5), end=(8, 5), width=0.2, layer="F.Cu", net="USB_CC1"), LayoutSegment(start=(0, 8), end=(20, 8), width=0.2, layer="F.Cu", net="I2C_SDA"), ], vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)], ) def _if_rows(**kwargs): base = { "net_name": "USB_D+", "length_mm": 10.0, "partner_net_name": "USB_D-", "is_differential": True, "z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0, "topologies": ["MICROSTRIP"], "flags": (), } base.update(kwargs) dm = { "net_name": "USB_D-", "length_mm": 12.5, "partner_net_name": "USB_D+", "is_differential": True, "z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0, "topologies": ["MICROSTRIP"], "flags": (), } return [base, dm] def test_emmaforo_usb_avg_in_window_min_out_is_margin(): """Zavg 88 Ω in 81–99, min 46 out → MARGIN; CC is not a 90 Ω pair.""" cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "impedance", "net_class": "usb", "zdiff_ohm": 90, "tolerance_pct": 10, "note": "USB DP/DM 90 Ω ±10%", "source_page": 12, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows()) zf = [f for f in findings if f.rule_id == "PE-SI-002"] assert len(zf) == 1 assert zf[0].finding.startswith("MARGIN:") complete_finding(zf[0]) assert zf[0].status == "WARNING" assert zf[0].finding_class != "RULE" assert not any("CC" in (f.net or "") for f in findings) assert not any("I2C" in (f.net or "") for f in findings) def test_usb_without_library_z_is_insufficient_not_90ohm_fail(): findings = check_si(_usb_graph(), {}, _usb_layout(), _if_rows()) ids = {f.rule_id for f in findings} assert "PE-SI-010" in ids assert "PE-SI-002" not in ids f = next(x for x in findings if x.rule_id == "PE-SI-010") assert f.evidence_status == "INSUFFICIENT" assert f.status == "INFO" assert "90" not in (f.requirement or "") blob = f"{f.finding} {f.requirement} {f.inference}" assert "FAIL" not in f.finding assert "90 Ω" not in blob or "folklore" in (f.inference or "").lower() assert "CC" not in (f.net or "") def test_length_match_2_5mm_vs_1mm_is_fail(): cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "length_match", "net_class": "usb", "max_distance_mm": 1.0, "note": "intra-pair < 1 mm", "source_page": 12, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows()) sk = [f for f in findings if f.rule_id == "PE-SI-001"] assert sk and sk[0].finding.startswith("FAIL:") complete_finding(sk[0]) assert sk[0].status == "ERROR" def test_i2c_not_checked_as_50_ohm(): cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "impedance", "z0_ohm": 50, "tolerance_pct": 10, "note": "should not hit I2C", "source_page": 1, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), [ *_if_rows(), {"net_name": "I2C_SDA", "z0_avg_ohms": 120.0, "length_mm": 20.0}, ]) assert all("I2C" not in (f.net or "") for f in findings) assert all("SDA" not in f.finding for f in findings) def test_en_rc_series_resistor_does_not_paint_usb(): """PE-SI-009 must not use ESP32 EN RC 10 kΩ / 1 µF as a USB series R.""" cons = ComponentConstraints( mpn="ESP32", pintable=[Pin(number="1", name="D+"), Pin(number="3", name="EN")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "series_resistor", "pin": "EN", "value_ohms": 10000, "note": "EN RC 10 kΩ / 1 µF", "source_page": 28, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows()) si009 = [f for f in findings if f.rule_id == "PE-SI-009"] assert si009 == [] zfail = [f for f in findings if f.rule_id == "PE-SI-002"] assert zfail == [] si010 = [f for f in findings if f.rule_id == "PE-SI-010"] assert si010 assert si010[0].evidence_status == "INSUFFICIENT" assert "88" in (si010[0].finding or "") assert "90" not in (si010[0].requirement or "") def _phy_graph() -> DesignGraph: pins = { "1": "ETH_TX+", "2": "ETH_TX-", "3": "RGMII_TXD0", "4": "RGMII_TXD1", "5": "USB_SSTX_P", "6": "USB_SSTX_N", "7": "DDR3_DQ0", "8": "DDR3_DQ1", "9": "DDR3_DQS0_P", "10": "DDR3_DQS0_N", "11": "USB_D+", "12": "USB_D-", } nets = { n: Net(name=n, net_type=NetType.SIGNAL, pins=[ PinConnection(component_ref="U1", pin_number=p), ]) for p, n in pins.items() } return DesignGraph( components={ "U1": Component( reference="U1", value="PHY", footprint="", component_type=ComponentType.IC, mpn="PHY", pins=pins, ), }, nets=nets, ) def _phy_layout() -> LayoutGraph: segs = [ LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="ETH_TX+"), LayoutSegment(start=(0, 0.4), end=(10.2, 0.4), width=0.2, layer="F.Cu", net="ETH_TX-"), LayoutSegment(start=(0, 2), end=(8, 2), width=0.15, layer="F.Cu", net="RGMII_TXD0"), LayoutSegment(start=(0, 2.4), end=(8.4, 2.4), width=0.15, layer="F.Cu", net="RGMII_TXD1"), LayoutSegment(start=(0, 4), end=(20, 4), width=0.12, layer="F.Cu", net="USB_SSTX_P"), LayoutSegment(start=(0, 4.3), end=(20.5, 4.3), width=0.12, layer="F.Cu", net="USB_SSTX_N"), LayoutSegment(start=(0, 6), end=(15, 6), width=0.1, layer="F.Cu", net="DDR3_DQ0"), LayoutSegment(start=(0, 6.2), end=(15.1, 6.2), width=0.1, layer="F.Cu", net="DDR3_DQ1"), LayoutSegment(start=(0, 7), end=(12, 7), width=0.1, layer="F.Cu", net="DDR3_DQS0_P"), LayoutSegment(start=(0, 7.2), end=(12.3, 7.2), width=0.1, layer="F.Cu", net="DDR3_DQS0_N"), LayoutSegment(start=(0, 9), end=(10, 9), width=0.2, layer="F.Cu", net="USB_D+"), LayoutSegment(start=(0, 9.4), end=(12.5, 9.4), width=0.2, layer="F.Cu", net="USB_D-"), ] return LayoutGraph(segments=segs, vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)]) def _z(name, partner, avg, length, **kw): row = { "net_name": name, "partner_net_name": partner, "is_differential": True, "z0_avg_ohms": avg, "z0_min_ohms": avg - 2, "z0_max_ohms": avg + 2, "length_mm": length, "topologies": ["MICROSTRIP"], "flags": (), } row.update(kw) return row def test_coverage_mdi_rgmii_ddr3_usb3_gated_not_cross_mapped(): cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="TX+")], absolute_maximum_ratings=[], rules=[], layout_rules=[ { "kind": "impedance", "net_class": "eth_mdi", "zdiff_ohm": 100, "tolerance_pct": 10, "note": "MDI 100 Ω to RJ45", "source_page": 40, }, { "kind": "max_length", "net_class": "rgmii", "max_distance_mm": 20, "note": "RGMII MAC–PHY trace < 20 mm", "source_page": 41, }, { "kind": "length_match", "net_class": "ddr3", "max_distance_mm": 1.0, "note": "DDR3 DQS intra-pair", "source_page": 42, }, { "kind": "impedance", "net_class": "usb3", "zdiff_ohm": 90, "tolerance_pct": 10, "note": "USB-C SuperSpeed 90 Ω", "source_page": 43, }, { "kind": "series_resistor", "net_class": "rgmii", "value_ohms": 22, "note": "RGMII series 22 Ω", "source_page": 41, }, ], ) rows = [ _z("ETH_TX+", "ETH_TX-", 95.0, 10.0), _z("ETH_TX-", "ETH_TX+", 95.0, 10.2), _z("USB_SSTX_P", "USB_SSTX_N", 88.0, 20.0), _z("USB_SSTX_N", "USB_SSTX_P", 88.0, 20.5), _z("DDR3_DQS0_P", "DDR3_DQS0_N", 50.0, 12.0), _z("DDR3_DQS0_N", "DDR3_DQS0_P", 50.0, 12.3), _z("USB_D+", "USB_D-", 88.0, 10.0), _z("USB_D-", "USB_D+", 88.0, 12.5), {"net_name": "RGMII_TXD0", "z0_avg_ohms": 48.0, "length_mm": 8.0, "topologies": ["MICROSTRIP"]}, {"net_name": "RGMII_TXD1", "z0_avg_ohms": 48.0, "length_mm": 8.4, "topologies": ["MICROSTRIP"]}, {"net_name": "DDR3_DQ0", "z0_avg_ohms": 50.0, "length_mm": 15.0, "topologies": ["MICROSTRIP"]}, ] findings = check_si(_phy_graph(), {"PHY": cons}, _phy_layout(), rows) by = {} for f in findings: by.setdefault(f.rule_id, []).append(f) mdi_z = [f for f in by.get("PE-SI-002", []) if f.net and "ETH" in f.net] assert mdi_z and mdi_z[0].finding.startswith("PASS:") usb3_z = [f for f in by.get("PE-SI-002", []) if f.net and "SSTX" in f.net] assert usb3_z and usb3_z[0].finding.startswith("PASS:") usb2_z = [f for f in by.get("PE-SI-002", []) if f.net and "USB_D" in (f.net or "")] assert usb2_z == [] rgmii_len = [f for f in by.get("PE-SI-003", []) if f.net and "RGMII" in f.net] assert rgmii_len and rgmii_len[0].finding.startswith("PASS:") assert not any(f.net and "ETH_TX" in f.net for f in by.get("PE-SI-003", [])) dqs = [f for f in by.get("PE-SI-001", []) if f.net and "DQS" in f.net] assert dqs and dqs[0].finding.startswith("PASS:") si009 = by.get("PE-SI-009", []) assert si009 assert all(f.net and "RGMII" in f.net for f in si009) assert not any(f.net and "USB" in f.net for f in si009) si010 = by.get("PE-SI-010", []) nets010 = {f.net for f in si010} assert any(n and "USB_D" in n for n in nets010) assert not any(n and "ETH_TX" in n for n in nets010) assert not any(n and "SSTX" in n for n in nets010) def test_empty_net_class_impedance_does_not_paint_usb(): cons = ComponentConstraints( mpn="PHY", pintable=[Pin(number="1", name="D+")], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "impedance", "z0_ohm": 50, "tolerance_pct": 10, "note": "generic 50 Ω with no bus", "source_page": 1, }], ) findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows()) assert all(f.rule_id != "PE-SI-002" for f in findings) assert any(f.rule_id == "PE-SI-010" for f in findings)