feat(LamNI): warn (overridable) when alignment steps are stale or missing

Nothing previously enforced the intended sequence: rotation-center
calibration -> X-ray-eye alignment -> fine alignment scan -> real
tomogram. Add three soft gates -- xrayeye_alignment_start() (no center
found yet), tomo_alignment_scan() (X-ray-eye fit missing/invalidated,
softened from a hard abort), and tomo_scan() (no fine alignment
loaded) -- each printing a specific warning and prompting to continue,
via a new shared _confirm_sequence_override() helper. Not a hard
block: each method also gets force: bool = False to skip the check
entirely for cases that legitimately don't need it (e.g. a large FOV
that doesn't need fine alignment).
This commit is contained in:
x01dc
2026-07-27 16:58:21 +02:00
parent 9fc0770055
commit e911f8d5e3
@@ -435,7 +435,7 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools
# X-ray eye alignment entry points
# ------------------------------------------------------------------
def xrayeye_alignment_start(self, keep_shutter_open: bool = False):
def xrayeye_alignment_start(self, keep_shutter_open: bool = False, force: bool = False):
"""Run the BEC GUI-based X-ray eye alignment procedure.
Creates a fresh :class:`XrayEyeAlignGUI` instance, which resets the
@@ -445,7 +445,19 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools
Args:
keep_shutter_open: If True the shutter is left open between angle
steps so the sample remains visible in live view.
force: skip the rotation-center-calibration check below without
prompting.
"""
if self.client.get_global_var("lamni_center_found_at") is None:
if not self._confirm_sequence_override(
"No rotation-center calibration has been recorded yet "
"(xrayeye_rotation_center_calibration_isolated/extended/"
"smear_experimental()). X-ray-eye alignment is normally done "
"after finding the rotation centre.",
force,
):
print("Aborting X-ray eye alignment.")
return
aligner = XrayEyeAlignGUI(self.client, self)
try:
aligner.align(keep_shutter_open=keep_shutter_open)
@@ -1223,7 +1235,7 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools
f.write(" ".join(f"{x:.2f}" for x in x_vals) + "\n")
f.write(" ".join(map(str, x_vals)) + "\n")
def tomo_alignment_scan(self) -> None:
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
tomo_parameters() are currently set (FOV/step/counting time --
@@ -1234,10 +1246,19 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools
~/data/raw/logs/ptychotomoalign_scannum.txt for BEC_ptycho_align,
prints them, and creates a scilog entry summarising the alignment
scan numbers.
Args:
force: skip the X-ray-eye-alignment check below without prompting.
"""
if self.client.get_global_var("tomo_fit_xray_eye") is None:
print("It appears that the xrayeye alignment was not performed or loaded. Aborting.")
return
if not self._confirm_sequence_override(
"No X-ray-eye alignment fit is loaded (or it was invalidated "
"by a rotation-center calibration run since). The alignment "
"scan's per-angle offsets would then all be zero.",
force,
):
print("Aborting alignment scan.")
return
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
@@ -1609,13 +1630,16 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools
)
return angle, subtomo_number
def tomo_scan(self, subtomo_start=1, start_angle=None, projection_number=None):
def tomo_scan(
self, subtomo_start=1, start_angle=None, projection_number=None, force: bool = False
):
"""Start a tomo scan.
Args:
subtomo_start (int): For tomo_type 1, the sub-tomogram to start from. Defaults to 1.
start_angle (float, optional): Override starting angle of the first sub-tomogram.
projection_number (int, optional): For tomo_types 2 and 3, resume from this index.
force: skip the fine-alignment check below without prompting.
"""
self.lamnigui_show_progress()
@@ -1632,6 +1656,17 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools
or (self.tomo_type == 2 and projection_number is None)
or (self.tomo_type == 3 and projection_number is None)
):
if not self.corr_pos_x:
if not self._confirm_sequence_override(
"No fine (ptycho) alignment correction is loaded -- the "
"sample centre will drift across projection angles "
"uncorrected. Fine for a large FOV that doesn't need it; "
"otherwise run tomo_alignment_scan() and "
"read_additional_correction() first.",
force,
):
print("Aborting tomo scan.")
return
# bec.active_account is already a plain str, not bytes -- .decode()
# crashes with AttributeError. Also guard against no active
# e-account (empty string, e.g. a dev/sim session not logged into
@@ -2061,6 +2096,31 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools
def _get_val(msg: str, default_value, data_type):
return data_type(input(f"{msg} ({default_value}): ") or default_value)
@staticmethod
def _confirm_sequence_override(warning: str, force: bool) -> bool:
"""Print *warning* and ask whether to proceed anyway.
Used by the alignment-sequence gates (xrayeye_alignment_start(),
tomo_alignment_scan(), tomo_scan()) -- these check whether the
expected prior step (rotation-center calibration / X-ray-eye
alignment / fine alignment) is still valid, but never hard-block:
an operator can always choose to continue, or pass force=True to
skip the prompt entirely (needed for non-interactive/queued use,
e.g. tomo-queue command jobs, where input() isn't viable).
Unlike most confirmation prompts in this codebase (which default to
"yes" on Enter), this defaults to "no" -- skipping a real sequence
check should be a deliberate choice, not an accidental Enter.
Returns:
bool: True if the caller should proceed.
"""
if force:
return True
print(warning)
answer = input("Continue anyway? [y/N]: ").strip().lower()
return answer in ("y", "yes")
# ------------------------------------------------------------------
# PDF report
# ------------------------------------------------------------------