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]
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)

View file

@ -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"