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:
2026-09-20 09:35:04 +02:00
parent facbaa2305
commit 342792df2d
16 changed files with 1259 additions and 27 deletions
+86 -17
View File
@@ -5,7 +5,11 @@ from __future__ import annotations
import logging
from collections import Counter
from backend.periscopex.derating import build_derating_table
from backend.periscopex.esd_return_check import check_esd, check_return_path
from backend.periscopex.finding_engine import complete_findings
from backend.periscopex.functional_groups import FunctionalGroupsReport
from backend.periscopex.hierarchy import check_hierarchy
from backend.periscopex.models import DesignGraph, Finding, LayoutGraph
from backend.periscopex.pcb_net_match import check_pcb_net_match
from backend.periscopex.pcb_power_thermal import (
@@ -15,8 +19,10 @@ from backend.periscopex.pcb_power_thermal import (
check_pcb_thermal_copper,
check_pcb_via_current,
)
from backend.periscopex.pi_check import check_power_integrity
from backend.periscopex.placement_check import check_placement
from backend.periscopex.si_check import check_si
from backend.periscopex.timing_check import check_timing
log = logging.getLogger(__name__)
@@ -40,32 +46,89 @@ def assign_pcb_finding_ids(findings: list[Finding]) -> None:
def check_pcb_derating(graph: DesignGraph) -> list[Finding]:
"""ERROR only when both Vop and Vrated exist and Vop exceeds Vrated."""
"""PASS / MARGIN / RISK (exceed) when both Vop and Vrated exist."""
out: list[Finding] = []
n_pass = 0
for row in build_derating_table(graph):
rated = row.get("rated_voltage_v")
op = row.get("operating_voltage_v")
stress = row.get("stress") or "UNKNOWN"
if rated is None or op is None:
continue
if float(op) <= float(rated):
continue
ref = str(row.get("designator") or "")
out.append(Finding(
designator=ref,
mpn=str(row.get("mpn") or ""),
aspect="derating",
finding=(
f"{ref} operates at {op:g} V with a {rated:g} V rating."
),
why="Capacitor voltage rating must exceed the rail (no invented derating %).",
status="ERROR",
recommendation=(
mpn = str(row.get("mpn") or "")
net = row.get("net_plus")
ratio = float(op) / float(rated)
if stress == "RISK":
rec = (
f"Replace {ref} with a capacitor rated above {op:g} V, "
"or lower the rail."
),
)
out.append(Finding(
designator=ref,
mpn=mpn,
aspect="derating",
finding=(
f"{ref} operates at {op:g} V with a {rated:g} V rating "
f"(Vop/Vrated={ratio:.2f})."
),
facts=f"Vop={op:g} V, Vrated={rated:g} V, ratio={ratio:.3f}.",
requirement="Operating voltage must not exceed the capacitor rating.",
inference="Exceeding Vr is a measured RULE, not a dielectric %.",
why="Capacitor voltage rating must exceed the rail (no invented derating %).",
status="ERROR",
recommendation=rec,
action=rec,
source="pcb_derating",
rule_id="PE-DRT-001",
evidence_status="SUFFICIENT",
net=net,
pins=[],
))
elif stress == "MARGIN":
rec = (
f"Confirm {ref} voltage margin ({op:g}/{rated:g} V) or pick a "
"higher Vr. This is utilization, not a 50% ceramic rule."
)
out.append(Finding(
designator=ref,
mpn=mpn,
aspect="derating",
finding=(
f"{ref} Vop/Vrated={ratio:.2f} ({op:g}/{rated:g} V) — MARGIN."
),
facts=f"Vop={op:g} V, Vrated={rated:g} V, ratio={ratio:.3f}.",
requirement="Utilization > 0.8 of Vr is flagged as MARGIN (RISK).",
inference="Not an invented IPC/ceramic derating percentage.",
why="PASS/MARGIN/RISK from Vop and Vrated only.",
status="WARNING",
recommendation=rec,
action=rec,
source="pcb_derating",
rule_id="PE-DRT-002",
evidence_status="SUFFICIENT",
net=net,
pins=[],
))
elif stress == "PASS":
n_pass += 1
if n_pass:
rec = "No voltage-rating change required for PASS capacitors."
out.append(Finding(
designator="C",
mpn="",
aspect="derating",
finding=f"{n_pass} capacitor(s) PASS (Vop/Vrated ≤ 0.8).",
facts=f"PASS count={n_pass} (both Vop and Vrated present).",
requirement="Rated voltage exists and Vop is ≤ 80% of Vr.",
inference="Summary INFO — per-cap PASS lives on the derating table.",
why="Avoid one INFO card per capacitor.",
status="INFO",
recommendation=rec,
action=rec,
source="pcb_derating",
rule_id="PE-DRT-001",
net=row.get("net_plus"),
rule_id="PE-DRT-003",
evidence_status="SUFFICIENT",
pins=[],
))
return out
@@ -110,8 +173,9 @@ def run_pcb_checks(
graph: DesignGraph,
constraints_map: dict,
layout: LayoutGraph | None,
plan: FunctionalGroupsReport | None = None,
) -> list[Finding]:
"""Placement, SI, pad-net match, derating — skip individually on error."""
"""Placement, SI, pad-net match, derating, hierarchy, timing, PI, ESD."""
out: list[Finding] = []
for name, fn in (
("pcb_net_match", lambda: check_pcb_net_match(graph, layout)),
@@ -123,6 +187,11 @@ def run_pcb_checks(
("pcb_thermal_copper", lambda: check_pcb_thermal_copper(graph, constraints_map, layout)),
("pcb_kelvin", lambda: check_pcb_kelvin(graph, constraints_map)),
("pcb_gnd_stitch", lambda: check_pcb_gnd_stitch(graph, layout)),
("hierarchy_check", lambda: check_hierarchy(graph, plan)),
("timing_check", lambda: check_timing(graph, constraints_map)),
("pi_check", lambda: check_power_integrity(graph, constraints_map, layout)),
("esd_check", lambda: check_esd(graph, constraints_map)),
("return_path", lambda: check_return_path(graph, layout)),
):
try:
out.extend(fn())