Static type analysis
This commit is contained in:
parent
3aebb8bead
commit
7866ec0f47
63 changed files with 1825 additions and 2722 deletions
|
|
@ -1,7 +1,9 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from labthings import StrictLock
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
class BaseStage(metaclass=ABCMeta):
|
||||
|
|
@ -39,11 +41,11 @@ class BaseStage(metaclass=ABCMeta):
|
|||
|
||||
@property
|
||||
@abstractmethod
|
||||
def position(self):
|
||||
def position(self) -> Tuple[int, int, int]:
|
||||
"""The current position, as a list"""
|
||||
|
||||
@property
|
||||
def position_map(self):
|
||||
def position_map(self) -> Dict[str, int]:
|
||||
return {"x": self.position[0], "y": self.position[1], "z": self.position[2]}
|
||||
|
||||
@property
|
||||
|
|
@ -52,19 +54,27 @@ class BaseStage(metaclass=ABCMeta):
|
|||
"""Get the distance used for backlash compensation."""
|
||||
|
||||
@backlash.setter
|
||||
@abstractmethod
|
||||
def backlash(self):
|
||||
"""Set the distance used for backlash compensation."""
|
||||
# See: https://github.com/python/mypy/issues/4165
|
||||
# Since we can't also decorate this with abstract method we want to be
|
||||
# sure that the setter doesn't actually get used as a noop.
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def move_rel(self, displacement: list, axis=None, backlash=True):
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
"""Make a relative move, optionally correcting for backlash.
|
||||
displacement: integer or array/list of 3 integers
|
||||
backlash: (default: True) whether to correct for backlash.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def move_abs(self, final, **kwargs):
|
||||
def move_abs(self, final: Tuple[int, int, int], **kwargs):
|
||||
"""Make an absolute move to a position"""
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -75,7 +85,12 @@ class BaseStage(metaclass=ABCMeta):
|
|||
def close(self):
|
||||
"""Cleanly close communication with the stage"""
|
||||
|
||||
def scan_linear(self, rel_positions, backlash=True, return_to_start=True):
|
||||
def scan_linear(
|
||||
self,
|
||||
rel_positions: List[Tuple[int, int, int]],
|
||||
backlash: bool = True,
|
||||
return_to_start: bool = True,
|
||||
):
|
||||
"""
|
||||
Scan through a list of (relative) positions (generator fn)
|
||||
rel_positions should be an nx3-element array (or list of 3 element arrays).
|
||||
|
|
@ -86,15 +101,15 @@ class BaseStage(metaclass=ABCMeta):
|
|||
exception occurs during the scan..
|
||||
"""
|
||||
starting_position = self.position
|
||||
rel_positions = np.array(rel_positions)
|
||||
assert rel_positions.shape[1] == 3, ValueError(
|
||||
rel_positions_array: np.ndarray = np.array(rel_positions)
|
||||
assert rel_positions_array.shape[1] == 3, ValueError(
|
||||
"Positions should be 3 elements long."
|
||||
)
|
||||
try:
|
||||
self.move_rel(rel_positions[0], backlash=backlash)
|
||||
self.move_rel(rel_positions_array[0], backlash=backlash)
|
||||
yield 0
|
||||
|
||||
for i, step in enumerate(np.diff(rel_positions, axis=0)):
|
||||
for i, step in enumerate(np.diff(rel_positions_array, axis=0)):
|
||||
self.move_rel(step, backlash=backlash)
|
||||
yield i + 1
|
||||
except Exception as e:
|
||||
|
|
@ -104,11 +119,11 @@ class BaseStage(metaclass=ABCMeta):
|
|||
if return_to_start:
|
||||
self.move_abs(starting_position, backlash=backlash)
|
||||
|
||||
def scan_z(self, dz, **kwargs):
|
||||
def scan_z(self, dz: List[int], **kwargs):
|
||||
"""Scan through a list of (relative) z positions (generator fn)
|
||||
This function takes a 1D numpy array of Z positions, relative to
|
||||
the position at the start of the scan, and converts it into an
|
||||
array of 3D positions with x=y=0. This, along with all the
|
||||
keyword arguments, is then passed to ``scan_linear``.
|
||||
"""
|
||||
return self.scan_linear([[0, 0, z] for z in dz], **kwargs)
|
||||
return self.scan_linear([(0, 0, z) for z in dz], **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import logging
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openflexure_microscope.stage.base import BaseStage
|
||||
from openflexure_microscope.utilities import axes_to_array
|
||||
|
|
@ -15,8 +17,6 @@ class MissingStage(BaseStage):
|
|||
self._n_axis = 3
|
||||
self._backlash = None
|
||||
|
||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""The general state dictionary of the board."""
|
||||
|
|
@ -66,14 +66,27 @@ class MissingStage(BaseStage):
|
|||
else:
|
||||
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
||||
|
||||
def move_rel(self, displacement: list, axis=None, backlash=True):
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
time.sleep(0.5)
|
||||
if axis is not None:
|
||||
assert axis in self.axis_names, "axis must be one of {}".format(
|
||||
self.axis_names
|
||||
if axis:
|
||||
# Displacement MUST be an integer if axis name is specified
|
||||
if not isinstance(displacement, int):
|
||||
raise TypeError(
|
||||
"Displacement must be an integer when axis is specified"
|
||||
)
|
||||
# Axis name MUST be x, y, or z
|
||||
if axis not in ("x", "y", "z"):
|
||||
raise ValueError("axis must be one of x, y, or z")
|
||||
move = (
|
||||
displacement if axis == "x" else 0,
|
||||
displacement if axis == "y" else 0,
|
||||
displacement if axis == "z" else 0,
|
||||
)
|
||||
move = np.zeros(self.n_axes, dtype=np.int)
|
||||
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
|
||||
displacement = move
|
||||
|
||||
initial_move = np.array(displacement, dtype=np.int)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import logging
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from sangaboard import Sangaboard
|
||||
from typing_extensions import Literal
|
||||
|
||||
from openflexure_microscope.stage.base import BaseStage
|
||||
from openflexure_microscope.utilities import axes_to_array
|
||||
|
|
@ -32,7 +34,6 @@ class SangaStage(BaseStage):
|
|||
None # Initialise backlash storage, used by property setter/getter
|
||||
)
|
||||
self.settle_time = 0.2 # Default move settle time
|
||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||
|
||||
self._position_on_enter = None
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ class SangaStage(BaseStage):
|
|||
@property
|
||||
def n_axes(self):
|
||||
"""The number of axes this stage has."""
|
||||
return len(self.board.axis_names)
|
||||
return 3
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
|
|
@ -111,7 +112,12 @@ class SangaStage(BaseStage):
|
|||
|
||||
return config
|
||||
|
||||
def move_rel(self, displacement: list, axis=None, backlash=True):
|
||||
def move_rel(
|
||||
self,
|
||||
displacement: Union[int, Tuple[int, int, int]],
|
||||
axis: Optional[Literal["x", "y", "z"]] = None,
|
||||
backlash: bool = True,
|
||||
):
|
||||
"""Make a relative move, optionally correcting for backlash.
|
||||
displacement: integer or array/list of 3 integers
|
||||
axis: None (for 3-axis moves) or one of 'x','y','z'
|
||||
|
|
@ -121,14 +127,21 @@ class SangaStage(BaseStage):
|
|||
logging.debug("Moving sangaboard by %s", displacement)
|
||||
if not backlash or self.backlash is None:
|
||||
return self.board.move_rel(displacement, axis=axis)
|
||||
if axis is not None:
|
||||
# backlash correction is easier if we're always in 3D
|
||||
# so this code just converts single-axis moves into all-axis moves.
|
||||
assert axis in self.axis_names, "axis must be one of {}".format(
|
||||
self.axis_names
|
||||
# If we specify an axis name and a displacement int, convert to a displacement tuple
|
||||
if axis:
|
||||
# Displacement MUST be an integer if axis name is specified
|
||||
if not isinstance(displacement, int):
|
||||
raise TypeError(
|
||||
"Displacement must be an integer when axis is specified"
|
||||
)
|
||||
# Axis name MUST be x, y, or z
|
||||
if axis not in ("x", "y", "z"):
|
||||
raise ValueError("axis must be one of x, y, or z")
|
||||
move = (
|
||||
displacement if axis == "x" else 0,
|
||||
displacement if axis == "y" else 0,
|
||||
displacement if axis == "z" else 0,
|
||||
)
|
||||
move = np.zeros(self.n_axes, dtype=np.int)
|
||||
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
|
||||
displacement = move
|
||||
|
||||
initial_move = np.array(displacement, dtype=np.int)
|
||||
|
|
@ -154,7 +167,7 @@ class SangaStage(BaseStage):
|
|||
# can just take over before settling
|
||||
time.sleep(self.settle_time)
|
||||
|
||||
def move_abs(self, final, **kwargs):
|
||||
def move_abs(self, final: Tuple[int, int, int], **kwargs):
|
||||
"""Make an absolute move to a position
|
||||
"""
|
||||
with self.lock:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue