diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 75ffd0fb..73d76fa7 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -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)) diff --git a/tests/test_scan_planners.py b/tests/test_scan_planners.py index da5ff961..f9b56a32 100644 --- a/tests/test_scan_planners.py +++ b/tests/test_scan_planners.py @@ -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 diff --git a/tests/utilities/example_smart_spiral.pkl b/tests/utilities/example_smart_spiral.pkl deleted file mode 100644 index 5d5727d8..00000000 Binary files a/tests/utilities/example_smart_spiral.pkl and /dev/null differ diff --git a/tests/utilities/example_smart_spiral_core.pkl b/tests/utilities/example_smart_spiral_core.pkl new file mode 100644 index 00000000..5b7ba2de Binary files /dev/null and b/tests/utilities/example_smart_spiral_core.pkl differ diff --git a/tests/utilities/example_smart_spiral_lobed.pkl b/tests/utilities/example_smart_spiral_lobed.pkl new file mode 100644 index 00000000..a935e162 Binary files /dev/null and b/tests/utilities/example_smart_spiral_lobed.pkl differ diff --git a/tests/utilities/example_smart_spiral_regular.pkl b/tests/utilities/example_smart_spiral_regular.pkl new file mode 100644 index 00000000..ddf784c8 Binary files /dev/null and b/tests/utilities/example_smart_spiral_regular.pkl differ diff --git a/tests/utilities/scan_test_helpers.py b/tests/utilities/scan_test_helpers.py index 6901361a..7ee583ed 100644 --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -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()