"""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