feat: implement cluster averaging pipeline, UI controls

This commit is contained in:
jack
2026-06-15 15:02:55 -07:00
parent 7d32fb4b65
commit e545c245d7
6 changed files with 158 additions and 9 deletions
@@ -15,7 +15,7 @@ from pygui.backend.wafer.zwafer_models import Sensor
from pygui.backend.wafer.zwafer_parser import ZWaferParser
from pygui.serialcomm.stream_reader import StreamReader
from pygui.backend.models.sensor_editor import SensorEditor
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
log = logging.getLogger(__name__)
MODE_LIVE = "live"
@@ -24,12 +24,13 @@ MODE_REVIEW = "review"
class SessionController(QObject):
# public signals
frameUpdated = Signal() # any property below may have changed
frameUpdated = Signal()
modeChanged = Signal()
stateChanged = Signal()
recordingChanged = Signal()
sensorsChanged = Signal()
loadedFileChanged = Signal()
clusterAveragingEnabledChanged = Signal()
# private: marshal a worker-thread frame onto the main thread
_liveFrame = Signal(object) # Frame
@@ -59,6 +60,8 @@ class SessionController(QObject):
self._sensor_editor = SensorEditor()
self._last_raw_frame: Frame | None = None
self._loaded_file: str = ""
self._cluster_averaging_enabled = False
self._active_clusters: list[list[int]] = []
# ---- properties QML binds to ----
@Property(str, notify=modeChanged)
@@ -163,6 +166,18 @@ class SessionController(QObject):
"""Indices of sensors that currently have a replacement or offset."""
return self._sensor_editor.active_indices()
@Property(bool, notify=clusterAveragingEnabledChanged)
def clusterAveragingEnabled(self) -> bool:
return self._cluster_averaging_enabled
@clusterAveragingEnabled.setter
def clusterAveragingEnabled(self, value: bool) -> None:
if self._cluster_averaging_enabled == value:
return
self._cluster_averaging_enabled = value
self.clusterAveragingEnabledChanged.emit()
self._reprocess_current()
# ---- mode + thresholds ----
@Slot(str)
def setMode(self, mode: str) -> None:
@@ -186,7 +201,6 @@ class SessionController(QObject):
from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv
from pygui.backend.wafer.wafer_layouts import WaferLayout, resolve_shape_and_size
sensors: list[Sensor] = []
frames = []
if is_official_csv(file_path):
@@ -218,6 +232,9 @@ class SessionController(QObject):
shape, size = resolve_shape_and_size(sensors, wafer_id)
self._sensors = WaferLayout(sensors, shape=shape, size=size)
self._active_clusters = getattr(self._sensors, 'clusters', [])
if not self._active_clusters:
self._active_clusters = group_sensors_by_radius(self._sensors)
self._player.load(frames)
self._model.reset()
self._loaded_file = file_path
@@ -270,13 +287,20 @@ class SessionController(QObject):
self._emit_current()
self._play_timer.start(self._next_interval_ms())
def _apply_pipeline(self, raw_values: list[float]) -> list[float]:
"""Apply override first, then optional cluster averaging."""
edited = self._sensor_editor.apply(raw_values)
if getattr(self, "_cluster_averaging_enabled", False):
return average_clusters(edited, getattr(self, "_active_clusters", []))
return edited
def _emit_current(self) -> None:
frame = self._player.current()
if frame is None:
return
self._last_raw_frame = frame
edited = Frame(seq=frame.seq, time=frame.time,
values=self._sensor_editor.apply(frame.values))
values=self._apply_pipeline(frame.values))
self._last = self._model.process(edited)
self.frameUpdated.emit()
self.stateChanged.emit()
@@ -287,7 +311,7 @@ class SessionController(QObject):
if frame is None:
return
edited = Frame(seq=frame.seq, time=frame.time,
values=self._sensor_editor.apply(frame.values))
values=self._apply_pipeline(frame.values))
self._last = self._model.process(edited)
self.frameUpdated.emit()
self.stateChanged.emit()
@@ -307,6 +331,10 @@ class SessionController(QObject):
except Exception as e:
log.warning("Could not load layout for %s: %s", family_code, e)
self._active_clusters = getattr(self._sensors, 'clusters', [])
if not self._active_clusters:
self._active_clusters = group_sensors_by_radius(self._sensors)
# The new binary protocol sends payload of (sensorCount * 2) bytes
# Each sensor is a big-endian 16-bit value (Sign-Magnitude).
expected_sensors = 80 if (family_code or "") == "X" else 244
@@ -379,7 +407,7 @@ class SessionController(QObject):
# Main thread (queued). Record raw, process edited; mark dirty for repaint.
self._last_raw_frame = frame
edited = Frame(seq=frame.seq, time=frame.time,
values=self._sensor_editor.apply(frame.values))
values=self._apply_pipeline(frame.values))
self._last = self._model.process(edited)
if self._recorder.is_recording:
self._recorder.write(frame) # always record raw values, never edited