POST /api/library/datasheets now writes an IC inbox card or a passive/discrete model. Empty pintables never enter library/extracted. AF Board+AI runs after unchanged run_pcb_checks (PE-SI / HF line stay).
206 lines
6.7 KiB
Python
206 lines
6.7 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 "_run_af_ai_section" in src
|
|
assert src.find("run_pcb_checks") < 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 "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)
|