Files
periscope/tests/pcb/test_protocol_l3_m9.py
T
michele 866c2e9242 Add protocol-certification M9 L3 channel certifier (2.71.0).
L3 CHANNEL structure for timing budget, insertion loss, return loss,
crosstalk, and complete channel. Numeric PASS/FAIL only from existing
channel FACT; otherwise a visible skip. No OpenEMS, no invented S-parameters,
no M6 packs.
2026-09-22 12:58:18 +02:00

317 lines
11 KiB
Python

"""M9 L3 CHANNEL certifier: structure + skip; numeric only with existing FACT."""
from __future__ import annotations
import json
from backend.periscopex.models import (
Component,
ComponentType,
DesignGraph,
Net,
NetType,
PinConnection,
)
from backend.periscopex.protocol_catalog import (
ConstraintSource,
PhysicalInterface,
ProtocolConstraint,
)
from backend.periscopex.protocol_l0 import protocol_exam
from backend.periscopex.protocol_l3 import (
_pack,
certify_instance_l3,
certify_l3,
remaining_margin_ps,
)
from backend.periscopex.protocol_recognize import (
PhysicalBusInstance,
recognize_physical_buses,
)
def _ic(ref: str, pins: dict[str, str], *, mpn: str = "", value: str = "") -> Component:
return Component(
reference=ref, value=value or mpn, footprint="",
component_type=ComponentType.IC, mpn=mpn or None, 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_connected() -> DesignGraph:
return DesignGraph(
components={
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}, mpn="CH340E"),
"J2": Component(
reference="J2", value="USB2_TypeA",
footprint="USB_A",
component_type=ComponentType.CONNECTOR,
pins={"2": "USB_D+", "3": "USB_D-"},
),
},
nets={
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "2")),
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "3")),
},
)
def _hdmi_graph() -> DesignGraph:
pins = {"1": "HDMI_TX0_P", "2": "HDMI_TX0_N", "3": "HDMI_CLK_P", "4": "HDMI_CLK_N"}
return DesignGraph(
components={
"U8": _ic("U8", pins, mpn="ADV7511", value="HDMI transmitter"),
"J8": Component(
reference="J8", value="HDMI-A",
footprint="HDMI",
component_type=ComponentType.CONNECTOR,
pins=pins,
),
},
nets={n: _net(n, ("U8", p), ("J8", p)) for p, n in pins.items()},
schematic_fields={"J8": {"protocol": "hdmi-1.4-tmds-type-a"}},
)
def _pcie_graph() -> DesignGraph:
pins = {"1": "PCIE_TX_P", "2": "PCIE_TX_N", "3": "PCIE_RX_P", "4": "PCIE_RX_N"}
return DesignGraph(
components={
"U7": _ic("U7", pins, mpn="PI7C9X2G304GP", value="PCIe switch"),
"J7": Component(
reference="J7", value="PCIe x1",
footprint="PCIE",
component_type=ComponentType.CONNECTOR,
pins=pins,
),
},
nets={n: _net(n, ("U7", p), ("J7", p)) for p, n in pins.items()},
schematic_fields={"U7": {"protocol": "pcie-phy-pcb"}},
)
def _axi_graph() -> DesignGraph:
return DesignGraph(
components={
"U3": _ic("U3", {"1": "AXI_AWVALID"}, mpn="XC7A100T", value="AXI4 interconnect"),
},
nets={"AXI_AWVALID": _net("AXI_AWVALID", ("U3", "1"))},
)
def _cite() -> ConstraintSource:
return ConstraintSource(
document="Fixture channel pack", organization="TESTLAB", section="4.1",
)
def _iface_with_numeric(*, il_db: float = -8.0, rl_db: float = -10.0,
margin_ps: float = 0.0) -> PhysicalInterface:
return PhysicalInterface(
id="fixture-hs-dpair",
logical_protocol_id="fixture-hs",
bus_type="SERIAL",
physical_layer="SERIAL_DIFFERENTIAL",
pcb_relevant="YES",
required_checks=[
"insertion_loss", "return_loss", "crosstalk",
"timing_budget", "channel",
],
constraints=[
ProtocolConstraint(
id="fixture-il", parameter="insertion_loss",
value_kind="NUMERIC", mandatory="MANDATORY",
source_type="STANDARD", source_class="NORMATIVE",
value=il_db, unit="dB", source=_cite(),
),
ProtocolConstraint(
id="fixture-rl", parameter="return_loss",
value_kind="NUMERIC", mandatory="MANDATORY",
source_type="STANDARD", source_class="NORMATIVE",
value=rl_db, unit="dB", source=_cite(),
),
ProtocolConstraint(
id="fixture-xt", parameter="crosstalk",
value_kind="NUMERIC", mandatory="RECOMMENDED",
source_type="STANDARD", source_class="NORMATIVE",
value=-20.0, unit="dB", source=_cite(),
),
ProtocolConstraint(
id="fixture-tb", parameter="timing_budget",
value_kind="NUMERIC", mandatory="MANDATORY",
source_type="STANDARD", source_class="NORMATIVE",
value=margin_ps, unit="ps", source=_cite(),
),
],
)
def _inst() -> PhysicalBusInstance:
return PhysicalBusInstance(
instance_id="fixture-1",
logical_protocol_id="fixture-hs",
physical_interface_id="fixture-hs-dpair",
pcb_relevant="YES",
confidence=1.0,
evidence_kind="net_name",
recognition_status="RECOGNIZED",
nets=["CH_P", "CH_N"],
host_ref="U1",
)
def test_remaining_margin_is_total_minus_used():
assert remaining_margin_ps(200.0, 120.0) == 80.0
assert remaining_margin_ps(200.0, 120.0, 77.0) == 77.0
assert remaining_margin_ps(None, 10.0) is None
def test_usb_hdmi_pcie_skip_without_channel_data():
for graph, token in ((_usb_connected(), "usb"), (_hdmi_graph(), "hdmi"), (_pcie_graph(), "pcie")):
sec, findings = protocol_exam(graph)
assert sec.max_level_reached == "L3"
assert sec.macrophase == "M9"
row = next(
r for r in sec.recognized_instances
if token in str(r.get("logical_protocol_id")).lower()
or token in str(r.get("physical_interface_id")).lower()
)
l3 = row["l3_checks"]
assert l3
names = {c["check"] for c in l3}
assert {"timing_budget", "insertion_loss", "return_loss", "crosstalk", "channel"} <= names
assert all(c["result"] != "PASS" for c in l3)
notes = " ".join(c.get("notes") or "" for c in l3)
assert "non certificata a L3 per mancanza" in notes
assert "OpenEMS" in notes
blob = json.dumps(l3)
assert "90" not in blob
assert any(f.rule_id == "PE-PRT-L3-001" for f in findings)
assert "s21" not in blob.lower() or "invent" in notes.lower()
def test_axi_l3_not_applicable_not_fail():
sec, findings = protocol_exam(_axi_graph())
row = next(
r for r in sec.recognized_instances
if "axi" in str(r.get("logical_protocol_id"))
)
l3 = row["l3_checks"]
assert l3
assert all(c["result"] == "NOT_APPLICABLE" for c in l3)
assert all(c["result"] != "FAIL" for c in l3)
assert all(f.rule_id != "PE-PRT-L3-002" for f in findings)
assert all(f.status != "ERROR" or f.source != "protocol_l3" for f in findings)
def test_fixture_channel_pass_with_existing_fact():
iface = _iface_with_numeric()
inst = _inst()
data = {
"instances": {
inst.instance_id: {
"insertion_loss_db": -3.0,
"return_loss_db": -15.0,
"crosstalk_db": -28.0,
"total_budget_ps": 200.0,
"used_budget_ps": 120.0,
"channel_complete": True,
}
}
}
rows = certify_instance_l3(inst, iface, channel_data=data)
by_check = {r.check: r for r in rows}
assert by_check["insertion_loss"].result == "PASS"
assert by_check["return_loss"].result == "PASS"
assert by_check["timing_budget"].result == "PASS"
assert by_check["timing_budget"].remaining_margin_ps == 80.0
assert by_check["channel"].result == "PASS"
assert by_check["insertion_loss"].measured_db == -3.0
notes = " ".join(r.notes.lower() for r in rows)
assert "not invented" in notes
assert "not openems" in notes
def test_fixture_channel_fail_mandatory_il():
iface = _iface_with_numeric()
inst = _inst()
data = {
inst.instance_id: {
"s21_db": -12.0,
"s11_db": -15.0,
"total_budget_ps": 200.0,
"used_budget_ps": 50.0,
}
}
rows = certify_instance_l3(inst, iface, channel_data=data)
il = next(r for r in rows if r.check == "insertion_loss")
assert il.result == "FAIL"
findings = []
from backend.periscopex.protocol_l3 import l3_findings
findings = l3_findings(inst, rows)
assert any(f.rule_id == "PE-PRT-L3-002" and f.status == "ERROR" for f in findings)
def test_recommended_never_fail_l3():
rec = _pack("crosstalk", "FAIL", "RECOMMENDED", notes="xt")
assert rec.result != "FAIL"
assert rec.status != "ERROR"
def test_raw_touchstone_without_fact_is_skip_not_invented():
iface = _iface_with_numeric()
inst = _inst()
data = {inst.instance_id: {"sparam_file": "lane.s4p", "touchstone": "lane.s4p"}}
rows = certify_instance_l3(inst, iface, channel_data=data)
assert all(r.result != "PASS" for r in rows)
notes = " ".join(r.notes for r in rows)
assert "non certificata a L3" in notes
assert "invent" in notes.lower() or "non inventati" in notes.lower() or "not invented" in notes.lower()
blob = json.dumps([r.model_dump() for r in rows])
# no synthesized S21 numbers
assert '"measured_db": null' in blob or all(r.measured_db is None for r in rows)
def test_openems_method_is_explicit_skip():
iface = _iface_with_numeric()
inst = _inst()
data = {
inst.instance_id: {
"insertion_loss_db": -2.0,
"method": "OpenEMS",
"solver": "FEM",
}
}
rows = certify_instance_l3(inst, iface, channel_data=data)
notes = " ".join(r.notes for r in rows)
assert "non certificata a L3" in notes
assert all(r.result != "PASS" for r in rows)
def test_no_sparam_invention_on_empty_channel_data():
insts = recognize_physical_buses(_usb_connected())
by, findings = certify_l3(insts, channel_data=None)
rows = [r for lst in by.values() for r in lst]
blob = json.dumps([r.model_dump() for r in rows])
assert "s21" not in blob.lower()
assert all(r.measured_db is None for r in rows)
assert any("mancanza" in r.notes for r in rows)
assert any(f.rule_id == "PE-PRT-L3-001" for f in findings)
def test_protocol_exam_attaches_l3_and_does_not_call_l0_electrical():
sec, _ = protocol_exam(_usb_connected())
row = sec.recognized_instances[0]
assert row.get("l3_checks")
assert all(c.get("level") == "L3" for c in row["l3_checks"])
notes = " ".join(c.get("notes") or "" for c in row["l3_checks"])
assert "L0/L1 are not electrical" in notes
l0notes = json.dumps(row.get("l0_checks") or [])
assert "ohm" not in l0notes.lower() or "not" in l0notes.lower()