Add /api/library datasheet import and component GET/PUT with no exam. Reorganize pytest into datasheet, library, schematic, PCB, and AF+AI. Document Rust criteria (none chosen; no rustup) and coding conformity.
399 lines
15 KiB
Python
399 lines
15 KiB
Python
"""Phase A: HF pair integrity + CPU/FPGA/DDR class — only if present.
|
|
|
|
Via ≠ pad ≠ track ≠ zone. No invented Z/I/mm. Skip when evidence is missing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from backend.periscopex.finding_engine import complete_finding, lookup_rule
|
|
from backend.periscopex.hf_bus_class import check_memory_fpga_classes
|
|
from backend.periscopex.hf_line_check import check_hf_lines
|
|
from backend.periscopex.models import (
|
|
Component,
|
|
ComponentConstraints,
|
|
ComponentType,
|
|
DesignGraph,
|
|
LayoutDielectric,
|
|
LayoutFootprint,
|
|
LayoutGraph,
|
|
LayoutPad,
|
|
LayoutSegment,
|
|
LayoutStackup,
|
|
LayoutVia,
|
|
LayoutZone,
|
|
Net,
|
|
NetType,
|
|
Pin,
|
|
PinConnection,
|
|
ResistorSpecs,
|
|
)
|
|
from backend.periscopex.pcb_checks import run_pcb_checks
|
|
from backend.periscopex.si_check import bus_class, check_si, skip_si_net
|
|
|
|
|
|
def _ic(ref: str, pins: dict[str, str], *, mpn: str = "PHY", subtype: str | None = None) -> Component:
|
|
return Component(
|
|
reference=ref, value=mpn, footprint="",
|
|
component_type=ComponentType.IC, mpn=mpn,
|
|
component_subtype=subtype, pins=pins,
|
|
)
|
|
|
|
|
|
def _net(name: str, *pairs: tuple[str, str], ntype: NetType = NetType.SIGNAL) -> Net:
|
|
return Net(
|
|
name=name, net_type=ntype,
|
|
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
|
|
)
|
|
|
|
|
|
def _usb_graph() -> DesignGraph:
|
|
return DesignGraph(
|
|
components={
|
|
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}),
|
|
"J2": Component(
|
|
reference="J2", value="USB_C", footprint="",
|
|
component_type=ComponentType.CONNECTOR, pins={"A6": "USB_D+", "A7": "USB_D-"},
|
|
),
|
|
},
|
|
nets={
|
|
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "A6")),
|
|
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "A7")),
|
|
},
|
|
)
|
|
|
|
|
|
def _fp_with_pads(ref: str, pads: list[LayoutPad], x: float = 0.0, y: float = 0.0) -> LayoutFootprint:
|
|
return LayoutFootprint(reference=ref, footprint="P", x=x, y=y, pads=pads)
|
|
|
|
|
|
def test_bus_class_mipi_hf_clk_not_xtal():
|
|
assert bus_class("MIPI_D0_P") == "mipi"
|
|
assert bus_class("CSI_CLK_N") == "mipi"
|
|
assert bus_class("DSI_D1_P") == "mipi"
|
|
assert bus_class("GTX_CLK") == "hf_clk"
|
|
assert bus_class("PCIE_REFCLK") == "pcie"
|
|
assert bus_class("CLK_P") == "hf_clk"
|
|
assert bus_class("XTAL_IN") is None
|
|
assert bus_class("OSCIN") is None
|
|
assert bus_class("HFXIN") is None
|
|
assert skip_si_net("USB_CC1")
|
|
assert bus_class("USB_D+") == "usb2"
|
|
|
|
|
|
def test_no_layout_skips_hf_geometry():
|
|
assert check_hf_lines(_usb_graph(), {}, None) == []
|
|
assert check_hf_lines(_usb_graph(), {}, LayoutGraph()) == []
|
|
|
|
|
|
def test_pad_to_pad_usb_has_no_stub():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [
|
|
LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+"),
|
|
LayoutPad(number="2", x=0.0, y=0.4, net="USB_D-"),
|
|
]),
|
|
"J2": _fp_with_pads("J2", [
|
|
LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+"),
|
|
LayoutPad(number="A7", x=10.0, y=0.4, net="USB_D-"),
|
|
], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(0, 0.4), end=(10, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
|
],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
|
|
|
|
|
def test_via_is_not_a_stub():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(5, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(5, 0), end=(10, 0), width=0.2, layer="B.Cu", net="USB_D+"),
|
|
],
|
|
vias=[LayoutVia(x=5.0, y=0.0, net="USB_D+", drill=0.3)],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
|
assert all(not isinstance(v, LayoutPad) for v in layout.vias)
|
|
|
|
|
|
def test_zone_is_not_a_track_stub():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(4, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
zones=[LayoutZone(
|
|
net="USB_D+", layer="F.Cu",
|
|
outlines=[[(3.5, -1.0), (8.0, -1.0), (8.0, 1.0), (3.5, 1.0)]],
|
|
)],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
|
|
|
|
|
def test_dangling_track_stub_vs_datasheet_mm_is_fail():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(10, 0), end=(10, 3), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
)
|
|
cons = ComponentConstraints(
|
|
mpn="PHY", pintable=[Pin(number="1", name="D+")],
|
|
absolute_maximum_ratings=[], rules=[],
|
|
layout_rules=[{
|
|
"kind": "stub", "net_class": "usb2", "max_distance_mm": 1.0,
|
|
"note": "USB stub < 1 mm", "source_page": 12,
|
|
}],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {"PHY": cons}, layout)
|
|
stubs = [f for f in findings if f.rule_id == "PE-SI-007"]
|
|
assert len(stubs) == 1
|
|
assert stubs[0].finding.startswith("FAIL:")
|
|
assert stubs[0].facts.startswith("stub_mm=")
|
|
assert "1" in stubs[0].requirement
|
|
complete_finding(stubs[0])
|
|
assert stubs[0].status == "WARNING"
|
|
assert stubs[0].finding_class != "RULE" or stubs[0].provenance != "MANDATORY"
|
|
assert stubs[0].evidence_status == "SUFFICIENT"
|
|
|
|
|
|
def test_measured_stub_without_datasheet_mm_is_insufficient():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(5, 0), end=(5, 4), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
stubs = [f for f in findings if f.rule_id == "PE-SI-007"]
|
|
assert len(stubs) == 1
|
|
assert stubs[0].status == "INFO"
|
|
assert stubs[0].evidence_status == "INSUFFICIENT"
|
|
assert "90" not in (stubs[0].requirement or "")
|
|
assert "FAIL" not in stubs[0].finding
|
|
|
|
|
|
def test_split_under_pair_needs_stackup_and_zone():
|
|
segs = [
|
|
LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
]
|
|
bare = LayoutGraph(segments=segs)
|
|
assert [f for f in check_hf_lines(_usb_graph(), {}, bare) if f.rule_id == "PE-SI-011"] == []
|
|
|
|
stack = LayoutStackup(
|
|
copper_layers=["F.Cu", "B.Cu"],
|
|
dielectrics=[LayoutDielectric(name="dielectric_1", er=4.5, height_mm=0.2)],
|
|
copper_thickness_mm=0.035,
|
|
)
|
|
no_zone = LayoutGraph(segments=segs, stackup=stack)
|
|
assert [f for f in check_hf_lines(_usb_graph(), {}, no_zone) if f.rule_id == "PE-SI-011"] == []
|
|
|
|
covered = LayoutGraph(
|
|
segments=segs, stackup=stack,
|
|
zones=[LayoutZone(
|
|
net="GND", layer="B.Cu",
|
|
outlines=[[(-1, -2), (21, -2), (21, 2), (-1, 2)]],
|
|
)],
|
|
)
|
|
assert [f for f in check_hf_lines(_usb_graph(), {}, covered) if f.rule_id == "PE-SI-011"] == []
|
|
|
|
split = LayoutGraph(
|
|
segments=segs, stackup=stack,
|
|
zones=[LayoutZone(
|
|
net="GND", layer="B.Cu",
|
|
outlines=[[(0, -2), (5, -2), (5, 2), (0, 2)]],
|
|
)],
|
|
)
|
|
hits = [f for f in check_hf_lines(_usb_graph(), {}, split) if f.rule_id == "PE-SI-011"]
|
|
assert len(hits) == 1
|
|
assert hits[0].status == "WARNING"
|
|
assert hits[0].finding_class == "REVIEW"
|
|
assert hits[0].facts
|
|
assert hits[0].requirement
|
|
assert hits[0].inference
|
|
complete_finding(hits[0])
|
|
assert hits[0].status != "ERROR"
|
|
|
|
|
|
def test_bom_termination_is_fact_missing_is_skip():
|
|
layout = LayoutGraph(
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
)
|
|
g = _usb_graph()
|
|
assert [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"] == []
|
|
|
|
g.components["R22"] = Component(
|
|
reference="R22", value="22R", footprint="",
|
|
component_type=ComponentType.RESISTOR,
|
|
pins={"1": "USB_D+", "2": "USB_D+_PHY"},
|
|
specs=ResistorSpecs(value_ohms=22.0, value_formatted="22"),
|
|
)
|
|
g.nets["USB_D+_PHY"] = _net("USB_D+_PHY", ("R22", "2"))
|
|
g.nets["USB_D+"].pins.append(PinConnection(component_ref="R22", pin_number="1"))
|
|
found = [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"]
|
|
assert len(found) == 1
|
|
assert found[0].status == "INFO"
|
|
assert found[0].evidence_status == "SUFFICIENT"
|
|
assert "R22" in found[0].facts
|
|
assert "22" in found[0].facts
|
|
assert "50 Ω" not in found[0].requirement
|
|
|
|
|
|
def test_no_ddr_cpu_fpga_on_usb_only_graph():
|
|
findings = check_memory_fpga_classes(_usb_graph())
|
|
assert findings == []
|
|
ids = {f.rule_id for f in run_pcb_checks(_usb_graph(), {}, None)}
|
|
assert not any(str(i).startswith(("PE-DDR", "PE-CPU", "PE-FPGA")) for i in ids)
|
|
|
|
|
|
def test_ddr_present_class_and_nets_skip_invented_vtt():
|
|
pins = {str(i + 1): f"DDR3_DQ{i}" for i in range(8)}
|
|
pins["20"] = "DDR3_DQS0_P"
|
|
pins["21"] = "DDR3_DQS0_N"
|
|
pins["22"] = "DDR3_CK_P"
|
|
pins["23"] = "DDR3_CK_N"
|
|
pins["24"] = "DDR3_A0"
|
|
comps = {"U10": _ic("U10", pins, mpn="MT41K256M16TW", subtype="ic.memory.ddr")}
|
|
nets = {n: _net(n, ("U10", p)) for p, n in pins.items()}
|
|
g = DesignGraph(components=comps, nets=nets)
|
|
findings = check_memory_fpga_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
assert [f.rule_id for f in findings if f.rule_id == "PE-DDR-001"]
|
|
assert [f for f in findings if f.rule_id == "PE-DDR-001"][0].status == "INFO"
|
|
assert [f for f in findings if f.rule_id == "PE-DDR-002"][0].status == "INFO"
|
|
assert not any(f.rule_id == "PE-DDR-003" for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-CPU") for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-FPGA") for f in findings)
|
|
|
|
g.nets["DDR_VTT"] = _net("DDR_VTT", ("U10", "30"), ntype=NetType.POWER)
|
|
g.components["U10"].pins["30"] = "DDR_VTT"
|
|
with_vtt = check_memory_fpga_classes(g)
|
|
assert [f for f in with_vtt if f.rule_id == "PE-DDR-003"][0].status == "INFO"
|
|
|
|
|
|
def test_cpu_parallel_bus_only_when_data_addr_control_exist():
|
|
mcu_pins = {str(i + 1): f"MCU_D{i}" for i in range(8)}
|
|
for i in range(8):
|
|
mcu_pins[str(20 + i)] = f"MCU_A{i}"
|
|
mcu_pins["40"] = "MCU_NWE"
|
|
mcu_pins["41"] = "MCU_NOE"
|
|
mcu_pins["42"] = "MCU_NCS"
|
|
sram_pins = dict(mcu_pins)
|
|
comps = {
|
|
"U1": _ic("U1", mcu_pins, mpn="STM32F407", subtype="ic.mcu"),
|
|
"U2": _ic("U2", sram_pins, mpn="IS61WV51216", subtype="ic.memory.sram"),
|
|
}
|
|
nets = {}
|
|
for p, n in mcu_pins.items():
|
|
nets[n] = _net(n, ("U1", p), ("U2", p))
|
|
g = DesignGraph(components=comps, nets=nets)
|
|
findings = check_memory_fpga_classes(g)
|
|
ids = {f.rule_id for f in findings}
|
|
assert "PE-CPU-001" in ids
|
|
assert "PE-CPU-002" in ids
|
|
assert "PE-CPU-003" in ids
|
|
assert "PE-CPU-004" in ids
|
|
assert not any(str(i).startswith("PE-DDR") for i in ids)
|
|
assert not any(str(i).startswith("PE-FPGA") for i in ids)
|
|
|
|
gpio = DesignGraph(
|
|
components={"U1": _ic("U1", {"1": "GPIO0", "2": "GPIO1"}, mpn="MSPM0", subtype="ic.mcu")},
|
|
nets={
|
|
"GPIO0": _net("GPIO0", ("U1", "1")),
|
|
"GPIO1": _net("GPIO1", ("U1", "2")),
|
|
},
|
|
)
|
|
assert check_memory_fpga_classes(gpio) == []
|
|
|
|
|
|
def test_fpga_only_when_present_config_flash_optional():
|
|
fpga_pins = {"1": "IO_L1P", "2": "IO_L1N", "3": "VCCINT", "4": "VCCIO"}
|
|
g = DesignGraph(
|
|
components={"U5": _ic("U5", fpga_pins, mpn="XC7A35T", subtype="ic.fpga")},
|
|
nets={
|
|
"IO_L1P": _net("IO_L1P", ("U5", "1")),
|
|
"IO_L1N": _net("IO_L1N", ("U5", "2")),
|
|
"VCCINT": _net("VCCINT", ("U5", "3"), ntype=NetType.POWER),
|
|
"VCCIO": _net("VCCIO", ("U5", "4"), ntype=NetType.POWER),
|
|
},
|
|
)
|
|
findings = check_memory_fpga_classes(g)
|
|
ids = {f.rule_id for f in findings}
|
|
assert "PE-FPGA-001" in ids
|
|
assert "PE-FPGA-003" in ids
|
|
assert "PE-FPGA-002" not in ids
|
|
assert not any(str(i).startswith("PE-DDR") for i in ids)
|
|
assert not any(str(i).startswith("PE-CPU") for i in ids)
|
|
|
|
g.components["U6"] = _ic("U6", {"1": "FPGA_CS", "2": "FPGA_MOSI"}, mpn="W25Q64", subtype="ic.memory.flash")
|
|
g.nets["FPGA_CS"] = _net("FPGA_CS", ("U6", "1"), ("U5", "10"))
|
|
g.components["U5"].pins["10"] = "FPGA_CS"
|
|
with_flash = check_memory_fpga_classes(g)
|
|
assert any(f.rule_id == "PE-FPGA-002" for f in with_flash)
|
|
|
|
|
|
def test_rule_catalog_hf_ids():
|
|
for rid, domain in (
|
|
("PE-SI-007", "pcb"),
|
|
("PE-SI-011", "pcb"),
|
|
("PE-SI-012", "pcb"),
|
|
("PE-DDR-001", "shared"),
|
|
("PE-DDR-002", "shared"),
|
|
("PE-DDR-003", "shared"),
|
|
("PE-CPU-001", "shared"),
|
|
("PE-CPU-002", "shared"),
|
|
("PE-CPU-003", "shared"),
|
|
("PE-CPU-004", "shared"),
|
|
("PE-FPGA-001", "shared"),
|
|
("PE-FPGA-002", "shared"),
|
|
("PE-FPGA-003", "shared"),
|
|
):
|
|
rec = lookup_rule(rid)
|
|
assert rec is not None, rid
|
|
assert rec.domain == domain
|
|
|
|
|
|
def test_existing_si_usb_not_polluted_by_hf_geometry():
|
|
layout = LayoutGraph(
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(0, 0.4), end=(12.5, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
|
],
|
|
vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)],
|
|
)
|
|
findings = check_si(_usb_graph(), {}, layout, [
|
|
{
|
|
"net_name": "USB_D+", "partner_net_name": "USB_D-",
|
|
"z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0,
|
|
"length_mm": 10.0, "topologies": ["MICROSTRIP"],
|
|
},
|
|
{
|
|
"net_name": "USB_D-", "partner_net_name": "USB_D+",
|
|
"z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0,
|
|
"length_mm": 12.5, "topologies": ["MICROSTRIP"],
|
|
},
|
|
])
|
|
assert any(f.rule_id == "PE-SI-010" for f in findings)
|
|
assert all(f.rule_id != "PE-SI-002" for f in findings)
|