"""AF board + AI: hypotheses of probable HF issues, then deterministic checks. Does not replace run_pcb_checks. Not DRC. Never invents Z. """ from __future__ import annotations from backend.periscopex.af_ai_hf import HfHypothesis, investigate_hf_hypotheses from backend.periscopex.hf_line_check import check_hf_lines from backend.periscopex.models import ( Component, ComponentConstraints, ComponentType, DesignGraph, LayoutFootprint, LayoutGraph, LayoutPad, LayoutSegment, LayoutStackup, LayoutDielectric, Net, NetType, Pin, PinConnection, ) from backend.periscopex.pcb_checks import run_pcb_checks def _ic(ref: str, pins: dict[str, str], *, mpn: str = "PHY") -> Component: return Component( reference=ref, value=mpn, footprint="", component_type=ComponentType.IC, mpn=mpn, pins=pins, ) def _net(name: str, *pairs: tuple[str, str]) -> Net: return Net( name=name, net_type=NetType.SIGNAL, 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(ref: str, pads: list[LayoutPad], x: float = 0.0) -> LayoutFootprint: return LayoutFootprint(reference=ref, footprint="P", x=x, y=0.0, pads=pads) def _stub_layout() -> LayoutGraph: return LayoutGraph( footprints={ "U1": _fp("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]), "J2": _fp("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+"), ], ) def _stub_constraints() -> dict: return { "PHY": 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, }], ), } def test_ai_stub_flag_then_deterministic_pe_si_007(): graph = _usb_graph() layout = _stub_layout() cons = _stub_constraints() flags = [HfHypothesis(net="USB_D+", issue_class="stub", why="looks long")] out = investigate_hf_hypotheses(graph, cons, layout, flags) codes = [f.rule_id for f in out] assert "PE-SI-007" in codes hit = next(f for f in out if f.rule_id == "PE-SI-007") assert "FAIL" in hit.finding assert hit.evidence_status == "SUFFICIENT" def test_ai_z0_flag_without_stackup_is_insufficient_no_ohm(): graph = _usb_graph() layout = _stub_layout() flags = [HfHypothesis(net="USB_D+", issue_class="z0", why="probably 90 ohm")] out = investigate_hf_hypotheses(graph, {}, layout, flags) assert len(out) == 1 f = out[0] assert f.rule_id == "PE-AF-001" assert f.evidence_status == "INSUFFICIENT" assert "90" not in f.finding assert "50" not in f.finding assert "Ω" not in f.finding and "ohm" not in f.finding.lower() def test_ai_flag_on_missing_superspeed_net_is_silence(): graph = _usb_graph() layout = _stub_layout() flags = [HfHypothesis(net="USB3_SSTX_P", issue_class="stub", why="SS pair")] out = investigate_hf_hypotheses(graph, _stub_constraints(), layout, flags) assert out == [] def test_run_pcb_checks_still_finds_stub_without_af_ai(): """AF+AI is extra: the PCB check list must still emit PE-SI-007 alone.""" graph = _usb_graph() layout = _stub_layout() pcb = run_pcb_checks(graph, _stub_constraints(), layout) assert any(f.rule_id == "PE-SI-007" for f in pcb) hf = check_hf_lines(graph, _stub_constraints(), layout) assert any(f.rule_id == "PE-SI-007" for f in hf) def test_z0_with_stackup_does_not_invent_ohm_number(): graph = _usb_graph() layout = _stub_layout() layout.stackup = LayoutStackup( copper_layers=["F.Cu", "B.Cu"], dielectrics=[LayoutDielectric(name="core", er=4.5, height_mm=0.15)], ) flags = [HfHypothesis(net="USB_D+", issue_class="z0", why="check Z0")] out = investigate_hf_hypotheses(graph, {}, layout, flags) for f in out: assert "50 Ω" not in (f.finding or "") assert "90 Ω" not in (f.finding or "")