Add some basic tests, and do minor fixes based on tests and MyPy
This commit is contained in:
parent
1f66e8bc7a
commit
842e196f9b
2 changed files with 158 additions and 12 deletions
|
|
@ -19,6 +19,32 @@ XYPosList: TypeAlias = list[XYPos]
|
|||
XYZPosList: TypeAlias = list[XYZPos]
|
||||
|
||||
|
||||
def enforce_xy_tuple(value: XYPos) -> XYPos:
|
||||
"""
|
||||
Used for enfocring that an input is a tuple and is the correct length
|
||||
"""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValueError("2 value tuple expected")
|
||||
if not len(value) == 2:
|
||||
raise ValueError("2 value tuple expected")
|
||||
if isinstance(value, list):
|
||||
return tuple(value)
|
||||
return value
|
||||
|
||||
|
||||
def enforce_xyz_tuple(value: XYZPos) -> XYZPos:
|
||||
"""
|
||||
Used for enfocring that an input is a tuple and is the correct length
|
||||
"""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValueError("3 value tuple expected")
|
||||
if not len(value) == 3:
|
||||
raise ValueError("3 value tuple expected")
|
||||
if isinstance(value, list):
|
||||
return tuple(value)
|
||||
return value
|
||||
|
||||
|
||||
class ScanPlanner:
|
||||
"""
|
||||
A base class for a scan planner.
|
||||
|
|
@ -29,7 +55,7 @@ class ScanPlanner:
|
|||
"""
|
||||
|
||||
def __init__(self, intial_position: XYPos, planner_settings: Optional[dict] = None):
|
||||
self._initial_position = tuple(intial_position)
|
||||
self._initial_position = enforce_xy_tuple(intial_position)
|
||||
|
||||
self._parse(planner_settings)
|
||||
|
||||
|
|
@ -102,13 +128,15 @@ class ScanPlanner:
|
|||
next_location = self._remaining_locations[0]
|
||||
|
||||
# If focussed locations exist return closest location, favouring most recent
|
||||
if self._focused_locations:
|
||||
z = self.closest_focus_site(next_location)[2]
|
||||
else:
|
||||
closest_pos = self.closest_focus_site(next_location)
|
||||
if closest_pos is None:
|
||||
z = None
|
||||
else:
|
||||
z = closest_pos[2]
|
||||
|
||||
return next_location, z
|
||||
|
||||
def closest_focus_site(self, xy_pos: XYPos) -> XYZPos:
|
||||
def closest_focus_site(self, xy_pos: XYPos) -> Optional[XYZPos]:
|
||||
"""
|
||||
Return the xyz position of the closest site where focus was achieved
|
||||
to the input xy_position, with the most recently taken image returned in
|
||||
|
|
@ -135,7 +163,7 @@ class ScanPlanner:
|
|||
return self._focused_locations[indicies[-1]]
|
||||
|
||||
def mark_location_visited(
|
||||
self, xyz_pos: XYPos, imaged: bool, focused: bool
|
||||
self, xyz_pos: XYZPos, imaged: bool, focused: bool
|
||||
) -> None:
|
||||
"""
|
||||
Mark the location as visited
|
||||
|
|
@ -146,7 +174,7 @@ class ScanPlanner:
|
|||
focused: true if autofocus completed successfully
|
||||
"""
|
||||
# ensure is tuple!
|
||||
xyz_pos = tuple(xyz_pos)
|
||||
xyz_pos = enforce_xyz_tuple(xyz_pos)
|
||||
xy_pos = xyz_pos[:2]
|
||||
|
||||
# Remove the expected position from the remaining locations list
|
||||
|
|
@ -196,7 +224,7 @@ class SmartSpiral(ScanPlanner):
|
|||
if not planner_settings:
|
||||
raise ValueError(invalid_msg + ",".join(expected_keys))
|
||||
if not all(keys in planner_settings for keys in expected_keys):
|
||||
raise ValueError(invalid_msg + ",".join(expected_keys))
|
||||
raise KeyError(invalid_msg + ",".join(expected_keys))
|
||||
|
||||
self._dx = int(planner_settings["dx"])
|
||||
self._dy = int(planner_settings["dy"])
|
||||
|
|
@ -211,7 +239,7 @@ class SmartSpiral(ScanPlanner):
|
|||
return [self._initial_position]
|
||||
|
||||
def mark_location_visited(
|
||||
self, xyz_pos: XYPos, imaged: bool = True, focused: bool = True
|
||||
self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Mark the location as visited. Adjust extra poisitons accordingly
|
||||
|
|
@ -224,7 +252,7 @@ class SmartSpiral(ScanPlanner):
|
|||
# First call the base class to update the positions
|
||||
super().mark_location_visited(xyz_pos, imaged, focused)
|
||||
|
||||
xy_pos = tuple(xyz_pos[:2])
|
||||
xy_pos = enforce_xy_tuple(xyz_pos[:2])
|
||||
if imaged:
|
||||
self._add_surrounding_positions(xy_pos)
|
||||
self._re_sort_remaining_locations(xy_pos)
|
||||
|
|
@ -287,7 +315,9 @@ class SmartSpiral(ScanPlanner):
|
|||
return np.max(np.abs(displacement_in_moves))
|
||||
|
||||
|
||||
def distance_between(current_pos: XYPos, next_pos: XYPos) -> float:
|
||||
def distance_between(
|
||||
current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray
|
||||
) -> float:
|
||||
"""
|
||||
Calculate the distance between the two xy positions
|
||||
|
||||
|
|
@ -295,4 +325,4 @@ def distance_between(current_pos: XYPos, next_pos: XYPos) -> float:
|
|||
"""
|
||||
next_pos = np.array(next_pos, dtype="float64")
|
||||
current_pos = np.array(current_pos, dtype="float64")
|
||||
return np.linalg.norm(next_pos - current_pos)
|
||||
return float(np.linalg.norm(next_pos - current_pos))
|
||||
|
|
|
|||
116
tests/test_scan_planners.py
Normal file
116
tests/test_scan_planners.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import pytest
|
||||
from openflexure_microscope_server import scan_planners
|
||||
from copy import copy
|
||||
|
||||
|
||||
def test_enforce_xy_tuple():
|
||||
bad_len_vals = [[1], [], (1,), (2, 4, 4), [1, 4, 5, 7]]
|
||||
bad_type_vals = ["hi", 1, {"this": "that"}, {1, 2}]
|
||||
for value in bad_len_vals + bad_type_vals:
|
||||
with pytest.raises(ValueError):
|
||||
scan_planners.enforce_xy_tuple(value)
|
||||
|
||||
assert (1, 6) == scan_planners.enforce_xy_tuple((1, 6))
|
||||
assert (1, 6) == scan_planners.enforce_xy_tuple([1, 6])
|
||||
|
||||
|
||||
def test_enforce_xyz_tuple():
|
||||
bad_len_vals = [[1], [], (1,), (2, 4), [1, 4, 5, 7]]
|
||||
bad_type_vals = ["hi!", 1, {"this": "that"}, {1, 2, 3}]
|
||||
for value in bad_len_vals + bad_type_vals:
|
||||
with pytest.raises(ValueError):
|
||||
scan_planners.enforce_xyz_tuple(value)
|
||||
|
||||
assert (1, 6, 2) == scan_planners.enforce_xyz_tuple((1, 6, 2))
|
||||
assert (1, 6, 6) == scan_planners.enforce_xyz_tuple([1, 6, 6])
|
||||
|
||||
|
||||
def test_v_basic_smart_spiral():
|
||||
intial_position = (100, 50)
|
||||
planner_settings = {"dx": 50, "dy": 50, "max_dist": 10000}
|
||||
planner = scan_planners.SmartSpiral(
|
||||
intial_position=intial_position, planner_settings=planner_settings
|
||||
)
|
||||
# Create a planner it shouldn't start complete
|
||||
assert not planner.scan_complete
|
||||
# When we start it should want to stay in the inital pos and have
|
||||
# no z_estimate
|
||||
xy_pos, z_pos = planner.get_next_location_and_z_estimate()
|
||||
assert xy_pos == intial_position
|
||||
assert z_pos is None
|
||||
|
||||
# Try to make imaged with only xy_position
|
||||
with pytest.raises(ValueError):
|
||||
planner.mark_location_visited(xy_pos, imaged=False, focused=False)
|
||||
# scan still not complete
|
||||
assert not planner.scan_complete
|
||||
# if we mark this position as visited but not imaged
|
||||
planner.mark_location_visited(
|
||||
(xy_pos[0], xy_pos[1], 10), imaged=False, focused=False
|
||||
)
|
||||
# scan is now complete
|
||||
assert planner.scan_complete
|
||||
|
||||
|
||||
def test_bad_smart_spiral_settings():
|
||||
intial_position = (100, 50)
|
||||
planner_settings = {"dx": 50, "dy": 50, "max_dist": 10000}
|
||||
with pytest.raises(KeyError):
|
||||
keys = ["dx", "dy", "max_dist"]
|
||||
for delkey in keys:
|
||||
bad_planner_settings = copy(planner_settings)
|
||||
del bad_planner_settings[delkey]
|
||||
scan_planners.SmartSpiral(
|
||||
intial_position=intial_position, planner_settings=bad_planner_settings
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
keys = ["dx", "dy", "max_dist"]
|
||||
for badkey in keys:
|
||||
bad_planner_settings = copy(planner_settings)
|
||||
bad_planner_settings[badkey] = "I can't be converted to an int"
|
||||
scan_planners.SmartSpiral(
|
||||
intial_position=intial_position, planner_settings=bad_planner_settings
|
||||
)
|
||||
|
||||
|
||||
def test__smart_spiral_next_pos():
|
||||
intial_position = (100, 50)
|
||||
planner_settings = {"dx": 50, "dy": 50, "max_dist": 10000}
|
||||
planner = scan_planners.SmartSpiral(
|
||||
intial_position=intial_position, planner_settings=planner_settings
|
||||
)
|
||||
# Create a planner it shouldn't start complete
|
||||
assert not planner.scan_complete
|
||||
# When we start it should want to stay in the inital pos and have
|
||||
# no z_estimate
|
||||
xy_pos, z_pos = planner.get_next_location_and_z_estimate()
|
||||
assert xy_pos == intial_position
|
||||
assert z_pos is None
|
||||
z_focus = 10
|
||||
xyz_pos = (xy_pos[0], xy_pos[1], z_focus)
|
||||
# if we mark this position as visited and imaged
|
||||
planner.mark_location_visited(xyz_pos, imaged=True, focused=True)
|
||||
|
||||
# scan is not complete
|
||||
assert not planner.scan_complete
|
||||
|
||||
# Lists should have updated to add the position to histories
|
||||
assert xyz_pos in planner._imaged_locations
|
||||
assert xyz_pos in planner._focused_locations
|
||||
assert xy_pos in planner._path_history
|
||||
|
||||
# remove from planned
|
||||
assert xy_pos not in planner._remaining_locations
|
||||
|
||||
# Add 4 new points to planned
|
||||
assert (100, 0) in planner._remaining_locations
|
||||
assert (100, 100) in planner._remaining_locations
|
||||
assert (150, 50) in planner._remaining_locations
|
||||
assert (50, 50) in planner._remaining_locations
|
||||
assert len(planner._remaining_locations) == 4
|
||||
|
||||
# Now the next location is update
|
||||
xy_pos, z_pos = planner.get_next_location_and_z_estimate()
|
||||
# move in negative x-dir first
|
||||
assert xy_pos == (50, 50)
|
||||
assert z_pos is z_focus
|
||||
Loading…
Add table
Add a link
Reference in a new issue