Loading openflexure_microscope/stage/sanga.py +90 −38 Original line number Diff line number Diff line Loading @@ -12,6 +12,19 @@ from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.utilities import axes_to_array def _displacement_to_array( displacement: int, axis: Literal["x", "y", "z"] ) -> np.ndarray: # Create the displacement array return np.array( [ displacement if axis == "x" else 0, displacement if axis == "y" else 0, displacement if axis == "z" else 0, ] ) class SangaStage(BaseStage): """ Sangaboard v0.2 and v0.3 powered Stage object Loading Loading @@ -55,7 +68,7 @@ class SangaStage(BaseStage): return 3 @property def position(self): def position(self) -> Tuple[int, int, int]: return self.board.position @property Loading Loading @@ -120,7 +133,7 @@ class SangaStage(BaseStage): def move_rel( self, displacement: Union[int, Tuple[int, int, int]], displacement: Union[int, Tuple[int, int, int], np.ndarray], axis: Optional[Literal["x", "y", "z"]] = None, backlash: bool = True, ): Loading @@ -141,10 +154,6 @@ class SangaStage(BaseStage): """ with self.lock: logging.debug("Moving sangaboard by %s", displacement) if not backlash or self.backlash is None: return self.board.move_rel(displacement, axis=axis) # 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 Loading @@ -155,16 +164,10 @@ class SangaStage(BaseStage): # 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") # Create the displacement array displacement_array: np.ndarray = np.array( [ displacement if axis == "x" else 0, displacement if axis == "y" else 0, displacement if axis == "z" else 0, ] # Calculate displacement array displacement_array: np.ndarray = _displacement_to_array( displacement, axis ) elif isinstance(displacement, np.ndarray): displacement_array = displacement elif isinstance(displacement, (list, tuple, GeneratorType)): Loading @@ -173,6 +176,11 @@ class SangaStage(BaseStage): else: raise TypeError(f"Unsupported displacement type {type(displacement)}") # Handle simple case, no backlash if not backlash or self.backlash is None: return self.board.move_rel(displacement_array) # Handle move with backlash correction # Calculate main movement initial_move: np.ndarray = displacement_array initial_move -= np.where( Loading @@ -191,7 +199,7 @@ class SangaStage(BaseStage): # can just take over before settling time.sleep(self.settle_time) def move_abs(self, final: Tuple[int, int, int], **kwargs): def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs): """Make an absolute move to a position """ with self.lock: Loading Loading @@ -244,15 +252,21 @@ class SangaStage(BaseStage): class SangaDeltaStage(SangaStage): def __init__( self, port=None, flex_h=80, flex_a=50, flex_b=50, camera_angle=0, **kwargs self, port: Optional[str] = None, flex_h: int = 80, flex_a: int = 50, flex_b: int = 50, camera_angle: float = 0, **kwargs, ): self.flex_h = flex_h self.flex_a = flex_a self.flex_b = flex_b self.flex_h: int = flex_h self.flex_a: int = flex_a self.flex_b: int = flex_b # Set up camera rotation relative to stage camera_theta = (camera_angle / 180) * np.pi self.R_camera = np.array( camera_theta: float = (camera_angle / 180) * np.pi self.R_camera: np.ndarray = np.array( [ [np.cos(camera_theta), -np.sin(camera_theta), 0], [np.sin(camera_theta), np.cos(camera_theta), 0], Loading @@ -263,13 +277,15 @@ class SangaDeltaStage(SangaStage): logging.debug(self.R_camera) # Transformation matrix converting delta into cartesian x_fac = -1 * np.multiply( x_fac: np.float = -1 * np.multiply( np.divide(2, np.sqrt(3)), np.divide(self.flex_b, self.flex_h) ) y_fac = -1 * np.divide(self.flex_b, self.flex_h) z_fac = np.multiply(np.divide(1, 3), np.divide(self.flex_b, self.flex_a)) y_fac: np.float = -1 * np.divide(self.flex_b, self.flex_h) z_fac: np.float = np.multiply( np.divide(1, 3), np.divide(self.flex_b, self.flex_a) ) self.Tvd = np.array( self.Tvd: np.ndarray = np.array( [ [-x_fac, x_fac, 0], [0.5 * y_fac, 0.5 * y_fac, -y_fac], Loading @@ -278,40 +294,76 @@ class SangaDeltaStage(SangaStage): ) logging.debug(self.Tvd) self.Tdv = np.linalg.inv(self.Tvd) self.Tdv: np.ndarray = np.linalg.inv(self.Tvd) logging.debug(self.Tdv) SangaStage.__init__(self, port=port, **kwargs) @property def raw_position(self) -> Tuple[int, int, int]: return self.board.position @property def position(self): # TODO: Account for camera rotation position = np.dot(self.Tvd, self.board.position) position: np.ndarray = np.dot(self.Tvd, self.raw_position) position = np.dot(np.linalg.inv(self.R_camera), position) position: np.ndarray = np.dot(np.linalg.inv(self.R_camera), position) return [int(p) for p in position] def move_rel(self, displacement, axis=None, backlash=True): def move_rel( self, displacement: Union[int, Tuple[int, int, int], np.ndarray], axis: Optional[Literal["x", "y", "z"]] = None, backlash: bool = True, ): # 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") # Calculate displacement array cartesian_displacement_array: np.ndarray = _displacement_to_array( displacement, axis ) elif isinstance(displacement, np.ndarray): cartesian_displacement_array = displacement elif isinstance(displacement, (list, tuple, GeneratorType)): # Convert our displacement tuple/generator into a numpy array cartesian_displacement_array = np.array(list(displacement)) else: raise TypeError(f"Unsupported displacement type {type(displacement)}") # Transform into camera coordinates displacement = np.dot(self.R_camera, displacement) camera_displacement_array: np.ndarray = np.dot( self.R_camera, cartesian_displacement_array ) # Transform into delta coordinates displacement = np.dot(self.Tdv, displacement) delta_displacement_array: np.ndarray = np.dot( self.Tdv, camera_displacement_array ) logging.debug("Delta displacement: %s", (displacement)) logging.debug("Delta displacement: %s", (delta_displacement_array)) # Do the move SangaStage.move_rel(self, displacement, axis=None, backlash=backlash) SangaStage.move_rel( self, delta_displacement_array, axis=None, backlash=backlash ) def move_abs(self, final, **kwargs): def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs): # Transform into camera coordinates final = np.dot(self.R_camera, final) camera_final_array: np.ndarray = np.dot(self.R_camera, final) # Transform into delta coordinates final = np.dot(self.Tdv, final) delta_final_array: np.ndarray = np.dot(self.Tdv, camera_final_array) logging.debug("Delta final: %s", (final)) # Do the move SangaStage.move_abs(self, final, **kwargs) SangaStage.move_abs(self, delta_final_array, **kwargs) Loading
openflexure_microscope/stage/sanga.py +90 −38 Original line number Diff line number Diff line Loading @@ -12,6 +12,19 @@ from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.utilities import axes_to_array def _displacement_to_array( displacement: int, axis: Literal["x", "y", "z"] ) -> np.ndarray: # Create the displacement array return np.array( [ displacement if axis == "x" else 0, displacement if axis == "y" else 0, displacement if axis == "z" else 0, ] ) class SangaStage(BaseStage): """ Sangaboard v0.2 and v0.3 powered Stage object Loading Loading @@ -55,7 +68,7 @@ class SangaStage(BaseStage): return 3 @property def position(self): def position(self) -> Tuple[int, int, int]: return self.board.position @property Loading Loading @@ -120,7 +133,7 @@ class SangaStage(BaseStage): def move_rel( self, displacement: Union[int, Tuple[int, int, int]], displacement: Union[int, Tuple[int, int, int], np.ndarray], axis: Optional[Literal["x", "y", "z"]] = None, backlash: bool = True, ): Loading @@ -141,10 +154,6 @@ class SangaStage(BaseStage): """ with self.lock: logging.debug("Moving sangaboard by %s", displacement) if not backlash or self.backlash is None: return self.board.move_rel(displacement, axis=axis) # 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 Loading @@ -155,16 +164,10 @@ class SangaStage(BaseStage): # 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") # Create the displacement array displacement_array: np.ndarray = np.array( [ displacement if axis == "x" else 0, displacement if axis == "y" else 0, displacement if axis == "z" else 0, ] # Calculate displacement array displacement_array: np.ndarray = _displacement_to_array( displacement, axis ) elif isinstance(displacement, np.ndarray): displacement_array = displacement elif isinstance(displacement, (list, tuple, GeneratorType)): Loading @@ -173,6 +176,11 @@ class SangaStage(BaseStage): else: raise TypeError(f"Unsupported displacement type {type(displacement)}") # Handle simple case, no backlash if not backlash or self.backlash is None: return self.board.move_rel(displacement_array) # Handle move with backlash correction # Calculate main movement initial_move: np.ndarray = displacement_array initial_move -= np.where( Loading @@ -191,7 +199,7 @@ class SangaStage(BaseStage): # can just take over before settling time.sleep(self.settle_time) def move_abs(self, final: Tuple[int, int, int], **kwargs): def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs): """Make an absolute move to a position """ with self.lock: Loading Loading @@ -244,15 +252,21 @@ class SangaStage(BaseStage): class SangaDeltaStage(SangaStage): def __init__( self, port=None, flex_h=80, flex_a=50, flex_b=50, camera_angle=0, **kwargs self, port: Optional[str] = None, flex_h: int = 80, flex_a: int = 50, flex_b: int = 50, camera_angle: float = 0, **kwargs, ): self.flex_h = flex_h self.flex_a = flex_a self.flex_b = flex_b self.flex_h: int = flex_h self.flex_a: int = flex_a self.flex_b: int = flex_b # Set up camera rotation relative to stage camera_theta = (camera_angle / 180) * np.pi self.R_camera = np.array( camera_theta: float = (camera_angle / 180) * np.pi self.R_camera: np.ndarray = np.array( [ [np.cos(camera_theta), -np.sin(camera_theta), 0], [np.sin(camera_theta), np.cos(camera_theta), 0], Loading @@ -263,13 +277,15 @@ class SangaDeltaStage(SangaStage): logging.debug(self.R_camera) # Transformation matrix converting delta into cartesian x_fac = -1 * np.multiply( x_fac: np.float = -1 * np.multiply( np.divide(2, np.sqrt(3)), np.divide(self.flex_b, self.flex_h) ) y_fac = -1 * np.divide(self.flex_b, self.flex_h) z_fac = np.multiply(np.divide(1, 3), np.divide(self.flex_b, self.flex_a)) y_fac: np.float = -1 * np.divide(self.flex_b, self.flex_h) z_fac: np.float = np.multiply( np.divide(1, 3), np.divide(self.flex_b, self.flex_a) ) self.Tvd = np.array( self.Tvd: np.ndarray = np.array( [ [-x_fac, x_fac, 0], [0.5 * y_fac, 0.5 * y_fac, -y_fac], Loading @@ -278,40 +294,76 @@ class SangaDeltaStage(SangaStage): ) logging.debug(self.Tvd) self.Tdv = np.linalg.inv(self.Tvd) self.Tdv: np.ndarray = np.linalg.inv(self.Tvd) logging.debug(self.Tdv) SangaStage.__init__(self, port=port, **kwargs) @property def raw_position(self) -> Tuple[int, int, int]: return self.board.position @property def position(self): # TODO: Account for camera rotation position = np.dot(self.Tvd, self.board.position) position: np.ndarray = np.dot(self.Tvd, self.raw_position) position = np.dot(np.linalg.inv(self.R_camera), position) position: np.ndarray = np.dot(np.linalg.inv(self.R_camera), position) return [int(p) for p in position] def move_rel(self, displacement, axis=None, backlash=True): def move_rel( self, displacement: Union[int, Tuple[int, int, int], np.ndarray], axis: Optional[Literal["x", "y", "z"]] = None, backlash: bool = True, ): # 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") # Calculate displacement array cartesian_displacement_array: np.ndarray = _displacement_to_array( displacement, axis ) elif isinstance(displacement, np.ndarray): cartesian_displacement_array = displacement elif isinstance(displacement, (list, tuple, GeneratorType)): # Convert our displacement tuple/generator into a numpy array cartesian_displacement_array = np.array(list(displacement)) else: raise TypeError(f"Unsupported displacement type {type(displacement)}") # Transform into camera coordinates displacement = np.dot(self.R_camera, displacement) camera_displacement_array: np.ndarray = np.dot( self.R_camera, cartesian_displacement_array ) # Transform into delta coordinates displacement = np.dot(self.Tdv, displacement) delta_displacement_array: np.ndarray = np.dot( self.Tdv, camera_displacement_array ) logging.debug("Delta displacement: %s", (displacement)) logging.debug("Delta displacement: %s", (delta_displacement_array)) # Do the move SangaStage.move_rel(self, displacement, axis=None, backlash=backlash) SangaStage.move_rel( self, delta_displacement_array, axis=None, backlash=backlash ) def move_abs(self, final, **kwargs): def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs): # Transform into camera coordinates final = np.dot(self.R_camera, final) camera_final_array: np.ndarray = np.dot(self.R_camera, final) # Transform into delta coordinates final = np.dot(self.Tdv, final) delta_final_array: np.ndarray = np.dot(self.Tdv, camera_final_array) logging.debug("Delta final: %s", (final)) # Do the move SangaStage.move_abs(self, final, **kwargs) SangaStage.move_abs(self, delta_final_array, **kwargs)