Make the component library a standalone product door (2.63.0).
Add /api/library datasheet import and component GET/PUT with no exam. Reorganize pytest into datasheet, library, schematic, PCB, and AF+AI. Document Rust criteria (none chosen; no rustup) and coding conformity.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""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 "")
|
||||
Reference in New Issue
Block a user