openflexure-microscope-server/tests/test_scan_planners.py
Joe Knapper 871cab72f4 Apply 5 suggestion(s) to 2 file(s)
Co-authored-by: Julian Stirling <julian@julianstirling.co.uk>
2025-04-17 08:43:51 +00:00

272 lines
10 KiB
Python

import pytest
from copy import copy
from openflexure_microscope_server import scan_planners
from .utilities import scan_test_helpers
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_base_class_not_implemented():
intial_position = (100, 50)
with pytest.raises(NotImplementedError):
scan_planners.ScanPlanner(intial_position=intial_position)
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 be 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 mark location as 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
# if scan is complete, asking for the next location returns an error
with pytest.raises(RuntimeError):
planner.get_next_location_and_z_estimate()
def test_bad_smart_spiral_settings():
intial_position = (100, 50)
# Class init should raise error if no planner_settings dictionary set
with pytest.raises(ValueError):
scan_planners.SmartSpiral(intial_position=intial_position)
planner_settings = {"dx": 50, "dy": 50, "max_dist": 10000}
keys = ["dx", "dy", "max_dist"]
# Class init should raise error if planner_settings is missing any key
for delkey in keys:
bad_planner_settings = copy(planner_settings)
del bad_planner_settings[delkey]
with pytest.raises(KeyError):
scan_planners.SmartSpiral(
intial_position=intial_position, planner_settings=bad_planner_settings
)
# Class init should raise error if planner_settings if any value can't be cast
# to int
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"
with pytest.raises(ValueError):
scan_planners.SmartSpiral(
intial_position=intial_position, planner_settings=bad_planner_settings
)
def test_smart_spiral_first_few_pos():
"""
This test is VERY long, not really a "unit". It checks step-by-step
that data is added correctly for the first few postions in a scan.
This should catch basic cases of if the algorithm is updated.
"""
intial_position = (100, 50)
planner_settings = {"dx": 50, "dy": 50, "max_dist": 10000}
# Create a planner
planner = scan_planners.SmartSpiral(
intial_position=intial_position, planner_settings=planner_settings
)
# 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_pos1, z_pos1 = planner.get_next_location_and_z_estimate()
assert xy_pos1 == intial_position
assert z_pos1 is None
# Set a focus value
z_focus = 10
xyz_pos1 = (xy_pos1[0], xy_pos1[1], z_focus)
# if we mark this position as visited, imaged, and focused
planner.mark_location_visited(xyz_pos1, 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_pos1 in planner.imaged_locations
assert xyz_pos1 in planner.focused_locations
assert xy_pos1 in planner.path_history
# remove from planned
assert xy_pos1 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 updated
xy_pos2, z_pos2 = planner.get_next_location_and_z_estimate()
# move in negative x-dir first
assert xy_pos2 == (50, 50)
# Focus set from closest point
assert z_pos2 is z_focus
xyz_pos2 = (xy_pos2[0], xy_pos2[1], z_focus)
# if we mark this position as visited, imaged, and NOT focused
planner.mark_location_visited(xyz_pos2, imaged=True, focused=False)
# Check this position remove from planned
assert xy_pos2 not in planner.remaining_locations
# Check original position not re-added
assert xy_pos1 not in planner.remaining_locations
# 3 old points still planned
assert (100, 0) in planner.remaining_locations
assert (100, 100) in planner.remaining_locations
assert (150, 50) in planner.remaining_locations
# Add 3 new points to planned
assert (0, 50) in planner.remaining_locations
assert (50, 100) in planner.remaining_locations
assert (50, 0) in planner.remaining_locations
assert len(planner.remaining_locations) == 6
# Now the next location is updated
xy_pos3, z_pos3 = planner.get_next_location_and_z_estimate()
# move in negative y-dir first next
assert xy_pos3 == (50, 0)
# Focus still set as before
assert z_pos3 is z_focus
# Check that the closest focus site to pos3 is pos 1 as
# pos 2 is not focussed
assert planner.closest_focus_site(xy_pos3) == xyz_pos1
new_z_focus = 20
xyz_pos3 = (xy_pos3[0], xy_pos3[1], new_z_focus)
# Finally check that if this is focused...
planner.mark_location_visited(xyz_pos3, imaged=True, focused=True)
# ... then the new 4th point ...
xy_pos4, z_pos4 = planner.get_next_location_and_z_estimate()
# ...(100, 0)...
assert xy_pos4 == (100, 0)
# ... and it should get its focus from the most recent point as this was focussed
assert z_pos4 is new_z_focus
assert planner.closest_focus_site(xy_pos4) == xyz_pos3
def test_smart_spiral_stops_on_max_dist():
intial_position = (0, 0)
planner_settings = {"dx": 100, "dy": 100, "max_dist": 1000}
# Create a planner
planner = scan_planners.SmartSpiral(
intial_position=intial_position, planner_settings=planner_settings
)
while not planner.scan_complete:
xy_pos, _ = planner.get_next_location_and_z_estimate()
xyz_pos = (xy_pos[0], xy_pos[1], 0)
planner.mark_location_visited(xyz_pos, imaged=True, focused=True)
# Estimate of radius = 10 scans, pir^2 = 314 images. In reality it works out as
# 317
assert len(planner.path_history) == 317
def test_mark_wrong_location():
"""
This test is VERY long, not really a "unit". It checks step-by-step
that data is added correctly for the first few postions in a scan.
This should catch basic caseses of if the algorithm is updated.
"""
intial_position = (100, 50)
planner_settings = {"dx": 50, "dy": 50, "max_dist": 10000}
# Create a planner
planner = scan_planners.SmartSpiral(
intial_position=intial_position, planner_settings=planner_settings
)
xy_pos, _ = planner.get_next_location_and_z_estimate()
# Trying to mark the wrong location as visited raises error
wrong_xyz_pos = (xy_pos[0] + 1, xy_pos[1], 0)
with pytest.raises(RuntimeError):
planner.mark_location_visited(wrong_xyz_pos, imaged=True, focused=True)
def test_closest_focus_wth_large_numbers():
"""
The number of steps gets very large in reality runs some tests to check
that everything works well with huge numbers of steps
"""
intial_position = (0, 0)
# Set this up, but we won't use the settings
planner_settings = {"dx": 10000, "dy": 10000, "max_dist": 100000}
# Create a planner
planner = scan_planners.SmartSpiral(
intial_position=intial_position, planner_settings=planner_settings
)
# Directly overwrite the private focussed locations list for test
# For two points 1m points away it should choose the last as they are equal
planner._focused_locations = [(1000000, 0, 0), (0, 1000000, 0)]
assert planner.closest_focus_site((0, 0)) == (0, 1000000, 0)
# Try similar
planner._focused_locations = [(1234567, 0, 0), (-1234567, 0, 0)]
assert planner.closest_focus_site((0, 0)) == (-1234567, 0, 0)
# Make the first point 1 step closer
planner._focused_locations = [(1234566, 0, 0), (-1234567, 0, 0)]
assert planner.closest_focus_site((0, 0)) == (1234566, 0, 0)
def test_example_smart_spiral():
"""Test the smart spiral scan algorithm on the sample types listed
below and defined in scan_test_helpers.load_sample_points
Will fail if the locations or path between locations visited has changed
for any of the samples listed"""
example_samples = [
"regular",
"lobed",
"core",
]
for sample_name in example_samples:
_, planner = scan_test_helpers.example_smart_spiral(sample_name)
expected_planner = (
scan_test_helpers.get_expected_result_for_example_smart_spiral(sample_name)
)
assert planner.path_history == expected_planner.path_history
assert planner.imaged_locations == expected_planner.imaged_locations