Add more ROM tests

This commit is contained in:
Julian Stirling 2025-10-17 00:01:03 +01:00
parent 50d6298c06
commit 664c7de5f8
2 changed files with 115 additions and 8 deletions

View file

@ -380,9 +380,8 @@ class RangeofMotionThing(lt.Thing):
Delta is updated and tracked after each move.
:param stream_resolution: The resolution of the stream from the camera.
:param direction: The direction the stage moves.
:param axis: The axis which is being measured. This must be 'x' or 'y'.
:param direction: The direction the stage moves.
:param rom_deps: All dependencies that were passed to the calling Action
"""
movement = self._movement_in_img_coords(
@ -428,16 +427,19 @@ class RangeofMotionThing(lt.Thing):
this method will move in the opposite direction.
:param rom_deps: All dependencies that were passed to the calling Action
"""
def _reverse_move_dict(displacement: float) -> dict[str, float]:
move = {"x": 0, "y": 0}
move[axis] = displacement * direction * -1
return move
# Array of increasing pixel sizes (powers of 2 from 1 to 512)
displacements = [2**i for i in range(10)]
movements = [_reverse_move_dict(2**i) for i in range(10)]
motion_minimum = 20 # minimum number of pixels for motion to be detected
movement = {"x": 0, "y": 0}
for displacement in displacements:
for movement in movements:
# Increment movement
movement[axis] = displacement * direction * -1
rom_deps.logger.info(f"Testing with step size {movement[axis]}")
offset = self._move_and_measure(
@ -463,7 +465,7 @@ class RangeofMotionThing(lt.Thing):
:param movement: A dictionary containing the distance to move in image coords.
:param rom_deps: All dependencies that were passed to the calling Action
:param perform_autofocus: Set to False to disable atutofocus after move.
:param perform_autofocus: Set to False to disable autofocus after move.
Default is True
:param max_autofocus_repeats: The number of times to repeat the focus if the
detected (on-axis) offset is below ``abs_min_offset``. This will only work

View file

@ -114,3 +114,108 @@ def test_offset_from(rom_thing, mock_rom_deps, mocker):
# Check the image inputs
assert displacement_kwargs["image_0"] == "MOCK_IMAGE"
assert displacement_kwargs["image_1"] == mock_after_image
@pytest.mark.parametrize("perform_autofocus", [True, False])
def test_move_and_measure(perform_autofocus, rom_thing, mock_rom_deps, mocker):
"""Test _move_and_measure with and without initial autofocus checking call counts.
This doesn't test with the repeate autofocus if motion isn't detected
"""
mock_offset_value = {"x": 100, "y": 3}
mocker.patch.object(rom_thing, "_offset_from", side_effect=(mock_offset_value,))
movement = {"x": 100, "y": 0}
offset = rom_thing._move_and_measure(
movement=movement, rom_deps=mock_rom_deps, perform_autofocus=perform_autofocus
)
# Check exactly 1 move
assert mock_rom_deps.csm.move_in_image_coordinates.call_count == 1
# The kwargs of the call should movement dict
assert mock_rom_deps.csm.move_in_image_coordinates.call_args.kwargs == movement
# Check autofocus call count
expected_af_count = 1 if perform_autofocus else 0
assert mock_rom_deps.autofocus.looping_autofocus.call_count == expected_af_count
# And check final return
assert offset == mock_offset_value
@pytest.mark.parametrize(
("x_offsets", "n_offset_measures", "expected_return"),
[
([5.1, 0.2], 1, {"x": 5.1, "y": 0}),
([-5.1, 0.2], 1, {"x": -5.1, "y": 0}),
([0.1, 5.2, 0.3, 0.4, 0.5], 2, {"x": 5.2, "y": 0}),
([0.1, 0.2, 5.3, 0.4, 0.5], 3, {"x": 5.3, "y": 0}),
([0.1, 0.2, 0.3, 5.4, 0.5], 4, {"x": 5.4, "y": 0}),
# n_offset_measures shouldn't go higher than 4 as max autofocus repeats is 3
([0.1, 0.2, 0.3, 0.4, 5.5], 4, {"x": 0.4, "y": 0}),
],
)
def test_move_and_measure_with_refocus(
x_offsets, n_offset_measures, expected_return, rom_thing, mock_rom_deps, mocker
):
"""Test _move_and_measure with final refocus if offset is too small."""
return_dicts = tuple({"x": x, "y": 0} for x in x_offsets)
offset_from_mock = mocker.patch.object(
rom_thing, "_offset_from", side_effect=return_dicts
)
movement = {"x": 10, "y": 0}
offset = rom_thing._move_and_measure(
movement=movement,
rom_deps=mock_rom_deps,
perform_autofocus=False,
max_autofocus_repeats=3,
abs_min_offset=5,
)
# Check exactly 1 move
assert mock_rom_deps.csm.move_in_image_coordinates.call_count == 1
# Check expected _offset_from calls
assert offset_from_mock.call_count == n_offset_measures
# The kwargs of the call should movement dict
assert mock_rom_deps.csm.move_in_image_coordinates.call_args.kwargs == movement
# Check autofocus call count is 1 less than number of offset measures as no autofocus
# is performed before the first one
expected_af_count = n_offset_measures - 1
assert mock_rom_deps.autofocus.looping_autofocus.call_count == expected_af_count
# And check final return
assert offset == expected_return
def test_move_back_until_motion_detected(rom_thing, mock_rom_deps, mocker):
"""Check that _move_back_until_motion_detected is making increasing negative moves.
The moves for this method should be in opposite direction to the direction
specified as this is moving back after the stage reaches end of its movement.
"""
def gen_offset(*_args, **_kwargs):
return {"x": 0, "y": 0}
mock_move_n_meas = mocker.patch.object(
rom_thing, "_move_and_measure", side_effect=gen_offset
)
with pytest.raises(RuntimeError, match="Cannot detect motion again"):
rom_thing._move_back_until_motion_detected("y", -1, rom_deps=mock_rom_deps)
assert mock_move_n_meas.call_count == 10
for i, call_args in enumerate(mock_move_n_meas.call_args_list):
call_args.kwargs["movement"] = {"x": 0, "y": 2**i}
call_args.kwargs["perform_autofocus"] = False
## Reset mock and change the side effect
mock_move_n_meas.reset_mock()
mock_move_n_meas.side_effect = (
{"x": 0, "y": 0},
{"x": 0, "y": 0},
{"x": 21, "y": 0},
)
# Other axis and direction this time
rom_thing._move_back_until_motion_detected("x", 1, rom_deps=mock_rom_deps)
# Should only be called 3 times
assert mock_move_n_meas.call_count == 3
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