Files
periscope/tests/af_ai/test_af_ai_hf.py
T
michele c87adf11a9 Add AF trace analysis after PCB checks (2.63.5).
Trigger λ/10 and tr/(6 tpd) from FACT; pairs analyzed together; Z only from
datasheet; visible INSUFFICIENT skips; lossless RLGC cascade (no OpenEMS).
2026-09-22 08:39:37 +02:00

209 lines
6.8 KiB
Python

"""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,
append_investigated,
hypotheses_from_rows,
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 "")
def test_append_investigated_keeps_pcb_checks_and_adds_pe_af():
graph = _usb_graph()
layout = _stub_layout()
cons = _stub_constraints()
pcb = run_pcb_checks(graph, cons, layout)
n_si = sum(1 for f in pcb if f.rule_id == "PE-SI-007")
assert n_si >= 1
flags = [
HfHypothesis(net="USB_D+", issue_class="stub", why="looks long"),
HfHypothesis(net="USB_D+", issue_class="z0", why="probably 90 ohm"),
]
added = append_investigated(pcb, graph, cons, layout, flags)
assert any(f.rule_id == "PE-AF-001" for f in added)
assert sum(1 for f in pcb if f.rule_id == "PE-SI-007") == n_si
assert "si_check" in _pcb_checks_source()
assert "hf_line" in _pcb_checks_source()
def test_hypotheses_from_rows_drops_unknown_class():
rows = [
{"net": "USB_D+", "issue_class": "stub", "why": "x"},
{"net": "USB_D+", "issue_class": "clearance", "why": "drc"},
{"net": "", "issue_class": "z0", "why": "empty"},
]
hyps = hypotheses_from_rows(rows)
assert len(hyps) == 1
assert hyps[0].issue_class == "stub"
def test_pcb_pipeline_af_ai_is_after_run_pcb_checks():
import inspect
from backend.periscopex import pcb_checks
from backend.services import pcb_pipeline
src = inspect.getsource(pcb_pipeline.run_pcb_pipeline)
assert "run_pcb_checks" in src
assert "check_af_traces" in src
assert "_run_af_ai_section" in src
assert src.find("run_pcb_checks") < src.find("check_af_traces")
assert src.find("check_af_traces") < src.find("_run_af_ai_section")
checks = inspect.getsource(pcb_checks.run_pcb_checks)
assert '("si_check"' in checks or '"si_check"' in checks
assert '"hf_line"' in checks
assert "af_ai" not in checks
assert "check_af_traces" not in checks
assert "investigate_hf_hypotheses" not in checks
def _pcb_checks_source() -> str:
import inspect
from backend.periscopex.pcb_checks import run_pcb_checks
return inspect.getsource(run_pcb_checks)