Ensure ROM z-predict returns an integer and add more tests.

This commit is contained in:
Julian Stirling 2025-10-17 12:06:34 +01:00
parent 3d49cbf921
commit 95f5bbb16c
2 changed files with 85 additions and 25 deletions

View file

@ -72,12 +72,12 @@ class RomDataTracker:
"""The last stage coordinate recorded."""
return self.stage_coords[-1].copy()
def predict_z_displacament(
def predict_z_displacement(
self,
movement: dict[str, int],
stage_position: dict[str, int],
csm_matrix: np.ndarray,
) -> float:
) -> int:
"""Predict the z-displacement needed for a given movement.
:param movement: The movement to be performed in image coordinates.
@ -99,7 +99,7 @@ class RomDataTracker:
stage_position[axis] + (movement[axis] / pixel_step[axis]), *fit_params
)
return z_dest - stage_position["z"]
return int(z_dest - stage_position["z"])
@dataclass
@ -357,8 +357,8 @@ class RangeofMotionThing(lt.Thing):
big_movement = self._movement_in_img_coords(
fov_perc=BIG_STEP, axis=axis, direction=direction
)
z_disp = self._rom_data.predict_z_displacament(
movement=big_movement[axis],
z_disp = self._rom_data.predict_z_displacement(
movement=big_movement,
stage_position=rom_deps.stage.position,
csm_matrix=rom_deps.csm.image_to_stage_displacement_matrix,
)

View file

@ -1,5 +1,6 @@
"""File contains unit tests for stage_measure."""
from copy import copy
import logging
import pytest
from openflexure_microscope_server.things import stage_measure
@ -7,8 +8,18 @@ from openflexure_microscope_server.things import stage_measure
LOGGER = logging.getLogger("mock-invocation_logger")
def test_predict_z():
"""Check that the prediction for the next z position is correct."""
@pytest.fixture
def csm_matrix():
"""Return an example CSM matrix."""
return [
[0.03061156624485296, 1.8031242270940833],
[1.773236372778601, 0.006660431608601435],
]
@pytest.fixture
def example_rom_data():
"""Return some example data in a RomDataTracker."""
mock_positions = [
{"x": 0, "y": 0, "z": 42},
{"x": 727, "y": 2, "z": 154},
@ -19,21 +30,21 @@ def test_predict_z():
]
rom_data = stage_measure.RomDataTracker()
# loop through mock positions recording them with an offset.
for position in mock_positions:
offset = {"x": 54.4, "y": 0}
rom_data.record_movement(position, offset)
return rom_data
csm_matrix = [
[0.03061156624485296, 1.8031242270940833],
[1.773236372778601, 0.006660431608601435],
]
mock_z_diff = rom_data.predict_z_displacament(
def test_predict_z(csm_matrix, example_rom_data):
"""Check that the prediction for the next z position is correct."""
mock_z_diff = example_rom_data.predict_z_displacement(
movement={"x": 2908, "y": 0},
stage_position={"x": 3635, "y": 10, "z": 617},
csm_matrix=csm_matrix,
)
expected_z_diff = 1343.1625053206606
expected_z_diff = 1343
assert mock_z_diff == expected_z_diff
@ -61,28 +72,56 @@ def test_error_on_axis_from_movement_dict(movement):
assert stage_measure._axis_from_movement_dict(movement)
def test_parasitic_detect():
"""Check that the parasitic error is raised correctly."""
with pytest.raises(stage_measure.ParasiticMotionError):
stage_measure._detect_parasitic_motion(
movement={"x": 2908, "y": 0}, offset={"x": 2908, "y": 300}
)
@pytest.mark.parametrize(
("par_fraction", "should_error"),
[
(-0.20, True),
(-0.11, True),
(-0.09, False),
(-0.05, False),
(0.00, False),
(0.05, False),
(0.09, False),
(0.11, True),
(0.20, True),
],
)
def test_parasitic_detect(par_fraction, should_error):
"""Check error is raised if the fraction of parastitic motion is too high."""
movement = {"x": 2908, "y": 0}
offset = copy(movement)
offset["y"] = movement["x"] * par_fraction
if should_error:
with pytest.raises(stage_measure.ParasiticMotionError):
stage_measure._detect_parasitic_motion(movement=movement, offset=offset)
else:
# Nothing to check here as the only job of _detect_parasitic_motion is to
# error if there is too much motion
stage_measure._detect_parasitic_motion(movement=movement, offset=offset)
@pytest.fixture
def rom_thing() -> stage_measure.RangeofMotionThing:
"""Return a RangeofMotionThing."""
return stage_measure.RangeofMotionThing()
def rom_thing(example_rom_data) -> stage_measure.RangeofMotionThing:
"""Return a RangeofMotionThing already populated with some example rom_data."""
rom_thing = stage_measure.RangeofMotionThing()
rom_thing._stream_resolution = [800, 600]
rom_thing._rom_data = example_rom_data
return rom_thing
@pytest.fixture
def mock_rom_deps(mocker) -> stage_measure.RomDeps:
def mock_rom_deps(csm_matrix, mocker) -> stage_measure.RomDeps:
"""Return a RomDeps object full of mocks, except the logger which is LOGGER."""
mock_csm = mocker.Mock()
# Set up mock csm to return a CSM matrix
mock_csm.image_to_stage_displacement_matrix = csm_matrix
return stage_measure.RomDeps(
autofocus=mocker.Mock(),
stage=mocker.Mock(),
cam=mocker.Mock(),
csm=mocker.Mock(),
csm=mock_csm,
logger=LOGGER,
)
@ -247,7 +286,6 @@ def test_stage_still_moves(
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):
@ -269,3 +307,25 @@ def test_stage_still_moves(
)
assert still_moves is expected_to_detect_motion
assert mock_offset_from.call_count == offset_calls
def test_big_z_corrected_movement(rom_thing, mock_rom_deps):
"""Check big z corrected move moves in x/y and z the expected distances."""
mock_rom_deps.stage.position = {"x": 5000, "y": 30, "z": 500}
rom_thing._big_z_corrected_movement("x", direction=1, rom_deps=mock_rom_deps)
expected_movement = {"x": 800 * stage_measure.BIG_STEP / 100, "y": 0}
# Check there is one z move in steps
assert mock_rom_deps.stage.move_relative.call_count == 1
move_kwargs = mock_rom_deps.stage.move_relative.call_args.kwargs
assert "x" not in move_kwargs
assert "y" not in move_kwargs
assert "z" in move_kwargs
move_kwargs["z"] = 1162
# And one move in image coordinates
assert mock_rom_deps.csm.move_in_image_coordinates.call_count == 1
lat_mov_kwargs = mock_rom_deps.csm.move_in_image_coordinates.call_args.kwargs
assert lat_mov_kwargs == expected_movement