Persist FFT settings, fix backend naming, parallelise batch DC caching

- FFT backend and pad factor persist across sessions via QSettings
  (IniFormat; tests redirect the settings path for hermeticity).
- The default backend was labelled "NumPy FFT" but always dispatched to
  scipy.fft — rename the canonical value to "scipy" ("numpy" stays as a
  legacy alias) and fix the dialog label.
- cache_file: DC caching fans out over angles via _parallel_map with
  per-angle budgets (the DcPrecomputeWorker pattern); FFT caching stays
  serial per angle because compute_rf_image now parallelises internally
  over blocks. Documented that the v7 FFT cache is natural-resolution
  (pad 1) by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-08-06 10:26:50 -05:00
parent 11ff3b62e2
commit 9a9a2557d6
5 changed files with 58 additions and 22 deletions
+22 -9
View File
@@ -25,7 +25,7 @@ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolb
from matplotlib.figure import Figure
from matplotlib.patches import Polygon
from matplotlib.path import Path as MplPath
from PyQt6.QtCore import QObject, Qt, QThread, pyqtSignal
from PyQt6.QtCore import QObject, QSettings, Qt, QThread, pyqtSignal
from PyQt6.QtGui import QAction, QKeyEvent
from PyQt6.QtWidgets import (
QApplication, QButtonGroup, QCheckBox, QComboBox, QDialog, QDialogButtonBox,
@@ -655,22 +655,22 @@ class FftOptionsDialog(QDialog):
grp_backend = QGroupBox("FFT Backend")
bl = QVBoxLayout(grp_backend)
self._btn_numpy = QRadioButton("NumPy FFT (always available)")
self._btn_scipy = QRadioButton("SciPy FFT (pocketfft) (always available)")
self._btn_pyfftw = QRadioButton(
"pyFFTW (faster for large arrays)" if PYFFTW_AVAILABLE
else "pyFFTW (not installed — run: pip install pyfftw)")
self._btn_pyfftw.setEnabled(PYFFTW_AVAILABLE)
self._backend_group = QButtonGroup(self)
self._backend_group.addButton(self._btn_numpy, id=0)
self._backend_group.addButton(self._btn_scipy, id=0)
self._backend_group.addButton(self._btn_pyfftw, id=1)
if current_backend == "pyfftw" and PYFFTW_AVAILABLE:
self._btn_pyfftw.setChecked(True)
else:
self._btn_numpy.setChecked(True)
self._btn_scipy.setChecked(True)
bl.addWidget(self._btn_numpy)
bl.addWidget(self._btn_scipy)
bl.addWidget(self._btn_pyfftw)
layout.addWidget(grp_backend)
@@ -737,7 +737,7 @@ class FftOptionsDialog(QDialog):
f"(at grating = {self._grating_um:.2f} µm)")
def get_backend(self) -> str:
return "pyfftw" if self._btn_pyfftw.isChecked() and PYFFTW_AVAILABLE else "numpy"
return "pyfftw" if self._btn_pyfftw.isChecked() and PYFFTW_AVAILABLE else "scipy"
def get_pad_factor(self) -> int:
return max(1, self._spin_pad.value())
@@ -1451,8 +1451,18 @@ class SrasViewerWindow(QMainWindow):
self._jobs: dict[str, tuple] = {}
self._progress_dlgs: dict[str, QProgressDialog] = {}
# FFT settings (configured via FFT Options dialog)
self._fft_pad_factor: int = 1 # 1 = no padding
# FFT settings (configured via FFT Options dialog, persisted across
# sessions). IniFormat: predictable cross-platform and redirectable
# in tests.
self._settings = QSettings(QSettings.Format.IniFormat,
QSettings.Scope.UserScope,
"sras-viewer", "sras-viewer")
compute.set_fft_backend(str(self._settings.value("fft/backend", "scipy")))
try:
pad = int(self._settings.value("fft/pad_factor", 1))
except (TypeError, ValueError):
pad = 1
self._fft_pad_factor: int = max(1, min(256, pad)) # 1 = no padding
# Convert menu: batch DC/FFT compute-and-store (v6 -> v7)
self._batch_errors: list[str] = []
@@ -1846,7 +1856,8 @@ class SrasViewerWindow(QMainWindow):
self._batch_fft_act = QAction("Batch Compute FFT and Sto&re…", self)
self._batch_fft_act.setStatusTip(
"Select .sras files and compute+store FFT peak-frequency images "
"for every angle, converting v6 files to v7 in place.")
"for every angle, converting v6 files to v7 in place. Stored "
"images are natural-resolution (pad 1); padded views compute live.")
self._batch_fft_act.triggered.connect(lambda: self._on_batch_compute("fft"))
convert_menu.addAction(self._batch_fft_act)
@@ -2748,6 +2759,8 @@ class SrasViewerWindow(QMainWindow):
return
compute.set_fft_backend(dlg.get_backend())
self._fft_pad_factor = dlg.get_pad_factor()
self._settings.setValue("fft/backend", compute.get_fft_backend())
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.