Skip PE-PWR/PE-VIA without datasheet current (2.61.1).
Trace-width vs current runs only with I_load/Imax/I_abs. Missing I is a skip, not INFO INSUFFICIENT, not USB 500 mA, not Iout_max-as-load.
This commit is contained in:
@@ -102,7 +102,10 @@ def _seed() -> None:
|
||||
requirement="I_load vs regulator capability (not Iout_max).")
|
||||
_add("PE-PWR-001", "MANDATORY", "RULE", domain="pcb",
|
||||
source="pcb_power_thermal",
|
||||
requirement="Trace width × copper thickness vs I_load (IPC-2221 when ΔT known).")
|
||||
requirement=(
|
||||
"Trace width × copper vs datasheet I_load/Imax/I_abs "
|
||||
"(IPC-2221 when ΔT known). Skip if that current is missing."
|
||||
))
|
||||
for rid, prov, cls, req in (
|
||||
("PE-SEQ-001", "RECOMMENDED", "RISK", "Power sequencing from datasheet notes."),
|
||||
("PE-FLT-001", "RECOMMENDED", "REVIEW", "Filter topology vs datasheet."),
|
||||
@@ -152,7 +155,10 @@ def _seed() -> None:
|
||||
_add("PE-PLC-004", "RECOMMENDED", "REVIEW", domain="pcb",
|
||||
requirement="Keepout from layout_rules — not a DRC for pad copper.")
|
||||
_add("PE-VIA-001", "TYPICAL", "INFO", domain="pcb",
|
||||
requirement="Via current share from I_load and via count (no invented table).")
|
||||
requirement=(
|
||||
"Via count on a net with datasheet I_load/Imax/I_abs "
|
||||
"(no invented ampacity table). Skip if that current is missing."
|
||||
))
|
||||
_add("PE-THM-001", "RECOMMENDED", "RISK", domain="pcb",
|
||||
requirement="Dissipation vs courtyard pour/vias when P is known.")
|
||||
_add("PE-KEL-001", "RECOMMENDED", "RISK", domain="pcb",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Not generic SI. Not via ampacity. Not thermal FEM. Graph connectivity only:
|
||||
class from BOM / footprint / net FACT, then connection integrity. Missing
|
||||
numbers are INSUFFICIENT_EVIDENCE. A bare RJ45 is not PoE.
|
||||
numbers are INSUFFICIENT_EVIDENCE. A bare RJ45 is not PoE. No invented
|
||||
VBUS/Ethernet current (PE-PWR skips when the datasheet has no I).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Power-trace, via, thermal-copper, and Kelvin checks on a LayoutGraph.
|
||||
|
||||
Skip when current, width, copper thickness, or datasheet numbers are missing.
|
||||
Trace-width vs current runs only if the datasheet reports I_load / Imax / I_abs.
|
||||
Missing that number → skip (no invented I, no USB 500 mA, no INFO INSUFFICIENT).
|
||||
Iout_max is a rating, not load. Via ampacity is never invented. No thermal FEM.
|
||||
IPC-2221 is cited only when ΔT can be taken from datasheet Tjmax − 25 °C.
|
||||
"""
|
||||
|
||||
@@ -30,6 +32,14 @@ from backend.periscopex.thermal_check import (
|
||||
)
|
||||
from backend.periscopex.constraints_lookup import match_constraints as _match_constraints
|
||||
|
||||
# Width/via current: I_load, Imax, or I_abs from the datasheet. Never Iout_max.
|
||||
_TRACE_CURRENT_KEYS = _LOAD_KEYS + (
|
||||
"i_max", "i_max_a", "imax", "imax_a",
|
||||
"i_abs", "i_abs_a", "iabs",
|
||||
"i_abs_max", "i_abs_max_a",
|
||||
"absolute_maximum_current_a",
|
||||
)
|
||||
|
||||
# IPC-2221 §6.2 (empirical): I = k · ΔT^0.44 · A^0.725, A in mil², I in A.
|
||||
_IPC_B = 0.44
|
||||
_IPC_C = 0.725
|
||||
@@ -105,18 +115,25 @@ def _external_layers(layers: list[str]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _datasheet_trace_current(comp) -> float | None:
|
||||
"""I_load / Imax / I_abs from specs. None if absent or ≤0. Never Iout_max."""
|
||||
i = _first(_specs_values(comp), _TRACE_CURRENT_KEYS)
|
||||
if i is None or i <= 0:
|
||||
return None
|
||||
return i
|
||||
|
||||
|
||||
def _load_on_output(
|
||||
graph: DesignGraph, cmap: dict, ref: str,
|
||||
) -> tuple[float, str, str] | None:
|
||||
"""I_load (not Iout_max) on an IC output net, plus that net name."""
|
||||
"""Datasheet current (not Iout_max) on an IC output net, plus that net name."""
|
||||
comp = graph.components.get(ref)
|
||||
if not comp or comp.component_type != ComponentType.IC:
|
||||
return None
|
||||
cons = _match_constraints(comp.mpn or comp.value, cmap)
|
||||
values = _specs_values(comp)
|
||||
i_load = _first(values, _LOAD_KEYS)
|
||||
i_load = _datasheet_trace_current(comp)
|
||||
if i_load is None:
|
||||
return None
|
||||
cons = _match_constraints(comp.mpn or comp.value, cmap)
|
||||
vout = _pin_net_by_role(graph, comp, cons, _VOUT_PIN)
|
||||
if not vout:
|
||||
return None
|
||||
@@ -128,7 +145,10 @@ def check_pcb_power_traces(
|
||||
constraints_map: dict,
|
||||
layout: LayoutGraph | None,
|
||||
) -> list[Finding]:
|
||||
"""Trace width × copper thickness vs datasheet I_load (IPC-2221 when ΔT known)."""
|
||||
"""Trace width × copper vs datasheet I_load/Imax/I_abs (IPC-2221 when ΔT known).
|
||||
|
||||
Missing datasheet current → skip. No invented I, no USB 500 mA, no INFO.
|
||||
"""
|
||||
if layout is None:
|
||||
return []
|
||||
t_mm = layout.stackup.copper_thickness_mm if layout.stackup else None
|
||||
@@ -137,7 +157,9 @@ def check_pcb_power_traces(
|
||||
for ref, comp in sorted(graph.components.items()):
|
||||
if comp.component_type != ComponentType.IC:
|
||||
continue
|
||||
i_load = _first(_specs_values(comp), _LOAD_KEYS)
|
||||
i_load = _datasheet_trace_current(comp)
|
||||
if i_load is None:
|
||||
continue
|
||||
cons = _match_constraints(comp.mpn or comp.value, constraints_map)
|
||||
vout = _pin_net_by_role(graph, comp, cons, _VOUT_PIN)
|
||||
if not vout:
|
||||
@@ -147,25 +169,6 @@ def check_pcb_power_traces(
|
||||
continue
|
||||
seen_nets.add(key)
|
||||
net = vout
|
||||
if i_load is None or i_load <= 0:
|
||||
out.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="layout_power",
|
||||
finding=(
|
||||
f"{net} has no datasheet I_load; IPC-2221 ampacity is not applied."
|
||||
),
|
||||
why="I_load is required; width/current are never invented.",
|
||||
status="INFO",
|
||||
recommendation="Add I_load to the shared library extraction, then re-run PCB review.",
|
||||
source="pcb_power_thermal",
|
||||
rule_id="PE-PWR-001",
|
||||
evidence_status="INSUFFICIENT",
|
||||
assumptions=["No default current; IPC-2221 skipped."],
|
||||
net=net,
|
||||
pins=[],
|
||||
))
|
||||
continue
|
||||
w_mm, layers = _min_width_layers(layout, net)
|
||||
if w_mm is None:
|
||||
continue
|
||||
@@ -230,7 +233,10 @@ def check_pcb_power_traces(
|
||||
f"{_IPC_K_EXT if ext else _IPC_K_INT}, ΔT={dt:.0f} °C, "
|
||||
f"w={w_mm:g} mm, t={t_mm * 1000:g} µm)."
|
||||
),
|
||||
why="IPC-2221 §6.2 I = k·ΔT^0.44·A^0.725. I_load is the datasheet/typical load, not Iout_max.",
|
||||
why=(
|
||||
"IPC-2221 §6.2 I = k·ΔT^0.44·A^0.725. "
|
||||
"Current is datasheet I_load / Imax / I_abs, not Iout_max, not USB 500 mA."
|
||||
),
|
||||
status="ERROR",
|
||||
recommendation=(
|
||||
f"Widen {net} (and/or add copper/vias/planes) so ampacity ≥ {i_load:.3g} A, "
|
||||
@@ -253,7 +259,7 @@ def check_pcb_via_current(
|
||||
constraints_map: dict,
|
||||
layout: LayoutGraph | None,
|
||||
) -> list[Finding]:
|
||||
"""Share I_load across vias; no invented via-ampacity table."""
|
||||
"""Share datasheet I across vias; skip if I is missing. No invented via table."""
|
||||
if layout is None:
|
||||
return []
|
||||
out: list[Finding] = []
|
||||
@@ -261,7 +267,9 @@ def check_pcb_via_current(
|
||||
for ref, comp in sorted(graph.components.items()):
|
||||
if comp.component_type != ComponentType.IC:
|
||||
continue
|
||||
i_load = _first(_specs_values(comp), _LOAD_KEYS)
|
||||
i_load = _datasheet_trace_current(comp)
|
||||
if i_load is None:
|
||||
continue
|
||||
cons = _match_constraints(comp.mpn or comp.value, constraints_map)
|
||||
net = _pin_net_by_role(graph, comp, cons, _VOUT_PIN)
|
||||
if not net:
|
||||
@@ -270,24 +278,6 @@ def check_pcb_via_current(
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if i_load is None or i_load <= 0:
|
||||
out.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="layout_power",
|
||||
finding=(
|
||||
f"{net} has no datasheet I_load; via current is not rated."
|
||||
),
|
||||
why="I_load is required; via ampacity is never invented.",
|
||||
status="INFO",
|
||||
recommendation="Add I_load to the shared library extraction, then re-run PCB review.",
|
||||
source="pcb_power_thermal",
|
||||
rule_id="PE-VIA-001",
|
||||
evidence_status="INSUFFICIENT",
|
||||
net=net,
|
||||
pins=[],
|
||||
))
|
||||
continue
|
||||
n, _drill = _via_stats(layout, net)
|
||||
if n > 0:
|
||||
# Geometry is present; no via-ampacity table in the library — do not
|
||||
|
||||
@@ -39,7 +39,8 @@ Do not claim antenna keepout geometry is missing if keepout zones are listed.
|
||||
- Interface class: USB-C CC/Rd/VBUS, Ethernet 10/100 vs GbE magnetics, \
|
||||
PoE only with evidence. Deterministic certifiers already ran; explain \
|
||||
those FACTS. Never invent PoE on a bare RJ45 or SuperSpeed on USB2 Type-C. \
|
||||
Never invent millimetres, Z, or I.
|
||||
Never invent millimetres, Z, or I. Do not assume USB 500 mA — PE-PWR \
|
||||
skips when the datasheet has no I_load/Imax/I_abs.
|
||||
|
||||
### Evidence
|
||||
Cite numbers from "Parsed board geometry". Say insufficient evidence ONLY \
|
||||
|
||||
@@ -43,7 +43,7 @@ compensation, feedback divider, sense resistor).
|
||||
- Dedicated interface class (USB-C CC1/CC2 Rp/Rd 5.1 kΩ VBUS/GND; Ethernet \
|
||||
10/100 vs GbE magnetics; PoE only with evidence). Deterministic certifiers \
|
||||
own those FACTS. Do not invent PoE on a bare RJ45, SuperSpeed on USB 2.0 \
|
||||
Type-C, or geometry/Z/I.
|
||||
Type-C, or geometry/Z/I. Do not assume USB 500 mA.
|
||||
|
||||
Then work the areas one at a time. For EACH area, don't just confirm a \
|
||||
part is present — ask what specific failure mode would make it wrong \
|
||||
|
||||
@@ -10,7 +10,7 @@ Non è un unico check SI generico. Non è il DRC KiCad. Non è “via ampacity I
|
||||
|
||||
Classe dichiarata o inferita (FACT/REQUIREMENT da BOM, net, footprint, sheet). Se manca evidenza: INSUFFICIENT_EVIDENCE, niente geometria/Z/I inventati. Via ≠ pad ≠ track.
|
||||
|
||||
## Prima fetta (shipped 2.61.0)
|
||||
## Prima fetta (shipped 2.61.0, skip-I 2.61.1)
|
||||
|
||||
| ID | Cosa certifica |
|
||||
| --- | --- |
|
||||
@@ -27,7 +27,7 @@ Classe dichiarata o inferita (FACT/REQUIREMENT da BOM, net, footprint, sheet). S
|
||||
| PE-POE-002 | Classe/tipo 802.3af/at/bt se citati; altrimenti INSUFFICIENT |
|
||||
| PE-POE-003 | Isolamento/magnetics solo con evidenza PoE |
|
||||
|
||||
Motore: `periscopex/interface_class_check.py` (grafo, non geometria). Schema `MODE=run` e PCB `MODE=pcb`. L’IA spiega i FACT, non certifica.
|
||||
Motore: `periscopex/interface_class_check.py` (grafo, non geometria). Schema `MODE=run` e PCB `MODE=pcb`. L’IA spiega i FACT, non certifica. Nessun I inventato (niente USB 500 mA): la corrente vs larghezza è PE-PWR, e **skip** se il datasheet non riporta I_load/Imax/I_abs.
|
||||
|
||||
## Dopo
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Vedi `motore-finding.md`. Invarianti:
|
||||
3. Classi **RULE | RISK | REVIEW | INFO**. LLM = REVIEW, mai RULE.
|
||||
4. `decisions.json` + fingerprint: rescan non ri-naga (PSEL low intenzionale, ecc.).
|
||||
5. `status`, `confidence`, `evidence_status` indipendenti.
|
||||
6. Evidenza insufficiente → lo si dice, niente 1 oz / 10 °C / 50 Ω / IEC inventati.
|
||||
6. Evidenza insufficiente → lo si dice, niente 1 oz / 10 °C / 50 Ω / IEC inventati. **Eccezione PE-PWR/PE-VIA:** se manca I_load/Imax/I_abs, skip silenzioso (niente INFO, niente 500 mA USB).
|
||||
7. Oggetto riproducibile: facts, requirement, source, calculation, assumptions, confidence, severity, action.
|
||||
|
||||
UI report (schema e layout): stessa `FindingCard` — Fact / Requirement / Inference, badge classe e provenance.
|
||||
@@ -58,7 +58,7 @@ Mutex vs analisi e vs placement. SSE `pcb_*`. IDs `PCB-{ref}-{001}`. KiCad `/net
|
||||
| PE-SI-002 | ImpedenceFinder Zavg/min/max vs `impedance` window |
|
||||
| PE-SI-003…009 | max length, spacing, ref plane, vias, return, series R — each a datasheet number |
|
||||
| PE-SI-010 | HS bus measured, library has no SI FACT (not 90 Ω folklore) |
|
||||
| PE-PWR-001 | I_load + width; IPC solo con thickness e Tjmax |
|
||||
| PE-PWR-001 | I_load/Imax/I_abs + width; **skip** se manca I (no 500 mA, no INFO); IPC solo con thickness e Tjmax |
|
||||
| PE-DRT-001 | Vop e Vrated, Vop > Vr → RULE ERROR |
|
||||
| PE-DRT-002 | 0.8 < Vop/Vr ≤ 1 → RISK MARGIN |
|
||||
| PE-DRT-003 | Vop/Vr ≤ 0.8 → PASS summary INFO |
|
||||
@@ -66,7 +66,7 @@ Mutex vs analisi e vs placement. SSE `pcb_*`. IDs `PCB-{ref}-{001}`. KiCad `/net
|
||||
| PE-TIM-001/002 | RC / strap solo con numeri |
|
||||
| PE-PI-001/002 | local vs bulk; mm solo da layout_rules |
|
||||
| PE-ESD-001 / PE-RET-001 | REVIEW unless mandatory FACT |
|
||||
| PE-VIA-001 | via count + I_load (INFO, no tabella inventata) |
|
||||
| PE-VIA-001 | via count + I datasheet (INFO, no tabella inventata); **skip** se manca I |
|
||||
| PE-THM-001 | P=I_load×drop, courtyard senza pour/via |
|
||||
| PE-THM-002 | Tj=Ta+P·θJA solo con P, θJA, area rame, conteggio via |
|
||||
| PE-BOM-010…014 | package / pinout / V / T / I quando BOM, PCB e datasheet hanno i numeri |
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
What's new in Periscope.
|
||||
|
||||
## 2.61.1 — 2026-09-21 — Skip trace-width vs current without datasheet I
|
||||
|
||||
PE-PWR-001 (and PE-VIA-001) run only when the datasheet reports I_load / Imax / I_abs. Missing that number is a skip — not INFO INSUFFICIENT, not a typical USB 500 mA, not Iout_max-as-load. USB-C / Ethernet / PoE class certifiers still have no current/ampacity check. No via IPC table. No FEM.
|
||||
|
||||
- [Fixed] PE-PWR-001 / PE-VIA-001 skip when datasheet current is absent.
|
||||
- [Fixed] Imax / I_abs accepted for width; Iout_max is still not load.
|
||||
|
||||
## 2.61.0 — 2026-09-21 — Interface class certifiers (USB-C, Ethernet, PoE)
|
||||
|
||||
Dedicated per-interface certifiers (deterministic + AI explanation). Not generic SI. USB-C: CC1/CC2, Rp/Rd 5.1 kΩ, VBUS/GND, SuperSpeed only if the class is USB3. Ethernet: 10/100 vs GbE; magnetics required for GbE. PoE only with PoE evidence — a bare RJ45 is N/A, never invented PoE. Missing generation/class/ohms → INSUFFICIENT. DDR3 and via IPC ampacity are out of this slice. No FEM.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "periscope-web",
|
||||
"version": "2.61.0",
|
||||
"version": "2.61.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"sync-version": "node scripts/sync-version.mjs",
|
||||
|
||||
@@ -607,3 +607,16 @@ def test_does_not_use_layout_geometry():
|
||||
assert "LayoutPad" not in src
|
||||
assert "LayoutSegment" not in src
|
||||
assert "LayoutZone" not in src
|
||||
|
||||
|
||||
def test_usbc_vbus_does_not_invent_usb_500ma():
|
||||
"""No typical USB current and no PE-PWR check without datasheet I."""
|
||||
findings = check_interface_classes(_usbc_device_graph())
|
||||
blob = " ".join(
|
||||
" ".join(filter(None, (f.finding, f.why, f.facts, f.requirement, f.inference)))
|
||||
for f in findings
|
||||
).lower()
|
||||
assert "500 ma" not in blob
|
||||
assert "500ma" not in blob
|
||||
assert "0.5 a" not in blob
|
||||
assert not any(f.rule_id in {"PE-PWR-001", "PE-VIA-001"} for f in findings)
|
||||
|
||||
@@ -515,9 +515,44 @@ def test_power_trace_skips_without_i_load():
|
||||
graph.components["U1"].specs.values["i_load_a"] = None # type: ignore[union-attr]
|
||||
graph.components["U1"].specs = None
|
||||
findings = check_pcb_power_traces(graph, cmap, layout)
|
||||
assert findings == []
|
||||
|
||||
|
||||
def test_power_trace_skips_iout_max_as_load():
|
||||
from backend.periscopex.pcb_power_thermal import check_pcb_power_traces
|
||||
|
||||
graph, cmap, layout = _ldo_graph_layout(i_load=10)
|
||||
graph.components["U1"].specs.values.clear() # type: ignore[union-attr]
|
||||
graph.components["U1"].specs.values["iout_max_a"] = 10.0 # type: ignore[union-attr]
|
||||
graph.components["U1"].specs.values["tj_max"] = 150.0 # type: ignore[union-attr]
|
||||
findings = check_pcb_power_traces(graph, cmap, layout)
|
||||
assert findings == []
|
||||
|
||||
|
||||
def test_power_trace_uses_imax_when_i_load_absent():
|
||||
from backend.periscopex.pcb_power_thermal import check_pcb_power_traces
|
||||
|
||||
graph, cmap, layout = _ldo_graph_layout(i_load=10)
|
||||
vals = graph.components["U1"].specs.values # type: ignore[union-attr]
|
||||
vals.pop("i_load_a", None)
|
||||
vals["i_max"] = 10.0
|
||||
findings = check_pcb_power_traces(graph, cmap, layout)
|
||||
assert findings
|
||||
assert findings[0].rule_id == "PE-PWR-001"
|
||||
assert findings[0].evidence_status == "INSUFFICIENT"
|
||||
assert findings[0].status == "ERROR"
|
||||
|
||||
|
||||
def test_power_trace_uses_i_abs_when_i_load_absent():
|
||||
from backend.periscopex.pcb_power_thermal import check_pcb_power_traces
|
||||
|
||||
graph, cmap, layout = _ldo_graph_layout(i_load=10)
|
||||
vals = graph.components["U1"].specs.values # type: ignore[union-attr]
|
||||
vals.pop("i_load_a", None)
|
||||
vals["i_abs"] = 10.0
|
||||
findings = check_pcb_power_traces(graph, cmap, layout)
|
||||
assert findings
|
||||
assert findings[0].rule_id == "PE-PWR-001"
|
||||
assert findings[0].status == "ERROR"
|
||||
|
||||
|
||||
def test_kelvin_sense_pin_on_shared_net():
|
||||
|
||||
@@ -307,7 +307,7 @@ def test_fb1_blm_is_bead_not_dcr_resistor():
|
||||
assert getattr(dcr.specs, "value_ohms", None) is None
|
||||
|
||||
|
||||
def test_via_current_insufficient_without_i_load():
|
||||
def test_via_current_skips_without_i_load():
|
||||
from backend.periscopex.models import LayoutStackup
|
||||
|
||||
cons = ComponentConstraints(
|
||||
@@ -339,11 +339,9 @@ def test_via_current_insufficient_without_i_load():
|
||||
],
|
||||
)
|
||||
findings = check_pcb_via_current(graph, {"LDO1": cons}, layout)
|
||||
assert findings and findings[0].rule_id == "PE-VIA-001"
|
||||
assert findings[0].evidence_status == "INSUFFICIENT"
|
||||
assert findings == []
|
||||
pwr = check_pcb_power_traces(graph, {"LDO1": cons}, layout)
|
||||
assert pwr and pwr[0].rule_id == "PE-PWR-001"
|
||||
assert pwr[0].evidence_status == "INSUFFICIENT"
|
||||
assert pwr == []
|
||||
|
||||
|
||||
def test_thinking_reasoning_content_echo_still_present():
|
||||
|
||||
Reference in New Issue
Block a user