Add protocol-certification M3 L1 geometric certifier (2.67.0).
Length, topology, vias, stubs, layer, grouping. Skew only vs NUMERIC mm or DESIGN LIMIT. No invented ohms/mm/ps. Not electrical. No L2/L3.
This commit is contained in:
@@ -294,6 +294,12 @@ def _seed() -> None:
|
|||||||
requirement="Protocol nets required at L0 must exist and connect two components.")
|
requirement="Protocol nets required at L0 must exist and connect two components.")
|
||||||
_add("PE-PRT-L0-002", "TYPICAL", "INFO", domain="pcb",
|
_add("PE-PRT-L0-002", "TYPICAL", "INFO", domain="pcb",
|
||||||
requirement="Internal interconnect is NOT_APPLICABLE for PCB routing.")
|
requirement="Internal interconnect is NOT_APPLICABLE for PCB routing.")
|
||||||
|
_add("PE-PRT-L1-001", "TYPICAL", "REVIEW", domain="pcb",
|
||||||
|
requirement="L1 geometric skip: missing PCB geometry or NUMERIC/DESIGN millimetre limit.")
|
||||||
|
_add("PE-PRT-L1-002", "TYPICAL", "REVIEW", domain="pcb",
|
||||||
|
requirement="L1 grouping (DQ/DQS / byte-lane) cannot be reconstructed — not PASS.")
|
||||||
|
_add("PE-PRT-L1-003", "MANDATORY", "RULE", domain="pcb",
|
||||||
|
requirement="L1 geometric length/via vs NUMERIC or DESIGN millimetre limit.")
|
||||||
|
|
||||||
|
|
||||||
_seed()
|
_seed()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from typing import Any, Literal
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from backend.periscopex.finding_engine import complete_finding
|
from backend.periscopex.finding_engine import complete_finding
|
||||||
from backend.periscopex.models import DesignGraph, Finding
|
from backend.periscopex.models import DesignGraph, Finding, LayoutGraph
|
||||||
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
|
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
|
||||||
from backend.periscopex.protocol_catalog import (
|
from backend.periscopex.protocol_catalog import (
|
||||||
SCHEMA_VERSION,
|
SCHEMA_VERSION,
|
||||||
@@ -93,6 +93,12 @@ def _constraint_for(iface: PhysicalInterface, check: str) -> ProtocolConstraint
|
|||||||
"superspeed_pairs": "superspeed_pairs",
|
"superspeed_pairs": "superspeed_pairs",
|
||||||
"topology": "topology",
|
"topology": "topology",
|
||||||
"byte_lane_mapping": "topology",
|
"byte_lane_mapping": "topology",
|
||||||
|
"byte_lane_skew": "byte_to_byte_skew",
|
||||||
|
"length": "max_length",
|
||||||
|
"clock_length": "max_length",
|
||||||
|
"data_length": "max_length",
|
||||||
|
"via_count": "vias",
|
||||||
|
"stub_length": "stub_length",
|
||||||
}
|
}
|
||||||
want = aliases.get(check, check)
|
want = aliases.get(check, check)
|
||||||
for c in iface.constraints:
|
for c in iface.constraints:
|
||||||
@@ -289,26 +295,36 @@ def certify_l0(
|
|||||||
def protocol_exam(
|
def protocol_exam(
|
||||||
graph: DesignGraph | None,
|
graph: DesignGraph | None,
|
||||||
catalog: ProtocolCatalog | None = None,
|
catalog: ProtocolCatalog | None = None,
|
||||||
|
layout: LayoutGraph | None = None,
|
||||||
) -> tuple[ProtocolCertificationSection, list[Finding]]:
|
) -> tuple[ProtocolCertificationSection, list[Finding]]:
|
||||||
|
from backend.periscopex.protocol_l1 import (
|
||||||
|
PACK_MACROPHASE as L1_MACRO,
|
||||||
|
LEVEL as L1_LEVEL,
|
||||||
|
certify_l1,
|
||||||
|
)
|
||||||
|
|
||||||
if graph is None:
|
if graph is None:
|
||||||
return empty_protocol_section(), []
|
return empty_protocol_section(), []
|
||||||
cat = catalog or load_catalog()
|
cat = catalog or load_catalog()
|
||||||
instances = recognize_physical_buses(graph, cat)
|
instances = recognize_physical_buses(graph, cat)
|
||||||
if not instances:
|
if not instances:
|
||||||
sec = empty_protocol_section()
|
sec = empty_protocol_section()
|
||||||
sec.macrophase = PACK_MACROPHASE
|
sec.macrophase = L1_MACRO
|
||||||
return sec, []
|
return sec, []
|
||||||
l0, findings = certify_l0(graph, instances, cat)
|
l0, findings = certify_l0(graph, instances, cat)
|
||||||
|
l1, l1_findings_out = certify_l1(graph, instances, layout, cat)
|
||||||
|
findings.extend(l1_findings_out)
|
||||||
dumps: list[dict[str, Any]] = []
|
dumps: list[dict[str, Any]] = []
|
||||||
for inst in instances:
|
for inst in instances:
|
||||||
row = inst.model_dump()
|
row = inst.model_dump()
|
||||||
row["l0_checks"] = [c.model_dump() for c in l0.get(inst.instance_id, [])]
|
row["l0_checks"] = [c.model_dump() for c in l0.get(inst.instance_id, [])]
|
||||||
|
row["l1_checks"] = [c.model_dump() for c in l1.get(inst.instance_id, [])]
|
||||||
dumps.append(row)
|
dumps.append(row)
|
||||||
sec = ProtocolCertificationSection(
|
sec = ProtocolCertificationSection(
|
||||||
schema_version=SCHEMA_VERSION,
|
schema_version=SCHEMA_VERSION,
|
||||||
macrophase=PACK_MACROPHASE,
|
macrophase=L1_MACRO,
|
||||||
recognized_instances=dumps,
|
recognized_instances=dumps,
|
||||||
message="",
|
message="",
|
||||||
max_level_reached=LEVEL,
|
max_level_reached=L1_LEVEL,
|
||||||
)
|
)
|
||||||
return sec, findings
|
return sec, findings
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
"""M3: L1 GEOMETRIC protocol certifier. Length, vias, layer, grouping, skew.
|
||||||
|
|
||||||
|
Skew is compared only when the constraint is NUMERIC (length unit) or an
|
||||||
|
explicit DESIGN LIMIT. Otherwise UNKNOWN / CONTROLLER_DEPENDENT /
|
||||||
|
PHY_DEPENDENT / VENDOR_DEPENDENT. Length is not delay. Not electrical.
|
||||||
|
RECOMMENDED never FAIL. Internal interfaces are NOT_APPLICABLE.
|
||||||
|
Missing DQ/DQS grouping or missing PCB geometry is a visible skip, not PASS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from backend.periscopex.finding_engine import complete_finding
|
||||||
|
from backend.periscopex.models import DesignGraph, Finding, LayoutGraph
|
||||||
|
from backend.periscopex.protocol_catalog import (
|
||||||
|
PhysicalInterface,
|
||||||
|
ProtocolCatalog,
|
||||||
|
ProtocolConstraint,
|
||||||
|
load_catalog,
|
||||||
|
map_protocol_outcome,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_l0 import _constraint_for, _mandatory
|
||||||
|
from backend.periscopex.protocol_recognize import PhysicalBusInstance, SignalGroup
|
||||||
|
from backend.periscopex.si_check import net_length_mm
|
||||||
|
|
||||||
|
PACK_MACROPHASE = "M3"
|
||||||
|
SOURCE = "protocol_l1"
|
||||||
|
LEVEL = "L1"
|
||||||
|
|
||||||
|
L1_CHECKS = frozenset({
|
||||||
|
"length",
|
||||||
|
"clock_length",
|
||||||
|
"data_length",
|
||||||
|
"intra_pair_skew",
|
||||||
|
"pair_skew",
|
||||||
|
"dq_to_dqs_skew",
|
||||||
|
"byte_lane_skew",
|
||||||
|
"ck_to_command_skew",
|
||||||
|
"clock_to_data_skew",
|
||||||
|
"vias",
|
||||||
|
"via_count",
|
||||||
|
"stubs",
|
||||||
|
"stub_length",
|
||||||
|
"topology",
|
||||||
|
"byte_lane_mapping",
|
||||||
|
"dq_group",
|
||||||
|
"dqs_relationship_if_present",
|
||||||
|
"cmd_dat_grouping",
|
||||||
|
"lane_relationships",
|
||||||
|
})
|
||||||
|
|
||||||
|
_TIME_UNITS = frozenset({"ps", "ns", "us", "µs", "ms", "s"})
|
||||||
|
|
||||||
|
MandatoryClass = Literal["MANDATORY", "RECOMMENDED", "OPTIONAL", "INFORMATIONAL"]
|
||||||
|
L1Result = Literal[
|
||||||
|
"PASS", "FAIL", "WARNING", "UNKNOWN", "NOT_APPLICABLE",
|
||||||
|
"VENDOR_DEPENDENT", "CONTROLLER_DEPENDENT", "PHY_DEPENDENT", "MISSING_SOURCE",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class L1CheckResult(BaseModel):
|
||||||
|
check: str
|
||||||
|
level: Literal["L1"] = "L1"
|
||||||
|
result: L1Result
|
||||||
|
mandatory: MandatoryClass
|
||||||
|
nets: list[str] = Field(default_factory=list)
|
||||||
|
measured_mm: float | None = None
|
||||||
|
limit_mm: float | None = None
|
||||||
|
margin_mm: float | None = None
|
||||||
|
value_kind: str = ""
|
||||||
|
notes: str = ""
|
||||||
|
finding_class: str = ""
|
||||||
|
status: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _pack(
|
||||||
|
check: str,
|
||||||
|
result: str,
|
||||||
|
mandatory: MandatoryClass,
|
||||||
|
*,
|
||||||
|
nets: list[str] | None = None,
|
||||||
|
measured_mm: float | None = None,
|
||||||
|
limit_mm: float | None = None,
|
||||||
|
value_kind: str = "",
|
||||||
|
notes: str = "",
|
||||||
|
) -> L1CheckResult:
|
||||||
|
if result == "FAIL" and mandatory != "MANDATORY":
|
||||||
|
result = "WARNING"
|
||||||
|
mapped = result
|
||||||
|
if result in {"CONTROLLER_DEPENDENT", "PHY_DEPENDENT"}:
|
||||||
|
mapped = "VENDOR_DEPENDENT"
|
||||||
|
cls, status, _ev = map_protocol_outcome(mapped, mandatory) # type: ignore[arg-type]
|
||||||
|
margin = None
|
||||||
|
if measured_mm is not None and limit_mm is not None:
|
||||||
|
margin = limit_mm - measured_mm
|
||||||
|
return L1CheckResult(
|
||||||
|
check=check,
|
||||||
|
result=result, # type: ignore[arg-type]
|
||||||
|
mandatory=mandatory,
|
||||||
|
nets=nets or [],
|
||||||
|
measured_mm=measured_mm,
|
||||||
|
limit_mm=limit_mm,
|
||||||
|
margin_mm=margin,
|
||||||
|
value_kind=value_kind,
|
||||||
|
notes=notes,
|
||||||
|
finding_class=cls,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def geometric_verdict(constraint: ProtocolConstraint | None, measured_mm: float | None) -> str:
|
||||||
|
"""PASS/FAIL only with NUMERIC length limit or DESIGN LIMIT. Never invent ps."""
|
||||||
|
if constraint is None:
|
||||||
|
return "UNKNOWN"
|
||||||
|
kind = constraint.value_kind
|
||||||
|
unit = (constraint.unit or "").strip().lower()
|
||||||
|
design = constraint.limit_kind == "DESIGN" and constraint.value is not None
|
||||||
|
numeric = kind == "NUMERIC" and constraint.value is not None
|
||||||
|
if numeric or design:
|
||||||
|
if unit in _TIME_UNITS:
|
||||||
|
return "UNKNOWN"
|
||||||
|
if measured_mm is None:
|
||||||
|
return "UNKNOWN"
|
||||||
|
if measured_mm <= constraint.value:
|
||||||
|
return "PASS"
|
||||||
|
return "FAIL"
|
||||||
|
if kind in {
|
||||||
|
"UNKNOWN", "CONTROLLER_DEPENDENT", "PHY_DEPENDENT",
|
||||||
|
"VENDOR_DEPENDENT", "MISSING_SOURCE", "NOT_APPLICABLE",
|
||||||
|
}:
|
||||||
|
return kind
|
||||||
|
return "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
|
def _len(layout: LayoutGraph, net: str) -> float:
|
||||||
|
return net_length_mm(layout, net)
|
||||||
|
|
||||||
|
|
||||||
|
def _via_count(layout: LayoutGraph, net: str) -> int:
|
||||||
|
return sum(1 for v in layout.vias if v.net == net)
|
||||||
|
|
||||||
|
|
||||||
|
def _layers(layout: LayoutGraph, net: str) -> list[str]:
|
||||||
|
return sorted({s.layer for s in layout.segments if s.net == net and s.layer})
|
||||||
|
|
||||||
|
|
||||||
|
def _instance_nets(inst: PhysicalBusInstance) -> list[str]:
|
||||||
|
names: list[str] = []
|
||||||
|
for g in inst.groups:
|
||||||
|
names.extend(g.nets)
|
||||||
|
names.extend(inst.nets)
|
||||||
|
# preserve order, unique
|
||||||
|
seen: set[str] = set()
|
||||||
|
out: list[str] = []
|
||||||
|
for n in names:
|
||||||
|
if n not in seen:
|
||||||
|
seen.add(n)
|
||||||
|
out.append(n)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _skip_no_geometry(check: str, mand: MandatoryClass, inst: PhysicalBusInstance) -> L1CheckResult:
|
||||||
|
return _pack(
|
||||||
|
check, "UNKNOWN", mand,
|
||||||
|
notes=(
|
||||||
|
f"Interfaccia {inst.physical_interface_id} non certificata a L1 "
|
||||||
|
f"per mancanza di geometria PCB (piste). L1 is geometric, not electrical."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _skip_grouping(check: str, mand: MandatoryClass, inst: PhysicalBusInstance) -> L1CheckResult:
|
||||||
|
return _pack(
|
||||||
|
check, "UNKNOWN", mand,
|
||||||
|
notes=(
|
||||||
|
f"Interfaccia {inst.physical_interface_id} non certificata a L1 "
|
||||||
|
f"per mancanza di grouping (DQ/DQS). Not PASS."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def certify_instance_l1(
|
||||||
|
graph: DesignGraph,
|
||||||
|
inst: PhysicalBusInstance,
|
||||||
|
iface: PhysicalInterface,
|
||||||
|
layout: LayoutGraph | None,
|
||||||
|
) -> list[L1CheckResult]:
|
||||||
|
if inst.pcb_relevant == "NO" or iface.pcb_relevant == "NO":
|
||||||
|
return [_pack(
|
||||||
|
"pcb_routing", "NOT_APPLICABLE", "INFORMATIONAL",
|
||||||
|
notes="Internal interconnect — L1 geometric N/A, not FAIL, not electrical.",
|
||||||
|
)]
|
||||||
|
checks = [c for c in iface.required_checks if c in L1_CHECKS]
|
||||||
|
if not checks:
|
||||||
|
checks = ["length", "topology"]
|
||||||
|
return [_run_l1_check(graph, inst, iface, layout, c) for c in checks]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_l1_check(
|
||||||
|
graph: DesignGraph,
|
||||||
|
inst: PhysicalBusInstance,
|
||||||
|
iface: PhysicalInterface,
|
||||||
|
layout: LayoutGraph | None,
|
||||||
|
check: str,
|
||||||
|
) -> L1CheckResult:
|
||||||
|
mand = _mandatory(iface, check)
|
||||||
|
cons = _constraint_for(iface, check)
|
||||||
|
kind = cons.value_kind if cons else "UNKNOWN"
|
||||||
|
if layout is None:
|
||||||
|
return _skip_no_geometry(check, mand, inst)
|
||||||
|
|
||||||
|
grouping_names = {
|
||||||
|
"byte_lane_mapping", "dq_group", "dqs_relationship_if_present",
|
||||||
|
"dq_to_dqs_skew", "byte_lane_skew",
|
||||||
|
}
|
||||||
|
lanes = [g for g in inst.groups if g.kind == "BYTE_LANE"]
|
||||||
|
if check in grouping_names and not lanes:
|
||||||
|
return _skip_grouping(check, mand, inst)
|
||||||
|
if check == "cmd_dat_grouping":
|
||||||
|
cmd = [g for g in inst.groups if g.kind in {"COMMAND_GROUP", "ADDRESS_GROUP", "CLOCK_GROUP"}]
|
||||||
|
if not cmd:
|
||||||
|
return _skip_grouping(check, mand, inst)
|
||||||
|
return _pack(
|
||||||
|
check, "PASS", mand, nets=_instance_nets(inst), value_kind=kind,
|
||||||
|
notes="CMD/DAT grouping present. L1 is not electrical certification.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if check in {"byte_lane_mapping", "dq_group", "dqs_relationship_if_present"}:
|
||||||
|
return _pack(
|
||||||
|
check, "PASS", mand, nets=_instance_nets(inst), value_kind=kind,
|
||||||
|
notes="Geometric grouping present. L1 is not electrical certification.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if check == "topology":
|
||||||
|
nets = _instance_nets(inst)
|
||||||
|
routed = [n for n in nets if _len(layout, n) > 0]
|
||||||
|
if not routed:
|
||||||
|
return _skip_no_geometry(check, mand, inst)
|
||||||
|
layers = sorted({ly for n in routed for ly in _layers(layout, n)})
|
||||||
|
return _pack(
|
||||||
|
check, "PASS", mand, nets=routed, value_kind=kind,
|
||||||
|
notes=f"Routed on layers {layers}. Geometric topology only, not electrical.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if check in {"length", "clock_length", "data_length"}:
|
||||||
|
nets = _clock_or_data_nets(inst, check)
|
||||||
|
if not nets:
|
||||||
|
nets = _instance_nets(inst)
|
||||||
|
if not nets:
|
||||||
|
return _skip_no_geometry(check, mand, inst)
|
||||||
|
measured = max(_len(layout, n) for n in nets)
|
||||||
|
limit = cons.value if cons and cons.value_kind == "NUMERIC" and (cons.unit or "mm").lower() not in _TIME_UNITS else None
|
||||||
|
verdict = geometric_verdict(cons, measured)
|
||||||
|
return _pack(
|
||||||
|
check, verdict, mand, nets=nets, measured_mm=measured,
|
||||||
|
limit_mm=limit, value_kind=kind,
|
||||||
|
notes="Geometric length (mm). Not delay; not electrical Z.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if check in {"vias", "via_count"}:
|
||||||
|
nets = _instance_nets(inst)
|
||||||
|
count = sum(_via_count(layout, n) for n in nets)
|
||||||
|
measured = float(count)
|
||||||
|
verdict = geometric_verdict(cons, measured)
|
||||||
|
return _pack(
|
||||||
|
check, verdict, mand, nets=nets, measured_mm=measured,
|
||||||
|
limit_mm=cons.value if cons and cons.value_kind == "NUMERIC" else None,
|
||||||
|
value_kind=kind,
|
||||||
|
notes=f"Via count {count} (geometric). No invented via ampacity.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if check in {"stubs", "stub_length"}:
|
||||||
|
verdict = geometric_verdict(cons, None)
|
||||||
|
if verdict == "FAIL":
|
||||||
|
verdict = "UNKNOWN"
|
||||||
|
return _pack(
|
||||||
|
check, verdict if verdict != "PASS" else "UNKNOWN", mand,
|
||||||
|
nets=_instance_nets(inst), value_kind=kind,
|
||||||
|
notes="Stub length not certified without a NUMERIC/DESIGN millimetre limit. Not invented.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if check in {"intra_pair_skew", "pair_skew"}:
|
||||||
|
return _pair_skew(inst, layout, cons, mand, check, kind)
|
||||||
|
|
||||||
|
if check == "dq_to_dqs_skew":
|
||||||
|
return _dq_dqs_skew(inst, lanes, layout, cons, mand, kind)
|
||||||
|
|
||||||
|
if check == "byte_lane_skew":
|
||||||
|
return _lane_spread(lanes, layout, cons, mand, kind, "byte_lane_skew")
|
||||||
|
|
||||||
|
if check in {"ck_to_command_skew", "clock_to_data_skew", "lane_relationships"}:
|
||||||
|
measured = _group_spread(inst, layout)
|
||||||
|
verdict = geometric_verdict(cons, measured)
|
||||||
|
return _pack(
|
||||||
|
check, verdict, mand, nets=_instance_nets(inst),
|
||||||
|
measured_mm=measured, value_kind=kind,
|
||||||
|
notes="Geometric length spread. No invented ps. Not electrical.",
|
||||||
|
)
|
||||||
|
return _pack(check, "UNKNOWN", mand, value_kind=kind, notes="Not an L1 geometric check.")
|
||||||
|
|
||||||
|
|
||||||
|
def _clock_or_data_nets(inst: PhysicalBusInstance, check: str) -> list[str]:
|
||||||
|
if check == "clock_length":
|
||||||
|
return [n for g in inst.groups if g.kind == "CLOCK_GROUP" for n in g.nets]
|
||||||
|
if check == "data_length":
|
||||||
|
return [
|
||||||
|
n for g in inst.groups
|
||||||
|
if g.kind in {"BYTE_LANE", "DATA_LANE", "DIFFERENTIAL_PAIR"}
|
||||||
|
for n in g.nets
|
||||||
|
]
|
||||||
|
return _instance_nets(inst)
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_skew(
|
||||||
|
inst: PhysicalBusInstance,
|
||||||
|
layout: LayoutGraph,
|
||||||
|
cons: ProtocolConstraint | None,
|
||||||
|
mand: MandatoryClass,
|
||||||
|
check: str,
|
||||||
|
kind: str,
|
||||||
|
) -> L1CheckResult:
|
||||||
|
pairs = [g for g in inst.groups if g.kind == "DIFFERENTIAL_PAIR"]
|
||||||
|
if not pairs:
|
||||||
|
return _pack(
|
||||||
|
check, "UNKNOWN", mand, value_kind=kind,
|
||||||
|
notes="No differential pair group for geometric skew.",
|
||||||
|
)
|
||||||
|
deltas: list[float] = []
|
||||||
|
nets: list[str] = []
|
||||||
|
for g in pairs:
|
||||||
|
if len(g.nets) < 2:
|
||||||
|
continue
|
||||||
|
a, b = g.nets[0], g.nets[1]
|
||||||
|
deltas.append(abs(_len(layout, a) - _len(layout, b)))
|
||||||
|
nets.extend([a, b])
|
||||||
|
measured = max(deltas) if deltas else None
|
||||||
|
verdict = geometric_verdict(cons, measured)
|
||||||
|
limit = cons.value if cons and cons.value_kind == "NUMERIC" and (cons.unit or "mm").lower() not in _TIME_UNITS else None
|
||||||
|
return _pack(
|
||||||
|
check, verdict, mand, nets=nets, measured_mm=measured,
|
||||||
|
limit_mm=limit, value_kind=kind,
|
||||||
|
notes="Intra-pair geometric length delta (mm). Not ps, not electrical Z.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dq_dqs_skew(
|
||||||
|
inst: PhysicalBusInstance,
|
||||||
|
lanes: list[SignalGroup],
|
||||||
|
layout: LayoutGraph,
|
||||||
|
cons: ProtocolConstraint | None,
|
||||||
|
mand: MandatoryClass,
|
||||||
|
kind: str,
|
||||||
|
) -> L1CheckResult:
|
||||||
|
deltas: list[float] = []
|
||||||
|
nets: list[str] = []
|
||||||
|
for lane in lanes:
|
||||||
|
dqs = [n for k, n in lane.roles.items() if k.upper().startswith("DQS")]
|
||||||
|
dqs_len = [_len(layout, n) for n in dqs]
|
||||||
|
ref = sum(dqs_len) / len(dqs_len) if dqs_len else None
|
||||||
|
if ref is None:
|
||||||
|
continue
|
||||||
|
for key, net in lane.roles.items():
|
||||||
|
if key.upper().startswith("DQ") and not key.upper().startswith("DQS"):
|
||||||
|
deltas.append(abs(_len(layout, net) - ref))
|
||||||
|
nets.append(net)
|
||||||
|
nets.extend(dqs)
|
||||||
|
measured = max(deltas) if deltas else None
|
||||||
|
if measured is None:
|
||||||
|
return _skip_grouping("dq_to_dqs_skew", mand, inst)
|
||||||
|
verdict = geometric_verdict(cons, measured)
|
||||||
|
limit = cons.value if cons and cons.value_kind == "NUMERIC" and (cons.unit or "mm").lower() not in _TIME_UNITS else None
|
||||||
|
return _pack(
|
||||||
|
"dq_to_dqs_skew", verdict, mand, nets=nets, measured_mm=measured,
|
||||||
|
limit_mm=limit, value_kind=kind,
|
||||||
|
notes="DQ-to-DQS geometric length delta (mm). No invented ps.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _lane_spread(
|
||||||
|
lanes: list[SignalGroup],
|
||||||
|
layout: LayoutGraph,
|
||||||
|
cons: ProtocolConstraint | None,
|
||||||
|
mand: MandatoryClass,
|
||||||
|
kind: str,
|
||||||
|
check: str,
|
||||||
|
) -> L1CheckResult:
|
||||||
|
spreads: list[float] = []
|
||||||
|
nets: list[str] = []
|
||||||
|
for lane in lanes:
|
||||||
|
lens = [_len(layout, n) for n in lane.nets]
|
||||||
|
if not lens:
|
||||||
|
continue
|
||||||
|
spreads.append(max(lens) - min(lens))
|
||||||
|
nets.extend(lane.nets)
|
||||||
|
measured = max(spreads) if spreads else None
|
||||||
|
verdict = geometric_verdict(cons, measured)
|
||||||
|
limit = cons.value if cons and cons.value_kind == "NUMERIC" and (cons.unit or "mm").lower() not in _TIME_UNITS else None
|
||||||
|
return _pack(
|
||||||
|
check, verdict, mand, nets=nets, measured_mm=measured,
|
||||||
|
limit_mm=limit, value_kind=kind,
|
||||||
|
notes="Byte-lane geometric length spread (mm). Not a global length-match PASS.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _group_spread(inst: PhysicalBusInstance, layout: LayoutGraph) -> float | None:
|
||||||
|
nets = _instance_nets(inst)
|
||||||
|
lens = [_len(layout, n) for n in nets if _len(layout, n) > 0]
|
||||||
|
if len(lens) < 2:
|
||||||
|
return None
|
||||||
|
return max(lens) - min(lens)
|
||||||
|
|
||||||
|
|
||||||
|
def l1_findings(inst: PhysicalBusInstance, results: list[L1CheckResult]) -> list[Finding]:
|
||||||
|
out: list[Finding] = []
|
||||||
|
designator = inst.host_ref or inst.physical_interface_id
|
||||||
|
for r in results:
|
||||||
|
if r.result == "PASS":
|
||||||
|
continue
|
||||||
|
if r.result == "FAIL" and r.mandatory != "MANDATORY":
|
||||||
|
continue
|
||||||
|
skip_family = r.result in {
|
||||||
|
"UNKNOWN", "VENDOR_DEPENDENT", "CONTROLLER_DEPENDENT",
|
||||||
|
"PHY_DEPENDENT", "MISSING_SOURCE",
|
||||||
|
}
|
||||||
|
fail = r.result == "FAIL" and r.mandatory == "MANDATORY"
|
||||||
|
na = r.result == "NOT_APPLICABLE"
|
||||||
|
if not skip_family and not fail and not na:
|
||||||
|
continue
|
||||||
|
if fail:
|
||||||
|
rule_id = "PE-PRT-L1-003"
|
||||||
|
status = "ERROR"
|
||||||
|
finding = f"L1 geometric {r.check}: {r.notes}"
|
||||||
|
elif skip_family:
|
||||||
|
grouping = "grouping" in (r.notes or "").lower() or "dq/dqs" in (r.notes or "").lower()
|
||||||
|
rule_id = "PE-PRT-L1-002" if grouping else "PE-PRT-L1-001"
|
||||||
|
status = "WARNING"
|
||||||
|
finding = r.notes or (
|
||||||
|
f"Interfaccia {inst.physical_interface_id} non certificata a L1 "
|
||||||
|
f"per {r.check} ({r.result}). Not electrical."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rule_id = "PE-PRT-L0-002"
|
||||||
|
status = "INFO"
|
||||||
|
finding = r.notes
|
||||||
|
f = Finding(
|
||||||
|
designator=designator,
|
||||||
|
mpn="",
|
||||||
|
aspect="protocol_l1",
|
||||||
|
finding=finding,
|
||||||
|
why=r.notes,
|
||||||
|
status=status,
|
||||||
|
source=SOURCE,
|
||||||
|
net=r.nets[0] if r.nets else None,
|
||||||
|
rule_id=rule_id,
|
||||||
|
facts=finding,
|
||||||
|
requirement="L1 geometric vs NUMERIC/DESIGN length only; never invent mm/ps.",
|
||||||
|
inference="L1 is geometric, not electrical certification.",
|
||||||
|
provenance="MANDATORY" if fail else "TYPICAL",
|
||||||
|
finding_class="RULE" if fail else ("REVIEW" if skip_family else "INFO"),
|
||||||
|
evidence_status="INSUFFICIENT" if skip_family else "SUFFICIENT",
|
||||||
|
)
|
||||||
|
out.append(complete_finding(f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def certify_l1(
|
||||||
|
graph: DesignGraph,
|
||||||
|
instances: list[PhysicalBusInstance],
|
||||||
|
layout: LayoutGraph | None,
|
||||||
|
catalog: ProtocolCatalog | None = None,
|
||||||
|
) -> tuple[dict[str, list[L1CheckResult]], list[Finding]]:
|
||||||
|
cat = catalog or load_catalog()
|
||||||
|
ifaces = {p.id: p for p in cat.physical_interfaces}
|
||||||
|
by_id: dict[str, list[L1CheckResult]] = {}
|
||||||
|
findings: list[Finding] = []
|
||||||
|
for inst in instances:
|
||||||
|
iface = ifaces.get(inst.physical_interface_id)
|
||||||
|
if iface is None:
|
||||||
|
continue
|
||||||
|
rows = certify_instance_l1(graph, inst, iface, layout)
|
||||||
|
by_id[inst.instance_id] = rows
|
||||||
|
findings.extend(l1_findings(inst, rows))
|
||||||
|
return by_id, findings
|
||||||
@@ -376,7 +376,7 @@ async def run_pcb_pipeline(
|
|||||||
return
|
return
|
||||||
|
|
||||||
_step(project_id, "write_report", "running")
|
_step(project_id, "write_report", "running")
|
||||||
proto_section, proto_findings = protocol_exam(graph)
|
proto_section, proto_findings = protocol_exam(graph, layout=layout)
|
||||||
findings.extend(proto_findings)
|
findings.extend(proto_findings)
|
||||||
assign_pcb_finding_ids(findings)
|
assign_pcb_finding_ids(findings)
|
||||||
annotate_findings_cad(findings, cad_index_from_graph(graph))
|
annotate_findings_cad(findings, cad_index_from_graph(graph))
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.67.0 — 2026-09-22 — Protocol certification M3 (L1 geometric)
|
||||||
|
|
||||||
|
L1 GEOMETRIC certifier: length, topology, vias, stubs, layer, grouping. Geometric skew only vs **NUMERIC** millimetre or an explicit **DESIGN LIMIT**. Otherwise UNKNOWN / VENDOR_DEPENDENT / CONTROLLER_DEPENDENT / PHY_DEPENDENT — never invent mm/ps. L1 is **not** electrical. No L2/L3. RECOMMENDED never FAIL. Internal interfaces stay NOT_APPLICABLE. Missing DQ/DQS grouping is a visible skip, not PASS.
|
||||||
|
|
||||||
|
- [New] `protocol_l1.py`; `PE-PRT-L1-001` / `PE-PRT-L1-002` / `PE-PRT-L1-003`.
|
||||||
|
- [New] pytest `tests/pcb/test_protocol_l1_m3.py`.
|
||||||
|
|
||||||
## 2.66.0 — 2026-09-22 — Protocol certification M2 (L0 structural)
|
## 2.66.0 — 2026-09-22 — Protocol certification M2 (L0 structural)
|
||||||
|
|
||||||
L0 STRUCTURAL certifier: nets must exist and connect at least two components. FAIL only for missing **MANDATORY** structure. AXI4 / internal interconnects are **NOT_APPLICABLE** (not FAIL). RECOMMENDED never FAIL. No L1–L3, no invented ohms/mm/ps.
|
L0 STRUCTURAL certifier: nets must exist and connect at least two components. FAIL only for missing **MANDATORY** structure. AXI4 / internal interconnects are **NOT_APPLICABLE** (not FAIL). RECOMMENDED never FAIL. No L1–L3, no invented ohms/mm/ps.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "periscope-web",
|
"name": "periscope-web",
|
||||||
"version": "2.66.0",
|
"version": "2.67.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sync-version": "node scripts/sync-version.mjs",
|
"sync-version": "node scripts/sync-version.mjs",
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function ProtocolSection({
|
|||||||
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
|
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
|
||||||
<h2 className="text-sm font-semibold">Protocolli</h2>
|
<h2 className="text-sm font-semibold">Protocolli</h2>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Logical protocol vs physical interface. L0 structural only —
|
Logical protocol vs physical interface. L0 structural, L1 geometric
|
||||||
presence and connection, no invented impedance.
|
(length, vias, layer, grouping). Not electrical; no invented ohms/mm/ps.
|
||||||
</p>
|
</p>
|
||||||
{instances.length === 0 ? (
|
{instances.length === 0 ? (
|
||||||
<p className="text-sm">{message}</p>
|
<p className="text-sm">{message}</p>
|
||||||
@@ -51,6 +51,14 @@ export function ProtocolSection({
|
|||||||
.join("; ")}
|
.join("; ")}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
{Array.isArray(row.l1_checks) && row.l1_checks.length > 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
L1{" "}
|
||||||
|
{(row.l1_checks as Record<string, unknown>[])
|
||||||
|
.map((c) => `${String(c.check)}=${String(c.result)}`)
|
||||||
|
.join("; ")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function isLayoutFinding(f: {
|
|||||||
f.source === "af_ai_hf" ||
|
f.source === "af_ai_hf" ||
|
||||||
f.source === "af_trace_check" ||
|
f.source === "af_trace_check" ||
|
||||||
f.source === "protocol_l0" ||
|
f.source === "protocol_l0" ||
|
||||||
|
f.source === "protocol_l1" ||
|
||||||
rid.startsWith("PE-PLC") ||
|
rid.startsWith("PE-PLC") ||
|
||||||
rid.startsWith("PE-LAY") ||
|
rid.startsWith("PE-LAY") ||
|
||||||
rid.startsWith("PE-SI") ||
|
rid.startsWith("PE-SI") ||
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from backend.periscopex.models import (
|
|||||||
PinConnection,
|
PinConnection,
|
||||||
)
|
)
|
||||||
from backend.periscopex.protocol_l0 import (
|
from backend.periscopex.protocol_l0 import (
|
||||||
PACK_MACROPHASE,
|
|
||||||
_pack,
|
_pack,
|
||||||
certify_l0,
|
certify_l0,
|
||||||
protocol_exam,
|
protocol_exam,
|
||||||
@@ -130,8 +129,9 @@ def test_axi_internal_l0_not_applicable_not_fail():
|
|||||||
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
||||||
assert all(r.result != "FAIL" for r in rows)
|
assert all(r.result != "FAIL" for r in rows)
|
||||||
sec, findings = protocol_exam(_axi_graph())
|
sec, findings = protocol_exam(_axi_graph())
|
||||||
assert sec.max_level_reached == "L0"
|
assert sec.max_level_reached == "L1"
|
||||||
assert sec.macrophase == PACK_MACROPHASE
|
from backend.periscopex.protocol_l1 import PACK_MACROPHASE as L1_MACRO
|
||||||
|
assert sec.macrophase == L1_MACRO
|
||||||
assert all(f.status != "ERROR" for f in findings)
|
assert all(f.status != "ERROR" for f in findings)
|
||||||
assert all("AXI_AWVALID" not in (f.net or "") for f in findings if f.rule_id == "PE-PRT-L0-001")
|
assert all("AXI_AWVALID" not in (f.net or "") for f in findings if f.rule_id == "PE-PRT-L0-001")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
"""M3 L1 GEOMETRIC certifier: mm vs NUMERIC/DESIGN only; no invented ps/ohm."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from backend.periscopex.models import (
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
DesignGraph,
|
||||||
|
LayoutGraph,
|
||||||
|
LayoutSegment,
|
||||||
|
LayoutVia,
|
||||||
|
Net,
|
||||||
|
NetType,
|
||||||
|
PinConnection,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_catalog import (
|
||||||
|
ConstraintSource,
|
||||||
|
PhysicalInterface,
|
||||||
|
ProtocolConstraint,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_l0 import protocol_exam
|
||||||
|
from backend.periscopex.protocol_l1 import (
|
||||||
|
_pack,
|
||||||
|
certify_instance_l1,
|
||||||
|
certify_l1,
|
||||||
|
geometric_verdict,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_recognize import 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 _usb_layout(*, length_p: float = 10.0, length_m: float = 10.1) -> LayoutGraph:
|
||||||
|
return LayoutGraph(
|
||||||
|
segments=[
|
||||||
|
LayoutSegment(start=(0, 0), end=(length_p, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||||
|
LayoutSegment(start=(0, 0.2), end=(length_m, 0.2), width=0.2, layer="F.Cu", net="USB_D-"),
|
||||||
|
],
|
||||||
|
vias=[LayoutVia(x=1.0, y=0.0, net="USB_D+", drill=0.3, layers=("F.Cu", "B.Cu"))],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 _ddr_with_lanes() -> DesignGraph:
|
||||||
|
pins = {str(i): f"DDR4_DQ{i}" for i in range(8)}
|
||||||
|
pins.update({"8": "DDR4_DQS0_P", "9": "DDR4_DQS0_N"})
|
||||||
|
u_mem = _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")
|
||||||
|
u_cpu = _ic("U4", {str(i): pins[str(i)] for i in pins}, mpn="STM32", value="MCU")
|
||||||
|
nets = {
|
||||||
|
n: _net(n, ("U5", p), ("U4", p)) for p, n in pins.items()
|
||||||
|
}
|
||||||
|
return DesignGraph(components={"U5": u_mem, "U4": u_cpu}, nets=nets)
|
||||||
|
|
||||||
|
|
||||||
|
def _ddr_no_grouping() -> DesignGraph:
|
||||||
|
pins = {str(i): f"MEM_DAT{i}" for i in range(8)}
|
||||||
|
return DesignGraph(
|
||||||
|
components={"U5": _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")},
|
||||||
|
nets={n: _net(n, ("U5", p)) for p, n in pins.items()},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cite() -> ConstraintSource:
|
||||||
|
return ConstraintSource(document="project-netclass.txt", organization="design")
|
||||||
|
|
||||||
|
|
||||||
|
def _design_mm(parameter: str, value: float) -> ProtocolConstraint:
|
||||||
|
return ProtocolConstraint(
|
||||||
|
id=f"design-{parameter}",
|
||||||
|
parameter=parameter,
|
||||||
|
value_kind="NUMERIC",
|
||||||
|
mandatory="MANDATORY",
|
||||||
|
source_type="PCB",
|
||||||
|
source_class="IMPLEMENTATION",
|
||||||
|
value=value,
|
||||||
|
unit="mm",
|
||||||
|
source=_cite(),
|
||||||
|
limit_kind="DESIGN",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _l1_for(graph: DesignGraph, logical_sub: str, layout: LayoutGraph | None):
|
||||||
|
insts = recognize_physical_buses(graph)
|
||||||
|
by, findings = certify_l1(graph, insts, layout)
|
||||||
|
rows = []
|
||||||
|
for inst in insts:
|
||||||
|
if logical_sub in inst.logical_protocol_id:
|
||||||
|
rows.extend(by.get(inst.instance_id, []))
|
||||||
|
return rows, findings, insts
|
||||||
|
|
||||||
|
|
||||||
|
def test_usb_skew_without_numeric_is_unknown_not_invented_ps():
|
||||||
|
rows, _, _ = _l1_for(_usb_connected(), "usb2", _usb_layout())
|
||||||
|
skew = [r for r in rows if r.check == "intra_pair_skew"]
|
||||||
|
assert skew
|
||||||
|
assert skew[0].result in {"UNKNOWN", "MISSING_SOURCE", "VENDOR_DEPENDENT"}
|
||||||
|
assert skew[0].result != "PASS"
|
||||||
|
blob = json.dumps(skew[0].model_dump())
|
||||||
|
assert "ohm" not in blob.lower()
|
||||||
|
assert "90" not in blob
|
||||||
|
assert "ps" not in blob.lower() or skew[0].result != "PASS"
|
||||||
|
|
||||||
|
|
||||||
|
def test_usb_length_unknown_without_numeric_limit():
|
||||||
|
rows, _, _ = _l1_for(_usb_connected(), "usb2", _usb_layout())
|
||||||
|
length = [r for r in rows if r.check == "length"]
|
||||||
|
assert length
|
||||||
|
assert length[0].measured_mm is not None
|
||||||
|
assert length[0].result in {"UNKNOWN", "MISSING_SOURCE"}
|
||||||
|
assert length[0].limit_mm is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_usb_no_layout_is_visible_skip_not_pass():
|
||||||
|
rows, findings, _ = _l1_for(_usb_connected(), "usb2", None)
|
||||||
|
assert rows
|
||||||
|
assert all(r.result != "PASS" or r.check == "never" for r in rows)
|
||||||
|
assert any(r.result == "UNKNOWN" for r in rows)
|
||||||
|
assert any(f.rule_id == "PE-PRT-L1-001" for f in findings)
|
||||||
|
assert any("non certificata a L1" in (f.finding or "") for f in findings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_axi_internal_l1_not_applicable():
|
||||||
|
rows, findings, _ = _l1_for(_axi_graph(), "axi4", LayoutGraph())
|
||||||
|
assert rows
|
||||||
|
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 == "L1"
|
||||||
|
assert sec.macrophase == "M3"
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def test_ddr_missing_grouping_visible_skip_not_pass():
|
||||||
|
rows, findings, insts = _l1_for(_ddr_no_grouping(), "ddr4", LayoutGraph())
|
||||||
|
assert insts
|
||||||
|
mapping = [r for r in rows if r.check in {"byte_lane_mapping", "dq_to_dqs_skew", "byte_lane_skew"}]
|
||||||
|
assert mapping
|
||||||
|
assert all(r.result != "PASS" for r in mapping)
|
||||||
|
assert any(r.result == "UNKNOWN" for r in mapping)
|
||||||
|
assert any(f.rule_id == "PE-PRT-L1-002" for f in findings)
|
||||||
|
assert any("grouping" in (f.finding or "").lower() for f in findings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ddr_reconstructed_lane_skew_stays_phy_dependent():
|
||||||
|
layout = LayoutGraph(segments=[
|
||||||
|
LayoutSegment(start=(0, 0), end=(20, 0), layer="F.Cu", net="DDR4_DQ0"),
|
||||||
|
LayoutSegment(start=(0, 0), end=(21, 0), layer="F.Cu", net="DDR4_DQS0_P"),
|
||||||
|
])
|
||||||
|
rows, _, insts = _l1_for(_ddr_with_lanes(), "ddr4", layout)
|
||||||
|
assert any(g.kind == "BYTE_LANE" for i in insts for g in i.groups)
|
||||||
|
mapping = [r for r in rows if r.check == "byte_lane_mapping"]
|
||||||
|
assert mapping and mapping[0].result == "PASS"
|
||||||
|
skew = [r for r in rows if r.check == "dq_to_dqs_skew"]
|
||||||
|
assert skew
|
||||||
|
assert skew[0].result in {"PHY_DEPENDENT", "CONTROLLER_DEPENDENT", "VENDOR_DEPENDENT", "UNKNOWN"}
|
||||||
|
assert skew[0].result != "PASS"
|
||||||
|
assert skew[0].limit_mm is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_numeric_mm_design_limit_pass_and_fail():
|
||||||
|
pass_v = geometric_verdict(_design_mm("length", 50.0), 10.0)
|
||||||
|
fail_v = geometric_verdict(_design_mm("length", 50.0), 80.0)
|
||||||
|
assert pass_v == "PASS"
|
||||||
|
assert fail_v == "FAIL"
|
||||||
|
graph = _usb_connected()
|
||||||
|
insts = recognize_physical_buses(graph)
|
||||||
|
usb = [i for i in insts if "usb" in i.logical_protocol_id][0]
|
||||||
|
iface = PhysicalInterface(
|
||||||
|
id="usb2-hs-dpair",
|
||||||
|
logical_protocol_id="usb2-hs",
|
||||||
|
bus_type="SERIAL",
|
||||||
|
physical_layer="SERIAL_DIFFERENTIAL",
|
||||||
|
pcb_relevant="YES",
|
||||||
|
required_checks=["length"],
|
||||||
|
constraints=[_design_mm("length", 50.0)],
|
||||||
|
)
|
||||||
|
short = certify_instance_l1(graph, usb, iface, _usb_layout(length_p=10, length_m=10))
|
||||||
|
assert short[0].result == "PASS"
|
||||||
|
assert short[0].measured_mm == 10.0
|
||||||
|
assert short[0].limit_mm == 50.0
|
||||||
|
long = certify_instance_l1(graph, usb, iface, _usb_layout(length_p=80, length_m=80))
|
||||||
|
assert long[0].result == "FAIL"
|
||||||
|
assert long[0].mandatory == "MANDATORY"
|
||||||
|
|
||||||
|
|
||||||
|
def test_time_unit_numeric_is_unknown_not_converted_ps():
|
||||||
|
cons = ProtocolConstraint(
|
||||||
|
id="skew-ps",
|
||||||
|
parameter="intra_pair_skew",
|
||||||
|
value_kind="NUMERIC",
|
||||||
|
mandatory="MANDATORY",
|
||||||
|
source_type="STANDARD",
|
||||||
|
source_class="NORMATIVE",
|
||||||
|
value=15.0,
|
||||||
|
unit="ps",
|
||||||
|
source=ConstraintSource(document="USB-IF", organization="USB-IF"),
|
||||||
|
limit_kind="STANDARD",
|
||||||
|
)
|
||||||
|
assert geometric_verdict(cons, 0.05) == "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
|
def test_recommended_never_fail_l1():
|
||||||
|
rec = _pack("length", "FAIL", "RECOMMENDED", notes="over")
|
||||||
|
assert rec.result != "FAIL"
|
||||||
|
assert rec.status != "ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
def test_l1_does_not_run_impedance():
|
||||||
|
rows, _, _ = _l1_for(_usb_connected(), "usb2", _usb_layout())
|
||||||
|
assert not any(r.check in {"differential_impedance", "impedance", "termination"} for r in rows)
|
||||||
|
blob = json.dumps([r.model_dump() for r in rows])
|
||||||
|
assert "90" not in blob
|
||||||
|
assert "electrical Z" in blob or "not electrical" in blob.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_protocol_exam_attaches_l1_checks():
|
||||||
|
sec, _ = protocol_exam(_usb_connected(), layout=_usb_layout())
|
||||||
|
assert sec.max_level_reached == "L1"
|
||||||
|
assert sec.recognized_instances
|
||||||
|
row = sec.recognized_instances[0]
|
||||||
|
assert row.get("l1_checks")
|
||||||
|
assert all(c.get("level") == "L1" for c in row["l1_checks"])
|
||||||
Reference in New Issue
Block a user