Refactor cache validation and optimize alignment wizard incremental updates

- Extract cache mismatch logic into reusable cache_mismatch_reasons() function
  for cleaner validation and consistent miss reporting
- Expose memory_budget_bytes() for external callers (aligned exporter)
- Optimize rebuild_stack() with incremental layer updates when only one angle
  parameters change, avoiding full reprojection overhead
- Remove unused seed_per_angle parameter from alignment wizard
- Simplify cached_rf_image() DC masking with cleaner early exit logic
- Clean up internal state tracking with explicit origin caching

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-08-09 14:30:02 -05:00
parent 8348ad313c
commit c30c8b1815
8 changed files with 392 additions and 269 deletions
+15 -31
View File
@@ -30,7 +30,8 @@ from .canvases import ImageCanvas, WaveformCanvas
from .common import (
CH1_DERIVED_MODES, CH_LABELS, CMAPS, VELOCITY_MODE_IDX, _CHANNEL_DISPLAY,
_CSS_BUSY, _axes_extent, _CSS_HINT, _CSS_INFO, _CSS_MUTED, _CSS_WARN, _LEFT_PANEL_W,
_RIGHT_PANEL_W, Jobs, _form, _group, _make_dspin, _scroll_panel, _wrap_label,
_RIGHT_PANEL_W, Jobs, _combo, _form, _group, _make_dspin, _scroll_panel,
_wrap_label,
)
from .align_wizard import AlignmentWizard
from .dialogs import FftOptionsDialog, RowAverageFftOptionsDialog
@@ -97,7 +98,6 @@ class SrasViewerWindow(QMainWindow):
# Angle alignment ("Fusion" menu)
self._alignment_result = None
self._alignment_generation: int = 0
self._aligned_cache: dict[tuple, np.ndarray] = {}
self._align_wizard: AlignmentWizard | None = None
@@ -242,14 +242,10 @@ class SrasViewerWindow(QMainWindow):
ar.addStretch()
view_form.addRow("Angle:", angle_field)
self.combo_channel = QComboBox()
self.combo_channel.addItems(CH_LABELS)
self.combo_channel = _combo(CH_LABELS, min_chars=12)
self.combo_channel.setEnabled(False)
self.combo_channel.setSizePolicy(QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Fixed)
self.combo_channel.setSizeAdjustPolicy(
QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
self.combo_channel.setMinimumContentsLength(12)
self.combo_channel.currentIndexChanged.connect(self._on_channel_changed)
view_form.addRow("Channel:", self.combo_channel)
vl.addLayout(view_form)
@@ -632,7 +628,8 @@ class SrasViewerWindow(QMainWindow):
back through a real FFT — worth saying out loud rather than leaving
the user to wonder why a file they batch-computed got slow.
Deliberately mirrors cached_rf_image's accept rule; the display
Asks compute for the reasons rather than restating the accept rule,
so a new provenance field can only be added in one place. The display
always asks for a raw per-pixel image, since row-averaging is a batch
option with no display control.
"""
@@ -640,17 +637,9 @@ class SrasViewerWindow(QMainWindow):
if s is None or all(x is None for x in s.precomputed_freq_mhz):
return []
reasons = []
if compute.pad_factor_for(s, self._current_n_fft()) != s.precomputed_pad_factor:
reasons.append(f"stored at pad {s.precomputed_pad_factor}x, "
f"viewing at pad {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 background subtraction "
f"{'on' if s.precomputed_bg_sub else 'off'}")
if s.precomputed_row_avg_n:
reasons.append(f"stored row-averaged (n={s.precomputed_row_avg_n}), "
"the display shows raw per-pixel FFTs")
reasons = compute.cache_mismatch_reasons(
s, n_fft=self._current_n_fft(),
apply_bg_sub=self.chk_bg_sub.isChecked(), row_avg_n=0)
if not reasons:
return []
return ["! Cached FFT unusable for this view — " + "; ".join(reasons)
@@ -1358,21 +1347,19 @@ class SrasViewerWindow(QMainWindow):
ref_idx = 0
threshold_mv = self.spin_threshold_mv.value()
seed: dict[int, ManualAngleParams] = {}
# Seed only from a previously *saved* alignment, never from
# self._alignment_result: the wizard's first page starts every angle
# pre-rotated from the stage angles and expects to own the parameters
# from there, so inheriting a half-edited in-memory state would make
# "Reset to pre-rotation only" mean something different each time.
# Only the threshold carries over from a saved alignment. The wizard's
# first page starts every angle pre-rotated from the stage angles and
# owns the parameters from there, so inheriting saved per-angle values
# would make "Reset to pre-rotation only" mean something different
# each time.
sidecar = load_manual_alignment(self._sras)
if sidecar is not None and sidecar.ref_angle_idx == ref_idx:
seed = dict(sidecar.per_angle)
threshold_mv = sidecar.dc_threshold_mv
cached_dc4 = {a: img for (a, ch), img in self._dc_cache.items() if ch == CH4_IDX}
wiz = AlignmentWizard(
self, self._sras, ref_angle_idx=ref_idx, dc_threshold_mv=threshold_mv,
seed_per_angle=seed, cached_dc4_mv=cached_dc4)
cached_dc4_mv=cached_dc4)
wiz.alignment_ready.connect(self._on_wizard_finished)
wiz.finished.connect(self._on_wizard_closed)
wiz.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
@@ -1411,15 +1398,12 @@ class SrasViewerWindow(QMainWindow):
if self._current_image is not None:
self._refresh_display()
def _apply_alignment_result(self, result, *, view_checked: bool,
bump_generation: bool = True):
def _apply_alignment_result(self, result, *, view_checked: bool):
"""Install (or clear, with result=None) the active alignment: reset
the aligned-image cache and set the Aligned View checkbox without
firing its change signal."""
self._alignment_result = result
self._aligned_cache = {}
if bump_generation:
self._alignment_generation += 1
with QSignalBlocker(self.chk_aligned_view):
self.chk_aligned_view.setChecked(view_checked)
self.chk_aligned_view.setEnabled(result is not None)