From a5fcc730499f0d472da38f63a61390a074c33729 Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 1 Oct 2026 15:38:56 +0200 Subject: [PATCH] feat(flomni): warn on OSA collision risk when picking a foptz target The tomo-queue "move_foptz_with_shift" dialog now shows a live warning when the entered foptz target would leave <=0 mm OSA clearance against a normal OMNY pin (same collision check fosa_in() already enforces for fosaz moves, surfaced here at data-entry time instead). Advisory only -- the queued action itself stays a plain absolute move, not gated on this. Extracted the clearance formula out of FlomniOpticsMixin._osa_remaining_space into a module-level compute_osa_remaining_space(foptz_val, fosaz_val) so the GUI (a separate process with only RPC device access, no Flomni instance to call a bound method on) can reuse the exact same formula instead of duplicating it. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug --- .../plugins/flomni/flomni_optics_mixin.py | 18 ++++++- .../widgets/tomo_params/tomo_params.py | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py index 2853a47d..f31dd04f 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py @@ -9,6 +9,22 @@ class FlomniOpticsError(Exception): pass +def compute_osa_remaining_space(foptz_val: float, fosaz_val: float) -> float: + """Clearance (mm) between the OSA and a normal OMNY pin for a given + foptz/fosaz pair. <=0 means collision. + + Module-level (not a method) so it can be imported and reused outside a + live Flomni session -- e.g. by the tomo-queue GUI (tomo_params.py), + which only has RPC device access (dev.foptz.readback.get()/ + dev.fosaz.readback.get()), not a Flomni instance to call a bound method + on. FlomniOpticsMixin's own methods below are thin wrappers around this + for the two directions actually used in a live session (varying fosaz + at the current foptz, and vice versa) -- single source of truth for the + formula/constant either way. + """ + return 33 - foptz_val - fosaz_val + + class FlomniOpticsMixin: @staticmethod def _get_user_param_safe(device, var): @@ -21,7 +37,7 @@ class FlomniOpticsMixin: """Clearance (mm) between the OSA and a normal OMNY pin if fosaz were at `fosaz_target`, at the *current* foptz reading. <=0 means collision.""" foptz_val = dev.foptz.readback.get() - return -fosaz_target + (33 - foptz_val) + return compute_osa_remaining_space(foptz_val, fosaz_target) def _fheater_is_up(self) -> bool: axis_id = dev.fheater._config["deviceConfig"].get("axis_Id") diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py index 7e1e16ae..b25b1aea 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py @@ -2186,6 +2186,53 @@ class CommandJobBuilderDialog(QDialog): label = f"{label} ({unit})" self._field_form.addRow(f"{label}:", widget) + if action_name == "move_foptz_with_shift" and "foptz_target" in self._step_field_widgets: + self._build_osa_collision_warning() + + def _build_osa_collision_warning(self) -> None: + """Live, advisory-only OSA-collision warning for + move_foptz_with_shift's foptz_target field. + + The move itself stays a plain absolute move (see + _queue_action_move_foptz_with_shift's docstring) -- this just + surfaces, at data-entry time, the same clearance check + FlomniOpticsMixin.fosa_in() enforces (by refusing) for fosaz moves, + so an operator can see a likely OSA collision before queuing a + foptz target, without the queue action itself blocking on it. + """ + from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_optics_mixin import ( + compute_osa_remaining_space, + ) + + lbl = QLabel() + lbl.setWordWrap(True) + lbl.setStyleSheet("color: #B8860B;") + lbl.setVisible(False) + self._field_form.addRow("", lbl) + + foptz_widget, _field = self._step_field_widgets["foptz_target"] + + def _update(_value=None): + fosaz = self._client.device_manager.devices.get("fosaz") + if fosaz is None: + return + try: + fosaz_val = fosaz.readback.get() + except Exception: + return + clearance = compute_osa_remaining_space(foptz_widget.value(), fosaz_val) + if clearance <= 0: + lbl.setText( + f"Warning: at this foptz target, OSA clearance would be " + f"{clearance:.2f} mm (<=0 = collision risk with a normal OMNY pin)." + ) + lbl.setVisible(True) + else: + lbl.setVisible(False) + + foptz_widget.valueChanged.connect(_update) + _update() + def _build_field_widget(self, field: dict) -> QWidget: field_type = field.get("type") if field_type == "float":