feat(viz): implement RBF heatmap and custom QPainter trend chart item

Introduce Radial Basis Function interpolation for fine wafer thermal gradients and GraphQuickItem for high-performance trend line rendering.
This commit is contained in:
jack
2026-06-25 13:47:48 -07:00
parent 2cbc143c20
commit f4621f1faf
3 changed files with 317 additions and 6 deletions
@@ -10,6 +10,7 @@ All sensor coordinates are center-origin mm (from wafer_layouts or a loaded CSV)
"""
from __future__ import annotations
import logging
import math
import numpy as np
@@ -22,13 +23,15 @@ from PySide6.QtGui import (
QPainter,
QPen,
QPolygon,
)
) # fmt: skip
from PySide6.QtQml import QmlElement
from PySide6.QtQuick import QQuickPaintedItem
from pygui.backend.visualization.rbf_heatmap import interpolate_field
from pygui.backend.wafer.zwafer_models import Sensor
log = logging.getLogger(__name__)
QML_IMPORT_NAME = "ISC.Wafer"
QML_IMPORT_MAJOR_VERSION = 1
@@ -197,10 +200,10 @@ class WaferMapItem(QQuickPaintedItem):
def thicknessData(self, val:list) -> None:
self._thickness_data = list(val or [])
self._rebuild_thickness()
self._thicknessChanged.emit()
self.thicknessChanged.emit()
self.update
@property(bool, notify=showThicknessChanged)
@Property(bool, notify=showThicknessChanged)
def showThickness(self) -> bool:
return self._show_thickness
@@ -265,14 +268,59 @@ class WaferMapItem(QQuickPaintedItem):
if not file_path:
return False
try:
image = self.grabToImage()
image.save(file_path, "PNG")
result = self.grabToImage()
img = result.image()
img.save(file_path, "PNG")
return True
except Exception as e:
log.error("Export failed: %s", e)
return False
# ── internal ─────────────────────────────────────────────────────────
def _rebuild_thickness(self) -> None:
"""Interpolate thickness data into a gray/orange heatmap QImage."""
if not self._sensors or not self._thickness_data:
self._thickness_heatmap = None
return
ds = self._draw_size()
r_mm = self._wafer_radius_mm()
xs = np.array([s.x for s in self._sensors])
ys = np.array([s.y for s in self._sensors])
vs = np.array(self._thickness_data[:len(self._sensors)], dtype=float)
if len(vs) < len(self._sensors):
self._thickness_heatmap = None
return
try:
field = interpolate_field(
xs, ys, vs,
width=ds, height=ds,
extent=(-r_mm, r_mm, -r_mm, r_mm),
round_clip=(self._shape == "round"),
)
except Exception:
self._thickness_heatmap = None
return
# Gray/orange colormap: map field range 0→1 to gray→orange
vmin, vmax = np.nanmin(field), np.nanmax(field)
span = vmax - vmin or 1.0
t = np.clip((field - vmin) / span, 0.0, 1.0)
# Gray (0.5, 0.5, 0.5) → orange (1.0, 0.65, 0.0)
rgb = np.zeros((ds, ds, 3), dtype=np.float32)
rgb[:, :, 0] = 0.5 + 0.5 * t
rgb[:, :, 1] = 0.5 + 0.15 * t
rgb[:, :, 2] = 0.5 - 0.5 * t
rgb = np.nan_to_num(rgb, nan=0.0)
rgba = np.zeros((ds, ds, 4), dtype=np.uint8)
rgba[:, :, :3] = (rgb * 255).astype(np.uint8)
rgba[:, :, 3] = np.where(np.isfinite(field), 180, 0).astype(np.uint8)
self._thickness_heatmap = (
QImage(rgba.tobytes(), ds, ds, QImage.Format.Format_RGBA8888).copy()
)
def _on_resize(self) -> None:
self._rebuild()