Commit Graph

2 Commits

Author SHA1 Message Date
Thomas Ales aa06fa1460 Per-angle background capture: v7/v11 .sras layout
A multi-angle scan runs for hours, but every angle was referenced against
one background captured before the first row of the first angle. That
reference has drifted by the last angle, and comparing angles — the whole
point of a multi-angle scan — was comparing each one against a noise floor
measured at whichever angle came first.

Every angle now captures its own. Before each angle's rows, the operator is
prompted to switch the Genesis laser off, the engine averages a fresh CH1
record, and the operator switches it back on. The data block therefore reads
[background][scan][background][scan] …, one pair per angle.

Format v7 (scan) and v11 (SAW check) carry the background inside the data
block, one length-prefixed block ahead of each angle's rows; the single
block that sat between the preambles and the data is gone. Per-angle offsets
now come from a walk of the data block at parse time rather than arithmetic
over the geometry table, and an angle whose background is not fully on disk
is the frontier — nothing of it was written yet.

v6/v10 files still read: SrasFile hands their one background to every angle,
so readers never branch on the version. Nothing writes them, and a resume
refuses them, since a re-acquired angle writes a block the old layout has no
room for. A resumed v7 angle rewrites its background in place, and the
engine checks the new block fits the room the file has before writing it —
anything else would shift every row behind it.

Two fixes made along the way:

  * QtScanController never accepted file_version, so every scan launched
    from the app raised TypeError at construction.
  * angle_status() left its cursor parked at the frontier, so every angle
    past it reported the frontier's own data_offset — which handed a resumed
    scan the same write position for several angles. Two recorded offsets in
    tests/golden/sras_expected.json are corrected accordingly.

The v6 goldens stay as parser fixtures; the writer is now locked against
bytes the test lays out from scan_format.md itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:44:25 -05:00
Thomas Ales d2734c45d6 Phase 3: viewer reads v6 via mmap; extract analysis core
The viewer could only parse v2-v4 headers while the app has been writing
v6 for some time — it could not open ANY file the current app produces.
It now uses core.sras_format directly (v6 only, per user decision).

New core/sras_analysis.py (Qt-free): ChannelCalibration, image reducers,
and SawPipeline. sras_viewer.py keeps only Qt.

Memory (measured, 92 MB synthetic scan, separate processes):
  old eager path  +305 MB   read()+slice-copy+astype+float32 mean
  new mmap path   + 31 MB   zero-copy view + mean(dtype=)
  -> identical DC image; old scaled at ~3.3x file size, new at image size
- load_angle() returns a read-only mmap view instead of reading the whole
  data block, then copying it twice
- SAW sweeps keep one scalar per pixel (process_shot_metrics) instead of
  retaining 5 full arrays x pixel count in a results list
- CH1 float32 materializes only for pixels passing the DC mask
- matched filter caches the template FFT instead of recomputing per pixel
- opening a new file drops every reference to the old one (compute/
  template/diagnostic workers used to pin the previous multi-GB mapping)

Responsiveness:
- 250 ms debounce coalesces spinbox storms into one recompute
- grating change is a display-time scalar multiply, not a full FFT rerun
- colormap/clim reuse the AxesImage (set_data/set_clim) instead of
  clf() + rebuilding the colorbar; draw_idle() throughout
- SAW diagnostics (21 pipeline runs) and CSV export moved off the GUI thread

Also: ragged per-angle geometry is respected (v6 angles differ in rows/
frames), truncated scans show only rows present on disk, dead decimation
path and v2 fallback branch removed, scipy added to viewer requirements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:55:38 -05:00