diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index 21e6d816f..db991944a 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -318,9 +318,9 @@ def verify_experiment_config(manager, configuration): device_ref ].keys() ): - autofocus_setting_dict[microscope_name][device][ - device_ref - ][k] = autofocus_sample_setting[k] + autofocus_setting_dict[microscope_name][device][device_ref][ + k + ] = autofocus_sample_setting[k] # remove non-consistent autofocus parameter for microscope_name in autofocus_setting_dict.keys(): @@ -611,13 +611,13 @@ def verify_experiment_config(manager, configuration): ] ] number_of_filter_wheels = len( - configuration["configuration"]["microscopes"][microscope_name]["filter_wheel"] + configuration["configuration"]["microscopes"][microscope_name].get("filter_wheel", []) ) filterwheel_list = [ list(filter_wheel_config["available_filters"].keys()) for filter_wheel_config in configuration["configuration"]["microscopes"][ microscope_name - ]["filter_wheel"] + ].get("filter_wheel", []) ] prefix = "channel_" channel_nums = configuration["configuration"]["gui"]["channels"]["count"] @@ -639,19 +639,16 @@ def verify_experiment_config(manager, configuration): channel_value["laser_index"] = laser_list.index(channel_value["laser"]) # filter wheel for i in range(number_of_filter_wheels): - ref_name = f"filter_wheel_{i}" + ref_name = configuration["configuration"]["microscopes"][microscope_name][ + "filter_wheel" + ][i].get("name", f"FilterWheel-{i}") if ( ref_name not in channel_value or channel_value[ref_name] not in filterwheel_list[i] ): channel_value[ref_name] = filterwheel_list[i][0] - channel_value[f"filter_position_{i}"] = filterwheel_list[i].index( - channel_value[ref_name] - ) if "filter" in channel_value: channel_value.pop("filter") - if "filter_position" in channel_value: - channel_value.pop("filter_position") # is_selected if ( "is_selected" not in channel_value.keys() @@ -965,13 +962,11 @@ def verify_configuration(manager, configuration): channel_count = 5 # generate hardware header section - hardware_dict = {} ref_list = { "filter_wheel": [], } required_devices = [ "camera", - "filter_wheel", "shutter", "remote_focus", "galvo", @@ -1028,7 +1023,10 @@ def verify_configuration(manager, configuration): elif "type" not in zoom_config["hardware"]: zoom_config["hardware"]["type"] = "Synthetic" - filter_wheel_config = device_config[microscope_name]["filter_wheel"] + filter_wheel_config = device_config[microscope_name].get("filter_wheel", None) + if filter_wheel_config is None: + continue + if type(filter_wheel_config) == DictProxy: # support older version of configuration.yaml # filter_wheel_delay and available filters @@ -1040,6 +1038,7 @@ def verify_configuration(manager, configuration): ) temp_config = device_config[microscope_name]["filter_wheel"] + filter_wheel_names = set() for _, filter_wheel_config in enumerate(temp_config): filter_wheel_idx = build_ref_name( "-", @@ -1049,23 +1048,51 @@ def verify_configuration(manager, configuration): if filter_wheel_idx not in ref_list["filter_wheel"]: ref_list["filter_wheel"].append(filter_wheel_idx) filter_wheel_seq.append(filter_wheel_config) + if ( + filter_wheel_config.get("name", None) is None + and filter_wheel_config.get("hardware", {}).get("name", None) + is not None + ): + filter_wheel_config["name"] = filter_wheel_config["hardware"][ + "name" + ] + idx = ref_list["filter_wheel"].index(filter_wheel_idx) + if filter_wheel_seq[idx].get("name", None): + filter_wheel_config["name"] = filter_wheel_seq[idx]["name"] + elif filter_wheel_config.get("name", None): + filter_wheel_seq[idx]["name"] = filter_wheel_config["name"] + elif filter_wheel_config.get("hardware", {}).get("name", None): + filter_wheel_seq[idx]["name"] = filter_wheel_config["hardware"]["name"] + if filter_wheel_seq[idx].get("name", None): + if filter_wheel_seq[idx]["name"] not in filter_wheel_names: + filter_wheel_names.add(filter_wheel_seq[idx]["name"]) + else: + filter_wheel_seq[idx]["name"] = None + + # make sure all filter wheel entries have hardware name + for i, filter_wheel_config in enumerate(filter_wheel_seq): + if filter_wheel_config.get("name", None) is None: + for j in range(len(filter_wheel_seq)): + temp_name = f"FilterWheel-{j}" + if temp_name not in filter_wheel_names: + filter_wheel_seq[i]["name"] = temp_name + filter_wheel_names.add(temp_name) + break # make sure all microscopes have the same filter wheel sequence - if len(device_config.keys()) > 1: + if len(filter_wheel_seq) > 0: for microscope_name in device_config.keys(): - temp_config = device_config[microscope_name]["filter_wheel"] - filter_wheel_ids = list(range(len(ref_list["filter_wheel"]))) - for _, filter_wheel_config in enumerate(temp_config): + temp_config = device_config[microscope_name].get("filter_wheel", None) + if temp_config is None: + continue + for i, filter_wheel_config in enumerate(temp_config): filter_wheel_idx = build_ref_name( "-", filter_wheel_config["hardware"]["type"], filter_wheel_config["hardware"]["wheel_number"], ) - filter_wheel_ids.remove( - ref_list["filter_wheel"].index(filter_wheel_idx) - ) - for i in filter_wheel_ids: - temp_config.insert(i, filter_wheel_seq[i]) + idx = ref_list["filter_wheel"].index(filter_wheel_idx) + temp_config[i]["name"] = filter_wheel_seq[idx]["name"] update_config_dict( manager, diff --git a/src/navigate/controller/configuration_controller.py b/src/navigate/controller/configuration_controller.py index 420af74e3..9d21f50d8 100644 --- a/src/navigate/controller/configuration_controller.py +++ b/src/navigate/controller/configuration_controller.py @@ -142,8 +142,9 @@ def channels_info(self) -> dict: setting = { "laser": self.lasers_info, } - for i, filter_wheel_config in enumerate(self.microscope_config["filter_wheel"]): - setting[f"filter_wheel_{i}"] = list( + for i, filter_wheel_config in enumerate(self.microscope_config.get("filter_wheel", [])): + filter_wheel_name = filter_wheel_config.get("name", f"FilterWheel-{i}") + setting[filter_wheel_name] = list( filter_wheel_config["available_filters"].keys() ) return setting @@ -532,9 +533,51 @@ def number_of_filter_wheels(self) -> int: """ if self.microscope_config is not None: - return len(self.microscope_config["filter_wheel"]) + return len(self.microscope_config.get("filter_wheel", [])) return 1 + @property + def filter_wheel_types(self) -> list[str]: + """Return a list of filter wheel hardware types. + + Returns + ------- + filter_wheel_types : list + List of filter wheel hardware types. + """ + filter_wheel_types = [] + if self.microscope_config is not None: + for i in range(self.number_of_filter_wheels): + hardware_config = self.microscope_config["filter_wheel"][i].get( + "hardware", {} + ) + filter_wheel_types.append(hardware_config.get("type", "")) + return filter_wheel_types + + @property + def filter_wheel_visibility(self) -> list[bool]: + """Return a list indicating which filter wheels are native to microscope. + + Returns + ------- + filter_wheel_visibility : list + ``True`` for wheels that are defined for this microscope. + """ + if self.microscope_config is None: + return [] + + visibility = self.microscope_config.get("filter_wheel_visibility") + if isinstance(visibility, ListProxy): + visibility = list(visibility) + + if not isinstance(visibility, list): + return [True] * self.number_of_filter_wheels + + if len(visibility) != self.number_of_filter_wheels: + return [True] * self.number_of_filter_wheels + + return [bool(value) for value in visibility] + @property def filter_wheel_names(self) -> list[str]: """Return a list of filter wheel names @@ -547,7 +590,7 @@ def filter_wheel_names(self) -> list[str]: filter_wheel_names = [] if self.microscope_config is not None: for i in range(self.number_of_filter_wheels): - name = self.microscope_config["filter_wheel"][i]["hardware"].get( + name = self.microscope_config["filter_wheel"][i].get( "name", f"Filter Wheel {i}" ) filter_wheel_names.append(name) diff --git a/src/navigate/controller/sub_controllers/channels_settings.py b/src/navigate/controller/sub_controllers/channels_settings.py index 23ce99f72..dc789a265 100644 --- a/src/navigate/controller/sub_controllers/channels_settings.py +++ b/src/navigate/controller/sub_controllers/channels_settings.py @@ -64,17 +64,6 @@ def __init__(self, view, parent_controller=None, configuration_controller=None): #: ConfigurationController: The configuration controller. self.configuration_controller = configuration_controller - # num: numbers of channels - self.num = self.configuration_controller.number_of_channels - self.number_of_filter_wheels = ( - self.configuration_controller.number_of_filter_wheels - ) - self.view.populate_frame( - channels=self.num, - filter_wheels=self.number_of_filter_wheels, - filter_wheel_names=self.configuration_controller.filter_wheel_names, - ) - #: str: The mode of the channel setting controller. Either 'live' or 'stop'. self.mode = "stop" @@ -87,6 +76,42 @@ def __init__(self, view, parent_controller=None, configuration_controller=None): #: dict: The channel setting dictionary. self.channel_setting_dict = None + self.rebuild_view() + self.initialize() + + @staticmethod + def _get_dropdown_values(dropdown): + """Return combobox values as a tuple.""" + values = dropdown["values"] + if not values: + return () + if isinstance(values, tuple): + return values + if isinstance(values, list): + return tuple(values) + if isinstance(values, str): + return tuple(dropdown.tk.splitlist(values)) + return tuple(values) + + def rebuild_view(self) -> None: + """Rebuild channel widgets from the active microscope configuration.""" + # num: numbers of channels + # add a flag to avoid triggering callback when initializing the view + self.in_initialization = True + self.num = self.configuration_controller.number_of_channels + self.number_of_filter_wheels = ( + self.configuration_controller.number_of_filter_wheels + ) + filter_wheel_types = getattr( + self.configuration_controller, "filter_wheel_types", [] + ) + self.view.populate_frame( + channels=self.num, + filter_wheels=self.number_of_filter_wheels, + filter_wheel_names=self.configuration_controller.filter_wheel_names, + filter_wheel_types=filter_wheel_types, + ) + # widget command binds for i in range(self.num): channel_vals = self.get_vals_by_channel(i) @@ -127,13 +152,14 @@ def set_mode(self, mode="stop"): def initialize(self): """Populates the laser and filter wheel options in the View.""" setting_dict = self.configuration_controller.channels_info + filter_wheel_names = self.configuration_controller.filter_wheel_names for i in range(self.num): self.view.laser_pulldowns[i]["values"] = setting_dict["laser"] for j in range(self.number_of_filter_wheels): - ref_name = f"filter_wheel_{j}" + ref_name = filter_wheel_names[j] self.view.filterwheel_pulldowns[i * self.number_of_filter_wheels + j][ "values" - ] = setting_dict[ref_name] + ] = setting_dict.get(ref_name, "") self.show_verbose_info("channel has been initialized") def populate_experiment_values(self, setting_dict): @@ -158,6 +184,7 @@ def populate_experiment_values(self, setting_dict): self.populate_empty_values() self.channel_setting_dict = setting_dict prefix = "channel_" + filter_wheel_names = self.configuration_controller.filter_wheel_names for channel in setting_dict.keys(): channel_id = int(channel[len(prefix) :]) - 1 channel_vals = self.get_vals_by_channel(channel_id) @@ -165,7 +192,19 @@ def populate_experiment_values(self, setting_dict): return channel_value = setting_dict[channel] for name in channel_vals: - channel_vals[name].set(channel_value[name]) + if channel_value.get(name, None): + # don't set the value if the filter wheel value not in the dropdown options + if name in filter_wheel_names and channel_value[ + name + ] not in self._get_dropdown_values( + self.view.filterwheel_pulldowns[ + channel_id * self.number_of_filter_wheels + + filter_wheel_names.index(name) + ] + ): + continue + + channel_vals[name].set(channel_value[name]) # validate exposure_time, interval, laser_power self.view.exptime_pulldowns[channel_id].trigger_focusout_validation() @@ -181,21 +220,20 @@ def populate_empty_values(self): to be populated with a default value. """ for i in range(self.num): - if ( - self.view.laser_pulldowns[i].get() - not in self.view.laser_pulldowns[i]["values"] - ): - self.view.laser_pulldowns[i].set( - self.view.laser_pulldowns[i]["values"][0] - ) + laser_values = self._get_dropdown_values(self.view.laser_pulldowns[i]) + if laser_values and self.view.laser_pulldowns[i].get() not in laser_values: + self.view.laser_pulldowns[i].set(laser_values[0]) - if ( - self.view.filterwheel_pulldowns[i].get() - not in self.view.filterwheel_pulldowns[i]["values"] - ): - self.view.filterwheel_pulldowns[i].set( - self.view.filterwheel_pulldowns[i]["values"][0] + for j in range(self.number_of_filter_wheels): + idx = i * self.number_of_filter_wheels + j + filter_values = self._get_dropdown_values( + self.view.filterwheel_pulldowns[idx] ) + if ( + filter_values + and self.view.filterwheel_pulldowns[idx].get() not in filter_values + ): + self.view.filterwheel_pulldowns[idx].set(filter_values[0]) if self.view.exptime_pulldowns[i].get() == "": self.view.exptime_pulldowns[i].set(100.0) @@ -321,13 +359,6 @@ def update_setting_dict(setting_dict, widget_name): setting_dict["laser_index"] = self.get_index( "laser", channel_vals["laser"].get() ) - elif widget_name.startswith("filter"): - for i in range(self.number_of_filter_wheels): - ref_name = f"filter_wheel_{i}" - setting_dict[ref_name] = channel_vals[ref_name].get() - setting_dict[f"filter_position_{i}"] = self.get_index( - ref_name, channel_vals[ref_name].get() - ) elif widget_name in [ "laser_power", "camera_exposure_time", @@ -436,8 +467,9 @@ def get_vals_by_channel(self, index): "interval_time": self.view.interval_variables[index], "defocus": self.view.defocus_variables[index], } + filter_wheel_names = self.configuration_controller.filter_wheel_names for i in range(self.number_of_filter_wheels): - ref_name = f"filter_wheel_{i}" + ref_name = filter_wheel_names[i] result[ref_name] = self.view.filterwheel_variables[ index * self.number_of_filter_wheels + i ] @@ -461,14 +493,16 @@ def get_index(self, dropdown_name, value): if not value: return -1 if dropdown_name == "laser": + values = self._get_dropdown_values(self.view.laser_pulldowns[0]) try: - laser_id = self.view.laser_pulldowns[0]["values"].index(value) + laser_id = values.index(value) except ValueError: return 0 return laser_id elif dropdown_name.startswith("filter"): idx = int(dropdown_name[dropdown_name.rfind("_") + 1 :]) - return self.view.filterwheel_pulldowns[idx]["values"].index(value) + values = self._get_dropdown_values(self.view.filterwheel_pulldowns[idx]) + return values.index(value) return -1 def verify_experiment_values(self): diff --git a/src/navigate/controller/sub_controllers/channels_tab.py b/src/navigate/controller/sub_controllers/channels_tab.py index 3e1a46a2f..c36d7bb8a 100644 --- a/src/navigate/controller/sub_controllers/channels_tab.py +++ b/src/navigate/controller/sub_controllers/channels_tab.py @@ -190,6 +190,7 @@ def initialize(self) -> None: main window. """ config = self.parent_controller.configuration_controller + self.channel_setting_controller.rebuild_view() self.stack_acq_widgets["cycling"].widget["values"] = ["Per Z", "Per Stack"] # Set the default stage for acquiring a z-stack. diff --git a/src/navigate/model/microscope.py b/src/navigate/model/microscope.py index 75dc6ff06..2a31c1c0b 100644 --- a/src/navigate/model/microscope.py +++ b/src/navigate/model/microscope.py @@ -170,7 +170,7 @@ def __init__( "mirror": ["type"], } - device_name_dict = {"laser": "wavelength"} + device_name_dict = {"laser": "wavelength", "filter_wheel": "name"} laser_list = self.configuration["configuration"]["microscopes"][ self.microscope_name diff --git a/src/navigate/view/main_window_content/channels_tab.py b/src/navigate/view/main_window_content/channels_tab.py index 8b74bffb4..7d1e547e2 100644 --- a/src/navigate/view/main_window_content/channels_tab.py +++ b/src/navigate/view/main_window_content/channels_tab.py @@ -205,7 +205,12 @@ def __init__( self.frame_columns = [] def populate_frame( - self, channels: int, filter_wheels: int, filter_wheel_names: list + self, + channels: int, + filter_wheels: int, + filter_wheel_names: list, + filter_wheel_types: list = None, + filter_wheel_visibility: list = None, ) -> None: """Populates the frame with the widgets. @@ -222,9 +227,19 @@ def populate_frame( The number of filter wheels filter_wheel_names : list The names of the filter wheels + filter_wheel_types : list + The types of filter wheels + filter_wheel_visibility : list + Indicates whether each filter wheel belongs to this microscope """ + self.reset_frame() - self.create_labels(filter_wheel_names, filter_wheels) + self.create_labels( + filter_wheel_names, + filter_wheels, + filter_wheel_types, + filter_wheel_visibility, + ) # Configure the columns for consistent spacing for i in range(len(self.label_text)): @@ -286,6 +301,10 @@ def populate_frame( ) ) self.filterwheel_pulldowns[-1].config(state="readonly") + if self.should_hide_filter_wheel( + i, filter_wheel_types, filter_wheel_visibility + ): + continue self.filterwheel_pulldowns[-1].grid( row=num + 1, column=(column_id := column_id + 1), @@ -344,7 +363,13 @@ def populate_frame( pady=self.pad_y, ) - def create_labels(self, filter_wheel_names: list, filter_wheels: int) -> None: + def create_labels( + self, + filter_wheel_names: list, + filter_wheels: int, + filter_wheel_types: list = None, + filter_wheel_visibility: list = None, + ) -> None: """Create the labels for the columns. Function to create the labels for the columns of the Channel Creator frame. @@ -355,6 +380,10 @@ def create_labels(self, filter_wheel_names: list, filter_wheels: int) -> None: A list of the names of the filter wheels filter_wheels : int Number of filter wheels + filter_wheel_types : list + The types of filter wheels + filter_wheel_visibility : list + Indicates whether each filter wheel belongs to this microscope """ # Create the labels for the columns. self.label_text = [ @@ -363,6 +392,10 @@ def create_labels(self, filter_wheel_names: list, filter_wheels: int) -> None: "Power", ] for i in range(filter_wheels): + if self.should_hide_filter_wheel( + i, filter_wheel_types, filter_wheel_visibility + ): + continue self.label_text.append(filter_wheel_names[i]) self.label_text += ["Exp. Time (ms)", "Interval", "Defocus"] @@ -379,6 +412,56 @@ def create_labels(self, filter_wheel_names: list, filter_wheels: int) -> None: row=0, column=0, sticky=tk.N, pady=self.pad_y, padx=self.pad_x ) + def reset_frame(self) -> None: + """Destroy existing channel widgets and clear references.""" + for child in self.winfo_children(): + child.destroy() + + self.channel_variables = [] + self.channel_checks = [] + self.laser_variables = [] + self.laser_pulldowns = [] + self.laserpower_variables = [] + self.laserpower_pulldowns = [] + self.filterwheel_variables = [] + self.filterwheel_pulldowns = [] + self.exptime_variables = [] + self.exptime_pulldowns = [] + self.interval_variables = [] + self.interval_spins = [] + self.defocus_variables = [] + self.defocus_spins = [] + self.labels = [] + self.frame_columns = [] + + @staticmethod + def is_synthetic_filter_wheel( + filter_wheel_idx: int, filter_wheel_types: list + ) -> bool: + """Return whether a filter wheel type should be hidden in the GUI.""" + if filter_wheel_types is None or filter_wheel_idx >= len(filter_wheel_types): + return False + + filter_wheel_type = str(filter_wheel_types[filter_wheel_idx]).strip().lower() + return filter_wheel_type in ("synthetic", "syntheticfilterwheel") + + @classmethod + def should_hide_filter_wheel( + cls, + filter_wheel_idx: int, + filter_wheel_types: list, + filter_wheel_visibility: list, + ) -> bool: + """Return whether a filter wheel should be hidden in the GUI.""" + if ( + filter_wheel_visibility is not None + and filter_wheel_idx < len(filter_wheel_visibility) + and not filter_wheel_visibility[filter_wheel_idx] + ): + return True + + return cls.is_synthetic_filter_wheel(filter_wheel_idx, filter_wheel_types) + class StackAcquisitionFrame(ttk.Labelframe): """This class is the frame that holds the stack acquisition settings.""" diff --git a/test/config/test_config.py b/test/config/test_config.py index 65567b3f9..6d3116163 100644 --- a/test/config/test_config.py +++ b/test/config/test_config.py @@ -266,6 +266,143 @@ def test_update_config_dict_with_file_name(self): os.remove(test_entry) +class TestVerifyConfiguration(unittest.TestCase): + def setUp(self): + self.manager = Manager() + current_path = os.path.abspath(os.path.dirname(__file__)) + root_path = os.path.dirname(os.path.dirname(current_path)) + self.config_path = os.path.join(root_path, "src", "navigate", "config") + + def tearDown(self): + self.manager.shutdown() + + def test_verify_configuration_with_valid_config(self): + configuration = config.load_configs( + self.manager, + configuration=os.path.join(self.config_path, "configuration.yaml"), + ) + + configuration["configuration"]["microscopes"]["Mesoscale"]["filter_wheel"][ + "hardware" + ]["wheel_number"] = 2 + try: + config.verify_configuration(self.manager, configuration) + except Exception as e: + self.fail(f"verify_configuration raised an exception: {e}") + + # assert same filter wheel name + microscope_names = list(configuration["configuration"]["microscopes"].keys()) + filter_wheel_name = None + for microscope_name in microscope_names: + temp = configuration["configuration"]["microscopes"][microscope_name][ + "filter_wheel" + ][0]["name"] + if filter_wheel_name is None: + filter_wheel_name = temp + else: + assert ( + filter_wheel_name == temp + ), "filter wheel names should be the same for all microscopes" + + def test_verify_configuration_with_no_filterwheel(self): + configuration = config.load_configs( + self.manager, + configuration=os.path.join(self.config_path, "configuration.yaml"), + ) + for microscope_name in configuration["configuration"]["microscopes"].keys(): + del configuration["configuration"]["microscopes"][microscope_name][ + "filter_wheel" + ] + + config.verify_configuration(self.manager, configuration) + # assert no filter wheel is added to configuration + for microscope_name in configuration["configuration"]["microscopes"].keys(): + assert ( + "filter_wheel" + not in configuration["configuration"]["microscopes"][ + microscope_name + ].keys() + ) + + def test_verify_configuration_with_one_microscope_has_filterwheel_and_another_microscope_has_no_filterwheel( + self, + ): + configuration = config.load_configs( + self.manager, + configuration=os.path.join(self.config_path, "configuration.yaml"), + ) + microscope_names = list(configuration["configuration"]["microscopes"].keys()) + # delete filter wheel of the first microscope + del configuration["configuration"]["microscopes"][microscope_names[0]][ + "filter_wheel" + ] + + config.verify_configuration(self.manager, configuration) + # assert no filter wheel is added to configuration + assert ( + "filter_wheel" + not in configuration["configuration"]["microscopes"][ + microscope_names[0] + ].keys() + ) + + for i in range(1, len(microscope_names)): + assert ( + "filter_wheel" + in configuration["configuration"]["microscopes"][ + microscope_names[i] + ].keys() + ) + + def test_verify_configuration_with_different_filterwheel_for_different_microscopes( + self, + ): + configuration = config.load_configs( + self.manager, + configuration=os.path.join(self.config_path, "configuration.yaml"), + ) + microscope_names = list(configuration["configuration"]["microscopes"].keys()) + # change filter wheel of the first microscope to have different number of filter wheels and different filter wheel types + configuration["configuration"]["microscopes"][microscope_names[0]][ + "filter_wheel" + ]["hardware"]["type"] = "ASI" + + config.verify_configuration(self.manager, configuration) + # assert each microscope has only one filter wheel and the filter wheel type is correct + assert ( + len( + configuration["configuration"]["microscopes"][microscope_names[0]][ + "filter_wheel" + ] + ) + == 1 + ) + assert configuration["configuration"]["microscopes"][microscope_names[0]][ + "filter_wheel" + ][0]["hardware"]["type"].startswith("ASI") + assert ( + len( + configuration["configuration"]["microscopes"][microscope_names[1]][ + "filter_wheel" + ] + ) + == 1 + ) + assert configuration["configuration"]["microscopes"][microscope_names[1]][ + "filter_wheel" + ][0]["hardware"]["type"].startswith("Sutter") + # assert filter wheel name is unique + filter_wheel_names = [] + for microscope_name in microscope_names: + filter_wheel_name = configuration["configuration"]["microscopes"][ + microscope_name + ]["filter_wheel"][0]["name"] + assert ( + filter_wheel_name not in filter_wheel_names + ), f"filter wheel name {filter_wheel_name} is not unique" + filter_wheel_names.append(filter_wheel_name) + + class TestVerifyExperimentConfig(unittest.TestCase): def setUp(self): self.manager = Manager() @@ -723,8 +860,7 @@ def test_load_experiment_file_with_wrong_parameter_values(self): "is_selected": 1, "laser": "48nm", "laser_index": -1, - "filter_wheel_0": "nonexsit_filter_***", - "filter_position_0": 1, + "FilterWheel-0": "nonexsit_filter_***", "camera_exposure_time": -200.0, "laser_power": "a", "interval_time": -3, @@ -735,8 +871,7 @@ def test_load_experiment_file_with_wrong_parameter_values(self): "is_selected": False, "laser": lasers[0], "laser_index": 0, - "filter_wheel_0": filterwheels[0], - "filter_position_0": 0, + "FilterWheel-0": filterwheels[0], "camera_exposure_time": 200.0, "laser_power": 20.0, "interval_time": 0.0, @@ -759,8 +894,7 @@ def test_load_experiment_file_with_wrong_parameter_values(self): "is_selected": 1, "laser": lasers[1], "laser_index": 3, - "filter_wheel_0": filterwheels[2], - "filter_position_0": 1, + "FilterWheel-0": filterwheels[2], "camera_exposure_time": -200.0, "laser_power": "a", "interval_time": -3, @@ -771,8 +905,7 @@ def test_load_experiment_file_with_wrong_parameter_values(self): "is_selected": False, "laser": lasers[1], "laser_index": 1, - "filter_wheel_0": filterwheels[2], - "filter_position_0": 2, + "FilterWheel-0": filterwheels[2], "camera_exposure_time": 200.0, "laser_power": 20.0, "interval_time": 0.0, diff --git a/test/controller/sub_controllers/test_channels_settings.py b/test/controller/sub_controllers/test_channels_settings.py index 81cd790a3..2206d4c3f 100644 --- a/test/controller/sub_controllers/test_channels_settings.py +++ b/test/controller/sub_controllers/test_channels_settings.py @@ -97,6 +97,8 @@ def test_set_mode(self, mode, state, state_readonly): assert ( str(self.channel_setting.view.filterwheel_pulldowns[i]["state"]) == state_readonly + if mode == "stop" + else "disabled" ) assert str(self.channel_setting.view.defocus_spins[i]["state"]) == state @@ -147,11 +149,6 @@ def test_channel_callback(self): assert ( setting_dict["laser_index"] == new_setting_dict["laser_index"] ) - elif k == "filter": - assert ( - setting_dict["filter_position"] - == new_setting_dict["filter_position"] - ) elif k == "camera_exposure_time" or k == "is_selected": assert ( self.channel_setting.parent_controller.commands.pop() @@ -159,6 +156,34 @@ def test_channel_callback(self): ) self.channel_setting.parent_controller.commands = [] # reset + def test_dropdown_values_initialized_in_constructor(self): + laser_values = self.channel_setting._get_dropdown_values( + self.channel_setting.view.laser_pulldowns[0] + ) + assert len(laser_values) > 0 + + for i in range(self.channel_setting.number_of_filter_wheels): + filter_values = self.channel_setting._get_dropdown_values( + self.channel_setting.view.filterwheel_pulldowns[i] + ) + assert len(filter_values) > 0 + + def test_populate_empty_values_with_empty_dropdowns(self): + self.channel_setting.view.laser_pulldowns[0]["values"] = () + self.channel_setting.view.filterwheel_pulldowns[0]["values"] = () + self.channel_setting.view.laser_variables[0].set("invalid_laser_value") + self.channel_setting.view.filterwheel_variables[0].set("invalid_filter_value") + + self.channel_setting.populate_empty_values() + + assert ( + self.channel_setting.view.laser_variables[0].get() == "invalid_laser_value" + ) + assert ( + self.channel_setting.view.filterwheel_variables[0].get() + == "invalid_filter_value" + ) + def test_get_vals_by_channel(self): # Not needed to test IMO pass diff --git a/test/controller/test_configuration_controller.py b/test/controller/test_configuration_controller.py new file mode 100644 index 000000000..37e8c3db3 --- /dev/null +++ b/test/controller/test_configuration_controller.py @@ -0,0 +1,118 @@ +# 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. + +from multiprocessing import Manager + +import pytest + +from navigate.controller.configuration_controller import ConfigurationController + + +@pytest.fixture +def configuration(): + return { + "experiment": {"MicroscopeState": {"microscope_name": "scope_a"}}, + "configuration": { + "microscopes": { + "scope_a": { + "galvo": [], + "filter_wheel": [ + { + "hardware": { + "type": "Sutter", + "name": "Wheel A", + "wheel_number": 0, + } + }, + { + "hardware": { + "type": "SyntheticFilterWheel", + "name": "Wheel B", + "wheel_number": 1, + } + }, + ], + }, + "scope_b": { + "galvo": [], + "filter_wheel": [ + { + "hardware": { + "type": "ASI", + "name": "Wheel C", + "wheel_number": 0, + } + } + ], + }, + } + }, + "gui": {"channel_settings": {"count": 5}}, + } + + +def test_filter_wheel_types(configuration): + controller = ConfigurationController(configuration) + + assert controller.filter_wheel_types == ["Sutter", "SyntheticFilterWheel"] + + +@pytest.mark.parametrize("visibility", [None, "invalid", [True]]) +def test_filter_wheel_visibility_defaults_to_all_true(configuration, visibility): + if visibility is not None: + configuration["configuration"]["microscopes"]["scope_a"][ + "filter_wheel_visibility" + ] = visibility + + controller = ConfigurationController(configuration) + + assert controller.filter_wheel_visibility == [True, True] + + +def test_filter_wheel_visibility_boolean_cast(configuration): + configuration["configuration"]["microscopes"]["scope_a"][ + "filter_wheel_visibility" + ] = [1, 0] + + controller = ConfigurationController(configuration) + + assert controller.filter_wheel_visibility == [True, False] + + +def test_filter_wheel_visibility_listproxy(configuration): + with Manager() as manager: + configuration["configuration"]["microscopes"]["scope_a"][ + "filter_wheel_visibility" + ] = manager.list([1, 0]) + + controller = ConfigurationController(configuration) + + assert controller.filter_wheel_visibility == [True, False] diff --git a/test/view/main_window_content/tabs/channels/test_channel_creator.py b/test/view/main_window_content/tabs/channels/test_channel_creator.py new file mode 100644 index 000000000..1e0a1670b --- /dev/null +++ b/test/view/main_window_content/tabs/channels/test_channel_creator.py @@ -0,0 +1,49 @@ +# 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. + +from navigate.view.main_window_content.channels_tab import ChannelCreator + + +def test_is_synthetic_filter_wheel_variants(): + assert ChannelCreator.is_synthetic_filter_wheel(0, ["synthetic"]) + assert ChannelCreator.is_synthetic_filter_wheel(0, [" SyntheticFilterWheel "]) + assert not ChannelCreator.is_synthetic_filter_wheel(0, ["sutter"]) + assert not ChannelCreator.is_synthetic_filter_wheel(1, ["sutter"]) + assert not ChannelCreator.is_synthetic_filter_wheel(0, None) + + +def test_should_hide_filter_wheel(): + assert ChannelCreator.should_hide_filter_wheel(0, ["sutter"], [False]) + assert ChannelCreator.should_hide_filter_wheel(0, ["synthetic"], [True]) + assert not ChannelCreator.should_hide_filter_wheel(0, ["sutter"], [True]) + assert ChannelCreator.should_hide_filter_wheel( + 1, ["sutter", "syntheticfilterwheel"], [True] + )