bfb97d5259
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>
65 lines
2.4 KiB
Python
65 lines
2.4 KiB
Python
"""Rendering for the experimental in-KiCad result overlays: a solved
|
|
field (|J|) as an RGBA PNG, one pixel per grid cell, transparent where
|
|
there is no copper. The pushing side (ReferenceImages via the IPC API)
|
|
lives in board_io; this module stays KiCad-free so it is testable
|
|
headless.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
|
|
import numpy as np
|
|
|
|
from . import config
|
|
|
|
# the colormap's near-black bottom must stay distinguishable from
|
|
# KiCad's dark canvas (matplotlib figures sit on a light background
|
|
# instead), so the log scale starts this far up the colormap
|
|
FLOOR = 0.18
|
|
|
|
|
|
def heatmap_png(data3: np.ndarray, li: int, alpha: int | None = None,
|
|
bleed: bool = True) -> bytes:
|
|
"""One layer of a field (e.g. |J|, NaN = no copper) as opaque-over-
|
|
copper RGBA PNG bytes. Color scale matches the plugin's log figure
|
|
(global vmax across layers). `bleed` extends the edge color one
|
|
pixel outward at half opacity: the raster mask covers cells whose
|
|
CENTER is inside the copper, so without it the overlay stops half a
|
|
cell short of the outline KiCad draws."""
|
|
import matplotlib
|
|
from PIL import Image
|
|
from scipy import ndimage
|
|
|
|
if alpha is None:
|
|
alpha = config.OVERLAY_ALPHA
|
|
if not np.isfinite(data3).any():
|
|
raise ValueError("field is empty - nothing to overlay")
|
|
vmax = float(np.nanmax(data3))
|
|
if vmax <= 0:
|
|
raise ValueError("field is empty - nothing to overlay")
|
|
vmin = vmax / config.CURRENT_DYNAMIC_RANGE
|
|
d = np.clip(data3[li], vmin, vmax)
|
|
if config.LOG_CURRENT_SCALE:
|
|
u = (np.log(d) - np.log(vmin)) / (np.log(vmax) - np.log(vmin))
|
|
else:
|
|
u = d / vmax
|
|
u = FLOOR + (1.0 - FLOOR) * u
|
|
cmap = matplotlib.colormaps[config.CMAP_CURRENT]
|
|
rgba = (cmap(np.nan_to_num(u)) * 255).astype(np.uint8)
|
|
copper = ~np.isnan(data3[li])
|
|
rgba[..., 3] = np.where(copper, alpha, 0)
|
|
|
|
if bleed and copper.any() and not copper.all():
|
|
ring = ndimage.binary_dilation(
|
|
copper, structure=np.ones((3, 3), dtype=bool)) & ~copper
|
|
iy, ix = ndimage.distance_transform_edt(
|
|
~copper, return_distances=False, return_indices=True)
|
|
rgba[ring, :3] = rgba[iy[ring], ix[ring], :3]
|
|
rgba[ring, 3] = alpha // 2
|
|
|
|
buf = io.BytesIO()
|
|
# no dpi metadata: KiCad assumes its 300 PPI default, which the
|
|
# pusher's scale computation relies on
|
|
Image.fromarray(rgba, "RGBA").save(buf, format="PNG")
|
|
return buf.getvalue()
|