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.
This commit is contained in:
@@ -141,7 +141,7 @@ def test_hbm_not_classic_ddr_dq_rules():
|
||||
assert one.groups == []
|
||||
assert one.physical_interface_id.endswith("-package")
|
||||
sec, findings = protocol_exam(_hbm_graph())
|
||||
assert sec.macrophase == "M8"
|
||||
assert sec.macrophase == "M9"
|
||||
assert all(f.status != "ERROR" for f in findings)
|
||||
rows = [
|
||||
c for r in sec.recognized_instances if "hbm" in str(r.get("logical_protocol_id"))
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_axi_internal_l0_not_applicable_not_fail():
|
||||
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
||||
assert all(r.result != "FAIL" for r in rows)
|
||||
sec, findings = protocol_exam(_axi_graph())
|
||||
assert sec.max_level_reached == "L2"
|
||||
assert sec.max_level_reached == "L3"
|
||||
from backend.periscopex.protocol_report import PACK_MACROPHASE as L8_MACRO
|
||||
assert sec.macrophase == L8_MACRO
|
||||
assert all(f.status != "ERROR" for f in findings)
|
||||
|
||||
@@ -165,8 +165,8 @@ def test_axi_internal_l1_not_applicable():
|
||||
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
||||
assert all(r.result != "FAIL" for r in rows)
|
||||
sec, exam_findings = protocol_exam(_axi_graph(), layout=LayoutGraph())
|
||||
assert sec.max_level_reached == "L2"
|
||||
assert sec.macrophase == "M8"
|
||||
assert sec.max_level_reached == "L3"
|
||||
assert sec.macrophase == "M9"
|
||||
assert all(f.status != "ERROR" for f in exam_findings)
|
||||
blob = json.dumps([r.model_dump() for r in rows])
|
||||
assert "electrical certification" not in blob.lower() or "not electrical" in blob.lower()
|
||||
@@ -257,7 +257,7 @@ def test_l1_does_not_run_impedance():
|
||||
|
||||
def test_protocol_exam_attaches_l1_checks():
|
||||
sec, _ = protocol_exam(_usb_connected(), layout=_usb_layout())
|
||||
assert sec.max_level_reached == "L2"
|
||||
assert sec.max_level_reached == "L3"
|
||||
assert sec.recognized_instances
|
||||
row = sec.recognized_instances[0]
|
||||
assert row.get("l1_checks")
|
||||
|
||||
@@ -204,8 +204,8 @@ def test_axi_internal_l2_not_applicable():
|
||||
assert rows
|
||||
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
||||
sec, findings = protocol_exam(_axi_graph())
|
||||
assert sec.max_level_reached == "L2"
|
||||
assert sec.macrophase == "M8"
|
||||
assert sec.max_level_reached == "L3"
|
||||
assert sec.macrophase == "M9"
|
||||
assert all(f.status != "ERROR" for f in findings)
|
||||
blob = json.dumps([r.model_dump() for r in rows])
|
||||
assert "L0/L1 are not electrical" in blob
|
||||
@@ -253,7 +253,7 @@ def test_l2_does_not_run_l3_or_geometry():
|
||||
|
||||
def test_protocol_exam_attaches_l2_checks():
|
||||
sec, _ = protocol_exam(_usb_connected())
|
||||
assert sec.max_level_reached == "L2"
|
||||
assert sec.max_level_reached == "L3"
|
||||
row = sec.recognized_instances[0]
|
||||
assert row.get("l2_checks")
|
||||
assert all(c.get("level") == "L2" for c in row["l2_checks"])
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
"""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()
|
||||
@@ -95,8 +95,8 @@ def test_result_rank_fail_before_warning():
|
||||
|
||||
def test_usb2_pair_l0_pass_l2_z_unknown_no_invented_ohm():
|
||||
sec, _ = protocol_exam(_usb_connected())
|
||||
assert sec.max_level_reached == "L2"
|
||||
assert sec.macrophase == "M8"
|
||||
assert sec.max_level_reached == "L3"
|
||||
assert sec.macrophase == "M9"
|
||||
usb = next(
|
||||
r for r in sec.recognized_instances
|
||||
if "usb" in str(r.get("logical_protocol_id"))
|
||||
|
||||
Reference in New Issue
Block a user