feat: wire ReplayStatsTracker into SessionController + complete GraphView.updateTrend
This commit is contained in:
@@ -11,6 +11,7 @@ from pygui.backend.cluster_average import average_clusters, group_sensors_by_rad
|
|||||||
from pygui.backend.data.csv_recorder import CsvRecorder
|
from pygui.backend.data.csv_recorder import CsvRecorder
|
||||||
from pygui.backend.models.frame import Frame
|
from pygui.backend.models.frame import Frame
|
||||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
|
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
|
||||||
|
from pygui.backend.models.replay_stats import ReplayStatsTracker
|
||||||
from pygui.backend.models.sensor_editor import SensorEditor
|
from pygui.backend.models.sensor_editor import SensorEditor
|
||||||
from pygui.backend.models.session_model import SessionModel
|
from pygui.backend.models.session_model import SessionModel
|
||||||
from pygui.backend.models.threshold_classifier import ThresholdConfig
|
from pygui.backend.models.threshold_classifier import ThresholdConfig
|
||||||
@@ -34,6 +35,8 @@ class SessionController(QObject):
|
|||||||
sensorsChanged = Signal()
|
sensorsChanged = Signal()
|
||||||
loadedFileChanged = Signal()
|
loadedFileChanged = Signal()
|
||||||
clusterAveragingEnabledChanged = Signal()
|
clusterAveragingEnabledChanged = Signal()
|
||||||
|
# trend: per-frame avg for live graph
|
||||||
|
trendData = Signal(str) # JSON array of floats
|
||||||
# private: marshal a worker-thread frame onto the main thread
|
# private: marshal a worker-thread frame onto the main thread
|
||||||
_liveFrame = Signal(object) # Frame
|
_liveFrame = Signal(object) # Frame
|
||||||
|
|
||||||
@@ -65,6 +68,7 @@ class SessionController(QObject):
|
|||||||
self._loaded_file: str = ""
|
self._loaded_file: str = ""
|
||||||
self._cluster_averaging_enabled = False
|
self._cluster_averaging_enabled = False
|
||||||
self._active_clusters: list[list[int]] = []
|
self._active_clusters: list[list[int]] = []
|
||||||
|
self._stats_tracker = ReplayStatsTracker()
|
||||||
|
|
||||||
# ---- properties QML binds to ----
|
# ---- properties QML binds to ----
|
||||||
@Property(str, notify=modeChanged)
|
@Property(str, notify=modeChanged)
|
||||||
@@ -248,6 +252,7 @@ class SessionController(QObject):
|
|||||||
self._active_clusters = group_sensors_by_radius(self._sensors)
|
self._active_clusters = group_sensors_by_radius(self._sensors)
|
||||||
self._player.load(frames)
|
self._player.load(frames)
|
||||||
self._model.reset()
|
self._model.reset()
|
||||||
|
self._stats_tracker.reset()
|
||||||
self._loaded_file = file_path
|
self._loaded_file = file_path
|
||||||
self.loadedFileChanged.emit()
|
self.loadedFileChanged.emit()
|
||||||
self.sensorsChanged.emit()
|
self.sensorsChanged.emit()
|
||||||
@@ -378,6 +383,7 @@ class SessionController(QObject):
|
|||||||
|
|
||||||
# Clear out any old data from the prev sessions
|
# Clear out any old data from the prev sessions
|
||||||
self._model.reset()
|
self._model.reset()
|
||||||
|
self._stats_tracker.reset()
|
||||||
self._last = None
|
self._last = None
|
||||||
self._last_raw_frame = None
|
self._last_raw_frame = None
|
||||||
|
|
||||||
@@ -424,6 +430,8 @@ class SessionController(QObject):
|
|||||||
@Slot(object)
|
@Slot(object)
|
||||||
@slot_error_boundary
|
@slot_error_boundary
|
||||||
def _on_live_frame(self, frame: Frame) -> None:
|
def _on_live_frame(self, frame: Frame) -> None:
|
||||||
|
import json
|
||||||
|
self._stats_tracker.track(frame.seq, frame.values)
|
||||||
# Main thread (queued). Record raw, process edited; mark dirty for repaint.
|
# Main thread (queued). Record raw, process edited; mark dirty for repaint.
|
||||||
self._last_raw_frame = frame
|
self._last_raw_frame = frame
|
||||||
edited = Frame(seq=frame.seq, time=frame.time,
|
edited = Frame(seq=frame.seq, time=frame.time,
|
||||||
@@ -432,6 +440,7 @@ class SessionController(QObject):
|
|||||||
if self._recorder.is_recording:
|
if self._recorder.is_recording:
|
||||||
self._recorder.write(frame) # always record raw values, never edited
|
self._recorder.write(frame) # always record raw values, never edited
|
||||||
self._dirty = True
|
self._dirty = True
|
||||||
|
self.trendData.emit(json.dumps(self._stats_tracker.get_trend()))
|
||||||
|
|
||||||
def _flush_repaint(self) -> None:
|
def _flush_repaint(self) -> None:
|
||||||
if not self._dirty:
|
if not self._dirty:
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class ReplayStatsTracker:
|
||||||
|
"""Accumulates per-frames statistic acrossa replay session."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.history: list[dict] = []
|
||||||
|
self.running_avg: float = 0.0
|
||||||
|
|
||||||
|
def track(self, frame_index: int, temperatures: list[float]) -> dict:
|
||||||
|
arr = np.array(temperatures, dtype = np.float32)
|
||||||
|
clean_arr = arr[np.isfinite(arr)]
|
||||||
|
|
||||||
|
if clean_arr.size == 0:
|
||||||
|
stats = {"frame": frame_index, "min":0.0, "max":0.0, "avg":0.0, "std":0.0}
|
||||||
|
else:
|
||||||
|
stats = {
|
||||||
|
"frame": frame_index,
|
||||||
|
"min": float(np.min(clean_arr)),
|
||||||
|
"max": float(np.max(clean_arr)),
|
||||||
|
"avg": float(np.mean(clean_arr)),
|
||||||
|
"std": float(np.std(clean_arr)),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.history.append(stats)
|
||||||
|
if self.history:
|
||||||
|
self.running_avg = float(np.mean([s["avg"] for s in self.history]))
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def get_trend(self) -> list[float]:
|
||||||
|
"""Return list of per-frame average for trend chat"""
|
||||||
|
return [s["avg"] for s in self.history]
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.history.clear()
|
||||||
|
self.running_avg = 0.0
|
||||||
@@ -164,3 +164,30 @@ class GraphView(QObject):
|
|||||||
self._plot_window = None
|
self._plot_window = None
|
||||||
self._plot_widget = None
|
self._plot_widget = None
|
||||||
self._series = []
|
self._series = []
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def updateTrend(self, avgs_json: str) -> None:
|
||||||
|
"""Plot the running avg temp series."""
|
||||||
|
import json
|
||||||
|
if not self._plot_widget:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
avgs = json.loads(avgs_json)
|
||||||
|
except (json.JSONDecodeError, TypeError) as exc:
|
||||||
|
log.error("Failed to parse trend data: %s", exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
if hasattr(self,'_trend_line') and self._trend_line is not None:
|
||||||
|
self._plot_widget.removeItem(self._trend_line)
|
||||||
|
|
||||||
|
if not avgs:
|
||||||
|
return
|
||||||
|
|
||||||
|
x = list(range(len(avgs)))
|
||||||
|
pen = pg.mkPen("#5B9DF5", width=2)
|
||||||
|
self._trend_line = self._plot_widget.plot(x, avgs, name="Average", pen=pen)
|
||||||
|
|
||||||
|
# Y-axis range with buffer
|
||||||
|
y_min, y_max = min(avgs), max(avgs)
|
||||||
|
buf = max((y_max - y_min) * 0.1, 1.0) if y_max > y_min else 5.0
|
||||||
|
self._plot_widget.setYRange(y_min - buf, y_max + buf)
|
||||||
|
|||||||
Reference in New Issue
Block a user