diff --git a/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py b/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py index ba0e2b8c5..5c9d0ceb6 100644 --- a/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py +++ b/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py @@ -264,6 +264,9 @@ def set_feedback_alignment(self, axis: str, aa: float) -> None: decreased by 1 or 2 as described at the page on tuning stages to minimize move time. + Desirable values for the autozero command are between 90 and 164. We attempt + to autozero it 3x. + Parameters ---------- axis : str @@ -273,8 +276,39 @@ def set_feedback_alignment(self, axis: str, aa: float) -> None: """ self.send_command(f"AA {axis}={aa}\r") self.read_response() - self.send_command(f"AZ {axis}\r") - self.read_response() + + for i in range(3): + self.send_command(f"AZ {axis}\r") + + def get_feedback_alignment(self, axis: str) -> float: + """Get the stage feedback alignment. + + Returns the current value of the feedback alignment for the + specified axis. The value is in the range of 0 to 1000, where 0 is the lowest + drive strength and 1000 is the highest drive strength. + + Currently only set to handle one axis at a time. + + Multi-axis command example: + Output command: AA X? Y? Z? T? V? W? + Response: :A X=88 Y=91 Z=91 T=85 V=91 W=88 + Single-axis command example: + Output command: AA X? + Response: :A X=88 + + Parameters + ---------- + axis : str + Stage axis + + Returns + ------- + float + Feedback alignment value + """ + self.send_command(f"AA {axis}?\r") + response = self.read_response() + return float(response.split("=")[1]) def set_backlash(self, axis: str, val: float) -> None: """Enable/disable stage backlash correction. @@ -302,6 +336,33 @@ def set_backlash(self, axis: str, val: float) -> None: self.send_command(f"B {axis}={val:.7f}\r") self.read_response() + def get_backlash(self, axis: str) -> float: + """Get the stage backlash correction value. + + Currently only set to handle one axis at a time. + + Multi-axis command example: + Output command: B X? Y? Z? T? V? + Response: :X=0.000000 Y=0.000000 Z=0.000000 T=0.000000 V=0.000000 A + + Single-axis command example: + Output command: B X? + Response: :X=0.000000 A + + Parameters + ---------- + axis : str + Stage axis + + Returns + ------- + float + Distance of anti-backlash motion [mm] + """ + self.send_command(f"B {axis}?\r") + response = self.read_response() + return float(response.split("=")[1].split()[0]) + def set_finishing_accuracy(self, axis: str, ac: float) -> None: """Set the stage finishing accuracy. @@ -324,6 +385,32 @@ def set_finishing_accuracy(self, axis: str, ac: float) -> None: self.send_command(f"PC {axis}={ac:.7f}\r") self.read_response() + def get_finishing_accuracy(self, axis: str) -> float: + """Get the stage finishing accuracy. + + Currently only set to handle one axis at a time. + + Multi-axis command example: + Output command: PC X? Y? Z? T? V? W? + Response: :A X=88 Y=91 Z=91 T=85 V=91 W=88 + Single-axis command example: + Output command: PC X? + Response: :A X=88 + + Parameters + ---------- + axis : str + Stage axis + + Returns + ------- + float + Position error [mm] + """ + self.send_command(f"PC {axis}?\r") + response = self.read_response() + return float(response.split("=")[1]) + def set_error(self, axis: str, ac: float) -> None: """Set the stage drift error @@ -345,6 +432,34 @@ def set_error(self, axis: str, ac: float) -> None: self.send_command(f"E {axis}={ac:.7f}\r") self.read_response() + def get_error(self, axis: str) -> float: + """Get the current stage error + + Get the current Drift Error setting. Currently only set to handle + one axis at a time. + + Multi-axis command example: + Output command: E X? Y? Z? T? V? + Response: :X=0.000780 Y=0.000780 Z=0.000780 T=0.100000 V=0.000780 A + Single-axis command example: + Output command: E X? + Response: :X=0.000780 A + + + Parameters + ---------- + axis : str + Stage axis + + Returns + ------- + float + Position error [mm] + """ + self.send_command(f"E {axis}?\r") + response = self.read_response() + return float(response.split("=")[1].split()[0]) + ##### END TODO ##### def disconnect_from_serial(self) -> None: @@ -1012,7 +1127,7 @@ def logic_card_off(self, axis: str): self.send_command(f"6 CCA Z=0\r") self.read_response() - def logic_cell_on(self, axis : str): + def logic_cell_on(self, axis: str): """Turn on internal logic cell Parameters @@ -1020,12 +1135,12 @@ def logic_cell_on(self, axis : str): axis : str The axis of the internal logic cell """ - self.send_command(f'6 M E = {axis}\r') + self.send_command(f"6 M E = {axis}\r") self.read_response() - self.send_command(f'6 CCA Z=1\r') + self.send_command(f"6 CCA Z=1\r") self.read_response() - def logic_cell_off(self, axis :str): + def logic_cell_off(self, axis: str): """Turn off internal logic cell Parameters @@ -1033,13 +1148,18 @@ def logic_cell_off(self, axis :str): axis : str The axis of the internal logic cell """ - self.send_command(f'6 M E = {axis}\r') + self.send_command(f"6 M E = {axis}\r") self.read_response() - self.send_command(f'6 CCA Z=0\r') + self.send_command(f"6 CCA Z=0\r") self.read_response() def single_axis_waveform( - self, axis: str, waveform: int = 0, amplitude: int = 1000, offset: int = 500, period: int = 10 + self, + axis: str, + waveform: int = 0, + amplitude: int = 1000, + offset: int = 500, + period: int = 10, ) -> None: """Programs the analog waveforms using SAA, SAO, SAP, and SAF Default waveform is a sawtooth waveform with an amplitude of 1V, an offset of 0.5V and period of 10 ms @@ -1059,10 +1179,10 @@ def single_axis_waveform( """ print(f"Period (ms): {period}") # takes amplitude and offset from navigate and modifies them to how the TG-1000 takes them - if (waveform % 128 == 3): - offset = .5*(offset+amplitude) + if waveform % 128 == 3: + offset = 0.5 * (offset + amplitude) - amplitude = amplitude*2 + amplitude = amplitude * 2 print("***", waveform, amplitude, axis, offset, period) # TODO: 3 is the address of the GALVO DAC. May need to make this configurable. diff --git a/src/navigate/model/devices/stage/asi.py b/src/navigate/model/devices/stage/asi.py index dcbb7a367..66a7f447d 100644 --- a/src/navigate/model/devices/stage/asi.py +++ b/src/navigate/model/devices/stage/asi.py @@ -91,20 +91,104 @@ def __init__( """ super().__init__(microscope_name, device_connection, configuration, device_id) - # Default axes mapping - axes_mapping = {"x": "Z", "y": "Y", "z": "X", "f": "M"} - if not self.axes_mapping: - self.axes_mapping = { - axis: axes_mapping[axis] for axis in self.axes if axis in axes_mapping - } - #: Mapping of axes to ASI axes - else: - # Force cast axes to uppercase - self.axes_mapping = {k: v.upper() for k, v in self.axes_mapping.items()} + #: object: ASI Tiger Controller + self.asi_controller = device_connection + self.set_axes_mapping() + self.set_feedback_alignment(configuration) + self.set_finishing_accuracy(configuration) + self.set_error() + self.set_backlash() + self.set_speed(percent=0.8) - self.asi_axes = dict(map(lambda v: (v[1], v[0]), self.axes_mapping.items())) + def set_backlash(self) -> None: + """Set the backlash for the ASI Stage. - # Set feedback alignment values - Default to 85 if not specified + The backlash is set to 0.1 for rotational axes and 0.0 for translation axes. + """ + if self.asi_controller is None: + return + + for ax in self.asi_axes.keys(): + backlash = 0.1 if ax == "theta" else 0.0 + current_backlash = self.asi_controller.get_backlash(ax) + if abs(current_backlash - backlash) > 0.0001: + logger.debug(f"Axis {ax}: accuracy {accuracy} → {tolerance}") + self.asi_controller.set_backlash(ax, backlash) + + def set_finishing_accuracy(self, configuration) -> None: + """Set the finishing accuracy for the ASI Stage. + + The finishing accuracy is set to half of the minimum pixel size + used in the microscope configuration. The finishing accuracy is set + in millimeters, while the pixel size is in microns. The finishing + accuracy is set for each axis, and the error is set to 1.2 times + the finishing accuracy for translation axes, and a fixed value for + the rotational axis (theta). + + Note: + If this changes, the stage must be power cycled for these + changes to take effect. The user will be notified of this. + """ + + if self.device_connection is None: + return + + # Get the minimum pixel size for all microscopes in the configuration. + pixel_sizes = [] + for microscope in list(configuration["configuration"]["microscopes"]): + pixel_size = min( + list( + configuration["configuration"]["microscopes"][microscope_name][ + "zoom" + ]["pixel_size"].values() + ) + ) + pixel_sizes.append(pixel_size) + + pixel_size = min(pixel_sizes) + self.finishing_accuracy = 0.001 * pixel_sizes / 2 + + for ax in self.asi_axes.keys(): + tolerance = 0.003013 if ax == "theta" else self.finishing_accuracy + accuracy = self.asi_controller.get_finishing_accuracy(ax) + if abs(accuracy - self.finishing_accuracy) > 0.0001: + logger.debug(f"Axis {ax}: accuracy {accuracy} → {tolerance}") + self.asi_controller.set_finishing_accuracy(ax, tolerance) + self._alert_user() + + @staticmethod + def _alert_user(): + print( + "The finishing accuracy or error settings for the ASI stage " + "have been updated. You will need to power cycle " + "the Tiger Controller for these changes to take effect." + ) + + def set_error(self) -> None: + if self.device_connection is None: + return + + # Default finishing accuracy is 1.2 times the minimum pixel size + for ax in self.asi_axes.keys(): + tolerance = 0.1 if ax == "theta" else 1.2 * self.finishing_accuracy + error = self.asi_controller.get_error(ax) + if abs(error - tolerance) > 0.0001: + logger.debug(f"Axis {ax}: error {error} → {tolerance}") + self.asi_controller.set_error(ax, tolerance) + self._alert_user() + + def set_feedback_alignment(self) -> None: + """Set the feedback alignment for the ASI Stage. + + The default feedback alignment is 85 for each axis if not specified. + If the configuration file specifies a different feedback alignment, + that alignment is used. + + Queries the ASI controller for the current feedback alignment, and only + updates the feedback alignment if the new value differs from the current one. + """ + + # Default feedback alignment of 85 if not specified. if self.stage_feedback is None: feedback_alignment = {axis: 85 for axis in self.asi_axes} else: @@ -113,48 +197,42 @@ def __init__( for axis, self.stage_feedback in zip(self.asi_axes, self.stage_feedback) } - self.asi_controller = device_connection - if device_connection is not None: - # Set feedback alignment values - for ax, aa in feedback_alignment.items(): + if self.asi_controller is None: + return + + for ax, aa in feedback_alignment.items(): + # Get current feedback alignment values from the ASI controller + current_aa = self.asi_controller.get_feedback_alignment(ax) + if aa != current_aa: self.asi_controller.set_feedback_alignment(ax, aa) - logger.debug(f"ASI Stage Feedback Alignment Settings: {feedback_alignment}") + logger.debug(f"Axis {ax}: feedback alignment {current_aa} → {aa}") - # Set finishing accuracy to half of the minimum pixel size we will use - # pixel size is in microns, finishing accuracy is in mm - # TODO: check this over all microscopes sharing this stage, - # not just the current one - finishing_accuracy = ( - 0.001 - * min( - list( - configuration["configuration"]["microscopes"][microscope_name][ - "zoom" - ]["pixel_size"].values() - ) - ) - / 2 - ) - # If this is changing, the stage must be power cycled for these changes to - # take effect. - for ax in self.asi_axes.keys(): - if self.asi_axes[ax] == "theta": - self.asi_controller.set_finishing_accuracy(ax, 0.003013) - self.asi_controller.set_error(ax, 0.1) - else: - self.asi_controller.set_finishing_accuracy(ax, finishing_accuracy) - self.asi_controller.set_error(ax, 1.2 * finishing_accuracy) + def set_axes_mapping(self) -> None: + """Set the axes mapping for the ASI Stage. - # Set backlash to 0 (less accurate) - for ax in self.asi_axes.keys(): - if self.asi_axes[ax] == "theta": - self.asi_controller.set_backlash(ax, 0.1) - self.asi_controller.set_backlash(ax, 0.0) + The default axes mapping is (software axis -> hardware axis): + - "x" -> "Z" + - "y" -> "Y" + - "z" -> "X" + - "f" -> "M" - # Speed optimizations - Set speed to 90% of maximum on each axis - self.set_speed(percent=0.9) + If the configuration file specifies a different mapping, that mapping is used. + """ + + # Default axes mapping + axes_mapping = {"x": "Z", "y": "Y", "z": "X", "f": "M"} + if not self.axes_mapping: + #: dict: Mapping of software axes to hardware axes + self.axes_mapping = { + axis: axes_mapping[axis] for axis in self.axes if axis in axes_mapping + } + + else: + #: dict: Mapping of software axes to hardware axes + self.axes_mapping = {k: v.upper() for k, v in self.axes_mapping.items()} + self.asi_axes = dict(map(lambda v: (v[1], v[0]), self.axes_mapping.items())) - def __del__(self): + def __del__(self) -> None: """Delete the ASI Stage connection.""" try: if self.asi_controller is not None: @@ -165,7 +243,7 @@ def __del__(self): raise @classmethod - def connect(cls, port, baudrate=115200, timeout=0.25): + def connect(cls, port: str, baudrate: int = 115200, timeout: float = 0.25) -> Any: """Connect to the ASI Stage Parameters @@ -192,7 +270,7 @@ def connect(cls, port, baudrate=115200, timeout=0.25): return asi_stage - def get_axis_position(self, axis): + def get_axis_position(self, axis: st) -> float: """Get position of specific axis Parameters @@ -214,7 +292,7 @@ def get_axis_position(self, axis): return float("inf") return pos - def report_position(self): + def report_position(self) -> Dict[str, float]: """Reports the position for all axes in microns, and create position dictionary. @@ -237,7 +315,9 @@ def report_position(self): return self.get_position_dict() - def move_axis_absolute(self, axis, abs_pos, wait_until_done=False): + def move_axis_absolute( + self, axis: str, abs_pos: float, wait_until_done: bool = False + ) -> bool: """Move stage along a single axis. Move absolute command for ASI is MOVE [Axis]=[units 1/10 microns] @@ -285,7 +365,7 @@ def move_axis_absolute(self, axis, abs_pos, wait_until_done=False): self.asi_controller.wait_for_device() return True - def verify_move(self, move_dictionary): + def verify_move(self, move_dictionary: dict) -> dict: """Don't submit a move command for axes that aren't moving. The Tiger controller wait time for each axis is additive. @@ -310,7 +390,9 @@ def verify_move(self, move_dictionary): res_dict[axis] = val return res_dict - def move_absolute(self, move_dictionary, wait_until_done=False): + def move_absolute( + self, move_dictionary: dict, wait_until_done: bool = False + ) -> bool: """Move Absolute Method. XYZ Values should remain in microns for the ASI API @@ -356,7 +438,7 @@ def move_absolute(self, move_dictionary, wait_until_done=False): return True - def stop(self): + def stop(self) -> None: """Stop all stage movement abruptly.""" try: self.asi_controller.stop() @@ -364,7 +446,9 @@ def stop(self): print(f"ASI stage halt command failed: {e}") logger.exception("ASI Stage Exception", e) - def set_speed(self, velocity_dict=None, percent=None): + def set_speed( + self, velocity_dict: Optional[dict] = None, percent: float = None + ) -> bool: """Set scan velocity. Parameters @@ -396,7 +480,7 @@ def set_speed(self, velocity_dict=None, percent=None): return False return True - def get_speed(self, axis): + def get_speed(self, axis: str) -> float: """Get scan velocity of the axis. Parameters @@ -418,7 +502,13 @@ def get_speed(self, axis): return 0 return velocity - def scanr(self, start_position_mm, end_position_mm, enc_divide, axis="z"): + def scanr( + self, + start_position_mm: float, + end_position_mm: float, + enc_divide: float, + axis: str = "z", + ) -> bool: """Set scan range Parameters @@ -454,8 +544,13 @@ def scanr(self, start_position_mm, end_position_mm, enc_divide, axis="z"): return True def scanv( - self, start_position_mm, end_position_mm, number_of_lines, overshoot, axis="z" - ): + self, + start_position_mm: float, + end_position_mm: float, + number_of_lines: int, + overshoot: float, + axis: str = "z", + ) -> bool: """Set scan range Parameters @@ -491,7 +586,7 @@ def scanv( return False return True - def start_scan(self, axis): + def start_scan(self, axis: str) -> bool: """Start scan state machine Parameters @@ -516,14 +611,14 @@ def start_scan(self, axis): return False return True - def stop_scan(self): + def stop_scan(self) -> None: """Stop scan""" try: self.asi_controller.stop_scan() except ASIException as e: logger.exception("ASI Stage Exception", e) - def wait_until_complete(self, axis): + def wait_until_complete(self, axis: str) -> bool: try: while self.asi_controller.is_axis_busy(axis): time.sleep(0.1)