Add min peak frequency floor (CACH v4) and Batch Export View as Images
Two strands of in-progress work, committed together because they overlap in sras_workers.py and main_window.py. Min peak frequency floor: - CACH tail bumped to version 4, adding u32 min_freq_khz provenance in fixed-point kHz (a float32 20.1 reads back as 20.10000038 and would report a spurious mismatch forever). v1-v3 tails read as no floor. - Stored FFT caches are accepted when the reader's floor is at or above the stored one, since a higher floor is re-applicable by masking. - Floor plumbed through compute_rf_image, BatchCacheWorker and the viewer. Batch Export View as Images: - New sras_render.py holds draw_view_image, shared by the Qt canvas and the headless exporter so a PNG cannot drift from what the GUI shows. Deliberately Qt-free so it is importable in a pool subprocess. - BatchExportImagesWorker renders the current view settings across many files, process-pooled with an inline fallback, reporting per-file output names so the caller can flag same-stem collisions. - _axes_extent extracted into sras_format for both render paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+31
-13
@@ -255,7 +255,7 @@ actions.
|
||||
| Offset | Size | Type | Field | Description |
|
||||
|--------|------|------|-------|-------------|
|
||||
| 0 | 4 | `char[4]` | `cach_magic` | `CACH` (ASCII). Missing/wrong magic → treat file as having no cache. |
|
||||
| 4 | 1 | `u8` | `cach_version` | Cache format version. Currently `3`; readers also accept `1` and `2` (each older tail simply lacks the fields added since — see the `SFFT` block below and [CACH tail version history](#cach-tail-version-history)). Any other value → treat the file as uncached (unlike v5's `PREC` section, which read but never validated its version byte). |
|
||||
| 4 | 1 | `u8` | `cach_version` | Cache format version. Currently `4`; readers also accept `1`–`3` (each older tail simply lacks the fields added since — see the `SFFT` block below and [CACH tail version history](#cach-tail-version-history)). Any other value → treat the file as uncached (unlike v5's `PREC` section, which read but never validated its version byte). |
|
||||
| 5 | 1 | `u8` | `block_flags` | Bit 0 = DC block (`SDCB`) follows. Bit 1 = FFT block (`SFFT`) follows, immediately after the DC block if both are present. Bits 2–7 reserved, must be zero on write. |
|
||||
|
||||
### DC block `SDCB` (present iff `block_flags & 0x01`)
|
||||
@@ -292,13 +292,16 @@ never shifts an existing offset:
|
||||
- **`cach_version` 1**: 7 bytes, format `">4sBH"` — magic, flags, n_stored.
|
||||
- **`cach_version` 2**: 8 bytes, format `">4sBHB"` — + `row_avg_n`.
|
||||
- **`cach_version` 3**: 10 bytes, format `">4sBHBH"` — + `pad_factor`.
|
||||
- **`cach_version` 4**: 14 bytes, format `">4sBHBHI"` — + `min_freq_khz`.
|
||||
Always written by current code.
|
||||
|
||||
An older tail is read with its absent fields taken as the only value such a
|
||||
tail can describe: `row_avg_n = 0` for a `cach_version` 1 tail, which
|
||||
predates row-averaged FFT caching, and `pad_factor = 1` for `cach_version`
|
||||
1 or 2, which predate padded caching and are therefore natural-resolution.
|
||||
Files cached before either change keep working with no recompute.
|
||||
predates row-averaged FFT caching, `pad_factor = 1` for `cach_version`
|
||||
1 or 2, which predate padded caching and are therefore natural-resolution,
|
||||
and `min_freq_khz = 0` (no floor) for `cach_version` 1–3, which predate the
|
||||
min peak frequency floor and therefore searched every bin above DC.
|
||||
Files cached before any of these changes keep working with no recompute.
|
||||
|
||||
| Offset (rel) | Size | Type | Field | Description |
|
||||
|--------------|------|------|-------|-------------|
|
||||
@@ -306,7 +309,8 @@ Files cached before either change keep working with no recompute.
|
||||
| 4 | 1 | `u8` | `flags` | Bit 0 = `bg_sub_applied` — background waveform was subtracted from CH1 before the FFT when these images were computed. Bit 1 = `row_averaged` — `peak_freq_mhz` came from same-row, distance-weighted averaged CH1 waveforms rather than raw per-pixel ones; `row_avg_n` (below) is the neighbor half-width used. Bits 2–7 reserved. |
|
||||
| 5 | 2 | `u16` | `n_stored` | Number of angle entries that follow |
|
||||
| 7 | 1 | `u8` | `row_avg_n` | *`cach_version` ≥ 2 only.* Same-row neighbor half-width, in pixels, that `peak_freq_mhz` was averaged over before its FFT; `0` = raw (unaveraged). Meaningful only when `flags` bit 1 is set — a `cach_version` 1 tail has no such byte and is always `row_avg_n = 0`. |
|
||||
| 8 | 2 | `u16` | `pad_factor` | *`cach_version` 3 only.* Zero-padding factor the stored `peak_freq_mhz` was resolved at: `n_fft = pad_factor × samples_per_frame`, so `1` = natural resolution. Never `0`; a `cach_version` 1 or 2 tail has no such field and is always `pad_factor = 1`. |
|
||||
| 8 | 2 | `u16` | `pad_factor` | *`cach_version` ≥ 3 only.* Zero-padding factor the stored `peak_freq_mhz` was resolved at: `n_fft = pad_factor × samples_per_frame`, so `1` = natural resolution. Never `0`; a `cach_version` 1 or 2 tail has no such field and is always `pad_factor = 1`. |
|
||||
| 10 | 4 | `u32` | `min_freq_khz` | *`cach_version` ≥ 4 only.* Min peak frequency floor the stored peak search excluded bins below, fixed-point in units of 0.001 MHz (kHz); `0` = no floor. Fixed-point rather than `f32` so a value that round-trips through the file compares exactly against the same value re-requested by a reader (the viewer's floor control has 0.001 MHz granularity). A `cach_version` 1–3 tail has no such field and is always `min_freq_khz = 0`. |
|
||||
|
||||
followed by `n_stored` entries, each:
|
||||
|
||||
@@ -341,14 +345,27 @@ Readers must fall back to real-time FFT computation (ignoring stored
|
||||
the reader is asking for: time-domain gating is active, the reader's
|
||||
requested `n_fft` doesn't equal `pad_factor × samples_per_frame`, the
|
||||
reader's background-subtraction setting doesn't match
|
||||
`flags.bg_sub_applied`, or the reader's requested `row_avg_n` doesn't match
|
||||
the stored value exactly. A raw request must never be served a row-averaged
|
||||
store, or vice versa; a request at one row-averaging window size must never
|
||||
be served a store at another; and a request at one padding must never be
|
||||
served a store at another, since a padded FFT interpolates between the
|
||||
natural bins and so resolves genuinely different peak frequencies. An
|
||||
`n_fft` that is not a whole multiple of `samples_per_frame` can never match
|
||||
any store, because only an integer `pad_factor` is representable.
|
||||
`flags.bg_sub_applied`, the reader's requested `row_avg_n` doesn't match
|
||||
the stored value exactly, or the reader's requested min peak frequency
|
||||
floor is *below* the stored `min_freq_khz`. A raw request must never be
|
||||
served a row-averaged store, or vice versa; a request at one row-averaging
|
||||
window size must never be served a store at another; and a request at one
|
||||
padding must never be served a store at another, since a padded FFT
|
||||
interpolates between the natural bins and so resolves genuinely different
|
||||
peak frequencies. An `n_fft` that is not a whole multiple of
|
||||
`samples_per_frame` can never match any store, because only an integer
|
||||
`pad_factor` is representable.
|
||||
|
||||
The min peak frequency floor is the one asymmetric provenance field. A
|
||||
request at a floor *below* the stored one cannot be served: the stored
|
||||
search never looked at bins below its floor, so the stored numbers cannot
|
||||
say what a lower-floored search would have found. A request at a floor at
|
||||
or *above* the stored one **is** servable — the difference is re-applied at
|
||||
display time by masking every pixel whose stored `peak_freq_mhz` is below
|
||||
the requested floor to `0` (the same sentinel as the DC threshold mask;
|
||||
a genuine peak can never be `0`, since bin 0 is always excluded from the
|
||||
search). Such masked pixels are *invalid*, not re-resolved — only a real
|
||||
recompute can recover the strongest peak above the floor for them.
|
||||
|
||||
### In-place write ordering
|
||||
|
||||
@@ -374,6 +391,7 @@ which has stayed `7` since the Cache Tail was introduced — this is the inner
|
||||
| 1 | Initial Cache Tail: `SDCB` (DC) and `SFFT` (FFT, 7-byte header) blocks. |
|
||||
| 2 | `SFFT` header grows one byte, `row_avg_n` — the same-row neighbor half-width the stored `peak_freq_mhz` was averaged over before its FFT, `0` = raw. Readers still accept a `cach_version` 1 tail, treated as `row_avg_n = 0` for every angle it stores, so files cached before this change keep working without a recompute. |
|
||||
| 3 | `SFFT` header grows a `u16` `pad_factor` — the zero-padding factor the stored `peak_freq_mhz` was resolved at, `1` = natural resolution. Before this, a padded view could never use a stored cache at all (the store was pad 1 by definition and readers rejected any `n_fft ≠ samples_per_frame`), so a user working at a pad factor got no benefit from batch-computing a file. Recording the factor lets such a view be served, while still refusing a store resolved at a *different* pad. Readers accept `cach_version` 1 and 2 tails as `pad_factor = 1`. |
|
||||
| 4 | `SFFT` header grows a `u32` `min_freq_khz` — the min peak frequency floor the stored peak search excluded bins below, in 0.001 MHz units, `0` = no floor. The floor exists because a pixel that passes the DC-bias threshold but carries only weak real signal can otherwise resolve to the un-subtracted background's DC-leakage skirt — an implausibly-near-zero frequency (and so an implausibly slow velocity) for a pixel that has a genuine peak higher up. Recording the floor is what makes it enforceable against a store: without it, a stored image silently bypassed the floor entirely. Unlike the other provenance fields it is asymmetric — a *higher* requested floor is servable by masking stored pixels below it, only a *lower* one forces a recompute (see above). Readers accept `cach_version` 1–3 tails as `min_freq_khz = 0`. |
|
||||
|
||||
A reader that does not know a `cach_version` must treat the file as
|
||||
uncached — not attempt a partial parse — and the file still reads as an
|
||||
|
||||
Reference in New Issue
Block a user