From 003717ff46f4ebca50792a65ea2388fdf7350175 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Mon, 28 Sep 2026 18:17:29 +0200 Subject: [PATCH] fix(positioner): validate deadband against tolerance --- .../base_classes/psi_positioner_base.py | 12 +++++--- tests/test_psi_positioner.py | 28 ++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/ophyd_devices/interfaces/base_classes/psi_positioner_base.py b/ophyd_devices/interfaces/base_classes/psi_positioner_base.py index 971b52e..bbf6ac4 100644 --- a/ophyd_devices/interfaces/base_classes/psi_positioner_base.py +++ b/ophyd_devices/interfaces/base_classes/psi_positioner_base.py @@ -142,7 +142,7 @@ class PSISimplePositionerBase(ABC, PSIDeviceBase, PositionerBase): Args: name (str): (required) the name of the device limits (list | tuple | None): If given, a length-2 sequence within the range of which movement is allowed. - deadband (float | None): If given, set a soft deadband of this absolute value, within which positioner moves will return immediately. If the positioner has no motor_done_move signal, you must provide this. + deadband (float | None): If given, set a soft deadband of this absolute value, within which positioner moves will return immediately. If the positioner has no motor_done_move signal, you must provide this. It must not exceed the configured tolerance when moving. use_put_completion (bool | None): If given, use put completion on the setpoint signal to resolve the move status. override_suffixes (dict[str, str]): a dictionary of signal_name: pv_suffix which will replace the values in the signal classvar. """ @@ -294,13 +294,17 @@ class PSISimplePositionerBase(ABC, PSIDeviceBase, PositionerBase): TimeoutError When motion takes longer than `timeout` ValueError - On invalid positions + On invalid positions or if the deadband exceeds the configured tolerance RuntimeError If motion fails other than timing out """ - if self._deadband is not None and abs(position - self._position) < self._deadband: - return MoveStatus(self, position, done=True, success=True) + if self._deadband is not None: + tolerance = self.tolerance.get() + if self._deadband > tolerance: + raise ValueError(f"Deadband {self._deadband} must not exceed tolerance {tolerance}") + if abs(position - self._position) < self._deadband: + return MoveStatus(self, position, done=True, success=True) if timeout is None: timeout = self._timeout diff --git a/tests/test_psi_positioner.py b/tests/test_psi_positioner.py index 9477ba0..e1a2eb9 100644 --- a/tests/test_psi_positioner.py +++ b/tests/test_psi_positioner.py @@ -178,7 +178,7 @@ def mock_done_signal_positioner_with_tolerance(): with patch.object(ophyd, "cl") as mock_cl: mock_cl.get_pv = MockPV mock_cl.thread_class = threading.Thread - dev = DoneSignalPositionerWithTolerance(name=name, prefix=prefix, deadband=0.01) + dev = DoneSignalPositionerWithTolerance(name=name, prefix=prefix, deadband=0.001) patch_dual_pvs(dev) dev.wait_for_connection() dev._set_position(0) @@ -287,3 +287,29 @@ def test_tolerance_checks_requested_target_even_if_setpoint_changes( assert st.target == 5 with pytest.raises(RuntimeError, match="outside of tolerance"): st.wait(timeout=1) + + +def test_move_rejects_tolerance_tighter_than_deadband_before_setpoint_write( + mock_done_signal_positioner_with_tolerance, +): + dev = mock_done_signal_positioner_with_tolerance + dev.tolerance.put(0.0005) + + with patch.object(dev.user_setpoint, "put", wraps=dev.user_setpoint.put) as put: + with pytest.raises(ValueError, match="tolerance"): + dev.move(5, wait=False) + put.assert_not_called() + + +def test_equal_deadband_and_tolerance_allow_instant_success( + mock_done_signal_positioner_with_tolerance, +): + dev = mock_done_signal_positioner_with_tolerance + assert dev.tolerance.get() == dev._deadband + + with patch.object(dev.user_setpoint, "put", wraps=dev.user_setpoint.put) as put: + st = dev.move(0.0005, wait=False) + put.assert_not_called() + + assert st.done + assert st.success