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

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:
2026-09-10 16:38:35 +02:00
co-authored by Claude Opus 5
parent c619d25c96
commit 3cea598057
+69 -12
View File
@@ -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()