Add aligned/cropped .sras export and the compute pieces behind it

The alignment machinery never modified a scan: it produced an
AlignmentResult and every consumer resampled on the fly. That is right
for a viewer but means the aligned stack cannot leave the process, so no
other tool can read it and reopening the scan redoes the registration.

sras_align_export.write_aligned_sras bakes an alignment into a new v6
file: each angle is gathered onto the shared canvas by nearest neighbour,
so every output angle shares one grid and the file opens already aligned.
Registering the export against itself returns identity, which is the test
that pins the whole index chain.

Three details that are easy to get wrong and are now covered:

  * Rounding must be floor(x + 0.5), not np.rint. scipy's order=0 rounds
    halves away from zero, and the canvas is snapped to the reference's
    pixel grid, so exact halves are common rather than hypothetical.
  * Out-of-bounds must be tested on the fractional coordinate against
    [0, n-1], not on the rounded index, or a one-pixel rim gets real data
    everywhere the Aligned View shows padding.
  * ...but with a tolerance, because the mm-space affine chain lands an
    exactly-integer transform a few times 1e-13 off. A bare >= 0 drops
    the *reference* angle's entire first row and last column.

Padding is the per-channel ADC code nearest 0 mV, not zero: zero ADC
decodes to ~+100 mV on real calibration and would masquerade as sample.

Source rows are served from sliding in-RAM bands. A rotated angle maps
one output row to a diagonal across the source, so indexing a memmap in
output order refaults nearly the whole angle per row — terabytes of
paging for a gigabyte of data.

