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:
2026-09-21 22:12:00 +02:00
parent 20733f0ec6
commit 4df04df5d4
101 changed files with 2060 additions and 302 deletions
+146
View File
@@ -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 "")