From 3d49cbf921708be394212ef26de68633c62de14f Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Fri, 17 Oct 2025 01:29:36 +0100 Subject: [PATCH] Adding even more ROM tests --- .../things/stage_measure.py | 22 ++++---- tests/test_stage_measure.py | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/openflexure_microscope_server/things/stage_measure.py b/src/openflexure_microscope_server/things/stage_measure.py index 518f64a8..a40ccd8b 100644 --- a/src/openflexure_microscope_server/things/stage_measure.py +++ b/src/openflexure_microscope_server/things/stage_measure.py @@ -43,6 +43,7 @@ MEDIUM_STEP = 50 BIG_STEP = 200 PARASITIC_MOTION_TOL = 0.1 +DETECT_MOTION_TOL = 0.65 class RomDataTracker: @@ -196,7 +197,7 @@ class RangeofMotionThing(lt.Thing): } def _set_stream_resolution(self, cam: CamDep) -> None: - """Set the self._stream_reolution attribute by reading camera. + """Set the self._stream_resolution attribute by reading camera. :param cam: The camera dependency. """ @@ -288,7 +289,7 @@ class RangeofMotionThing(lt.Thing): """ if self._stream_resolution is None: raise RuntimeError( - "Stream resolution mut be set before generating movement in coords" + "Stream resolution must be set before generating movement in coords" ) img_index = 0 if axis == "x" else 1 @@ -372,13 +373,11 @@ class RangeofMotionThing(lt.Thing): ) -> bool: """Carry out 3 small moves in a given direction and axis. - Each position after the first is correlated with the previous position - to check the stage has moved as far as it should. If the correlation is - less than expected, 3 attempts are made to refocus the image to ensure that - image quality is not causing the correlation to be unsuccessful. If the correlation - is still too low then the edge is found. - - Delta is updated and tracked after each move. + An image is taken before and after each move to check has moved as far as it + should. If the offset (calculated by cross correlation) is less than expected, + 3 attempts are made to refocus the image to ensure that image quality is not + causing the offset to mistakenly be reported as too low be unsuccessful. If the + calculated offset is still too low then the edge is found. :param axis: The axis which is being measured. This must be 'x' or 'y'. :param direction: The direction the stage moves. @@ -387,7 +386,7 @@ class RangeofMotionThing(lt.Thing): movement = self._movement_in_img_coords( fov_perc=SMALL_STEP, axis=axis, direction=direction ) - abs_min_offset = abs(movement[axis] * 0.65) + abs_min_offset = abs(movement[axis] * DETECT_MOTION_TOL) for _loop in range(3): offset = self._move_and_measure( @@ -472,8 +471,7 @@ class RangeofMotionThing(lt.Thing): if ``abs_min_offset`` is also set :param abs_min_offset: The absolute minimum (on-axis) offset, under which the autofocus is repeated. - :return: All required data for the next move. This includes the updated delta - value and offset. + :return: The calculated offset from cross correlation. """ # Take image before move before_img = rom_deps.cam.grab_as_array() diff --git a/tests/test_stage_measure.py b/tests/test_stage_measure.py index 796748ac..fb2ffb74 100644 --- a/tests/test_stage_measure.py +++ b/tests/test_stage_measure.py @@ -219,3 +219,53 @@ def test_move_back_until_motion_detected(rom_thing, mock_rom_deps, mocker): for i, call_args in enumerate(mock_move_n_meas.call_args_list): call_args.kwargs["movement"] = {"x": -(2**i), "y": 0} call_args.kwargs["perform_autofocus"] = False + + +@pytest.mark.parametrize( + ("good_moves", "expected_to_detect_motion", "offset_calls"), + [ + ([0, 1, 2], True, 3), # First 3 pass all good + ([1, 2, 3], True, 4), # First is bad, will refocus, still complete + ([2, 3, 4], True, 5), # First 2 are bad, will refocus twice, still complete + ([3, 4, 5], True, 6), # First 3 are bad, will refocus 3 times, still complete + ([4, 5, 6], False, 4), # First 4 are bad, fails + # Check second and 3rd measurement can fail after first fails 3 times + ([3, 5, 7], True, 8), + ([3, 6, 9], True, 10), + ([3, 7, 11], True, 12), + # But they can't fail 4 times + ([3, 8, 9], False, 8), + ([3, 7, 12], False, 12), + ], +) +def test_stage_still_moves( + good_moves, + expected_to_detect_motion, + offset_calls, + rom_thing, + mock_rom_deps, + mocker, +): + """Test _stage_still_moves correctly detects stage movement.""" + rom_thing._stream_resolution = [800, 600] + min_offset = 800 * stage_measure.SMALL_STEP / 100 * stage_measure.DETECT_MOTION_TOL + + def gen_offsets(*_args, **_kwargs): + """Generate offset dictionaries with small moves unless count matches ``good_moves``.""" + i = 0 + while True: + x = min_offset * 1.2 if i in good_moves else 0.1 + yield {"x": x, "y": 0} + i += 1 + + mock_offset_from = mocker.patch.object( + rom_thing, "_offset_from", side_effect=gen_offsets() + ) + + still_moves = rom_thing._stage_still_moves( + axis="x", + direction=1, + rom_deps=mock_rom_deps, + ) + assert still_moves is expected_to_detect_motion + assert mock_offset_from.call_count == offset_calls