Also in compute: crop_alignment_result (a crop is pure index translation,
so it folds into the affine's offset rather than becoming a second
transform), overlap_stats, largest_rect_at_least for a crop that stays
inside the overlap region, and seed/sign/refine knobs on
register_angle_to_reference whose defaults leave existing behaviour and
tests byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-08-07 22:34:18 -05:00
parent 3989c2a1b8
commit 6d0e30b9ce
8 changed files with 1389 additions and 36 deletions
+612
View File
@@ -0,0 +1,612 @@
"""Aligned/cropped .sras export: does the written file actually hold the
alignment the viewer showed?
The export is the one place an alignment stops being a transform applied on the
fly and becomes bytes on disk, so these tests care about two things above all:
the file's geometry describes what was written, and the pixels in it are the
same pixels apply_alignment would have drawn. The strongest check is the
round-trip — register the exported file against itself and demand identity,
which no amount of self-consistent-but-wrong index math can fake.
No Qt: this exercises sras_align_export and sras_compute directly.
"""
import struct
import numpy as np
import pytest
import sras_align_export as export
import sras_compute as compute
from sras_format import CH3_IDX, CH4_IDX, HDR_SIZE_V6, SrasFile, adc_to_mv, mv_to_adc
import tools.make_test_sras as gen
_THRESHOLD_MV = 80.0
# Same reasoning as tests/test_alignment.py: a quarter degree is already
# sub-pixel for this sample at the registration pitch.
_ROT_TOL_DEG = 0.5
_SHIFT_TOL_MM = 0.02
def dc_mv(sras: SrasFile, angle_idx: int, ch: int = CH4_IDX) -> np.ndarray:
return adc_to_mv(compute.compute_dc_image(sras, angle_idx, ch), *sras.cal(ch))
@pytest.fixture(scope="module")
def rig(tmp_path_factory):
"""The rotating-sample scan, its truth alignment, and its export."""
tmpdir = tmp_path_factory.mktemp("sras_export")
src_path = tmpdir / "rotating.sras"
meta = gen.write_rotating(src_path, n_angles=4)
sras = SrasFile(str(src_path))
params = {a: compute.ManualAngleParams(rot, shift)
for a, (rot, shift) in meta["truth"].items()}
result = compute.build_manual_alignment(sras, 0, _THRESHOLD_MV, params)
out_path = tmpdir / "rotating_aligned.sras"
export.write_aligned_sras(sras, result, out_path)
return type("Rig", (), dict(
tmpdir=tmpdir, src_path=src_path, sras=sras, meta=meta,
result=result, out_path=out_path, out=SrasFile(str(out_path))))
# ---------------------------------------------------------------------------
# Geometry and file structure
# ---------------------------------------------------------------------------
def test_output_is_v6_with_uniform_geometry(rig):
out, result = rig.out, rig.result
n_rows, n_cols = result.canvas_shape
assert out.version == 6
assert out.n_angles == rig.sras.n_angles
assert set(out.n_rows) == {n_rows}, "every angle must share the canvas rows"
assert set(out.n_frames) == {n_cols}, "every angle must share the canvas frames"
assert np.allclose(out.x_start_mm, result.canvas_origin_mm[0])
# x_delta must stay velocity/laser_freq or x_axis_mm() contradicts the
# geometry table; the canvas pitch is the reference angle's own pitch, so
# this is exact rather than approximate.
assert np.allclose(out.x_delta_mm_per_angle, rig.sras.pixel_x_mm)
assert out.pixel_x_mm == pytest.approx(rig.sras.pixel_x_mm)
def test_row_table_matches_the_canvas(rig):
expected = (rig.result.canvas_origin_mm[1]
+ np.arange(rig.result.canvas_shape[0]) * rig.result.canvas_dy_mm)
for a in range(rig.out.n_angles):
assert rig.out.y_positions_mm(a) == pytest.approx(expected, abs=1e-4)
def test_angle_table_and_calibration_round_trip(rig):
assert rig.out.angles_deg == pytest.approx(rig.sras.angles_deg)
for ch in range(rig.sras.n_channels):
assert rig.out.cal(ch) == pytest.approx(rig.sras.cal(ch))
assert rig.out.samples_per_frame == rig.sras.samples_per_frame
assert rig.out.bytes_per_sample == rig.sras.bytes_per_sample
assert rig.out.n_channels == rig.sras.n_channels
assert rig.out.background == pytest.approx(rig.sras.background)
def test_no_cache_tail(rig):
"""File ends exactly at the waveform data — nothing trailing.
A stale cache tail would be indexed by the *input's* grid, so the export
must not carry one; asserting on the exact file size is what proves it,
since a v7 tail would simply be ignored by a v6 parser.
"""
end = max(off + n for _, off, n in rig.out.iter_angle_blocks())
assert rig.out_path.stat().st_size == end
assert all(img is None for img in rig.out.precomputed_dc4_mv)
def test_declared_header_size_is_v6(rig):
raw = rig.out_path.read_bytes()[:HDR_SIZE_V6]
magic, version, n_angles = struct.unpack(">4sBH", raw[:7])
assert (magic, version, n_angles) == (b"SRAS", 6, rig.sras.n_angles)
# ---------------------------------------------------------------------------
# The pixels themselves
# ---------------------------------------------------------------------------
def test_export_matches_apply_alignment(rig):
"""The exported waveforms decode to the same DC image the viewer drew —
over the *whole* canvas, padding included.
Two rules have to be exactly right for this, and each fails differently:
* rounding must be floor(x + 0.5), not np.rint, or pixels on exact
half-integer boundaries pick the neighbouring source pixel;
* out-of-bounds must be tested on the fractional coordinate against
[0, n-1], not on the rounded index, or a one-pixel rim gets real data
where the preview shows padding.
Comparing every pixel rather than only the interior is what catches the
second one, since a rim discrepancy hides inside a `preview != 0` mask.
"""
for a in range(rig.sras.n_angles):
preview = compute.apply_alignment(rig.result, a, dc_mv(rig.sras, a))
actual = dc_mv(rig.out, a)
assert actual.shape == preview.shape
# Padding matches to within half an ADC step: apply_alignment pads with
# literal 0.0 mV, the export with the nearest integer ADC code to 0 mV.
tol = abs(rig.sras.cal(CH4_IDX)[0]) / 2.0 + 1e-4
# Exclude the epsilon rim the export deliberately keeps and scipy drops
# (see test_edge_tolerance_only_affects_the_epsilon_rim).
sr, sc = export._src_coords(rig.result.per_angle[a],
np.arange(preview.shape[0]), preview.shape[1])
rim = export._in_bounds(sr, sc, *rig.sras.image_shape(a)) & (preview == 0.0)
cmp = ~rim
assert actual[cmp] == pytest.approx(preview[cmp], abs=tol), \
f"angle {a}: exported pixels differ from the aligned preview"
# And exactly, wherever there is real data.
inside = (preview != 0.0)
assert inside.any(), f"angle {a}: preview is entirely padding"
assert actual[inside] == pytest.approx(preview[inside], abs=1e-6), \
f"angle {a}: exported data pixels are not bit-equal to the preview"
def test_reference_angle_is_exported_whole(rig):
"""The reference angle must survive as a complete, exact integer crop.
It is the coordinate authority — its transform is the identity with an
integer offset by construction — so every one of its source pixels has to
appear in the export. This is what _EDGE_TOL exists for: that offset comes
out of the mm-space affine chain as -20 - 7e-15, and a bare `>= 0` bounds
test silently drops the angle's entire first row and last column.
"""
ref = rig.result.ref_angle_idx
src_rows, src_frames = rig.sras.image_shape(ref)
plan = export.plan_export(rig.sras, rig.result)
assert plan.valid_px[ref] == src_rows * src_frames, \
"reference angle lost pixels to the in-bounds test"
# And the values themselves land as an exact, unrotated block.
src_img = dc_mv(rig.sras, ref)
out_img = dc_mv(rig.out, ref)
t = rig.result.per_angle[ref]
row0, col0 = (int(round(-t.offset[0])), int(round(-t.offset[1])))
assert np.array_equal(out_img[row0:row0 + src_rows, col0:col0 + src_frames],
src_img), \
"reference angle is not a verbatim block in the export"
def test_edge_tolerance_only_affects_the_epsilon_rim(rig):
"""Where the export's bounds test and scipy's disagree, the coordinate must
be within _EDGE_TOL of the boundary — i.e. only pixels whose scipy answer
was itself decided by float noise, never a real half-pixel decision."""
for a in range(rig.sras.n_angles):
t = rig.result.per_angle[a]
src_rows, src_frames = rig.sras.image_shape(a)
n_rows, n_cols = rig.result.canvas_shape
ones = np.ones((src_rows, src_frames), dtype=np.float32)
scipy_valid = compute.apply_alignment(rig.result, a, ones) > 0.5
sr, sc = export._src_coords(t, np.arange(n_rows), n_cols)
ours = export._in_bounds(sr, sc, src_rows, src_frames)
differ = ours != scipy_valid
assert not (scipy_valid & ~ours).any(), \
f"angle {a}: export drops pixels scipy keeps"
if differ.any():
# Every disagreement sits within the tolerance of an edge.
near = (np.abs(sr) <= export._EDGE_TOL)
near |= (np.abs(sr - (src_rows - 1)) <= export._EDGE_TOL)
near |= (np.abs(sc) <= export._EDGE_TOL)
near |= (np.abs(sc - (src_frames - 1)) <= export._EDGE_TOL)
assert near[differ].all(), \
f"angle {a}: bounds differ away from the epsilon rim"
def test_export_matches_apply_alignment_on_ch3(rig):
"""Channel-agnostic: the gather moves whole pixels, not per-channel images."""
for a in range(rig.sras.n_angles):
preview = compute.apply_alignment(rig.result, a,
dc_mv(rig.sras, a, CH3_IDX))
actual = dc_mv(rig.out, a, CH3_IDX)
inside = preview != 0.0
assert actual[inside] == pytest.approx(preview[inside], abs=1e-3)
def test_padding_is_zero_mv_not_zero_adc(rig):
"""Unreachable canvas pixels must read as ~0 mV on every channel.
Filling with literal zero ADC would decode to (0 - yoff) * ymult + yzero —
for this fixture's CH4 calibration that is +100 mV, well above any sensible
mask threshold, so the padding would masquerade as valid sample everywhere.
"""
a = rig.sras.n_angles - 1
t = rig.result.per_angle[a]
n_rows, n_cols = rig.result.canvas_shape
src_rows, src_frames = rig.sras.image_shape(a)
sr, sc = export._src_coords(t, np.arange(n_rows), n_cols)
outside = ~export._in_bounds(sr, sc, src_rows, src_frames)
assert outside.any(), "rotated angle should leave unreachable canvas corners"
for ch in (CH3_IDX, CH4_IDX):
img = dc_mv(rig.out, a, ch)
half_step = abs(rig.sras.cal(ch)[0]) / 2.0
assert np.abs(img[outside]).max() <= half_step + 1e-6, \
f"CH{ch} padding is not within half an ADC step of 0 mV"
# And the sanity check that makes the above meaningful: zero ADC would not
# have passed it.
assert abs(adc_to_mv(0, *rig.sras.cal(CH4_IDX))) > 10.0
def test_reregistering_the_export_is_identity(rig):
"""The export really is aligned: registering it against its own angle 0
recovers no rotation and no shift.
The end-to-end check — it fails for any index error, sign flip, wrong pivot
or origin mistake anywhere in crop/affine/gather, in a way the
self-consistency tests above cannot.
"""
dc4 = {a: dc_mv(rig.out, a) for a in range(rig.out.n_angles)}
for a in range(1, rig.out.n_angles):
fit = compute.register_angle_to_reference(
rig.out, a, 0, dc4, dc_threshold_mv=_THRESHOLD_MV,
seed_deg=0.0, seed_signs=(1,))
assert abs(fit.rotation_deg) <= _ROT_TOL_DEG, \
f"angle {a} still rotated by {fit.rotation_deg:.3f}° after export"
assert float(np.hypot(*fit.shift_mm)) <= _SHIFT_TOL_MM, \
f"angle {a} still shifted by {fit.shift_mm} mm after export"
def test_export_of_int16_input(rig, tmp_path):
"""bps=2 inputs keep their big-endian int16 dtype through the gather."""
src_path = tmp_path / "i16.sras"
gen.write(src_path, n_angles=2, samples_per_frame=16, bps=2)
sras = SrasFile(str(src_path))
result = compute.build_manual_alignment(sras, 0, 0.0, {})
out_path = tmp_path / "i16_aligned.sras"
export.write_aligned_sras(sras, result, out_path)
out = SrasFile(str(out_path))
assert out.bytes_per_sample == 2
assert out.data[0].dtype == np.dtype(">i2")
for a in range(sras.n_angles):
preview = compute.apply_alignment(result, a, dc_mv(sras, a))
actual = dc_mv(out, a)
inside = preview != 0.0
assert actual[inside] == pytest.approx(preview[inside], abs=1e-3)
# ---------------------------------------------------------------------------
# Cropping
# ---------------------------------------------------------------------------
def test_crop_is_a_window_of_the_full_canvas(rig):
"""crop_alignment_result must resample exactly the sub-rectangle it names.
Asserted as bit-exact equality, not approximately: the crop composes into
the affine's offset by an integer number of canvas pixels, so anything but
an exact match means the composition is wrong.
"""
n_rows, n_cols = rig.result.canvas_shape
row0, col0 = n_rows // 5, n_cols // 4
nr, nc = n_rows // 2, n_cols // 3
cropped = compute.crop_alignment_result(rig.result, row0, col0, nr, nc)
assert cropped.canvas_shape == (nr, nc)
assert cropped.canvas_origin_mm[0] == pytest.approx(
rig.result.canvas_origin_mm[0] + col0 * rig.result.canvas_dx_mm)
assert cropped.canvas_origin_mm[1] == pytest.approx(
rig.result.canvas_origin_mm[1] + row0 * rig.result.canvas_dy_mm)
for a in range(rig.sras.n_angles):
img = dc_mv(rig.sras, a)
full = compute.apply_alignment(rig.result, a, img)
assert np.array_equal(
compute.apply_alignment(cropped, a, img),
full[row0:row0 + nr, col0:col0 + nc]), \
f"angle {a}: cropped resample is not the same window"
# Rotation/shift are properties of the angle, not of the canvas.
assert cropped.per_angle[a].rotation_deg == rig.result.per_angle[a].rotation_deg
assert cropped.per_angle[a].shift_mm == rig.result.per_angle[a].shift_mm
def test_cropped_export_round_trips(rig, tmp_path):
n_rows, n_cols = rig.result.canvas_shape
row0, col0, nr, nc = n_rows // 4, n_cols // 4, n_rows // 2, n_cols // 2
cropped = compute.crop_alignment_result(rig.result, row0, col0, nr, nc)
out_path = tmp_path / "cropped.sras"
export.write_aligned_sras(rig.sras, cropped, out_path)
out = SrasFile(str(out_path))
assert set(out.n_rows) == {nr} and set(out.n_frames) == {nc}
assert out.x_start_mm[0] == pytest.approx(cropped.canvas_origin_mm[0], abs=1e-4)
for a in range(rig.sras.n_angles):
preview = compute.apply_alignment(cropped, a, dc_mv(rig.sras, a))
actual = dc_mv(out, a)
inside = preview != 0.0
if inside.any():
assert actual[inside] == pytest.approx(preview[inside], abs=1e-3)
def test_crop_rejects_empty_window(rig):
with pytest.raises(ValueError, match="empty crop"):
compute.crop_alignment_result(rig.result, 0, 0, 0, 10)
with pytest.raises(ValueError, match="empty crop"):
compute.crop_alignment_result(rig.result, 0, 0, 10, -1)
# ---------------------------------------------------------------------------
# plan_export and overlap_stats
# ---------------------------------------------------------------------------
def test_plan_export_matches_what_was_written(rig):
plan = export.plan_export(rig.sras, rig.result)
n_rows, n_cols = rig.result.canvas_shape
assert (plan.n_rows, plan.n_frames) == (n_rows, n_cols)
assert plan.n_angles == rig.sras.n_angles
data_bytes = sum(n for _, _, n in rig.out.iter_angle_blocks())
assert plan.total_bytes == data_bytes
assert plan.bytes_per_angle * plan.n_angles == plan.total_bytes
# Coverage must agree with the pixels that actually carry data. The
# reference angle is unrotated, so its whole footprint lands inside.
ref_px = np.prod(rig.sras.image_shape(0))
assert plan.valid_px[0] == ref_px
for a in range(1, rig.sras.n_angles):
assert 0 < plan.valid_px[a] <= n_rows * n_cols
assert 0.0 < plan.coverage_frac(a) < 1.0
def test_plan_export_flags_a_crop_that_misses_an_angle(rig):
"""A crop over a corner the rotated angles cannot reach must warn, and the
export must still succeed by writing that angle as padding."""
n_rows, n_cols = rig.result.canvas_shape
corner = compute.crop_alignment_result(rig.result, 0, 0,
max(1, n_rows // 12),
max(1, n_cols // 12))
plan = export.plan_export(rig.sras, corner)
empty = [a for a in range(rig.sras.n_angles) if plan.valid_px[a] == 0]
assert empty, "top-left canvas corner should be unreachable for some angle"
assert any("all padding" in w for w in plan.warnings)
def test_overlap_stats():
counts = np.array([[0, 1, 2], [3, 3, 0], [0, 2, 3]])
stats = compute.overlap_stats(counts, 3)
assert stats["union_px"] == 6
assert stats["full_px"] == 3
assert stats["full_frac"] == pytest.approx(0.5)
assert stats["max_count"] == 3
assert stats["mean_count"] == pytest.approx((1 + 2 + 3 + 3 + 2 + 3) / 6)
assert stats["empty"] is False
empty = compute.overlap_stats(np.zeros((4, 4), dtype=int), 3)
assert empty["empty"] is True
assert empty["full_frac"] == 0.0 and empty["mean_count"] == 0.0
def test_largest_rect_at_least():
# A 2x3 block of 3s with a notch that a bounding box would swallow.
counts = np.array([
[0, 0, 0, 0, 0],
[0, 3, 3, 3, 0],
[0, 3, 3, 3, 0],
[0, 3, 0, 3, 0],
])
row0, col0, nr, nc = compute.largest_rect_at_least(counts, 3)
assert (nr * nc) == 6 and (row0, col0, nr, nc) == (1, 1, 2, 3)
assert (counts[row0:row0 + nr, col0:col0 + nc] >= 3).all()
# A column taller than the wide block is the better rectangle.
tall = np.array([[3, 3], [3, 0], [3, 0], [3, 0]])
r0, c0, nr2, nc2 = compute.largest_rect_at_least(tall, 3)
assert (r0, c0, nr2, nc2) == (0, 0, 4, 1)
assert compute.largest_rect_at_least(np.zeros((3, 3), dtype=int), 1) is None
# Whole-array case: no notch, so the answer is the array itself.
assert compute.largest_rect_at_least(np.full((3, 4), 2), 2) == (0, 0, 3, 4)
def test_largest_rect_is_pure_on_the_real_fixture(rig):
"""On real overlap counts the returned rectangle must contain only
full-overlap pixels — the property a bounding box would violate."""
n = rig.sras.n_angles
masks = {a: (dc_mv(rig.sras, a) >= _THRESHOLD_MV).astype(np.float32)
for a in range(n)}
counts = sum(compute.apply_alignment(rig.result, a, masks[a]) > 0.5
for a in range(n)).astype(int)
assert counts.max() == n, "fixture alignment should have a full-overlap region"
rect = compute.largest_rect_at_least(counts, n)
assert rect is not None
row0, col0, nr, nc = rect
assert (counts[row0:row0 + nr, col0:col0 + nc] == n).all(), \
"convenience crop must not include pixels some angle misses"
# And it must beat the naive bounding box, which here is impure.
rr, cc = np.nonzero(counts == n)
bbox_pure = (counts[rr.min():rr.max() + 1, cc.min():cc.max() + 1] == n).all()
assert not bbox_pure, "fixture no longer exercises the bounding-box hazard"
# ---------------------------------------------------------------------------
# Legacy inputs, validation and durability
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("version", [2, 4])
def test_legacy_input_exports_as_v6(version, tmp_path):
"""v2-v5 inputs keep no verbatim preamble/background spans, so those
sections have to be re-encoded. v2 additionally has neither."""
src_path = tmp_path / f"legacy_v{version}.sras"
gen.write_legacy(src_path, version=version, n_angles=2)
sras = SrasFile(str(src_path))
result = compute.build_manual_alignment(sras, 0, 0.0, {})
out_path = tmp_path / f"legacy_v{version}_aligned.sras"
export.write_aligned_sras(sras, result, out_path)
out = SrasFile(str(out_path))
assert out.version == 6
assert out.n_angles == sras.n_angles
# A zero background rather than a zero-length one: consumers subtract it
# from a (spf,)-shaped row, which a length-0 array cannot broadcast against.
assert out.background is not None
assert out.background.size == sras.samples_per_frame
if sras.background is None:
assert np.all(out.background == 0)
assert any("no background" in w for w in
export.plan_export(sras, result).warnings)
# Calibration must survive: v2 has no preambles and falls back to the
# hardcoded scope constants, and the re-encoded empty preambles must land on
# exactly the same fallback.
for ch in range(sras.n_channels):
assert out.cal(ch) == pytest.approx(sras.cal(ch))
for a in range(sras.n_angles):
preview = compute.apply_alignment(result, a, dc_mv(sras, a))
actual = dc_mv(out, a)
inside = preview != 0.0
assert actual[inside] == pytest.approx(preview[inside], abs=1e-3)
def test_too_many_rows_is_rejected_before_writing(rig, tmp_path):
"""The geometry table stores n_rows as a u16; silently truncating would
write a file whose header disagrees with its own waveform block."""
huge = compute.crop_alignment_result(rig.result, 0, 0, 70000, 4)
out_path = tmp_path / "huge.sras"
with pytest.raises(ValueError, match="exceeds the .sras per-angle geometry"):
export.write_aligned_sras(rig.sras, huge, out_path)
assert not out_path.exists()
assert not out_path.with_name(out_path.name + ".part").exists()
def test_missing_transform_is_rejected(rig, tmp_path):
broken = compute.crop_alignment_result(rig.result, 0, 0,
*rig.result.canvas_shape)
del broken.per_angle[1]
with pytest.raises(ValueError, match="no transform for angle"):
export.write_aligned_sras(rig.sras, broken, tmp_path / "broken.sras")
def test_cancelled_export_leaves_nothing_behind(rig, tmp_path):
out_path = tmp_path / "cancelled.sras"
written = export.write_aligned_sras(rig.sras, rig.result, out_path,
should_stop=lambda: True)
assert written == out_path
assert not out_path.exists(), "cancelled export must not leave an output file"
assert not out_path.with_name(out_path.name + ".part").exists()
def test_failed_write_leaves_nothing_behind(rig, tmp_path):
"""An exception mid-write must remove the partial file: a short .sras is
not detectably broken — the v6 parser reads it as an aborted scan."""
out_path = tmp_path / "boom.sras"
def explode(_pct):
raise RuntimeError("boom")
with pytest.raises(RuntimeError, match="boom"):
export.write_aligned_sras(rig.sras, rig.result, out_path,
progress_cb=explode)
assert not out_path.exists()
assert not out_path.with_name(out_path.name + ".part").exists()
def test_progress_is_monotonic_and_completes(rig, tmp_path):
seen: list[int] = []
export.write_aligned_sras(rig.sras, rig.result, tmp_path / "prog.sras",
progress_cb=seen.append)
assert seen and seen[-1] == 100
assert seen == sorted(seen)
assert all(0 <= p <= 100 for p in seen)
def test_band_reader_path_is_byte_identical(rig, tmp_path, monkeypatch):
"""A source block too large to hold in RAM is served from sliding bands
instead. That path only runs on multi-gigabyte scans, so force it with a
tiny budget and demand the same bytes — otherwise the one code path that
matters on real data is the one never tested."""
whole = tmp_path / "whole.sras"
export.write_aligned_sras(rig.sras, rig.result, whole)
monkeypatch.setattr(compute, "_TOTAL_BYTES_BUDGET", 4096)
banded = tmp_path / "banded.sras"
export.write_aligned_sras(rig.sras, rig.result, banded)
assert banded.read_bytes() == whole.read_bytes()
def test_row_chunking_is_invariant(rig, tmp_path, monkeypatch):
"""Output must not depend on how many rows are buffered per write."""
base = tmp_path / "base.sras"
export.write_aligned_sras(rig.sras, rig.result, base)
monkeypatch.setattr(export, "_ROW_CHUNK", 1)
one = tmp_path / "one.sras"
export.write_aligned_sras(rig.sras, rig.result, one)
assert one.read_bytes() == base.read_bytes()
def test_refuses_to_overwrite_the_source(rig):
"""The source's waveform blocks are live read-only memmaps; writing over
the file would corrupt the reads the gather is making from it."""
with pytest.raises(ValueError, match="refusing to export onto the source"):
export.write_aligned_sras(rig.sras, rig.result, rig.src_path)
assert SrasFile(str(rig.src_path)).n_angles == rig.sras.n_angles
def test_overwrites_an_existing_file(rig, tmp_path):
out_path = tmp_path / "existing.sras"
out_path.write_bytes(b"not a scan")
export.write_aligned_sras(rig.sras, rig.result, out_path)
assert SrasFile(str(out_path)).version == 6
# ---------------------------------------------------------------------------
# The registration knobs the wizard exposes
# ---------------------------------------------------------------------------
def test_locked_rotation_returns_exactly_the_seed(rig):
"""search_deg=0 + one sign + refine=False pins rotation to the seed, which
is what "lock rotation to the stage angle" means on the wizard's first
page. Only the translation may be searched."""
dc4 = {a: dc_mv(rig.sras, a) for a in range(rig.sras.n_angles)}
for a in range(1, rig.sras.n_angles):
nominal = compute.nominal_delta_deg(rig.sras, a, 0)
fit = compute.register_angle_to_reference(
rig.sras, a, 0, dc4, dc_threshold_mv=_THRESHOLD_MV,
search_deg=0.0, coarse_step_deg=2.0, seed_signs=(-1,), refine=False)
assert fit.rotation_deg == pytest.approx(-nominal)
def test_seed_deg_overrides_the_stage_angle(rig):
"""seed_deg=0.0 searches around no rotation at all, so a scan whose angles
are genuinely ~37° apart must fail to find them within a ±2° window —
proving the seed is what positions the search."""
dc4 = {a: dc_mv(rig.sras, a) for a in range(rig.sras.n_angles)}
fit = compute.register_angle_to_reference(
rig.sras, 1, 0, dc4, dc_threshold_mv=_THRESHOLD_MV,
search_deg=2.0, seed_deg=0.0, seed_signs=(1,), refine=False)
truth_rot = rig.meta["truth"][1][0]
assert abs(fit.rotation_deg) <= 2.0
assert abs(fit.rotation_deg - truth_rot) > 10.0
def test_rotation_candidates_signs():
both = compute._rotation_candidates(10.0, 2.0, 2.0)
assert both == compute._rotation_candidates(10.0, 2.0, 2.0, (-1, 1)), \
"default must stay the both-signs sweep"
assert compute._rotation_candidates(10.0, 0.0, 2.0, (1,)) == [10.0]
assert compute._rotation_candidates(10.0, 0.0, 2.0, (-1,)) == [-10.0]
# A zero seed collapses the two windows; the dedupe must keep one copy.
assert compute._rotation_candidates(0.0, 2.0, 2.0) == [-2.0, 0.0, 2.0]
def test_zero_mv_fill_code_is_clipped_to_dtype():
"""mv_to_adc is unclamped, so the fill code must be clipped or the int8
cast wraps around to a large-magnitude value."""
fake = type("S", (), dict(
n_channels=1, samples_per_frame=2,
cal=lambda self, ch: (1e-6, 0.0, 5000.0)))()
row = export._fill_row(fake, 3, np.dtype(np.int8))
assert row.shape == (1, 3, 2)
assert row.min() == row.max() == np.iinfo(np.int8).min
assert mv_to_adc(0.0, 1e-6, 0.0, 5000.0) < np.iinfo(np.int8).min