feat(map): add batch export and adjust layout alignment
- Implement synchronous offscreen wafer map PNG rendering and CSV summary. - Add "Batch Export" button to file browser sidebar. - Adjust trend pane bottom spacer to align with About button when visible. - Fix type check errors and add pytest coverage for batch export.
This commit is contained in:
@@ -12,7 +12,6 @@ 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, SessionUpdate
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig
|
||||
@@ -39,7 +38,8 @@ class SessionController(QObject):
|
||||
clusterAveragingEnabledChanged = Signal()
|
||||
liveStatsChanged = Signal() # emitted when receivedCount or errorCount change
|
||||
# trend: per-frame avg for live graph
|
||||
trendData = Signal(str) # JSON array of floats
|
||||
trendDelta = Signal(str) # JSON [[elapsed_s, avg]] — one new point per live frame
|
||||
trendReset = Signal() # live trend buffer cleared (new stream started)
|
||||
# NOTE: dict (QVariantMap), not object — Signal(object) delivers Python
|
||||
# dicts to QML as an opaque empty wrapper with no accessible fields.
|
||||
comparisonResult = Signal(dict) # dict with success, distance, warping_path, max_sensor_deviation, series_a, series_b
|
||||
@@ -59,13 +59,7 @@ class SessionController(QObject):
|
||||
self._sensors: list[Sensor] = []
|
||||
self._last: Optional[SessionUpdate] = None
|
||||
self._elapsed = 0.0
|
||||
self._trend_buffer: list[float] = [] # rolling window
|
||||
self._trend_timestamps: list[float] = [] # monotonic timestamps
|
||||
|
||||
# Trend refresh timer - update every 1s
|
||||
self._trend_timer = QTimer(self)
|
||||
self._trend_timer.setInterval(1000)
|
||||
self._trend_timer.timeout.connect(self._flush_trend)
|
||||
self._stream_start_time: float = 0.0
|
||||
|
||||
self._play_timer = QTimer(self)
|
||||
self._play_timer.timeout.connect(self._advance)
|
||||
@@ -85,7 +79,6 @@ class SessionController(QObject):
|
||||
self._loaded_file: str = ""
|
||||
self._cluster_averaging_enabled = False
|
||||
self._active_clusters: list[list[int]] = []
|
||||
self._stats_tracker = ReplayStatsTracker()
|
||||
self._received_count: int = 0
|
||||
self._error_count: int = 0
|
||||
|
||||
@@ -293,15 +286,10 @@ 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()
|
||||
self._emit_current()
|
||||
# Populate stats tracker from all loaded frames for trend chart (P3.1)
|
||||
for frame in self._player._frames:
|
||||
self._stats_tracker.track(frame.seq, frame.values)
|
||||
self.trendData.emit(json.dumps(self._stats_tracker.get_trend()))
|
||||
|
||||
@Slot()
|
||||
@slot_error_boundary
|
||||
@@ -309,7 +297,6 @@ class SessionController(QObject):
|
||||
"""Clear the loaded file, resetting the player and frame states."""
|
||||
self._player.load([])
|
||||
self._model.reset()
|
||||
self._stats_tracker.reset()
|
||||
self._loaded_file = ""
|
||||
self.loadedFileChanged.emit()
|
||||
self.sensorsChanged.emit()
|
||||
@@ -689,10 +676,7 @@ class SessionController(QObject):
|
||||
|
||||
# Clear out any old data from the prev sessions
|
||||
self._model.reset()
|
||||
self._trend_buffer.clear()
|
||||
self._trend_timestamps.clear()
|
||||
self._trend_timer.start()
|
||||
self._stats_tracker.reset()
|
||||
self._reset_live_trend()
|
||||
self._last = None
|
||||
self._last_raw_frame = None
|
||||
self._received_count = 0
|
||||
@@ -734,7 +718,6 @@ class SessionController(QObject):
|
||||
@slot_error_boundary
|
||||
def stopStream(self) -> None:
|
||||
self._repaint_timer.stop()
|
||||
self._trend_timer.stop()
|
||||
self._received_count = 0
|
||||
self._error_count = 0
|
||||
self.liveStatsChanged.emit()
|
||||
@@ -760,10 +743,8 @@ class SessionController(QObject):
|
||||
@Slot(object)
|
||||
@slot_error_boundary
|
||||
def _on_live_frame(self, frame: Frame) -> None:
|
||||
import json
|
||||
self._received_count += 1
|
||||
self.liveStatsChanged.emit()
|
||||
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,
|
||||
@@ -772,22 +753,11 @@ 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()))
|
||||
|
||||
if self._last and self._last.stats:
|
||||
avg = self._last.stats.avg
|
||||
now = time.monotonic()
|
||||
self._trend_buffer.append(avg)
|
||||
self._trend_timestamps.append(now)
|
||||
|
||||
# drop entries older than 60 secs
|
||||
cutoff = now -60
|
||||
while self._trend_timestamps and self._trend_timestamps[0] < cutoff:
|
||||
self._trend_timestamps.pop(0)
|
||||
self._trend_buffer.pop(0)
|
||||
|
||||
|
||||
|
||||
elapsed = time.monotonic() - self._stream_start_time
|
||||
self.trendDelta.emit(json.dumps([[elapsed, avg]]))
|
||||
|
||||
def _flush_repaint(self) -> None:
|
||||
if not self._dirty:
|
||||
@@ -843,7 +813,8 @@ class SessionController(QObject):
|
||||
self._sensor_editor.clear()
|
||||
self._reprocess_current()
|
||||
|
||||
def _flush_trend(self) -> None:
|
||||
import json
|
||||
self.trendData.emit(json.dumps(self._trend_buffer))
|
||||
def _reset_live_trend(self) -> None:
|
||||
"""Mark a fresh stream start for elapsed-time trend deltas."""
|
||||
self._stream_start_time = time.monotonic()
|
||||
self.trendReset.emit()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user