Add batch export of DC/RF/Velocity map images
Add Export -> Batch Export Images..., which renders and saves one PNG per angle for whichever of CH1 (RF), CH3 (DC-A), CH4 (DC-B), and Velocity the user selects, for the currently open file. Each channel gets its own fixed min/max colorbar range, entered once in the dialog and held constant across every exported angle, so the resulting images are directly comparable to each other instead of each auto-scaling to its own data (today's live-view default). BatchExportDialog (sras_viewer.py) collects the output folder, file prefix, and per-channel checkbox + range, seeded from whatever's already cached (session DC/FFT caches, or the file's own v7 cache blocks) so opening the dialog never triggers a fresh compute. Export runs on a background thread via a new BatchExportWorker (sras_workers.py), which computes each angle's channel image with the existing dc_image_mv/compute_rf_image helpers (reusing one FFT per angle for both RF and Velocity) and renders it with a headless matplotlib Agg canvas. Also includes several small correctness/robustness fixes that were already staged in the working tree: an int16-vs-float32 accumulator mismatch in sras_average.py's row averaging, a DC4-mask cache reuse and atomic sidecar write in sras_compute.py, a dc3/dc4 pairing guard in sras_format.py's v7 cache writer, and matching updates to the tools/ test fixtures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+24
-8
@@ -523,7 +523,13 @@ class SrasFile:
|
||||
final_freq = new_freq_mhz if new_freq_mhz is not None else self.precomputed_freq_mhz
|
||||
final_bg_sub = new_bg_sub if new_bg_sub is not None else self.precomputed_bg_sub
|
||||
|
||||
dc_entries = [a for a in range(self.n_angles) if final_dc3[a] is not None]
|
||||
# dc3/dc4 are always populated together by every current caller, but
|
||||
# guard the per-angle pairing explicitly rather than assume it: an
|
||||
# angle present in only one of the two arrays would otherwise crash
|
||||
# below on final_dc4[a].astype(...) (or silently store the wrong
|
||||
# dc3/dc4 pairing for that angle).
|
||||
dc_entries = [a for a in range(self.n_angles)
|
||||
if final_dc3[a] is not None and final_dc4[a] is not None]
|
||||
fft_entries = [a for a in range(self.n_angles) if final_freq[a] is not None]
|
||||
|
||||
block_flags = ((CACH_FLAG_DC if dc_entries else 0)
|
||||
@@ -552,13 +558,23 @@ class SrasFile:
|
||||
f.truncate()
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
# Version-byte flip last: if the process dies before this point,
|
||||
# the file is still readable as plain v6 (v6 parsing only
|
||||
# bounds-checks per-angle offset+nbytes <= file_size, it never
|
||||
# asserts exactly how many bytes follow the last angle) — so an
|
||||
# interrupted write can never corrupt the file, only leave
|
||||
# harmless trailing bytes that the next successful write
|
||||
# overwrites via this same deterministic cache offset.
|
||||
# Version-byte flip last: when converting a v6 source (self.version
|
||||
# was 6 on entry), if the process dies before this point the file
|
||||
# is still readable as plain v6 (v6 parsing only bounds-checks
|
||||
# per-angle offset+nbytes <= file_size, it never asserts exactly
|
||||
# how many bytes follow the last angle) — so an interrupted write
|
||||
# can never corrupt the file, only leave harmless trailing bytes
|
||||
# that the next successful write overwrites via this same
|
||||
# deterministic cache offset.
|
||||
#
|
||||
# That guarantee does NOT extend to updating an already-v7 file:
|
||||
# the version byte here is already 7 before this call, so a crash
|
||||
# during the payload write above (before flush/fsync/truncate)
|
||||
# can leave a cache tail that mixes a prefix of the new payload
|
||||
# with a stale suffix of the old one, and this method has no
|
||||
# protection against that case (no atomic rename — the tail is
|
||||
# rewritten in place to avoid copying the, potentially huge,
|
||||
# waveform data that precedes it).
|
||||
f.seek(4)
|
||||
f.write(struct.pack("B", 7))
|
||||
f.flush()
|
||||
|
||||
Reference in New Issue
Block a user