Commit Graph

4 Commits

Author SHA1 Message Date
Thomas Ales f40c965b74 Replace the two Fusion alignment actions with a three-step wizard
Alignment was two disconnected menu actions. `Angle Alignment` ran the
registration with ref_angle_idx hard-coded to 0, no exposed parameters and
no way to retry; `Manual Alignment...` was a separate dialog that
deliberately refused to inherit the automatic result, so a bad fit meant
starting over by hand. Neither told you whether the alignment was any
good, and neither produced anything durable beyond an in-memory result.

Fusion -> Alignment Wizard... now covers all of it in three steps:

  1. Correlate. Reference angle, DC threshold, source, rotation seed and
     sign, search window, coarse step and fine grid are all on the page,
     and Run/Re-run is repeatable. Angles start pre-rotated from the
     stage angles in the file, so the page is informative before any
     correlation runs. The verdict is a picture: every angle's DC mask
     reprojected onto the shared canvas and summed, coloured by how many
     angles cover each pixel, so a good alignment reads as one saturated
     plateau and a bad one as a fringe of low-count halos. A per-angle
     fit table flags angles that did not register or that disagree with
     their stage angle by more than a degree.
  2. Crop. An axis-aligned rectangle on that canvas, with numeric
     canvas-pixel boxes synced both ways and a live size estimate.
     "Fit to full overlap" uses a largest-rectangle sweep rather than a
     bounding box: the overlap region of several rotated scans is roughly
     a disc, whose bounding box has corners no angle covers.
  3. Save. Writes the aligned, cropped stack to a new .sras.

Because the wizard replaces both actions it absorbs the old dialog's
by-eye nudge editor — without it, a scan the search cannot fit would
have no fallback at all. ManualAlignmentDialog is therefore deleted
rather than left orphaned, and its tests move to the wizard.

Two bugs found while driving it end to end and fixed here: QSpinBox
setRange clamps and emits valueChanged, which committed a 1x1 crop
before the default preset could run; and the mm round trip returns an
exact pixel boundary as 11.000000000000002, so a bare ceil() added a
spurious column on every rectangle edit.

Verified: 103 pass, the 6 test_stored_cache failures are pre-existing on
main, and tools/check_equivalence.py is byte-identical to main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:00:59 -05:00
Thomas Ales 26a34f7436 Split sras_viewer.py into a package (pure move)
The 2790-line module becomes sras_viewer/: common.py (constants +
layout helpers), canvases.py (RoiQuad, ImageCanvas, WaveformCanvas,
ManualAlignOverlayCanvas), dialogs.py (FftOptionsDialog,
ManualAlignmentDialog), main_window.py (SrasViewerWindow + main), with
__init__ re-exporting the public names and __main__ keeping
`python -m sras_viewer` working. pyproject gains a `sras-viewer`
console script.

Code moved verbatim; only import headers are new (pyflakes-clean).
tests/test_gui.py patch targets follow the classes to their new
modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 10:47:34 -05:00
Thomas Ales 00a7afade0 Dedup format layer; public accessors replace private reach-throughs
- SrasFile: _parse_v6 now retains per-angle x_delta and the verbatim
  preamble/background byte spans, and gains public data_offset,
  y_pos_per_angle, and iter_angle_blocks() (which now owns the ragged
  block-offset walk used three separate places before).
- sras_edit_scans: the 70-line re-parse of the v6 header sections
  (_read_v6_sections/_reread_span/_v6_angle_offsets) collapses into a
  _write_v6 that consumes SrasFile directly — verified byte-identical
  round-trip on v6 int8/int16 and legacy files. Eight print-and-exit
  pairs become _die().
- tools/make_test_sras imports the struct layouts from sras_format and
  the rotation matrix from sras_compute instead of re-declaring them
  (byte assembly stays independent of the reader).
- sras_compute: block_mean_2d, pixel_pitch_mm, nominal_delta_deg made
  public (they were GUI-facing); registration_workers() and
  default_max_workers() wrap the remaining private reach-throughs from
  sras_workers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 10:40:42 -05:00
Thomas Ales 007089dd48 Convert test scripts to pytest; extend the equivalence harness
- pyproject.toml replaces sras_viewer_requirements.txt (same pins) and
  adds a dev extra with pytest.
- tools/test_refactor.py, test_alignment.py, test_gui.py become
  tests/test_compute.py, tests/test_alignment.py, tests/test_gui.py with
  assertions preserved verbatim. test_gui.py stays one ordered
  integration sequence over a shared module-scoped window.
- check_equivalence.py: drop the dead pre-refactor monolith shim (and the
  _compute_angle_alignment alias it consumed), extend the pad sweep to
  (1, 2, 4, 8, 40), add legacy-v4 and big-endian int16 legs (new bps=2
  option in make_test_sras) so the padded FFT path and the >i2 memmap
  path are in the baseline before the FFT rewrite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 09:45:35 -05:00