diff --git a/src/aare/gui/panels/smart_rotation_panel.py b/src/aare/gui/panels/smart_rotation_panel.py index 625f9fd5..c031f6e2 100644 --- a/src/aare/gui/panels/smart_rotation_panel.py +++ b/src/aare/gui/panels/smart_rotation_panel.py @@ -55,6 +55,15 @@ DEFAULT_MIN_EXP_TIME_S = 0.0005 # there. Stated explicitly so it does not silently drop to 1.2 ms if # EXP_TIME_STEP_S is ever made finer. STRATEGY_MIN_EXP_TIME_S = {MXBeamline.X06DA: 0.002} +# Entry limits for the visible-resolution field. Named because the clamp on +# the DAQ value has to use the same numbers as the widget's validator. +VISIBLE_RES_MIN_A = 0.8 +VISIBLE_RES_MAX_A = 10.0 +VISIBLE_RES_DEFAULT_A = 2.0 +# Two DAQ resolutions closer than this are the same raster result reported +# again, not a new one. +DAQ_RES_EPS_A = 0.001 +VISIBLE_RES_LABEL = "Visible resolution" DTZ_STEP_MM = 10.0 EXP_TIME_STEP_S = 0.001 TRANSMISSION_SIG_DIGITS = 2 @@ -98,6 +107,10 @@ class SimpleRotationSettingsPanel(QWidget): super().__init__(parent) self._beamline = mx_beamline() + self._daq_best_res = None + # Distinguishes "no DAQ resolution yet" from "DAQ reports none", so the + # first status tick always paints the label. + self._daq_res_applied = False self.n_images = 1 self.transmission = 1.0 self.delivered_dose_MGy = 0.0 @@ -126,10 +139,17 @@ class SimpleRotationSettingsPanel(QWidget): m = self._layout.contentsMargins() self._layout.setContentsMargins(m.left(), 0, m.right(), 3) - # Visible resolution (entry) - self._layout.addWidget(QLabel("Visible resolution", parent=self), 0, 0) + # Visible resolution (entry). The label carries the provenance of the + # value: silence means it came from the DAQ, otherwise it says so. + self.visible_res_label = QLabel(VISIBLE_RES_LABEL, parent=self) + self._layout.addWidget(self.visible_res_label, 0, 0) self.visible_res_enter = NumberLineEdit( - 0.8, 10.0, decimals=2, default=2.0, parent=self, track_pending=True + VISIBLE_RES_MIN_A, + VISIBLE_RES_MAX_A, + decimals=2, + default=VISIBLE_RES_DEFAULT_A, + parent=self, + track_pending=True, ) self._layout.addWidget(self.visible_res_enter, 0, 1, 1, 3) self._layout.addWidget(QLabel("Å", parent=self), 0, 4) @@ -300,18 +320,10 @@ class SimpleRotationSettingsPanel(QWidget): @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel): self._d = s - # TODO only update best_res after raster finished otherwise ask to update. or have toggle to overwrite with user value # TODO only take best res from flat face scan # TODO identify flat face! - best_res = s.last_best_res self._omega = s.geom.omega_deg - - if best_res is not None: - # clamp to control limits and update field; this will also trigger recalculation - lo, hi = 0.8, 10.0 - v = max(lo, min(hi, float(best_res))) - self.visible_res_enter.update_value(v) - self.set_visible_resolution(v) + self._apply_daq_resolution(s.last_best_res) self._wilson_b = s.last_best_b_factor @@ -337,6 +349,51 @@ class SimpleRotationSettingsPanel(QWidget): self.wavelength_label.setText(f"{s.diffraction.wavelength_angstrom:.3f}") self.update_calculated_labels() + def _apply_daq_resolution(self, best_res: float | None) -> None: + """Track ``DAQStatusModel.last_best_res`` in the resolution field. + + Written on every *change* of the DAQ value rather than on every 2 Hz + tick, so a fresh raster result always lands while a repeat of the same + value leaves a manual override alone. + + Two things this deliberately does not use: + + * ``update_value`` compares against ``saved_value``, which + ``on_editing_finished`` never updates (it self-assigns), so it only + ever sees programmatic writes. Once the operator has typed over a + pushed value, the same DAQ value could never re-assert itself. + ``set_committed_value`` compares against what the field actually + shows, which is the question being asked here. + * A bare ``is not None`` guard. The DAQ clears ``last_best_res`` on + mount, and skipping the update then leaves the previous crystal's + resolution in the field with nothing to say it is stale. + """ + if not self._daq_res_applied: + unchanged = False + elif best_res is None or self._daq_best_res is None: + unchanged = best_res is None and self._daq_best_res is None + else: + unchanged = abs(best_res - self._daq_best_res) <= DAQ_RES_EPS_A + if unchanged: + return + + self._daq_res_applied = True + self._daq_best_res = best_res + if best_res is None: + # No raster result for this sample: either none has run yet, or the + # pgroup gate in the status endpoint stripped it. Either way the + # field is showing a default, and the strategy below is built on a + # number nothing measured -- say so instead of looking authoritative. + value = self.visible_res_enter.get_default() + self.visible_res_label.setText( + f"""{VISIBLE_RES_LABEL} (no raster)""" + ) + else: + value = max(VISIBLE_RES_MIN_A, min(VISIBLE_RES_MAX_A, float(best_res))) + self.visible_res_label.setText(VISIBLE_RES_LABEL) + self.visible_res_enter.set_committed_value(value) + self.update_calculated_labels() + @Slot(float) def set_visible_resolution(self, v: float): self.update_calculated_labels()