From 72761acaa3515b5fdab7631f3966c2586178f9e6 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:02:32 -0500 Subject: [PATCH 1/4] Enable digital or analog-only triggering. --- .../model/device_startup_functions.py | 25 +++- src/navigate/model/devices/lasers/ni.py | 120 +++++++++++++----- src/navigate/model/microscope.py | 2 + test/model/devices/lasers/test_laser_ni.py | 8 +- 4 files changed, 114 insertions(+), 41 deletions(-) diff --git a/src/navigate/model/device_startup_functions.py b/src/navigate/model/device_startup_functions.py index 8b6dcd987..57571010b 100644 --- a/src/navigate/model/device_startup_functions.py +++ b/src/navigate/model/device_startup_functions.py @@ -1155,16 +1155,35 @@ def start_lasers( device_type = "SyntheticLaser" else: - device_type = configuration["configuration"]["microscopes"][microscope_name][ + analog = configuration["configuration"]["microscopes"][microscope_name][ "lasers" ][id]["power"]["hardware"]["type"] - if device_type == "NI": + digital = configuration["configuration"]["microscopes"][microscope_name][ + "lasers" + ][id]["onoff"]["hardware"]["type"] + + device_type = analog + + print("Analog: ", analog, "Digital: ", digital, "Device Type: ", device_type) + + if analog == "NI" or digital == "NI": if device_connection is not None: return device_connection from navigate.model.devices.lasers.ni import LaserNI - return LaserNI(microscope_name, device_connection, configuration, id) + if analog == "NI" and digital == "NI": + modulation = "mixed" + elif analog == "NI": + modulation = "analog" + elif digital == "NI": + modulation = "digital" + + return LaserNI(microscope_name=microscope_name, + device_connection=device_connection, + configuration=configuration, + laser_id=id, + modulation_type=modulation) elif device_type.lower() == "syntheticlaser" or device_type.lower() == "synthetic": if device_connection is not None: diff --git a/src/navigate/model/devices/lasers/ni.py b/src/navigate/model/devices/lasers/ni.py index a9337aa12..26695d6d7 100644 --- a/src/navigate/model/devices/lasers/ni.py +++ b/src/navigate/model/devices/lasers/ni.py @@ -54,7 +54,8 @@ class LaserNI(LaserBase): This class is used to control a laser connected to a National Instruments DAQ. """ - def __init__(self, microscope_name, device_connection, configuration, laser_id): + def __init__(self, microscope_name, device_connection, configuration, + laser_id, modulation_type="digital"): """Initialize the LaserNI class. Parameters @@ -67,13 +68,75 @@ def __init__(self, microscope_name, device_connection, configuration, laser_id): The device configuration. laser_id : str The laser id. + modulation_type : str + The modulation type of the laser - Analog, Digital, or Mixed. """ super().__init__(microscope_name, device_connection, configuration, laser_id) + #: str: The modulation type of the laser - Analog, Digital, or Mixed. + self.modulation_type = modulation_type + #: str: Modulation type of the laser - Analog or Digital. - self.on_off_type = None + self.digital_port_type = None + + #: float: The minimum digital modulation voltage. + self.laser_min_do = None + + #: float: The maximum digital modulation voltage. + self.laser_max_do = None + + #: nidaqmx.Task: The laser digital modulation task. + self.laser_do_task = None + + #: float: The minimum analog modulation voltage. + self.laser_min_ao = None + + #: float: The maximum analog modulation voltage. + self.laser_max_ao = None + + #: nidaqmx.Task: The laser analog modulation task. + self.laser_ao_task = None + + #: float: Current laser intensity. + self._current_intensity = 0 + + # Initialize the laser modulation type. + if self.modulation_type == "mixed": + self.initialize_digital_modulation() + self.initialize_analog_modulation() + logger.info(f"{str(self)} initialized with mixed modulation.") + + elif self.modulation_type == "analog": + self.initialize_analog_modulation() + logger.info(f"{str(self)} initialized with analog modulation.") + + elif self.modulation_type == "digital": + self.initialize_digital_modulation() + logger.info(f"{str(self)} initialized with digital modulation.") - # Digital out (if using mixed modulation mode) + def initialize_analog_modulation(self): + try: + laser_ao_port = self.device_config["power"]["hardware"]["channel"] + + #: float: The minimum analog modulation voltage. + self.laser_min_ao = self.device_config["power"]["hardware"]["min"] + + #: float: The maximum analog modulation voltage. + self.laser_max_ao = self.device_config["power"]["hardware"]["max"] + + #: object: The laser analog modulation task. + self.laser_ao_task = nidaqmx.Task() + self.laser_ao_task.ao_channels.add_ao_voltage_chan( + laser_ao_port, min_val=self.laser_min_ao, + max_val=self.laser_max_ao + ) + except DaqError as e: + logger.debug( + f"{str(self)} error:, {e}, {e.error_type}, {e.error_code}") + print(f"{str(self)} error:, {e}, {e.error_type}, {e.error_code}") + + def initialize_digital_modulation(self): + """Initialize the digital modulation of the laser.""" try: laser_do_port = self.device_config["onoff"]["hardware"]["channel"] @@ -87,18 +150,20 @@ def __init__(self, microscope_name, device_connection, configuration, laser_id): self.laser_do_task = nidaqmx.Task() if "/ao" in laser_do_port: - # Artificial Digital Modulation via an Analog Port + # Perform the digital modulation with an analog output port. self.laser_do_task.ao_channels.add_ao_voltage_chan( - laser_do_port, min_val=self.laser_min_do, max_val=self.laser_max_do + laser_do_port, min_val=self.laser_min_do, + max_val=self.laser_max_do ) - self.on_off_type = "analog" + self.digital_port_type = "analog" else: # Digital Modulation via a Digital Port self.laser_do_task.do_channels.add_do_chan( - laser_do_port, line_grouping=LineGrouping.CHAN_FOR_ALL_LINES + laser_do_port, + line_grouping=LineGrouping.CHAN_FOR_ALL_LINES ) - self.on_off_type = "digital" + self.digital_port_type = "digital" except (KeyError, DaqError) as e: self.laser_do_task = None if isinstance(e, DaqError): @@ -108,28 +173,6 @@ def __init__(self, microscope_name, device_connection, configuration, laser_id): print(e.error_code) print(e.error_type) - #: float: Current laser intensity. - self._current_intensity = 0 - - # Analog out - try: - laser_ao_port = self.device_config["power"]["hardware"]["channel"] - - #: float: The minimum analog modulation voltage. - self.laser_min_ao = self.device_config["power"]["hardware"]["min"] - - #: float: The maximum analog modulation voltage. - self.laser_max_ao = self.device_config["power"]["hardware"]["max"] - - #: object: The laser analog modulation task. - self.laser_ao_task = nidaqmx.Task() - self.laser_ao_task.ao_channels.add_ao_voltage_chan( - laser_ao_port, min_val=self.laser_min_ao, max_val=self.laser_max_ao - ) - except DaqError as e: - logger.debug(f"{str(self)} error:, {e}, {e.error_type}, {e.error_code}") - print(f"{str(self)} error:, {e}, {e.error_type}, {e.error_code}") - def set_power(self, laser_intensity): """Sets the laser power. @@ -138,6 +181,8 @@ def set_power(self, laser_intensity): laser_intensity : float The laser intensity. """ + if self.laser_ao_task is None: + return try: scaled_laser_voltage = (int(laser_intensity) / 100) * self.laser_max_ao self.laser_ao_task.write(scaled_laser_voltage, auto_start=True) @@ -147,32 +192,39 @@ def set_power(self, laser_intensity): def turn_on(self): """Turns on the laser.""" + if self.laser_do_task is None: + return try: self.set_power(self._current_intensity) if self.laser_do_task is not None: - if self.on_off_type == "digital": + if self.digital_port_type == "digital": self.laser_do_task.write(True, auto_start=True) - elif self.on_off_type == "analog": + elif self.digital_port_type == "analog": self.laser_do_task.write(self.laser_max_do, auto_start=True) except DaqError as e: logger.exception(e) def turn_off(self): """Turns off the laser.""" + if self.laser_do_task is None: + return + try: tmp = self._current_intensity self.set_power(0) self._current_intensity = tmp if self.laser_do_task is not None: - if self.on_off_type == "digital": + if self.digital_port_type == "digital": self.laser_do_task.write(False, auto_start=True) - elif self.on_off_type == "analog": + elif self.digital_port_type == "analog": self.laser_do_task.write(self.laser_min_do, auto_start=True) except DaqError as e: logger.exception(e) def close(self): """Close the NI Task before exit.""" + if self.laser_ao_task is None: + return try: self.laser_ao_task.close() if self.laser_do_task is not None: diff --git a/src/navigate/model/microscope.py b/src/navigate/model/microscope.py index 0631c6c95..0baffb6d5 100644 --- a/src/navigate/model/microscope.py +++ b/src/navigate/model/microscope.py @@ -728,6 +728,8 @@ def prepare_next_channel(self, update_daq_task_flag=True): self.lasers[str(self.laser_wavelength[self.current_laser_index])].set_power( channel["laser_power"] ) + logger.info(f"{self.laser_wavelength[self.current_laser_index]} " + f"nm laser power set to {channel['laser_power']}") # self.lasers[str(self.laser_wavelength[self.current_laser_index])].turn_on() # stop daq before writing new waveform diff --git a/test/model/devices/lasers/test_laser_ni.py b/test/model/devices/lasers/test_laser_ni.py index 333be2832..4fb07e811 100644 --- a/test/model/devices/lasers/test_laser_ni.py +++ b/test/model/devices/lasers/test_laser_ni.py @@ -67,11 +67,11 @@ def test_set_power(self): assert self.laser._current_intensity == self.current_intensity def test_turn_on(self): - self.laser.on_off_type = "digital" + self.laser.digital_port_type = "digital" self.laser.turn_on() self.laser.laser_do_task.write.assert_called_with(True, auto_start=True) - self.laser.on_off_type = "analog" + self.laser.digital_port_type = "analog" self.laser.turn_on() self.laser.laser_do_task.write.assert_called_with( self.laser.laser_max_do, auto_start=True @@ -81,13 +81,13 @@ def test_turn_off(self): self.current_intensity = random.randint(1, 100) self.laser._current_intensity = self.current_intensity - self.laser.on_off_type = "digital" + self.laser.digital_port_type = "digital" self.laser.turn_off() self.laser.laser_do_task.write.assert_called_with(False, auto_start=True) assert self.laser._current_intensity == self.current_intensity - self.laser.on_off_type = "analog" + self.laser.digital_port_type = "analog" self.laser.turn_off() self.laser.laser_do_task.write.assert_called_with( self.laser.laser_min_do, auto_start=True From e3e5cea0c53b43a3fb9480e8a835015ebdc3dd71 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:34:28 -0500 Subject: [PATCH 2/4] Small fix --- .../model/device_startup_functions.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/navigate/model/device_startup_functions.py b/src/navigate/model/device_startup_functions.py index 57571010b..9df4cecfc 100644 --- a/src/navigate/model/device_startup_functions.py +++ b/src/navigate/model/device_startup_functions.py @@ -1151,22 +1151,22 @@ def start_lasers( Trigger class. """ + analog, digital, modulation = None, None, None + if is_synthetic: device_type = "SyntheticLaser" else: analog = configuration["configuration"]["microscopes"][microscope_name][ "lasers" - ][id]["power"]["hardware"]["type"] + ][id]["power"]["hardware"].get("type", None) digital = configuration["configuration"]["microscopes"][microscope_name][ "lasers" - ][id]["onoff"]["hardware"]["type"] + ][id]["onoff"]["hardware"].get("type", None) device_type = analog - print("Analog: ", analog, "Digital: ", digital, "Device Type: ", device_type) - if analog == "NI" or digital == "NI": if device_connection is not None: return device_connection @@ -1179,11 +1179,13 @@ def start_lasers( elif digital == "NI": modulation = "digital" - return LaserNI(microscope_name=microscope_name, - device_connection=device_connection, - configuration=configuration, - laser_id=id, - modulation_type=modulation) + return LaserNI( + microscope_name=microscope_name, + device_connection=device_connection, + configuration=configuration, + laser_id=id, + modulation_type=modulation, + ) elif device_type.lower() == "syntheticlaser" or device_type.lower() == "synthetic": if device_connection is not None: From 028deaeefc1868c1723e9c89dd63acec6311c650 Mon Sep 17 00:00:00 2001 From: Kevin Dean <42547789+AdvancedImagingUTSW@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:07:46 -0500 Subject: [PATCH 3/4] Another small fix. --- test/model/devices/lasers/test_laser_ni.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/model/devices/lasers/test_laser_ni.py b/test/model/devices/lasers/test_laser_ni.py index 4fb07e811..22406c4ef 100644 --- a/test/model/devices/lasers/test_laser_ni.py +++ b/test/model/devices/lasers/test_laser_ni.py @@ -50,6 +50,7 @@ def setUp(self) -> None: device_connection=self.device_connection, configuration=self.configuration, laser_id=laser_id, + modulation_type="mixed", ) def tearDown(self): From 7bdf9a592b9a24cf4d41fb5a91a80fae889de946 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 8 Oct 2024 14:08:36 -0700 Subject: [PATCH 4/4] small tweaks --- src/navigate/model/devices/lasers/ni.py | 36 ++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/navigate/model/devices/lasers/ni.py b/src/navigate/model/devices/lasers/ni.py index 26695d6d7..265f9e108 100644 --- a/src/navigate/model/devices/lasers/ni.py +++ b/src/navigate/model/devices/lasers/ni.py @@ -192,41 +192,41 @@ def set_power(self, laser_intensity): def turn_on(self): """Turns on the laser.""" + # set ao power + self.set_power(self._current_intensity) + if self.laser_do_task is None: return try: - self.set_power(self._current_intensity) - if self.laser_do_task is not None: - if self.digital_port_type == "digital": - self.laser_do_task.write(True, auto_start=True) - elif self.digital_port_type == "analog": - self.laser_do_task.write(self.laser_max_do, auto_start=True) + if self.digital_port_type == "digital": + self.laser_do_task.write(True, auto_start=True) + elif self.digital_port_type == "analog": + self.laser_do_task.write(self.laser_max_do, auto_start=True) except DaqError as e: logger.exception(e) def turn_off(self): """Turns off the laser.""" + # set ao power to zero + tmp = self._current_intensity + self.set_power(0) + self._current_intensity = tmp + if self.laser_do_task is None: return - try: - tmp = self._current_intensity - self.set_power(0) - self._current_intensity = tmp - if self.laser_do_task is not None: - if self.digital_port_type == "digital": - self.laser_do_task.write(False, auto_start=True) - elif self.digital_port_type == "analog": - self.laser_do_task.write(self.laser_min_do, auto_start=True) + if self.digital_port_type == "digital": + self.laser_do_task.write(False, auto_start=True) + elif self.digital_port_type == "analog": + self.laser_do_task.write(self.laser_min_do, auto_start=True) except DaqError as e: logger.exception(e) def close(self): """Close the NI Task before exit.""" - if self.laser_ao_task is None: - return try: - self.laser_ao_task.close() + if self.laser_ao_task is not None: + self.laser_ao_task.close() if self.laser_do_task is not None: self.laser_do_task.close() except DaqError as e: