feat(lamni): number_of_projections argument for tomo_alignment_scan
CI for csaxs_bec / test (push) Successful in 4m16s

Optional, default 13 (every 30 deg, 0 and 360 inclusive). The angles
actually used are written to ptychotomoalign_scannum.txt. Values that
are not an integer >= 2 raise ValueError before anything moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
x12saandClaude Opus 5.5 committed 2026-10-20 15:08:12 +02:00
1 parent 411a9ff1aa
commit 0830a71759
3 files changed
+63 -15

No files matched your search

@@ -1636,14 +1636,18 @@ class LamNI(
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
def alignment_scan_angles(number_of_projections: int = 13) -> list[float]:
"""Angles of tomo_alignment_scan(): number_of_projections equally
spaced angles from 0 to 360 inclusive (default 13, i.e. every 30
degrees). 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))
return list(np.linspace(0, 360, num=number_of_projections))
def write_alignment_scan_numbers(
self, first_scan: int, scans: list[int] | None = None
self,
first_scan: int,
scans: list[int] | None = None,
angles: list[float] | None = None,
) -> None:
"""Write the scan-number/angle/offset log consumed by BEC_ptycho_align.
@@ -1652,12 +1656,15 @@ 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 13), used to rewrite the
acquired so far (possibly fewer than all angles), 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``.
``angles`` (optional): the alignment angles; defaults to
alignment_scan_angles().
File format, as read by BEC_ptycho_align (MATLAB):
line 1 scans first scan number per angle
line 2 theta angle [deg]
@@ -1667,7 +1674,8 @@ class LamNI(
the LaMNI ellipse fit uses just the clicks, scans and theta.
lamni_compute_additional_correction_xeye_mu() returns mm, hence *1000.
"""
angles = self.alignment_scan_angles()
if angles is None:
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):
@@ -1687,10 +1695,10 @@ class LamNI(
f.write(" ".join(f"{x:.4f}" for x in x_um) + "\n")
f.write(" ".join(f"{y:.4f}" for y in y_um) + "\n")
def tomo_alignment_scan(self, force: bool = False) -> None:
def tomo_alignment_scan(self, number_of_projections: int = 13, force: bool = False) -> None:
"""Perform a laminogram alignment scan: a quick ptychography scan at
13 angles, every 30 degrees from 0 to 360 inclusive (see
alignment_scan_angles()), using whatever
number_of_projections equally spaced angles from 0 to 360 inclusive
(default 13, every 30 degrees; 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
@@ -1701,8 +1709,16 @@ class LamNI(
scan numbers.
Args:
number_of_projections: number of angles, 0 and 360 included
(at least 2).
force: skip the X-ray-eye-alignment check below without prompting.
"""
if int(number_of_projections) != number_of_projections or number_of_projections < 2:
raise ValueError(
f"number_of_projections must be an integer >= 2, got {number_of_projections}."
)
number_of_projections = int(number_of_projections)
if self.client.get_global_var("tomo_fit_xray_eye") is None:
if not self._confirm_sequence_override(
"No X-ray-eye alignment fit is loaded (or it was invalidated "
@@ -1728,9 +1744,9 @@ class LamNI(
print("Aborting alignment scan.")
return
self.write_alignment_scan_numbers(bec.queue.next_scan_number)
angles = self.alignment_scan_angles(number_of_projections)
self.write_alignment_scan_numbers(bec.queue.next_scan_number, angles=angles)
angles = self.alignment_scan_angles()
alignment_scan_numbers = []
self.alignment_scan_progress.reset()
@@ -1765,7 +1781,9 @@ class LamNI(
# 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.write_alignment_scan_numbers(
first_scan_per_angle[0], scans=first_scan_per_angle, angles=angles
)
self.alignment_scan_progress.update(angle_index=idx + 1, angle=angle)
self._lamnigui_update_alignment_progress()
+2 -2
View File
@@ -169,8 +169,8 @@ 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 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.
* `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 runs its own set of equally spaced angles from 0 to 360 inclusive (360 repeats 0 as a drift check), independent of the main tomogram's settings. By default this is 13 points, every 30 degrees; see `number_of_projections` below.
* `lamni.tomo_alignment_scan()` perform the alignment scan. Optional argument `number_of_projections` (default 13) sets the number of angles, 0 and 360 both counted, e.g. `lamni.tomo_alignment_scan(number_of_projections=25)` for every 15 degrees. It must be at least 2. 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
`lamni.read_additional_correction('/sls/X12SA/data/e20632/Data10/cxs_software/ptycho/correction_lamni_um_S05389_lamni_fit.txt')`
@@ -264,3 +264,33 @@ def test_write_alignment_scan_numbers_lines_3_4_are_x_and_y_in_um(tmp_path, monk
lines = (tmp_path / "data/raw/logs/ptychotomoalign_scannum.txt").read_text().splitlines()
assert [float(v) for v in lines[2].split()] == pytest.approx([12.5] * 13)
assert [float(v) for v in lines[3].split()] == pytest.approx([-3.25] * 13)
def test_tomo_alignment_scan_number_of_projections(tmp_path, monkeypatch):
lamni = make_lamni(monkeypatch, xray_eye_fit=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
monkeypatch.setattr(
"os.path.expanduser",
lambda p: p.replace("~", str(tmp_path)) if p.startswith("~") else p,
)
calls = []
def _fake_projection(angle, _internal=False, **kw):
calls.append(angle)
builtins.__dict__["bec"].queue.next_scan_number += 1
lamni.tomo_scan_projection = _fake_projection
lamni.tomo_alignment_scan(number_of_projections=5)
assert calls == [0.0, 90.0, 180.0, 270.0, 360.0]
lines = (tmp_path / "data/raw/logs/ptychotomoalign_scannum.txt").read_text().splitlines()
assert [int(s) for s in lines[0].split()] == [100, 101, 102, 103, 104]
assert [float(a) for a in lines[1].split()] == [0.0, 90.0, 180.0, 270.0, 360.0]
@pytest.mark.parametrize("n", [0, 1, 2.5])
def test_tomo_alignment_scan_rejects_invalid_number_of_projections(monkeypatch, n):
lamni = make_lamni(monkeypatch, xray_eye_fit=[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
lamni.tomo_scan_projection = lambda angle, **kw: pytest.fail("must not scan")
with pytest.raises(ValueError):
lamni.tomo_alignment_scan(number_of_projections=n)