Implement row-averaged FFT feature for same-row SNR cleanup

Add row-averaged FFT feature with configurable window size (row_avg_n
parameter) for improved signal-to-noise ratio on noisy scans. Includes:

- Gaussian-weighted same-row neighbor averaging (never crosses rows)
- Masked/renormalized convolution handling for edge cases and masked samples
- Cache format v2 with row_avg_n tracking to prevent silent cache mismatches
- GUI dialog option for row-average window configuration
- Comprehensive tests validating kernel properties, background subtraction
  invariance, and cache dispatch

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-08-07 21:50:05 -05:00
parent eecb0e3a82
commit 3989c2a1b8
10 changed files with 1363 additions and 51 deletions
+14 -5
View File
@@ -187,9 +187,11 @@ class BatchCacheWorker(QObject):
an existing v7 file's cache blocks without disturbing whatever the other
block already holds.
*mode* is ``"dc"`` (CH3/CH4 mean images) or ``"fft"`` (CH1 peak-frequency
*mode* is ``"dc"`` (CH3/CH4 mean images), ``"fft"`` (CH1 peak-frequency
images, unmasked — masking is applied at display time, same as v5's PREC
convention).
convention), or ``"fft_rowavg"`` (same-row, distance-weighted CH1
averaging before the FFT — needs *dc_threshold_mv* and a positive
*row_avg_n*; see ``sras_compute.cache_file``).
Files are processed one per subprocess: they are fully independent, each
opens its own memmap and writes only its own bytes, and only path strings
@@ -201,11 +203,14 @@ class BatchCacheWorker(QObject):
file_done = pyqtSignal(str, str)
finished = pyqtSignal()
def __init__(self, paths: list[str], mode: str, apply_bg_sub: bool):
def __init__(self, paths: list[str], mode: str, apply_bg_sub: bool,
dc_threshold_mv: float | None = None, row_avg_n: int = 0):
super().__init__()
self._paths = paths
self._mode = mode
self._apply_bg_sub = apply_bg_sub
self._dc_threshold = dc_threshold_mv
self._row_avg_n = row_avg_n
def _report(self, path: str, err: str, done: int, total: int):
self.file_done.emit(path, err)
@@ -230,7 +235,9 @@ class BatchCacheWorker(QObject):
with ProcessPoolExecutor(max_workers=n_procs) as executor:
futures = {
executor.submit(cache_file, p, self._mode, self._apply_bg_sub,
compute.get_fft_backend(), per_proc_workers): p
compute.get_fft_backend(), per_proc_workers,
dc_threshold_mv=self._dc_threshold,
row_avg_n=self._row_avg_n): p
for p in paths
}
for fut in as_completed(futures):
@@ -254,7 +261,9 @@ class BatchCacheWorker(QObject):
try:
err = cache_file(path, self._mode, self._apply_bg_sub,
compute.get_fft_backend(),
compute.default_max_workers())
compute.default_max_workers(),
dc_threshold_mv=self._dc_threshold,
row_avg_n=self._row_avg_n)
except Exception as exc:
err = str(exc)
done += 1