From ec25ae71265f3800c5cf459efee50cef9a3f7be8 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Mon, 14 Apr 2025 18:37:30 +0100 Subject: [PATCH 01/10] Add a scan path planner that prioritises shorter moves --- .../scan_planners.py | 151 ++++++++++++++++++ tests/utilities/scan_test_helpers.py | 2 +- 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 75ffd0fb..20e99360 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -362,6 +362,135 @@ class SmartSpiral(ScanPlanner): return np.max(np.abs(displacement_in_moves)) +class ShortSmartSpiral(ScanPlanner): + """ + 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 + tried). However if a location is not imaged due no sample being detected + then neibouring positions are not imaged. + + The next image taken is the closes to the centre (considering the largest + of vertical or horizontal distance), ties are broken by the distance from + the current position. + """ + + _max_dist: int = 0 + _dx: int = 0 + _dy: int = 0 + + def _parse(self, planner_settings: Optional[dict] = None) -> None: + """ + Parse SmartSpiral Settings. This should be a dictionary + + "dx" - the movement size in x + "dy" - the movement size in y + "max_dist" - The maximum distance to a location can be from the centre. + """ + + expected_keys = ["max_dist", "dx", "dy"] + invalid_msg = "SmartSpiral requires a planner_settings dictionary with keys: " + if not planner_settings: + raise ValueError(invalid_msg + ",".join(expected_keys)) + if not all(keys in planner_settings for keys in expected_keys): + raise KeyError(invalid_msg + ",".join(expected_keys)) + + self._dx = int(planner_settings["dx"]) + self._dy = int(planner_settings["dy"]) + self._max_dist = int(planner_settings["max_dist"]) + + def _intial_location_list(self) -> XYPosList: + """ + Called on initalisation. Sets the initial list of locations for this scan planner + + For smart spiral this is just the first point + """ + return [self._initial_position] + + def mark_location_visited( + self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True + ) -> None: + """ + Mark the location as visited. Adjust extra positions accordingly + + Args: + xyz_pos: the x_y_z position + imaged: true if an image was taken, false if not (due to background detect) + focused: true if autofocus completed successfully + """ + # First call the base class to update the positions + super().mark_location_visited(xyz_pos, imaged, focused) + + xy_pos = enforce_xy_tuple(xyz_pos[:2]) + if imaged: + self._add_surrounding_positions(xy_pos) + self._re_sort_remaining_locations(xy_pos) + + def _add_surrounding_positions(self, xy_pos: XYPos) -> None: + """ + This adds the surrounding (4 point connectivity) positions + to the remaining locations if they are not too far away or + already planned or already visited + """ + new_positions = [ + (xy_pos[0] - self._dx, xy_pos[1]), + (xy_pos[0] + self._dx, xy_pos[1]), + (xy_pos[0], xy_pos[1] - self._dy), + (xy_pos[0], xy_pos[1] + self._dy), + ] + + for new_pos in new_positions: + # Skip position if already planned or visited + if self.position_planned(new_pos) or self.position_visited(new_pos): + continue + + dist = distance_between(new_pos, self._initial_position) + if dist > self._max_dist: + LOGGER.debug("Rejected moving to %s as it is out of range", new_pos) + continue + self._remaining_locations.append(new_pos) + + def _re_sort_remaining_locations(self, current_pos: XYPos) -> None: + """ + Sort the remaining positions besed on the current location + """ + + # Defined rather than use a lambda for readability + def sort_key(pos): + return ( + moves_between(current_pos, pos, [self._dx, self._dy]), + self.moves_from_centre(pos), + distance_between(current_pos, pos) + ) + + self._remaining_locations.sort(key=sort_key) + + def moves_from_centre( + self, + xy_pos: XYPos, + ) -> float: + """ + Return the number of moves from the centre 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 + """ + 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 + + return np.max(np.abs(displacement_in_moves)) + + def distance_between( current_pos: XYPos | np.ndarray, next_pos: XYPos | np.ndarray ) -> float: @@ -373,3 +502,25 @@ def distance_between( next_pos = np.array(next_pos, dtype="float64") current_pos = np.array(current_pos, dtype="float64") return float(np.linalg.norm(next_pos - current_pos)) + +def moves_between( + starting_pos: XYPos | np.ndarray, + ending_pos: XYPos | np.ndarray, + move_size: XYPos | np.ndarray | list[int, int] +) -> float: + """ + Return the number of moves between two xy positions in the x or y direction + whichever is largest + + Args: + starting_pos: the position to measure from + ending_pos: the position to measure to + move_size: the step size for the scan, both in x and y + """ + starting_pos = np.array(starting_pos, dtype="float64") + ending_pos = np.array(ending_pos, dtype="float64") + move_size = np.array(move_size) + + displacement_in_moves = (ending_pos - starting_pos) / move_size + + return np.max(np.abs(displacement_in_moves)) diff --git a/tests/utilities/scan_test_helpers.py b/tests/utilities/scan_test_helpers.py index 6901361a..9b1b7a50 100644 --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -127,7 +127,7 @@ def example_smart_spiral() -> tuple[FakeSample, scan_planners.ScanPlanner]: img_size = (1000, 1000) intial_position = (0, 0) planner_settings = {"dx": 700, "dy": 700, "max_dist": 100000} - planner = scan_planners.SmartSpiral( + planner = scan_planners.ShortSmartSpiral( intial_position=intial_position, planner_settings=planner_settings ) From 7373424fcf40f7cfcc47da497c564566bc66a3e5 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 15 Apr 2025 10:40:45 +0100 Subject: [PATCH 02/10] Ruff formatting --- src/openflexure_microscope_server/scan_planners.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 20e99360..35f5f00b 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -463,7 +463,7 @@ class ShortSmartSpiral(ScanPlanner): return ( moves_between(current_pos, pos, [self._dx, self._dy]), self.moves_from_centre(pos), - distance_between(current_pos, pos) + distance_between(current_pos, pos), ) self._remaining_locations.sort(key=sort_key) @@ -503,10 +503,11 @@ def distance_between( current_pos = np.array(current_pos, dtype="float64") return float(np.linalg.norm(next_pos - current_pos)) + def moves_between( starting_pos: XYPos | np.ndarray, ending_pos: XYPos | np.ndarray, - move_size: XYPos | np.ndarray | list[int, int] + move_size: XYPos | np.ndarray | list[int, int], ) -> float: """ Return the number of moves between two xy positions in the x or y direction From b414697cfbc2cd81a6930b167ca799c95b6ff691 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Tue, 15 Apr 2025 16:29:29 +0100 Subject: [PATCH 03/10] Update testing for multiple sample shapes --- .../scan_planners.py | 123 ------------------ tests/test_scan_planners.py | 23 +++- tests/utilities/example_smart_spiral.pkl | Bin 13513 -> 0 bytes tests/utilities/example_smart_spiral_core.pkl | Bin 0 -> 4139 bytes .../utilities/example_smart_spiral_lobed.pkl | Bin 0 -> 8295 bytes .../example_smart_spiral_regular.pkl | Bin 0 -> 5877 bytes tests/utilities/scan_test_helpers.py | 57 +++++--- 7 files changed, 61 insertions(+), 142 deletions(-) delete mode 100644 tests/utilities/example_smart_spiral.pkl create mode 100644 tests/utilities/example_smart_spiral_core.pkl create mode 100644 tests/utilities/example_smart_spiral_lobed.pkl create mode 100644 tests/utilities/example_smart_spiral_regular.pkl diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 35f5f00b..38a8d317 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -240,129 +240,6 @@ class ScanPlanner: class SmartSpiral(ScanPlanner): - """ - This is a smart spiral scan that spirals out from the centre. - - 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 - tried). However if a location is not imaged due no sample being detected - then neibouring positions are not imaged. - - The next image taken is the closes to the centre (considering the largest - of vertical or horizontal distance), ties are broken by the distance from - the current position. - """ - - _max_dist: int = 0 - _dx: int = 0 - _dy: int = 0 - - def _parse(self, planner_settings: Optional[dict] = None) -> None: - """ - Parse SmartSpiral Settings. This should be a dictionary - - "dx" - the movement size in x - "dy" - the movement size in y - "max_dist" - The maximum distance to a location can be from the centre. - """ - - expected_keys = ["max_dist", "dx", "dy"] - invalid_msg = "SmartSpiral requires a planner_settings dictionary with keys: " - if not planner_settings: - raise ValueError(invalid_msg + ",".join(expected_keys)) - if not all(keys in planner_settings for keys in expected_keys): - raise KeyError(invalid_msg + ",".join(expected_keys)) - - self._dx = int(planner_settings["dx"]) - self._dy = int(planner_settings["dy"]) - self._max_dist = int(planner_settings["max_dist"]) - - def _intial_location_list(self) -> XYPosList: - """ - Called on initalisation. Sets the initial list of locations for this scan planner - - For smart spiral this is just the first point - """ - return [self._initial_position] - - def mark_location_visited( - self, xyz_pos: XYZPos, imaged: bool = True, focused: bool = True - ) -> None: - """ - Mark the location as visited. Adjust extra positions accordingly - - Args: - xyz_pos: the x_y_z position - imaged: true if an image was taken, false if not (due to background detect) - focused: true if autofocus completed successfully - """ - # First call the base class to update the positions - super().mark_location_visited(xyz_pos, imaged, focused) - - xy_pos = enforce_xy_tuple(xyz_pos[:2]) - if imaged: - self._add_surrounding_positions(xy_pos) - self._re_sort_remaining_locations(xy_pos) - - def _add_surrounding_positions(self, xy_pos: XYPos) -> None: - """ - This adds the surrounding (4 point connectivity) positions - to the remaining locations if they are not too far away or - already planned or already visited - """ - new_positions = [ - (xy_pos[0] - self._dx, xy_pos[1]), - (xy_pos[0] + self._dx, xy_pos[1]), - (xy_pos[0], xy_pos[1] - self._dy), - (xy_pos[0], xy_pos[1] + self._dy), - ] - - for new_pos in new_positions: - # Skip position if already planned or visited - if self.position_planned(new_pos) or self.position_visited(new_pos): - continue - - dist = distance_between(new_pos, self._initial_position) - if dist > self._max_dist: - LOGGER.debug("Rejected moving to %s as it is out of range", new_pos) - continue - self._remaining_locations.append(new_pos) - - def _re_sort_remaining_locations(self, current_pos: XYPos) -> None: - """ - Sort the remaining positions besed on the current location - """ - - # Defined rather than use a lambda for readability - def sort_key(pos): - return self.moves_from_centre(pos), distance_between(current_pos, pos) - - self._remaining_locations.sort(key=sort_key) - - def moves_from_centre( - self, - xy_pos: XYPos, - ) -> float: - """ - Return the number of moves from the centre 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 - """ - 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 - - return np.max(np.abs(displacement_in_moves)) - - -class ShortSmartSpiral(ScanPlanner): """ 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 diff --git a/tests/test_scan_planners.py b/tests/test_scan_planners.py index da5ff961..93750764 100644 --- a/tests/test_scan_planners.py +++ b/tests/test_scan_planners.py @@ -253,7 +253,22 @@ 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_type in example_samples: + _, planner = scan_test_helpers.example_smart_spiral(sample=sample_type) + expected_planner = ( + scan_test_helpers.get_expected_result_for_example_smart_spiral( + sample=sample_type + ) + ) + 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 5d5727d87df59a640d40cb8366f60a0e22d91b1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13513 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 diff --git a/tests/utilities/example_smart_spiral_core.pkl b/tests/utilities/example_smart_spiral_core.pkl new file mode 100644 index 0000000000000000000000000000000000000000..ee598e73eecccadf0a81492f082dbece7809a3c5 GIT binary patch literal 4139 zcmZo*ovI+f00uqU`30$YX*sDCrA4XnxtYmD`Nhfk1*!4HsYPX}MS8`_iFxq_If;3B zsYS(8dbopg6N^fM3o?rmbEZt5qS-j5c8W%iV0>m?W=UpZPJBUrab`(oe%=&s25*M8 zDLu^bDHT(EH?V*hl~a5bn0q+ma}z7#Q!pTkH&+$1|$bD*9*jiS_WY|uz}nH zHp~~o_D$e~s_;$VglO|k;6yklffH_zuK*uRsQ@2TsQ@2BsQ@3cQm};}+d#&F90F!< zV1bK!Gr*Z(2O)SM)d=2)kN^Lp@!+8XmV^5o#DloY3&eyv7Q%-37QzO3w0(;20U@Xg z-vdGrZN3MD5Z*Z;gz(M*A-D@bY8pO6BFI;O&kN2@;6$)Du)x_~4sVg&59Yz$59Yz$ z58^=_0@eg|9fS>aKS&8UqS~kU7KlSt_!fvmwD}f@Bivsgj&Og0IKuq};&8Wt)F*s^ z`sIL-7o07?2WNXtc=7)~G_gR~kOU28L#zce;TA)9a4&+i!z~BPLER5wH}HVch1Y|} z|Nr|UvmtQ=5r_I6tOja1hz+$I#6(yQ=D{ro^FYZ7WQnhW4BS^BHazAOWZ(`2iNkb* z*s!?qRgi&L=c^zC3-t>M2;(j&AOiJ*0>Zcp3W!L!pa6HK?*#>@aTgSz#(5dsM@}bT z9?T^WIhb3(JcxEM6WIV*M1i!!!WqnlhAo)AfFBebUJbX9Ed=x67J_+j3qd@ng&-!( zLNFT|Ibb$4(!gw}g$#5k`DSO5Qq8VF`13By%`CE(^jc(8;7=D|W5Y#7wzU^dj_ zU^c|#UJ4ig|Mx{^L)C!Ap?bk=Xkdcb(3B2hLmdcWA{+?j!6GcRM<_lmKe@CRy}rue z%izxt$PmmB$`H;F$q>yD%Mj0y$dJsC%8<^G$&k&E%aG4d$WY8s%23Ww$xzKu%TUkI z$k5Et%Fxcx$$}r9_$uP|@%P`Nd$gs??%COF`$*|3^%dpRI$Z*VX z%5ctb$#BhZ%W%)|$nebY%J9zc$?(na%ka+#$Oy~`$_UN~$q3B|%Lvbi$cW5{%81U0 z$%xH}%ZSfN$Vkjc%1F*g$wh$;i#f%gE0t$SBMx$|%k#$tcYz z%P7yN$f(Sy%Baq$$*9e!%c#$2$Y{)H%4p7L$!N`J%V^K&$mq=I%IMDM$>`1K%jnOT zkTEf1QpV(rDH&5Ure#den2|9vV^(SpZ+t;wNk)7|W^qY=QRS4_Dd5_@4OX|eK`L-? zh(p<+Mg_RC@>O64sRL7x8odqF&H%}SYH(1_0Skar5J)AI0oDv-gVGZU8=?)GlG~<$ z8DJ|QEC)8IGkifTZw4?0vK_>QROD?_z_}hwK^+dNe?iFvEC$g6uE^V__=2i&Fa=Wz zt->K-F9gm@Dq1a=}s zEP)di2w)bdo#hK+fm$M9KZC3AwkhBW98AGH0Xv{7}zkdf59xUFTtz@{B2Vp z^*vY&RCR+R8zKhsIfMna9wZC49>joI4`xHH2eYBpgV|8)L2O@;RiL&TD0Cs=0c914 zgJYp>3aIXfhKzy?ECiwL1c)re9*{a{=!04aU@346gW3roH-i*F4TrWD+NStITMG~t z#E~E|sFBci0$3L0M_+Io0n7!*0!S&uNO1KJOMRfK9afrv*${Vu)qsovGr(?xut2VZ zu)wha(gI0yU=}EdAhHL9+NOY6&|pBc8``Eo*bsL^*icJB)jrr#5CdW_WXgSH#mrhvpC7W;zQ3}6AUrJ$Ar$V!j`XmmEH z!lKwf3zpEKEs3@%zR-3Agaz>zNDOK?s09I*0vir#27r?zsImup7sP-iM=%>=23RR9 zqQK%1&w$0jkqTjf0|g`xi3u|IRLuA2@1Brnh z2V%e+2WCSZ2WCUU6rv4ek?#XNkS5;;dQdGO7DNMx1yK%SL48ti2HGWUo8kp#L&6Nq f1{nlqK*9|q4e>OX1$H=?1@;V>1@d%iX_6iQH|ltB literal 0 HcmV?d00001 diff --git a/tests/utilities/example_smart_spiral_lobed.pkl b/tests/utilities/example_smart_spiral_lobed.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b64c0a6765f23f80d576e0969615161b168a90ab GIT binary patch literal 8295 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 MatPa return MatPath(path_points, closed=True) -def example_smart_spiral() -> tuple[FakeSample, scan_planners.ScanPlanner]: +def example_smart_spiral( + sample: 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=sample) sample = FakeSample(xy_sample_points) img_size = (1000, 1000) intial_position = (0, 0) - planner_settings = {"dx": 700, "dy": 700, "max_dist": 100000} - planner = scan_planners.ShortSmartSpiral( + 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: 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,54 @@ 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}.pkl") with open(pkl_fname, "wb") as pkl_file_obj: - _, planner = example_smart_spiral() + _, planner = example_smart_spiral(sample=sample) 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: str): """ 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}.pkl") with open(pkl_fname, "rb") as pkl_file_obj: planner = pickle.load(pkl_file_obj) return planner +def load_sample_points(sample: str): + """Returns the points to generate a FakeSample of sample type "sample" """ + 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), + ], + } + return sample_options[sample] + + if __name__ == "__main__": profile_and_save_plot_for_example_smart_spiral() From c18667c9731dfbb7ab67c7184a96a4a5a4e952ad Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Tue, 15 Apr 2025 16:38:11 +0100 Subject: [PATCH 04/10] Update pickles --- tests/utilities/example_smart_spiral_core.pkl | Bin 4139 -> 4139 bytes .../utilities/example_smart_spiral_lobed.pkl | Bin 8295 -> 8295 bytes .../example_smart_spiral_regular.pkl | Bin 5877 -> 5877 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/utilities/example_smart_spiral_core.pkl b/tests/utilities/example_smart_spiral_core.pkl index ee598e73eecccadf0a81492f082dbece7809a3c5..5b7ba2de52d49d0611a2eae688f1d446428e50e6 100644 GIT binary patch delta 1609 zcmZ3juv%fm4#vsv8K>8KIlTS<-#3BNo1uLQgzdnFU@I`g+1?Crrq_c%|NkR+2Yw>+ z7JP&7d^fOoHGGD$6F3oU0X{Fd8iC0-82Q9u#`y~HL2U6A;PX{rp4`L~Vk*GrmGA*- z$^jt+yFeU;4Y#R4+-t$N$??o$^>9-Q#G$4Zi2Fi3TNRgm$5c?!ga zJ6Ayl;SB{DxC6W*-xOg_jg!Vk3t|rW&FMi z_`xCCaO?kn-v*w^=UK%hp)mm#f;ij@#P(*GJdag;@=R8KrUl<7uVIs%ypoMy8e{|5 zgbP=L-r5KVMLQ`^NWz0>aN16ySdLZBPZX zCx2%3Ff!1B8fc(}Fwj5?VW5E)!axHpuZGW?ecAPxCU4-JsPFaRBiP4n(BzlE31d01 z!B`5+P?k5tc zK^zDY10^bm7}Wd%ad6my#k?876vX%gLcXA&@GTIB(BQ<|HpN##2CNL4a6tlIkaP=T z%78r%VL>g0rdWs=DBFYW0B3rLgP~#qd|nQ3C--wpOn$;G2i*uXQHg(pWCoCzTA@OtnV91D|Od86t!KQ&2pp3-9ugwlJ!S{gBWCQ*j7LfSlv;0xXUJ4ig|Mvws8dSD`90kd5 zZBxLx4NO5z1?4D+T_6_3DIgXkbU-Xnfd|n6jS+BOo2<`gQ-47Lk_H-7A+&)OIQ)G< zIk0VtFEsx_SWru#c@H86wZuRRoO#=(fU+^vk_URQ9PvO8mUtfML9?3g13i%Q`Tn;} O@qM5N3X{~*Bs~Bf4>+Cx delta 1664 zcmZ3juv%fm4n_k3K3@f9Z-(|Mz5;yS3{ZyGgFpZO!`KIYBG?PQP2R{jZ}NAhfcgYZ zuZGW1nGGy(wwJ@(|Np%fe1ph9crX(nJeb*F9>hv86Y35K+kp*aJ%pXW31a&eh(lHQ z7KnQ@h(W~=PAw4kI`DIH0kc>=++79YUI`x{F7Z7egkTHs!P#CDUi|;>3$+f+h6D## z9O^odFx(!ngcsaPU^%#VK|J5d{w(Eez6vs42_GikV-|CT$%2Gn`ao=0K=>-iKrHfA zkb!yef&$!yz84g{8a_{6$R?%?*L^_&?jGL@3Q*k_6nqt!Cm&*|3P6N^!>#}S5sn4( zVAgtpco3~%Ce&&O8yX-WCB6xqlkHi>m=^F)zRczk-k^$bnSmBO-hBm43iJAicj9n%FhA{t;tVWr*95s*I?4~bzo})B~4!iW-#r| z0H(l3LRcV+Agl%7CYNx|nEaT_OCFjy+ot$JlV;l#Zw9bi+opI0{GMFECC2{Y^ z#0%im+cpK9cEJ?X9$!$ht^fJ|KSU7Zdk71vADV>0CV>MU;#W|zZkytpzzGxEzyf0x zh=Xker&A;rC;-7?U{8S(D%1sD6JBUS6G__?unKTQ!q^aZfW<*6A(1Di{=s8#$bsDK z1!DUy;D@*htL z375&K{Fe0%s$lKD(A?HG1;l~|s(}_Ps-gL>Z3-yILA`CD1@ksE=YeHG?(;R!g18Qp z;}9&6lov<{mL4u#{VxY8G}@*>Bp^mZ*r4<N3ldf!7Q_JG2YQp2@WWYJ@P<607d=r#Fwt7u? z@&7-J4K@)f{^8?feI7AhumKPuZ-&XaJSUhYyqN6ID<%cD@qr%L46uz*cEIn+>AY@I zU}F_b!O~!3q3jPIC$HxfV+!~^`980e8dxU58mtOzAe0R@5XuhtJ=vbm3S!>^N3c?` zG?X3id-G(zh1?Fl2ZX?}(KZDZk!|p3Y@6Z>jl{Mo&?s!1f{4JjDP9Y{P4<@(s|UFU zY6>`DU~Evtc`3a5|G#aD7g!BQplu2`*ukL*Hsb$(7#kF7P&PE$Ax?m31Enq~OMnlm z4CKHAdJjN0cr$n%05c#igt9??hKYlM%Im;`$t$I1*FzFe+Z11L@@bpm3r;+3Q+&Zm zr)`QaIN`KS@oiA`Wsro>3NjFJ2YHA(FkK+-3#JbUL1+O!2%W&`3sUdR0M-t66NCkF zk?-VaX|q7EAHb@>p$1`r!U@6xITykLYXO-CF$Tl}CrfbXfKBz?zyeC9zh%6`L9$*3 z_y7L~y8~iJf>PTQh#g=S*bXoYYzLSHwgb!p#ey%0<;?)5KxRA0Pd+Le%#@%sSwODa z9AeZ1J!rK0f>~grz$~y)U>4XY5X+mv_ki}~J93V$ARfd~3`Q_VDVV}c0<*v-fmvXa zz$~yyAQmVNe=+5Eq@FE delta 1274 zcmaFv@Z4d;WzIw|hqwR#doeu!|KFRTeTvtIkI1|Q-;j9$zma&}3~)`p8(0wR1Wp87 zfX|EJ`Q%cr&dCPcfnmNE6nqtAK(_cQ$iP?y;xN_$As9=556bdpfHJ&5R>RmJN5R-1 zK2Dy%Ev7o*#sB{>p$CuQY>>l1T#%jK43mAi#3y@j@pFRx2oj%sjyqJso543h38VsI z36#Cy+hjW)F&R+cz@)*JK&8RDb9qiM1^k}u&nqScGSc^f9@rSL9w>Xli^=J{Zc-5E zn}Vew&WEr+e4MIReH8Sqo)DBeiV`DC9tCqHT&V zG(y{^_=2MoWTNi@AyCAcOV6HcCF5J~yMe`*L9%U%Z-Xj?21j(;6i76;P4NXsa@!PN za1?_L@>P%l8x5wx(cCr#68<0wu=BuD;PeP#frAw+3yugd3*<{*88Fk}J~-OJP6ulM zxkf=|vVwxM8OY@zrC=+;K?7C>WqC6|8DIy%*bBZ*-XJCB0J0S(1P;Ih4?rORcA*!P z4GusE+nWI@2ns-u(BvC3-t`XhVEcT*v;ZH3J|G043&bI`f((Sdpa7=9$r2VQ2};m7 z^98d&4uP;h&V#VPfdZBV`4l1s4wD3>$yKt!ldsD*J9;zt9?*uF`9Ke5CYS{_6U+je z31)%K1hc?qKG2&yQO?no!KiJDH-j&T0*9Edf+@@-FbixFm<2Wo%mSMPW`RvoFr6$e zZ*A_);Ok%kb5DXb%oH#SYzmkKHU-QAo04EXxl`WUu)z-O3J?Wx#R5l|elQEHAIt*l QU*I_Tx4b$_YH5-l0BlruHUIzs diff --git a/tests/utilities/example_smart_spiral_regular.pkl b/tests/utilities/example_smart_spiral_regular.pkl index 1ce6405bc86a5ec2c57099d4002b2db955a4dbf3..ddf784c8d469a1f5cb2d4bddb6d5d65e58aab204 100644 GIT binary patch delta 841 zcmeyW`&D}EKrs=1C-$v@O!d6ml*qpkN^MsD#%RU&M}eS_kfTW!}I_D z!I}m5CiiiQsc&F`3wblZnO-0lB6thFP4?!LQULh`AqX%n7)ab63){r^9?mrGpD zn*lE9yMYD4PT)ka1^6~^=aT2FpYQ@44sBDsKt}sANVb832qp$H5+Vk24VVQEt+pwU zuxgtE3IUKNUvN0JP4NYXQri^Y0&%c%Uj-T82ci&h1(~)fAa!6G9CB^-V1qyc;9zT; z0`|5qNC4~~2n*y-2n*~;uq;^2LB4GYL>-s~b|i@9&EN~7K#l~{37inV03Vq4GPwW$ zzwZM6wkebE3%~a)5C;b-*pV=n03VE%zzJhPBdTo*G@78Ug-UxZ_%`{2h*&+yi7+8h z=)%~bc=A$s_5VLKtif>s4Q&`36y7j4$TLtjG(y3l4i)l+MkY8!VPXe_ydH!+2HD^% oz~^<~0hpD*31e+wfwH_ApbT(m!`NWUA)35Ep$ipHEltt`05A@n`2YX_ delta 799 zcmeyW`&D?R%bNkp0NVs(FZecjABR|^!`uJ=VL~51 z{{Qc*AOkYd3*S? z_2J_rF|m4(v!Ej03{Zye1{N3#8iH+8VByy`#TOcSZBu*;#9`_R#C;hg!Ez899F}cU ze8C~vHU;ESkUTgP+aR$8R#*=SBaoaIIOM=AkTb#3V8=sPU@-+5u*1P2+%^TG7c2(y zhA)T(4qJ$YATG!OU>Y2%5C?+!2ZX?Z From 871cab72f4bed7a201885904f0691150ceec3d65 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Thu, 17 Apr 2025 08:43:51 +0000 Subject: [PATCH 05/10] Apply 5 suggestion(s) to 2 file(s) Co-authored-by: Julian Stirling --- tests/test_scan_planners.py | 8 +++----- tests/utilities/scan_test_helpers.py | 28 ++++++++++++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/test_scan_planners.py b/tests/test_scan_planners.py index 93750764..f9b56a32 100644 --- a/tests/test_scan_planners.py +++ b/tests/test_scan_planners.py @@ -263,12 +263,10 @@ def test_example_smart_spiral(): "lobed", "core", ] - for sample_type in example_samples: - _, planner = scan_test_helpers.example_smart_spiral(sample=sample_type) + 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=sample_type - ) + 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/scan_test_helpers.py b/tests/utilities/scan_test_helpers.py index 31794551..06bce2a6 100644 --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -112,13 +112,13 @@ def interp_closed_path(xy_points: list[tuple[int, int]], n_points: int) -> MatPa def example_smart_spiral( - sample: str = "lobed", + 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 = load_sample_points(sample=sample) + xy_sample_points = load_sample_points(sample_name) sample = FakeSample(xy_sample_points) img_size = (1000, 1000) intial_position = (0, 0) @@ -162,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(sample: str): +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,25 +174,28 @@ def update_example_smart_spiral_pickle(sample: 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}.pkl") + pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl") with open(pkl_fname, "wb") as pkl_file_obj: - _, planner = example_smart_spiral(sample=sample) + _, planner = example_smart_spiral(sample_name) pickle.dump(planner, pkl_file_obj, pickle.HIGHEST_PROTOCOL) -def get_expected_result_for_example_smart_spiral(sample: str): +def get_expected_result_for_example_smart_spiral(sample_name: str) -> 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, f"example_smart_spiral_{sample}.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: str): - """Returns the points to generate a FakeSample of sample type "sample" """ +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), @@ -216,7 +219,12 @@ def load_sample_points(sample: str): (1000, 0), ], } - return sample_options[sample] + 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__": From 1248c2b7c8800dbf3fbcd4e78f54ab05ab5b9719 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Thu, 17 Apr 2025 08:47:24 +0000 Subject: [PATCH 06/10] Remove unneeded third sort key --- src/openflexure_microscope_server/scan_planners.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 38a8d317..2b4fd172 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -340,7 +340,6 @@ class SmartSpiral(ScanPlanner): return ( moves_between(current_pos, pos, [self._dx, self._dy]), self.moves_from_centre(pos), - distance_between(current_pos, pos), ) self._remaining_locations.sort(key=sort_key) From 041106ebea29ee291db6de047300d949bdfa329d Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Thu, 17 Apr 2025 08:48:14 +0000 Subject: [PATCH 07/10] Only open pickle file if planner was successfully generated --- tests/utilities/scan_test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utilities/scan_test_helpers.py b/tests/utilities/scan_test_helpers.py index 06bce2a6..a14f1405 100644 --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -175,8 +175,8 @@ def update_example_smart_spiral_pickle(sample_name: str): 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: - _, planner = example_smart_spiral(sample_name) pickle.dump(planner, pkl_file_obj, pickle.HIGHEST_PROTOCOL) From c18472dc8a667d093b1ffea2bf92f2eda4e14611 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 17 Apr 2025 09:51:03 +0100 Subject: [PATCH 08/10] Add the import info for ScanPlanner --- tests/utilities/scan_test_helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/utilities/scan_test_helpers.py b/tests/utilities/scan_test_helpers.py index a14f1405..7ee583ed 100644 --- a/tests/utilities/scan_test_helpers.py +++ b/tests/utilities/scan_test_helpers.py @@ -180,7 +180,9 @@ def update_example_smart_spiral_pickle(sample_name: str): pickle.dump(planner, pkl_file_obj, pickle.HIGHEST_PROTOCOL) -def get_expected_result_for_example_smart_spiral(sample_name: str) -> ScanPlanner: +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. From c895383a8342fba428d91d979ffef90cf6f5678b Mon Sep 17 00:00:00 2001 From: jaknapper Date: Thu, 17 Apr 2025 10:10:13 +0100 Subject: [PATCH 09/10] Remove repeated moves_from_centre() code --- .../scan_planners.py | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 2b4fd172..18fcd18e 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -338,31 +338,31 @@ class SmartSpiral(ScanPlanner): # Defined rather than use a lambda for readability def sort_key(pos): return ( - moves_between(current_pos, pos, [self._dx, self._dy]), - self.moves_from_centre(pos), + self.moves_between(current_pos, pos), + self.moves_between(self._initial_position, 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)) @@ -378,26 +378,3 @@ def distance_between( next_pos = np.array(next_pos, dtype="float64") current_pos = np.array(current_pos, dtype="float64") return float(np.linalg.norm(next_pos - current_pos)) - - -def moves_between( - starting_pos: XYPos | np.ndarray, - ending_pos: XYPos | np.ndarray, - move_size: XYPos | np.ndarray | list[int, int], -) -> float: - """ - Return the number of moves between two xy positions in the x or y direction - whichever is largest - - Args: - starting_pos: the position to measure from - ending_pos: the position to measure to - move_size: the step size for the scan, both in x and y - """ - starting_pos = np.array(starting_pos, dtype="float64") - ending_pos = np.array(ending_pos, dtype="float64") - move_size = np.array(move_size) - - displacement_in_moves = (ending_pos - starting_pos) / move_size - - return np.max(np.abs(displacement_in_moves)) From 07cbef05365767b3a656dc8f3bc7066db234fb49 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Thu, 17 Apr 2025 10:25:40 +0100 Subject: [PATCH 10/10] Reimplement tie breaking by distance between --- src/openflexure_microscope_server/scan_planners.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 18fcd18e..73d76fa7 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -340,6 +340,7 @@ class SmartSpiral(ScanPlanner): 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)