Experimental: push |J| heatmap overlays into KiCad (dialog opt-in)
Build PCM package / build (push) Successful in 6s
Build PCM package / build (push) Successful in 6s
After a solve, the per-layer current-density maps can be pushed into the open board as reference images on User.9..User.12 (stackup order, top first) - visible right in the editor, toggled like any layer, never plotted to gerbers. Dialog checkbox, default OFF; every push replaces all reference images on those layers. Rendering (fill_resistance/overlay.py, KiCad-free and tested headless): one pixel per grid cell, opaque over copper with the log scale lifted off the colormap's near-black bottom (dark canvas), transparent elsewhere, one pixel of half-alpha edge bleed so the overlay reaches the drawn outline instead of stopping half a cell short. Pushing lives in board_io (ReferenceImage via the IPC API, KiCad >= 10.0.1; scale = width / (pixels * 1 inch / 300 PPI), position = image center); kipy 0.7.1 swallows creation errors, so the per-item status is read from the raw CreateItemsResponse. pipeline.run takes an optional overlay callback; failures are reported, never fatal. tools/kicad_overlay_test.py pushes a fiducial alignment pattern (KiCad bbox readback verified placement to half a pixel); tools/ kicad_heatmap_overlay.py runs the whole thing headless against the open board, filtering marker rectangles of other nets' analyses via the exact copper test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""In-KiCad overlay rendering (fill_resistance.overlay): copper-shaped
|
||||
RGBA heatmaps with a visibility floor and a soft edge bleed. The kipy
|
||||
pushing side is exercised only against a live KiCad (tools/)."""
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from fill_resistance import config, overlay
|
||||
|
||||
|
||||
def _field(ny=20, nx=30):
|
||||
"""Two-layer |J| field: copper disc on layer 0, NaN elsewhere."""
|
||||
data = np.full((2, ny, nx), np.nan)
|
||||
yy, xx = np.mgrid[:ny, :nx]
|
||||
disc = (yy - ny / 2) ** 2 + (xx - nx / 2) ** 2 <= 8 ** 2
|
||||
data[0][disc] = 1.0 + xx[disc] # spans the log range
|
||||
data[1][disc] = 1e-12 # below the global log floor
|
||||
return data, disc
|
||||
|
||||
|
||||
def test_heatmap_png_shape_and_alpha():
|
||||
data, disc = _field()
|
||||
img = Image.open(io.BytesIO(overlay.heatmap_png(data, 0, bleed=False)))
|
||||
assert img.size == (30, 20)
|
||||
rgba = np.asarray(img)
|
||||
assert (rgba[..., 3][disc] == config.OVERLAY_ALPHA).all()
|
||||
assert (rgba[..., 3][~disc] == 0).all()
|
||||
|
||||
|
||||
def test_heatmap_floor_not_black():
|
||||
"""The coldest copper must stay distinguishable from a dark canvas:
|
||||
the colormap starts FLOOR up, never at its near-black bottom."""
|
||||
data, disc = _field()
|
||||
rgba = np.asarray(Image.open(io.BytesIO(
|
||||
overlay.heatmap_png(data, 1, bleed=False)))) # layer 1: all-cold
|
||||
floor = np.array(__import__("matplotlib").colormaps[
|
||||
config.CMAP_CURRENT](overlay.FLOOR)[:3]) * 255
|
||||
assert np.abs(rgba[..., :3][disc] - floor).max() <= 1
|
||||
assert rgba[..., :3][disc].sum(axis=-1).min() > 30 # not near-black
|
||||
|
||||
|
||||
def test_heatmap_bleed_ring():
|
||||
"""bleed=True: one pixel of half-alpha edge color outside the copper
|
||||
(the mask stops half a cell short of the drawn outline)."""
|
||||
from scipy import ndimage
|
||||
data, disc = _field()
|
||||
rgba = np.asarray(Image.open(io.BytesIO(overlay.heatmap_png(data, 0))))
|
||||
ring = ndimage.binary_dilation(
|
||||
disc, structure=np.ones((3, 3), dtype=bool)) & ~disc
|
||||
assert (rgba[..., 3][ring] == config.OVERLAY_ALPHA // 2).all()
|
||||
outside = ~disc & ~ring
|
||||
assert (rgba[..., 3][outside] == 0).all()
|
||||
assert (rgba[..., 3][disc] == config.OVERLAY_ALPHA).all()
|
||||
|
||||
|
||||
def test_heatmap_empty_field():
|
||||
with pytest.raises(ValueError):
|
||||
overlay.heatmap_png(np.full((1, 4, 4), np.nan), 0)
|
||||
Reference in New Issue
Block a user