feat: auto-centering row with apply-after-mount, grid button raster-only

Auto Centering moves out of the experiment tabs to its own row between
Manual sample and Exp. Config., with an apply-after-mount checkbox that
re-runs it on every detected sample mount (guarded against firing on GUI
startup). Draw a Grid now only shows on the Raster scan tab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 16:21:21 +02:00
co-authored by Claude Fable 5
parent 36a4a5fbb8
commit 623f803ceb
2 changed files with 100 additions and 15 deletions
+43 -13
View File
@@ -3,6 +3,7 @@ from aarecommon.math.sample_geometry import SampleGeometryModel
from aarecommon.models.models import DAQStatusModel
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import (
QCheckBox,
QDoubleSpinBox,
QFrame,
QHBoxLayout,
@@ -55,9 +56,24 @@ class DataCollectionSettings(QFrame):
self.manual_sample_panel = ManualSamplePanel(self)
v_layout.addWidget(self.manual_sample_panel)
# QTabBar + QStackedWidget instead of QTabWidget: the loop-centering
# button row must sit BETWEEN the tab bar and the pages, which a
# QTabWidget cannot host.
# Auto Centering applies to every scan type, so it lives outside the
# experiment tabs, between Manual sample and Exp. Config. The checkbox
# arms an automatic run after each sample mount (see update_daq_status).
self.find_tip = QPushButton("Auto Centering", parent=self)
self.auto_center_after_mount = QCheckBox("apply after mount", parent=self)
self.auto_center_after_mount.setToolTip(
"Run Auto Centering automatically after each sample mount"
)
auto_center_row = QWidget(self)
auto_center_layout = QHBoxLayout(auto_center_row)
auto_center_layout.setContentsMargins(0, 0, 0, 0)
auto_center_layout.addWidget(self.find_tip, 1)
auto_center_layout.addWidget(self.auto_center_after_mount)
v_layout.addWidget(auto_center_row)
# QTabBar + QStackedWidget instead of QTabWidget: the grid button
# must sit BETWEEN the tab bar and the pages, which a QTabWidget
# cannot host.
self._tab_bar = QTabBar(self)
self._stack = QStackedWidget(self)
@@ -76,14 +92,9 @@ class DataCollectionSettings(QFrame):
self._stack.addWidget(panel)
self._tab_bar.addTab(label)
# Ex-"Loop centering" panel buttons; always visible, whatever the tab.
self.find_tip = QPushButton("Auto Centering", parent=self)
# Only meaningful for raster scans; hidden on the other tabs
# (_on_tab_changed).
self.bounding_box = QPushButton("Draw a Grid", parent=self)
centering_row = QWidget(self)
centering_layout = QHBoxLayout(centering_row)
centering_layout.setContentsMargins(0, 0, 0, 0)
centering_layout.addWidget(self.find_tip)
centering_layout.addWidget(self.bounding_box)
# Live readout mirrored from the Beamline setup panel, same reason:
# "what is" and "what to set" must not share one ambiguous row.
@@ -120,7 +131,7 @@ class DataCollectionSettings(QFrame):
# No bottom padding: the pages' own bottom margins breathe inside the
# border, and the Abort button should hug the pane.
pane_layout.setContentsMargins(6, 6, 6, 0)
pane_layout.addWidget(centering_row)
pane_layout.addWidget(self.bounding_box)
pane_layout.addWidget(current_energy_row)
pane_layout.addWidget(energy_row)
pane_layout.addWidget(self._stack)
@@ -163,6 +174,10 @@ class DataCollectionSettings(QFrame):
self.file_path_panel.path_updated.connect(self.screening.update_filename)
self.file_path_panel.path_updated.connect(self.simple.update_filename)
self._sample_id = None
# False until the first DAQ status: a sample already mounted at GUI
# startup must not trigger an auto-centering (hardware would move
# uninvited on every restart).
self._status_seen = False
self._tab_bar.currentChanged.connect(self._stack.setCurrentIndex)
self._tab_bar.currentChanged.connect(self._on_tab_changed)
@@ -208,12 +223,27 @@ class DataCollectionSettings(QFrame):
self.raster.update_daq_status(s)
self.screening.update_daq_status(s)
self.simple.update_daq_status(s)
if s.sample is not None and s.sample.db_id != self._sample_id:
self._sample_id = s.sample.db_id
self._track_sample(None if s.sample is None else s.sample.db_id)
def _track_sample(self, db_id: int | None):
# Separate from update_daq_status so tests can drive mount detection
# without building a full DAQStatusModel.
if db_id is None:
# Forget on unmount so remounting the same sample counts as a
# fresh mount below.
self._sample_id = None
elif db_id != self._sample_id:
self._sample_id = db_id
self._tab_bar.setCurrentIndex(0)
if self._status_seen and self.auto_center_after_mount.isChecked():
# Reuse the button wiring (clicked -> daq.center_loop in
# main_window) instead of a second signal path.
self.find_tip.click()
self._status_seen = True
@Slot(int)
def _on_tab_changed(self, idx: int):
self.bounding_box.setVisible(idx == 0)
# 0: Raster, 1: Rotation (incl. screening), 2: Simple (rotation wrapper), 3: XRF (ignore)
kind = "rotation"
if idx == 0:
@@ -12,6 +12,7 @@ from aarecommon.math.coordinate import Coordinate, SmargonCoordinate
from aarecommon.math.diffraction_geometry import DiffractionGeometry
from aarecommon.models.models import SampleGeometryModel
from aare.gui.panels.data_collection_settings import DataCollectionSettings
from aare.gui.panels.raster_data_collection import RasterDataCollectionPanel
from aare.gui.panels.rotation_data_collection import RotationDataCollectionPanel
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
@@ -171,8 +172,8 @@ def test_user_override_persists_across_samples(panel, diffraction):
@pytest.fixture
def raster_panel(qapp, diffraction):
geom = SampleGeometryModel(
def geom():
return SampleGeometryModel(
beam_location_pxl=Coordinate(x=500, y=500),
pixel_in_mm=0.001,
aerotech=Coordinate(x=0, y=0, z=0),
@@ -181,6 +182,10 @@ def raster_panel(qapp, diffraction):
omega_deg=0.0,
beam_size_mm=Coordinate(x=0.01, y=0.01),
)
@pytest.fixture
def raster_panel(qapp, diffraction, geom):
mgr = RasterGridManager(geom)
return RasterDataCollectionPanel(raster_mgr=mgr, diffraction=diffraction)
@@ -212,3 +217,53 @@ def test_grid_size_field_shares_panel_toggle(raster_panel):
_edit(raster_panel.high_res_enter, "2.50")
assert raster_panel._source == DbOverrideLineEdit.SOURCE_MINE
assert raster_panel.width_enter.source() == DbOverrideLineEdit.SOURCE_MINE
# ---------------------------------------------------------------------------
# DataCollectionSettings: per-tab grid button + auto-center after mount
# ---------------------------------------------------------------------------
@pytest.fixture
def settings_panel(qapp, diffraction, geom):
return DataCollectionSettings(geom, RasterGridManager(geom), diffraction)
def test_grid_button_only_on_raster_tab(settings_panel):
assert not settings_panel.bounding_box.isHidden() # Raster is the default tab
for idx in (1, 2, 3):
settings_panel._tab_bar.setCurrentIndex(idx)
assert settings_panel.bounding_box.isHidden()
settings_panel._tab_bar.setCurrentIndex(0)
assert not settings_panel.bounding_box.isHidden()
def test_auto_center_fires_on_mount_only_when_armed(settings_panel):
clicks = []
settings_panel.find_tip.clicked.connect(lambda: clicks.append(1))
settings_panel.auto_center_after_mount.setChecked(True)
# First status with a sample already mounted = GUI (re)start: hardware
# must not move, only the id is recorded.
settings_panel._track_sample(1)
assert not clicks
# Real mount after an unmount fires the centering.
settings_panel._track_sample(None)
settings_panel._track_sample(2)
assert len(clicks) == 1
# Direct sample exchange (no unmount tick in between) also fires.
settings_panel._track_sample(3)
assert len(clicks) == 2
# Remounting the SAME sample counts as a fresh mount.
settings_panel._track_sample(None)
settings_panel._track_sample(3)
assert len(clicks) == 3
# Disarmed: mounts no longer trigger.
settings_panel.auto_center_after_mount.setChecked(False)
settings_panel._track_sample(None)
settings_panel._track_sample(4)
assert len(clicks) == 3