Merge branch 'shorter-move-spiral-path' into 'v3'
Add a scan path planner that prioritises shorter moves See merge request openflexure/openflexure-microscope-server!245
This commit is contained in:
commit
257d700333
7 changed files with 86 additions and 30 deletions
|
|
@ -241,7 +241,9 @@ class ScanPlanner:
|
|||
|
||||
class SmartSpiral(ScanPlanner):
|
||||
"""
|
||||
This is a smart spiral scan that spirals out from the centre.
|
||||
This is a smart spiral scan that spirals out from the centre, but prioritises
|
||||
short moves over rigidly sticking to minimising radius from the centre of the
|
||||
scan.
|
||||
|
||||
Each time and image is taken the four neighbouring images are added
|
||||
to the list of poisitions to image (unless they are already listed or
|
||||
|
|
@ -335,29 +337,33 @@ class SmartSpiral(ScanPlanner):
|
|||
|
||||
# Defined rather than use a lambda for readability
|
||||
def sort_key(pos):
|
||||
return self.moves_from_centre(pos), distance_between(current_pos, pos)
|
||||
return (
|
||||
self.moves_between(current_pos, pos),
|
||||
self.moves_between(self._initial_position, pos),
|
||||
distance_between(current_pos, pos),
|
||||
)
|
||||
|
||||
self._remaining_locations.sort(key=sort_key)
|
||||
|
||||
def moves_from_centre(
|
||||
def moves_between(
|
||||
self,
|
||||
xy_pos: XYPos,
|
||||
starting_pos: XYPos | np.ndarray,
|
||||
ending_pos: XYPos | np.ndarray,
|
||||
) -> float:
|
||||
"""
|
||||
Return the number of moves from the centre in the x or y direction
|
||||
Return the number of moves between two xy positions in the x or y direction
|
||||
whichever is largest
|
||||
|
||||
Args:
|
||||
xy_pos: the position
|
||||
|
||||
Note this has been renamed from `steps_from_centre` as that implied
|
||||
stepper motor steps not number of moves in a scan
|
||||
starting_pos: the position to measure from
|
||||
ending_pos: the position to measure to
|
||||
"""
|
||||
move_size = np.array([self._dx, self._dy])
|
||||
starting_pos = np.array(self._initial_position, dtype="float64")
|
||||
current_pos = np.array(xy_pos, dtype="float64")
|
||||
|
||||
displacement_in_moves = (current_pos - starting_pos) / move_size
|
||||
starting_pos = np.array(starting_pos, dtype="float64")
|
||||
ending_pos = np.array(ending_pos, dtype="float64")
|
||||
|
||||
displacement_in_moves = (ending_pos - starting_pos) / move_size
|
||||
|
||||
return np.max(np.abs(displacement_in_moves))
|
||||
|
||||
|
|
|
|||
|
|
@ -253,7 +253,20 @@ def test_closest_focus_wth_large_numbers():
|
|||
|
||||
|
||||
def test_example_smart_spiral():
|
||||
_, planner = scan_test_helpers.example_smart_spiral()
|
||||
expected_planner = scan_test_helpers.get_expected_result_for_example_smart_spiral()
|
||||
assert planner.path_history == expected_planner.path_history
|
||||
assert planner.imaged_locations == expected_planner.imaged_locations
|
||||
"""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
|
||||
|
|
|
|||
Binary file not shown.
BIN
tests/utilities/example_smart_spiral_core.pkl
Normal file
BIN
tests/utilities/example_smart_spiral_core.pkl
Normal file
Binary file not shown.
BIN
tests/utilities/example_smart_spiral_lobed.pkl
Normal file
BIN
tests/utilities/example_smart_spiral_lobed.pkl
Normal file
Binary file not shown.
BIN
tests/utilities/example_smart_spiral_regular.pkl
Normal file
BIN
tests/utilities/example_smart_spiral_regular.pkl
Normal file
Binary file not shown.
|
|
@ -111,22 +111,18 @@ def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPa
|
|||
return MatPath(path_points, closed=True)
|
||||
|
||||
|
||||
def example_smart_spiral() -> tuple[FakeSample, scan_planners.ScanPlanner]:
|
||||
def example_smart_spiral(
|
||||
sample_name: str = "lobed",
|
||||
) -> tuple[FakeSample, scan_planners.ScanPlanner]:
|
||||
"""
|
||||
Run an example scan and return the sample scanned and the planner object
|
||||
after scan is complete
|
||||
"""
|
||||
xy_sample_points = [
|
||||
(-5000, -5000),
|
||||
(-2000, 10000),
|
||||
(1000, 2000),
|
||||
(6000, 7000),
|
||||
(9000, 2000),
|
||||
]
|
||||
xy_sample_points = load_sample_points(sample_name)
|
||||
sample = FakeSample(xy_sample_points)
|
||||
img_size = (1000, 1000)
|
||||
intial_position = (0, 0)
|
||||
planner_settings = {"dx": 700, "dy": 700, "max_dist": 100000}
|
||||
planner_settings = {"dx": 1200, "dy": 800, "max_dist": 100000}
|
||||
planner = scan_planners.SmartSpiral(
|
||||
intial_position=intial_position, planner_settings=planner_settings
|
||||
)
|
||||
|
|
@ -166,7 +162,7 @@ def profile_and_save_plot_for_example_smart_spiral():
|
|||
run_stats.print_stats("scan_planners.py")
|
||||
|
||||
|
||||
def update_example_smart_spiral_pickle():
|
||||
def update_example_smart_spiral_pickle(sample_name: str):
|
||||
"""
|
||||
Pickle the ScanPlanner for the example_smart_spiral(),
|
||||
this is done so the history can be compared by testing to check
|
||||
|
|
@ -174,23 +170,64 @@ def update_example_smart_spiral_pickle():
|
|||
|
||||
If the algorithm is purposefully changed then this will need to be
|
||||
run to update the pickle for the test to pass.
|
||||
|
||||
Takes sample, the sample type we have generated, so we can make a
|
||||
pickle for each sample type
|
||||
"""
|
||||
pkl_fname = os.path.join(THIS_DIR, "example_smart_spiral.pkl")
|
||||
pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl")
|
||||
_, planner = example_smart_spiral(sample_name)
|
||||
with open(pkl_fname, "wb") as pkl_file_obj:
|
||||
_, planner = example_smart_spiral()
|
||||
pickle.dump(planner, pkl_file_obj, pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
|
||||
def get_expected_result_for_example_smart_spiral():
|
||||
def get_expected_result_for_example_smart_spiral(
|
||||
sample_name: str,
|
||||
) -> scan_planners.ScanPlanner:
|
||||
"""
|
||||
Return the expected ScanPlanner object for the example_smart_spiral(),
|
||||
this is pickled, so that it can be committed.
|
||||
"""
|
||||
pkl_fname = os.path.join(THIS_DIR, "example_smart_spiral.pkl")
|
||||
pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl")
|
||||
with open(pkl_fname, "rb") as pkl_file_obj:
|
||||
planner = pickle.load(pkl_file_obj)
|
||||
return planner
|
||||
|
||||
|
||||
def load_sample_points(sample_name: str):
|
||||
"""Return the points to generate the FakeSample corresponding to the given input name
|
||||
|
||||
Options are "lobed", "regular", and "core".
|
||||
"""
|
||||
sample_options = {
|
||||
"lobed": [
|
||||
(-5000, -5000),
|
||||
(-2000, 16000),
|
||||
(1000, 2000),
|
||||
(6000, 7000),
|
||||
(9000, 2000),
|
||||
],
|
||||
"regular": [
|
||||
(-5000, -5000),
|
||||
(-5000, 5000),
|
||||
(5000, 5000),
|
||||
(5000, -5000),
|
||||
],
|
||||
"core": [
|
||||
(-12000, 2000),
|
||||
(-12000, 1000),
|
||||
(0, -2000),
|
||||
(10000, -2000),
|
||||
(10000, -1000),
|
||||
(1000, 0),
|
||||
],
|
||||
}
|
||||
if sample_name not in sample_options:
|
||||
all_samples = ", ".join(sample_options.keys())
|
||||
raise ValueError(
|
||||
f"{sample_name} is not a valid sample name. Valid names : {all_samples}"
|
||||
)
|
||||
return sample_options[sample_name]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
profile_and_save_plot_for_example_smart_spiral()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue