Static type analysis

This commit is contained in:
Joel Collins 2020-11-30 13:36:45 +00:00
parent 3aebb8bead
commit 7866ec0f47
63 changed files with 1825 additions and 2722 deletions

View file

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

View file

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

View file

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