Files
AareDAQ/tests/unit/gui/test_data_collection_settings.py
appleb_mandClaude Opus 4.8 476aa69d70 data collection settings: panel-level Database/User-values toggle
Add DbOverrideLineEdit: a numeric field holding both a database value and
the user's value, driven by a per-tab Database/User-values toggle. The
user's value persists across samples, can be reverted to the database
value, and recovered again - replacing the clunky per-field checkboxes.

Fix the dtz<->resolution coupling so editing one updates the other in both
Database and User modes (previously it broke whenever a value was
overridden). Wire resolution, detector distance, transmission, exposure,
rotation total/image angle and the raster grid element size through the
new widget; other panels are untouched.

Add tests covering persist/revert/recover, the toggle's downstream emit,
the dtz/resolution coupling in both modes, and cross-sample persistence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:30:08 +02:00

219 lines
8.0 KiB
Python

"""Tests for the data-collection settings override model.
Covers DbOverrideLineEdit (database value vs user's value, persistence across
samples, go-back-to-DB, recover-my-value) and the panel-level Database/My-values
toggle with the dtz<->resolution coupling that must hold in both modes.
"""
import types
import pytest
from aare.common.coordinate import Coordinate, SmargonCoordinate
from aare.common.diffraction_geometry import DiffractionGeometry
from aare.common.models import SampleGeometryModel
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
from aare.gui.widgets.number_line_edit import DbOverrideLineEdit
def _edit(field: DbOverrideLineEdit, text: str):
"""Simulate a user typing into and committing a field."""
field.editor.setText(text)
field.editor.on_editing_finished()
# ---------------------------------------------------------------------------
# Widget: DbOverrideLineEdit
# ---------------------------------------------------------------------------
def test_db_override_follows_database_until_edited(qapp):
w = DbOverrideLineEdit(0, 1000, default=200.0, decimals=2)
assert w.source() == DbOverrideLineEdit.SOURCE_DB
assert not w.has_user_value()
w.set_db_value(150.0)
assert w.value == 150.0 # untouched field tracks the database
def test_db_override_user_value_persists_and_recovers(qapp):
w = DbOverrideLineEdit(0, 1000, default=200.0, decimals=2)
_edit(w, "300.00")
assert w.has_user_value()
assert w.source() == DbOverrideLineEdit.SOURCE_MINE
assert w.value == 300.0
# A new database value (e.g. next sample) must not clobber the user's value.
w.set_db_value(120.0)
assert w.value == 300.0
# Go back to the database value, then recover the user's value.
w.set_source(DbOverrideLineEdit.SOURCE_DB, emit=False)
assert w.value == 120.0
w.set_source(DbOverrideLineEdit.SOURCE_MINE, emit=False)
assert w.value == 300.0
def test_db_override_reset_forgets_user_value(qapp):
w = DbOverrideLineEdit(0, 1000, default=200.0, decimals=2)
_edit(w, "300.00")
w.reset()
assert w.value == 200.0
assert w.source() == DbOverrideLineEdit.SOURCE_DB
assert not w.has_user_value()
def test_db_override_emits_value_changed_on_toggle(qapp):
w = DbOverrideLineEdit(0, 1000, default=200.0, decimals=2)
_edit(w, "300.00") # mine = 300, db = 200
seen = []
w.valueChanged.connect(lambda v: seen.append(v))
w.set_source(DbOverrideLineEdit.SOURCE_DB)
assert seen[-1] == 200.0 # toggling pushes the now-active value downstream
w.set_source(DbOverrideLineEdit.SOURCE_MINE)
assert seen[-1] == 300.0
# ---------------------------------------------------------------------------
# Panel: toggle + dtz/resolution coupling
# ---------------------------------------------------------------------------
@pytest.fixture
def diffraction():
return DiffractionGeometry(
energy_keV=12.0,
dtz_mm=150.0,
pixel_size_mm=0.075,
beam_center_pxl=(1000.0, 1000.0),
detector_size_pxl=(2000, 2000),
detector_description="Eiger 16M",
detector_serial_number="123",
poni_rot1_rad=0.0,
poni_rot2_rad=0.0,
)
@pytest.fixture
def panel(qapp, diffraction):
return RotationDataCollectionPanel(diffraction=diffraction)
def test_editing_dtz_updates_resolution_and_switches_to_mine(panel, diffraction):
_edit(panel.dtz_enter, "250.00")
assert panel._source == DbOverrideLineEdit.SOURCE_MINE
assert abs(panel.high_res_enter.value - diffraction.resolution_angstrom(250.0)) < 0.01
def test_editing_resolution_updates_dtz(panel, diffraction):
_edit(panel.high_res_enter, "2.00")
expected = max(panel.MIN_DTZ, diffraction.calc_dtz_mm(2.0))
assert abs(panel.dtz_enter.value - expected) < 0.01
def test_coupling_holds_in_database_mode(panel, diffraction):
# Set a database resolution; dtz database value must stay consistent.
panel._apply_db_resolution(2.5)
assert panel._source == DbOverrideLineEdit.SOURCE_DB
expected_dtz = max(panel.MIN_DTZ, diffraction.calc_dtz_mm(2.5))
assert abs(panel.dtz_enter.value - expected_dtz) < 0.01
assert abs(panel.high_res_enter.value - 2.5) < 0.01
def test_toggle_pushes_active_value_downstream(panel):
emitted = []
panel.dtz_updated.connect(lambda v: emitted.append(round(v, 2)))
_edit(panel.dtz_enter, "250.00") # mine dtz = 250
mine_dtz = panel.dtz_enter.value
panel.set_source(DbOverrideLineEdit.SOURCE_DB)
db_dtz = panel.dtz_enter.value
assert emitted[-1] == round(db_dtz, 2) # downstream got the db value
panel.set_source(DbOverrideLineEdit.SOURCE_MINE)
assert panel.dtz_enter.value == mine_dtz # user value recovered
assert emitted[-1] == round(mine_dtz, 2) # downstream got the user value
def test_user_override_persists_across_samples(panel, diffraction):
# Sample 1 loads a database resolution.
panel._sample = types.SimpleNamespace(db_id=1)
panel._params = types.SimpleNamespace(
targetresolution=2.5, transmission=0.5,
totalrange=180.0, oscillation=0.1, exposure=0.02,
)
panel.update_data_collection_parameters()
# User overrides the exposure time.
_edit(panel.image_time_enter, "0.0500")
assert panel.image_time_enter.value == 0.05
# Sample 2 arrives with a different database exposure.
panel._sample = types.SimpleNamespace(db_id=2)
panel._params = types.SimpleNamespace(
targetresolution=1.8, transmission=1.0,
totalrange=360.0, oscillation=0.2, exposure=0.01,
)
panel.update_data_collection_parameters()
# In My-values mode the user's exposure persists across the sample change.
assert panel.image_time_enter.value == 0.05
# Going back to Database shows the new sample's database value.
panel.set_source(DbOverrideLineEdit.SOURCE_DB)
assert panel.image_time_enter.value == 0.01
# ---------------------------------------------------------------------------
# Raster grid element size follows the same override model
# ---------------------------------------------------------------------------
@pytest.fixture
def raster_panel(qapp, diffraction):
geom = SampleGeometryModel(
beam_location_pxl=Coordinate(x=500, y=500),
pixel_in_mm=0.001,
aerotech=Coordinate(x=0, y=0, z=0),
aerotech_meas=Coordinate(x=0, y=0, z=0),
smargon=SmargonCoordinate(
sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0.0, chi_deg=0.0
),
omega_deg=0.0,
beam_size_mm=Coordinate(x=0.01, y=0.01),
)
mgr = RasterGridManager(geom)
return RasterDataCollectionPanel(raster_mgr=mgr, diffraction=diffraction)
def test_grid_element_size_user_value_persists_over_geometry(raster_panel):
emitted = []
raster_panel.grid_size_updated.connect(lambda x, y: emitted.append((x, y)))
_edit(raster_panel.width_enter, "20")
assert raster_panel._source == DbOverrideLineEdit.SOURCE_MINE
assert raster_panel.width_enter.value == 20.0
assert emitted # editing pushed the new element size downstream
# The live grid geometry keeps reporting a different element size; the
# user's value must persist (only the database side changes).
raster_panel.width_enter.set_db_value(35.0)
assert raster_panel.width_enter.value == 20.0
# Revert to the geometry value, then recover the user's value.
raster_panel.set_source(DbOverrideLineEdit.SOURCE_DB)
assert raster_panel.width_enter.value == 35.0
raster_panel.set_source(DbOverrideLineEdit.SOURCE_MINE)
assert raster_panel.width_enter.value == 20.0
def test_grid_size_field_shares_panel_toggle(raster_panel):
# Editing the resolution flips the whole panel to "My values", which the
# grid-size field shares.
_edit(raster_panel.high_res_enter, "2.50")
assert raster_panel._source == DbOverrideLineEdit.SOURCE_MINE
assert raster_panel.width_enter.source() == DbOverrideLineEdit.SOURCE_MINE