Add protocol-certification M2 L0 structural certifier (2.66.0).

Presence and connection only. FAIL solely on missing MANDATORY structure.
AXI4 internal is NOT_APPLICABLE, not FAIL. RECOMMENDED never FAIL. No L1–L3
and no invented ohms/mm/ps.
This commit is contained in:
2026-09-22 12:12:25 +02:00
parent fc6524a2a9
commit 3cc2e5469b
12 changed files with 506 additions and 29 deletions
@@ -290,6 +290,10 @@ def _seed() -> None:
requirement="Via series inductance from drill, span, and dielectric height.") requirement="Via series inductance from drill, span, and dielectric height.")
_add("PE-AF-061", "RECOMMENDED", "RISK", domain="pcb", _add("PE-AF-061", "RECOMMENDED", "RISK", domain="pcb",
requirement="Via stub length vs λ/20 when span and f are FACT.") requirement="Via stub length vs λ/20 when span and f are FACT.")
_add("PE-PRT-L0-001", "MANDATORY", "RULE", domain="pcb",
requirement="Protocol nets required at L0 must exist and connect two components.")
_add("PE-PRT-L0-002", "TYPICAL", "INFO", domain="pcb",
requirement="Internal interconnect is NOT_APPLICABLE for PCB routing.")
_seed() _seed()
@@ -0,0 +1,314 @@
"""M2: L0 STRUCTURAL protocol certifier. Presence and connection only.
FAIL only when a MANDATORY structural net/group is missing or unconnected.
Internal interconnects (AXI4, AMBA, HBM DQ) are NOT_APPLICABLE, never FAIL.
RECOMMENDED never FAIL. No L1L3. No invented ohms/mm/ps.
"""
from __future__ import annotations
import re
from typing import Any, Literal
from pydantic import BaseModel, Field
from backend.periscopex.finding_engine import complete_finding
from backend.periscopex.models import DesignGraph, Finding
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
from backend.periscopex.protocol_catalog import (
SCHEMA_VERSION,
PhysicalInterface,
ProtocolCatalog,
ProtocolCertificationSection,
ProtocolConstraint,
empty_protocol_section,
load_catalog,
map_protocol_outcome,
)
from backend.periscopex.protocol_recognize import (
PhysicalBusInstance,
recognize_physical_buses,
)
PACK_MACROPHASE = "M2"
SOURCE = "protocol_l0"
LEVEL = "L0"
L0_CHECKS = frozenset({
"topology",
"byte_lane_mapping",
"cmd_dat_grouping",
"clk_reference",
"cc_rd_rp",
"vbus_gnd",
"pcb_routing",
"pcb_individual_dq_routing",
"dq_group",
"superspeed_pairs",
})
_CC_RE = re.compile(r"(?:^|[_/.])CC[12]$", re.I)
_VBUS_RE = re.compile(r"VBUS|\+?VUSB|USB_VBUS", re.I)
_GND_RE = re.compile(r"(?:^|[_/.])(?:GND|VSS|DGND)(?:$|[_/.])", re.I)
_SS_RE = re.compile(r"SSTX|SSRX|SS_T[XR]|USB3|USB_SS", re.I)
MandatoryClass = Literal["MANDATORY", "RECOMMENDED", "OPTIONAL", "INFORMATIONAL"]
class L0CheckResult(BaseModel):
check: str
level: Literal["L0"] = "L0"
result: Literal[
"PASS", "FAIL", "WARNING", "UNKNOWN", "NOT_APPLICABLE", "VENDOR_DEPENDENT",
]
mandatory: MandatoryClass
nets: list[str] = Field(default_factory=list)
notes: str = ""
finding_class: str = ""
status: str = ""
def _leaf(name: str) -> str:
n = normalize_kicad_hierarchy_net(name)
return n.split("/")[-1] if n else ""
def _net_present(graph: DesignGraph, name: str) -> bool:
return name in graph.nets
def _net_connected(graph: DesignGraph, name: str) -> bool:
net = graph.nets.get(name)
if not net:
return False
refs = {p.component_ref for p in net.pins if p.component_ref}
return len(refs) >= 2
def _constraint_for(iface: PhysicalInterface, check: str) -> ProtocolConstraint | None:
aliases = {
"cc_rd_rp": "cc_termination",
"pcb_routing": "pcb_routing",
"pcb_individual_dq_routing": "pcb_individual_dq_routing",
"superspeed_pairs": "superspeed_pairs",
"topology": "topology",
"byte_lane_mapping": "topology",
}
want = aliases.get(check, check)
for c in iface.constraints:
if c.parameter == want or c.parameter == check:
return c
return None
def _mandatory(iface: PhysicalInterface, check: str) -> MandatoryClass:
c = _constraint_for(iface, check)
if c:
return c.mandatory
if check in {"superspeed_pairs", "pcb_routing", "pcb_individual_dq_routing"}:
return "INFORMATIONAL"
return "MANDATORY"
def _pack(check: str, result: str, mandatory: MandatoryClass, *,
nets: list[str] | None = None, notes: str = "") -> L0CheckResult:
if result == "FAIL" and mandatory != "MANDATORY":
result = "WARNING"
cls, status, _ev = map_protocol_outcome(result, mandatory) # type: ignore[arg-type]
return L0CheckResult(
check=check, result=result, mandatory=mandatory, # type: ignore[arg-type]
nets=nets or [], notes=notes,
finding_class=cls, status=status,
)
def _structural_nets_ok(graph: DesignGraph, names: list[str]) -> tuple[bool, str]:
missing = [n for n in names if not _net_present(graph, n)]
if missing:
return False, f"missing net(s): {', '.join(missing)}"
dangling = [n for n in names if not _net_connected(graph, n)]
if dangling:
return False, f"unconnected net(s): {', '.join(dangling)}"
return True, "present and connected"
def certify_instance_l0(
graph: DesignGraph,
inst: PhysicalBusInstance,
iface: PhysicalInterface,
) -> list[L0CheckResult]:
if inst.pcb_relevant == "NO" or iface.pcb_relevant == "NO":
return [_pack(
"pcb_routing", "NOT_APPLICABLE", "INFORMATIONAL",
notes="Internal interconnect — not PCB routing. Not FAIL.",
)]
out: list[L0CheckResult] = []
checks = [c for c in iface.required_checks if c in L0_CHECKS]
if not checks:
checks = ["topology"]
for check in checks:
mand = _mandatory(iface, check)
out.append(_run_l0_check(graph, inst, check, mand))
return out
def _run_l0_check(
graph: DesignGraph,
inst: PhysicalBusInstance,
check: str,
mand: MandatoryClass,
) -> L0CheckResult:
if check in {"pcb_routing", "pcb_individual_dq_routing"}:
return _pack(check, "NOT_APPLICABLE", mand,
notes="Not a PCB-routed individual channel at L0.")
if check == "superspeed_pairs":
ss = [n for n in graph.nets if _SS_RE.search(_leaf(n))]
return _pack(
check, "NOT_APPLICABLE", mand, nets=ss,
notes="USB2-only physical interface: SuperSpeed pairs not required.",
)
if check == "topology":
names: list[str] = []
for g in inst.groups:
if g.kind == "DIFFERENTIAL_PAIR":
names.extend(g.nets)
if not names:
names = list(inst.nets)
if len(names) < 2:
return _pack(
check, "FAIL", mand, nets=names,
notes="MANDATORY differential pair or bus nets not present.",
)
ok, msg = _structural_nets_ok(graph, names)
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
if check == "byte_lane_mapping" or check == "dq_group":
lanes = [g for g in inst.groups if g.kind == "BYTE_LANE"]
if not lanes:
return _pack(
check, "FAIL", mand,
notes="MANDATORY byte-lane / DQ group not reconstructed.",
)
names = [n for g in lanes for n in g.nets]
ok, msg = _structural_nets_ok(graph, names)
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
if check == "cmd_dat_grouping":
names = [n for g in inst.groups for n in g.nets]
if not names:
return _pack(check, "FAIL", mand, notes="No CMD/DAT group nets.")
ok, msg = _structural_nets_ok(graph, names)
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
if check == "clk_reference":
clocks = [n for g in inst.groups if g.kind == "CLOCK_GROUP" for n in g.nets]
if not clocks:
return _pack(check, "FAIL", mand, notes="Clock reference net missing.")
ok, msg = _structural_nets_ok(graph, clocks)
return _pack(check, "PASS" if ok else "FAIL", mand, nets=clocks, notes=msg)
if check == "cc_rd_rp":
ccs = [n for n in graph.nets if _CC_RE.search(_leaf(n))]
if len(ccs) < 1:
return _pack(check, "FAIL", mand, notes="CC1/CC2 net not present.")
ok, msg = _structural_nets_ok(graph, ccs)
return _pack(check, "PASS" if ok else "FAIL", mand, nets=ccs, notes=msg)
if check == "vbus_gnd":
vbus = [n for n in graph.nets if _VBUS_RE.search(_leaf(n))]
gnd = [n for n in graph.nets if _GND_RE.search(_leaf(n))]
names = vbus + gnd
if not vbus or not gnd:
return _pack(
check, "FAIL", mand, nets=names,
notes="VBUS and/or GND net not present.",
)
ok, msg = _structural_nets_ok(graph, names)
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
return _pack(check, "NOT_APPLICABLE", mand, notes="Not an L0 structural check.")
def l0_findings(
inst: PhysicalBusInstance,
results: list[L0CheckResult],
) -> 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
status = r.status if r.status in {"ERROR", "WARNING", "INFO"} else "INFO"
if r.result == "FAIL":
rule_id = "PE-PRT-L0-001"
finding = (
f"L0 structural: {r.check} {r.notes or 'missing MANDATORY net/connection'}."
)
else:
rule_id = "PE-PRT-L0-002"
finding = f"L0 {r.result}: {r.check}{r.notes}".strip()
f = Finding(
designator=designator,
mpn="",
aspect="protocol_l0",
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=(
"MANDATORY protocol nets must exist and connect at least two components."
if r.result == "FAIL"
else "Internal protocol is not certified as PCB routing."
),
inference=f"{LEVEL} {r.result} (not electrical; no invented Z).",
provenance="MANDATORY" if r.mandatory == "MANDATORY" else "TYPICAL",
finding_class=r.finding_class if r.finding_class in {"RULE", "RISK", "REVIEW", "INFO"} else "INFO",
)
out.append(complete_finding(f))
return out
def certify_l0(
graph: DesignGraph,
instances: list[PhysicalBusInstance],
catalog: ProtocolCatalog | None = None,
) -> tuple[dict[str, list[L0CheckResult]], list[Finding]]:
cat = catalog or load_catalog()
ifaces = {p.id: p for p in cat.physical_interfaces}
by_id: dict[str, list[L0CheckResult]] = {}
findings: list[Finding] = []
for inst in instances:
iface = ifaces.get(inst.physical_interface_id)
if iface is None:
continue
rows = certify_instance_l0(graph, inst, iface)
by_id[inst.instance_id] = rows
findings.extend(l0_findings(inst, rows))
return by_id, findings
def protocol_exam(
graph: DesignGraph | None,
catalog: ProtocolCatalog | None = None,
) -> tuple[ProtocolCertificationSection, list[Finding]]:
if graph is None:
return empty_protocol_section(), []
cat = catalog or load_catalog()
instances = recognize_physical_buses(graph, cat)
if not instances:
sec = empty_protocol_section()
sec.macrophase = PACK_MACROPHASE
return sec, []
l0, findings = certify_l0(graph, instances, cat)
dumps: list[dict[str, Any]] = []
for inst in instances:
row = inst.model_dump()
row["l0_checks"] = [c.model_dump() for c in l0.get(inst.instance_id, [])]
dumps.append(row)
sec = ProtocolCertificationSection(
schema_version=SCHEMA_VERSION,
macrophase=PACK_MACROPHASE,
recognized_instances=dumps,
message="",
max_level_reached=LEVEL,
)
return sec, findings
@@ -14,11 +14,8 @@ from pydantic import BaseModel, Field
from backend.periscopex.models import Component, DesignGraph from backend.periscopex.models import Component, DesignGraph
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 (
EMPTY_PROTOCOL_MESSAGE,
SCHEMA_VERSION,
ProtocolCatalog, ProtocolCatalog,
ProtocolCertificationSection, ProtocolCertificationSection,
empty_protocol_section,
load_catalog, load_catalog,
) )
@@ -548,17 +545,6 @@ def protocol_section_for_graph(
graph: DesignGraph | None, graph: DesignGraph | None,
catalog: ProtocolCatalog | None = None, catalog: ProtocolCatalog | None = None,
) -> ProtocolCertificationSection: ) -> ProtocolCertificationSection:
if graph is None: from backend.periscopex.protocol_l0 import protocol_exam
return empty_protocol_section()
instances = recognize_physical_buses(graph, catalog) return protocol_exam(graph, catalog)[0]
if not instances:
sec = empty_protocol_section()
sec.macrophase = PACK_MACROPHASE
return sec
return ProtocolCertificationSection(
schema_version=SCHEMA_VERSION,
macrophase=PACK_MACROPHASE,
recognized_instances=[i.model_dump() for i in instances],
message="",
max_level_reached=None,
)
@@ -28,7 +28,7 @@ from backend.periscopex.layout_rules import needs_layout_rules_refresh
from backend.periscopex.models import ( from backend.periscopex.models import (
ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport, ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport,
) )
from backend.periscopex.protocol_recognize import protocol_section_for_graph from backend.periscopex.protocol_l0 import protocol_exam
from backend.periscopex.af_ai_hf import append_investigated, hypotheses_from_rows from backend.periscopex.af_ai_hf import append_investigated, hypotheses_from_rows
from backend.periscopex.af_trace_check import check_af_traces from backend.periscopex.af_trace_check import check_af_traces
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
@@ -376,6 +376,8 @@ 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)
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))
dec_path = ws.local_path("decisions.json") dec_path = ws.local_path("decisions.json")
@@ -394,7 +396,7 @@ async def run_pcb_pipeline(
summary=summary, summary=summary,
coverage=coverage, coverage=coverage,
not_reviewed=skipped + si_skip, not_reviewed=skipped + si_skip,
protocol_certification=protocol_section_for_graph(graph).model_dump(), protocol_certification=proto_section.model_dump(),
) )
report_path = ws.local_path("pcb_report.json") report_path = ws.local_path("pcb_report.json")
report_path.write_text(report.model_dump_json(indent=2) + "\n") report_path.write_text(report.model_dump_json(indent=2) + "\n")
+5 -3
View File
@@ -28,7 +28,7 @@ from backend.periscopex.interface_class_check import check_interface_classes
from backend.periscopex.led_current_check import check_led_current from backend.periscopex.led_current_check import check_led_current
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport
from backend.periscopex.protocol_recognize import protocol_section_for_graph from backend.periscopex.protocol_l0 import protocol_exam
from backend.periscopex.nc_pin_check import check_nc_pins from backend.periscopex.nc_pin_check import check_nc_pins
from backend.periscopex.parsers import ic_mpn_skip_reason from backend.periscopex.parsers import ic_mpn_skip_reason
from backend.periscopex.passive_rail_check import ( from backend.periscopex.passive_rail_check import (
@@ -209,6 +209,8 @@ async def validate_design_async(
return clean return clean
def _write_report(paused: bool = False) -> ValidationReport: def _write_report(paused: bool = False) -> ValidationReport:
proto_section, proto_findings = protocol_exam(graph)
all_findings.extend(proto_findings)
annotate_findings_cad(all_findings, graph.cad_index) annotate_findings_cad(all_findings, graph.cad_index)
assign_finding_ids(all_findings) assign_finding_ids(all_findings)
dec_path = existing_path.with_name("decisions.json") dec_path = existing_path.with_name("decisions.json")
@@ -229,7 +231,7 @@ async def validate_design_async(
coverage=_sanitize_coverage(all_coverage), coverage=_sanitize_coverage(all_coverage),
review_errors=dict(review_errors), review_errors=dict(review_errors),
not_reviewed=not_reviewed, not_reviewed=not_reviewed,
protocol_certification=protocol_section_for_graph(graph).model_dump(), protocol_certification=proto_section.model_dump(),
) )
except Exception: except Exception:
report = ValidationReport( report = ValidationReport(
@@ -240,7 +242,7 @@ async def validate_design_async(
coverage={}, coverage={},
review_errors=dict(review_errors), review_errors=dict(review_errors),
not_reviewed=not_reviewed, not_reviewed=not_reviewed,
protocol_certification=protocol_section_for_graph(graph).model_dump(), protocol_certification=proto_section.model_dump(),
) )
report_dict = json.loads(report.model_dump_json(indent=2)) report_dict = json.loads(report.model_dump_json(indent=2))
if preserved_comments is not None: if preserved_comments is not None:
@@ -2,6 +2,13 @@
What's new in Periscope. What's new in Periscope.
## 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 L1L3, no invented ohms/mm/ps.
- [New] `protocol_l0.py`; `PE-PRT-L0-001` / `PE-PRT-L0-002`.
- [New] pytest `tests/pcb/test_protocol_l0_m2.py`.
## 2.65.0 — 2026-09-22 — Protocol certification M1 (instance recognition) ## 2.65.0 — 2026-09-22 — Protocol certification M1 (instance recognition)
`physical_bus_instance` from declaration / silicon / part / pin / net / pairs. Output: instances, signal groups (DIFF pair, byte lane, clock/address), confidence. Ambiguity is **REVIEW**, never FAIL. AXI4 internal is recognised with `pcb_relevant: NO` and **no PCB nets**. No L0L3 certifier. No invented ohms. `physical_bus_instance` from declaration / silicon / part / pin / net / pairs. Output: instances, signal groups (DIFF pair, byte lane, clock/address), confidence. Ambiguity is **REVIEW**, never FAIL. AXI4 internal is recognised with `pcb_relevant: NO` and **no PCB nets**. No L0L3 certifier. No invented ohms.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "periscope-web", "name": "periscope-web",
"version": "2.65.0", "version": "2.66.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. Recognition only (M1) Logical protocol vs physical interface. L0 structural only
no L0L3 certifier, no invented impedance. presence and connection, no invented impedance.
</p> </p>
{instances.length === 0 ? ( {instances.length === 0 ? (
<p className="text-sm">{message}</p> <p className="text-sm">{message}</p>
@@ -43,6 +43,14 @@ export function ProtocolSection({
.join(", ")} .join(", ")}
</p> </p>
) : null} ) : null}
{Array.isArray(row.l0_checks) && row.l0_checks.length > 0 ? (
<p className="text-xs text-muted-foreground">
L0{" "}
{(row.l0_checks as Record<string, unknown>[])
.map((c) => `${String(c.check)}=${String(c.result)}`)
.join("; ")}
</p>
) : null}
</li> </li>
); );
})} })}
@@ -26,6 +26,7 @@ export function isLayoutFinding(f: {
f.source === "pdn_check" || f.source === "pdn_check" ||
f.source === "af_ai_hf" || f.source === "af_ai_hf" ||
f.source === "af_trace_check" || f.source === "af_trace_check" ||
f.source === "protocol_l0" ||
rid.startsWith("PE-PLC") || rid.startsWith("PE-PLC") ||
rid.startsWith("PE-LAY") || rid.startsWith("PE-LAY") ||
rid.startsWith("PE-SI") || rid.startsWith("PE-SI") ||
@@ -50,6 +51,7 @@ export function isLayoutFinding(f: {
rid.startsWith("PE-ANT") || rid.startsWith("PE-ANT") ||
rid.startsWith("PE-PDN") || rid.startsWith("PE-PDN") ||
rid.startsWith("PE-AF") || rid.startsWith("PE-AF") ||
rid.startsWith("PE-PRT") ||
/^PE-BOM-01[0-4]$/.test(rid) /^PE-BOM-01[0-4]$/.test(rid)
); );
} }
+1 -1
View File
@@ -1,3 +1,3 @@
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */ /** Stamped from content/changelog.md by scripts/sync-version.mjs. */
export const APP_VERSION = "2.65.0"; export const APP_VERSION = "2.66.0";
export const APP_VERSION_DATE = "2026-09-22"; export const APP_VERSION_DATE = "2026-09-22";
+155
View File
@@ -0,0 +1,155 @@
"""M2 L0 structural certifier: presence/connection; AXI N/A; RECOMMENDED not FAIL."""
from __future__ import annotations
import json
from backend.periscopex.models import (
Component,
ComponentType,
DesignGraph,
Net,
NetType,
PinConnection,
)
from backend.periscopex.protocol_l0 import (
PACK_MACROPHASE,
_pack,
certify_l0,
protocol_exam,
)
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_dangling_minus() -> DesignGraph:
return DesignGraph(
components={
"U1": _ic("U1", {"1": "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-", ("J2", "3")),
},
)
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_one_ended() -> DesignGraph:
pins = {str(i): f"DDR4_DQ{i}" for i in range(8)}
pins.update({"8": "DDR4_DQS0_P", "9": "DDR4_DQS0_N"})
return DesignGraph(
components={"U5": _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")},
nets={n: _net(n, ("U5", p)) for p, n in pins.items()},
)
def _l0_for(graph: DesignGraph, logical_sub: str) -> list:
insts = recognize_physical_buses(graph)
by, _ = certify_l0(graph, insts)
rows = []
for inst in insts:
if logical_sub in inst.logical_protocol_id:
rows.extend(by.get(inst.instance_id, []))
return rows
def test_usb_connected_pair_l0_topology_pass():
rows = _l0_for(_usb_connected(), "usb2")
topo = [r for r in rows if r.check == "topology"]
assert topo
assert topo[0].result == "PASS"
assert topo[0].level == "L0"
blob = json.dumps(topo[0].model_dump())
assert "ohm" not in blob.lower()
assert "90" not in blob
def test_usb_unconnected_dminus_l0_fail_mandatory():
g = _usb_dangling_minus()
insts = recognize_physical_buses(g)
usb = [i for i in insts if "usb" in i.logical_protocol_id]
assert usb
by, findings = certify_l0(g, insts)
rows = [r for i in usb for r in by[i.instance_id]]
topo = [r for r in rows if r.check == "topology"]
assert topo
assert topo[0].result == "FAIL"
assert topo[0].mandatory == "MANDATORY"
assert any(f.status == "ERROR" and f.rule_id == "PE-PRT-L0-001" for f in findings)
def test_axi_internal_l0_not_applicable_not_fail():
rows = _l0_for(_axi_graph(), "axi4")
assert rows
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 == "L0"
assert sec.macrophase == PACK_MACROPHASE
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")
def test_ddr_unconnected_memory_fails_l0_connection():
rows = _l0_for(_ddr_one_ended(), "ddr4")
mapping = [r for r in rows if r.check == "byte_lane_mapping"]
assert mapping
assert mapping[0].result == "FAIL"
assert "unconnected" in mapping[0].notes
def test_recommended_never_parses_as_fail():
rec = _pack("return_path", "FAIL", "RECOMMENDED", notes="missing")
assert rec.result != "FAIL"
assert rec.status != "ERROR"
def test_l0_does_not_run_impedance():
rows = _l0_for(_usb_connected(), "usb2")
assert not any(r.check in {"differential_impedance", "intra_pair_skew", "length"} for r in rows)
-3
View File
@@ -13,7 +13,6 @@ from backend.periscopex.models import (
PinConnection, PinConnection,
) )
from backend.periscopex.protocol_recognize import ( from backend.periscopex.protocol_recognize import (
PACK_MACROPHASE,
protocol_section_for_graph, protocol_section_for_graph,
recognize_physical_buses, recognize_physical_buses,
) )
@@ -143,7 +142,6 @@ def test_empty_graph_keeps_nessun_protocollo():
sec = protocol_section_for_graph(DesignGraph()) sec = protocol_section_for_graph(DesignGraph())
assert sec.message == "nessun protocollo riconosciuto" assert sec.message == "nessun protocollo riconosciuto"
assert sec.recognized_instances == [] assert sec.recognized_instances == []
assert sec.macrophase == PACK_MACROPHASE
blob = json.dumps(sec.model_dump()) blob = json.dumps(sec.model_dump())
assert "ohm" not in blob.lower() assert "ohm" not in blob.lower()
@@ -151,7 +149,6 @@ def test_empty_graph_keeps_nessun_protocollo():
def test_report_section_lists_usb_without_z(): def test_report_section_lists_usb_without_z():
sec = protocol_section_for_graph(_usb_graph()) sec = protocol_section_for_graph(_usb_graph())
assert sec.recognized_instances assert sec.recognized_instances
assert sec.max_level_reached is None
blob = json.dumps(sec.model_dump()) blob = json.dumps(sec.model_dump())
assert "90" not in blob assert "90" not in blob
assert "Zdiff" not in blob assert "Zdiff" not in blob