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 1a5c070..b18e5f1 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py @@ -162,16 +162,30 @@ class TomoParamsWidget(BECWidget, QWidget): super().__init__(parent=parent, **kwargs) self.get_bec_shortcuts() self._edit_mode = False - # widget refs populated by _build_params_panel self._pw: dict[str, QWidget] = {} # key -> input widget + self._queue_dlg: Optional["TomoQueueDialog"] = None + # check whether the FlOMNI setup is active before building the UI + self._flomni_available = self._check_flomni_available() self._build_ui() - # polling timer – does NOT refresh params (avoid overwriting edits) self._poll_timer = QTimer(self) self._poll_timer.setInterval(self._POLL_INTERVAL_MS) self._poll_timer.timeout.connect(self._on_poll) - self._poll_timer.start() - # initial load - self.refresh() + if self._flomni_available: + self._poll_timer.start() + self.refresh() + + # ── setup detection ────────────────────────────────────────────────────── + + def _check_flomni_available(self) -> bool: + """ + Return True if the FlOMNI setup is active in this BEC session. + Uses ``fsamroy`` (the FlOMNI-specific rotation stage) as the + discriminator — it is absent in OMNY and LamNI sessions. + """ + try: + return getattr(self.dev, "fsamroy", None) is not None + except Exception: + return False # ── global-var I/O ─────────────────────────────────────────────────────── @@ -212,6 +226,19 @@ class TomoParamsWidget(BECWidget, QWidget): root = QVBoxLayout(self) root.setSpacing(6) + if not self._flomni_available: + lbl = QLabel( + "⚠ FlOMNI setup not detected in this BEC session.\n\n" + "This widget is only meaningful when the FlOMNI\n" + "BEC session is active (fsamroy device present)." + ) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + lbl.setWordWrap(True) + root.addStretch() + root.addWidget(lbl) + root.addStretch() + return + # scrollable params panel params_scroll = QScrollArea() params_scroll.setWidgetResizable(True) @@ -230,7 +257,6 @@ class TomoParamsWidget(BECWidget, QWidget): self._btn_queue.setToolTip("Open the tomo queue manager") self._btn_queue.clicked.connect(self._show_queue_dialog) root.addWidget(self._btn_queue) - self._queue_dlg: Optional["TomoQueueDialog"] = None def _build_header(self) -> QGroupBox: box = QGroupBox("Sample") @@ -340,6 +366,13 @@ class TomoParamsWidget(BECWidget, QWidget): form = QFormLayout(box) form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) + self._add_int( + form, + "_numprj_type3", + "Projections per sub-tomo (type 3)", + min_=1, + max_=10000, + ) self._add_int( form, "golden_ratio_bunch_size", "Bunch size (type 2 only)", min_=1, max_=10000 ) @@ -448,6 +481,15 @@ class TomoParamsWidget(BECWidget, QWidget): self._pw["_requested_total"].blockSignals(False) self._update_projection_preview() + + # type-3: derive projections-per-subtomo from stored tomo_angle_stepsize + # (flomni uses 180 / tomo_angle_stepsize for type 3, hardcoded to 180) + stepsize = float(params.get("tomo_angle_stepsize", 10.0)) + numprj_t3 = int(180.0 / stepsize) if stepsize > 0 else 18 + self._pw["_numprj_type3"].blockSignals(True) + self._pw["_numprj_type3"].setValue(numprj_t3) + self._pw["_numprj_type3"].blockSignals(False) + self._update_type_visibility(type_val) def _read_fields(self) -> dict[str, Any]: @@ -470,12 +512,19 @@ class TomoParamsWidget(BECWidget, QWidget): elif isinstance(widget, QComboBox): params[key] = widget.currentData() - # derive tomo_angle_stepsize from requested_total + angle_range + # derive tomo_angle_stepsize from requested_total + angle_range (type 1) angle_range = int(params.get("tomo_angle_range", 180)) requested = self._pw["_requested_total"].value() stepsize = _requested_to_stepsize(angle_range, requested) params["tomo_angle_stepsize"] = stepsize + # type 3: override tomo_angle_stepsize from projections-per-subtomo + # (flomni uses 180 / numprj for type 3) + if params.get("tomo_type") == 3: + numprj = self._pw["_numprj_type3"].value() + if numprj > 0: + params["tomo_angle_stepsize"] = 180.0 / numprj + # single_point forces stitch to 0 if params.get("single_point_instead_of_fermat_scan"): params["stitch_x"] = 0 @@ -508,19 +557,30 @@ class TomoParamsWidget(BECWidget, QWidget): def _update_type_visibility(self, tomo_type: int) -> None: self._sec_type1.setVisible(tomo_type == 1) self._sec_type23.setVisible(tomo_type in (2, 3)) - # bunch_size is type-2 only; find its row in the form layout - bunch_widget = self._pw.get("golden_ratio_bunch_size") - if bunch_widget is not None: - bunch_widget.setVisible(tomo_type == 2) - # also hide the label in the form layout - form = self._sec_type23.layout() - if isinstance(form, QFormLayout): - idx = form.indexOf(bunch_widget) - if idx >= 0: - row, role = form.getItemPosition(idx) - label_item = form.itemAt(row, QFormLayout.ItemRole.LabelRole) - if label_item and label_item.widget(): - label_item.widget().setVisible(tomo_type == 2) + # _numprj_type3: only visible for type 3 + _w__numprj_type3 = self._pw.get("_numprj_type3") + if _w__numprj_type3 is not None: + _w__numprj_type3.setVisible(tomo_type == 3) + _form = self._sec_type23.layout() + if isinstance(_form, QFormLayout): + _idx = _form.indexOf(_w__numprj_type3) + if _idx >= 0: + _row, _role = _form.getItemPosition(_idx) + _lbl = _form.itemAt(_row, QFormLayout.ItemRole.LabelRole) + if _lbl and _lbl.widget(): + _lbl.widget().setVisible(tomo_type == 3) + # golden_ratio_bunch_size: only visible for type 2 + _w_golden_ratio_bunch_size = self._pw.get("golden_ratio_bunch_size") + if _w_golden_ratio_bunch_size is not None: + _w_golden_ratio_bunch_size.setVisible(tomo_type == 2) + _form = self._sec_type23.layout() + if isinstance(_form, QFormLayout): + _idx = _form.indexOf(_w_golden_ratio_bunch_size) + if _idx >= 0: + _row, _role = _form.getItemPosition(_idx) + _lbl = _form.itemAt(_row, QFormLayout.ItemRole.LabelRole) + if _lbl and _lbl.widget(): + _lbl.widget().setVisible(tomo_type == 2) # ── type-1 projection preview ─────────────────────────────────────────────