From 422e63ea9015c5fab0ebe8d5e9bfe72617327c8e Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 15 Aug 2025 18:56:26 +0100 Subject: [PATCH 1/5] Increase neighbour ratio to 1.6 --- 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 b30cacc7..615e6df5 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -20,9 +20,10 @@ XYZPosList: TypeAlias = list[XYZPos] # how many times the minimum distance between images to include as a "nearby" image -# default 1.4 includes images offset in x or y, but not diagonally. +# default 1.6 includes images offset in x or y, but not diagonally. # This wis based of a 4:3 aspect ratio. So x moves are 1.33 times larger than y -NEIGHBOUR_CUTOFF = 1.4 +# testing revealed that asymmetric CSM led to anything below 1.5 being insufficient +NEIGHBOUR_CUTOFF = 1.6 def enforce_xy_tuple(value: XYPos) -> XYPos: From 501fe3f9d2bcba7bf2f63e7540403ee7a0e83142 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 19 Aug 2025 14:07:30 +0100 Subject: [PATCH 2/5] Switch to neighbour cutoff based on dx and dy --- src/openflexure_microscope_server/scan_planners.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 615e6df5..3151e5f4 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -370,8 +370,7 @@ class SmartSpiral(ScanPlanner): Lowest position is best, as starting too high causes smart stacking to autofocus and restart. Starting too low just requires extra movements in +z. - Nearby is defined as within NEIGHBOUR_CUTOFF times the distance to the closest - neighbour. + Nearby is defined as within 1.1 times the larger of the x and y scan offsets. Returns None if there if no focused locations are present """ @@ -386,9 +385,13 @@ class SmartSpiral(ScanPlanner): # Note linalg.norm always uses float64 dists = np.linalg.norm((path_pos - current_pos), axis=1) - # Get indices of all focused sites within NEIGHBOUR_CUTOFF the minimum distance. + # distance_cutoff is the larger of the x and y offsets, times 1.1 to ensure + # that rounding at any point doesn't cause problems + distance_cutoff = max([self._dx, self._dy]) * 1.1 + + # Get indices of all focused sites within distance_cutoff. # Note np.where always returns a tuple of arrays, hence the trailing [0] - indices = np.where(dists <= NEIGHBOUR_CUTOFF * np.min(dists))[0] + indices = np.where(dists <= distance_cutoff)[0] # Turning into an array allows slicing based on a list focused_locations_array = np.array(self._focused_locations) From 85e10189be5eb503d1d43d17acbcc8dbc61de529 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 19 Aug 2025 14:54:38 +0100 Subject: [PATCH 3/5] Restore behaviour that uses nearest focused pos if adjacent fails --- src/openflexure_microscope_server/scan_planners.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 3151e5f4..e1fba871 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -372,7 +372,10 @@ class SmartSpiral(ScanPlanner): autofocus and restart. Starting too low just requires extra movements in +z. Nearby is defined as within 1.1 times the larger of the x and y scan offsets. - Returns None if there if no focused locations are present + If no focused sites are within this range, use the height of the nearest + focused site. + + Returns None if no focused locations are present """ if not self._focused_locations: return None @@ -393,6 +396,13 @@ class SmartSpiral(ScanPlanner): # Note np.where always returns a tuple of arrays, hence the trailing [0] indices = np.where(dists <= distance_cutoff)[0] + # Handle the case that no focused positions are within this range, and + # instead use the nearest focused position. This will always return a + # height, due to the check that self._focused_locations exists. + if len(indices) == 0: + distance_cutoff = min(dists) + indices = np.where(dists <= distance_cutoff)[0] + # Turning into an array allows slicing based on a list focused_locations_array = np.array(self._focused_locations) From ce5a22eb646b8bf7768ac37b333947f6fc65087e Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 5 Sep 2025 10:55:03 +0100 Subject: [PATCH 4/5] Added neighbour ratio to spiral scan init --- .../scan_planners.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index e1fba871..40130606 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -19,13 +19,6 @@ XYPosList: TypeAlias = list[XYPos] XYZPosList: TypeAlias = list[XYZPos] -# how many times the minimum distance between images to include as a "nearby" image -# default 1.6 includes images offset in x or y, but not diagonally. -# This wis based of a 4:3 aspect ratio. So x moves are 1.33 times larger than y -# testing revealed that asymmetric CSM led to anything below 1.5 being insufficient -NEIGHBOUR_CUTOFF = 1.6 - - def enforce_xy_tuple(value: XYPos) -> XYPos: """Check input is a tuple and is of length 2. @@ -243,18 +236,30 @@ class SmartSpiral(ScanPlanner): Each time and image is taken the four neighbouring images are added to the list of positions 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. + tried). However, if a location is not imaged due no sample being detected, + then neighbouring 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. + The next image taken is the fewest scan sites (moves in dx and dy) from the current + site, with ties broken by minimising the moves away from the start of the scan. + Final tiebreak is the distance to each site, in motor steps rather than multiple of + dx and dy. """ _max_dist: int = 0 _dx: int = 0 _dy: int = 0 + def __init__( + self, initial_position: XYPos, planner_settings: Optional[dict] = None + ): + """Set up the lists inherited from ScanPlanner, plus a distance cutoff. + + Use the supplied _dx and _dy to set a distance cutoff for an image to be + considered neighbouring another + """ + super().__init__(initial_position, planner_settings) + self._distance_cutoff = max([self._dx, self._dy]) * 1.1 + def _parse(self, planner_settings: Optional[dict] = None) -> None: """Parse SmartSpiral Settings dictionary. @@ -388,13 +393,9 @@ class SmartSpiral(ScanPlanner): # Note linalg.norm always uses float64 dists = np.linalg.norm((path_pos - current_pos), axis=1) - # distance_cutoff is the larger of the x and y offsets, times 1.1 to ensure - # that rounding at any point doesn't cause problems - distance_cutoff = max([self._dx, self._dy]) * 1.1 - # Get indices of all focused sites within distance_cutoff. # Note np.where always returns a tuple of arrays, hence the trailing [0] - indices = np.where(dists <= distance_cutoff)[0] + indices = np.where(dists <= self._distance_cutoff)[0] # Handle the case that no focused positions are within this range, and # instead use the nearest focused position. This will always return a From 980b1e5187a5a071e43a49dab7bdb7a2be0cc072 Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 5 Sep 2025 11:22:43 +0100 Subject: [PATCH 5/5] Type hints to inits in scan planners --- src/openflexure_microscope_server/scan_planners.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index 40130606..847fee45 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -74,7 +74,7 @@ class ScanPlanner: def __init__( self, initial_position: XYPos, planner_settings: Optional[dict] = None - ): + ) -> None: """Set up lists for the path planning, and scan history.""" self._initial_position = enforce_xy_tuple(initial_position) self._parse(planner_settings) @@ -251,14 +251,14 @@ class SmartSpiral(ScanPlanner): def __init__( self, initial_position: XYPos, planner_settings: Optional[dict] = None - ): + ) -> None: """Set up the lists inherited from ScanPlanner, plus a distance cutoff. Use the supplied _dx and _dy to set a distance cutoff for an image to be considered neighbouring another """ super().__init__(initial_position, planner_settings) - self._distance_cutoff = max([self._dx, self._dy]) * 1.1 + self._distance_cutoff: float = max([self._dx, self._dy]) * 1.1 def _parse(self, planner_settings: Optional[dict] = None) -> None: """Parse SmartSpiral Settings dictionary.