fix(aerotech): move settles only after holding the tolerance for 0.1 s

After a hard stop (5 mm/s, 500 mm/s^2) X rings at ~10 Hz with ~20 um. The single
position error check caught a zero crossing (-0.19 um), the move was declared
settled 3.5 um off the target and failed (premove of a scan aborted). Now position
error and distance to the target must stay within the tolerance for
in_position_hold (0.1 s) without interruption.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM
This commit is contained in:
Mirko HollerandClaude Opus 5.5 committed 2026-10-29 12:42:36 +01:00
1 parent 201e6406d2
commit 4d95cf755f
2 files changed
+38 -5

No files matched your search

+26 -5
View File
@@ -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)
+12
View File
@@ -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)