Simplify planners a little to consolidate snake and raster

This commit is contained in:
Julian Stirling 2026-02-10 17:33:31 +00:00
parent 1221b6ca02
commit 9e477a26d8
2 changed files with 25 additions and 54 deletions

View file

@ -304,7 +304,7 @@ class ScanPlanner:
return [FutureScanLocation(location) for line in grid for location in line] 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.""" """Base class for planners that operate on a rectangular grid."""
_dx: int = 0 _dx: int = 0
@ -312,7 +312,7 @@ class RectangleScan(ScanPlanner):
def _parse(self, planner_settings: Optional[dict] = None) -> None: def _parse(self, planner_settings: Optional[dict] = None) -> None:
expected_keys = ["dx", "dy"] expected_keys = ["dx", "dy"]
invalid_msg = "RectangleScan requires planner_settings with keys: " invalid_msg = "RectGridPlanner requires planner_settings with keys: "
if not planner_settings: if not planner_settings:
raise ValueError(invalid_msg + ",".join(expected_keys)) raise ValueError(invalid_msg + ",".join(expected_keys))
if not all(k in planner_settings for k in 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]] return focused_locations[nearby_indices[-1]]
class SmartSpiral(RectangleScan): class SmartSpiral(RectGridPlanner):
"""A scan planner that spirals outward from the centre, prioritising short moves. """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 This planner spirals out from the centre, but prioritises short moves over rigidly
@ -603,8 +603,10 @@ class SmartSpiral(RectangleScan):
) )
class SnakeScan(RectangleScan): class RegularGridPlanner(RectGridPlanner):
"""A scan planner that performs a snake scan, right and down from a corner. """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, 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.) starting moving right and down (assuming positive dx and dy.)
@ -612,11 +614,12 @@ class SnakeScan(RectangleScan):
_x_count: int = 0 _x_count: int = 0
_y_count: int = 0 _y_count: int = 0
_style: Literal["snake", "raster"]
def _parse(self, planner_settings: Optional[dict] = None) -> None: def _parse(self, planner_settings: Optional[dict] = None) -> None:
super()._parse(planner_settings) 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: " invalid_msg = "SnakeScan requires planner_settings with keys: "
if not planner_settings or not all( if not planner_settings or not all(
k in planner_settings for k in expected_keys 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._x_count = int(planner_settings["x_count"])
self._y_count = int(planner_settings["y_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]: def _initial_location_list(self) -> list[FutureScanLocation]:
"""Set the initial list of locations for this scan planner. """Set the initial list of locations for this scan planner.
@ -639,49 +648,7 @@ class SnakeScan(RectangleScan):
y_count=self._y_count, y_count=self._y_count,
dx=self._dx, dx=self._dx,
dy=self._dy, dy=self._dy,
style="snake", style=self._style,
)
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",
) )
return self._grid_to_future_locations(grid) return self._grid_to_future_locations(grid)

View file

@ -9,6 +9,7 @@ from __future__ import annotations
import os import os
from typing import ( from typing import (
Generic, Generic,
Literal,
Mapping, Mapping,
Optional, Optional,
TypeVar, TypeVar,
@ -19,10 +20,9 @@ from pydantic import BaseModel
import labthings_fastapi as lt import labthings_fastapi as lt
from openflexure_microscope_server.scan_planners import ( from openflexure_microscope_server.scan_planners import (
RasterScan, RegularGridPlanner,
ScanPlanner, ScanPlanner,
SmartSpiral, SmartSpiral,
SnakeScan,
) )
from openflexure_microscope_server.stitching import ( from openflexure_microscope_server.stitching import (
STITCHING_RESOLUTION, STITCHING_RESOLUTION,
@ -563,6 +563,7 @@ class RegularGridSettingsModel(BaseModel):
dy: int dy: int
x_count: int x_count: int
y_count: int y_count: int
style: Literal["snake", "raster"]
images_dir: str images_dir: str
autofocus_dz: int autofocus_dz: int
save_resolution: tuple[int, int] save_resolution: tuple[int, int]
@ -577,7 +578,8 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
"""The number of rows in the scan.""" """The number of rows in the scan."""
_settings_model = RegularGridSettingsModel _settings_model = RegularGridSettingsModel
_planner_cls: type[SnakeScan] | type[RasterScan] _planner_cls = RegularGridPlanner
_grid_style: Literal["snake", "raster"]
def all_settings( def all_settings(
self, images_dir: str self, images_dir: str
@ -597,6 +599,7 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
dy=dy, dy=dy,
x_count=self.x_count, x_count=self.x_count,
y_count=self.y_count, y_count=self.y_count,
style=self._grid_style,
images_dir=images_dir, images_dir=images_dir,
autofocus_dz=self.autofocus_dz, autofocus_dz=self.autofocus_dz,
save_resolution=self.save_resolution, save_resolution=self.save_resolution,
@ -626,6 +629,7 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
"dy": settings.dy, "dy": settings.dy,
"x_count": settings.x_count, "x_count": settings.x_count,
"y_count": settings.y_count, "y_count": settings.y_count,
"style": settings.style,
} }
return self._planner_cls( return self._planner_cls(
initial_position=(position["x"], position["y"]), initial_position=(position["x"], position["y"]),
@ -675,7 +679,7 @@ class SnakeWorkflow(RegularGridWorkflow):
), ),
readonly=True, readonly=True,
) )
_planner_cls = SnakeScan _grid_style = "snake"
class RasterWorkflow(RegularGridWorkflow): class RasterWorkflow(RegularGridWorkflow):
@ -695,4 +699,4 @@ class RasterWorkflow(RegularGridWorkflow):
readonly=True, readonly=True,
) )
_planner_cls = RasterScan _grid_style = "raster"