diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 2248a55c..3a215183 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -409,7 +409,9 @@ class SmartSpiral(ScanPlanner): self._add_surrounding_positions(xy_pos) else: self._add_intermediate_positions(xy_pos) - self._re_sort_remaining_locations(xy_pos) + # Don't re-sort after imaging a secondary location or it can cause scan + # direction to reverse, breaking the spiral. + self._re_sort_remaining_locations(xy_pos) def _add_surrounding_positions(self, xy_pos: XYPos) -> None: """Add the 4 surrounding positions to the list of remaining locations to visit. @@ -502,8 +504,9 @@ class SmartSpiral(ScanPlanner): """Sort the remaining positions based on the current location.""" # Defined rather than use a lambda for readability - def sort_key(pos: FutureScanLocation) -> tuple[float, float, float]: + def sort_key(pos: FutureScanLocation) -> tuple[bool, float, float, float]: return ( + self._is_primary_location(pos), # False sorts low self.moves_between(current_pos, pos), self.moves_between(self._initial_position, pos), distance_between(current_pos, pos), diff --git a/tests/utilities/example_smart_spiral_core.pkl b/tests/utilities/example_smart_spiral_core.pkl index b3b47db8..717d4eab 100644 Binary files a/tests/utilities/example_smart_spiral_core.pkl 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 index 21bcf3e1..2ba515c9 100644 Binary files a/tests/utilities/example_smart_spiral_lobed.pkl 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 index aa9b847b..ca6ccf15 100644 Binary files a/tests/utilities/example_smart_spiral_regular.pkl 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 old mode 100644 new mode 100755 index 4d32a2fb..bf22876a --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -1,9 +1,12 @@ +#! /usr/bin/env python3 + """Utility functions for testing scan planners. These including fake sample creation, scan path visualisation, and persistent storage of expected scan paths for samples. """ +import argparse import os import pickle @@ -18,6 +21,8 @@ from openflexure_microscope_server import scan_planners THIS_DIR = os.path.dirname(os.path.realpath(__file__)) +ALL_SAMPLE_NAMES = ("lobed", "regular", "core") + class FakeSample: """A fake sample to test scan algorithms. @@ -62,7 +67,10 @@ def visualise_scan(sample: FakeSample, planner: scan_planners.ScanPlanner) -> Fi ax.add_artist(sample.patch) xh, yh = zip(*planner.path_history, strict=True) xi, yi, _zi = zip(*planner.imaged_locations, strict=True) - xs, ys, _zs = zip(*planner.secondary_locations, strict=True) + if planner.secondary_locations: + xs, ys, _zs = zip(*planner.secondary_locations, strict=True) + else: + xs, ys, _zs = [], [], [] # convert history to numpy array so can calculate quiver arrows xh = np.array(xh) @@ -135,33 +143,33 @@ def example_smart_spiral( return sample, planner -def profile_and_save_plot_for_example_smart_spiral(): - """Run the example scan and save a plot and the profile data. - - Also print the cumulative stats. - - This runs if you run this file directly. - """ +def profile_example_smart_spiral(): + """Profile running an example scan and print the cumulative profile stats.""" import pstats import cProfile profiler = cProfile.Profile() - sample, planner = profiler.runcall(example_smart_spiral) + profiler.runcall(example_smart_spiral) stats_fname = os.path.join(THIS_DIR, "scan_example_stats.pstats") profiler.dump_stats(stats_fname) - png_fname = os.path.join(THIS_DIR, "scan_example_plot.png") - fig = visualise_scan(sample, planner) - fig.savefig(png_fname, dpi=200) - run_stats = pstats.Stats(stats_fname) run_stats.strip_dirs() run_stats.sort_stats("cumulative") run_stats.print_stats("scan_planners.py") -def update_example_smart_spiral_pickle(sample_name: str): +def plot_all_examples(): + """Plot all examples as png files.""" + for sample_name in ALL_SAMPLE_NAMES: + sample, planner = example_smart_spiral(sample_name) + png_fname = os.path.join(THIS_DIR, f"scan_example_plot_{sample_name}.png") + fig = visualise_scan(sample, planner) + fig.savefig(png_fname, dpi=200) + + +def update_example_smart_spiral_pickles(): """Pickle the ScanPlanner for the example_smart_spiral(). This is done so the history can be compared by testing to check @@ -173,10 +181,11 @@ def update_example_smart_spiral_pickle(sample_name: str): 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, f"example_smart_spiral_{sample_name}.pkl") - _, planner = example_smart_spiral(sample_name) - with open(pkl_fname, "wb") as pkl_file_obj: - pickle.dump(planner, pkl_file_obj, pickle.HIGHEST_PROTOCOL) + for sample_name in ALL_SAMPLE_NAMES: + 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: + pickle.dump(planner, pkl_file_obj, pickle.HIGHEST_PROTOCOL) def get_expected_result_for_example_smart_spiral( @@ -227,5 +236,35 @@ def load_sample_points(sample_name: str): return sample_options[sample_name] +def main(): + """Run the profiler, the plotting, or update the pickles based on command line input. + + This only runs if run as a command line script. + """ + parser = argparse.ArgumentParser(description="Simulated scan-planning utility") + + subparsers = parser.add_subparsers(dest="command", required=True) + + # profile + subparsers.add_parser("profile", help="Run simulation under cProfile") + + # plot + subparsers.add_parser("plot", help="Run simulation and produce plots") + + # update + subparsers.add_parser("update", help="Update stored reference pickles") + + args = parser.parse_args() + + if args.command == "profile": + profile_example_smart_spiral() + elif args.command == "plot": + plot_all_examples() + elif args.command == "update": + update_example_smart_spiral_pickles() + else: + parser.error(f"Unknown command: {args.command}") + + if __name__ == "__main__": - profile_and_save_plot_for_example_smart_spiral() + main()