diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/extra_tomo.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/extra_tomo.py index 1b07dfa1..fa325343 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/extra_tomo.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/extra_tomo.py @@ -18,6 +18,11 @@ import numpy as np from bec_lib import bec_logger from bec_lib.alarm_handler import AlarmBase +from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.filter_check import ( + filters_out_of_beam, + warn_and_confirm, +) + from .lamni import LamNI logger = bec_logger.logger @@ -56,7 +61,7 @@ class MagLamNI(LamNI): lamni_at_each_angle(self, angle) return - self.tomo_scan_projection(angle) + self.tomo_scan_projection(angle, _internal=True) self.tomo_reconstruct() @@ -87,6 +92,14 @@ class DataDrivenLamNI(LamNI): """ bec = builtins.__dict__.get("bec") scans = builtins.__dict__.get("scans") + dev = builtins.__dict__.get("dev") + + all_out, offending = filters_out_of_beam(dev) + if not all_out: + warning = f"Not all filters are out of the beam: {', '.join(offending)}." + if not warn_and_confirm(self, warning, interactive=True, force=False): + print("Aborting tomo scan.") + return bec.builtin_actors.scan_interlock.trigger_setting = "restart_scan" bec.builtin_actors.scan_interlock.enabled = True diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index 7a88df3e..ff951cf6 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -14,6 +14,10 @@ from typeguard import typechecked from csaxs_bec.bec_ipython_client.plugins.LamNI.gui_tools import LamniGuiTools from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni_alignment_mixin import LamNIAlignmentMixin +from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.filter_check import ( + filters_out_of_beam, + warn_and_confirm, +) from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.omny_general_tools import ( OMNYTools, PtychoReconstructor, @@ -1159,9 +1163,33 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools # Scan projection # ------------------------------------------------------------------ - def tomo_scan_projection(self, angle: float): + def tomo_scan_projection(self, angle: float, _internal: bool = False): + """Acquire one fermat-scan projection at `angle`. + + _internal=True is passed by callers that already sit inside an + already-checked flow (tomo_scan()'s/tomo_alignment_scan()'s own + per-angle loop, via _at_each_angle) so the filter-out-of-beam check + below only runs once per tomogram/alignment run instead of once per + projection. Direct/standalone calls (the default) are checked every + time, mirroring Flomni.tomo_scan_projection()'s _internal convention. + """ scans = builtins.__dict__.get("scans") + if not _internal: + dev = builtins.__dict__.get("dev") + all_out, offending = filters_out_of_beam(dev) + if not all_out: + warning = f"Not all filters are out of the beam: {', '.join(offending)}." + if not warn_and_confirm(self, warning, interactive=True, force=False): + # Raise rather than return: this can run inside a + # per-angle loop (directly, or via a custom + # at_each_angle hook), and a bare return would leave the + # projection silently missing / the job stuck running. + raise LamNIError( + "tomo_scan_projection: declined to proceed with filter(s) " + f"in the beam ({', '.join(offending)})." + ) + additional_correction = self.compute_additional_correction(angle) additional_correction_2 = self.compute_additional_correction_2(angle) correction_xeye_mu = self.lamni_compute_additional_correction_xeye_mu(angle) @@ -1262,6 +1290,13 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools bec = builtins.__dict__.get("bec") dev = builtins.__dict__.get("dev") + all_out, offending = filters_out_of_beam(dev) + if not all_out: + warning = f"Not all filters are out of the beam: {', '.join(offending)}." + if not warn_and_confirm(self, warning, interactive=True, force=force): + print("Aborting alignment scan.") + return + self.leye_out() self.write_alignment_scan_numbers(bec.queue.next_scan_number) @@ -1281,7 +1316,7 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools while not successful: try: start_scan_number = bec.queue.next_scan_number - self.tomo_scan_projection(angle) + self.tomo_scan_projection(angle, _internal=True) except AlarmBase as exc: if exc.alarm_type == "TimeoutError": bec.queue.request_queue_reset() @@ -1333,7 +1368,7 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools hook(self, angle) return - self.tomo_scan_projection(angle) + self.tomo_scan_projection(angle, _internal=True) self.tomo_reconstruct() # ------------------------------------------------------------------ @@ -1667,6 +1702,14 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools ) time.sleep(10) + dev = builtins.__dict__.get("dev") + all_out, offending = filters_out_of_beam(dev) + if not all_out: + warning = f"Not all filters are out of the beam: {', '.join(offending)}." + if not warn_and_confirm(self, warning, interactive=interactive, force=force): + print("Aborting tomo scan.") + return + self.lamnigui_show_progress() bec = builtins.__dict__.get("bec") diff --git a/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/filter_check.py b/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/filter_check.py new file mode 100644 index 00000000..1503433a --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/filter_check.py @@ -0,0 +1,46 @@ +import time + +from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission + + +def filters_out_of_beam(dev, tol: float = 0.1) -> tuple[bool, list[str]]: + """Check whether all cSAXS exposure-box filters are retracted. + + Reuses cSAXSFilterTransmission's own axis list/position table (index 0 of + each row is the "out" position) so there is one source of truth for what + "out" means, matching csaxs.fil_trans()/_fil_trans_report()'s own check. + """ + offending = [] + for axis_name, positions in zip( + cSAXSFilterTransmission._AXES, cSAXSFilterTransmission._POSITIONS_USER + ): + axis_obj = getattr(dev, axis_name, None) + out_position = positions[0] + if axis_obj is None or out_position is None: + continue + try: + rb = float(axis_obj.readback.get()) + except Exception: + continue + if abs(rb - out_position) > tol: + offending.append(axis_name) + return not offending, offending + + +def warn_and_confirm(setup, warning: str, interactive: bool = True, force: bool = False) -> bool: + """Eye/optics-check-style warn-and-confirm gate, reusable across plugins. + + Returns True if the caller should proceed. Interactive: red warning + a + "Continue anyway?" prompt defaulting to no. Non-interactive (queued/ + unattended): warns, waits 10s, and always proceeds -- matches + LamNI.tomo_scan()'s existing eye/optics gate so queued jobs never hang on + input(). + """ + if force: + return True + setup.OMNYTools.printredbold(f"WARNING: {warning}") + if interactive: + return setup.OMNYTools.yesno("Continue anyway?", "n") + setup.OMNYTools.printredbold("Proceeding automatically in 10 s (unattended/queued run)...") + time.sleep(10) + return True diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index 91a603ba..36c22c3e 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -19,6 +19,10 @@ from csaxs_bec.bec_ipython_client.plugins.cSAXS import cSAXSBeamlineChecks from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_optics_mixin import FlomniOpticsMixin from csaxs_bec.bec_ipython_client.plugins.flomni.gui_tools import flomniGuiTools from csaxs_bec.bec_ipython_client.plugins.flomni.x_ray_eye_align import XrayEyeAlign +from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.filter_check import ( + filters_out_of_beam, + warn_and_confirm, +) from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.tomo_queue_mixin import ( TomoQueueMixin, _GlobalVarParam, @@ -1787,7 +1791,7 @@ class Flomni( upload_url="https://omny.psi.ch/upload.php", local_port=8080, ) - self._webpage_gen.start() + #self._webpage_gen.start() self.OMNYTools = OMNYTools(self.client) self.reconstructor = PtychoReconstructor(self.ptycho_reconstruct_foldername) @@ -2197,6 +2201,13 @@ class Flomni( dev = builtins.__dict__.get("dev") bec = builtins.__dict__.get("bec") + all_out, offending = filters_out_of_beam(dev) + if not all_out: + warning = f"Not all filters are out of the beam: {', '.join(offending)}." + if not warn_and_confirm(self, warning, interactive=True, force=False): + print("Aborting tomo alignment scan.") + return + # Only run the full eye-out / optics-in transition if we are not # already in measurement condition with feedback running. That # transition disables and re-enables-with-reset the rt feedback, which @@ -2637,8 +2648,10 @@ class Flomni( Args: interactive: accepted for signature compatibility with LamNI.tomo_scan() (tomo_queue_execute() calls both the same - way) -- unused here, FlOMNI has no fine-alignment gate. + way). FlOMNI has no fine-alignment gate, but it is used by + the filter-out-of-beam check below. """ + dev = builtins.__dict__.get("dev") if not self._check_eye_out_and_optics_in(): print( @@ -2650,6 +2663,13 @@ class Flomni( print("Stopping.") return + all_out, offending = filters_out_of_beam(dev) + if not all_out: + warning = f"Not all filters are out of the beam: {', '.join(offending)}." + if not warn_and_confirm(self, warning, interactive=interactive, force=False): + print("Aborting tomo scan.") + return + self.flomnigui_show_progress() bec = builtins.__dict__.get("bec") @@ -3343,6 +3363,21 @@ class Flomni( _at_each_angle, and tomo_alignment_scan, which needs real ptycho data) pass _internal=True to skip the prompt. """ + if not _internal: + dev = builtins.__dict__.get("dev") + all_out, offending = filters_out_of_beam(dev) + if not all_out: + warning = f"Not all filters are out of the beam: {', '.join(offending)}." + if not warn_and_confirm(self, warning, interactive=True, force=False): + # Raise rather than return, for the same reason as the + # fermat-vs-single-point decline below: this can run + # inside a per-angle loop, and a bare return would leave + # the projection silently missing / the job stuck. + raise FlomniError( + "tomo_scan_projection: declined to proceed with filter(s) " + f"in the beam ({', '.join(offending)})." + ) + if not _internal and self.single_point_instead_of_fermat_scan: print( "\x1b[93mWarning: single_point_instead_of_fermat_scan is set, but"