From 62e86e62adaae9b85efa40bb3b72d6c67f0d47a1 Mon Sep 17 00:00:00 2001 From: qi2lab Date: Wed, 23 Oct 2024 13:03:26 -0700 Subject: [PATCH 1/6] Added Thorlabs Kenisis communication driver using pylablib package to communicate via serial to the FTDI chip in Linux. Minor change to loading Photometrics camera. --- .../model/device_startup_functions.py | 28 +- .../devices/APIs/asi/asi_MS2000_controller.py | 9 +- .../devices/APIs/thorlabs/kenisis_motor.py | 139 ++++++++++ .../model/devices/camera/photometrics.py | 4 +- .../devices/stages/tl_kinesis_steppermotor.py | 245 ++++++++++++++++++ 5 files changed, 410 insertions(+), 15 deletions(-) create mode 100644 src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py create mode 100644 src/navigate/model/devices/stages/tl_kinesis_steppermotor.py diff --git a/src/navigate/model/device_startup_functions.py b/src/navigate/model/device_startup_functions.py index ec7c2eaca..d4f5fb3e1 100644 --- a/src/navigate/model/device_startup_functions.py +++ b/src/navigate/model/device_startup_functions.py @@ -511,8 +511,18 @@ def load_stages( exception=TLFTDICommunicationError, ) ) - - elif stage_type == "KST101": + elif stage_type == "KINESIS" and platform.system() == "Linux": + from navigate.model.devices.stages.tl_kinesis_steppermotor import ( + build_KSTStage_connection + ) + stage_devices.append( + auto_redial( + build_KSTStage_connection, + (stage_config["serial_number"],), + exception=Exception + ) + ) + elif stage_type == "KST101" and platform.system=="Windows": from navigate.model.devices.stages.tl_kcube_steppermotor import ( build_TLKSTStage_connection, ) @@ -562,12 +572,8 @@ def load_stages( ) ) - elif stage_type == "MS2000" and platform.system() == "Windows": - """Filter wheel can be controlled from the same Controller. If - so, then we will load this as a shared device. If not, we will create the - connection to the Controller. - - TODO: Evaluate whether MS2000 should be able to operate as a shared device. + elif stage_type == "MS2000": + """Stage and filter wheel are independent and should not be a shared device """ from navigate.model.devices.stages.asi_MSTwoThousand import ( @@ -705,7 +711,10 @@ def start_stage( from navigate.model.devices.stages.tl_kcube_inertial import TLKIMStage return TLKIMStage(microscope_name, device_connection, configuration, id) - + elif device_type == "KINESIS": + from navigate.model.devices.stages.tl_kinesis_steppermotor import TLKINStage + + return TLKINStage(microscope_name, device_connection, configuration, id) elif device_type == "KST101": from navigate.model.devices.stages.tl_kcube_steppermotor import TLKSTStage @@ -1265,7 +1274,6 @@ def start_lasers( modulation = "analog" elif digital == "NI": modulation = "digital" - return LaserNI( microscope_name=microscope_name, device_connection=device_connection, diff --git a/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py b/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py index d7524766f..e23f20f32 100644 --- a/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py +++ b/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py @@ -34,6 +34,7 @@ import threading import time import logging +import platform # Third Party Imports from serial import Serial @@ -191,8 +192,9 @@ def connect_to_serial( self.serial_port.write_timeout = write_timeout self.serial_port.timeout = read_timeout - # set the size of the rx and tx buffers before calling open - self.serial_port.set_buffer_size(rx_size, tx_size) + if platform.system()=="Windows": + # Only changed the buffer size in windows + self.serial_port.set_buffer_size(rx_size, tx_size) try: self.serial_port.open() except SerialException: @@ -217,7 +219,8 @@ def connect_to_serial( "Y", "Z", ] # self.get_default_motor_axis_sequence() - + else: + print("Didnt open") def get_default_motor_axis_sequence(self) -> None: """Get the default motor axis sequence from the ASI device diff --git a/src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py b/src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py new file mode 100644 index 000000000..bd920ad89 --- /dev/null +++ b/src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py @@ -0,0 +1,139 @@ +""" +API for connection to Thorlabs.MotionControl.KCube.StepperMotor.dll. +See Thorlabs.MotionControl.KCube.StepperMotor.h for more functions to implement. +""" + +""" +2024/10/23 Sheppard: Initialized to control Kinesis Stepper motor in Linux +""" +from pylablib.devices import Thorlabs + +def in_enum(value, enum): + values = set(item.value for item in enum) + return value in values + + +def errcheck(result, func, args): + """ + Wraps the call to DLL functions. + + Parameters + ---------- + result : ctypes.c_short + Error code or 0 if successful. + func : function + DLL function + args : tuple + Arguments passed to the DLL function, defined in argtypes + + Returns + ------- + result : int + Error code or 0 if successful. + """ + + return 0 + + +def KST_Open(connection): + """ + Open the device for communications. + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + + Returns + ------- + int + The error code or 0 if successful. + """ + try: + stage = Thorlabs.KinesisMotor(("serial", connection), scale="step") + success = True + except Exception as e: + success = False + raise ConnectionError(f"KST101 stage connection failed! \nError: {e}") + if success: + return stage + +def KST_Close(stage): + """ + Disconnect and close the device. + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + + Returns + ------- + None + """ + stage.stop() + stage.close() + +def KST_MoveToPosition(stage, position, wait_till_done, steps_per_um): + """Move to position (um) + """ + stage.get_position(channel=1, scale=False) + cur_pos = stage.get_position(channel=1, scale=False) + position_um = cur_pos / steps_per_um + # calculate the distance needed to move + distance = position - position_um + # convert total distance to steps + steps = steps_per_um * distance + stage.move_by(steps, channel=1, scale=False) + if wait_till_done: + stage.wait_move(channel=1) + return 0 + +def KST_GetCurrentPosition(stage, steps_per_um): + """Get the current position + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + + Returns + ------- + int + Current position. + """ + stage.get_position(channel=1, scale=False) + position = stage.get_position(channel=1, scale="False") + position_um = position / steps_per_um + return position_um + +def KST_MoveStop(stage): + """ + Halt motion + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + channel : int + The device channel. One of SCC_Channels. + + Returns + ------- + int + The error code or 0 if successful. + """ + stage.stage.stop() + return 0 + +def KST_HomeDevice(stage): + """Home Device + """ + stage.stage.home() + return 0 + +def KST_SetVelocityParams(stage, min_velocity, max_velocity, acceleration): + """Set velocity profile required for move + """ + stage.set_move_params(min_velocity, max_velocity, acceleration) + return 0 \ No newline at end of file diff --git a/src/navigate/model/devices/camera/photometrics.py b/src/navigate/model/devices/camera/photometrics.py index d46fa83ca..ad2e1fee1 100644 --- a/src/navigate/model/devices/camera/photometrics.py +++ b/src/navigate/model/devices/camera/photometrics.py @@ -66,8 +66,8 @@ def build_photometrics_connection(camera_connection): """ try: pvc.init_pvcam() - # camera_names = Camera.get_available_camera_names() - camera_to_open = Camera.select_camera(camera_connection) + camera_names = Camera.get_available_camera_names() + camera_to_open = Camera.select_camera(camera_names[0]) camera_to_open.open() return camera_to_open except Exception as e: diff --git a/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py b/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py new file mode 100644 index 000000000..6bf6704aa --- /dev/null +++ b/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py @@ -0,0 +1,245 @@ +# Copyright (c) 2021-2024 The University of Texas Southwestern Medical Center. +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only (subject to the +# limitations in the disclaimer below) provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +""" + +Builds from + stage: + hardware: + - + name: stage + type: KINESIS + serial_number: "/dev/ttyUSB1" + axes: [f] + axes_mapping: [1] + steps_per_um: 2008.623 + axes_channels: autofocus + max: 0 + min: 25 + +""" +# Standard Library imports +import importlib +import logging +import time +from multiprocessing.managers import ListProxy +from numpy import round + +# Third Party Library imports + +# Local Imports +from navigate.model.devices.stages.base import StageBase +from navigate.tools.decorators import log_initialization + +# Logger Setup +p = __name__.split(".")[1] +logger = logging.getLogger(p) + + +def build_KSTStage_connection(serial_number): + """Connect to the Thorlabs KST Stage + + Parameters + ---------- + serialnum : str + Serial number of the stage. + + Returns + ------- + kst_controller + Thorlabs KST Stage controller + """ + kst_controller = importlib.import_module( + "navigate.model.devices.APIs.thorlabs.kenisis_motor" + ) + connection = {"port":serial_number,"baudrate":115200,"rtscts":True} + stage = kst_controller.KST_Open(connection) + return kst_controller, stage + + +@log_initialization +class TLKINStage(StageBase): + """Thorlabs KST Stage""" + + def __init__(self, microscope_name, device_connection, configuration, device_id=0): + """Initialize the stage. + + Parameters + ---------- + microscope_name : str + Name of the microscope. + device_connection : str + Connection string for the device. + configuration : dict + Configuration dictionary for the device. + device_id : int + Device ID for the device. + """ + super().__init__(microscope_name, device_connection, configuration, device_id) + + #: dict: Mapping of axes to KST axes. Only support one axis. + axes_mapping = {"x": 1, "y": 1, "z": 1, "f": 1} + + if not self.axes_mapping: + if self.axes[0] not in axes_mapping: + raise KeyError(f"KTS101 doesn't support axis: {self.axes[0]}") + self.axes_mapping = {self.axes[0]: axes_mapping[self.axes[0]]} + + #: list: List of KST axes available. + self.KST_axes = list(self.axes_mapping.values()) + + device_config = configuration["configuration"]["microscopes"][microscope_name][ + "stage" + ]["hardware"] + if type(device_config) == ListProxy: + #: str: Serial number of the stage. + self.serial_number = str(device_config[device_id]["serial_number"]) + + #: float: Device units per mm. + self.device_unit_scale = device_config[device_id]["steps_per_um"] + else: + self.serial_number = device_config["serial_number"] + self.device_unit_scale = device_config["steps_per_um"] + + if device_connection is not None: + #: object: Thorlabs KST Stage controller + self.kst_controller, self.stage = device_connection + else: + self.kst_controller, self.stage = build_KSTStage_connection(self.serial_number) + + def __del__(self): + """Delete the KST Connection""" + try: + self.kst_controller.stop() + self.kst_controller.stage.close(self.serial_number) + except AttributeError: + pass + + def report_position(self): + """ + Report the position of the stage. + + Reports the position of the stage for all axes, and creates the hardware + position dictionary. + + Returns + ------- + position_dict : dict + Dictionary containing the current position of the stage. + """ + try: + pos = self.kst_controller.KST_GetCurrentPosition( + self.serial_number + ) / float(self.device_unit_scale) + setattr(self, f"{self.axes[0]}_pos", pos) + except Exception: + pass + + return self.get_position_dict() + + def move_axis_absolute(self, axes, abs_pos, wait_until_done=False): + """ + Implement movement. + + Parameters + ---------- + axes : str + An axis. For example, 'x', 'y', 'z', 'f', 'theta'. + abs_pos : float + Absolute position value + wait_until_done : bool + Block until stage has moved to its new spot. + + Returns + ------- + bool + Was the move successful? + """ + axis_abs = self.get_abs_position(axes, abs_pos) + if axis_abs == -1e50: + return False + self.kst_controller.KST_MoveToPosition(self.stage, axis_abs, wait_until_done, self.device_unit_scale) + return True + + def move_absolute(self, move_dictionary, wait_until_done=False): + """Move stage along a single axis. + + Parameters + ---------- + move_dictionary : dict + A dictionary of values required for movement. Includes 'x_abs', etc. for + one or more axes. Expects values in micrometers, except for theta, which is + in degrees. + wait_until_done : bool + Block until stage has moved to its new spot. + + Returns + ------- + success : bool + Was the move successful? + """ + + result = True + result = ( + self.move_axis_absolute("f", move_dictionary["f_abs"], wait_until_done), + result, + ) + + return result + + def move_to_position(self, position, wait_until_done=False): + """Perform a move to position + + Parameters + ---------- + position : float + Stage position in mm. + wait_until_done : bool + Block until stage has moved to its new spot. + + Returns + ------- + success : bool + Was the move successful? + """ + self.kst_controller.KST_MoveToPosition( + self.stage, position, wait_until_done, self.device_unit_scale + ) + + def run_homing(self): + """Run homing sequence.""" + self.kst_controller.KST_HomeDevice(self.serial_number) + self.move_to_position(12.5, wait_until_done=True) + + def stop(self): + """ + Stop all stage channels move + """ + self.kst_controller.KST_MoveStop(self) From 6d8ef3d0152daa8f6a646c726928896f703700f6 Mon Sep 17 00:00:00 2001 From: qi2lab Date: Thu, 24 Oct 2024 12:07:51 -0700 Subject: [PATCH 2/6] Fixed Kinesis stage driver error for setting the position dict. WIP on a bug where stopping or starting an acquisition moves the F stage. --- .../model/device_startup_functions.py | 4 +- .../devices/APIs/asi/asi_MS2000_controller.py | 8 +- .../devices/APIs/thorlabs/kenisis_motor.py | 139 ---------------- .../APIs/thorlabs/pykinesis_controller.py | 149 ++++++++++++++++++ .../devices/stages/tl_kinesis_steppermotor.py | 46 +++--- 5 files changed, 178 insertions(+), 168 deletions(-) delete mode 100644 src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py create mode 100644 src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py diff --git a/src/navigate/model/device_startup_functions.py b/src/navigate/model/device_startup_functions.py index d4f5fb3e1..a05c37b60 100644 --- a/src/navigate/model/device_startup_functions.py +++ b/src/navigate/model/device_startup_functions.py @@ -513,11 +513,11 @@ def load_stages( ) elif stage_type == "KINESIS" and platform.system() == "Linux": from navigate.model.devices.stages.tl_kinesis_steppermotor import ( - build_KSTStage_connection + build_KINESIS_Stage_connection ) stage_devices.append( auto_redial( - build_KSTStage_connection, + build_KINESIS_Stage_connection, (stage_config["serial_number"],), exception=Exception ) diff --git a/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py b/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py index e23f20f32..d4eb4ff3c 100644 --- a/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py +++ b/src/navigate/model/devices/APIs/asi/asi_MS2000_controller.py @@ -80,6 +80,7 @@ def __init__(self, code: str): ":N-6": "Undefined Error (command is incorrect, but the controller does " "not know exactly why.", ":N-21": "Serial Command halted by the HALT command", + ":N-21\r\n": "Serial Command halted by the HALT command", } #: str: Error code received from MS2000 Console self.code = code @@ -218,9 +219,8 @@ def connect_to_serial( "X", "Y", "Z", - ] # self.get_default_motor_axis_sequence() - else: - print("Didnt open") + ] + def get_default_motor_axis_sequence(self) -> None: """Get the default motor axis sequence from the ASI device @@ -377,7 +377,7 @@ def read_response(self) -> str: self.report_to_console(f"Received Response: {response.strip()}") if response.startswith(":N"): logger.error(f"Incorrect response received: {response}") - raise MS2000Exception(response) + raise MS2000Exception(response.strip()) return response # in case we want to read the response diff --git a/src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py b/src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py deleted file mode 100644 index bd920ad89..000000000 --- a/src/navigate/model/devices/APIs/thorlabs/kenisis_motor.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -API for connection to Thorlabs.MotionControl.KCube.StepperMotor.dll. -See Thorlabs.MotionControl.KCube.StepperMotor.h for more functions to implement. -""" - -""" -2024/10/23 Sheppard: Initialized to control Kinesis Stepper motor in Linux -""" -from pylablib.devices import Thorlabs - -def in_enum(value, enum): - values = set(item.value for item in enum) - return value in values - - -def errcheck(result, func, args): - """ - Wraps the call to DLL functions. - - Parameters - ---------- - result : ctypes.c_short - Error code or 0 if successful. - func : function - DLL function - args : tuple - Arguments passed to the DLL function, defined in argtypes - - Returns - ------- - result : int - Error code or 0 if successful. - """ - - return 0 - - -def KST_Open(connection): - """ - Open the device for communications. - - Parmeters - --------- - serial_number : str - Serial number of Thorlabs Kinesis Stepper Motor (KST) device. - - Returns - ------- - int - The error code or 0 if successful. - """ - try: - stage = Thorlabs.KinesisMotor(("serial", connection), scale="step") - success = True - except Exception as e: - success = False - raise ConnectionError(f"KST101 stage connection failed! \nError: {e}") - if success: - return stage - -def KST_Close(stage): - """ - Disconnect and close the device. - - Parmeters - --------- - serial_number : str - Serial number of Thorlabs Kinesis Stepper Motor (KST) device. - - Returns - ------- - None - """ - stage.stop() - stage.close() - -def KST_MoveToPosition(stage, position, wait_till_done, steps_per_um): - """Move to position (um) - """ - stage.get_position(channel=1, scale=False) - cur_pos = stage.get_position(channel=1, scale=False) - position_um = cur_pos / steps_per_um - # calculate the distance needed to move - distance = position - position_um - # convert total distance to steps - steps = steps_per_um * distance - stage.move_by(steps, channel=1, scale=False) - if wait_till_done: - stage.wait_move(channel=1) - return 0 - -def KST_GetCurrentPosition(stage, steps_per_um): - """Get the current position - - Parmeters - --------- - serial_number : str - Serial number of Thorlabs Kinesis Stepper Motor (KST) device. - - Returns - ------- - int - Current position. - """ - stage.get_position(channel=1, scale=False) - position = stage.get_position(channel=1, scale="False") - position_um = position / steps_per_um - return position_um - -def KST_MoveStop(stage): - """ - Halt motion - - Parmeters - --------- - serial_number : str - Serial number of Thorlabs Kinesis Stepper Motor (KST) device. - channel : int - The device channel. One of SCC_Channels. - - Returns - ------- - int - The error code or 0 if successful. - """ - stage.stage.stop() - return 0 - -def KST_HomeDevice(stage): - """Home Device - """ - stage.stage.home() - return 0 - -def KST_SetVelocityParams(stage, min_velocity, max_velocity, acceleration): - """Set velocity profile required for move - """ - stage.set_move_params(min_velocity, max_velocity, acceleration) - return 0 \ No newline at end of file diff --git a/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py b/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py new file mode 100644 index 000000000..52cc020bd --- /dev/null +++ b/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py @@ -0,0 +1,149 @@ +""" +API for connection to Thorlabs.MotionControl.KCube.StepperMotor.dll. +See Thorlabs.MotionControl.KCube.StepperMotor.h for more functions to implement. +""" + +""" +2024/10/23 Sheppard: Initialized to control Kinesis Stepper motor in Linux +""" +from pylablib.devices import Thorlabs +import logging +# Local Imports + +# Logger Setup +p = __name__.split(".")[1] +logger = logging.getLogger(p) + +class KinesisStage(): + def __init__(self, dev_path: str, verbose: bool): + """_summary_ + + Args: + connection (_type_): _description_ + """ + connection = {"port":dev_path,"baudrate":115200,"rtscts":True} + self.verbose = verbose + self.dev_path = dev_path + self.defualt_axes = ["f"] + + self.move_params = {"min_velocity":None, + "max_velocity":None, + "acceleration":None} + + self.open(connection) + + + def __str__(self) -> str: + """Returns the string representation of the MS2000 Controller class""" + return "KinesisController" + + def open(self, connection): + """ + Open the device for communications. + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + + Returns + ------- + int + The error code or 0 if successful. + """ + try: + self.stage = Thorlabs.KinesisMotor(("serial", connection), scale="step") + success = True + except Exception as e: + success = False + raise ConnectionError(f"KST101 stage connection failed! \nError: {e}") + + def close(self): + """ + Disconnect and close the device. + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + + Returns + ------- + None + """ + self.stage.stop() + self.stage.close() + + def move_to_position(self, position, steps_per_um, wait_till_done): + """Move to position (um) + """ + self.stage.get_position(channel=1, scale=False) + cur_pos = self.stage.get_position(channel=1, scale=False) + position_um = cur_pos / steps_per_um + # calculate the distance needed to move + distance = position - position_um + # convert total distance to steps + steps = steps_per_um * distance + self.stage.move_by(steps, channel=1, scale=False) + if wait_till_done: + self.stage.wait_move(channel=1) + return 0 + + def get_current_position(self, steps_per_um): + """Get the current position + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + + Returns + ------- + int + Current position. + """ + self.stage.get_position(channel=1, scale=False) + position = self.stage.get_position(channel=1, scale="False") + position_um = position / steps_per_um + return round(position_um, 2) + + def stop(self): + """ + Halt motion + + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Stepper Motor (KST) device. + channel : int + The device channel. One of SCC_Channels. + + Returns + ------- + int + The error code or 0 if successful. + """ + self.stage.stop() + return 0 + + def home_stage(self): + """Home Device + """ + self.stage.home() + return 0 + + def set_velocity_params(self, + min_velocity, + max_velocity, + acceleration, + steps_per_um): + """Set velocity profile required for move + """ + min_velocity *= steps_per_um + max_velocity *= steps_per_um + acceleration *= steps_per_um + self.stage.set_move_params(min_velocity, max_velocity, acceleration) + self.move_params = {"min_velocity":min_velocity, + "max_velocity":max_velocity, + "acceleration":acceleration} + return 0 \ No newline at end of file diff --git a/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py b/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py index 6bf6704aa..339edb3e9 100644 --- a/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py +++ b/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py @@ -57,13 +57,13 @@ # Local Imports from navigate.model.devices.stages.base import StageBase from navigate.tools.decorators import log_initialization - +from navigate.model.devices.APIs.thorlabs.pykinesis_controller import KinesisStage # Logger Setup p = __name__.split(".")[1] logger = logging.getLogger(p) -def build_KSTStage_connection(serial_number): +def build_KINESIS_Stage_connection(serial_number): """Connect to the Thorlabs KST Stage Parameters @@ -73,15 +73,14 @@ def build_KSTStage_connection(serial_number): Returns ------- - kst_controller + kin_controller Thorlabs KST Stage controller """ - kst_controller = importlib.import_module( - "navigate.model.devices.APIs.thorlabs.kenisis_motor" - ) - connection = {"port":serial_number,"baudrate":115200,"rtscts":True} - stage = kst_controller.KST_Open(connection) - return kst_controller, stage + kstage = KinesisStage(serial_number, False) + if not kstage.stage.is_opened(): + logger.error("KinesisStage connection failed.") + raise Exception("Kinesis stage connection failed.") + return kstage @log_initialization @@ -130,15 +129,15 @@ def __init__(self, microscope_name, device_connection, configuration, device_id= if device_connection is not None: #: object: Thorlabs KST Stage controller - self.kst_controller, self.stage = device_connection + self.kin_controller = device_connection else: - self.kst_controller, self.stage = build_KSTStage_connection(self.serial_number) + self.kin_controller = build_KINESIS_Stage_connection(self.serial_number) def __del__(self): """Delete the KST Connection""" try: - self.kst_controller.stop() - self.kst_controller.stage.close(self.serial_number) + self.kin_controller.stop() + self.kin_controller.close() except AttributeError: pass @@ -155,9 +154,7 @@ def report_position(self): Dictionary containing the current position of the stage. """ try: - pos = self.kst_controller.KST_GetCurrentPosition( - self.serial_number - ) / float(self.device_unit_scale) + pos = self.kin_controller.get_current_position(self.device_unit_scale) setattr(self, f"{self.axes[0]}_pos", pos) except Exception: pass @@ -185,7 +182,9 @@ def move_axis_absolute(self, axes, abs_pos, wait_until_done=False): axis_abs = self.get_abs_position(axes, abs_pos) if axis_abs == -1e50: return False - self.kst_controller.KST_MoveToPosition(self.stage, axis_abs, wait_until_done, self.device_unit_scale) + self.kin_controller.move_to_position(axis_abs, + self.device_unit_scale, + wait_until_done) return True def move_absolute(self, move_dictionary, wait_until_done=False): @@ -229,17 +228,18 @@ def move_to_position(self, position, wait_until_done=False): success : bool Was the move successful? """ - self.kst_controller.KST_MoveToPosition( - self.stage, position, wait_until_done, self.device_unit_scale - ) - + self.kin_controller.move_to_position(position, + self.device_unit_scale, + wait_until_done) + def run_homing(self): """Run homing sequence.""" - self.kst_controller.KST_HomeDevice(self.serial_number) + self.kin_controller.home_stage() + # move to mid travel self.move_to_position(12.5, wait_until_done=True) def stop(self): """ Stop all stage channels move """ - self.kst_controller.KST_MoveStop(self) + self.kin_controller.stop() From 81f27a85a4729a831a213933622dc97979cce015 Mon Sep 17 00:00:00 2001 From: qi2lab Date: Mon, 18 Nov 2024 11:53:54 -0700 Subject: [PATCH 3/6] Changes to accomodate opposite moving Z and F stages, unknown error in DAQ, changes should be kept local, however a solution for stage directions should be made. --- .../controller/sub_controllers/tiling.py | 22 +++++++------------ src/navigate/model/devices/daq/ni.py | 14 +++++++----- .../model/features/common_features.py | 4 ++-- src/navigate/model/microscope.py | 2 ++ .../view/custom_widgets/validation.py | 2 +- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/navigate/controller/sub_controllers/tiling.py b/src/navigate/controller/sub_controllers/tiling.py index 76b6600a7..8880874df 100644 --- a/src/navigate/controller/sub_controllers/tiling.py +++ b/src/navigate/controller/sub_controllers/tiling.py @@ -266,26 +266,19 @@ def set_table(self): y_stop = float(self.variables["y_end"].get()) y_tiles = int(self.variables["y_tiles"].get()) + # NOTE: Removed shifting by the origin becuase, it was not clear how to set the origin. # shift z by coordinate origin of local z-stack - z_start = float(self.variables["z_start"].get()) - float( - self.stack_acq_widgets["start_position"].get() - ) - z_stop = float(self.variables["z_end"].get()) - float( - self.stack_acq_widgets["end_position"].get() - ) + z_start = float(self.variables["z_start"].get()) # - float(self.stack_acq_widgets["start_position"].get()) + z_stop = float(self.variables["z_end"].get()) # - float(self.stack_acq_widgets["end_position"].get()) z_tiles = int(self.variables["z_tiles"].get()) - + # Default to fixed theta r_start = float(self.stage_position_vars["theta"].get()) r_stop = float(self.stage_position_vars["theta"].get()) r_tiles = 1 - f_start = float(self.variables["f_start"].get()) - float( - self.stack_acq_widgets["start_focus"].get() - ) - f_stop = float(self.variables["f_end"].get()) - float( - self.stack_acq_widgets["end_focus"].get() - ) + f_start = float(self.variables["f_start"].get()) #- float(self.stack_acq_widgets["start_focus"].get()) + f_stop = float(self.variables["f_end"].get()) #- float(self.stack_acq_widgets["end_focus"].get()) f_tiles = int(self.variables["f_tiles"].get()) # for consistency, always go from low to high @@ -308,11 +301,12 @@ def sort_vars(a, b): return b, a return a, b + #NOTE: Sorting variables breaks down if the F and Z stage are not both moving in the same direction. On our microscope, the F stage moves in a negative direction for positive z-stack acquisitions. I also have a hard coded (-) in commmon_features.py to allow the focus stage to move in a negative direction. x_start, x_stop = sort_vars(x_start, x_stop) y_start, y_stop = sort_vars(y_start, y_stop) z_start, z_stop = sort_vars(z_start, z_stop) r_start, r_stop = sort_vars(r_start, r_stop) - f_start, f_stop = sort_vars(f_start, f_stop) + # f_start, f_stop = sort_vars(f_start, f_stop) overlap = float(self._percent_overlap) / 100 table_values = compute_tiles_from_bounding_box( diff --git a/src/navigate/model/devices/daq/ni.py b/src/navigate/model/devices/daq/ni.py index 9034d7836..e4956b919 100644 --- a/src/navigate/model/devices/daq/ni.py +++ b/src/navigate/model/devices/daq/ni.py @@ -157,12 +157,14 @@ def set_external_trigger(self, external_trigger=None) -> None: self.analog_output_tasks[ board_name ].triggers.start_trigger.cfg_dig_edge_start_trig(trigger_source) - try: - self.analog_output_tasks[board_name].register_done_event(None) - except Exception: - logger.debug( - f"Error Registering Done Event: {traceback.format_exc()}" - ) + # NOTE: this was causing an error for me using PCIe-6343 in Linux. Not sure if it was board or OS related. + # try: + # # print(board_name) + # # self.analog_output_tasks[board_name].register_done_event(None) + # except Exception: + # logger.debug( + # f"Error Registering Done Event: {traceback.format_exc()}" + # ) else: # close master trigger task if self.master_trigger_task: diff --git a/src/navigate/model/features/common_features.py b/src/navigate/model/features/common_features.py index d8aac6169..c574b6e50 100644 --- a/src/navigate/model/features/common_features.py +++ b/src/navigate/model/features/common_features.py @@ -1026,10 +1026,10 @@ def pre_signal_func(self): self.z_stack_distance = abs( self.start_z_position - float(microscope_state["end_position"]) ) - + # NOTE: added a quick fix for the focus stage moving in a negative direction during z-stack acquisitions. Somehow a negative step needs to be allowed. Flipping the axes direction did not have the desired outcome. self.start_focus = float(microscope_state["start_focus"]) end_focus = float(microscope_state["end_focus"]) - self.focus_step_size = (end_focus - self.start_focus) / self.number_z_steps + self.focus_step_size = -(end_focus - self.start_focus) / self.number_z_steps #: float: The focus stack distance for the z-stack. self.f_stack_distance = abs(end_focus - self.start_focus) diff --git a/src/navigate/model/microscope.py b/src/navigate/model/microscope.py index 7d96d18bf..c4df743b1 100644 --- a/src/navigate/model/microscope.py +++ b/src/navigate/model/microscope.py @@ -488,6 +488,7 @@ def end_acquisition(self): self.daq.stop_acquisition() self.stop_stage() if self.central_focus is not None: + print(self.central_focus) self.move_stage({"f_abs": self.central_focus}) if self.camera.is_acquiring: self.camera.close_image_series() @@ -766,6 +767,7 @@ def prepare_next_channel(self, update_daq_task_flag=True): if self.central_focus is None: self.central_focus = self.get_stage_position().get("f_pos") if self.central_focus is not None: + #TODO: This causes the F-stage to move the defocus distance every time the "stop" button is selected. self.move_stage( {"f_abs": self.central_focus + float(channel["defocus"])}, wait_until_done=True, diff --git a/src/navigate/view/custom_widgets/validation.py b/src/navigate/view/custom_widgets/validation.py index 816c22aa0..a9745b52a 100644 --- a/src/navigate/view/custom_widgets/validation.py +++ b/src/navigate/view/custom_widgets/validation.py @@ -895,7 +895,7 @@ class ValidatedSpinbox(ValidatedMixin, ttk.Spinbox): ignore key if proposed value requires more precision than increment, ignore key On focus out, make sure number is a valid number string and greater than from value If given a min_var, max_var, or focus_update_var, then the spinbox range will - update dynamically when those valuse are changed (can be used to link to other + update dynamically when those values are changed (can be used to link to other widgets) """ From 1b6cefa1debde5effd58415655f62b9ebc6464db Mon Sep 17 00:00:00 2001 From: qi2lab Date: Fri, 13 Dec 2024 14:41:05 -0700 Subject: [PATCH 4/6] removed debugging print statement --- src/navigate/model/microscope.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/navigate/model/microscope.py b/src/navigate/model/microscope.py index f9a7286aa..45d64d4d8 100644 --- a/src/navigate/model/microscope.py +++ b/src/navigate/model/microscope.py @@ -542,7 +542,6 @@ def end_acquisition(self) -> None: self.daq.stop_acquisition() self.stop_stage() if self.central_focus is not None: - print(self.central_focus) self.move_stage({"f_abs": self.central_focus}) if self.camera.is_acquiring: self.camera.close_image_series() From 6cf04d5b536717b556f981e936d725a5937304da Mon Sep 17 00:00:00 2001 From: qi2lab Date: Wed, 9 Jul 2025 21:04:42 -0700 Subject: [PATCH 5/6] current state --- src/navigate/config/configuration.yaml | 521 ++++++------------ src/navigate/config/experiment.yml | 14 +- src/navigate/config/gui_configuration.yml | 12 +- .../model/device_startup_functions.py | 3 - 4 files changed, 184 insertions(+), 366 deletions(-) diff --git a/src/navigate/config/configuration.yaml b/src/navigate/config/configuration.yaml index 483243825..bd095e3e1 100644 --- a/src/navigate/config/configuration.yaml +++ b/src/navigate/config/configuration.yaml @@ -1,409 +1,230 @@ -# Only one microscope can be active in the GUI at a time, but all microscopes will be accessible microscopes: Mesoscale: daq: hardware: type: NI - - # NI PCIe-1073 Chassis with PXI-6259 and PXI-6733 DAQ Boards. - # Sampling rate in Hz sample_rate: 100000 - - # triggers - master_trigger_out_line: PXI6259/port0/line1 - camera_trigger_out_line: /PXI6259/ctr0 - trigger_source: /PXI6259/PFI0 - - # Digital Laser Outputs - laser_port_switcher: PXI6733/port0/line0 + master_trigger_out_line: Dev1/port0/line10 + camera_trigger_out_line: /Dev1/ctr0 + trigger_source: /Dev1/PFI3 + laser_port_switcher: Dev1/port0/line11 laser_switch_state: False + + zoom: + hardware: + type: synthetic + servo_id: 1 + port: None + baudrate: + position: + N/A: 0.0 + pixel_size: + N/A: 2.125 + + shutter: + hardware: + type: synthetic + channel: Dev1/port0/line16 + min: 0.0 + max: 5.0 camera: hardware: - type: HamamatsuOrca - serial_number: 302158 - camera_connection: PMPCIECam00 #Photometrics only - defect_correct_mode: 2.0 - delay: 1.0 #ms - settle_down: 0.0 #ms + type: Photometrics + serial_number: A17K631096 + camera_connection: pvcamPCIE_0 + speed_table_index: 0 + readout_port: 0 + gain: 1 + defect_correct_mode: 1.0 + delay: 1.0 + settle_down: 0.0 flip_x: False flip_y: False + supported_channel_count: 4 + remote_focus_device: hardware: - name: daq type: NI - channel: PXI6259/ao2 - min: 0 - max: 5 - port: - baudrate: + channel: Dev1/ao2 + min: 0.0 + max: 10.0 + port: None + baudrate: 0 + galvo: - + name: 0 hardware: type: NI - channel: PXI6259/ao0 - min: -5 - max: 5 + channel: Dev1/ao1 + min: -5.0 + max: 5.0 waveform: sine - phase: 1.57079 # pi/2 - filter_wheel: - hardware: - type: SutterFilterWheel - wheel_number: 1 - port: - baudrate: 0 - filter_wheel_delay: .030 # in seconds - available_filters: - Empty-Alignment: 0 - GFP - FF01-515/30-32: 1 - RFP - FF01-595/31-32: 2 - Far-Red - BLP01-647R/31-32: 3 - Blocked1: 4 - Blocked2: 5 - Blocked3: 6 - Blocked4: 7 - Blocked5: 8 - Blocked6: 9 - stage: - hardware: - - - type: PI - serial_number: 119060508 - axes: [x, y, z, theta, f] - axes_mapping: [1, 2, 3, 4, 5] - feedback_alignment: - device_units_per_mm: - volts_per_micron: - min: 0.0 + phase: 1.57079 + - + name: 1 + hardware: + type: NI + channel: Dev1/ao0 + min: -5.0 max: 5.0 - distance_threshold: 5.0 - settle_duration_ms: 18.0 - controllername: 'C-884' - stages: L-509.20DG10 L-509.40DG10 L-509.20DG10 M-060.DG M-406.4PD NOSTAGE - refmode: FRF FRF FRF FRF FRF FRF - port: - baudrate: 0 - timeout: 0.25 - - joystick_axes: [x, y, z] - # coupled_axes: - # z: f - x_max: 100000 - x_min: -100000 - y_max: 100000 - y_min: -100000 - z_max: 100000 - z_min: -100000 - f_max: 100000 - f_min: -100000 - theta_max: 360 - theta_min: 0 - - x_offset: 0 - y_offset: 0 - z_offset: 0 - theta_offset: 0 - f_offset: 0 + waveform: sawtooth + phase: 0 - flip_x: False - flip_y: False - flip_z: False - flip_f: False - - zoom: - hardware: - type: DynamixelZoom - servo_id: 1 - port: COM9 - baudrate: 100000 - position: - 0.63x: 0 - 1x: 627 - 2x: 1711 - 3x: 2301 - 4x: 2710 - 5x: 3079 - 6x: 3383 - pixel_size: - 0.63x: 9.7 - 1x: 6.38 - 2x: 3.14 - 3x: 2.12 - 4x: 1.609 - 5x: 1.255 - 6x: 1.044 - stage_positions: - BABB: - f: - 0.63x: 0 - 1x: 1 - 2x: 2 - 3x: 3 - 4x: 4 - 5x: 5 - 6x: 6 - shutter: + filter_wheel: hardware: type: NI - channel: PXI6259/port0/line0 - min: 0 - max: 5 + wheel_number: 1 + port: None + baudrate: + name: None + filter_wheel_delay: 0.05 + available_filters: + 473nm: Dev1/port0/line3 + 532nm: Dev1/port0/line2 + 561nm: Dev1/port0/line1 + 638nm: Dev1/port0/line0 + lasers: - # Omicron LightHub Ultra - # 488 and 640 are LuxX+ Lasers - # 561 is a Coherent OBIS Laser - # Digital Laser Outputs - - wavelength: 488 + - + wavelength: 473 onoff: hardware: type: NI - channel: PXI6733/port0/line2 - min: 0 - max: 5 + channel: Dev1/port0/line7 + min: 0.0 + max: 5.0 power: hardware: - type: NI - channel: PXI6733/ao0 - min: 0 - max: 5 - type: LuxX - - wavelength: 562 + type: synthetic + channel: none/ao4 + min: 0.0 + max: 5.0 + - + wavelength: 532 onoff: hardware: type: NI - channel: PXI6733/port0/line3 - min: 0 - max: 5 + channel: Dev1/port0/line6 + min: 0.0 + max: 5.0 power: hardware: - type: NI - channel: PXI6733/ao1 - min: 0 - max: 5 - type: Obis - - wavelength: 642 + type: synthetic + channel: none/ao3 + min: 0.0 + max: 5.0 + - + wavelength: 561 onoff: hardware: type: NI - channel: PXI6733/port0/line4 - min: 0 - max: 5 + channel: Dev1/port0/line5 + min: 0.0 + max: 5.0 power: hardware: - type: NI - channel: PXI6733/ao2 - min: 0 - max: 5 - type: LuxX - Nanoscale: - daq: - hardware: - type: NI - - # NI PCIe-1073 Chassis with PXI-6259 and PXI-6733 DAQ Boards. - # Sampling rate in Hz - sample_rate: 100000 - - # triggers - master_trigger_out_line: PXI6259/port0/line1 - camera_trigger_out_line: /PXI6259/ctr0 - trigger_source: /PXI6259/PFI0 - - # Digital Laser Outputs - laser_port_switcher: PXI6733/port0/line0 - laser_switch_state: True - - camera: - hardware: - type: HamamatsuOrca - serial_number: 302352 - camera_connection: PMPCIECam00 #Photometrics only - defect_correct_mode: 2.0 - delay: 1.0 #ms - settle_down: 0 #ms - flip_x: False - flip_y: False - remote_focus_device: - hardware: - type: NI - channel: PXI6259/ao3 - min: -0.7 - max: 0.7 - port: - baudrate: 0 - galvo: + type: synthetic + channel: none/ao2 + min: 0.0 + max: 5.0 - - hardware: - type: NI - channel: PXI6259/ao1 - min: -5 - max: 5 - waveform: sine - phase: 1.57079 # pi/2 - filter_wheel: + wavelength: 638 + onoff: + hardware: + type: NI + channel: Dev1/port0/line4 + min: 0.0 + max: 5.0 + power: + hardware: + type: synthetic + channel: none/ao1 + min: 0.0 + max: 5.0 + + mirror: hardware: - type: SutterFilterWheel - wheel_number: 2 - port: - baudrate: - filter_wheel_delay: .030 # in seconds - available_filters: - Empty-Alignment: 0 - GFP - FF01-515/30-32: 1 - RFP - FF01-595/31-32: 2 - Far-Red - BLP01-647R/31-32: 3 - Blocked1: 4 - Blocked2: 5 - Blocked3: 6 - Blocked4: 7 - Blocked5: 8 - Blocked6: 9 - stage: + type: SyntheticMirror + flat_path: + n_modes: 32 + + stage: hardware: - - type: PI - serial_number: 119060508 - axes: [x, y, z, theta] - axes_mapping: [1, 2, 3, 4] - feedback_alignment: - device_units_per_mm: - volts_per_micron: - min: 0.0 - max: 5.0 - distance_threshold: 5.0 - settle_duration_ms: 18.0 - controllername: 'C-884' - stages: L-509.20DG10 L-509.40DG10 L-509.20DG10 M-060.DG M-406.4PD NOSTAGE - refmode: FRF FRF FRF FRF FRF FRF - port: - baudrate: 0 - timeout: 0.25 + type: MS2000 + serial_number: 1906420147517051597 + port: /dev/ttyUSB0 + baudrate: 115200 + axes: [x, y, z] # Software + axes_mapping: [X, Y, Z] + feedback_alignment: [90, 90, 90, 90] + max_speed_perc: 0.5 + axes_accel: [70,70,150] + axes_velocity: [0.5,0.5,0.5] - - type: Thorlabs - serial_number: 74000375 + type: KINESIS + serial_number: "/dev/ttyUSB1" axes: [f] axes_mapping: [1] - feedback_alignment: + steps_per_um: 2008.623 + axes_channels: autofocus + start: 14800 + max: 25 + min: 0 + - + type: synthetic + serial_number: 123 + axes: ['theta'] + axes_mapping: ['xylophone'] + feedback_alignment: None device_units_per_mm: - volts_per_micron: - min: 0.0 - max: 5.0 - distance_threshold: 5.0 - settle_duration_ms: 18.0 - controllername: - stages: - refmode: - port: - baudrate: 0 - timeout: 0.25 - - joystick_axes: [x, y, z] - # coupled_axes: - # z: f - x_max: 100000 - x_min: -100000 - y_max: 100000 - y_min: -100000 - z_max: 100000 - z_min: -100000 - f_max: 100000 - f_min: -100000 - theta_max: 360 - theta_min: 0 - - x_offset: 1 - y_offset: 1 - z_offset: 1 - theta_offset: 0 - f_offset: 0 + volts_per_micron: None + min: + max: + distance_threshold: + settle_duration_ms: + controllername: None + stages: None + refmode: None + stagesport: None + baudrate: + timeout: + joystick_axes: ['x', 'y', 'z'] + x_min: -100000.0 + x_max: 100000.0 + y_min: -100000.0 + y_max: 100000.0 + z_min: -100000.0 + y_max: 100000.0 + z_min: -100000.0 + z_max: 100000.0 + theta_min: 0.0 + theta_max: 360.0 + f_min: -100000.0 + f_max: 100000.0 + x_offset: 0.0 + y_offset: 0.0 + z_offset: 0.0 + theta_offset: 0.0 + f_offset: 0.0 flip_x: False flip_y: False flip_z: False - flip_f: False - + flip_f: True + zoom: hardware: type: synthetic - servo_id: - port: - baudrate: + servo_id: 1 + port: None + baudrate: position: - N/A: 0 + N/A: 0.0 pixel_size: - N/A: 0.167 - stage_positions: - BABB: - f: - N/A: 0 - shutter: - hardware: - name: daq - type: NI - channel: PXI6259/port2/line0 - min: 0.0 - max: 5.0 - lasers: - # Omicron LightHub Ultra - # 488 and 640 are LuxX+ Lasers - # 561 is a Coherent OBIS Laser - # Digital Laser Outputs - - wavelength: 488 - onoff: - hardware: - type: NI - channel: PXI6733/port0/line2 - min: 0 - max: 5 - power: - hardware: - type: NI - channel: PXI6733/ao0 - min: 0 - max: 5 - type: LuxX - - wavelength: 562 - onoff: - hardware: - type: NI - channel: PXI6733/port0/line3 - min: 0 - max: 5 - power: - hardware: - type: NI - channel: PXI6733/ao1 - min: 0 - max: 5 - type: Obis - - wavelength: 642 - onoff: - hardware: - type: NI - channel: PXI6733/port0/line4 - min: 0 - max: 5 - power: - hardware: - type: NI - channel: PXI6733/ao2 - min: 0 - max: 5 - type: LuxX + N/A: 2.12500 gui: channels: - count: 5 - -BDVParameters: -# The following parameters are used to configure the BigDataViewer - # visualization. See the BigDataViewer documentation for more details. - # https://imagej.net/BigDataViewer - shear: - shear_data: True - shear_dimension: YZ # XZ, YZ, or XY - shear_angle: 45 - rotate: - rotate_data: False - X: 0 - Y: 0 - Z: 0 + count: 4 diff --git a/src/navigate/config/experiment.yml b/src/navigate/config/experiment.yml index 92ff0accd..1ac0c17bd 100644 --- a/src/navigate/config/experiment.yml +++ b/src/navigate/config/experiment.yml @@ -1,12 +1,12 @@ User: - name: Kevin_Dean + name: Steven_Sheppard Saving: - root_directory: C:\Users\MicroscopyInnovation\Desktop\Data - save_directory: E://Kevin\Lung\MV3\GFP\2022-02-18\Cell000 - user: Kevin - tissue: Lung - celltype: MV3 - label: GFP + root_directory: ~/data/ + save_directory: /mnt/data/ + user: Steven + tissue: NA + celltype: NA + label: NA file_type: TIFF date: 2022-06-07 solvent: BABB diff --git a/src/navigate/config/gui_configuration.yml b/src/navigate/config/gui_configuration.yml index ff9a2ddf1..6661c74a0 100644 --- a/src/navigate/config/gui_configuration.yml +++ b/src/navigate/config/gui_configuration.yml @@ -1,5 +1,5 @@ channel_settings: - count: 5 + count: 4 laser_power: step: 1 min: 0 @@ -7,14 +7,14 @@ channel_settings: exposure_time: step: 1 min: 1 - max: 1000 + max: 2000 interval: step: 1 min: 1 max: 10 defocus: step: 1 - min: 0 + min: -100 max: 100 stack_acquisition: step_size: @@ -30,9 +30,9 @@ stack_acquisition: min: -10000 max: 10000 f_start_pos: - step: 0.01 - min: -200 - max: 2000 + step: 1.0 + min: -5000 + max: 5000 f_end_pos: step: 0.01 min: -200 diff --git a/src/navigate/model/device_startup_functions.py b/src/navigate/model/device_startup_functions.py index a05c37b60..aabce3b3a 100644 --- a/src/navigate/model/device_startup_functions.py +++ b/src/navigate/model/device_startup_functions.py @@ -450,7 +450,6 @@ def load_stages( stage_devices = [] stages = configuration["configuration"]["hardware"]["stage"] - if type(stages) != ListProxy: stages = [stages] @@ -461,7 +460,6 @@ def load_stages( else: stage_type = stage_config["type"] - if stage_type == "PI" and platform.system() == "Windows": from navigate.model.devices.stages.pi import build_PIStage_connection from pipython.pidevice.gcserror import GCSError @@ -1424,7 +1422,6 @@ def start_galvo( Galvo : GalvoBase Galvo scanning class. """ - if plugin_devices is None: plugin_devices = {} From 92ed4813d52bf078abd78b5518712336d685ecc9 Mon Sep 17 00:00:00 2001 From: SJShep Date: Thu, 10 Jul 2025 17:10:13 -0700 Subject: [PATCH 6/6] added debugging flags for setting up a z-stack. Modified the focus stage step logic to move in the direction dictated by the GUI start and end positions. --- src/navigate/config/configuration.yaml | 324 ++++++++++-------- src/navigate/controller/controller.py | 1 - .../sub_controllers/channels_tab.py | 35 +- .../APIs/thorlabs/pykinesis_controller.py | 1 + src/navigate/model/devices/daq/ni.py | 14 +- .../model/features/common_features.py | 18 +- src/navigate/model/microscope.py | 6 +- 7 files changed, 234 insertions(+), 165 deletions(-) diff --git a/src/navigate/config/configuration.yaml b/src/navigate/config/configuration.yaml index bd095e3e1..9b46a403f 100644 --- a/src/navigate/config/configuration.yaml +++ b/src/navigate/config/configuration.yaml @@ -1,60 +1,53 @@ +# Only one microscope can be active in the GUI at a time, but all microscopes will be accessible microscopes: Mesoscale: daq: hardware: type: NI + # Sampling rate in Hz sample_rate: 100000 + + # triggers master_trigger_out_line: Dev1/port0/line10 camera_trigger_out_line: /Dev1/ctr0 trigger_source: /Dev1/PFI3 + + # Digital Laser Outputs laser_port_switcher: Dev1/port0/line11 laser_switch_state: False - - zoom: - hardware: - type: synthetic - servo_id: 1 - port: None - baudrate: - position: - N/A: 0.0 - pixel_size: - N/A: 2.125 - - shutter: - hardware: - type: synthetic - channel: Dev1/port0/line16 - min: 0.0 - max: 5.0 - + camera: hardware: type: Photometrics serial_number: A17K631096 - camera_connection: pvcamPCIE_0 - speed_table_index: 0 - readout_port: 0 - gain: 1 - defect_correct_mode: 1.0 - delay: 1.0 - settle_down: 0.0 + camera_connection: pvcamPCIE_0 + delay: 0.002 #ms + settle_down: 0 #ms flip_x: False flip_y: False - supported_channel_count: 4 - + subsampling: [1, 2, 4] + readout_port: 0 + delay_percent: 10 + gain: 1 + speed_table_index: 0 + exposure_time_range: + min: 1 + max: 1000 + step: 1 + unitforlinedelay: 10.26 + remote_focus_device: hardware: type: NI - channel: Dev1/ao2 - min: 0.0 - max: 10.0 - port: None - baudrate: 0 - + channel: Dev1/ao1 + min: 0 + max: 10 + port: + baudrate: 0 + galvo: - - name: 0 + name: GM1 hardware: type: NI channel: Dev1/ao1 @@ -63,7 +56,7 @@ microscopes: waveform: sine phase: 1.57079 - - name: 1 + name: GM2 hardware: type: NI channel: Dev1/ao0 @@ -76,81 +69,14 @@ microscopes: hardware: type: NI wheel_number: 1 - port: None - baudrate: - name: None - filter_wheel_delay: 0.05 + filter_wheel_delay: 0.050 # in seconds available_filters: 473nm: Dev1/port0/line3 532nm: Dev1/port0/line2 561nm: Dev1/port0/line1 638nm: Dev1/port0/line0 - - lasers: - - - wavelength: 473 - onoff: - hardware: - type: NI - channel: Dev1/port0/line7 - min: 0.0 - max: 5.0 - power: - hardware: - type: synthetic - channel: none/ao4 - min: 0.0 - max: 5.0 - - - wavelength: 532 - onoff: - hardware: - type: NI - channel: Dev1/port0/line6 - min: 0.0 - max: 5.0 - power: - hardware: - type: synthetic - channel: none/ao3 - min: 0.0 - max: 5.0 - - - wavelength: 561 - onoff: - hardware: - type: NI - channel: Dev1/port0/line5 - min: 0.0 - max: 5.0 - power: - hardware: - type: synthetic - channel: none/ao2 - min: 0.0 - max: 5.0 - - - wavelength: 638 - onoff: - hardware: - type: NI - channel: Dev1/port0/line4 - min: 0.0 - max: 5.0 - power: - hardware: - type: synthetic - channel: none/ao1 - min: 0.0 - max: 5.0 - - mirror: - hardware: - type: SyntheticMirror - flat_path: - n_modes: 32 - - stage: + + stage: hardware: - type: MS2000 @@ -170,61 +96,165 @@ microscopes: axes_mapping: [1] steps_per_um: 2008.623 axes_channels: autofocus + max: 0 + min: 25 start: 14800 - max: 25 - min: 0 - - type: synthetic + type: SyntheticStage serial_number: 123 - axes: ['theta'] - axes_mapping: ['xylophone'] - feedback_alignment: None - device_units_per_mm: + axes: [theta] + axes_mapping: [xylophone] volts_per_micron: None - min: - max: - distance_threshold: - settle_duration_ms: - controllername: None - stages: None - refmode: None - stagesport: None - baudrate: - timeout: - joystick_axes: ['x', 'y', 'z'] - x_min: -100000.0 - x_max: 100000.0 - y_min: -100000.0 - y_max: 100000.0 - z_min: -100000.0 - y_max: 100000.0 - z_min: -100000.0 - z_max: 100000.0 - theta_min: 0.0 - theta_max: 360.0 - f_min: -100000.0 - f_max: 100000.0 - x_offset: 0.0 - y_offset: 0.0 - z_offset: 0.0 - theta_offset: 0.0 - f_offset: 0.0 + axes_channels: None + max: None + min: None + + joystick_axes: [x, y, z] + x_max: 100000 + x_min: -100000 + y_max: 100000 + y_min: -100000 + z_max: 100000 + z_min: -100000 + f_max: 100000 + f_min: -100000 + theta_max: 360 + theta_min: 0 + + x_offset: 0 + y_offset: 0 + z_offset: 0 + theta_offset: 0 + f_offset: 0 flip_x: False flip_y: False flip_z: False - flip_f: True - + flip_f: False + zoom: hardware: type: synthetic servo_id: 1 - port: None - baudrate: position: - N/A: 0.0 + N/A: 0 pixel_size: - N/A: 2.12500 + N/A: 2.125 + + shutter: + hardware: + name: daq + type: synthetic + channel: Dev1/port0/line16 + min: 0.0 + max: 5.0 + + lasers: + # Oxxius L4CC [473, 532, 561, 638] + - wavelength: 473 + onoff: + hardware: + type: NI + channel: Dev1/port0/line7 + min: 0 + max: 5 + power: + hardware: + type: synthetic + channel: Dev1/ao3 + min: 0 + max: 5 + type: LBX + - wavelength: 532 + onoff: + hardware: + type: NI + channel: Dev1/port0/line6 + min: 0 + max: 5 + power: + hardware: + type: synthetic + channel: Dev1/ao3 + min: 0 + max: 5 + # type: LCX + - wavelength: 561 + onoff: + hardware: + type: NI + channel: Dev1/port0/line5 + min: 0 + max: 5 + power: + hardware: + type: synthetic + channel: Dev1/ao3 + min: 0 + max: 5 + type: LCX + - wavelength: 638 + onoff: + hardware: + type: NI + channel: Dev1/port0/line4 + min: 0 + max: 5 + power: + hardware: + type: synthetic + channel: Dev1/ao2 + min: 0 + max: 5 + type: LBX gui: channels: count: 4 + laser_power: + min: 0 + max: 100 + step: 10 + exposure_time: + min: 1 + max: 1000 + step: 5 + interval_time: + min: 0 + max: 1000 + step: 5 + stack_acquisition: + step_size: + min: 0.100 + max: 1000 + step: 0.1 + start_pos: + min: -5000 + max: 5000 + step: 1 + end_pos: + min: -5000 + max: 10000 + step: 1 + timepoint: + timepoints: + min: 1 + max: 1000 + step: 1 + stack_pause: + min: 0 + max: 1000 + step: 1 + +BDVParameters: +# The following parameters are used to configure the BigDataViewer + # visualization. See the BigDataViewer documentation for more details. + # https://imagej.net/BigDataViewer + shear: + shear_data: False + shear_dimension: YZ # XZ, YZ, or XY + shear_angle: 45 + rotate: + rotate_data: False + X: 0 + Y: 0 + Z: 0 diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index 6b12eda64..6cc9bcbd1 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -29,7 +29,6 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. - # Standard Library Imports from multiprocessing import Manager import tkinter diff --git a/src/navigate/controller/sub_controllers/channels_tab.py b/src/navigate/controller/sub_controllers/channels_tab.py index 63e4cde2c..769681edf 100644 --- a/src/navigate/controller/sub_controllers/channels_tab.py +++ b/src/navigate/controller/sub_controllers/channels_tab.py @@ -30,6 +30,8 @@ # POSSIBILITY OF SUCH DAMAGE. # +DEBUGGING = True + # Standard Library Imports import logging import datetime @@ -195,12 +197,19 @@ def populate_experiment_values(self): self.microscope_state_dict = self.parent_controller.configuration["experiment"][ "MicroscopeState" ] + # NOTE: If the step size is negative, this forces a positive step size. + # So if the z start/stop is decreasing it will not run as expected. if self.microscope_state_dict["step_size"] < 0: self.microscope_state_dict["step_size"] = -self.microscope_state_dict[ "step_size" ] + if DEBUGGING: + print('--ChannelsTab-- step size reversed') + + if DEBUGGING: + print(f'--ChannelsTab-- stack acq vals:\n {self.stack_acq_vals}') + self.set_info(self.stack_acq_vals, self.microscope_state_dict) - # self.set_info(self.conpro_acq_vals, self.microscope_state_dict) self.set_info(self.timepoint_vals, self.microscope_state_dict) # check configuration for multiposition settings @@ -229,6 +238,7 @@ def populate_experiment_values(self): # after initialization self.in_initialization = False self.channel_setting_controller.in_initialization = False + # update z and f position self.z_origin = self.parent_controller.configuration["experiment"][ "StageParameters" @@ -375,6 +385,7 @@ def update_z_steps(self, *args): # Calculate the number of slices and set GUI try: # validate the spinbox's value + # NOTE: start/end position are relative, not absolute start_position = float(self.stack_acq_vals["start_position"].get()) end_position = float(self.stack_acq_vals["end_position"].get()) step_size = float(self.stack_acq_vals["step_size"].get()) @@ -391,11 +402,7 @@ def update_z_steps(self, *args): except (KeyError, AttributeError): logger.error("Error caught: updating z_steps") return - - # if step_size < 0.001: - # step_size = 0.001 - # self.stack_acq_vals['step_size'].set(step_size) - + number_z_steps = int( np.ceil(np.abs((end_position - start_position) / step_size)) ) @@ -421,6 +428,14 @@ def update_z_steps(self, *args): "abs_z_start" ].get() self.microscope_state_dict["abs_z_end"] = self.stack_acq_vals["abs_z_end"].get() + + if DEBUGGING: + print( + f"--ChannelsTab--\n", + f" f start: {self.stack_acq_vals['start_focus'].get()}\n", + f" f end: {self.stack_acq_vals['end_focus'].get()}\n", + f" f origin: {self.focus_origin}\n", + ) try: self.microscope_state_dict["start_focus"] = self.stack_acq_vals[ "start_focus" @@ -451,7 +466,7 @@ def update_start_position(self, *args): Values is a dict as follows {'start_position': , 'abs_z_start': , 'stack_z_origin': } """ - + # NOTE: pressing the set start/stop button sets the z/f origins. and sets the start and end positions to 0. So when setting up the zstack it should be done by first pressing start, then go to the end and press end? # We have a new origin self.z_origin = self.parent_controller.configuration["experiment"][ "StageParameters" @@ -489,6 +504,7 @@ def update_end_position(self, *args): "StageParameters" ]["f"] + # NOTE: Here we are setting the z/f start as the origin z_start = self.z_origin focus_start = self.focus_origin @@ -497,6 +513,9 @@ def update_end_position(self, *args): z_start, z_end = z_end, z_start focus_start, focus_end = focus_end, focus_start + # NOTE: Now after making sure we are moving forward, we redefine the origin at the middle of the stack. + # This means when first setting start, then setting end, origin is going to be calculated as the middle between the 2, + # and the start positions are the old origins. This logic is why the z-stack setup is so specific. # set origin to be in the middle of start and end self.z_origin = (z_start + z_end) / 2 self.focus_origin = (focus_start + focus_end) / 2 @@ -507,6 +526,7 @@ def update_end_position(self, *args): end_pos = z_end - self.z_origin start_focus = focus_start - self.focus_origin end_focus = focus_end - self.focus_origin + # NOTE: The parameters in the GUI are relative to origin which is 1/2 way between the start/end positions, when pressed if flip_flags["z"]: start_pos, end_pos = end_pos, start_pos start_focus, end_focus = end_focus, start_focus @@ -797,7 +817,6 @@ def update_experiment_values(self): self.channel_setting_controller.update_experiment_values() self.update_z_steps() - def verify_experiment_values(self): """Verify channel tab settings and return warning info diff --git a/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py b/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py index 52cc020bd..84ee9bd07 100644 --- a/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py +++ b/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py @@ -84,6 +84,7 @@ def move_to_position(self, position, steps_per_um, wait_till_done): distance = position - position_um # convert total distance to steps steps = steps_per_um * distance + # TODO: Does steps need to be an int? self.stage.move_by(steps, channel=1, scale=False) if wait_till_done: self.stage.wait_move(channel=1) diff --git a/src/navigate/model/devices/daq/ni.py b/src/navigate/model/devices/daq/ni.py index 6cbe2d22f..b758519fd 100644 --- a/src/navigate/model/devices/daq/ni.py +++ b/src/navigate/model/devices/daq/ni.py @@ -29,6 +29,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +DEBUGGING = True # Standard Imports import logging @@ -140,7 +141,10 @@ def set_external_trigger(self, external_trigger=None) -> None: "self-trigger" if external_trigger is None else "external-trigger" ) self.external_trigger = external_trigger - + + if DEBUGGING: + print(f'--DAQ-- trigger_mode:{self.trigger_mode}') + # change trigger mode during acquisition in a feature if self.trigger_mode == "self-trigger": self.create_master_trigger_task() @@ -159,6 +163,7 @@ def set_external_trigger(self, external_trigger=None) -> None: self.camera_trigger_task.triggers.start_trigger.cfg_dig_edge_start_trig( trigger_source ) + # TODO: Is this a spot to ad a check for if it needs to be reprogrammed? self.camera_trigger_task.triggers.start_trigger.retriggerable = False # set analog task trigger source for board_name in self.analog_output_tasks.keys(): @@ -172,6 +177,7 @@ def set_external_trigger(self, external_trigger=None) -> None: self.analog_output_tasks[ board_name ].triggers.start_trigger.cfg_dig_edge_start_trig(trigger_source) + # NOTE: this was causing an error for me using PCIe-6343 in Linux. Not sure if it was board or OS related. # try: # # print(board_name) @@ -302,6 +308,9 @@ def create_camera_task(self, channel_key: str) -> None: # apply waveform templates camera_waveform_repeat_num = self.waveform_repeat_num * self.waveform_expand_num + if DEBUGGING: + print(f'--DAQ-- camera waveform repeat:{camera_waveform_repeat_num}') + if self.analog_outputs: camera_high_time = 0.004 camera_low_time = self.sweep_times[channel_key] - camera_high_time @@ -352,6 +361,9 @@ def create_analog_output_tasks(self, channel_key: str) -> None: """ self.n_sample = int(self.sample_rate * self.sweep_times[channel_key]) max_sample = self.n_sample * self.waveform_expand_num + + if DEBUGGING: + print(f'--DAQ-- analog waveform max sample:{max_sample}, repeat num:{self.waveform_repeat_num}') # TODO: GalvoStage and remote_focus waveform are not calculated based on a # same sweep time. There needs some fix. diff --git a/src/navigate/model/features/common_features.py b/src/navigate/model/features/common_features.py index 447a561ea..c7e77b410 100644 --- a/src/navigate/model/features/common_features.py +++ b/src/navigate/model/features/common_features.py @@ -29,7 +29,7 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # - +DEBUGGING = True # Standard library imports import time import ast @@ -988,12 +988,17 @@ def pre_signal_func(self): self.z_stack_distance = abs( self.start_z_position - float(microscope_state["end_position"]) ) - # NOTE: added a quick fix for the focus stage moving in a negative direction during z-stack acquisitions. Somehow a negative step needs to be allowed. Flipping the axes direction did not have the desired outcome. + # NOTE: To allow for the focus stage to move in a negative direction while the z-stage is + # moving in a positive direction, check the focus start/stop and modify the step size self.start_focus = float(microscope_state["start_focus"]) end_focus = float(microscope_state["end_focus"]) - self.focus_step_size = -(end_focus - self.start_focus) / self.number_z_steps - #: float: The focus stack distance for the z-stack. + if self.start_focus > end_focus: + focus_direction = -1 + else: + focus_direction = 1 self.f_stack_distance = abs(end_focus - self.start_focus) + self.focus_step_size = focus_direction * self.f_stack_distance / self.number_z_steps + #: float: The focus stack distance for the z-stack. # restore z, f pos_dict = self.model.get_stage_position() @@ -1055,8 +1060,7 @@ def pre_signal_func(self): self.need_to_move_z_position = True #: bool: Flag to determine whether to pause the data thread. self.should_pause_data_thread = False - # TODO: distance > 1000 should not be hardcoded and somehow related to - # different kinds of stage devices. + # NOTE: For large acquisitions this ight be a fault if not near the starting point? self.stage_distance_threshold = 1000 self.defocus = [ @@ -1157,6 +1161,8 @@ def signal_func(self): self.model.pause_data_thread() logger.info("Data thread paused.") + if DEBUGGING: + print(f'--CommonFeatures-- current_focus_position:{self.current_focus_position}') self.model.move_stage( { "z_abs": self.current_z_position, diff --git a/src/navigate/model/microscope.py b/src/navigate/model/microscope.py index 45d64d4d8..dd66f8916 100644 --- a/src/navigate/model/microscope.py +++ b/src/navigate/model/microscope.py @@ -793,9 +793,10 @@ def prepare_next_channel(self, update_daq_task_flag: bool = True) -> None: self.daq.stop_acquisition() self.daq.prepare_acquisition(channel_key) + # TODO: Here is the logic for adding the defocus for each channel. This runs before imaging each channel + # NOTE: We are using the current stage position for the central focus # Add Defocus term # Assume wherever we start is the central focus - # TODO: is this the correct assumption? if self.central_focus is None: self.central_focus = self.get_stage_position().get("f_pos") if self.central_focus is not None: @@ -895,7 +896,8 @@ def stop_stage(self) -> None: for stage, axes in self.stages_list: stage.stop() - self.central_focus = self.get_stage_position().get("f_pos", self.central_focus) + # NOTE: removed extra arg in get from dictionary + self.central_focus = self.get_stage_position().get("f_pos") def get_stage_position(self) -> dict: """Get stage position.