Add v7 format with on-disk DC/FFT cache, replace v5 pre-process with Convert menu
Scans come off the scope as v6; a new trailing CACH section (independent SDCB/SFFT blocks, sized per-angle from the existing geometry table) lets computed DC and FFT images be cached in place, bumping the file to v7 the first time either is stored. The Convert menu's "Batch Compute DC and Store"/"Batch Compute FFT and Store" actions run this across multiple files in the background. compute_rf_image and the DC compute workers now reuse cached v7 data instead of recomputing it. Removes "Pre-process and Save as v5", which never supported v6 sources and is superseded by in-place v7 caching. Legacy v2-v5 reading, including v5's PREC section, is unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+117
-2
@@ -1,4 +1,4 @@
|
||||
# SRAS Scan Binary Format — Version 6
|
||||
# SRAS Scan Binary Format — Version 6 / 7
|
||||
|
||||
Each `.sras` file contains **one complete scan**: all GR rotation angles and all
|
||||
Y rows. Files are named `{prefix}.sras`.
|
||||
@@ -10,6 +10,14 @@ rotated by that specific angle** — not the worst case across all angles — so
|
||||
fewer rows than a 45° scan of the same ROI, and the file format reflects that
|
||||
instead of forcing every angle to the largest bounding box.
|
||||
|
||||
v7 is byte-identical to v6 (same header, angle table, geometry table, row
|
||||
table, preamble blocks, background block, waveform data) plus an optional
|
||||
trailing **Cache Tail** holding precomputed per-angle DC and/or FFT images
|
||||
(see [Cache Tail (v7)](#cache-tail-v7) below) — only the header's `version`
|
||||
field and the presence of that trailing section differ. Scans come off the
|
||||
scope as v6; `sras_viewer.py`'s "Convert" menu batch actions convert a file
|
||||
to v7 **in place** the first time either cache block is computed and stored.
|
||||
|
||||
---
|
||||
|
||||
## File Layout
|
||||
@@ -22,6 +30,7 @@ instead of forcing every angle to the largest bounding box.
|
||||
[Preamble Blocks — n_channels × (uint16 length + UTF-8 WFMOutpre string)]
|
||||
[Background Block — uint32 n_bg_samples + n_bg_samples × int8 bytes]
|
||||
[Waveform Data (ragged) — per angle: n_rows[a] × n_channels × n_frames[a] × samples_per_frame × bps bytes]
|
||||
[Cache Tail (optional) — "CACH" + DC block (optional) + FFT block (optional); v7 only]
|
||||
```
|
||||
|
||||
All multi-byte integers and floats use **big-endian** byte order
|
||||
@@ -182,6 +191,111 @@ using that angle's `x_start` from the Per-Angle Geometry Table (not
|
||||
|
||||
---
|
||||
|
||||
## Cache Tail (v7)
|
||||
|
||||
Present iff `version == 7` and `file_size > cache_offset`, where:
|
||||
|
||||
```
|
||||
cache_offset = data_offset + Σ over angles a of:
|
||||
n_rows[a] × n_channels × n_frames[a] × samples_per_frame × bytes_per_sample
|
||||
```
|
||||
|
||||
i.e. exactly `data_offset + waveform_bytes` — the same "Total data size"
|
||||
formula as Waveform Data above. This offset is derivable from the header and
|
||||
Per-Angle Geometry Table alone and does **not** depend on which cache
|
||||
block(s) are present, so a writer can always seek straight there without
|
||||
reading or touching any waveform byte before it.
|
||||
|
||||
Unlike the (removed) v5 `PREC` section, which stored one omnibus per-angle
|
||||
entry (FFT + both DC channels together) in a dense, uniform-geometry array,
|
||||
the v7 Cache Tail splits DC and FFT into two **independent** sub-blocks —
|
||||
each sized per-angle from the Per-Angle Geometry Table, each independently
|
||||
present, and each independently updatable in any order, any number of
|
||||
times, without disturbing the other. This matches `sras_viewer.py`'s
|
||||
"Convert" menu, which exposes DC and FFT store as two separate batch
|
||||
actions.
|
||||
|
||||
### CACH outer header (6 bytes, `">4sBB"`)
|
||||
|
||||
| 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 `1`. Readers must treat the file as uncached if this is not a version they understand (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`)
|
||||
|
||||
7-byte block header, format `">4sBH"`:
|
||||
|
||||
| Offset (rel) | Size | Type | Field | Description |
|
||||
|--------------|------|------|-------|-------------|
|
||||
| 0 | 4 | `char[4]` | `magic` | `SDCB` |
|
||||
| 4 | 1 | `u8` | `reserved` | `0`, reserved for future use |
|
||||
| 5 | 2 | `u16` | `n_stored` | Number of angle entries that follow, `0 ≤ n_stored ≤ n_angles` |
|
||||
|
||||
followed by `n_stored` entries, each:
|
||||
|
||||
```
|
||||
u16 angle_idx — index into the angle table (0-based)
|
||||
f32[n_rows[angle_idx] × n_frames[angle_idx]] dc3_mv — CH3 waveform mean, mV, row-major
|
||||
f32[n_rows[angle_idx] × n_frames[angle_idx]] dc4_mv — CH4 waveform mean, mV, row-major
|
||||
```
|
||||
|
||||
Entries may appear in any order and need not be contiguous from angle 0 —
|
||||
this supports storing (or re-storing) a subset of angles, or an
|
||||
interrupted batch run leaving only some angles cached. Readers bounds-check
|
||||
`angle_idx < n_angles` on each entry and stop parsing on an out-of-range
|
||||
value, same as v5's `PREC` section.
|
||||
|
||||
### FFT block `SFFT` (present iff `block_flags & 0x02`)
|
||||
|
||||
7-byte block header, format `">4sBH"`:
|
||||
|
||||
| Offset (rel) | Size | Type | Field | Description |
|
||||
|--------------|------|------|-------|-------------|
|
||||
| 0 | 4 | `char[4]` | `magic` | `SFFT` |
|
||||
| 4 | 1 | `u8` | `flags` | Bit 0 = `bg_sub_applied` — background waveform was subtracted from CH1 before the FFT when these images were computed. Bits 1–7 reserved. |
|
||||
| 5 | 2 | `u16` | `n_stored` | Number of angle entries that follow |
|
||||
|
||||
followed by `n_stored` entries, each:
|
||||
|
||||
```
|
||||
u16 angle_idx — index into the angle table (0-based)
|
||||
f32[n_rows[angle_idx] × n_frames[angle_idx]] peak_freq_mhz — CH1 FFT peak frequency, MHz, row-major
|
||||
```
|
||||
|
||||
**`peak_freq_mhz`** is computed without any DC-threshold masking (i.e. the
|
||||
FFT is run on every pixel unconditionally, same as v5's `PREC` convention).
|
||||
Readers apply the DC4 threshold at display time:
|
||||
|
||||
```
|
||||
pixel is valid ⟺ dc4_mv[r][f] ≥ threshold_mv
|
||||
display_value = peak_freq_mhz[r][f] if valid, else 0
|
||||
```
|
||||
|
||||
using the DC4 image from the DC block if that angle is also cached there,
|
||||
else computed on demand.
|
||||
|
||||
Readers must fall back to real-time FFT computation (ignoring stored
|
||||
`peak_freq_mhz`) under the same conditions as v5's PREC fast path: time-domain
|
||||
gating is active, zero-padding (`n_fft ≠ samples_per_frame`) is requested, or
|
||||
the reader's background-subtraction setting doesn't match `flags.bg_sub_applied`.
|
||||
|
||||
### In-place write ordering
|
||||
|
||||
A writer updating a file's Cache Tail must write the payload (CACH header +
|
||||
whichever block(s) are present) **before** flipping the header's `version`
|
||||
byte to `7`, and `truncate()` the file to the new payload's end immediately
|
||||
after writing it. If the process is interrupted between the payload write
|
||||
and the version-byte flip, the file is still valid v6 — v6 parsing only
|
||||
bounds-checks each angle's `offset + nbytes ≤ file_size`, it never asserts
|
||||
exactly how many bytes follow the last angle's waveform block — so the
|
||||
interrupted write leaves harmless trailing bytes rather than a corrupt file,
|
||||
and the next successful write overwrites them via the same deterministic
|
||||
`cache_offset`.
|
||||
|
||||
---
|
||||
|
||||
## Acquisition Settings (fixed by sc3_aui_app.py)
|
||||
|
||||
| Parameter | Value |
|
||||
@@ -206,5 +320,6 @@ using that angle's `x_start` from the Per-Angle Geometry Table (not
|
||||
| 3 | Added preamble blocks (WFMOutpre strings) after the row table, one length-prefixed UTF-8 block per channel. |
|
||||
| 4 | Added background waveform block (CH1, Helios ON / Genesis OFF) after the preamble blocks; stored as `uint32` sample count followed by raw `int8` ADC bytes. |
|
||||
| 5 | (skipped) |
|
||||
| 6 | Each angle now scans only the bounding box of the nominal ROI rotated by that angle instead of the AABB-expanded worst case across all angles. Header no longer carries a single global `x_start`/`x_delta`/`n_rows` — replaced with `*_nominal` reference fields plus a new Per-Angle Geometry Table (`x_start`, `x_delta`, `n_frames`, `n_rows` per angle) and a ragged Row Table / Waveform Data block sized per angle. **Not compatible with pre-v6 readers** that assume uniform geometry. `sras_viewer.py` reads v6 natively (per-angle `n_rows`/`n_frames`/`x_start`); the "Pre-process and Save as v5" fast-path export is not offered for v6 files since the flat v5 layout cannot represent per-angle geometry. |
|
||||
| 6 | Each angle now scans only the bounding box of the nominal ROI rotated by that angle instead of the AABB-expanded worst case across all angles. Header no longer carries a single global `x_start`/`x_delta`/`n_rows` — replaced with `*_nominal` reference fields plus a new Per-Angle Geometry Table (`x_start`, `x_delta`, `n_frames`, `n_rows` per angle) and a ragged Row Table / Waveform Data block sized per angle. **Not compatible with pre-v6 readers** that assume uniform geometry. `sras_viewer.py` reads v6 natively (per-angle `n_rows`/`n_frames`/`x_start`). |
|
||||
| 7 | Adds an optional trailing **Cache Tail** (`CACH` section, see [Cache Tail (v7)](#cache-tail-v7)) after the ragged waveform data, holding independently-present, independently-updatable per-angle DC (`SDCB`: dc3_mv + dc4_mv) and FFT (`SFFT`: peak_freq_mhz) blocks, so previously-computed images redisplay instantly instead of being recomputed. Header / angle table / geometry table / row table / preamble blocks / background block / waveform data are byte-identical to v6 — only the version byte and the optional Cache Tail differ. Scans still come off the scope as v6; `sras_viewer.py`'s "Convert" menu ("Batch Compute DC and Store" / "Batch Compute FFT and Store") converts a file to v7 **in place** on first use, or updates an existing v7 file's cache blocks, without rewriting any waveform bytes. Supersedes the removed "Pre-process and Save as v5" workflow, which was never available for v6 sources since the flat v5 `PREC` layout can't represent per-angle geometry. |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user