feat(lamni): alignment scan includes 360 deg; lsamrot limits -2..362
CI for csaxs_bec / test (push) Successful in 3m3s

The alignment scan now measures 13 angles, 0 to 360 inclusive; 360 repeats
0 as a check that nothing drifted during the run. The angle list lives in
alignment_scan_angles(), shared with write_alignment_scan_numbers().
lsamrot soft limits set to -2/362 (were 0/0, i.e. none).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ALDnH16dC6iyHjhe5Ey4w9
This commit is contained in:
x12saandClaude Opus 5.5 committed 2026-10-15 17:03:19 +02:00
1 parent e5c5d64307
commit 9c7b6dfd3b
4 files changed
+32 -24

No files matched your search

@@ -1481,6 +1481,13 @@ class LamNI(
if _reconstruct:
self.tomo_reconstruct()
@staticmethod
def alignment_scan_angles() -> list[float]:
"""Angles of tomo_alignment_scan(): every 30 degrees from 0 to 360
inclusive. 360 is the same orientation as 0; it is measured anyway as
a last data point confirming the sample did not drift during the run."""
return list(np.linspace(0, 360, num=13))
def write_alignment_scan_numbers(
self, first_scan: int, scans: list[int] | None = None
) -> None:
@@ -1491,19 +1498,19 @@ class LamNI(
(2*lamni_stitch_x+1)*(2*lamni_stitch_y+1) scans).
``scans`` (optional): the actual first scan number of the angles
acquired so far (possibly fewer than all 12), used to rewrite the
acquired so far (possibly fewer than all 13), 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
adapted to lamni's own alignment-scan angle set (13 points, 0 to 360
degrees inclusive, see alignment_scan_angles()) 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))
angles = self.alignment_scan_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):
@@ -1526,7 +1533,8 @@ class LamNI(
def tomo_alignment_scan(self, force: bool = False) -> None:
"""Perform a laminogram alignment scan: a quick ptychography scan at
12 angles evenly spaced across the full 360 degrees, using whatever
13 angles, every 30 degrees from 0 to 360 inclusive (see
alignment_scan_angles()), using whatever
tomo_parameters() are currently set (FOV/step/counting time --
tomo_type and requested projection count are ignored, same as
Flomni.tomo_alignment_scan()). Collects all scan numbers acquired
@@ -1566,7 +1574,7 @@ class LamNI(
self.write_alignment_scan_numbers(bec.queue.next_scan_number)
angles = list(np.linspace(0, 360, num=12, endpoint=False))
angles = self.alignment_scan_angles()
alignment_scan_numbers = []
self.alignment_scan_progress.reset()
+2 -2
View File
@@ -133,8 +133,8 @@ lsamrot:
axis_Id: C
host: mpc2680.psi.ch
limits:
- 0
- 0
- -2
- 362
port: 8081
sign: 1
tolerance: 1
+1 -1
View File
@@ -168,7 +168,7 @@ The correction tables are read from the files written by `BEC_ptycho_align`, wit
The alignment scan itself already uses the x-ray eye correction and any correction tables that are loaded. So the ptychographic alignment measures what is left over after those corrections, which is why a second iteration with `read_additional_correction_2()` is useful.
* `lamni.tomo_parameters()` adjust the ptychographic scan parameters for the alignment scan (FOV/step size/counting time) — `tomo_type` and number of projections are ignored, since the alignment scan always runs its own fixed 12 points spread evenly across the full 360 degrees, independent of the main tomogram's settings.
* `lamni.tomo_parameters()` adjust the ptychographic scan parameters for the alignment scan (FOV/step size/counting time) — `tomo_type` and number of projections are ignored, since the alignment scan always runs its own fixed 13 points, every 30 degrees from 0 to 360 inclusive (360 repeats 0 as a drift check), independent of the main tomogram's settings.
* `lamni.tomo_alignment_scan()` perform the alignment scan. Requires x-ray-eye alignment to have already been done — it will abort with a message otherwise. Scan numbers, angles and the existing x-ray-eye-fit offset at each angle are written to `~/data/raw/logs/ptychotomoalign_scannum.txt` and also printed at the end.
* Run `BEC_ptycho_align` (in Matlab, **force ptycho=1**, and **correct scan numbers**) using the printed/logged scan numbers.
* Click the sample position in the Matlab GUI and then load the generated file by, for example
@@ -93,7 +93,7 @@ def test_tomo_alignment_scan_aborts_without_xray_eye_fit(monkeypatch):
assert lamni._scilog_calls == []
def test_tomo_alignment_scan_runs_12_projections_across_360(monkeypatch):
def test_tomo_alignment_scan_runs_13_projections_0_to_360(monkeypatch):
lamni = make_lamni(monkeypatch, xray_eye_fit=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
calls = []
@@ -105,12 +105,12 @@ def test_tomo_alignment_scan_runs_12_projections_across_360(monkeypatch):
lamni.tomo_alignment_scan()
expected_angles = list(np.linspace(0, 360, num=12, endpoint=False))
expected_angles = list(np.linspace(0, 360, num=13))
assert calls == expected_angles
assert len(lamni._scilog_calls) == 1
content, tags = lamni._scilog_calls[0]
assert tags == ["alignmentscan"]
assert "Number of alignment scans: 12" in content, content
assert "Number of alignment scans: 13" in content, content
def test_tomo_alignment_scan_rotates_back_to_zero(monkeypatch):
@@ -138,10 +138,10 @@ def test_write_alignment_scan_numbers_format(tmp_path, monkeypatch):
assert len(lines) == 4
scans = [int(s) for s in lines[0].split()]
angles = [float(a) for a in lines[1].split()]
assert scans == list(range(100, 112))
assert angles == list(np.linspace(0, 360, num=12, endpoint=False))
assert len(lines[2].split()) == 12
assert len(lines[3].split()) == 12
assert scans == list(range(100, 113))
assert angles == list(np.linspace(0, 360, num=13))
assert len(lines[2].split()) == 13
assert len(lines[3].split()) == 13
def _timeout_alarm():
@@ -176,8 +176,8 @@ def test_tomo_alignment_scan_retries_timed_out_angle_and_records_real_scan_numbe
lamni.tomo_alignment_scan()
expected_angles = list(np.linspace(0, 360, num=12, endpoint=False))
assert len(calls) == 13, "the timed-out angle must be re-acquired"
expected_angles = list(np.linspace(0, 360, num=13))
assert len(calls) == 14, "the timed-out angle must be re-acquired"
assert calls[2] == calls[3] == expected_angles[2]
log_file = tmp_path / "data/raw/logs/ptychotomoalign_scannum.txt"
@@ -185,10 +185,10 @@ def test_tomo_alignment_scan_retries_timed_out_angle_and_records_real_scan_numbe
scans = [int(s) for s in lines[0].split()]
# scans 100, 101 = angles 0, 1; 102 = the failed attempt at angle 2 (must be
# skipped); 103.. = the retry and the remaining angles
assert scans == [100, 101] + list(range(103, 113))
assert scans == [100, 101] + list(range(103, 114))
assert [float(a) for a in lines[1].split()] == expected_angles
content, _tags = lamni._scilog_calls[0]
assert "Number of alignment scans: 12" in content
assert "Number of alignment scans: 13" in content
def test_tomo_alignment_scan_projections_are_not_queued_for_reconstruction(monkeypatch):
@@ -201,7 +201,7 @@ def test_tomo_alignment_scan_projections_are_not_queued_for_reconstruction(monke
lamni.tomo_alignment_scan()
assert len(kwargs_seen) == 12
assert len(kwargs_seen) == 13
assert all(kw.get("_reconstruct") is False for kw in kwargs_seen)
@@ -219,13 +219,13 @@ def test_write_alignment_scan_numbers_steps_by_stitch_tiles(tmp_path, monkeypatc
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)
range(100, 217, 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)
range(110, 218, 9)
)
@@ -247,4 +247,4 @@ def test_tomo_alignment_scan_with_stitching_records_first_scan_per_angle(tmp_pat
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))
assert scans == list(range(100, 217, 9))