From 0527990282ec0752ee6f8b8a3cd962d6b4d01d06 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 24 Feb 2026 17:45:21 +0000 Subject: [PATCH] Use backlash compensation in smart scan and autofocus --- .../things/autofocus.py | 25 +++++++++++-------- .../things/smart_scan.py | 3 ++- .../things/stage/__init__.py | 4 ++- tests/unit_tests/test_autofocus.py | 2 ++ 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 0ee8d32b..909ff229 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -22,7 +22,7 @@ import labthings_fastapi as lt from labthings_fastapi.types.numpy import NDArray from .camera import BaseCamera, CaptureParams -from .stage import BaseStage +from .stage import BacklashCompensation, BaseStage LOGGER = logging.getLogger(__name__) MIN_TEST_IMAGE_COUNT = 3 @@ -458,14 +458,17 @@ class AutofocusThing(lt.Thing): up to 10 times. """ attempt = 0 - backlash = 200 with JPEGSharpnessMonitor(self._stage, self._cam) as sharpness_monitor: while attempt < 10: attempt += 1 if start == "centre": - self._stage.move_relative(x=0, y=0, z=-int(backlash + dz / 2)) - self._stage.move_relative(x=0, y=0, z=backlash) + self._stage.move_relative( + x=0, + y=0, + z=-int(dz / 2), + backlash_compensation=BacklashCompensation.Z_ONLY, + ) # Always start centrally for future runs start = "centre" @@ -480,8 +483,10 @@ class AutofocusThing(lt.Thing): target_max = np.max(heights) - dz / 5 # move to the peak - self._stage.move_absolute(z=peak_height - backlash) - self._stage.move_absolute(z=peak_height) + self._stage.move_absolute( + z=peak_height, + backlash_compensation=BacklashCompensation.Z_ONLY, + ) if target_min < peak_height < target_max: # If it is within the target range then return @@ -622,12 +627,10 @@ class AutofocusThing(lt.Thing): # Better to start too low and take too many images than too high and need to refocus self._stage.move_relative( z=-int( - stack_parameters.steps_undershoot - + stack_parameters.backlash_correction - + stack_parameters.stack_z_range / 2 - ) + stack_parameters.steps_undershoot + stack_parameters.stack_z_range / 2 + ), + backlash_compensation=BacklashCompensation.Z_ONLY, ) - self._stage.move_relative(z=stack_parameters.backlash_correction) captures: list[CaptureInfo] = [] # Always check for focus using the the last `min_images_to_test` in the diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 56cf7e0b..d587de3c 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -37,7 +37,7 @@ from openflexure_microscope_server.utilities import coerce_thing_selector # Things from .camera import BaseCamera from .scan_workflows import ScanWorkflow -from .stage import BaseStage +from .stage import BacklashCompensation, BaseStage T = TypeVar("T") P = ParamSpec("P") @@ -311,6 +311,7 @@ class SmartScanThing(lt.Thing): x=next_point[0], y=next_point[1], z=z_estimate, + backlash_compensation=BacklashCompensation.XY_ONLY, ) return (next_point[0], next_point[1], z_estimate) diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index e949531d..2a6a776e 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -142,7 +142,9 @@ class BaseStage(lt.Thing): """Current position of the stage.""" return self._apply_axis_direction(self._hardware_position) - backlash_steps: dict[str, int] = lt.setting(default={"x": 200, "y": 200, "z": 200}) + backlash_steps: dict[str, int] = lt.setting( + default={"x": 200, "y": 200, "z": 200}, readonly=True + ) """The number of steps to elimate backlash. The sign sets the direction. A positive number sets the direction of the second move in a backlash correction. diff --git a/tests/unit_tests/test_autofocus.py b/tests/unit_tests/test_autofocus.py index 1c3f6e08..3661cbc6 100644 --- a/tests/unit_tests/test_autofocus.py +++ b/tests/unit_tests/test_autofocus.py @@ -60,6 +60,8 @@ def test_looping_autofocus(start_z, max_loc, centre, attempts_expected, passes, def adjust_pos(**kwargs: int) -> None: """Move relative should update position. So make a side effect for the mock.""" + # Remove backlash compensation from the dict if it exists. + kwargs.pop("backlash_compensation", None) for axis, value in kwargs.items(): autofocus_thing._stage.position[axis] += value