From 175aecb1cb4bdac3b510f75dd3c1fe7091b4b377 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Wed, 16 Jul 2025 07:32:05 -0500 Subject: [PATCH 1/6] Update Tiger Controller Stage Commands Now retrieve current settings before by default changing them. Goal is to modify settings as little as is possible. And importantly, greatly reduce the maximum speed... --- .../devices/APIs/asi/asi_tiger_controller.py | 111 ++++++++++++++++++ src/navigate/model/devices/stage/asi.py | 100 +++++++++++++--- 2 files changed, 194 insertions(+), 17 deletions(-) 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 7eedaf9ec..cece3da90 100644 --- a/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py +++ b/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py @@ -276,6 +276,36 @@ def set_feedback_alignment(self, axis: str, aa: float) -> None: self.send_command(f"AZ {axis}\r") self.read_response() + 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 +332,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 +381,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 +428,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: diff --git a/src/navigate/model/devices/stage/asi.py b/src/navigate/model/devices/stage/asi.py index 0ac9ae7e0..7971e9a01 100644 --- a/src/navigate/model/devices/stage/asi.py +++ b/src/navigate/model/devices/stage/asi.py @@ -94,17 +94,17 @@ def __init__( # 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 } - #: Mapping of axes to ASI axes + else: - # Force cast axes to uppercase + #: 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())) - # Set feedback alignment values - Default to 85 if not specified + # Set feedback alignment values. Default 85 if not specified. if self.stage_feedback is None: feedback_alignment = {axis: 85 for axis in self.asi_axes} else: @@ -115,10 +115,19 @@ def __init__( self.asi_controller = device_connection if device_connection is not None: - # Set feedback alignment values + for ax, aa in feedback_alignment.items(): - self.asi_controller.set_feedback_alignment(ax, aa) - logger.debug("ASI Stage Feedback Alignment Settings:", feedback_alignment) + # Get current feedback alignment values + current_aa = self.asi_controller.get_feedback_alignment(ax) + logger.debug(f"ASI Stage - Current Feedback Alignment for " + f"{ax} is {current_aa}") + + # Set feedback alignment values only if they differ + if aa != current_aa: + self.asi_controller.set_feedback_alignment(ax, aa) + logger.debug(f"ASI Stage - UPdated Feedback Alignment for " + f"{ax} to {aa}") + # Set finishing accuracy to half of the minimum pixel size we will use # pixel size is in microns, finishing accuracy is in mm @@ -135,24 +144,81 @@ def __init__( ) / 2 ) - # If this is changing, the stage must be power cycled for these changes to - # take effect. + + # Set finishing accuracy and error for each axis + # If this changes, the stage must be power cycled for these + # changes to take effect. Track with updated_values variable. + updated_values = False 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) + + # Get current finishing accuracy and error values + accuracy = self.asi_controller.get_finishing_accuracy(ax) + logger.debug(f"ASI Stage - Accuracy for {ax} is {accuracy}") + + # Set finishing accuracy if it differs. Rotational + # finishing accuracy differs from translation stages. + if abs(accuracy - 0.003013) > 0.0001: + logger.debug(f"ASI Stage - Setting Finishing Accuracy for " + f"{ax} to {finishing_accuracy}") + self.asi_controller.set_finishing_accuracy(ax, 0.003013) + updated_values = True + + # Get current error value + error = self.asi_controller.get_error(ax) + logger.debug(f"ASI Stage - Error for {ax} is {error}") + + # Set error if it differs + if abs(error - 0.1) > 0.0001: + logger.debug(f"ASI Stage - Setting Error for {ax} to 0.1") + self.asi_controller.set_error(ax, 0.1) + updated_values = True else: - self.asi_controller.set_finishing_accuracy(ax, finishing_accuracy) - self.asi_controller.set_error(ax, 1.2 * finishing_accuracy) + # Get current finishing accuracy and error values + accuracy = self.asi_controller.get_finishing_accuracy(ax) + logger.debug(f"ASI Stage - Accuracy for {ax} is {accuracy}") + + # Set finishing accuracy if it differs + if abs(accuracy - finishing_accuracy) > 0.0001: + logger.debug(f"ASI Stage - Setting Finishing Accuracy for " + f"{ax} to {finishing_accuracy}") + self.asi_controller.set_finishing_accuracy(ax, finishing_accuracy) + updated_values = True + + # Get current error value + error = self.asi_controller.get_error(ax) + logger.debug(f"ASI Stage - Error for {ax} is {error}") + + # Set error if it differs + if abs(error - 1.2 * finishing_accuracy) > 0.0001: + logger.debug(f"ASI Stage - Setting Error for {ax} to " + f"{1.2 * finishing_accuracy}") + self.asi_controller.set_error(ax, 1.2 * finishing_accuracy) + updated_values = True + + if updated_values: + 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.") # Set backlash to 0 (less accurate) for ax in self.asi_axes.keys(): - if self.asi_axes[ax] == "theta": + # Get the current backlash value for the axis + backlash = self.asi_controller.get_backlash(ax) + logger.debug(f"ASI Stage - Backlash for {ax} is {backlash}") + + if self.asi_axes[ax] == "theta" and abs(backlash - 0.1) > 0.0001: + logger.debug(f"ASI Stage - Setting Backlash for {ax} to 0.1") self.asi_controller.set_backlash(ax, 0.1) - self.asi_controller.set_backlash(ax, 0.0) - # Speed optimizations - Set speed to 90% of maximum on each axis - self.set_speed(percent=0.9) + elif self.asi_axes[ax] != "theta" and abs(backlash) > 0.0001: + logger.debug(f"ASI Stage - Setting Backlash for {ax} to 0.0") + self.asi_controller.set_backlash(ax, 0.0) + + # Speed optimizations - Set speed to 30% of maximum on each axis. + # Previously set to 90%, but we suspect this may be causing the + # observed jitter. + self.set_speed(percent=0.3) def __del__(self): """Delete the ASI Stage connection.""" From 88ec5b6b980bd553663007035f9b6db048f95bf5 Mon Sep 17 00:00:00 2001 From: "Kevin M. Dean" Date: Thu, 7 Aug 2025 14:38:56 -0500 Subject: [PATCH 2/6] Update asi_tiger_controller.py --- .../devices/APIs/asi/asi_tiger_controller.py | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) 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 c54c13fa3..74f6e6327 100644 --- a/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py +++ b/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py @@ -273,8 +273,17 @@ 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() + + # We only call AZ once. Recommended to check the return value. + for i in range(5): + self.send_command(f"AZ {axis}\r") + response = self.read_response() + # Acceptable values may fall between 90 and 164. + # If the value is not in this range, we will try again. + if 90 <= float(response.split("=")[1]) <= 164: + return + # If we reach here, we have tried 5 times and still not in range. + print("Zeroing stage failed to get within acceptable range after 5 attempts.") def get_feedback_alignment(self, axis: str) -> float: """Get the stage feedback alignment. @@ -1123,7 +1132,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 @@ -1131,12 +1140,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 @@ -1144,13 +1153,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 @@ -1170,10 +1184,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. From b97428465a678d92782f7ea35277e741dd2f092c Mon Sep 17 00:00:00 2001 From: "Kevin M. Dean" Date: Thu, 7 Aug 2025 14:39:34 -0500 Subject: [PATCH 3/6] Update asi.py --- src/navigate/model/devices/stage/asi.py | 61 +++++++++++++++---------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/navigate/model/devices/stage/asi.py b/src/navigate/model/devices/stage/asi.py index 7971e9a01..7524df4fd 100644 --- a/src/navigate/model/devices/stage/asi.py +++ b/src/navigate/model/devices/stage/asi.py @@ -119,15 +119,17 @@ def __init__( for ax, aa in feedback_alignment.items(): # Get current feedback alignment values current_aa = self.asi_controller.get_feedback_alignment(ax) - logger.debug(f"ASI Stage - Current Feedback Alignment for " - f"{ax} is {current_aa}") + logger.debug( + f"ASI Stage - Current Feedback Alignment for " + f"{ax} is {current_aa}" + ) # Set feedback alignment values only if they differ if aa != current_aa: self.asi_controller.set_feedback_alignment(ax, aa) - logger.debug(f"ASI Stage - UPdated Feedback Alignment for " - f"{ax} to {aa}") - + logger.debug( + f"ASI Stage - Updated Feedback Alignment for " f"{ax} to {aa}" + ) # Set finishing accuracy to half of the minimum pixel size we will use # pixel size is in microns, finishing accuracy is in mm @@ -159,8 +161,10 @@ def __init__( # Set finishing accuracy if it differs. Rotational # finishing accuracy differs from translation stages. if abs(accuracy - 0.003013) > 0.0001: - logger.debug(f"ASI Stage - Setting Finishing Accuracy for " - f"{ax} to {finishing_accuracy}") + logger.debug( + f"ASI Stage - Setting Finishing Accuracy for " + f"{ax} to {finishing_accuracy}" + ) self.asi_controller.set_finishing_accuracy(ax, 0.003013) updated_values = True @@ -180,9 +184,13 @@ def __init__( # Set finishing accuracy if it differs if abs(accuracy - finishing_accuracy) > 0.0001: - logger.debug(f"ASI Stage - Setting Finishing Accuracy for " - f"{ax} to {finishing_accuracy}") - self.asi_controller.set_finishing_accuracy(ax, finishing_accuracy) + logger.debug( + f"ASI Stage - Setting Finishing Accuracy for " + f"{ax} to {finishing_accuracy}" + ) + self.asi_controller.set_finishing_accuracy( + ax, finishing_accuracy + ) updated_values = True # Get current error value @@ -191,15 +199,19 @@ def __init__( # Set error if it differs if abs(error - 1.2 * finishing_accuracy) > 0.0001: - logger.debug(f"ASI Stage - Setting Error for {ax} to " - f"{1.2 * finishing_accuracy}") + logger.debug( + f"ASI Stage - Setting Error for {ax} to " + f"{1.2 * finishing_accuracy}" + ) self.asi_controller.set_error(ax, 1.2 * finishing_accuracy) updated_values = True if updated_values: - 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.") + 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." + ) # Set backlash to 0 (less accurate) for ax in self.asi_axes.keys(): @@ -334,9 +346,7 @@ def move_axis_absolute(self, axis, abs_pos, wait_until_done=False): # Move stage try: if axis == "theta": - self.asi_controller.move_axis( - self.axes_mapping[axis], axis_abs * 1000 - ) + self.asi_controller.move_axis(self.axes_mapping[axis], axis_abs * 1000) else: # The 10 is to account for the ASI units, 1/10 of a micron self.asi_controller.move_axis(self.axes_mapping[axis], axis_abs * 10) @@ -635,7 +645,9 @@ def __init__( device_id : int Device ID for the stage, default to 0 """ - StageBase.__init__(self, microscope_name, device_connection, configuration, device_id) + StageBase.__init__( + self, microscope_name, device_connection, configuration, device_id + ) # Default axes mapping axes_mapping = {"x": "X", "y": "Y", "z": "Z"} @@ -714,7 +726,10 @@ def connect(cls, port, baudrate=115200, timeout=0.25): asi_stage : object Successfully initialized stage object. """ - from navigate.model.devices.APIs.asi.asi_MS2000_controller import MS2000Controller + from navigate.model.devices.APIs.asi.asi_MS2000_controller import ( + MS2000Controller, + ) + # wait until ASI device is ready asi_stage = MS2000Controller(port, baudrate) asi_stage.connect_to_serial() @@ -724,7 +739,6 @@ def connect(cls, port, baudrate=115200, timeout=0.25): return asi_stage - def move_axis_relative(self, axis, distance, wait_until_done=False): """Move the stage relative to the current position along the specified axis. XYZ Values should remain in microns for the ASI API @@ -813,7 +827,8 @@ def scan_axis_triggered_move( return False return True - + + class MFC2000Stage(ASIStage): """Applied Scientific Instrumentation (ASI) Stage Class @@ -867,6 +882,7 @@ def connect(cls, port, baudrate=115200, timeout=0.25): Successfully initialized stage object. """ from navigate.model.devices.APIs.asi.asi_MFC_controller import MFCTwoThousand + # wait until ASI device is ready asi_stage = MFCTwoThousand(port, baudrate) asi_stage.connect_to_serial() @@ -875,4 +891,3 @@ def connect(cls, port, baudrate=115200, timeout=0.25): raise Exception("ASI stage connection failed.") return asi_stage - \ No newline at end of file From e4997e4dc4274de255965b4ecbf916f135d33c73 Mon Sep 17 00:00:00 2001 From: "Kevin M. Dean" Date: Thu, 7 Aug 2025 14:57:38 -0500 Subject: [PATCH 4/6] Update asi_tiger_controller.py --- .../model/devices/APIs/asi/asi_tiger_controller.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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 74f6e6327..4fddb8598 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 @@ -275,15 +278,8 @@ def set_feedback_alignment(self, axis: str, aa: float) -> None: self.read_response() # We only call AZ once. Recommended to check the return value. - for i in range(5): + for i in range(3): self.send_command(f"AZ {axis}\r") - response = self.read_response() - # Acceptable values may fall between 90 and 164. - # If the value is not in this range, we will try again. - if 90 <= float(response.split("=")[1]) <= 164: - return - # If we reach here, we have tried 5 times and still not in range. - print("Zeroing stage failed to get within acceptable range after 5 attempts.") def get_feedback_alignment(self, axis: str) -> float: """Get the stage feedback alignment. From 3308b3843131b26e9ed2aeecf8e5cb2fa5d9fba2 Mon Sep 17 00:00:00 2001 From: "Kevin M. Dean" Date: Thu, 7 Aug 2025 14:57:49 -0500 Subject: [PATCH 5/6] Update asi_tiger_controller.py --- src/navigate/model/devices/APIs/asi/asi_tiger_controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 4fddb8598..18d0855e6 100644 --- a/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py +++ b/src/navigate/model/devices/APIs/asi/asi_tiger_controller.py @@ -265,7 +265,7 @@ def set_feedback_alignment(self, axis: str, aa: float) -> None: move time. Desirable values for the autozero command are between 90 and 164. We attempt - to autozero it 3x. + to autozero it 3x. Parameters ---------- @@ -277,7 +277,6 @@ def set_feedback_alignment(self, axis: str, aa: float) -> None: self.send_command(f"AA {axis}={aa}\r") self.read_response() - # We only call AZ once. Recommended to check the return value. for i in range(3): self.send_command(f"AZ {axis}\r") From b1ade7da40477f9cb2efd7a2d466ae93decda175 Mon Sep 17 00:00:00 2001 From: "Kevin M. Dean" Date: Wed, 13 Aug 2025 07:30:48 -0500 Subject: [PATCH 6/6] Update asi.py --- src/navigate/model/devices/stage/asi.py | 297 +++++++++++++----------- 1 file changed, 157 insertions(+), 140 deletions(-) diff --git a/src/navigate/model/devices/stage/asi.py b/src/navigate/model/devices/stage/asi.py index 7524df4fd..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: - #: 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 - } + #: 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) - 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 set_backlash(self) -> None: + """Set the backlash for the ASI Stage. + + 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 - # Set feedback alignment values. Default 85 if not specified. + # 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,126 +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: - - for ax, aa in feedback_alignment.items(): - # Get current feedback alignment values - current_aa = self.asi_controller.get_feedback_alignment(ax) - logger.debug( - f"ASI Stage - Current Feedback Alignment for " - f"{ax} is {current_aa}" - ) - - # Set feedback alignment values only if they differ - if aa != current_aa: - self.asi_controller.set_feedback_alignment(ax, aa) - logger.debug( - f"ASI Stage - Updated Feedback Alignment for " f"{ax} to {aa}" - ) + if self.asi_controller is None: + return - # 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 - ) + 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"Axis {ax}: feedback alignment {current_aa} → {aa}") - # Set finishing accuracy and error for each axis - # If this changes, the stage must be power cycled for these - # changes to take effect. Track with updated_values variable. - updated_values = False - for ax in self.asi_axes.keys(): - if self.asi_axes[ax] == "theta": - - # Get current finishing accuracy and error values - accuracy = self.asi_controller.get_finishing_accuracy(ax) - logger.debug(f"ASI Stage - Accuracy for {ax} is {accuracy}") - - # Set finishing accuracy if it differs. Rotational - # finishing accuracy differs from translation stages. - if abs(accuracy - 0.003013) > 0.0001: - logger.debug( - f"ASI Stage - Setting Finishing Accuracy for " - f"{ax} to {finishing_accuracy}" - ) - self.asi_controller.set_finishing_accuracy(ax, 0.003013) - updated_values = True - - # Get current error value - error = self.asi_controller.get_error(ax) - logger.debug(f"ASI Stage - Error for {ax} is {error}") - - # Set error if it differs - if abs(error - 0.1) > 0.0001: - logger.debug(f"ASI Stage - Setting Error for {ax} to 0.1") - self.asi_controller.set_error(ax, 0.1) - updated_values = True - else: - # Get current finishing accuracy and error values - accuracy = self.asi_controller.get_finishing_accuracy(ax) - logger.debug(f"ASI Stage - Accuracy for {ax} is {accuracy}") - - # Set finishing accuracy if it differs - if abs(accuracy - finishing_accuracy) > 0.0001: - logger.debug( - f"ASI Stage - Setting Finishing Accuracy for " - f"{ax} to {finishing_accuracy}" - ) - self.asi_controller.set_finishing_accuracy( - ax, finishing_accuracy - ) - updated_values = True - - # Get current error value - error = self.asi_controller.get_error(ax) - logger.debug(f"ASI Stage - Error for {ax} is {error}") - - # Set error if it differs - if abs(error - 1.2 * finishing_accuracy) > 0.0001: - logger.debug( - f"ASI Stage - Setting Error for {ax} to " - f"{1.2 * finishing_accuracy}" - ) - self.asi_controller.set_error(ax, 1.2 * finishing_accuracy) - updated_values = True - - if updated_values: - 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_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(): - # Get the current backlash value for the axis - backlash = self.asi_controller.get_backlash(ax) - logger.debug(f"ASI Stage - Backlash for {ax} is {backlash}") + The default axes mapping is (software axis -> hardware axis): + - "x" -> "Z" + - "y" -> "Y" + - "z" -> "X" + - "f" -> "M" - if self.asi_axes[ax] == "theta" and abs(backlash - 0.1) > 0.0001: - logger.debug(f"ASI Stage - Setting Backlash for {ax} to 0.1") - self.asi_controller.set_backlash(ax, 0.1) + If the configuration file specifies a different mapping, that mapping is used. + """ - elif self.asi_axes[ax] != "theta" and abs(backlash) > 0.0001: - logger.debug(f"ASI Stage - Setting Backlash for {ax} to 0.0") - self.asi_controller.set_backlash(ax, 0.0) + # 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 + } - # Speed optimizations - Set speed to 30% of maximum on each axis. - # Previously set to 90%, but we suspect this may be causing the - # observed jitter. - self.set_speed(percent=0.3) + 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: @@ -243,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 @@ -270,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 @@ -292,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. @@ -315,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] @@ -363,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. @@ -388,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 @@ -434,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() @@ -442,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 @@ -474,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 @@ -496,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 @@ -532,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 @@ -569,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 @@ -594,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)