diff --git a/docs/source/contributing/feature_container.rst b/docs/source/contributing/feature_container.rst index 667bdd870..ea25be7fe 100644 --- a/docs/source/contributing/feature_container.rst +++ b/docs/source/contributing/feature_container.rst @@ -253,7 +253,7 @@ To create a customized feature list, follow these steps: {"name": PrepareNextChannel}, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.selected_channels",), + "args": ("channels",), }, ) ] diff --git a/docs/source/user_guide/acquiring_guide.rst b/docs/source/user_guide/acquiring_guide.rst index 163cd53fb..73dddcc71 100644 --- a/docs/source/user_guide/acquiring_guide.rst +++ b/docs/source/user_guide/acquiring_guide.rst @@ -42,7 +42,7 @@ a :doc:`feature list `, shown in its {"name": PrepareNextChannel}, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.selected_channels",), + "args": ("channels",), }, ) ] diff --git a/docs/source/user_guide/features/example_feature_lists.rst b/docs/source/user_guide/features/example_feature_lists.rst index ec6bc4302..39152cd2e 100644 --- a/docs/source/user_guide/features/example_feature_lists.rst +++ b/docs/source/user_guide/features/example_feature_lists.rst @@ -30,7 +30,7 @@ prior to moving to the next position in the multi-position table. {"name": CalculateFocusRange,}, {"name": ZStackAcquisition,"args": (True,True,"z-stack",),}, {"name": WaitToContinue,}, - {"name": LoopByCount,"args": ("experiment.MicroscopeState.multiposition_count",),}, + {"name": LoopByCount,"args": ("positions",),}, ), ] diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index e71c8494b..bbd7bb30e 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -108,6 +108,7 @@ def get_configuration_paths(): "rest_api_config.yml", "waveform_templates.yml", "gui_configuration.yml", + "multi_positions.yml" ] base_directory = Path(__file__).resolve().parent @@ -520,8 +521,6 @@ def verify_experiment_config(manager, configuration): "timepoint_interval": 0, "experiment_duration": 1.03, "is_multiposition": False, - "multiposition_count": 1, - "selected_channels": 0, "stack_z_origin": 0, "stack_focus_origin": 0, "start_focus": 0.0, @@ -654,31 +653,6 @@ def verify_experiment_config(manager, configuration): if channel_value[k] < 0: channel_value[k] = temp[k] - microscope_setting_dict["selected_channels"] = selected_channel_num - - # MultiPositions - if ( - "MultiPositions" not in configuration["experiment"] - or type(configuration["experiment"]["MultiPositions"]) is not ListProxy - ): - update_config_dict(manager, configuration["experiment"], "MultiPositions", []) - position_ids = [] - multipositions = configuration["experiment"]["MultiPositions"] - for i, position in enumerate(multipositions): - try: - for j in range(5): - float(position[j]) - except (ValueError, KeyError): - position_ids.append(i) - - for idx in position_ids[::-1]: - del multipositions[idx] - if len(multipositions) < 1: - multipositions.append([10.0, 10.0, 10.0, 10.0, 10.0]) - - microscope_setting_dict["multiposition_count"] = len(multipositions) - - def verify_waveform_constants(manager, configuration): """Verifies and updates the waveform constants in the configuration dictionary. @@ -1119,3 +1093,18 @@ def verify_configuration(manager, configuration): "gui", {"channels": {"count": channel_count}}, ) + +def verify_positions_config(positions): + if positions is None or type(positions) not in (list, ListProxy): + return [] + # MultiPositions + position_num = len(positions) + for i in range(position_num-1, -1, -1): + position = positions[i] + try: + for j in range(5): + float(position[j]) + except (ValueError, KeyError, IndexError): + del positions[i] + + return positions diff --git a/src/navigate/config/experiment.yml b/src/navigate/config/experiment.yml index 03dd4f84e..92ff0accd 100644 --- a/src/navigate/config/experiment.yml +++ b/src/navigate/config/experiment.yml @@ -54,8 +54,6 @@ MicroscopeState: timepoint_interval: 0 experiment_duration: 600.0899999999995 is_multiposition: False - multiposition_count: 2 - selected_channels: 3 channels: {'channel_1': {'is_selected': True, 'laser': '488nm', 'laser_index': 0, 'filter': 'GFP - FF01-515/30-32', 'filter_position': 1, 'camera_exposure_time': 200.0, 'laser_power': '20', 'interval_time': '1', 'defocus': 100.0}, 'channel_2': {'is_selected': True, 'laser': '562nm', 'laser_index': 1, 'filter': 'RFP - FF01-595/31-32', 'filter_position': 2, 'camera_exposure_time': 200.0, 'laser_power': '20', 'interval_time': '1', 'defocus': 200.0}, 'channel_3': {'is_selected': True, 'laser': '642nm', 'laser_index': 2, 'filter': 'Far-Red - BLP01-647R/31-32', 'filter_position': 3, 'camera_exposure_time': 200.0, 'laser_power': '20', 'interval_time': '1', 'defocus': 0.0}} stack_z_origin: 0.0 stack_focus_origin: 0.0 diff --git a/src/navigate/config/multi_positions.yml b/src/navigate/config/multi_positions.yml new file mode 100644 index 000000000..59f58d0ff --- /dev/null +++ b/src/navigate/config/multi_positions.yml @@ -0,0 +1 @@ +[[0, 0, 0, 0, 0]] \ No newline at end of file diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index f540605d7..1137ff76c 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -80,10 +80,16 @@ update_config_dict, verify_experiment_config, verify_waveform_constants, + verify_positions_config, verify_configuration, get_navigate_path, ) -from navigate.tools.file_functions import create_save_path, save_yaml_file, get_ram_info +from navigate.tools.file_functions import ( + create_save_path, + load_yaml_file, + save_yaml_file, + get_ram_info, +) from navigate.tools.common_dict_tools import update_stage_dict from navigate.tools.multipos_table_tools import update_table from navigate.tools.common_functions import combine_funcs @@ -108,6 +114,7 @@ def __init__( rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, args, ): """Initialize the Navigate Controller. @@ -198,6 +205,10 @@ def __init__( verify_experiment_config(self.manager, self.configuration) verify_waveform_constants(self.manager, self.configuration) + positions = load_yaml_file(multi_positions_path) + positions = verify_positions_config(positions) + self.configuration["multi_positions"] = positions + total_ram, available_ram = get_ram_info() logger.info( f"Total RAM: {total_ram / 1024**3:.2f} GB. " @@ -460,7 +471,7 @@ def populate_experiment_setting(self, file_name=None, in_initialize=False): self.acquire_bar_controller.populate_experiment_values() # self.stage_controller.populate_experiment_values() self.multiposition_tab_controller.set_positions( - self.configuration["experiment"]["MultiPositions"] + self.configuration["multi_positions"] ) self.channels_tab_controller.populate_experiment_values() self.camera_setting_controller.populate_experiment_values() @@ -510,10 +521,7 @@ def update_experiment_setting(self): # update multi-positions positions = self.multiposition_tab_controller.get_positions() - self.configuration["experiment"]["MultiPositions"] = positions - self.configuration["experiment"]["MicroscopeState"][ - "multiposition_count" - ] = len(positions) + self.configuration["multi_positions"] = positions if ( self.configuration["experiment"]["MicroscopeState"]["is_multiposition"] @@ -848,6 +856,13 @@ def execute(self, command, *args): filename="waveform_constants.yml", ) + # Save multi_positions.yml file + save_yaml_file( + file_directory=file_directory, + content_dict=self.configuration["multi_positions"], + filename="multi_positions.yml", + ) + self.camera_setting_controller.solvent = self.configuration["experiment"][ "Saving" ]["solvent"] @@ -952,6 +967,12 @@ def execute(self, command, *args): content_dict=self.configuration["experiment"], filename="experiment.yml", ) + save_yaml_file( + file_directory=file_directory, + content_dict=self.configuration["multi_positions"], + filename="multi_positions.yml", + ) + if hasattr(self, "waveform_popup_controller"): self.waveform_popup_controller.save_waveform_constants() diff --git a/src/navigate/controller/sub_controllers/acquire_bar.py b/src/navigate/controller/sub_controllers/acquire_bar.py index d8b6bf568..e9425ecf7 100644 --- a/src/navigate/controller/sub_controllers/acquire_bar.py +++ b/src/navigate/controller/sub_controllers/acquire_bar.py @@ -147,7 +147,7 @@ def progress_bar( number_of_positions = 1 else: number_of_positions = len( - self.parent_controller.configuration["experiment"]["MultiPositions"] + self.parent_controller.configuration["multi_positions"] ) if mode == "single": diff --git a/src/navigate/controller/sub_controllers/camera_view.py b/src/navigate/controller/sub_controllers/camera_view.py index 7ae074b58..4afa9a764 100644 --- a/src/navigate/controller/sub_controllers/camera_view.py +++ b/src/navigate/controller/sub_controllers/camera_view.py @@ -526,8 +526,8 @@ def initialize_non_live_display(self, microscope_state, camera_parameters): self.slice_index = 0 self.image_mode = microscope_state["image_mode"] self.stack_cycling_mode = microscope_state["stack_cycling_mode"] - self.number_of_channels = int(microscope_state["selected_channels"]) self.get_selected_channels(microscope_state) + self.number_of_channels = len(self.selected_channels) self.number_of_slices = int(microscope_state["number_z_steps"]) self.total_images_per_volume = self.number_of_channels * self.number_of_slices self.original_image_width = int(camera_parameters["img_x_pixels"]) @@ -1169,8 +1169,7 @@ def slider_update(self, *_): slider_index = self.view.slider.get() channel_index = self.view.live_frame.channel.get() - channel_index = channel_index[-1] - channel_index = int(channel_index) - 1 + channel_index = self.selected_channels.index(channel_index) image = self.spooled_images.load_image( channel=channel_index, slice_index=slider_index ) diff --git a/src/navigate/controller/sub_controllers/channels_tab.py b/src/navigate/controller/sub_controllers/channels_tab.py index 30b7ac962..c24daf907 100644 --- a/src/navigate/controller/sub_controllers/channels_tab.py +++ b/src/navigate/controller/sub_controllers/channels_tab.py @@ -32,7 +32,6 @@ # Standard Library Imports import logging -from functools import reduce import datetime # Third Party Imports @@ -773,12 +772,6 @@ def execute(self, command, *args): Returns parent_controller.execute(command) if command = 'get_stage_position' """ if command == "recalculate_timepoint": - # update selected channels num - self.microscope_state_dict["selected_channels"] = reduce( - lambda count, channel: count + (channel["is_selected"] is True), - self.microscope_state_dict["channels"].values(), - 0, - ) self.update_timepoint_setting() # update framerate info in camera setting tab exposure_time = max( diff --git a/src/navigate/controller/sub_controllers/multiposition.py b/src/navigate/controller/sub_controllers/multiposition.py index 1671425c2..016cf2dd8 100644 --- a/src/navigate/controller/sub_controllers/multiposition.py +++ b/src/navigate/controller/sub_controllers/multiposition.py @@ -100,7 +100,12 @@ def set_positions(self, positions): """ axis_dict = {"x": "X", "y": "Y", "z": "Z", "theta": "R", "f": "F"} data = {} - + if len(positions) == 0: + # add current stage position to the table + stage_position = self.parent_controller.configuration["experiment"][ + "StageParameters" + ] + positions = [[stage_position[axis] for axis in axis_dict.keys()]] for i, name in enumerate(axis_dict.keys()): data[axis_dict[name]] = list(pos[i] for pos in positions) self.table.model.df = pd.DataFrame(data) diff --git a/src/navigate/main.py b/src/navigate/main.py index 5d933308e..73aa8b0dc 100644 --- a/src/navigate/main.py +++ b/src/navigate/main.py @@ -103,6 +103,7 @@ def main(): logging_path, configurator, gui_configuration_path, + multi_positions_path, ) = evaluate_parser_input_arguments(args) log_setup("logging.yml", logging_path) @@ -119,6 +120,7 @@ def main(): rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, args, ) diff --git a/src/navigate/model/features/common_features.py b/src/navigate/model/features/common_features.py index b35a2381a..d8b638ac9 100644 --- a/src/navigate/model/features/common_features.py +++ b/src/navigate/model/features/common_features.py @@ -36,6 +36,7 @@ from functools import reduce from threading import Lock import logging +from multiprocessing.managers import ListProxy # Third party imports @@ -183,7 +184,16 @@ def __init__(self, model, saving_flag=False): self.saving_flag = saving_flag #: dict: A dictionary defining the configuration for the data capture process. - self.config_table = {"data": {"main": self.data_func}} + self.config_table = { + "signal": {"main": self.signal_func}, + "data": {"main": self.data_func}, + } + + def signal_func(self) -> bool: + """Mark saving flags in the signal function""" + if self.saving_flag: + self.model.mark_saving_flags([self.model.frame_id]) + return True def data_func(self, frame_ids: list) -> bool: """Capture data frames and log camera information. @@ -201,8 +211,6 @@ def data_func(self, frame_ids: list) -> bool: bool A boolean value indicating the success of the data capture process. """ - if self.saving_flag: - self.model.mark_saving_flags(frame_ids) logger.info(f"the camera is:{self.model.active_microscope_name}, {frame_ids}") return True @@ -211,7 +219,7 @@ class WaitForExternalTrigger: """WaitForExternalTrigger class to time features using external input. This class waits for either an external trigger (or the timeout) before continuing - on to the next feature block in the list. Useful when combined with LoopByCounts + on to the next feature block in the list. Useful when combined with LoopByCount when each iteration may depend on some external event happening. Notes: @@ -451,31 +459,50 @@ def __init__(self, model, steps=1): self.model = model #: bool: A boolean value indicating whether to step by frame or by step. - self.step_by_frame = True + self.step_by_frame = False if type(steps) is str else True #: int: The remaining number of steps or frames. self.steps = steps - if type(steps) is str: - self.step_by_frame = False - try: - parameters = steps.split(".") - config_ref = reduce((lambda pre, n: f"{pre}['{n}']"), parameters, "") - exec(f"self.steps = int(self.model.configuration{config_ref})") - except: # noqa - self.steps = 1 #: int: The remaining number of steps. - self.signals = self.steps + self.signals = 1 #: int: The remaining number of frames. - self.data_frames = self.steps + self.data_frames = 1 + + #: bool: Initialization flag + self.initialized = VariableWithLock(bool) + self.initialized.value = False #: dict: A dictionary defining the configuration for the loop control process. self.config_table = { - "signal": {"main": self.signal_func}, - "data": {"main": self.data_func}, + "signal": { + "init": self.pre_func, + "main": self.signal_func, + }, + "data": { + "init": self.pre_func, + "main": self.data_func + }, } + def pre_func(self): + """Initialize loop parameters""" + if self.initialized.value: + return + + with self.initialized as initialized: + if initialized.value: + return + + self.get_steps() + + self.signals = self.steps + self.data_frames = self.steps + initialized.value = True + + logger.debug(f"LoopByCount-initialize: {self.signals}, {self.data_frames}") + def signal_func(self): """Control the signal acquisition loop and update the remaining steps. @@ -520,6 +547,34 @@ def data_func(self, frame_ids): return False return True + def get_steps(self): + """Get number of steps + + Returns: + -------- + int + Number of steps. + """ + if type(self.steps) is int: + return self.steps + if self.steps == "channels": + self.steps = len(self.model.active_microscope.available_channels) + elif self.steps == "positions": + self.steps = len(self.model.configuration["multi_positions"]) + else: + try: + parameters = self.steps.split(".") + config_ref = reduce((lambda pre, n: f"{pre}['{n}']"), parameters, "") + exec(f"self.steps = self.model.configuration{config_ref}") + except: # noqa + self.steps = 1 + + if type(self.steps) in [list, ListProxy]: + self.steps = len(self.steps) + else: + self.steps = int(self.steps) + return self.steps + class PrepareNextChannel: """PrepareNextChannel class for preparing microscopes for the next imaging channel. @@ -635,14 +690,10 @@ def __init__(self, model, resolution_value=None, zoom_value=None, offset=None): self.current_idx = 0 #: dict: A dictionary defining the configuration for the position control - self.multiposition_table = self.model.configuration["experiment"][ - "MultiPositions" - ] + self.multiposition_table = [] #: int: The total number of positions in the multi-position table. - self.position_count = self.model.configuration["experiment"]["MicroscopeState"][ - "multiposition_count" - ] + self.position_count = 0 #: int: The stage distance threshold for pausing the data thread. self.stage_distance_threshold = 1000 @@ -661,6 +712,8 @@ def pre_signal_func(self): if self.initialized: return self.initialized = True + self.multiposition_table = self.model.configuration["multi_positions"] + self.position_count = len(self.multiposition_table) if type(self.offset) is str: try: self.offset = ast.literal_eval(self.offset) @@ -1011,7 +1064,14 @@ def pre_signal_func(self): self.stack_cycling_mode = microscope_state["stack_cycling_mode"] # get available channels - self.channels = microscope_state["selected_channels"] + self.channels = len( + list( + filter( + lambda channel: channel["is_selected"], + microscope_state["channels"].values(), + ) + ) + ) #: int: The current channel being acquired in the z-stack self.current_channel_in_list = 0 @@ -1041,7 +1101,7 @@ def pre_signal_func(self): # position: x, y, z, theta, f if bool(microscope_state["is_multiposition"]): - self.positions = self.model.configuration["experiment"]["MultiPositions"] + self.positions = self.model.configuration["multi_positions"] else: self.positions = [ [ diff --git a/src/navigate/model/metadata_sources/metadata.py b/src/navigate/model/metadata_sources/metadata.py index aa7d4e1d7..d47280a01 100644 --- a/src/navigate/model/metadata_sources/metadata.py +++ b/src/navigate/model/metadata_sources/metadata.py @@ -195,7 +195,7 @@ def set_shape_from_configuration_experiment(self) -> None: self._multiposition = state["is_multiposition"] if bool(self._multiposition): - self.positions = len(self.configuration["experiment"]["MultiPositions"]) + self.positions = len(self.configuration["multi_positions"]) else: self.positions = 1 @@ -205,7 +205,7 @@ def set_shape_from_configuration_experiment(self) -> None: # let the data sources have the ability to save more frames # self._multiposition = True # self.positions = len( - # self.configuration["experiment"]["MultiPositions"] + # self.configuration["multi_positions"] # ) # Allow additional axes (e.g. f) to couple onto existing axes (e.g. z) diff --git a/src/navigate/model/model.py b/src/navigate/model/model.py index 6cdbcc4f0..6ba16c469 100644 --- a/src/navigate/model/model.py +++ b/src/navigate/model/model.py @@ -309,7 +309,7 @@ def __init__( {"name": PrepareNextChannel}, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.selected_channels",), + "args": ("channels",), }, ), { @@ -338,7 +338,7 @@ def __init__( {"name": WaitToContinue}, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.multiposition_count",), + "args": ("positions",), }, ), ] @@ -361,7 +361,7 @@ def __init__( }, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.multiposition_count",), + "args": ("positions",), }, ), {"name": RemoveEmptyPositions, "args": (records,)}, @@ -374,7 +374,7 @@ def __init__( {"name": PrepareNextChannel}, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.selected_channels",), + "args": ("channels",), }, ) ], @@ -383,7 +383,7 @@ def __init__( {"name": PrepareNextChannel}, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.selected_channels",), + "args": ("channels",), }, ) ], @@ -552,6 +552,10 @@ def run_command( self.is_save = self.configuration["experiment"]["MicroscopeState"][ "is_save" ] + if len(self.configuration["multi_positions"]) == 0: + self.configuration["experiment"]["MicroscopeState"][ + "is_multiposition" + ] = False # Calculate waveforms, turn on lasers, etc. self.prepare_acquisition() @@ -1187,7 +1191,7 @@ def reset_feature_list(self) -> None: {"name": PrepareNextChannel}, { "name": LoopByCount, - "args": ("experiment.MicroscopeState.selected_channels",), + "args": ("channels",), }, ) ], diff --git a/src/navigate/tools/main_functions.py b/src/navigate/tools/main_functions.py index 393043e48..701503ebf 100644 --- a/src/navigate/tools/main_functions.py +++ b/src/navigate/tools/main_functions.py @@ -72,6 +72,8 @@ def evaluate_parser_input_arguments(args): True if configurator is enabled gui_configuration_path Path to gui_configuration file + multi_positions_path + Path to multi_positions file """ # Retrieve the Default Configuration paths ( @@ -81,6 +83,7 @@ def evaluate_parser_input_arguments(args): rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, ) = get_configuration_paths() # Evaluate Input Arguments @@ -125,7 +128,13 @@ def evaluate_parser_input_arguments(args): assert ( args.gui_config_file.exists() ), "gui_configuration Path {} not valid".format(args.gui_config_file) - gui_configuration_path = args.gui_config + gui_configuration_path = args.gui_config_file + + if args.multi_positions_file: + assert ( + args.multi_positions_file.exists() + ), "multi_positions Path {} not valid".format(args.multi_positions_file) + multi_positions_path = args.multi_positions_file # Creating Loggers etc., they exist globally so no need to pass if args.logging_config: @@ -145,6 +154,7 @@ def evaluate_parser_input_arguments(args): logging_path, configurator, gui_configuration_path, + multi_positions_path, ) @@ -257,4 +267,13 @@ def create_parser(): "This file specifies how the logging will be performed.", ) + input_args.add_argument( + "--multi-positions-file", + type=Path, + required=False, + default=None, + help="Non-default path to the multi_positions.yml file. \n" + "This file contains a list of stage positions.", + ) + return parser diff --git a/test/config/test_config.py b/test/config/test_config.py index 851aeba74..02219c97d 100644 --- a/test/config/test_config.py +++ b/test/config/test_config.py @@ -77,6 +77,7 @@ def test_config_methods(): "update_config_dict", "verify_experiment_config", "verify_waveform_constants", + "verify_positions_config", "verify_configuration", "yaml", "logging", @@ -122,7 +123,7 @@ def test_get_configuration_paths(): paths = config.get_configuration_paths() for path in paths: assert isinstance(path, pathlib.Path) - assert len(paths) == 6 + assert len(paths) == 7 def test_get_configuration_paths_create_dir(monkeypatch): @@ -348,8 +349,6 @@ def setUp(self): "timepoint_interval": 0, "experiment_duration": 1.03, "is_multiposition": False, - "multiposition_count": 1, - "selected_channels": 0, "stack_z_origin": 0, "stack_focus_origin": 0, "start_focus": 0.0, @@ -366,7 +365,6 @@ def setUp(self): "CameraParameters": camera_parameters_dict_sample, "StageParameters": stage_parameters_dict_sample, "MicroscopeState": microscope_parameters_dict_sample, - "MultiPositions": multipositions_sample, } def tearDown(self): @@ -420,9 +418,9 @@ def test_load_empty_experiment_file(self): experiement_config["MicroscopeState"], ) - # MultiPositions - for i, position in enumerate(self.experiment_sample["MultiPositions"]): - assert position == experiement_config["MultiPositions"][i] + # # MultiPositions + # for i, position in enumerate(self.experiment_sample["MultiPositions"]): + # assert position == experiement_config["MultiPositions"][i] def test_load_experiment_file_with_missing_parameters(self): experiment = load_yaml_file(os.path.join(self.config_path, "experiment.yml")) @@ -784,12 +782,6 @@ def test_load_experiment_file_with_wrong_parameter_values(self): == expected_value[k] ) - # selected_channels - assert experiment["MicroscopeState"]["selected_channels"] == 0 - experiment["MicroscopeState"]["channels"]["channel_2"]["is_selected"] = True - config.verify_experiment_config(self.manager, configuration) - assert experiment["MicroscopeState"]["selected_channels"] == 1 - def select_random_entries_from_list(self, parameter_list): n = random.randint(1, len(parameter_list)) return random.choices(parameter_list, k=n) @@ -801,3 +793,31 @@ def delete_random_entries_from_dict(self, parameter_list, parameter_dict): if k in parameter_dict.keys(): del parameter_dict[k] return deleted_parameters + + def test_load_empty_multi_positions(self): + positions_file_path = os.path.join(self.test_root, "multi_positions.yml") + with open(positions_file_path, "w") as f: + f.write("") + positions = load_yaml_file(positions_file_path) + new_positions = config.verify_positions_config(positions) + assert isinstance(new_positions, list) + assert len(new_positions) == 0 + + def test_load_multi_positions_with_corrupted_values(self): + positions = [[1, 2, 3], ['a', 'b', 'c', 1, 2], [10, 'a', 30, 40,]] + new_positions = config.verify_positions_config(positions) + assert isinstance(new_positions, list) + assert len(new_positions) == 0 + + + positions = [[1, 2, 3], ['a', 'b', 'c', 1, 2], [1, 2, 3, 4, 5], [10, 'a', 30, 40,]] + new_positions = config.verify_positions_config(positions) + assert isinstance(new_positions, list) + assert len(new_positions) == 1 + + positions = [[1, 2, 3], ['a', 'b', 'c', 1, 2], [1, 2, 3, 4, 5], [10, 'a', 30, 40,], + [1, 2, 3, 4, 5, 6]] + new_positions = config.verify_positions_config(positions) + assert isinstance(new_positions, list) + assert len(new_positions) == 2 + diff --git a/test/config/test_experiment.py b/test/config/test_experiment.py index fbb02be32..04dfc6f47 100644 --- a/test/config/test_experiment.py +++ b/test/config/test_experiment.py @@ -164,8 +164,6 @@ def test_microscope_state(self): "timepoint_interval": int, "experiment_duration": float, "is_multiposition": bool, - "multiposition_count": int, - "selected_channels": int, "channels": dict, "stack_z_origin": float, "stack_focus_origin": float, @@ -203,12 +201,3 @@ def test_microscope_state(self): expected_values[key_key], ), f"{key_key} is not of type {expected_values[key_key]}" - def test_multiposition(self): - expected_values = [float, float, float, float, float] - positions = self.data["MultiPositions"] - print("*** positions:", positions) - for position in positions: - for i in range(len(expected_values)): - assert isinstance( - position[i], expected_values[i] - ), f"{i} is not of type {expected_values[i]}" diff --git a/test/controller/sub_controllers/test_camera_view.py b/test/controller/sub_controllers/test_camera_view.py index dce8bc4b1..37bfc7c39 100644 --- a/test/controller/sub_controllers/test_camera_view.py +++ b/test/controller/sub_controllers/test_camera_view.py @@ -92,7 +92,6 @@ def setup_class(self, dummy_controller): }, "number_z_steps": np.random.randint(10, 100), "stack_cycling_mode": "per_stack", - "selected_channels": 3, "image_mode": "z-stack", } diff --git a/test/controller/test_controller.py b/test/controller/test_controller.py index a4b154567..e9245bb43 100644 --- a/test/controller/test_controller.py +++ b/test/controller/test_controller.py @@ -31,6 +31,9 @@ def controller(tk_root): gui_configuration_path = Path.joinpath( configuration_directory, "gui_configuration.yml" ) + multi_positions_path = Path.joinpath( + configuration_directory, "multi_positions.yml" + ) args = SimpleNamespace(synthetic_hardware=True) controller = Controller( @@ -42,6 +45,7 @@ def controller(tk_root): rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, args, ) # To make sure the testcases won't hang on because of the model.event_queue diff --git a/test/model/devices/galvo/test_galvo_base.py b/test/model/devices/galvo/test_galvo_base.py index 1a9e3bd12..574dd4539 100644 --- a/test/model/devices/galvo/test_galvo_base.py +++ b/test/model/devices/galvo/test_galvo_base.py @@ -55,6 +55,7 @@ def setUp(self) -> None: rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, ) = get_configuration_paths() self.configuration = load_configs( diff --git a/test/model/devices/galvo/test_galvo_ni.py b/test/model/devices/galvo/test_galvo_ni.py index f1c263b30..4d43cebf6 100644 --- a/test/model/devices/galvo/test_galvo_ni.py +++ b/test/model/devices/galvo/test_galvo_ni.py @@ -56,6 +56,7 @@ def setUp(self) -> None: rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, ) = get_configuration_paths() self.configuration = load_configs( diff --git a/test/model/devices/galvo/test_galvo_synthetic.py b/test/model/devices/galvo/test_galvo_synthetic.py index 08eac5a25..5e9fde1f6 100644 --- a/test/model/devices/galvo/test_galvo_synthetic.py +++ b/test/model/devices/galvo/test_galvo_synthetic.py @@ -53,6 +53,7 @@ def setUp(self) -> None: rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, ) = get_configuration_paths() self.configuration = load_configs( diff --git a/test/model/devices/lasers/test_laser_ni.py b/test/model/devices/lasers/test_laser_ni.py index 22406c4ef..a420f2708 100644 --- a/test/model/devices/lasers/test_laser_ni.py +++ b/test/model/devices/lasers/test_laser_ni.py @@ -24,6 +24,7 @@ def setUp(self) -> None: rest_api_path, waveform_templates_path, gui_configuration_path, + multi_positions_path, ) = get_configuration_paths() self.configuration = load_configs( diff --git a/test/model/dummy.py b/test/model/dummy.py index 032c7edb3..8fe8d5146 100644 --- a/test/model/dummy.py +++ b/test/model/dummy.py @@ -45,6 +45,7 @@ verify_experiment_config, verify_waveform_constants, verify_configuration, + verify_positions_config, ) from navigate.model.devices.camera.synthetic import ( SyntheticCamera, @@ -53,6 +54,7 @@ from navigate.model.features.feature_container import ( load_features, ) +from navigate.tools.file_functions import load_yaml_file class DummyController: @@ -189,6 +191,9 @@ def __init__(self): gui_configuration_path = Path.joinpath( configuration_directory, "gui_configuration.yml" ) + multi_positions_path = Path.joinpath( + configuration_directory, "multi_positions.yml" + ) #: Manager: The manager. self.manager = Manager() @@ -205,6 +210,10 @@ def __init__(self): verify_experiment_config(self.manager, self.configuration) verify_waveform_constants(self.manager, self.configuration) + positions = load_yaml_file(multi_positions_path) + positions = verify_positions_config(positions) + self.configuration["multi_positions"] = positions + #: DummyDevice: The device. self.device = DummyDevice() #: Pipe: The pipe for sending signals. diff --git a/test/model/features/test_common_features.py b/test/model/features/test_common_features.py index 3b5ae2097..7f1371e49 100644 --- a/test/model/features/test_common_features.py +++ b/test/model/features/test_common_features.py @@ -52,7 +52,7 @@ def _prepare_test(self, dummy_model_to_test_features): self.config["end_position"] - self.config["start_position"] ) / self.config["number_z_steps"] - position_list = self.model.configuration["experiment"]["MultiPositions"] + position_list = self.model.configuration["multi_positions"] if len(position_list) < 5: for i in range(5): pos = [0] * 5 @@ -94,7 +94,7 @@ def z_stack_verification(self): mode = self.config["stack_cycling_mode"] # per_z/pre_stack is_multiposition = self.config["is_multiposition"] if is_multiposition: - positions = self.model.configuration["experiment"]["MultiPositions"] + positions = self.model.configuration["multi_positions"] else: pos_dict = self.model.configuration["experiment"]["StageParameters"] positions = [ @@ -281,7 +281,6 @@ def test_single_position_one_channel_per_z(self, has_ni_galvo_stage): # 1 channel per_z self.config["stack_cycling_mode"] = "per_z" - self.config["selected_channels"] = 1 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False @@ -299,7 +298,6 @@ def test_single_position_one_channel_per_stack(self, has_ni_galvo_stage): # 1 channel per_stack self.config["stack_cycling_mode"] = "per_stack" - self.config["selected_channels"] = 1 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False @@ -316,7 +314,6 @@ def test_single_position_two_channels_per_z(self, has_ni_galvo_stage): # 2 channels per_z self.config["stack_cycling_mode"] = "per_z" - self.config["selected_channels"] = 2 for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True @@ -334,7 +331,6 @@ def test_single_position_two_channels_per_stack(self, has_ni_galvo_stage): # 2 channels per_stack self.config["stack_cycling_mode"] = "per_stack" - self.config["selected_channels"] = 2 for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True @@ -351,7 +347,6 @@ def test_single_position_three_channels_per_stack(self, has_ni_galvo_stage): ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_stack - self.config["selected_channels"] = 3 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True @@ -368,7 +363,6 @@ def test_single_position_three_channels_per_z(self, has_ni_galvo_stage): ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_z - self.config["selected_channels"] = 3 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True @@ -386,7 +380,6 @@ def test_multi_position_one_channel_per_z(self, has_ni_galvo_stage): # 1 channel per_z self.config["stack_cycling_mode"] = "per_z" - self.config["selected_channels"] = 1 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False @@ -404,7 +397,6 @@ def test_multi_position_one_channel_per_stack(self, has_ni_galvo_stage): # 1 channel per_stack self.config["stack_cycling_mode"] = "per_stack" - self.config["selected_channels"] = 1 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = False self.config["channels"]["channel_3"]["is_selected"] = False @@ -422,7 +414,6 @@ def test_multi_position_two_channels_per_z(self, has_ni_galvo_stage): # 2 channels per_z self.config["stack_cycling_mode"] = "per_z" - self.config["selected_channels"] = 2 for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True @@ -441,7 +432,6 @@ def test_multi_position_two_channels_per_stack(self, has_ni_galvo_stage): # 2 channels per_stack self.config["stack_cycling_mode"] = "per_stack" - self.config["selected_channels"] = 2 for i in range(3): for j in range(3): self.config["channels"]["channel_" + str(j + 1)]["is_selected"] = True @@ -459,7 +449,6 @@ def test_multi_position_three_channels_per_stack(self, has_ni_galvo_stage): ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_stack - self.config["selected_channels"] = 3 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True @@ -477,7 +466,6 @@ def test_multi_position_three_channels_per_z(self, has_ni_galvo_stage): ]["stage"]["has_ni_galvo_stage"] = has_ni_galvo_stage # 3 channels per_z - self.config["selected_channels"] = 3 self.config["channels"]["channel_1"]["is_selected"] = True self.config["channels"]["channel_2"]["is_selected"] = True self.config["channels"]["channel_3"]["is_selected"] = True diff --git a/test/model/test_model.py b/test/model/test_model.py index 4c133a13d..39f9af948 100644 --- a/test/model/test_model.py +++ b/test/model/test_model.py @@ -54,7 +54,9 @@ def model(): verify_experiment_config, verify_waveform_constants, verify_configuration, + verify_positions_config, ) + from navigate.tools.file_functions import load_yaml_file with Manager() as manager: @@ -70,6 +72,7 @@ def model(): configuration_directory, "waveform_constants.yml" ) rest_api_path = Path.joinpath(configuration_directory, "rest_api_config.yml") + multi_positions_path = Path.joinpath(configuration_directory, "multi_positions.yml") event_queue = MagicMock() @@ -84,6 +87,10 @@ def model(): verify_experiment_config(manager, configuration) verify_waveform_constants(manager, configuration) + positions = load_yaml_file(multi_positions_path) + positions = verify_positions_config(positions) + configuration["multi_positions"] = positions + model = Model( args=SimpleNamespace(synthetic_hardware=True), configuration=configuration, @@ -104,7 +111,7 @@ def test_single_acquisition(model): state["image_mode"] = "single" state["is_save"] = False - n_frames = state["selected_channels"] + n_frames = len(list(filter(lambda channel: channel["is_selected"], state["channels"].values()))) show_img_pipe = model.create_pipe("show_img_pipe") @@ -219,8 +226,8 @@ def test_multiposition_acquisition(model): model.configuration["experiment"]["MicroscopeState"]["is_multiposition"] = True update_config_dict( model.__test_manager, # noqa - model.configuration["experiment"], - "MultiPositions", + model.configuration, + "multi_positions", [[10.0, 10.0, 10.0, 10.0, 10.0]], ) model.configuration["experiment"]["MicroscopeState"]["image_mode"] = "z-stack" @@ -245,8 +252,8 @@ def test_multiposition_acquisition(model): # Multiposition is selected but not actually True update_config_dict( model.__test_manager, - model.configuration["experiment"], - "MultiPositions", + model.configuration, + "multi_positions", [], # noqa ) diff --git a/test/view/popups/test_feature_list_popup.py b/test/view/popups/test_feature_list_popup.py index 315e62e3e..5707a79f6 100644 --- a/test/view/popups/test_feature_list_popup.py +++ b/test/view/popups/test_feature_list_popup.py @@ -67,7 +67,7 @@ def test_feature_icon(tk_root): ), ("ChangeResolution", ["resolution_mode", "zoom_value"], ["high", "N/A"]), ("LoopByCount", ["steps"], [1]), - ("LoopByCount", ["steps"], ["experiment.MicroscopeState.selected_channels"]), + ("LoopByCount", ["steps"], ["channels"]), ], ) def test_feature_config_popup(feature_name, args_name, args_value, tk_root):