# Copyright (c) 2021-2025 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. import random import pytest from navigate.model.features.common_features import ZStackAcquisition class TestZStack: @pytest.fixture(autouse=True) def _prepare_test(self, dummy_model_to_test_features): self.model = dummy_model_to_test_features self.model.virtual_microscopes = {} self.config = self.model.configuration["experiment"]["MicroscopeState"] self.record_num = 0 self.feature_list = [[{"name": ZStackAcquisition}]] self.config["start_position"] = 0 self.config["end_position"] = 200 self.config["number_z_steps"] = 5 self.config["step_size"] = ( self.config["end_position"] - self.config["start_position"] ) / self.config["number_z_steps"] position_list = self.model.configuration["multi_positions"] if len(position_list) < 5: for i in range(5): pos = [0] * len(position_list[0]) for i in range(len(position_list[0])): pos[i] = random.randint(1, 10000) position_list.append(pos) def get_next_record(self, record_prefix, idx): idx += 1 while self.model.signal_records[idx][0] != record_prefix: idx += 1 if idx >= self.record_num: assert False, "Some device movements are missed!" return idx def exist_record(self, record_prefix, idx_start, idx_end): for i in range(idx_start, idx_end + 1): if self.model.signal_records[i][0] == record_prefix: return True return False def z_stack_verification(self): self.record_num = len(self.model.signal_records) change_channel_func_str = "active_microscope.prepare_next_channel" close_daq_tasks_str = "active_microscope.daq.stop_acquisition" create_daq_tasks_str = "active_microscope.daq.prepare_acquisition" # save all the selected channels selected_channels = [] for channel_key in self.config["channels"].keys(): if self.config["channels"][channel_key]["is_selected"]: selected_channels.append(dict(self.config["channels"][channel_key])) selected_channels[-1]["id"] = int(channel_key[len("channel_") :]) # restore z and f pos_dict = self.model.get_stage_position() restore_z = pos_dict["z_pos"] restore_f = pos_dict["f_pos"] mode = self.config["stack_cycling_mode"] # per_z/pre_stack is_multiposition = self.config["is_multiposition"] if is_multiposition: positions = self.model.configuration["multi_positions"][1:] else: pos_dict = self.model.configuration["experiment"]["StageParameters"] positions = [ [ pos_dict["x"], pos_dict["y"], self.config.get("stack_z_origin", pos_dict["z"]), pos_dict["theta"], self.config.get("stack_focus_origin", pos_dict["f"]), ] ] z_step = self.config["step_size"] f_step = (self.config["end_focus"] - self.config["start_focus"]) / self.config[ "number_z_steps" ] frame_id = 0 idx = -1 z_moved_times = 0 if mode == "per_z": z_should_move_times = len(positions) * int(self.config["number_z_steps"]) else: z_should_move_times = ( len(selected_channels) * len(positions) * int(self.config["number_z_steps"]) ) has_ni_galvo_stage = self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] prepared_next_channel = False # prepare first channel in pre_signal_func idx = self.get_next_record(change_channel_func_str, idx) prepared_next_channel = True pre_change_channel_idx = idx assert ( self.model.signal_records[idx][2]["__test_frame_id_completed"] == -1 ), "prepare first channel should happen before 0" assert ( self.model.signal_records[idx][2]["__test_frame_id"] == 0 ), "prepare first channel should happen for frame: 0" for i, pos in enumerate(positions): idx = self.get_next_record("move_stage", idx) # x, y, theta pos_moved = self.model.signal_records[idx][1][0] for i, axis in [(0, "x"), (1, "y"), (3, "theta")]: assert pos[i] == pos_moved[axis + "_abs"], ( f"should move to {axis}: {pos[i]}, " f"but moved to {pos_moved[axis + '_abs']}" ) # (x, y, z, theta, f) z_pos = pos[2] + self.config["start_position"] f_pos = pos[4] + self.config["start_focus"] if mode == "per_z": f_pos += selected_channels[0]["defocus"] for j in range(self.config["number_z_steps"]): idx = self.get_next_record("move_stage", idx) pos_moved = self.model.signal_records[idx][1][0] # z, f assert pos_moved["z_abs"] == z_pos + j * z_step, ( f"should move to z: {z_pos + j * z_step}, " f"but moved to {pos_moved['z_abs']}" ) assert pos_moved["f_abs"] == f_pos + j * f_step, ( f"should move to f: {f_pos + j * f_step}, " f"but moved to {pos_moved['f_abs']}" ) z_moved_times += 1 # if the system has NIGalvo stage, should close the DAQ tasks and # then create new tasks to override the new waveforms if has_ni_galvo_stage and prepared_next_channel: idx = self.get_next_record(close_daq_tasks_str, idx) pre_change_channel_idx = idx assert ( self.model.signal_records[idx][2]["__test_frame_id"] == frame_id ), f"close DAQ tasks should happen before {frame_id}" idx = self.get_next_record(create_daq_tasks_str, idx) pre_change_channel_idx = idx assert ( self.model.signal_records[idx][2]["__test_frame_id"] == frame_id ), f"create DAQ tasks should happen before {frame_id}" # channel for k in range(len(selected_channels)): idx = self.get_next_record(change_channel_func_str, idx) prepared_next_channel = True assert ( self.model.signal_records[idx][2]["__test_frame_id"] == frame_id ), ( "prepare next channel (change channel) " f"should happen after {frame_id}" ) assert ( self.model.signal_records[idx][2][ "__test_frame_id_completed" ] == self.model.signal_records[idx][2]["__test_frame_id"] ), ( "prepare next channel (change channel) " "should happen inside signal_end_func()" ) assert ( self.exist_record( change_channel_func_str, pre_change_channel_idx + 1, idx - 1, ) is False ), ( "prepare next channel (change channel) " "should not happen more than once" ) pre_change_channel_idx = idx frame_id += 1 else: # per_stack for k in range(len(selected_channels)): # z f_pos += selected_channels[k]["defocus"] for j in range(self.config["number_z_steps"]): idx = self.get_next_record("move_stage", idx) pos_moved = self.model.signal_records[idx][1][0] # z, f assert pos_moved["z_abs"] == z_pos + j * z_step, ( f"should move to z: {z_pos + j * z_step}, " f"but moved to {pos_moved['z_abs']}" ) assert pos_moved["f_abs"] == f_pos + j * f_step, ( f"should move to f: {f_pos + j * f_step}, " f"but moved to {pos_moved['f_abs']}" ) z_moved_times += 1 frame_id += 1 f_pos -= selected_channels[k]["defocus"] idx = self.get_next_record(change_channel_func_str, idx) prepared_next_channel = True assert ( self.model.signal_records[idx][2]["__test_frame_id"] == frame_id - 1 ), ( "prepare next channel (change channel) " f"should happen at {frame_id - 1}" ) # restore z, f idx = self.get_next_record("move_stage", idx) pos_moved = self.model.signal_records[idx][1][0] assert ( pos_moved["z_abs"] == restore_z ), f"should restore z to {restore_z}, but moved to {pos_moved['z_abs']}" assert ( pos_moved["f_abs"] == restore_f ), f"should restore f to {restore_f}, but moved to {pos_moved['f_abs']}" assert z_moved_times == z_should_move_times, ( f"should verify all the stage movements! {z_moved_times} -- " f"{z_should_move_times}" ) @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_single_position_one_channel_per_z(self, has_ni_galvo_stage): # single position self.config["is_multiposition"] = False self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 1 channel per_z self.config["stack_cycling_mode"] = "per_z" self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False self.model.start(self.feature_list) print(self.model.signal_records) self.z_stack_verification() @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_single_position_one_channel_per_stack(self, has_ni_galvo_stage): # single position self.config["is_multiposition"] = False self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 1 channel per_stack self.config["stack_cycling_mode"] = "per_stack" self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False self.model.start(self.feature_list) self.z_stack_verification() @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_single_position_two_channels_per_z(self, has_ni_galvo_stage): # single position self.config["is_multiposition"] = False self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 2 channels per_z self.config["stack_cycling_mode"] = "per_z" for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False self.model.start(self.feature_list) self.z_stack_verification() @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_single_position_two_channels_per_stack(self, has_ni_galvo_stage): # single position self.config["is_multiposition"] = False self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 2 channels per_stack self.config["stack_cycling_mode"] = "per_stack" for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False self.model.start(self.feature_list) self.z_stack_verification() @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_single_position_three_channels_per_stack(self, has_ni_galvo_stage): # single position self.config["is_multiposition"] = False self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_stack self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True self.config["stack_cycling_mode"] = "per_stack" self.model.start(self.feature_list) self.z_stack_verification() @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_single_position_three_channels_per_z(self, has_ni_galvo_stage): # single position self.config["is_multiposition"] = False self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_z self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True self.config["stack_cycling_mode"] = "per_z" self.model.start(self.feature_list) self.z_stack_verification() @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_multi_position_one_channel_per_z(self, has_ni_galvo_stage): # multi position self.config["is_multiposition"] = True self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 1 channel per_z self.config["stack_cycling_mode"] = "per_z" self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False self.model.start(self.feature_list) self.z_stack_verification() self.config["is_multiposition"] = False @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_multi_position_one_channel_per_stack(self, has_ni_galvo_stage): self.config["is_multiposition"] = True self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 1 channel per_stack self.config["stack_cycling_mode"] = "per_stack" self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False self.model.start(self.feature_list) self.z_stack_verification() self.config["is_multiposition"] = False @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_multi_position_two_channels_per_z(self, has_ni_galvo_stage): self.config["is_multiposition"] = True self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 2 channels per_z self.config["stack_cycling_mode"] = "per_z" for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False self.model.start(self.feature_list) self.z_stack_verification() self.config["is_multiposition"] = False @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_multi_position_two_channels_per_stack(self, has_ni_galvo_stage): self.config["is_multiposition"] = True self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 2 channels per_stack self.config["stack_cycling_mode"] = "per_stack" for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True self.config["channels"]["channel_" + str(i + 1)]["is_selected"] = False self.model.start(self.feature_list) self.z_stack_verification() self.config["is_multiposition"] = False @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_multi_position_three_channels_per_stack(self, has_ni_galvo_stage): self.config["is_multiposition"] = True self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_stack self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True self.config["stack_cycling_mode"] = "per_stack" self.model.start(self.feature_list) self.z_stack_verification() self.config["is_multiposition"] = False @pytest.mark.parametrize("has_ni_galvo_stage", [False]) def test_multi_position_three_channels_per_z(self, has_ni_galvo_stage): self.config["is_multiposition"] = True self.model.configuration["configuration"]["microscopes"][ self.config["microscope_name"] ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_z self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True self.config["stack_cycling_mode"] = "per_z" self.model.start(self.feature_list) self.z_stack_verification() self.config["is_multiposition"] = False