From 9e477a26d85afcbde085b682cbd6495b2446fb7e Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Tue, 10 Feb 2026 17:33:31 +0000 Subject: [PATCH] Simplify planners a little to consolidate snake and raster --- .../scan_planners.py | 65 +++++-------------- .../things/scan_workflows.py | 14 ++-- 2 files changed, 25 insertions(+), 54 deletions(-) diff --git a/src/openflexure_microscope_server/scan_planners.py b/src/openflexure_microscope_server/scan_planners.py index e08ec62f..d684634f 100644 --- a/src/openflexure_microscope_server/scan_planners.py +++ b/src/openflexure_microscope_server/scan_planners.py @@ -304,7 +304,7 @@ class ScanPlanner: return [FutureScanLocation(location) for line in grid for location in line] -class RectangleScan(ScanPlanner): +class RectGridPlanner(ScanPlanner): """Base class for planners that operate on a rectangular grid.""" _dx: int = 0 @@ -312,7 +312,7 @@ class RectangleScan(ScanPlanner): def _parse(self, planner_settings: Optional[dict] = None) -> None: expected_keys = ["dx", "dy"] - invalid_msg = "RectangleScan requires planner_settings with keys: " + invalid_msg = "RectGridPlanner requires planner_settings with keys: " if not planner_settings: raise ValueError(invalid_msg + ",".join(expected_keys)) if not all(k in planner_settings for k in expected_keys): @@ -394,7 +394,7 @@ class RectangleScan(ScanPlanner): return focused_locations[nearby_indices[-1]] -class SmartSpiral(RectangleScan): +class SmartSpiral(RectGridPlanner): """A scan planner that spirals outward from the centre, prioritising short moves. This planner spirals out from the centre, but prioritises short moves over rigidly @@ -603,8 +603,10 @@ class SmartSpiral(RectangleScan): ) -class SnakeScan(RectangleScan): - """A scan planner that performs a snake scan, right and down from a corner. +class RegularGridPlanner(RectGridPlanner): + """A scan planner that performs a snake or a raster scan. + + Direction cannot yet be set it always scans, right and down from a corner. This planner starts at the corner of the region to scan, snaking back and forth, starting moving right and down (assuming positive dx and dy.) @@ -612,11 +614,12 @@ class SnakeScan(RectangleScan): _x_count: int = 0 _y_count: int = 0 + _style: Literal["snake", "raster"] def _parse(self, planner_settings: Optional[dict] = None) -> None: super()._parse(planner_settings) - expected_keys = ["x_count", "y_count"] + expected_keys = ["x_count", "y_count", "style"] invalid_msg = "SnakeScan requires planner_settings with keys: " if not planner_settings or not all( k in planner_settings for k in expected_keys @@ -625,6 +628,12 @@ class SnakeScan(RectangleScan): self._x_count = int(planner_settings["x_count"]) self._y_count = int(planner_settings["y_count"]) + style = planner_settings["style"] + if style not in ("snake", "raster"): + raise ValueError( + f"Unknown regular grid style {style}. Use snake or raster." + ) + self._style = style def _initial_location_list(self) -> list[FutureScanLocation]: """Set the initial list of locations for this scan planner. @@ -639,49 +648,7 @@ class SnakeScan(RectangleScan): y_count=self._y_count, dx=self._dx, dy=self._dy, - style="snake", - ) - - return self._grid_to_future_locations(grid) - - -class RasterScan(RectangleScan): - """A scan planner that performs a snake scan, always moving right and down. - - This planner starts at the corner of the region to scan, and always moves right - to the end of the row, then down to the next row (assuming positive dx, dy). - """ - - _x_count: int = 0 - _y_count: int = 0 - - def _parse(self, planner_settings: Optional[dict] = None) -> None: - super()._parse(planner_settings) - - expected_keys = ["x_count", "y_count"] - invalid_msg = "SnakeScan requires planner_settings with keys: " - if not planner_settings or not all( - k in planner_settings for k in expected_keys - ): - raise KeyError(invalid_msg + ",".join(expected_keys)) - - self._x_count = int(planner_settings["x_count"]) - self._y_count = int(planner_settings["y_count"]) - - def _initial_location_list(self) -> list[FutureScanLocation]: - """Set the initial list of locations for this scan planner. - - This is called on initialisation. - - For raster scan, this is the full grid, and none will be added during scanning. - """ - grid = create_rectangular_scan_path( - starting_pos=self._initial_position, - x_count=self._x_count, - y_count=self._y_count, - dx=self._dx, - dy=self._dy, - style="raster", + style=self._style, ) return self._grid_to_future_locations(grid) diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 1afac9b8..9c538bb4 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -9,6 +9,7 @@ from __future__ import annotations import os from typing import ( Generic, + Literal, Mapping, Optional, TypeVar, @@ -19,10 +20,9 @@ from pydantic import BaseModel import labthings_fastapi as lt from openflexure_microscope_server.scan_planners import ( - RasterScan, + RegularGridPlanner, ScanPlanner, SmartSpiral, - SnakeScan, ) from openflexure_microscope_server.stitching import ( STITCHING_RESOLUTION, @@ -563,6 +563,7 @@ class RegularGridSettingsModel(BaseModel): dy: int x_count: int y_count: int + style: Literal["snake", "raster"] images_dir: str autofocus_dz: int save_resolution: tuple[int, int] @@ -577,7 +578,8 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]): """The number of rows in the scan.""" _settings_model = RegularGridSettingsModel - _planner_cls: type[SnakeScan] | type[RasterScan] + _planner_cls = RegularGridPlanner + _grid_style: Literal["snake", "raster"] def all_settings( self, images_dir: str @@ -597,6 +599,7 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]): dy=dy, x_count=self.x_count, y_count=self.y_count, + style=self._grid_style, images_dir=images_dir, autofocus_dz=self.autofocus_dz, save_resolution=self.save_resolution, @@ -626,6 +629,7 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]): "dy": settings.dy, "x_count": settings.x_count, "y_count": settings.y_count, + "style": settings.style, } return self._planner_cls( initial_position=(position["x"], position["y"]), @@ -675,7 +679,7 @@ class SnakeWorkflow(RegularGridWorkflow): ), readonly=True, ) - _planner_cls = SnakeScan + _grid_style = "snake" class RasterWorkflow(RegularGridWorkflow): @@ -695,4 +699,4 @@ class RasterWorkflow(RegularGridWorkflow): readonly=True, ) - _planner_cls = RasterScan + _grid_style = "raster"