Fix typing in stage_measure

This commit is contained in:
Julian Stirling 2025-12-17 22:19:25 +00:00
parent 85671e897e
commit dcf329f1c5

View file

@ -14,8 +14,9 @@ this is 10% of the expected motion is the measured axis.
""" """
import time import time
from copy import copy
from threading import Lock from threading import Lock
from typing import Any, Literal, Optional, overload from typing import Any, Literal, Mapping, Optional, overload
import numpy as np import numpy as np
@ -48,20 +49,20 @@ class RomDataTracker:
def __init__(self) -> None: def __init__(self) -> None:
"""Define useful data tracked throughout test.""" """Define useful data tracked throughout test."""
self.stage_coords: list[dict[str, int]] = [] self.stage_coords: list[Mapping[str, int]] = []
self.offsets: list[dict[str, float]] = [] self.offsets: list[Mapping[str, float]] = []
def record_movement( def record_movement(
self, current_pos: dict[str, int], offset: dict[str, float] self, current_pos: Mapping[str, int], offset: Mapping[str, float]
) -> None: ) -> None:
"""Record the current position and the measured offset of the last move.""" """Record the current position and the measured offset of the last move."""
self.stage_coords.append(current_pos) self.stage_coords.append(current_pos)
self.offsets.append(offset) self.offsets.append(offset)
@property @property
def final_position(self) -> dict[str, int]: def final_position(self) -> Mapping[str, int]:
"""The last stage coordinate recorded.""" """The last stage coordinate recorded."""
return self.stage_coords[-1].copy() return copy(self.stage_coords[-1])
def fit_axis(self, axis: Literal["x", "y"]) -> np.poly1d: def fit_axis(self, axis: Literal["x", "y"]) -> np.poly1d:
"""Quadratic fit stage z against x or y and return poly1d of fit. """Quadratic fit stage z against x or y and return poly1d of fit.
@ -78,8 +79,8 @@ class RomDataTracker:
def predict_z_displacement( def predict_z_displacement(
self, self,
axis: Literal["x", "y"], axis: Literal["x", "y"],
stage_movement: dict[str, int], stage_movement: Mapping[str, int],
stage_position: dict[str, int], stage_position: Mapping[str, int],
) -> int: ) -> int:
"""Predict the z-displacement needed for a given movement. """Predict the z-displacement needed for a given movement.
@ -93,7 +94,7 @@ class RomDataTracker:
return int(z_dest - stage_position["z"]) return int(z_dest - stage_position["z"])
def find_turning_point(self, axis: Literal["x", "y"]) -> dict[str, int]: def find_turning_point(self, axis: Literal["x", "y"]) -> Mapping[str, int]:
"""Find the turing point from the recorded coordinates.""" """Find the turing point from the recorded coordinates."""
fit_func = self.fit_axis(axis) fit_func = self.fit_axis(axis)
turning_loc = fit_func.deriv().roots[0] turning_loc = fit_func.deriv().roots[0]
@ -121,7 +122,9 @@ class RangeofMotionThing(lt.Thing):
_csm: CameraStageMapper = lt.thing_slot() _csm: CameraStageMapper = lt.thing_slot()
_stage: BaseStage = lt.thing_slot() _stage: BaseStage = lt.thing_slot()
calibrated_range: Optional[list[int, int]] = lt.setting(default=None, readonly=True) calibrated_range: Optional[tuple[int, int]] = lt.setting(
default=None, readonly=True
)
def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None: def __init__(self, thing_server_interface: lt.ThingServerInterface) -> None:
"""Initialise and create the lock.""" """Initialise and create the lock."""
@ -131,7 +134,7 @@ class RangeofMotionThing(lt.Thing):
self._rom_data = RomDataTracker() self._rom_data = RomDataTracker()
@lt.action @lt.action
def perform_rom_test(self) -> dict[str, Any]: def perform_rom_test(self) -> Mapping[str, Any]:
"""Measures the range of motion of the stage across the x and y axes. """Measures the range of motion of the stage across the x and y axes.
:return: Results dictionary separated into keys of each axis and direction. :return: Results dictionary separated into keys of each axis and direction.
@ -173,7 +176,7 @@ class RangeofMotionThing(lt.Thing):
self.logger.info( self.logger.info(
f"Range of motion is {step_range[0]} x {step_range[1]} steps" f"Range of motion is {step_range[0]} x {step_range[1]} steps"
) )
self.calibrated_range = step_range self.calibrated_range = (step_range[0], step_range[1])
finally: finally:
self._lock.release() self._lock.release()
return { return {
@ -324,7 +327,7 @@ class RangeofMotionThing(lt.Thing):
# if the distance is less than 1 big step away then move to it and exit # if the distance is less than 1 big step away then move to it and exit
if abs(img_perc) < BIG_STEP: if abs(img_perc) < BIG_STEP:
self.logger.info(f"Estimated centre of {axis}-axis is {estimate[axis]}") self.logger.info(f"Estimated centre of {axis}-axis is {estimate[axis]}")
self._stage.move_absolute(**estimate) self._stage.move_absolute(**estimate, block_cancellation=False)
# Note the second return, the direction, is meaningless here. # Note the second return, the direction, is meaningless here.
return True, 1 return True, 1
self.logger.info( self.logger.info(
@ -353,7 +356,7 @@ class RangeofMotionThing(lt.Thing):
return (fov_perc / 100) * self._stream_resolution[img_index] return (fov_perc / 100) * self._stream_resolution[img_index]
def _img_dir_from_stage_coords( def _img_dir_from_stage_coords(
self, target: dict[str, int], axis: Literal["x", "y"] self, target: Mapping[str, int], axis: Literal["x", "y"]
) -> Literal[1, -1]: ) -> Literal[1, -1]:
"""For a target location in stage coords, return the direction in image coordinates. """For a target location in stage coords, return the direction in image coordinates.
@ -369,7 +372,7 @@ class RangeofMotionThing(lt.Thing):
return -1 if target_im_coords[axis] < current_loc_im_coords[axis] else 1 return -1 if target_im_coords[axis] < current_loc_im_coords[axis] else 1
def _distance_in_img_percentage( def _distance_in_img_percentage(
self, target: dict[str, int], axis: Literal["x", "y"] self, target: Mapping[str, int], axis: Literal["x", "y"]
) -> float: ) -> float:
"""For a target location in stage coords return the distance in percentage of FOV. """For a target location in stage coords return the distance in percentage of FOV.
@ -394,7 +397,7 @@ class RangeofMotionThing(lt.Thing):
fov_perc: int, fov_perc: int,
axis: Literal["x", "y"], axis: Literal["x", "y"],
direction: Literal[1, -1], direction: Literal[1, -1],
) -> dict[str, float]: ) -> Mapping[str, float]:
"""Return the dictionary for a move in image coordinates. """Return the dictionary for a move in image coordinates.
This dictionary can be passed directly to csm.move_in_image_coordinates This dictionary can be passed directly to csm.move_in_image_coordinates
@ -546,11 +549,11 @@ class RangeofMotionThing(lt.Thing):
def _move_and_measure( def _move_and_measure(
self, self,
movement: dict[str, float], movement: Mapping[str, float],
perform_autofocus: bool = True, perform_autofocus: bool = True,
max_autofocus_repeats: int = 0, max_autofocus_repeats: int = 0,
abs_min_offset: float = 0.0, abs_min_offset: float = 0.0,
) -> dict[str, float]: ) -> Mapping[str, float]:
"""Move the stage and measure the offset between the two positions. """Move the stage and measure the offset between the two positions.
:param movement: A dictionary containing the distance to move in image coords. :param movement: A dictionary containing the distance to move in image coords.
@ -596,7 +599,7 @@ class RangeofMotionThing(lt.Thing):
return offset return offset
def _offset_from(self, before_img: np.ndarray) -> dict[str, float]: def _offset_from(self, before_img: np.ndarray) -> Mapping[str, float]:
"""Take an image and calculate the offset from an input image. """Take an image and calculate the offset from an input image.
:param before_img: The image the offset should be calculated with respect to. :param before_img: The image the offset should be calculated with respect to.
@ -623,11 +626,11 @@ class RangeofMotionThing(lt.Thing):
# let MyPy know with overloads typed to Literal[False] and Literal[True]. # let MyPy know with overloads typed to Literal[False] and Literal[True].
@overload @overload
def _axis_from_movement_dict( def _axis_from_movement_dict(
movement: dict[str, float], return_other: Literal[False] movement: Mapping[str, float], return_other: Literal[False]
) -> str: ... ) -> str: ...
@overload @overload
def _axis_from_movement_dict( def _axis_from_movement_dict(
movement: dict[str, float], return_other: Literal[True] movement: Mapping[str, float], return_other: Literal[True]
) -> tuple[str, str]: ... ) -> tuple[str, str]: ...
@ -635,12 +638,12 @@ def _axis_from_movement_dict(
# because MyPy doesn't see that return other's default is `False` and use # because MyPy doesn't see that return other's default is `False` and use
# the `Literal[False]` overload. # the `Literal[False]` overload.
@overload @overload
def _axis_from_movement_dict(movement: dict[str, float]) -> str: ... def _axis_from_movement_dict(movement: Mapping[str, float]) -> str: ...
# Finally The function. # Finally The function.
def _axis_from_movement_dict( def _axis_from_movement_dict(
movement: dict[str, float], return_other: bool = False movement: Mapping[str, float], return_other: bool = False
) -> str | tuple[str, str]: ) -> str | tuple[str, str]:
"""Return the axis that a given movement dictionary moves in. """Return the axis that a given movement dictionary moves in.
@ -661,7 +664,7 @@ def _axis_from_movement_dict(
def _parasitic_motion_detected( def _parasitic_motion_detected(
movement: dict[str, float], offset: dict[str, float] movement: Mapping[str, float], offset: Mapping[str, float]
) -> bool: ) -> bool:
"""Compare desired movement to measured offset, report if parasitic motion was detected. """Compare desired movement to measured offset, report if parasitic motion was detected.