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
+24 -9
View File
@@ -35,15 +35,17 @@ try:
except ImportError:
threadpool_limits = None
_fft_backend = "numpy" # "numpy" (scipy.fft) or "pyfftw"; set via set_fft_backend()
_fft_backend = "scipy" # "scipy" or "pyfftw"; set via set_fft_backend()
def set_fft_backend(name: str):
"""Select the rfft implementation. Module-level state, so it must be set
explicitly inside each multiprocessing child — it does not survive a
spawn."""
spawn. "numpy" is accepted as a legacy alias for "scipy"."""
global _fft_backend
_fft_backend = name if (name != "pyfftw" or PYFFTW_AVAILABLE) else "numpy"
if name == "numpy":
name = "scipy"
_fft_backend = name if (name == "pyfftw" and PYFFTW_AVAILABLE) else "scipy"
def get_fft_backend() -> str:
@@ -599,13 +601,17 @@ def compute_rf_image(sras: SrasFile, angle_idx: int,
# ---------------------------------------------------------------------------
def cache_file(path: str, mode: str, apply_bg_sub: bool,
fft_backend: str = "numpy", max_workers: int = 0) -> str:
fft_backend: str = "scipy", max_workers: int = 0) -> str:
"""Compute and store DC or FFT images for every angle of one file,
converting v6 → v7 in place. Returns "" on success or an error message.
Module-level and picklable so it can run in a ProcessPoolExecutor. The
FFT backend and worker cap are passed explicitly because module globals
do not survive a spawn.
The stored FFT cache is always natural-resolution (pad 1): the v7 SFFT
block records no pad factor, and padded views compute live fast enough
(see _peak_bins_zoom) that caching them is not worth a format change.
"""
global _MAX_WORKERS
try:
@@ -621,17 +627,26 @@ def cache_file(path: str, mode: str, apply_bg_sub: bool,
"can be batch-cached")
n = sras.n_angles
n_workers, angle_budget = plan_angle_level(sras)
if mode == "dc":
dc3 = [adc_to_mv(compute_dc_image(sras, a, CH3_IDX), *sras.cal(CH3_IDX))
for a in range(n)]
dc4 = [adc_to_mv(compute_dc_image(sras, a, CH4_IDX), *sras.cal(CH4_IDX))
for a in range(n)]
dc3 = _parallel_map(
lambda a: adc_to_mv(
compute_dc_image(sras, a, CH3_IDX, max_workers=1,
budget=angle_budget), *sras.cal(CH3_IDX)),
range(n), n_workers)
dc4 = _parallel_map(
lambda a: adc_to_mv(
compute_dc_image(sras, a, CH4_IDX, max_workers=1,
budget=angle_budget), *sras.cal(CH4_IDX)),
range(n), n_workers)
sras.write_v7_cache(new_dc3_mv=dc3, new_dc4_mv=dc4)
else:
effective_bg = apply_bg_sub and sras.background is not None
# dc_threshold_mv=None: store unmasked images and mask at display
# time (same convention as v5's PREC block). Skipping the mask
# also skips reading CH4 entirely.
# also skips reading CH4 entirely. The FFT path parallelises
# internally over blocks, so angles run one at a time with the
# full budget.
freq = [compute_rf_image(sras, a, dc_threshold_mv=None,
apply_bg_sub=effective_bg)
for a in range(n)]