diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index a7b1e7ba..b3eb69b8 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -524,6 +524,9 @@ class A3200Motor(Device, PositionerBase): position error to settle within tolerance before the position is compared with the target. The controller's In Position status is not used: its InPositionDistance can be tighter than the axis settles (X: 10 nm). + in_position_hold: position error and distance to the target must stay within + tolerance this long (s) before the move counts as settled. Default 0.1 s: after + a hard stop X rings at ~10 Hz, a single reading can be a zero crossing. acceleration: ramp rate for moves in user units/s^2; None keeps the controller's ramp settings (DefaultRampRate). max_scan_velocity: highest velocity a continuous scan may set on this axis, in user @@ -580,6 +583,7 @@ class A3200Motor(Device, PositionerBase): egu="mm", tolerance=0.001, in_position_timeout=2.0, + in_position_hold=0.1, require_homed=True, acceleration=None, max_scan_velocity=None, @@ -598,6 +602,7 @@ class A3200Motor(Device, PositionerBase): self.sign = sign self.tolerance = tolerance self.in_position_timeout = in_position_timeout + self.in_position_hold = in_position_hold self.require_homed = require_homed self.pso_input = pso_input self.pso_counts_divider = pso_counts_divider @@ -696,13 +701,29 @@ class A3200Motor(Device, PositionerBase): logger.warning(f"{self.name} stopped with faults {faults}.") self._done_moving(success=not faults) return - # the profile is done, the axis may still be settling + # the profile is done, the axis may still be settling: after a hard stop X rings + # at ~10 Hz with ~20 um, so a single small position error can be a zero crossing. + # Settled = position error and distance to the target within the tolerance for + # in_position_hold without interruption. deadline = time.time() + self.in_position_timeout - while abs(self.controller.position_error(self.axis_Id)) > self.tolerance: - if time.time() > deadline: + settled_since = None + while True: + now = time.time() + in_tolerance = ( + abs(self.controller.position_error(self.axis_Id)) <= self.tolerance + and abs(self.readback.get() - position) <= self.tolerance + ) + if not in_tolerance: + settled_since = None + elif settled_since is None: + settled_since = now + if settled_since is not None and now - settled_since >= self.in_position_hold: + break + if now > deadline: logger.warning( - f"{self.name}: position error not within {self.tolerance}" - f" {self.egu} {self.in_position_timeout} s after the end of the move." + f"{self.name}: not settled within {self.tolerance} {self.egu} for" + f" {self.in_position_hold} s, {self.in_position_timeout} s after the end" + " of the move." ) break time.sleep(0.02) diff --git a/tests/tests_devices/test_a3200.py b/tests/tests_devices/test_a3200.py index a5f8c28a..e69a744f 100644 --- a/tests/tests_devices/test_a3200.py +++ b/tests/tests_devices/test_a3200.py @@ -377,6 +377,18 @@ def test_move_waits_for_position_error_within_tolerance(): assert motor.controller.position_error("X") == 0.0 +def test_move_does_not_settle_on_a_zero_crossing(): + # ringing after the stop: one reading within tolerance, then out again, then settled + motor = _sim_motor(velocity=100.0, tolerance=0.001, in_position_hold=0.1) + errors = iter([0.0] + [0.01] * 5) + motor.controller.position_error = lambda axis: next(errors, 0.0) + start = time.time() + status = motor.move(1.0, wait=True) + # 5 readings out of tolerance (~0.1 s), then held for 0.1 s + assert time.time() - start >= 0.2 + assert status.success + + def test_move_ignores_in_position_tighter_than_tolerance(): # settled within tolerance, but outside the controller's InPositionDistance motor = _sim_motor(velocity=100.0, tolerance=0.001)