diff --git a/src/navigate/config/configuration.yaml b/src/navigate/config/configuration.yaml index 483243825..9b46a403f 100644 --- a/src/navigate/config/configuration.yaml +++ b/src/navigate/config/configuration.yaml @@ -4,90 +4,112 @@ microscopes: 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 + master_trigger_out_line: Dev1/port0/line10 + camera_trigger_out_line: /Dev1/ctr0 + trigger_source: /Dev1/PFI3 # Digital Laser Outputs - laser_port_switcher: PXI6733/port0/line0 + laser_port_switcher: Dev1/port0/line11 laser_switch_state: False - + 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 + delay: 0.002 #ms + settle_down: 0 #ms flip_x: False flip_y: False + 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: - name: daq type: NI - channel: PXI6259/ao2 + channel: Dev1/ao1 min: 0 - max: 5 - port: - baudrate: + max: 10 + port: + baudrate: 0 + galvo: - + name: GM1 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 + phase: 1.57079 + - + name: GM2 + hardware: + type: NI + channel: Dev1/ao0 + min: -5.0 + max: 5.0 + waveform: sawtooth + phase: 0 + filter_wheel: hardware: - type: SutterFilterWheel + type: NI wheel_number: 1 - port: - baudrate: 0 - filter_wheel_delay: .030 # in seconds + filter_wheel_delay: 0.050 # 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 + 473nm: Dev1/port0/line3 + 532nm: Dev1/port0/line2 + 561nm: Dev1/port0/line1 + 638nm: Dev1/port0/line0 + 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 - 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: KINESIS + serial_number: "/dev/ttyUSB1" + axes: [f] + axes_mapping: [1] + steps_per_um: 2008.623 + axes_channels: autofocus + max: 0 + min: 25 + start: 14800 + - + type: SyntheticStage + serial_number: 123 + axes: [theta] + axes_mapping: [xylophone] + volts_per_micron: None + axes_channels: None + max: None + min: None joystick_axes: [x, y, z] - # coupled_axes: - # z: f x_max: 100000 x_min: -100000 y_max: 100000 @@ -104,7 +126,6 @@ microscopes: z_offset: 0 theta_offset: 0 f_offset: 0 - flip_x: False flip_y: False flip_z: False @@ -112,294 +133,124 @@ microscopes: zoom: hardware: - type: DynamixelZoom + type: synthetic servo_id: 1 - port: COM9 - baudrate: 100000 position: - 0.63x: 0 - 1x: 627 - 2x: 1711 - 3x: 2301 - 4x: 2710 - 5x: 3079 - 6x: 3383 + N/A: 0 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 + N/A: 2.125 + shutter: hardware: - type: NI - channel: PXI6259/port0/line0 - min: 0 - max: 5 + name: daq + type: synthetic + channel: Dev1/port0/line16 + 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 + # Oxxius L4CC [473, 532, 561, 638] + - wavelength: 473 onoff: hardware: type: NI - channel: PXI6733/port0/line4 + channel: Dev1/port0/line7 min: 0 max: 5 power: hardware: - type: NI - channel: PXI6733/ao2 + type: synthetic + channel: Dev1/ao3 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: - - - hardware: - type: NI - channel: PXI6259/ao1 - min: -5 - max: 5 - waveform: sine - phase: 1.57079 # pi/2 - filter_wheel: - 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: - 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: Thorlabs - serial_number: 74000375 - axes: [f] - axes_mapping: [1] - 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: - 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 - flip_x: False - flip_y: False - flip_z: False - flip_f: False - - zoom: - hardware: - type: synthetic - servo_id: - port: - baudrate: - position: - N/A: 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 + type: LBX + - wavelength: 532 onoff: hardware: type: NI - channel: PXI6733/port0/line2 + channel: Dev1/port0/line6 min: 0 max: 5 power: hardware: - type: NI - channel: PXI6733/ao0 + type: synthetic + channel: Dev1/ao3 min: 0 max: 5 - type: LuxX - - wavelength: 562 + # type: LCX + - wavelength: 561 onoff: hardware: type: NI - channel: PXI6733/port0/line3 + channel: Dev1/port0/line5 min: 0 max: 5 power: hardware: - type: NI - channel: PXI6733/ao1 + type: synthetic + channel: Dev1/ao3 min: 0 max: 5 - type: Obis - - wavelength: 642 + type: LCX + - wavelength: 638 onoff: hardware: type: NI - channel: PXI6733/port0/line4 + channel: Dev1/port0/line4 min: 0 max: 5 power: hardware: - type: NI - channel: PXI6733/ao2 + type: synthetic + channel: Dev1/ao2 min: 0 max: 5 - type: LuxX + type: LBX gui: channels: - count: 5 - + 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: True + shear_data: False shear_dimension: YZ # XZ, YZ, or XY shear_angle: 45 rotate: 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/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/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/device_startup_functions.py b/src/navigate/model/device_startup_functions.py index ec7c2eaca..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 @@ -511,8 +509,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_KINESIS_Stage_connection + ) + stage_devices.append( + auto_redial( + build_KINESIS_Stage_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 +570,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 +709,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 +1272,6 @@ def start_lasers( modulation = "analog" elif digital == "NI": modulation = "digital" - return LaserNI( microscope_name=microscope_name, device_connection=device_connection, @@ -1416,7 +1422,6 @@ def start_galvo( Galvo : GalvoBase Galvo scanning class. """ - if plugin_devices is None: plugin_devices = {} 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..d4eb4ff3c 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 @@ -79,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 @@ -191,8 +193,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: @@ -216,8 +219,8 @@ def connect_to_serial( "X", "Y", "Z", - ] # self.get_default_motor_axis_sequence() - + ] + def get_default_motor_axis_sequence(self) -> None: """Get the default motor axis sequence from the ASI device @@ -374,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/pykinesis_controller.py b/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py new file mode 100644 index 000000000..84ee9bd07 --- /dev/null +++ b/src/navigate/model/devices/APIs/thorlabs/pykinesis_controller.py @@ -0,0 +1,150 @@ +""" +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 + # 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) + 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/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/daq/ni.py b/src/navigate/model/devices/daq/ni.py index 88620a0a4..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,12 +177,15 @@ 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: @@ -300,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 @@ -350,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/devices/stages/tl_kinesis_steppermotor.py b/src/navigate/model/devices/stages/tl_kinesis_steppermotor.py new file mode 100644 index 000000000..339edb3e9 --- /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 +from navigate.model.devices.APIs.thorlabs.pykinesis_controller import KinesisStage +# Logger Setup +p = __name__.split(".")[1] +logger = logging.getLogger(p) + + +def build_KINESIS_Stage_connection(serial_number): + """Connect to the Thorlabs KST Stage + + Parameters + ---------- + serialnum : str + Serial number of the stage. + + Returns + ------- + kin_controller + Thorlabs KST Stage controller + """ + 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 +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.kin_controller = device_connection + else: + self.kin_controller = build_KINESIS_Stage_connection(self.serial_number) + + def __del__(self): + """Delete the KST Connection""" + try: + self.kin_controller.stop() + self.kin_controller.close() + 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.kin_controller.get_current_position(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.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): + """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.kin_controller.move_to_position(position, + self.device_unit_scale, + wait_until_done) + + def run_homing(self): + """Run homing sequence.""" + 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.kin_controller.stop() diff --git a/src/navigate/model/features/common_features.py b/src/navigate/model/features/common_features.py index 2b744216c..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: 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 76de2a860..dd66f8916 100644 --- a/src/navigate/model/microscope.py +++ b/src/navigate/model/microscope.py @@ -793,12 +793,14 @@ 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: + #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, @@ -894,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. 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) """