Implement FFT pad-factor caching and the stored-cache display fast path
tests/test_stored_cache.py exercised two features that were never built, so six of its tests had been failing on main. Both are now implemented. Pad-factor caching. A padded view could not use a stored FFT cache at all: the store was pad 1 by definition and cached_rf_image rejected any n_fft outright, so a user working at a pad factor got nothing from batch-computing a file. CACH tail v3 records the pad the images were resolved at, cache_file computes at a requested pad, and the batch actions pass the viewer's own pad down — while still refusing a store resolved at a different pad, since a padded FFT interpolates between the natural bins and so resolves genuinely different peak frequencies. v1/v2 tails read as pad 1 and keep working. Stored-cache dispatch. _refresh_display only ever consulted this window's in-session dicts, so after a batch every angle change still queued a worker and a progress popup for an image already on disk — the exact cost the batch was run to avoid. It now checks the file's own DC/FFT blocks first, asking with allow_dc_recompute=False so the GUI thread never touches I/O. When the stored cache genuinely cannot serve the view, the scan info panel says why rather than leaving the silent recompute a mystery. Two bugs surfaced on the way: - The angle spinbox was wired on editingFinished, which QAbstractSpinBox emits only on Return or focus-out — never on a step. Clicking its arrows, the ordinary way to walk a scan, moved the number and left the image behind. Now valueChanged with keyboard tracking off, which fires on a step and once on commit, but not per keystroke mid-typing. Every existing GUI test called _on_view_changed() by hand and so could not have caught this; two new tests pin it and fail against the old wiring. - A plain "fft" batch over a file previously cached with fft_rowavg carried the old row_avg_n forward, labelling raw images as row-averaged. It now writes row_avg_n=0 explicitly. Verified: 111 passed (was 103 passed / 6 failed), and tools/check_equivalence.py is byte-identical to the pre-change baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,52 @@ at one window size can never be served a cache at another — see
|
||||
`scan_format.md`'s Cache Tail / CACH tail version history sections for the
|
||||
on-disk `row_avg_n` field this depends on.
|
||||
|
||||
## Serving a stored cache: provenance, not just presence
|
||||
|
||||
A stored `peak_freq_mhz` image is only interchangeable with a live compute
|
||||
for the *exact* settings it was computed under. Three of them are baked
|
||||
irreversibly into the numbers — background subtraction, row-averaging window,
|
||||
and zero-padding — so all three are recorded in the `SFFT` block and checked
|
||||
by `cached_rf_image` before it hands the image back. Getting this wrong is
|
||||
not a slow display, it is a *wrong* display, which is why the check is a
|
||||
single predicate in one place rather than spread across callers.
|
||||
|
||||
Padding is the subtlest of the three, because a padded FFT looks like it
|
||||
should be a refinement of the unpadded one. It isn't: zero-padding
|
||||
interpolates between the natural bins, so it resolves a different peak
|
||||
frequency for the same waveform. `precomputed_pad_factor` exists so a padded
|
||||
view can be served from a cache computed at *its* pad while still refusing
|
||||
one computed at any other, including pad 1. Before it existed the store was
|
||||
pad 1 by definition and any `n_fft` was rejected outright — correct, but it
|
||||
meant a user working at a pad factor got nothing at all from batch-computing
|
||||
a file, which is most of the point of the feature. `pad_factor_for` maps an
|
||||
`n_fft` request onto the integer factor a store could have recorded, and
|
||||
returns 0 for a request that is not a whole multiple of `samples_per_frame`
|
||||
— unmatchable by construction, since only integer factors are representable.
|
||||
|
||||
The batch actions therefore have to cache at the *viewer's* current pad
|
||||
factor, not a fixed one: a cache stored at a pad nobody is viewing at is
|
||||
dead weight. When the two do diverge (the user changes the pad after
|
||||
batching), `_cache_mismatch_notes` says so in the scan info panel, because
|
||||
the symptom otherwise is just "the file I pre-computed got slow again" with
|
||||
no visible cause.
|
||||
|
||||
### Two caches, in cost order
|
||||
|
||||
`_refresh_display` consults this window's in-session `_fft_cache`/`_dc_cache`
|
||||
dicts first, then the open file's stored v5/v7 blocks, and only then
|
||||
dispatches a `ComputeWorker`. The second tier is what makes a batch-computed
|
||||
file worth having; without it every angle change queued a worker and a
|
||||
progress popup for an image already sitting on disk — precisely the cost the
|
||||
batch was run to avoid. (The stored image *was* reachable before, but only
|
||||
from inside `ComputeWorker`, i.e. after paying for the thread and the popup.)
|
||||
|
||||
The file tier asks with `allow_dc_recompute=False`. If applying the DC4 mask
|
||||
would mean reading a whole CH4 channel, it declines rather than blocking the
|
||||
GUI thread, and the fall-through worker reaches the same stored image via
|
||||
`compute_rf_image` and pays for the mask off-thread. So the GUI thread never
|
||||
does I/O, and the slow path is still a fast path.
|
||||
|
||||
## Angle alignment coordinate frames (`sras_compute.py`)
|
||||
|
||||
Alignment puts every angle's images onto one shared, zero-padded pixel grid
|
||||
|
||||
Reference in New Issue
Block a user