Ship Fase B slices: hierarchy, derating bands, timing, PI, ESD/return.
ERROR stays only for RULE+MANDATORY. Hierarchy walks component→pin→net→block. Derating is PASS/MARGIN/RISK from Vop/Vrated. Timing and PI skip without numbers. ESD and HS return path are REVIEW, not RULE ERROR.
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
"""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"
|
||||
@@ -52,6 +52,56 @@ def test_llm_review_is_never_rule():
|
||||
assert f.finding_class == "REVIEW"
|
||||
assert f.facts
|
||||
assert f.requirement
|
||||
assert f.status == "WARNING"
|
||||
|
||||
|
||||
def test_review_and_risk_cannot_stay_error():
|
||||
review = Finding(
|
||||
designator="U1",
|
||||
finding="PowerPAD vias",
|
||||
why="layout note",
|
||||
status="ERROR",
|
||||
source="pcb_review",
|
||||
facts="0 vias under U1",
|
||||
requirement="datasheet PowerPAD",
|
||||
source_quote="Connect the thermal pad with vias to GND.",
|
||||
evidence_status="SUFFICIENT",
|
||||
)
|
||||
complete_finding(review)
|
||||
assert review.finding_class == "REVIEW"
|
||||
assert review.status == "WARNING"
|
||||
|
||||
risk = Finding(
|
||||
designator="C1",
|
||||
finding="high utilization",
|
||||
why="Vop near Vr",
|
||||
status="ERROR",
|
||||
rule_id="PE-DRT-002",
|
||||
source="pcb_derating",
|
||||
evidence_status="SUFFICIENT",
|
||||
facts="3.0 / 3.3 V",
|
||||
requirement="utilization band",
|
||||
)
|
||||
complete_finding(risk)
|
||||
assert risk.finding_class == "RISK"
|
||||
assert risk.status == "WARNING"
|
||||
|
||||
|
||||
def test_mandatory_rule_stays_error():
|
||||
f = Finding(
|
||||
designator="U1",
|
||||
finding="NC tied",
|
||||
why="NC must float",
|
||||
status="ERROR",
|
||||
rule_id="PE-NC-001",
|
||||
source="nc_pin_check",
|
||||
facts="NC on GND",
|
||||
requirement="NC pins must not be connected.",
|
||||
evidence_status="SUFFICIENT",
|
||||
)
|
||||
complete_finding(f)
|
||||
assert f.finding_class == "RULE"
|
||||
assert f.provenance == "MANDATORY"
|
||||
assert f.status == "ERROR"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user