pre merge cleanup
This commit is contained in:
+101
-9
@@ -79,11 +79,15 @@ class SrasViewerWindow(QMainWindow):
|
||||
|
||||
# Display-only settings (colormap, grating) never trigger a
|
||||
# recompute — they're applied to cached data on redraw. DC images
|
||||
# (CH3/CH4) are cheap and precomputed for every angle in the
|
||||
# background right after load. CH1/Velocity FFT images are
|
||||
# computed lazily (with a progress popup) the first time an
|
||||
# angle/threshold combination is viewed — using the cached DC4
|
||||
# image to skip the FFT entirely for masked-out pixels — and
|
||||
# (CH3/CH4) are cheap: they come straight from the file's stored
|
||||
# cache when Batch Compute DC has been run for it (see
|
||||
# _stored_dc_image), and are otherwise precomputed for every angle
|
||||
# in the background right after load. CH1/Velocity FFT images come
|
||||
# from the file's own stored cache when Batch Compute FFT has been
|
||||
# run for it (see _stored_fft_image), and are otherwise computed
|
||||
# lazily (with a progress popup) the first time an angle/threshold
|
||||
# combination is viewed — using the cached DC4 image to skip the
|
||||
# FFT entirely for masked-out pixels. Either way the result is
|
||||
# cached per (angle, bg_sub, n_fft, threshold) so revisiting the
|
||||
# same combination is free.
|
||||
self._dc_cache: dict[tuple[int, int], np.ndarray] = {}
|
||||
@@ -219,7 +223,14 @@ class SrasViewerWindow(QMainWindow):
|
||||
self.spin_angle.setRange(0, 0)
|
||||
self.spin_angle.setEnabled(False)
|
||||
self.spin_angle.setMinimumWidth(64)
|
||||
self.spin_angle.editingFinished.connect(self._on_view_changed)
|
||||
# Keyboard tracking off is what makes valueChanged safe to act on: it
|
||||
# stops the signal firing per keystroke, so typing "12" is one change
|
||||
# to angle 12 (on Return or focus-out) rather than a trip to angle 1
|
||||
# first. Arrow clicks and the wheel still emit immediately, which
|
||||
# editingFinished alone did not — that is why stepping the angle used
|
||||
# to leave the plot on the previous one until the box lost focus.
|
||||
self.spin_angle.setKeyboardTracking(False)
|
||||
self.spin_angle.valueChanged.connect(self._on_view_changed)
|
||||
self.lbl_angle_deg = QLabel("—")
|
||||
angle_field = QWidget()
|
||||
ar = QHBoxLayout(angle_field)
|
||||
@@ -598,12 +609,32 @@ class SrasViewerWindow(QMainWindow):
|
||||
n_fft = sum(1 for x in s.precomputed_freq_mhz if x is not None)
|
||||
if n_dc or n_fft:
|
||||
bg_note = " (bg-sub)" if s.precomputed_bg_sub else " (no bg-sub)"
|
||||
pad_note = (f", pad {s.precomputed_pad_factor}x"
|
||||
if n_fft and s.precomputed_pad_factor > 1 else "")
|
||||
notes.append(
|
||||
f"Cached images: DC {n_dc}/{s.n_angles} angles, "
|
||||
f"FFT {n_fft}/{s.n_angles} angles{bg_note if n_fft else ''} "
|
||||
f"FFT {n_fft}/{s.n_angles} angles"
|
||||
f"{bg_note + pad_note if n_fft else ''} "
|
||||
"— display is instant for cached angles")
|
||||
elif s.version == 7:
|
||||
notes.append("v7 format: no cache blocks stored yet")
|
||||
|
||||
# A stored FFT cache the current settings can't read back is the one
|
||||
# failure here with no other symptom: the images are right there in
|
||||
# the file and every angle still recomputes. Say why.
|
||||
if n_fft:
|
||||
reasons = []
|
||||
if s.precomputed_pad_factor != self._fft_pad_factor:
|
||||
reasons.append(f"stored at pad {s.precomputed_pad_factor}x, "
|
||||
f"FFT Options is set to {self._fft_pad_factor}x")
|
||||
if s.precomputed_bg_sub != (self.chk_bg_sub.isChecked()
|
||||
and s.background is not None):
|
||||
reasons.append("stored with bg-sub "
|
||||
f"{'on' if s.precomputed_bg_sub else 'off'}")
|
||||
if reasons:
|
||||
notes.append(f"! Cached FFT unusable as configured ({'; '.join(reasons)})"
|
||||
" — CH1/Velocity will recompute. Re-run Convert -> "
|
||||
"Batch Compute FFT to cache at the current settings.")
|
||||
self.lbl_frame_warn.setText("\n".join(notes))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -656,6 +687,9 @@ class SrasViewerWindow(QMainWindow):
|
||||
# Background subtraction changes the FFT input, so it genuinely
|
||||
# invalidates the cached raw FFT (the cache key includes it) —
|
||||
# _refresh_display() recomputes only on a miss for the new state.
|
||||
# It can also decide whether the file's stored cache is readable, so
|
||||
# the info panel's verdict on that has to be redrawn with it.
|
||||
self._update_scan_info_labels()
|
||||
if self._is_fft_mode():
|
||||
self._refresh_display()
|
||||
|
||||
@@ -838,6 +872,14 @@ class SrasViewerWindow(QMainWindow):
|
||||
return None
|
||||
return self._sras.samples_per_frame * self._fft_pad_factor
|
||||
|
||||
def _cached_fft_pad_factor(self) -> int | None:
|
||||
"""The pad factor the open file's stored FFT images were computed at,
|
||||
or None if it has none stored."""
|
||||
if self._sras is None or not any(x is not None
|
||||
for x in self._sras.precomputed_freq_mhz):
|
||||
return None
|
||||
return self._sras.precomputed_pad_factor
|
||||
|
||||
def _is_fft_mode(self) -> bool:
|
||||
"""Is the selected channel an FFT-derived (CH1/Velocity) mode?"""
|
||||
return (self._sras is not None
|
||||
@@ -877,6 +919,45 @@ class SrasViewerWindow(QMainWindow):
|
||||
self._aligned_cache[key] = cached
|
||||
return cached
|
||||
|
||||
def _stored_fft_image(self, angle_idx: int) -> np.ndarray | None:
|
||||
"""The file's own batch-computed FFT image for this angle, masked for
|
||||
the current threshold — or None if it can't serve the current settings.
|
||||
|
||||
Batch Compute FFT stores a peak-frequency image per angle in the file
|
||||
itself, and the whole point of paying for that once is that display is
|
||||
then instant. Without this check the viewer only ever found the stored
|
||||
image deep inside ComputeWorker, so every angle change after a batch
|
||||
still dispatched a background job behind a "Computing FFT…" popup for
|
||||
an image that was already sitting on disk.
|
||||
"""
|
||||
img = compute.cached_rf_image(
|
||||
self._sras, angle_idx,
|
||||
dc_threshold_mv=self.spin_threshold_mv.value(),
|
||||
apply_bg_sub=self.chk_bg_sub.isChecked(),
|
||||
n_fft=self._current_n_fft(),
|
||||
dc4_mv=self._dc_cache.get((angle_idx, CH4_IDX)),
|
||||
# Masking is a copy plus a comparison, but *reading* CH4 to build
|
||||
# the mask is a whole-channel read — that case belongs on a worker,
|
||||
# so it returns None here and falls through to _start_compute.
|
||||
allow_dc_recompute=False)
|
||||
if img is not None:
|
||||
self._fft_cache[self._fft_cache_key(angle_idx)] = img
|
||||
return img
|
||||
|
||||
def _stored_dc_image(self, angle_idx: int, ch_idx: int) -> np.ndarray | None:
|
||||
"""The file's own batch-computed DC image for (angle, channel), if it
|
||||
has one — the CH3/CH4 half of _stored_fft_image.
|
||||
|
||||
The worker this saves would have handed back the very same stored
|
||||
array (dc_image_mv prefers it over recomputing), so all it ever cost
|
||||
was a thread and a progress popup — but that is exactly what made
|
||||
"Batch Compute DC and Store" look like it had done nothing.
|
||||
"""
|
||||
img = self._sras.cached_dc_mv(angle_idx, ch_idx)
|
||||
if img is not None:
|
||||
self._dc_cache[(angle_idx, ch_idx)] = img
|
||||
return img
|
||||
|
||||
def _refresh_display(self):
|
||||
"""Show the image for the current angle/channel/threshold, using
|
||||
cached data whenever possible and only falling back to a background
|
||||
@@ -888,12 +969,16 @@ class SrasViewerWindow(QMainWindow):
|
||||
|
||||
if ch_idx in CH1_DERIVED_MODES:
|
||||
raw = self._fft_cache.get(self._fft_cache_key(angle_idx))
|
||||
if raw is None:
|
||||
raw = self._stored_fft_image(angle_idx)
|
||||
if raw is not None:
|
||||
self._show_image_now(self._scale_for_display(raw, ch_idx),
|
||||
angle_idx, ch_idx)
|
||||
return
|
||||
else:
|
||||
cached = self._dc_cache.get((angle_idx, ch_idx))
|
||||
if cached is None:
|
||||
cached = self._stored_dc_image(angle_idx, ch_idx)
|
||||
if cached is not None:
|
||||
self._show_image_now(cached, angle_idx, ch_idx)
|
||||
return
|
||||
@@ -1163,7 +1248,11 @@ class SrasViewerWindow(QMainWindow):
|
||||
return
|
||||
|
||||
self._batch_errors = []
|
||||
worker = BatchCacheWorker(paths, mode, self.chk_bg_sub.isChecked())
|
||||
# Cache at the pad factor currently configured, not a fixed pad 1 —
|
||||
# a cache stored at any other padding is one this viewer can never
|
||||
# read back (see _stored_fft_image).
|
||||
worker = BatchCacheWorker(paths, mode, self.chk_bg_sub.isChecked(),
|
||||
self._fft_pad_factor)
|
||||
started = self._run_worker(
|
||||
Jobs.BATCH, worker,
|
||||
connect=(
|
||||
@@ -1338,6 +1427,7 @@ class SrasViewerWindow(QMainWindow):
|
||||
samples_per_frame=self._sras.samples_per_frame if self._sras else None,
|
||||
sample_rate_hz=self._sras.sample_rate_hz if self._sras else None,
|
||||
grating_um=self.spin_grating_um.value(),
|
||||
cached_pad_factor=self._cached_fft_pad_factor(),
|
||||
)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
@@ -1347,7 +1437,9 @@ class SrasViewerWindow(QMainWindow):
|
||||
self._settings.setValue("fft/pad_factor", self._fft_pad_factor)
|
||||
# Pad factor changes the FFT bin count, so it genuinely invalidates
|
||||
# the cached raw FFT (part of the cache key) — _refresh_display()
|
||||
# recomputes only on a cache miss.
|
||||
# recomputes only on a cache miss. It also decides whether the file's
|
||||
# stored cache can be read back at all, which the info panel reports.
|
||||
self._update_scan_info_labels()
|
||||
if self._is_fft_mode():
|
||||
self._refresh_display()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user