diff --git a/src/pygui/backend/controllers/session_controller.py b/src/pygui/backend/controllers/session_controller.py index 0288f6a..a327c44 100644 --- a/src/pygui/backend/controllers/session_controller.py +++ b/src/pygui/backend/controllers/session_controller.py @@ -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.models.frame import Frame 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.session_model import SessionModel from pygui.backend.models.threshold_classifier import ThresholdConfig @@ -34,6 +35,8 @@ class SessionController(QObject): sensorsChanged = Signal() loadedFileChanged = 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 _liveFrame = Signal(object) # Frame @@ -65,6 +68,7 @@ class SessionController(QObject): self._loaded_file: str = "" self._cluster_averaging_enabled = False self._active_clusters: list[list[int]] = [] + self._stats_tracker = ReplayStatsTracker() # ---- properties QML binds to ---- @Property(str, notify=modeChanged) @@ -248,6 +252,7 @@ class SessionController(QObject): self._active_clusters = group_sensors_by_radius(self._sensors) self._player.load(frames) self._model.reset() + self._stats_tracker.reset() self._loaded_file = file_path self.loadedFileChanged.emit() self.sensorsChanged.emit() @@ -378,6 +383,7 @@ class SessionController(QObject): # Clear out any old data from the prev sessions self._model.reset() + self._stats_tracker.reset() self._last = None self._last_raw_frame = None @@ -424,6 +430,8 @@ class SessionController(QObject): @Slot(object) @slot_error_boundary 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. self._last_raw_frame = frame edited = Frame(seq=frame.seq, time=frame.time, @@ -432,6 +440,7 @@ class SessionController(QObject): if self._recorder.is_recording: self._recorder.write(frame) # always record raw values, never edited self._dirty = True + self.trendData.emit(json.dumps(self._stats_tracker.get_trend())) def _flush_repaint(self) -> None: if not self._dirty: diff --git a/src/pygui/backend/models/replay_stats.py b/src/pygui/backend/models/replay_stats.py new file mode 100644 index 0000000..7794b6e --- /dev/null +++ b/src/pygui/backend/models/replay_stats.py @@ -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 diff --git a/src/pygui/backend/visualization/graph_view.py b/src/pygui/backend/visualization/graph_view.py index 1a3d7ba..874fe38 100644 --- a/src/pygui/backend/visualization/graph_view.py +++ b/src/pygui/backend/visualization/graph_view.py @@ -164,3 +164,30 @@ class GraphView(QObject): self._plot_window = None self._plot_widget = None 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)