feat(lamni): number_of_projections argument for tomo_alignment_scan
CI for csaxs_bec / test (push) Successful in 4m16s
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:
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()
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user