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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user