fix(gui): keep the smart panel resolution tied to DAQStatus
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
The visible-resolution field is fed from DAQStatusModel.last_best_res, but the update was guarded by a bare `is not None`. main now clears last_best_res on mount (daq.py), so on a fresh sample the DAQ reports "no resolution" and the panel silently kept the previous crystal's number -- and then sized detector distance, exposure and the dose budget from it. Track the DAQ value explicitly instead: - Push on a change of the DAQ value rather than on every 2 Hz tick, so a new raster result always lands while a repeat leaves a manual override alone. - Handle None: reset the field to its default instead of leaving a stale value, and mark the row "(no raster)" in the alert colour. There was previously nothing to distinguish a measured resolution from a default or a typed one, which is what made this hard to spot at the beamline; it also shows when the status endpoint's pgroup gate has stripped the value. - Use set_committed_value, which compares against what the field actually shows, rather than update_value, which compares against saved_value. on_editing_finished self-assigns saved_value (number_line_edit.py:102), so it only ever reflects programmatic writes. - Name the entry limits so the clamp on the DAQ value cannot drift from the widget's own validator range. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018JVrLMLGNUXhfJjWxUcTBj
This commit is contained in:
@@ -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"<b>{s.diffraction.wavelength_angstrom:.3f}</b>")
|
||||
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} <span style="color: {STATUS_ALERT} ; ">(no raster)</span>"""
|
||||
)
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user