From de14963f3ef65696a9f6aef7345f3c21e35797d6 Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 15 Oct 2026 15:27:24 +0200 Subject: [PATCH] fix(lamni): alignment scan-number file lists first scan of each stitch series The file written before the alignment scan predicted first_scan + k per angle, which is wrong with stitching ((2*sx+1)*(2*sy+1) scans per angle). Predict with that stride, and rewrite the file after every angle so an interrupted run still leaves correct numbers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ALDnH16dC6iyHjhe5Ey4w9 --- .../bec_ipython_client/plugins/LamNI/lamni.py | 41 +++++++++-------- .../test_lamni_tomo_alignment_scan.py | 45 +++++++++++++++++++ 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index 57516624..6a0f8002 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -1486,22 +1486,28 @@ class LamNI( ) -> None: """Write the scan-number/angle/offset log consumed by BEC_ptycho_align. - ``scans`` (optional): the actual scan number of each alignment point, - used to rewrite the file after the scan when a retried angle consumed - extra scan numbers. Defaults to the contiguous ``first_scan + k`` - prediction written before the scan starts. + One scan number per alignment angle: the first scan of that angle's + stitch series (with stitching, each projection is + (2*lamni_stitch_x+1)*(2*lamni_stitch_y+1) scans). - Mirrors Flomni.write_alignment_scan_numbers() exactly (same 4-line - format), adapted to lamni's own alignment-scan angle set (12 points - across the full 360 degrees, see tomo_alignment_scan()) and its own - x-ray-eye-fit offset source (lamni_compute_additional_correction_xeye_mu() - instead of get_alignment_offset()). Assumes exactly one scan per - alignment point (no stitching) -- same assumption flomni's own - version makes. + ``scans`` (optional): the actual first scan number of the angles + acquired so far (possibly fewer than all 12), used to rewrite the + file during/after the scan when a retried angle consumed extra scan + numbers. Angles not yet acquired are predicted from the last known + one, stepping by the number of stitch tiles per projection. Without + ``scans`` the whole list is predicted from ``first_scan``. + + Mirrors Flomni.write_alignment_scan_numbers() (same 4-line format), + adapted to lamni's own alignment-scan angle set (12 points across the + full 360 degrees, see tomo_alignment_scan()) and its own x-ray-eye-fit + offset source (lamni_compute_additional_correction_xeye_mu() instead + of get_alignment_offset()). """ angles = list(np.linspace(0, 360, num=12, endpoint=False)) - if scans is None: - scans = [first_scan + k for k in range(len(angles))] + scans_per_angle = (2 * self.lamni_stitch_x + 1) * (2 * self.lamni_stitch_y + 1) + scans = list(scans) if scans else [first_scan] + while len(scans) < len(angles): + scans.append(scans[-1] + scans_per_angle) x_vals = [] for angle in angles: @@ -1593,14 +1599,13 @@ class LamNI( first_scan_per_angle.append(start_scan_number) alignment_scan_numbers.extend(range(start_scan_number, end_scan_number)) + # Keep the file current after every angle, so an interrupted run + # still leaves correct first-of-stitch-series numbers behind. + self.write_alignment_scan_numbers(first_scan_per_angle[0], scans=first_scan_per_angle) + self.alignment_scan_progress.update(angle_index=idx + 1, angle=angle) self._lamnigui_update_alignment_progress() - # The file written up front assumes first_scan + k per angle; a retried - # angle consumes extra scan numbers, so rewrite it with what was - # actually acquired. - self.write_alignment_scan_numbers(first_scan_per_angle[0], scans=first_scan_per_angle) - umv(dev.lsamrot, 0) self.OMNYTools.printgreenbold( "\n\nAlignment scan finished. Please run BEC_ptycho_align and load the new fit" diff --git a/tests/tests_bec_ipython_client/test_lamni_tomo_alignment_scan.py b/tests/tests_bec_ipython_client/test_lamni_tomo_alignment_scan.py index a3d45ab8..3807f730 100644 --- a/tests/tests_bec_ipython_client/test_lamni_tomo_alignment_scan.py +++ b/tests/tests_bec_ipython_client/test_lamni_tomo_alignment_scan.py @@ -203,3 +203,48 @@ def test_tomo_alignment_scan_projections_are_not_queued_for_reconstruction(monke assert len(kwargs_seen) == 12 assert all(kw.get("_reconstruct") is False for kw in kwargs_seen) + + +def test_write_alignment_scan_numbers_steps_by_stitch_tiles(tmp_path, monkeypatch): + """With stitching each angle is (2*sx+1)*(2*sy+1) scans; the file must list + the first scan of each angle's stitch series.""" + lamni = make_lamni(monkeypatch, xray_eye_fit=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + lamni.client.set_global_var("lamni_stitch_x", 1) + lamni.client.set_global_var("lamni_stitch_y", 1) + monkeypatch.setattr( + "os.path.expanduser", + lambda p: p.replace("~", str(tmp_path)) if p.startswith("~") else p, + ) + log_file = tmp_path / "data/raw/logs/ptychotomoalign_scannum.txt" + + lamni.write_alignment_scan_numbers(100) + assert [int(s) for s in log_file.read_text().split("\n")[0].split()] == list( + range(100, 208, 9) + ) + + # partial actual list (angle 1 retried, +1 scan): rest predicted from it + lamni.write_alignment_scan_numbers(100, scans=[100, 110]) + assert [int(s) for s in log_file.read_text().split("\n")[0].split()] == [100] + list( + range(110, 209, 9) + ) + + +def test_tomo_alignment_scan_with_stitching_records_first_scan_per_angle(tmp_path, monkeypatch): + lamni = make_lamni(monkeypatch, xray_eye_fit=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + lamni.client.set_global_var("lamni_stitch_x", 1) + lamni.client.set_global_var("lamni_stitch_y", 1) + monkeypatch.setattr( + "os.path.expanduser", + lambda p: p.replace("~", str(tmp_path)) if p.startswith("~") else p, + ) + + def _projection(angle, _internal=False, **kw): + builtins.__dict__["bec"].queue.next_scan_number += 9 + + lamni.tomo_scan_projection = _projection + + lamni.tomo_alignment_scan() + + log_file = tmp_path / "data/raw/logs/ptychotomoalign_scannum.txt" + scans = [int(s) for s in log_file.read_text().splitlines()[0].split()] + assert scans == list(range(100, 208, 9))