"""Fase B slices: hierarchy, derating stress, timing, PI, ESD/return.""" from __future__ import annotations from backend.periscopex.esd_return_check import check_esd, check_return_path from backend.periscopex.finding_engine import complete_finding from backend.periscopex.hierarchy import build_hierarchy, check_hierarchy from backend.periscopex.models import ( CapacitorSpecs, Component, ComponentConstraints, ComponentType, DesignGraph, Finding, InternalFeatures, LayoutFootprint, LayoutGraph, LayoutSegment, LayoutVia, LayoutZone, Net, NetType, Pin, PinConnection, ResistorSpecs, SimpleComponentSpecs, ) from backend.periscopex.pcb_checks import check_pcb_derating from backend.periscopex.pi_check import check_power_integrity from backend.periscopex.timing_check import check_timing def test_hierarchy_component_pin_net_block(): graph = DesignGraph( components={ "U1": Component( reference="U1", value="LDO", footprint="", component_type=ComponentType.IC, mpn="LDO", pins={"1": "VIN", "2": "+3V3", "3": ""}, ), }, nets={ "VIN": Net( name="VIN", net_type=NetType.POWER, voltage=5.0, pins=[PinConnection(component_ref="U1", pin_number="1")], ), "+3V3": Net( name="+3V3", net_type=NetType.POWER, voltage=3.3, pins=[PinConnection(component_ref="U1", pin_number="2")], ), }, ) hier = build_hierarchy(graph) assert hier.components[0].ref == "U1" assert {p.number: p.net for p in hier.components[0].pins}["1"] == "VIN" assert any(b.kind == "rail" for b in hier.blocks) dangling = check_hierarchy(graph) assert dangling and dangling[0].rule_id == "PE-HIER-001" assert dangling[0].status == "INFO" assert dangling[0].facts def test_derating_pass_margin_risk(): def cap(ref, rated, net="3V3"): return Component( reference=ref, value="", footprint="", component_type=ComponentType.CAPACITOR, component_subtype="passive.capacitor.ceramic", mpn=ref, pins={"1": net, "2": "GND"}, specs=CapacitorSpecs( value_farads=1e-6, value_formatted="1uF", voltage_rating_v=f"{rated}V", dielectric="X7R", ), ) g = DesignGraph( components={ "C1": cap("C1", 16), "C2": cap("C2", 4.0), "C3": cap("C3", 3.0), }, nets={ "3V3": Net( name="3V3", net_type=NetType.POWER, voltage=3.3, pins=[ PinConnection(component_ref="C1", pin_number="1"), PinConnection(component_ref="C2", pin_number="1"), PinConnection(component_ref="C3", pin_number="1"), ], ), "GND": Net(name="GND", net_type=NetType.GROUND, voltage=0.0, pins=[]), }, ) from backend.periscopex.derating import build_derating_table by = {r["designator"]: r["stress"] for r in build_derating_table(g)} assert by["C1"] == "PASS" assert by["C2"] == "MARGIN" assert by["C3"] == "RISK" findings = check_pcb_derating(g) ids = {f.rule_id for f in findings} assert "PE-DRT-001" in ids assert "PE-DRT-002" in ids assert "PE-DRT-003" in ids exceed = next(f for f in findings if f.rule_id == "PE-DRT-001") complete_finding(exceed) assert exceed.status == "ERROR" assert exceed.finding_class == "RULE" margin = next(f for f in findings if f.rule_id == "PE-DRT-002") complete_finding(margin) assert margin.status == "WARNING" assert margin.finding_class == "RISK" def test_timing_skips_without_numbers(): graph = DesignGraph( components={ "U1": Component( reference="U1", value="", footprint="", component_type=ComponentType.IC, mpn="MCU", pins={"1": "NRST"}, ), "R1": Component( reference="R1", value="10k", footprint="", component_type=ComponentType.RESISTOR, mpn="", pins={"1": "NRST", "2": "+3V3"}, ), }, nets={"NRST": Net(name="NRST", net_type=NetType.SIGNAL, pins=[])}, ) cons = ComponentConstraints( mpn="MCU", pintable=[Pin(number="1", name="NRST")], absolute_maximum_ratings=[], rules=[], ) assert check_timing(graph, {"MCU": cons}) == [] def test_timing_reset_rc_vs_t_reset(): graph = DesignGraph( components={ "U1": Component( reference="U1", value="", footprint="", component_type=ComponentType.IC, mpn="MCU", pins={"1": "NRST"}, specs=SimpleComponentSpecs( specs_type="discrete", values={"t_reset_min_s": 0.01}, ), ), "R1": Component( reference="R1", value="1k", footprint="", component_type=ComponentType.RESISTOR, mpn="", pins={"1": "NRST", "2": "+3V3"}, specs=ResistorSpecs(value_ohms=1000, value_formatted="1k"), ), "C1": Component( reference="C1", value="100n", footprint="", component_type=ComponentType.CAPACITOR, mpn="", pins={"1": "NRST", "2": "GND"}, specs=CapacitorSpecs(value_farads=100e-9, value_formatted="100n"), ), }, nets={ "NRST": Net( name="NRST", net_type=NetType.SIGNAL, pins=[ PinConnection(component_ref="U1", pin_number="1"), PinConnection(component_ref="R1", pin_number="1"), PinConnection(component_ref="C1", pin_number="1"), ], ), "+3V3": Net(name="+3V3", net_type=NetType.POWER, voltage=3.3, pins=[]), "GND": Net(name="GND", net_type=NetType.GROUND, pins=[]), }, ) cons = ComponentConstraints( mpn="MCU", pintable=[Pin(number="1", name="NRST")], absolute_maximum_ratings=[], rules=[], ) findings = check_timing(graph, {"MCU": cons}) assert findings and findings[0].rule_id == "PE-TIM-001" complete_finding(findings[0]) assert findings[0].status == "ERROR" assert findings[0].finding_class == "RULE" def test_pi_missing_local_is_risk_not_error(): graph = DesignGraph( components={ "U1": Component( reference="U1", value="LDO", footprint="", component_type=ComponentType.IC, mpn="LDO", pins={"1": "VIN"}, ), }, nets={ "VIN": Net( name="VIN", net_type=NetType.POWER, voltage=5.0, pins=[PinConnection(component_ref="U1", pin_number="1")], ), }, ) cons = ComponentConstraints( mpn="LDO", pintable=[Pin(number="1", name="VIN")], absolute_maximum_ratings=[], rules=[], ) findings = check_power_integrity(graph, {"LDO": cons}) assert findings and findings[0].rule_id == "PE-PI-001" complete_finding(findings[0]) assert findings[0].status == "WARNING" assert findings[0].finding_class == "RISK" def test_esd_without_part_is_review_not_error(): graph = DesignGraph( components={ "J1": Component( reference="J1", value="USB", footprint="", component_type=ComponentType.CONNECTOR, mpn="", pins={"1": "USB_DP"}, ), "U2": Component( reference="U2", value="UART", footprint="", component_type=ComponentType.IC, mpn="CH", pins={"1": "USB_DP"}, ), }, nets={ "USB_DP": Net( name="USB_DP", net_type=NetType.SIGNAL, pins=[ PinConnection(component_ref="J1", pin_number="1"), PinConnection(component_ref="U2", pin_number="1"), ], ), }, ) cons = ComponentConstraints( mpn="CH", pintable=[Pin(number="1", name="UD+")], absolute_maximum_ratings=[], rules=[], internal_features=InternalFeatures(esd_clamp_pins=["UD+"]), ) findings = check_esd(graph, {"CH": cons}) assert findings and findings[0].rule_id == "PE-ESD-001" complete_finding(findings[0]) assert findings[0].finding_class == "REVIEW" assert findings[0].status != "ERROR" def test_return_path_hs_is_review(): graph = DesignGraph( components={ "U1": Component( reference="U1", value="", footprint="", component_type=ComponentType.IC, mpn="X", pins={"1": "USB_DP"}, ), }, nets={ "USB_DP": Net( name="USB_DP", net_type=NetType.SIGNAL, pins=[PinConnection(component_ref="U1", pin_number="1")], ), }, ) layout = LayoutGraph( footprints={"U1": LayoutFootprint(reference="U1", x=0, y=0, layer="F.Cu")}, segments=[ LayoutSegment(start=(0, 0), end=(20, 8), width=0.2, layer="F.Cu", net="USB_DP"), ], zones=[ LayoutZone(net="GND", layer="B.Cu", outlines=[[(0, -5), (20, -5), (20, 10), (0, 10)]]), ], vias=[], ) findings = check_return_path(graph, layout) assert findings and findings[0].rule_id == "PE-RET-001" complete_finding(findings[0]) assert findings[0].finding_class == "REVIEW" assert findings[0].status != "ERROR" layout.vias = [LayoutVia(x=10, y=4, net="GND", drill=0.3)] assert check_return_path(graph, layout) == [] def test_llm_error_review_clamped_in_complete_finding(): f = Finding( designator="U1", finding="no vias", why="PowerPAD", status="ERROR", source="pcb_review", facts="0 vias", requirement="must have vias", source_quote="Use thermal vias in the exposed pad.", evidence_status="SUFFICIENT", ) complete_finding(f) assert f.finding_class == "REVIEW" assert f.status == "WARNING"