From a0fbbc3e4063b843106adf9d11ed7c45eb7862e8 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Fri, 29 Nov 2024 12:53:04 +0000 Subject: [PATCH] Convert Stage to a Protocol The Stage is now implemented as a runtime-checkable Protocol. Part of this led to the realisation that we can't create a DirectThingClient based on a Protocol, only on a concrete Thing. While it might be a good idea to address that in LabThings in due course, for now we work around it by creating both a Protocol and a stub class. The stub class implements the protocol, and can be used to declare a dependency. --- .../things/auto_recentre_stage.py | 3 +- .../things/autofocus.py | 3 +- .../things/camera/simulation.py | 2 +- .../things/camera_stage_mapping.py | 10 +-- .../things/smart_scan.py | 3 +- .../things/stage/__init__.py | 77 ++++++++++++++++--- .../things/stage/dummy.py | 4 +- 7 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/openflexure_microscope_server/things/auto_recentre_stage.py b/src/openflexure_microscope_server/things/auto_recentre_stage.py index 1a22c9d4..f0dfcf74 100644 --- a/src/openflexure_microscope_server/things/auto_recentre_stage.py +++ b/src/openflexure_microscope_server/things/auto_recentre_stage.py @@ -4,11 +4,10 @@ import logging from labthings_fastapi.thing import Thing from labthings_fastapi.dependencies.thing import direct_thing_client_dependency from labthings_fastapi.decorators import thing_action -from .stage import Stage +from .stage import StageDependency as StageDep from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper -StageDep = direct_thing_client_dependency(Stage, "/stage/") CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/") diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 4250ff7d..9f2dae2f 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -21,11 +21,10 @@ from labthings_fastapi.decorators import thing_action from labthings_fastapi.types.numpy import NDArray from .camera import RawCameraDependency as Camera from .camera import CameraDependency as WrappedCamera -from .stage import Stage as StageThing +from .stage import StageDependency as Stage import numpy as np from pydantic import BaseModel -Stage = direct_thing_client_dependency(StageThing, "/stage/") ### Autofocus utilities diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 9e764df7..dd169283 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -25,7 +25,7 @@ from labthings_fastapi.types.numpy import NDArray from labthings_fastapi.server import ThingServer from . import BaseCamera, JPEGBlob -from ..stage import Stage +from ..stage import StageProtocol as Stage class SimulatedCamera(BaseCamera): diff --git a/src/openflexure_microscope_server/things/camera_stage_mapping.py b/src/openflexure_microscope_server/things/camera_stage_mapping.py index cabc26a1..72415336 100644 --- a/src/openflexure_microscope_server/things/camera_stage_mapping.py +++ b/src/openflexure_microscope_server/things/camera_stage_mapping.py @@ -21,17 +21,13 @@ from camera_stage_mapping.camera_stage_calibration_1d import ( calibrate_backlash_1d, image_to_stage_displacement_from_1d, ) -from camera_stage_mapping.camera_stage_tracker import Tracker -from .camera import CameraDependency as Camera -from .stage import Stage as StageThing - -from labthings_fastapi.dependencies.thing import direct_thing_client_dependency from labthings_fastapi.dependencies.invocation import InvocationCancelledError, InvocationLogger from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.thing import Thing - -Stage = direct_thing_client_dependency(StageThing, "/stage/") +from camera_stage_mapping.camera_stage_tracker import Tracker +from .camera import CameraDependency as Camera +from .stage import StageDependency as Stage CoordinateType = Tuple[float, float, float] XYCoordinateType = Tuple[float, float] diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 7a9c60af..51a445eb 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -29,13 +29,12 @@ from labthings_fastapi.dependencies.invocation import CancelHook, InvocationLogg from labthings_fastapi.decorators import thing_action, thing_property, fastapi_endpoint from labthings_fastapi.outputs.blob import blob_type from .camera import CameraDependency as CamDep -from .stage import Stage +from .stage import StageDependency as StageDep from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.auto_recentre_stage import RecentringThing -StageDep = direct_thing_client_dependency(Stage, "/stage/") CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/") RecentreStage = direct_thing_client_dependency(RecentringThing, "/auto_recentre_stage/") diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index f600f58e..36d11a8b 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -1,16 +1,63 @@ from __future__ import annotations +from typing import Any, Protocol, TypeAlias, runtime_checkable from labthings_fastapi.descriptors.property import PropertyDescriptor from labthings_fastapi.thing import Thing from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.dependencies.invocation import CancelHook +from labthings_fastapi.dependencies.thing import direct_thing_client_dependency from collections.abc import Sequence, Mapping +@runtime_checkable +class StageProtocol(Protocol): + """A protocol for the OpenFlexure translation stage + """ + _axis_names: Sequence[str] -class Stage(Thing): - """A dummy stage for testing purposes + @property + def axis_names(self) -> Sequence[str]: + """The names of the stage's axes, in order.""" + ... + + @property + def position(self) -> Mapping[str, int]: + """Current position of the stage""" + ... + + @property + def moving() -> bool: + "Whether the stage is in motion" + ... + + @property + def thing_state(self) -> Mapping[str, Any]: + """Summary metadata describing the current state of the stage""" + ... - This stage should work similarly to a Sangaboard stage, but without any - hardware attached. + def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]): + """Make a relative move. Keyword arguments should be axis names.""" + ... + + def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]): + """Make an absolute move. Keyword arguments should be axis names.""" + ... + + def set_zero_position(self): + """Make the current position zero in all axes + + This action does not move the stage, but resets the position to zero. + It is intended for use after manually or automatically recentring the + stage. + """ + ... + + +class BaseStage(Thing): + """A base stage class for OpenFlexure translation stages + + This can't be used directly but should reduce boilerplate code when + implementing new stages. A minimal working stage must implement + `move_relative` and `move_absolute` actions, which update the + `position` property on completion, and provide `set_zero_position`. """ _axis_names = ("x", "y", "z") @@ -41,17 +88,26 @@ class Stage(Thing): return { "position": self.position } - + + +class StageStub(BaseStage): + """A Stub Thing for the OpenFlexure translation stage + + As of LabThings-FastAPI 0.0.7, we can't make a thing client dependency + based on a protocol, because the protocol is not a Thing, and its + methods/properties are not decorated as Affordances. This stub class + is a workaround for that limitation, and should not be used directly. + """ @thing_action def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]): """Make a relative move. Keyword arguments should be axis names.""" - raise NotImplementedError("Subclasses should implement this method") + raise NotImplementedError("StageStub should not be used directly") @thing_action def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]): """Make an absolute move. Keyword arguments should be axis names.""" - raise NotImplementedError("Subclasses should implement this method") - + raise NotImplementedError("StageStub should not be used directly") + @thing_action def set_zero_position(self): """Make the current position zero in all axes @@ -60,4 +116,7 @@ class Stage(Thing): It is intended for use after manually or automatically recentring the stage. """ - raise NotImplementedError("Subclasses should implement this method") + raise NotImplementedError("StageStub should not be used directly") + + +StageDependency: TypeAlias = direct_thing_client_dependency(StageStub, "/stage/") diff --git a/src/openflexure_microscope_server/things/stage/dummy.py b/src/openflexure_microscope_server/things/stage/dummy.py index 6e984470..907ebb16 100644 --- a/src/openflexure_microscope_server/things/stage/dummy.py +++ b/src/openflexure_microscope_server/things/stage/dummy.py @@ -4,10 +4,10 @@ from labthings_fastapi.dependencies.invocation import CancelHook, InvocationCanc from collections.abc import Mapping import time -from . import Stage +from . import BaseStage -class DummyStage(Stage): +class DummyStage(BaseStage): """A dummy stage for testing purposes This stage should work similarly to a Sangaboard stage, but without any