diff --git a/software/control/core/core.py b/software/control/core/core.py index afd283771..b1ae4b9d2 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -35,16 +35,17 @@ except: pass -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, Dict, Any from queue import Queue from threading import Thread, Lock from pathlib import Path from datetime import datetime +from enum import Enum +from control.utils_config import ChannelConfig, ChannelMode, LaserAFConfig import time import subprocess import shutil import itertools -from lxml import etree import json import math import random @@ -444,46 +445,11 @@ def close(self): self.thread.join() -class Configuration: - def __init__( - self, - mode_id=None, - name=None, - color=None, - camera_sn=None, - exposure_time=None, - analog_gain=None, - illumination_source=None, - illumination_intensity=None, - z_offset=None, - pixel_format=None, - _pixel_format_options=None, - emission_filter_position=None, - ): - self.id = mode_id - self.name = name - self.color = color - self.exposure_time = exposure_time - self.analog_gain = analog_gain - self.illumination_source = illumination_source - self.illumination_intensity = illumination_intensity - self.camera_sn = camera_sn - self.z_offset = z_offset - self.pixel_format = pixel_format - if self.pixel_format is None: - self.pixel_format = "default" - self._pixel_format_options = _pixel_format_options - if _pixel_format_options is None: - self._pixel_format_options = self.pixel_format - self.emission_filter_position = emission_filter_position - - class LiveController(QObject): def __init__( self, camera, microcontroller, - configurationManager, illuminationController, parent=None, control_illumination=True, @@ -494,7 +460,6 @@ def __init__( self.microscope = parent self.camera = camera self.microcontroller = microcontroller - self.configurationManager = configurationManager self.currentConfiguration = None self.trigger_mode = TriggerMode.SOFTWARE # @@@ change to None self.is_live = False @@ -534,7 +499,7 @@ def __init__( def turn_on_illumination(self): if self.illuminationController is not None and not "LED matrix" in self.currentConfiguration.name: self.illuminationController.turn_on_illumination( - int(self.configurationManager.extract_wavelength(self.currentConfiguration.name)) + int(utils.extract_wavelength_from_config_name(self.currentConfiguration.name)) ) elif SUPPORT_SCIMICROSCOPY_LED_ARRAY and "LED matrix" in self.currentConfiguration.name: self.led_array.turn_on_illumination() @@ -545,7 +510,7 @@ def turn_on_illumination(self): def turn_off_illumination(self): if self.illuminationController is not None and not "LED matrix" in self.currentConfiguration.name: self.illuminationController.turn_off_illumination( - int(self.configurationManager.extract_wavelength(self.currentConfiguration.name)) + int(utils.extract_wavelength_from_config_name(self.currentConfiguration.name)) ) elif SUPPORT_SCIMICROSCOPY_LED_ARRAY and "LED matrix" in self.currentConfiguration.name: self.led_array.turn_off_illumination() @@ -598,7 +563,7 @@ def set_illumination(self, illumination_source, intensity, update_channel_settin # update illumination if self.illuminationController is not None: self.illuminationController.set_intensity( - int(self.configurationManager.extract_wavelength(self.currentConfiguration.name)), intensity + int(utils.extract_wavelength_from_config_name(self.currentConfiguration.name)), intensity ) elif ENABLE_NL5 and NL5_USE_DOUT and "Fluorescence" in self.currentConfiguration.name: wavelength = int(self.currentConfiguration.name[13:16]) @@ -1362,7 +1327,7 @@ class MultiPointWorker(QObject): image_to_display = Signal(np.ndarray) spectrum_to_display = Signal(np.ndarray) image_to_display_multi = Signal(np.ndarray, int) - signal_current_configuration = Signal(Configuration) + signal_current_configuration = Signal(ChannelMode) signal_register_current_fov = Signal(float, float) signal_detection_stats = Signal(object) signal_update_stats = Signal(object) @@ -1388,7 +1353,8 @@ def __init__(self, multiPointController): self.piezo: PiezoStage = self.multiPointController.piezo self.liveController = self.multiPointController.liveController self.autofocusController = self.multiPointController.autofocusController - self.configurationManager = self.multiPointController.configurationManager + self.objectiveStore = self.multiPointController.objectiveStore + self.channelConfigurationManager = self.multiPointController.channelConfigurationManager self.NX = self.multiPointController.NX self.NY = self.multiPointController.NY self.NZ = self.multiPointController.NZ @@ -1764,7 +1730,7 @@ def perform_autofocus(self, region_id, fov): config_AF = next( ( config - for config in self.configurationManager.configurations + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name_AF ) ) @@ -1786,7 +1752,7 @@ def perform_autofocus(self, region_id, fov): config_AF = next( ( config - for config in self.configurationManager.configurations + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name_AF ) ) @@ -1900,7 +1866,7 @@ def acquire_rgb_image(self, config, file_ID, current_path, current_round_images, rgb_channels = ["BF LED matrix full_R", "BF LED matrix full_G", "BF LED matrix full_B"] images = {} - for config_ in self.configurationManager.configurations: + for config_ in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): if config_.name in rgb_channels: # update the current configuration self.signal_current_configuration.emit(config_) @@ -2166,7 +2132,7 @@ class MultiPointController(QObject): image_to_display = Signal(np.ndarray) image_to_display_multi = Signal(np.ndarray, int) spectrum_to_display = Signal(np.ndarray) - signal_current_configuration = Signal(Configuration) + signal_current_configuration = Signal(ChannelMode) signal_register_current_fov = Signal(float, float) detection_stats = Signal(object) signal_stitcher = Signal(str) @@ -2185,7 +2151,8 @@ def __init__( microcontroller: Microcontroller, liveController, autofocusController, - configurationManager, + objectiveStore, + channelConfigurationManager, usb_spectrometer=None, scanCoordinates=None, parent=None, @@ -2200,7 +2167,8 @@ def __init__( self.microcontroller = microcontroller self.liveController = liveController self.autofocusController = autofocusController - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.channelConfigurationManager = channelConfigurationManager self.multiPointWorker: Optional[MultiPointWorker] = None self.thread: Optional[QThread] = None self.NX = 1 @@ -2330,10 +2298,8 @@ def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prep self.recording_start_time = time.time() # create a new folder utils.ensure_directory_exists(os.path.join(self.base_path, self.experiment_ID)) - # TODO(imo): If the config has changed since boot, is this still the correct config? - configManagerThrowaway = ConfigurationManager(self.configurationManager.config_filename) - configManagerThrowaway.write_configuration_selected( - self.selected_configurations, os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" + self.channelConfigurationManager.write_configuration_selected( + self.objectiveStore.current_objective, self.selected_configurations, os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" ) # save the configuration for the experiment # Prepare acquisition parameters acquisition_parameters = { @@ -2377,7 +2343,9 @@ def set_selected_configurations(self, selected_configurations_name): for configuration_name in selected_configurations_name: self.selected_configurations.append( next( - (config for config in self.configurationManager.configurations if config.name == configuration_name) + (config + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == configuration_name) ) ) @@ -2642,14 +2610,15 @@ class TrackingController(QObject): signal_tracking_stopped = Signal() image_to_display = Signal(np.ndarray) image_to_display_multi = Signal(np.ndarray, int) - signal_current_configuration = Signal(Configuration) + signal_current_configuration = Signal(ChannelMode) def __init__( self, camera, microcontroller: Microcontroller, stage: AbstractStage, - configurationManager, + objectiveStore, + channelConfigurationManager, liveController: LiveController, autofocusController, imageDisplayWindow, @@ -2658,7 +2627,8 @@ def __init__( self.camera = camera self.microcontroller = microcontroller self.stage = stage - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.channelConfigurationManager = channelConfigurationManager self.liveController = liveController self.autofocusController = autofocusController self.imageDisplayWindow = imageDisplayWindow @@ -2764,7 +2734,8 @@ def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prep # create a new folder try: utils.ensure_directory_exists(os.path.join(self.base_path, self.experiment_ID)) - self.configurationManager.write_configuration( + self.channelConfigurationManager.save_current_configuration_to_path( + self.objectiveStore.current_objective, os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" ) # save the configuration for the experiment except: @@ -2775,8 +2746,10 @@ def set_selected_configurations(self, selected_configurations_name): self.selected_configurations = [] for configuration_name in selected_configurations_name: self.selected_configurations.append( - next( - (config for config in self.configurationManager.configurations if config.name == configuration_name) + next(( + config + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == configuration_name) ) ) @@ -2865,7 +2838,7 @@ class TrackingWorker(QObject): finished = Signal() image_to_display = Signal(np.ndarray) image_to_display_multi = Signal(np.ndarray, int) - signal_current_configuration = Signal(Configuration) + signal_current_configuration = Signal(ChannelMode) def __init__(self, trackingController: TrackingController): QObject.__init__(self) @@ -2876,7 +2849,7 @@ def __init__(self, trackingController: TrackingController): self.microcontroller = self.trackingController.microcontroller self.liveController = self.trackingController.liveController self.autofocusController = self.trackingController.autofocusController - self.configurationManager = self.trackingController.configurationManager + self.channelConfigurationManager = self.trackingController.channelConfigurationManager self.imageDisplayWindow = self.trackingController.imageDisplayWindow self.crop_width = self.trackingController.crop_width self.crop_height = self.trackingController.crop_height @@ -3595,88 +3568,245 @@ def display_image(self, image, illumination_source): self.graphics_widget_4.img.setImage(image, autoLevels=False) -class ConfigurationManager(QObject): - def __init__(self, filename="channel_configurations.xml"): - QObject.__init__(self) +class ConfigType(Enum): + CHANNEL = "channel" + CONFOCAL = "confocal" + WIDEFIELD = "widefield" + + +class ChannelConfigurationManager: + def __init__(self): self._log = squid.logging.get_logger(self.__class__.__name__) - self.config_filename = filename - self.configurations = [] - self.read_configurations() + self.config_root = None + self.all_configs: Dict[ConfigType, Dict[str, ChannelConfig]] = { + ConfigType.CHANNEL: {}, + ConfigType.CONFOCAL: {}, + ConfigType.WIDEFIELD: {} + } + self.active_config_type = ConfigType.CHANNEL if not ENABLE_SPINNING_DISK_CONFOCAL else ConfigType.CONFOCAL - def save_configurations(self): - self.write_configuration(self.config_filename) + def set_profile_path(self, profile_path: Path) -> None: + """Set the root path for configurations""" + self.config_root = profile_path - def write_configuration(self, filename): - try: - self.config_xml_tree.write(filename, encoding="utf-8", xml_declaration=True, pretty_print=True) - return True - except IOError: - self._log.exception("Couldn't write configuration.") - return False + def _load_xml_config(self, objective: str, config_type: ConfigType) -> None: + """Load XML configuration for a specific config type, generating default if needed""" + config_file = self.config_root / objective / f"{config_type.value}_configurations.xml" - def read_configurations(self): - if os.path.isfile(self.config_filename) == False: - utils_config.generate_default_configuration(self.config_filename) - print("genenrate default config files") - self.config_xml_tree = etree.parse(self.config_filename) - self.config_xml_tree_root = self.config_xml_tree.getroot() - self.num_configurations = 0 - for mode in self.config_xml_tree_root.iter("mode"): - self.num_configurations += 1 - self.configurations.append( - Configuration( - mode_id=mode.get("ID"), - name=mode.get("Name"), - color=self.get_channel_color(mode.get("Name")), - exposure_time=float(mode.get("ExposureTime")), - analog_gain=float(mode.get("AnalogGain")), - illumination_source=int(mode.get("IlluminationSource")), - illumination_intensity=float(mode.get("IlluminationIntensity")), - camera_sn=mode.get("CameraSN"), - z_offset=float(mode.get("ZOffset")), - pixel_format=mode.get("PixelFormat"), - _pixel_format_options=mode.get("_PixelFormat_options"), - emission_filter_position=int(mode.get("EmissionFilterPosition", 1)), - ) - ) + if not config_file.exists(): + utils_config.generate_default_configuration(str(config_file)) + + xml_content = config_file.read_bytes() + self.all_configs[config_type][objective] = ChannelConfig.from_xml(xml_content) + + def load_configurations(self, objective: str) -> None: + """Load available configurations for an objective""" + if ENABLE_SPINNING_DISK_CONFOCAL: + # Load both confocal and widefield configurations + self._load_xml_config(objective, ConfigType.CONFOCAL) + self._load_xml_config(objective, ConfigType.WIDEFIELD) + else: + # Load only channel configurations + self._load_xml_config(objective, ConfigType.CHANNEL) + + def _save_xml_config(self, objective: str, config_type: ConfigType) -> None: + """Save XML configuration for a specific config type""" + if objective not in self.all_configs[config_type]: + return + + config = self.all_configs[config_type][objective] + save_path = self.config_root / objective / f"{config_type.value}_configurations.xml" + + if not save_path.parent.exists(): + save_path.parent.mkdir(parents=True) + + xml_str = config.to_xml(pretty_print=True, encoding='utf-8') + save_path.write_bytes(xml_str) - def update_configuration(self, configuration_id, attribute_name, new_value): - conf_list = self.config_xml_tree_root.xpath("//mode[contains(@ID," + "'" + str(configuration_id) + "')]") - mode_to_update = conf_list[0] - mode_to_update.set(attribute_name, str(new_value)) - self.save_configurations() - - def update_configuration_without_writing(self, configuration_id, attribute_name, new_value): - conf_list = self.config_xml_tree_root.xpath("//mode[contains(@ID," + "'" + str(configuration_id) + "')]") - mode_to_update = conf_list[0] - mode_to_update.set(attribute_name, str(new_value)) - - def write_configuration_selected( - self, selected_configurations, filename - ): # to be only used with a throwaway instance - for conf in self.configurations: - self.update_configuration_without_writing(conf.id, "Selected", 0) - for conf in selected_configurations: - self.update_configuration_without_writing(conf.id, "Selected", 1) - self.write_configuration(filename) - for conf in selected_configurations: - self.update_configuration_without_writing(conf.id, "Selected", 0) - - def get_channel_color(self, channel): - channel_info = CHANNEL_COLORS_MAP.get(self.extract_wavelength(channel), {"hex": 0xFFFFFF, "name": "gray"}) - return channel_info["hex"] - - def extract_wavelength(self, name): - # Split the string and find the wavelength number immediately after "Fluorescence" - parts = name.split() - if "Fluorescence" in parts: - index = parts.index("Fluorescence") + 1 - if index < len(parts): - return parts[index].split()[0] # Assuming 'Fluorescence 488 nm Ex' and taking '488' - for color in ["R", "G", "B"]: - if color in parts or "full_" + color in parts: - return color - return None + def save_configurations(self, objective: str) -> None: + """Save configurations based on spinning disk configuration""" + if ENABLE_SPINNING_DISK_CONFOCAL: + # Save both confocal and widefield configurations + self._save_xml_config(objective, ConfigType.CONFOCAL) + self._save_xml_config(objective, ConfigType.WIDEFIELD) + else: + # Save only channel configurations + self._save_xml_config(objective, ConfigType.CHANNEL) + + def save_current_configuration_to_path(self, objective: str, path: Path) -> None: + """Only used in TrackingController. Might be temporary.""" + config = self.all_configs[self.active_config_type][objective] + xml_str = config.to_xml(pretty_print=True, encoding='utf-8') + path.write_bytes(xml_str) + + def get_configurations(self, objective: str) -> List[ChannelMode]: + """Get channel modes for current active type""" + config = self.all_configs[self.active_config_type].get(objective) + if not config: + return [] + return config.modes + + def update_configuration(self, objective: str, config_id: str, attr_name: str, value: Any) -> None: + """Update a specific configuration in current active type""" + config = self.all_configs[self.active_config_type].get(objective) + if not config: + self._log.error(f"Objective {objective} not found") + return + + for mode in config.modes: + if mode.id == config_id: + setattr(mode, utils_config.get_attr_name(attr_name), value) + break + + self.save_configurations(objective) + + def write_configuration_selected(self, objective: str, selected_configurations: List[ChannelMode], filename: str) -> None: + """Write selected configurations to a file""" + config = self.all_configs[self.active_config_type].get(objective) + if not config: + raise ValueError(f"Objective {objective} not found") + + # Update selected status + for mode in config.modes: + mode.selected = any(conf.id == mode.id for conf in selected_configurations) + + # Save to specified file + xml_str = config.to_xml(pretty_print=True, encoding='utf-8') + filename = Path(filename) + filename.write_bytes(xml_str) + + # Reset selected status + for mode in config.modes: + mode.selected = False + self.save_configurations(objective) + + def get_channel_configurations_for_objective(self, objective: str) -> List[ChannelMode]: + """Get Configuration objects for current active type (alias for get_configurations)""" + return self.get_configurations(objective) + + def toggle_confocal_widefield(self, confocal: bool) -> None: + """Toggle between confocal and widefield configurations""" + self.active_config_type = ConfigType.CONFOCAL if confocal else ConfigType.WIDEFIELD + + +class LaserAFSettingManager: + """Manages JSON-based laser autofocus configurations.""" + def __init__(self): + self.autofocus_configurations: Dict[str, LaserAFConfig] = {} # Dict[str, Dict[str, Any]] + self.current_profile_path = None + + def set_profile_path(self, profile_path: Path) -> None: + self.current_profile_path = profile_path + + def load_configurations(self, objective: str) -> None: + """Load autofocus configurations for a specific objective.""" + config_file = self.current_profile_path / objective / "laser_af_settings.json" + if config_file.exists(): + with open(config_file, 'r') as f: + config_dict = json.load(f) + self.autofocus_configurations[objective] = LaserAFConfig(**config_dict) + + def save_configurations(self, objective: str) -> None: + """Save autofocus configurations for a specific objective.""" + if objective not in self.autofocus_configurations: + return + + objective_path = self.current_profile_path / objective + if not objective_path.exists(): + objective_path.mkdir(parents=True) + config_file = objective_path / "laser_af_settings.json" + + config_dict = self.autofocus_configurations[objective].model_dump() + with open(config_file, 'w') as f: + json.dump(config_dict, f, indent=4) + + def get_settings_for_objective(self, objective: str) -> Dict[str, Any]: + if objective not in self.autofocus_configurations: + raise ValueError(f"No configuration found for objective {objective}") + return self.autofocus_configurations[objective] + + def get_laser_af_settings(self) -> Dict[str, Any]: + return self.autofocus_configurations + + def update_laser_af_settings(self, objective: str, updates: Dict[str, Any]) -> None: + if objective not in self.autofocus_configurations: + self.autofocus_configurations[objective] = LaserAFConfig(**updates) + else: + config = self.autofocus_configurations[objective] + self.autofocus_configurations[objective] = config.model_copy(update=updates) + +class ConfigurationManager: + """Main configuration manager that coordinates channel and autofocus configurations.""" + def __init__(self, + channel_manager: ChannelConfigurationManager, + laser_af_manager: Optional[LaserAFSettingManager] = None, + base_config_path: Path = Path("acquisition_configurations"), + profile: str = "default_profile"): + super().__init__() + self.base_config_path = Path(base_config_path) + self.current_profile = profile + self.available_profiles = self._get_available_profiles() + + self.channel_manager = channel_manager + self.laser_af_manager = laser_af_manager + + self.load_profile(profile) + + def _get_available_profiles(self) -> List[str]: + """Get all available user profile names in the base config path. Use default profile if no other profiles exist.""" + if not self.base_config_path.exists(): + os.makedirs(self.base_config_path) + os.makedirs(self.base_config_path / "default_profile") + for objective in OBJECTIVES: + os.makedirs(self.base_config_path / "default_profile" / objective) + return [d.name for d in self.base_config_path.iterdir() if d.is_dir()] + + def _get_available_objectives(self, profile_path: Path) -> List[str]: + """Get all available objective names in a profile.""" + return [d.name for d in profile_path.iterdir() if d.is_dir()] + + def load_profile(self, profile_name: str) -> None: + """Load all configurations from a specific profile.""" + profile_path = self.base_config_path / profile_name + if not profile_path.exists(): + raise ValueError(f"Profile {profile_name} does not exist") + + self.current_profile = profile_name + if self.channel_manager: + self.channel_manager.set_profile_path(profile_path) + if self.laser_af_manager: + self.laser_af_manager.set_profile_path(profile_path) + + # Load configurations for each objective + for objective in self._get_available_objectives(profile_path): + if self.channel_manager: + self.channel_manager.load_configurations(objective) + if self.laser_af_manager: + self.laser_af_manager.load_configurations(objective) + + def create_new_profile(self, profile_name: str) -> None: + """Create a new profile using current configurations.""" + new_profile_path = self.base_config_path / profile_name + if new_profile_path.exists(): + raise ValueError(f"Profile {profile_name} already exists") + os.makedirs(new_profile_path) + + objectives = OBJECTIVES + + self.current_profile = profile_name + if self.channel_manager: + self.channel_manager.set_profile_path(new_profile_path) + if self.laser_af_manager: + self.laser_af_manager.set_profile_path(new_profile_path) + + for objective in objectives: + os.makedirs(new_profile_path / objective) + if self.channel_manager: + self.channel_manager.save_configurations(objective) + if self.laser_af_manager: + self.laser_af_manager.save_configurations(objective) + + self.available_profiles = self._get_available_profiles() class ContrastManager: @@ -4942,4 +5072,4 @@ def get_image(self) -> Optional[np.ndarray]: finally: # turn off the laser self.microcontroller.turn_off_AF_laser() - self.microcontroller.wait_till_operation_is_completed() + self.microcontroller.wait_till_operation_is_completed() \ No newline at end of file diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 69e9e96d6..21331ef65 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -231,12 +231,17 @@ def loadObjects(self, is_simulation): # Common object initialization self.objectiveStore = core.ObjectiveStore(parent=self) - self.configurationManager = core.ConfigurationManager(filename="./channel_configurations.xml") + self.channelConfigurationManager = core.ChannelConfigurationManager() + if SUPPORT_LASER_AUTOFOCUS: + self.laserAFSettingManager = core.LaserAFSettingManager() + else: + self.laserAFSettingManager = None + self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, laser_af_manager=self.laserAFSettingManager) self.contrastManager = core.ContrastManager() self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP / 100) self.liveController = core.LiveController( - self.camera, self.microcontroller, self.configurationManager, self.illuminationController, parent=self + self.camera, self.microcontroller, self.illuminationController, parent=self ) self.slidePositionController = core.SlidePositionController( @@ -258,7 +263,8 @@ def loadObjects(self, is_simulation): self.camera, self.microcontroller, self.stage, - self.configurationManager, + self.objectiveStore, + self.channelConfigurationManager, self.liveController, self.autofocusController, self.imageDisplayWindow, @@ -277,20 +283,17 @@ def loadObjects(self, is_simulation): self.microcontroller, self.liveController, self.autofocusController, - self.configurationManager, + self.objectiveStore, + self.channelConfigurationManager, scanCoordinates=self.scanCoordinates, parent=self, ) if SUPPORT_LASER_AUTOFOCUS: - self.configurationManager_focus_camera = core.ConfigurationManager( - filename="./focus_camera_configurations.xml" - ) self.streamHandler_focus_camera = core.StreamHandler() self.liveController_focus_camera = core.LiveController( self.camera_focus, self.microcontroller, - self.configurationManager_focus_camera, self, control_illumination=False, for_displacement_measurement=True, @@ -543,7 +546,7 @@ def waitForMicrocontroller(self, timeout=5.0, error_message=None): def loadWidgets(self): # Initialize all GUI widgets if ENABLE_SPINNING_DISK_CONFOCAL: - self.spinningDiskConfocalWidget = widgets.SpinningDiskConfocalWidget(self.xlight, self.configurationManager) + self.spinningDiskConfocalWidget = widgets.SpinningDiskConfocalWidget(self.xlight, self.channelConfigurationManager) if ENABLE_NL5: import control.NL5Widget as NL5Widget @@ -563,10 +566,12 @@ def loadWidgets(self): include_camera_temperature_setting=False, include_camera_auto_wb_setting=True, ) + self.profileWidget = widgets.ProfileWidget(self.configurationManager) self.liveControlWidget = widgets.LiveControlWidget( self.streamHandler, self.liveController, - self.configurationManager, + self.objectiveStore, + self.channelConfigurationManager, show_display_options=True, show_autolevel=True, autolevel=True, @@ -618,10 +623,10 @@ def loadWidgets(self): include_camera_temperature_setting=False, include_camera_auto_wb_setting=True, ) - self.liveControlWidget_focus_camera = widgets.LiveControlWidget( + self.focusCameraControlWidget = widgets.FocusCameraControlWidget( self.streamHandler_focus_camera, self.liveController_focus_camera, - self.configurationManager_focus_camera, + self.laserAutofocusController, stretch=False, ) # ,show_display_options=True) self.waveformDisplay = widgets.WaveformDisplay(N=1000, include_x=True, include_y=False) @@ -654,7 +659,7 @@ def loadWidgets(self): self.navigationViewer, self.multipointController, self.objectiveStore, - self.configurationManager, + self.channelConfigurationManager, self.scanCoordinates, self.focusMapWidget, ) @@ -663,7 +668,7 @@ def loadWidgets(self): self.navigationViewer, self.multipointController, self.objectiveStore, - self.configurationManager, + self.channelConfigurationManager, self.scanCoordinates, self.focusMapWidget, self.napariMosaicDisplayWidget, @@ -673,11 +678,12 @@ def loadWidgets(self): if ENABLE_TRACKING: self.trackingControlWidget = widgets.TrackingControllerWidget( self.trackingController, - self.configurationManager, + self.objectiveStore, + self.channelConfigurationManager, show_configurations=TRACKING_SHOW_MICROSCOPE_CONFIGURATIONS, ) if ENABLE_STITCHER: - self.stitcherWidget = widgets.StitcherWidget(self.configurationManager, self.contrastManager) + self.stitcherWidget = widgets.StitcherWidget(self.objectiveStore, self.channelConfigurationManager, self.contrastManager) self.recordTabWidget = QTabWidget() self.setupRecordTabWidget() @@ -691,7 +697,8 @@ def setupImageDisplayTabs(self): self.streamHandler, self.liveController, self.stage, - self.configurationManager, + self.objectiveStore, + self.channelConfigurationManager, self.contrastManager, self.wellSelectionWidget, ) @@ -732,9 +739,9 @@ def setupImageDisplayTabs(self): dock_laserfocus_liveController = dock.Dock("Focus Camera Controller", autoOrientation=False) dock_laserfocus_liveController.showTitleBar() - dock_laserfocus_liveController.addWidget(self.liveControlWidget_focus_camera) + dock_laserfocus_liveController.addWidget(self.focusCameraControlWidget) dock_laserfocus_liveController.setStretch(x=100, y=100) - dock_laserfocus_liveController.setFixedWidth(self.liveControlWidget_focus_camera.minimumSizeHint().width()) + dock_laserfocus_liveController.setFixedWidth(self.focusCameraControlWidget.minimumSizeHint().width()) dock_waveform = dock.Dock("Displacement Measurement", autoOrientation=False) dock_waveform.showTitleBar() @@ -797,6 +804,7 @@ def setupLayout(self): if USE_NAPARI_FOR_LIVE_CONTROL and not self.live_only_mode: layout.addWidget(self.navigationWidget) else: + layout.addWidget(self.profileWidget) layout.addWidget(self.liveControlWidget) layout.addWidget(self.cameraTabWidget) @@ -900,6 +908,8 @@ def makeConnections(self): self.stitcherWidget.updateRegistrationZLevels ) + self.profileWidget.signal_profile_changed.connect(self.liveControlWidget.refresh_mode_list) + self.liveControlWidget.signal_newExposureTime.connect(self.cameraSettingWidget.set_exposure_time) self.liveControlWidget.signal_newAnalogGain.connect(self.cameraSettingWidget.set_analog_gain) if not self.live_only_mode: @@ -967,14 +977,23 @@ def makeConnections(self): self.wellSelectionWidget.signal_wellSelected.connect(self.wellplateMultiPointWidget.update_well_coordinates) self.objectivesWidget.signal_objective_changed.connect(self.wellplateMultiPointWidget.update_coordinates) + self.objectivesWidget.signal_objective_changed.connect(lambda: self.liveControlWidget.update_microscope_mode_by_name( + self.liveControlWidget.currentConfiguration.name + )) + if SUPPORT_LASER_AUTOFOCUS: - self.liveControlWidget_focus_camera.signal_newExposureTime.connect( - self.cameraSettingWidget_focus_camera.set_exposure_time + def connect_objective_changed_laser_af(): + self.laserAutofocusController.on_objective_changed() + self.laserAutofocusControlWidget.update_init_state() + + self.objectivesWidget.signal_objective_changed.connect( + connect_objective_changed_laser_af ) - self.liveControlWidget_focus_camera.signal_newAnalogGain.connect( - self.cameraSettingWidget_focus_camera.set_analog_gain + self.focusCameraControlWidget.signal_newExposureTime.connect( + self.cameraSettingWidget_focus_camera.set_exposure_time ) - self.liveControlWidget_focus_camera.update_camera_settings() + + self.focusCameraControlWidget.update_exposure_time(self.focusCameraControlWidget.exposure_spinbox.value()) self.streamHandler_focus_camera.signal_new_frame_received.connect( self.liveController_focus_camera.on_new_frame diff --git a/software/control/utils.py b/software/control/utils.py index 1ddb550e9..e7a6ff57e 100644 --- a/software/control/utils.py +++ b/software/control/utils.py @@ -11,6 +11,7 @@ from scipy.ndimage import label from scipy import signal import os +from control._def import CHANNEL_COLORS_MAP from typing import Optional, Tuple from enum import Enum, auto import squid.logging @@ -174,6 +175,22 @@ def ensure_directory_exists(raw_string_path: str): path.mkdir(parents=True, exist_ok=True) +def extract_wavelength_from_config_name(name): + # Split the string and find the wavelength number immediately after "Fluorescence" + parts = name.split() + if "Fluorescence" in parts: + index = parts.index("Fluorescence") + 1 + if index < len(parts): + return parts[index].split()[0] # Assuming 'Fluorescence 488 nm Ex' and taking '488' + for color in ["R", "G", "B"]: + if color in parts or "full_" + color in parts: + return color + return None + + +def get_channel_color(channel): + channel_info = CHANNEL_COLORS_MAP.get(extract_wavelength_from_config_name(channel), {"hex": 0xFFFFFF, "name": "gray"}) + return channel_info["hex"] class SpotDetectionMode(Enum): """Specifies which spot to detect when multiple spots are present. @@ -395,4 +412,4 @@ def get_script_dir(follow_symlinks=True): return f"{repo.head.object.hexsha} (dirty={repo.is_dirty()})" except git.GitError as e: _log.warning(f"Failed to get script git repo info: {e}") - return None + return None \ No newline at end of file diff --git a/software/control/utils_config.py b/software/control/utils_config.py index 4eb2b8a15..5cb0f1614 100644 --- a/software/control/utils_config.py +++ b/software/control/utils_config.py @@ -1,254 +1,263 @@ -from lxml import etree as ET +from pydantic import BaseModel +from pydantic_xml import BaseXmlModel, element, attr +from typing import List, Optional +from pathlib import Path +import control.utils as utils -top = ET.Element("modes") +class LaserAFConfig(BaseModel): + """Pydantic model for laser autofocus configuration""" + x_offset: float = 0.0 + y_offset: float = 0.0 + width: int = 1536 + height: int = 256 + pixel_to_um: float = 0.4 + x_reference: float = 0.0 + has_two_interfaces: bool = False + use_glass_top: bool = True + focus_camera_exposure_time_ms: int = 2 + focus_camera_analog_gain: int = 0 -def generate_default_configuration(filename): +class ChannelMode(BaseXmlModel, tag='mode'): + """Channel configuration model""" + id: str = attr(name='ID') + name: str = attr(name='Name') + exposure_time: float = attr(name='ExposureTime') + analog_gain: float = attr(name='AnalogGain') + illumination_source: int = attr(name='IlluminationSource') + illumination_intensity: float = attr(name='IlluminationIntensity') + camera_sn: Optional[str] = attr(name='CameraSN', default=None) + z_offset: float = attr(name='ZOffset') + emission_filter_position: int = attr(name='EmissionFilterPosition', default=1) + selected: bool = attr(name='Selected', default=False) + color: Optional[str] = None # Not stored in XML but computed from name - mode_1 = ET.SubElement(top, "mode") - mode_1.set("ID", "1") - mode_1.set("Name", "BF LED matrix full") - mode_1.set("ExposureTime", "12") - mode_1.set("AnalogGain", "0") - mode_1.set("IlluminationSource", "0") - mode_1.set("IlluminationIntensity", "5") - mode_1.set("CameraSN", "") - mode_1.set("ZOffset", "0.0") - mode_1.set("PixelFormat", "default") - mode_1.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_1.set("EmissionFilterPosition", "1") + def __init__(self, **data): + super().__init__(**data) + self.color = utils.get_channel_color(self.name) - mode_4 = ET.SubElement(top, "mode") - mode_4.set("ID", "4") - mode_4.set("Name", "DF LED matrix") - mode_4.set("ExposureTime", "22") - mode_4.set("AnalogGain", "0") - mode_4.set("IlluminationSource", "3") - mode_4.set("IlluminationIntensity", "5") - mode_4.set("CameraSN", "") - mode_4.set("ZOffset", "0.0") - mode_4.set("PixelFormat", "default") - mode_4.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_4.set("EmissionFilterPosition", "1") +class ChannelConfig(BaseXmlModel, tag='modes'): + """Root configuration file model""" + modes: List[ChannelMode] = element(tag='mode') - mode_5 = ET.SubElement(top, "mode") - mode_5.set("ID", "5") - mode_5.set("Name", "Fluorescence 405 nm Ex") - mode_5.set("ExposureTime", "100") - mode_5.set("AnalogGain", "10") - mode_5.set("IlluminationSource", "11") - mode_5.set("IlluminationIntensity", "100") - mode_5.set("CameraSN", "") - mode_5.set("ZOffset", "0.0") - mode_5.set("PixelFormat", "default") - mode_5.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_5.set("EmissionFilterPosition", "1") +def get_attr_name(attr_name: str) -> str: + """Get the attribute name for a given configuration attribute""" + attr_map = { + 'ID': 'id', + 'Name': 'name', + 'ExposureTime': 'exposure_time', + 'AnalogGain': 'analog_gain', + 'IlluminationSource': 'illumination_source', + 'IlluminationIntensity': 'illumination_intensity', + 'CameraSN': 'camera_sn', + 'ZOffset': 'z_offset', + 'EmissionFilterPosition': 'emission_filter_position', + 'Selected': 'selected', + 'Color': 'color' + } + return attr_map[attr_name] - mode_6 = ET.SubElement(top, "mode") - mode_6.set("ID", "6") - mode_6.set("Name", "Fluorescence 488 nm Ex") - mode_6.set("ExposureTime", "100") - mode_6.set("AnalogGain", "10") - mode_6.set("IlluminationSource", "12") - mode_6.set("IlluminationIntensity", "100") - mode_6.set("CameraSN", "") - mode_6.set("ZOffset", "0.0") - mode_6.set("PixelFormat", "default") - mode_6.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_6.set("EmissionFilterPosition", "1") +def generate_default_configuration(filename: str) -> None: + """Generate default configuration using Pydantic models""" + default_modes = [ + ChannelMode( + id="1", + name="BF LED matrix full", + exposure_time=12, + analog_gain=0, + illumination_source=0, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="4", + name="DF LED matrix", + exposure_time=22, + analog_gain=0, + illumination_source=3, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="5", + name="Fluorescence 405 nm Ex", + exposure_time=100, + analog_gain=10, + illumination_source=11, + illumination_intensity=100, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="6", + name="Fluorescence 488 nm Ex", + exposure_time=100, + analog_gain=10, + illumination_source=12, + illumination_intensity=100, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="7", + name="Fluorescence 638 nm Ex", + exposure_time=100, + analog_gain=10, + illumination_source=13, + illumination_intensity=100, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="8", + name="Fluorescence 561 nm Ex", + exposure_time=100, + analog_gain=10, + illumination_source=14, + illumination_intensity=100, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="12", + name="Fluorescence 730 nm Ex", + exposure_time=50, + analog_gain=10, + illumination_source=15, + illumination_intensity=100, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="9", + name="BF LED matrix low NA", + exposure_time=20, + analog_gain=0, + illumination_source=4, + illumination_intensity=20, + camera_sn="", + z_offset=0.0 + ), + # Commented out modes for reference + # ChannelMode( + # id="10", + # name="BF LED matrix left dot", + # exposure_time=20, + # analog_gain=0, + # illumination_source=5, + # illumination_intensity=20, + # camera_sn="", + # z_offset=0.0 + # ), + # ChannelMode( + # id="11", + # name="BF LED matrix right dot", + # exposure_time=20, + # analog_gain=0, + # illumination_source=6, + # illumination_intensity=20, + # camera_sn="", + # z_offset=0.0 + # ), + ChannelMode( + id="2", + name="BF LED matrix left half", + exposure_time=16, + analog_gain=0, + illumination_source=1, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="3", + name="BF LED matrix right half", + exposure_time=16, + analog_gain=0, + illumination_source=2, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="12", + name="BF LED matrix top half", + exposure_time=20, + analog_gain=0, + illumination_source=7, + illumination_intensity=20, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="13", + name="BF LED matrix bottom half", + exposure_time=20, + analog_gain=0, + illumination_source=8, + illumination_intensity=20, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="14", + name="BF LED matrix full_R", + exposure_time=12, + analog_gain=0, + illumination_source=0, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="15", + name="BF LED matrix full_G", + exposure_time=12, + analog_gain=0, + illumination_source=0, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="16", + name="BF LED matrix full_B", + exposure_time=12, + analog_gain=0, + illumination_source=0, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="21", + name="BF LED matrix full_RGB", + exposure_time=12, + analog_gain=0, + illumination_source=0, + illumination_intensity=5, + camera_sn="", + z_offset=0.0 + ), + ChannelMode( + id="20", + name="USB Spectrometer", + exposure_time=20, + analog_gain=0, + illumination_source=6, + illumination_intensity=0, + camera_sn="", + z_offset=0.0 + ) + ] - mode_7 = ET.SubElement(top, "mode") - mode_7.set("ID", "7") - mode_7.set("Name", "Fluorescence 638 nm Ex") - mode_7.set("ExposureTime", "100") - mode_7.set("AnalogGain", "10") - mode_7.set("IlluminationSource", "13") - mode_7.set("IlluminationIntensity", "100") - mode_7.set("CameraSN", "") - mode_7.set("ZOffset", "0.0") - mode_7.set("PixelFormat", "default") - mode_7.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_7.set("EmissionFilterPosition", "1") - - mode_8 = ET.SubElement(top, "mode") - mode_8.set("ID", "8") - mode_8.set("Name", "Fluorescence 561 nm Ex") - mode_8.set("ExposureTime", "100") - mode_8.set("AnalogGain", "10") - mode_8.set("IlluminationSource", "14") - mode_8.set("IlluminationIntensity", "100") - mode_8.set("CameraSN", "") - mode_8.set("ZOffset", "0.0") - mode_8.set("PixelFormat", "default") - mode_8.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_8.set("EmissionFilterPosition", "1") - - mode_12 = ET.SubElement(top, "mode") - mode_12.set("ID", "12") - mode_12.set("Name", "Fluorescence 730 nm Ex") - mode_12.set("ExposureTime", "50") - mode_12.set("AnalogGain", "10") - mode_12.set("IlluminationSource", "15") - mode_12.set("IlluminationIntensity", "100") - mode_12.set("CameraSN", "") - mode_12.set("ZOffset", "0.0") - mode_12.set("PixelFormat", "default") - mode_12.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_12.set("EmissionFilterPosition", "1") - - mode_9 = ET.SubElement(top, "mode") - mode_9.set("ID", "9") - mode_9.set("Name", "BF LED matrix low NA") - mode_9.set("ExposureTime", "20") - mode_9.set("AnalogGain", "0") - mode_9.set("IlluminationSource", "4") - mode_9.set("IlluminationIntensity", "20") - mode_9.set("CameraSN", "") - mode_9.set("ZOffset", "0.0") - mode_9.set("PixelFormat", "default") - mode_9.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_9.set("EmissionFilterPosition", "1") - - # mode_10 = ET.SubElement(top,'mode') - # mode_10.set('ID','10') - # mode_10.set('Name','BF LED matrix left dot') - # mode_10.set('ExposureTime','20') - # mode_10.set('AnalogGain','0') - # mode_10.set('IlluminationSource','5') - # mode_10.set('IlluminationIntensity','20') - # mode_10.set('CameraSN','') - # mode_10.set('ZOffset','0.0') - # mode_10.set('PixelFormat','default') - # mode_10.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - # mode_11 = ET.SubElement(top,'mode') - # mode_11.set('ID','11') - # mode_11.set('Name','BF LED matrix right dot') - # mode_11.set('ExposureTime','20') - # mode_11.set('AnalogGain','0') - # mode_11.set('IlluminationSource','6') - # mode_11.set('IlluminationIntensity','20') - # mode_11.set('CameraSN','') - # mode_11.set('ZOffset','0.0') - # mode_11.set('PixelFormat','default') - # mode_11.set('_PixelFormat_options','[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]') - - mode_2 = ET.SubElement(top, "mode") - mode_2.set("ID", "2") - mode_2.set("Name", "BF LED matrix left half") - mode_2.set("ExposureTime", "16") - mode_2.set("AnalogGain", "0") - mode_2.set("IlluminationSource", "1") - mode_2.set("IlluminationIntensity", "5") - mode_2.set("CameraSN", "") - mode_2.set("ZOffset", "0.0") - mode_2.set("PixelFormat", "default") - mode_2.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_2.set("EmissionFilterPosition", "1") - - mode_3 = ET.SubElement(top, "mode") - mode_3.set("ID", "3") - mode_3.set("Name", "BF LED matrix right half") - mode_3.set("ExposureTime", "16") - mode_3.set("AnalogGain", "0") - mode_3.set("IlluminationSource", "2") - mode_3.set("IlluminationIntensity", "5") - mode_3.set("CameraSN", "") - mode_3.set("ZOffset", "0.0") - mode_3.set("PixelFormat", "default") - mode_3.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_3.set("EmissionFilterPosition", "1") - - mode_12 = ET.SubElement(top, "mode") - mode_12.set("ID", "12") - mode_12.set("Name", "BF LED matrix top half") - mode_12.set("ExposureTime", "20") - mode_12.set("AnalogGain", "0") - mode_12.set("IlluminationSource", "7") - mode_12.set("IlluminationIntensity", "20") - mode_12.set("CameraSN", "") - mode_12.set("ZOffset", "0.0") - mode_12.set("PixelFormat", "default") - mode_12.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_12.set("EmissionFilterPosition", "1") - - mode_13 = ET.SubElement(top, "mode") - mode_13.set("ID", "13") - mode_13.set("Name", "BF LED matrix bottom half") - mode_13.set("ExposureTime", "20") - mode_13.set("AnalogGain", "0") - mode_13.set("IlluminationSource", "8") - mode_13.set("IlluminationIntensity", "20") - mode_13.set("CameraSN", "") - mode_13.set("ZOffset", "0.0") - mode_13.set("PixelFormat", "default") - mode_13.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_13.set("EmissionFilterPosition", "1") - - mode_14 = ET.SubElement(top, "mode") - mode_14.set("ID", "1") - mode_14.set("Name", "BF LED matrix full_R") - mode_14.set("ExposureTime", "12") - mode_14.set("AnalogGain", "0") - mode_14.set("IlluminationSource", "0") - mode_14.set("IlluminationIntensity", "5") - mode_14.set("CameraSN", "") - mode_14.set("ZOffset", "0.0") - mode_14.set("PixelFormat", "default") - mode_14.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_14.set("EmissionFilterPosition", "1") - - mode_15 = ET.SubElement(top, "mode") - mode_15.set("ID", "1") - mode_15.set("Name", "BF LED matrix full_G") - mode_15.set("ExposureTime", "12") - mode_15.set("AnalogGain", "0") - mode_15.set("IlluminationSource", "0") - mode_15.set("IlluminationIntensity", "5") - mode_15.set("CameraSN", "") - mode_15.set("ZOffset", "0.0") - mode_15.set("PixelFormat", "default") - mode_15.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_15.set("EmissionFilterPosition", "1") - - mode_16 = ET.SubElement(top, "mode") - mode_16.set("ID", "1") - mode_16.set("Name", "BF LED matrix full_B") - mode_16.set("ExposureTime", "12") - mode_16.set("AnalogGain", "0") - mode_16.set("IlluminationSource", "0") - mode_16.set("IlluminationIntensity", "5") - mode_16.set("CameraSN", "") - mode_16.set("ZOffset", "0.0") - mode_16.set("PixelFormat", "default") - mode_16.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_16.set("EmissionFilterPosition", "1") - - mode_21 = ET.SubElement(top, "mode") - mode_21.set("ID", "21") - mode_21.set("Name", "BF LED matrix full_RGB") - mode_21.set("ExposureTime", "12") - mode_21.set("AnalogGain", "0") - mode_21.set("IlluminationSource", "0") - mode_21.set("IlluminationIntensity", "5") - mode_21.set("CameraSN", "") - mode_21.set("ZOffset", "0.0") - mode_21.set("PixelFormat", "default") - mode_21.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_21.set("EmissionFilterPosition", "1") - - mode_20 = ET.SubElement(top, "mode") - mode_20.set("ID", "20") - mode_20.set("Name", "USB Spectrometer") - mode_20.set("ExposureTime", "20") - mode_20.set("AnalogGain", "0") - mode_20.set("IlluminationSource", "6") - mode_20.set("IlluminationIntensity", "0") - mode_20.set("CameraSN", "") - mode_20.set("ZOffset", "0.0") - mode_20.set("PixelFormat", "default") - mode_20.set("_PixelFormat_options", "[default,MONO8,MONO12,MONO14,MONO16,BAYER_RG8,BAYER_RG12]") - mode_20.set("EmissionFilterPosition", "1") - - tree = ET.ElementTree(top) - tree.write(filename, encoding="utf-8", xml_declaration=True, pretty_print=True) + config = ChannelConfig(modes=default_modes) + xml_str = config.to_xml(pretty_print=True, encoding='utf-8') + + # Write to file + path = Path(filename) + if not path.parent.exists(): + path.parent.mkdir(parents=True) + path.write_bytes(xml_str) \ No newline at end of file diff --git a/software/control/widgets.py b/software/control/widgets.py index 02c9e3f09..1d2788836 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -66,7 +66,8 @@ def __init__(self, title): def toggle_content(self, state): self.content_widget.setVisible(state) - +''' +# Planning to replace this with a better design class ConfigEditorForAcquisitions(QDialog): def __init__(self, configManager, only_z_offset=True): super().__init__() @@ -203,7 +204,7 @@ def load_config_from_file(self, only_z_offset=None): self.scroll_area_widget.setLayout(self.scroll_area_layout) self.scroll_area.setWidget(self.scroll_area_widget) self.init_ui(only_z_offset) - +''' class ConfigEditor(QDialog): def __init__(self, config): @@ -343,6 +344,120 @@ def apply_and_exit(self): self.close() +class FocusCameraControlWidget(QWidget): + + signal_newExposureTime = Signal(float) + signal_start_live = Signal() + + def __init__(self, streamHandler, liveController, laser_af_controller, stretch=True): + super().__init__() + self.streamHandler = streamHandler + self.liveController = liveController + self.laser_af_controller = laser_af_controller + self.stretch = stretch + + # Enable background filling + self.setAutoFillBackground(True) + + # Create and set background color + palette = self.palette() + palette.setColor(self.backgroundRole(), QColor(240, 240, 240)) + self.setPalette(palette) + + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + layout.setContentsMargins(9, 9, 9, 9) + + # Live control group + live_group = QFrame() + live_group.setFrameStyle(QFrame.Panel | QFrame.Raised) + live_layout = QVBoxLayout() + + # Live button + self.btn_live = QPushButton("Start Live") + self.btn_live.setCheckable(True) + self.btn_live.setStyleSheet("background-color: #C2C2FF") + + # Exposure time control + exposure_layout = QHBoxLayout() + exposure_layout.addWidget(QLabel("Focus Camera Exposure (ms):")) + self.exposure_spinbox = QDoubleSpinBox() + self.exposure_spinbox.setRange(0, 1000) + self.exposure_spinbox.setValue(2) + self.exposure_spinbox.setDecimals(1) + exposure_layout.addWidget(self.exposure_spinbox) + + # Add to live group + live_layout.addWidget(self.btn_live) + live_layout.addLayout(exposure_layout) + live_group.setLayout(live_layout) + + # Settings group + settings_group = QFrame() + settings_group.setFrameStyle(QFrame.Panel | QFrame.Raised) + settings_layout = QVBoxLayout() + + # Two interfaces checkbox + self.has_two_interfaces_cb = QCheckBox("Has Two Interfaces") + + # Glass top checkbox + self.use_glass_top_cb = QCheckBox("Use Glass Top") + + # Range + ''' + range_layout = QHBoxLayout() + range_layout.addWidget(QLabel("Laser AF Range (μm):")) + self.range_spinbox = QDoubleSpinBox() + self.range_spinbox.setRange(0, 1000) + self.range_spinbox.setValue(100) + self.range_spinbox.setDecimals(1) + range_layout.addWidget(self.range_spinbox) + layout.addLayout(range_layout) + ''' + + # Apply button + self.apply_button = QPushButton("Apply and Initialize") + + # Add settings controls + settings_layout.addWidget(self.has_two_interfaces_cb) + settings_layout.addWidget(self.use_glass_top_cb) + settings_layout.addWidget(self.apply_button) + settings_group.setLayout(settings_layout) + + # Add to main layout + layout.addWidget(live_group) + layout.addWidget(settings_group) + self.setLayout(layout) + + if not self.stretch: + layout.addStretch() + + # Connect all signals to slots + self.btn_live.clicked.connect(self.toggle_live) + self.exposure_spinbox.valueChanged.connect(self.update_exposure_time) + self.apply_button.clicked.connect(self.apply_settings) + + def toggle_live(self, pressed): + if pressed: + self.liveController.start_live() + self.btn_live.setText("Stop Live") + else: + self.liveController.stop_live() + self.btn_live.setText("Start Live") + + def update_exposure_time(self, value): + self.signal_newExposureTime.emit(value) + + def apply_settings(self): + self.laser_af_controller.set_laser_af_properties( + has_two_interfaces=self.has_two_interfaces_cb.isChecked(), + use_glass_top=self.use_glass_top_cb.isChecked(), + ) + self.laser_af_controller.initialize_auto() + + class SpinningDiskConfocalWidget(QWidget): def __init__(self, xlight, config_manager=None): super(SpinningDiskConfocalWidget, self).__init__() @@ -361,17 +476,12 @@ def __init__(self, xlight, config_manager=None): self.disk_position_state = self.xlight.get_disk_position() + if self.config_manager: + self.config_manager.toggle_confocal_widefield(self.disk_position_state) + if self.disk_position_state == 1: self.btn_toggle_widefield.setText("Switch to Widefield") - if self.config_manager is not None: - if self.disk_position_state == 1: - self.config_manager.config_filename = "confocal_configurations.xml" - else: - self.config_manager.config_filename = "widefield_configurations.xml" - self.config_manager.configurations = [] - self.config_manager.read_configurations() - self.btn_toggle_widefield.clicked.connect(self.toggle_disk_position) self.btn_toggle_motor.clicked.connect(self.toggle_motor) @@ -487,12 +597,7 @@ def toggle_disk_position(self): self.disk_position_state = self.xlight.set_disk_position(1) self.btn_toggle_widefield.setText("Switch to Widefield") if self.config_manager is not None: - if self.disk_position_state == 1: - self.config_manager.config_filename = "confocal_configurations.xml" - else: - self.config_manager.config_filename = "widefield_configurations.xml" - self.config_manager.configurations = [] - self.config_manager.read_configurations() + self.config_manager.toggle_confocal_widefield(self.disk_position_state) self.enable_all_buttons() def toggle_motor(self): @@ -855,6 +960,78 @@ def update_blacklevel(self, blacklevel): pass +class ProfileWidget(QFrame): + + signal_profile_changed = Signal() + + def __init__( + self, + configurationManager, + *args, + **kwargs + ): + super().__init__(*args, **kwargs) + self.configurationManager = configurationManager + + self.setFrameStyle(QFrame.Panel | QFrame.Raised) + self.setup_ui() + + def setup_ui(self): + # Create widgets + self.dropdown_profiles = QComboBox() + self.dropdown_profiles.addItems(self.configurationManager.available_profiles) + self.dropdown_profiles.setCurrentText(self.configurationManager.current_profile) + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.dropdown_profiles.setSizePolicy(sizePolicy) + + self.btn_loadProfile = QPushButton("Load") + self.btn_newProfile = QPushButton("Save As") + + # Connect signals + self.btn_loadProfile.clicked.connect(self.load_profile) + self.btn_newProfile.clicked.connect(self.create_new_profile) + + # Layout + layout = QHBoxLayout() + layout.addWidget(QLabel("Configuration Profile")) + layout.addWidget(self.dropdown_profiles, 2) + layout.addWidget(self.btn_loadProfile) + layout.addWidget(self.btn_newProfile) + + self.setLayout(layout) + + def load_profile(self): + """Load the selected profile.""" + profile_name = self.dropdown_profiles.currentText() + # Load the profile + self.configurationManager.load_profile(profile_name) + self.signal_profile_changed.emit() + + def create_new_profile(self): + """Create a new profile with current configurations.""" + dialog = QInputDialog() + profile_name, ok = dialog.getText( + self, + "New Profile", + "Enter new profile name:", + QLineEdit.Normal, + "" + ) + + if ok and profile_name: + try: + self.configurationManager.create_new_profile(profile_name) + # Update profile dropdown + self.dropdown_profiles.addItem(profile_name) + self.dropdown_profiles.setCurrentText(profile_name) + except ValueError as e: + QMessageBox.warning(self, "Error", str(e)) + + def get_current_profile(self): + """Return the currently selected profile name.""" + return self.dropdown_profiles.currentText() + + class LiveControlWidget(QFrame): signal_newExposureTime = Signal(float) @@ -867,7 +1044,8 @@ def __init__( self, streamHandler, liveController, - configurationManager=None, + objectiveStore, + channelConfigurationManager, show_trigger_options=True, show_display_options=False, show_autolevel=False, @@ -880,15 +1058,15 @@ def __init__( super().__init__(*args, **kwargs) self.liveController = liveController self.streamHandler = streamHandler - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.channelConfigurationManager = channelConfigurationManager self.fps_trigger = 10 self.fps_display = 10 self.liveController.set_trigger_fps(self.fps_trigger) self.streamHandler.set_display_fps(self.fps_display) self.triggerMode = TriggerMode.SOFTWARE - # note that this references the object in self.configurationManager.configurations - self.currentConfiguration = self.configurationManager.configurations[0] + self.currentConfiguration = self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)[0] self.add_components(show_trigger_options, show_display_options, show_autolevel, autolevel, stretch) self.setFrameStyle(QFrame.Panel | QFrame.Raised) @@ -914,7 +1092,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole # line 2: choose microscope mode / toggle live mode self.dropdown_modeSelection = QComboBox() - for microscope_configuration in self.configurationManager.configurations: + for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.dropdown_modeSelection.addItems([microscope_configuration.name]) self.dropdown_modeSelection.setCurrentText(self.currentConfiguration.name) self.dropdown_modeSelection.setSizePolicy(sizePolicy) @@ -1084,16 +1262,28 @@ def update_camera_settings(self): self.signal_newAnalogGain.emit(self.entry_analogGain.value()) self.signal_newExposureTime.emit(self.entry_exposureTime.value()) + def refresh_mode_list(self): + # Update the mode selection dropdown + self.dropdown_modeSelection.blockSignals(True) + self.dropdown_modeSelection.clear() + for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + self.dropdown_modeSelection.addItem(microscope_configuration.name) + self.dropdown_modeSelection.blockSignals(False) + + # Update to first configuration + if self.dropdown_modeSelection.count() > 0: + self.update_microscope_mode_by_name(self.dropdown_modeSelection.currentText()) + def update_microscope_mode_by_name(self, current_microscope_mode_name): self.is_switching_mode = True - # identify the mode selected (note that this references the object in self.configurationManager.configurations) + # identify the mode selected (note that this references the object in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)) self.currentConfiguration = next( ( config - for config in self.configurationManager.configurations + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == current_microscope_mode_name - ), - None, + ), + None, ) self.signal_live_configuration.emit(self.currentConfiguration) # update the microscope to the current configuration @@ -1110,20 +1300,20 @@ def update_trigger_mode(self): def update_config_exposure_time(self, new_value): if self.is_switching_mode == False: self.currentConfiguration.exposure_time = new_value - self.configurationManager.update_configuration(self.currentConfiguration.id, "ExposureTime", new_value) + self.channelConfigurationManager.update_configuration(self.objectiveStore.current_objective, self.currentConfiguration.id, "ExposureTime", new_value) self.signal_newExposureTime.emit(new_value) def update_config_analog_gain(self, new_value): if self.is_switching_mode == False: self.currentConfiguration.analog_gain = new_value - self.configurationManager.update_configuration(self.currentConfiguration.id, "AnalogGain", new_value) + self.channelConfigurationManager.update_configuration(self.objectiveStore.current_objective, self.currentConfiguration.id, "AnalogGain", new_value) self.signal_newAnalogGain.emit(new_value) def update_config_illumination_intensity(self, new_value): if self.is_switching_mode == False: self.currentConfiguration.illumination_intensity = new_value - self.configurationManager.update_configuration( - self.currentConfiguration.id, "IlluminationIntensity", new_value + self.channelConfigurationManager.update_configuration( + self.objectiveStore.current_objective, self.currentConfiguration.id, "IlluminationIntensity", new_value ) self.liveController.set_illumination( self.currentConfiguration.illumination_source, self.currentConfiguration.illumination_intensity @@ -1891,7 +2081,7 @@ def __init__( navigationViewer, multipointController, objectiveStore, - configurationManager, + channelConfigurationManager, scanCoordinates, focusMapWidget, *args, @@ -1906,7 +2096,7 @@ def __init__( self.navigationViewer = navigationViewer self.multipointController = multipointController self.objectiveStore = objectiveStore - self.configurationManager = configurationManager + self.channelConfigurationManager = channelConfigurationManager self.scanCoordinates = scanCoordinates self.focusMapWidget = focusMapWidget self.base_path_is_set = False @@ -2058,7 +2248,7 @@ def add_components(self): self.entry_Nt.setFixedWidth(max_num_width) self.list_configurations = QListWidget() - for microscope_configuration in self.configurationManager.configurations: + for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.list_configurations.addItems([microscope_configuration.name]) self.list_configurations.setSelectionMode( QAbstractItemView.MultiSelection @@ -3088,7 +3278,7 @@ def __init__( navigationViewer, multipointController, objectiveStore, - configurationManager, + channelConfigurationManager, scanCoordinates, focusMapWidget=None, napariMosaicWidget=None, @@ -3101,7 +3291,7 @@ def __init__( self.navigationViewer = navigationViewer self.multipointController = multipointController self.objectiveStore = objectiveStore - self.configurationManager = configurationManager + self.channelConfigurationManager = channelConfigurationManager self.scanCoordinates = scanCoordinates self.focusMapWidget = focusMapWidget if napariMosaicWidget is None: @@ -3217,7 +3407,7 @@ def add_components(self): self.combobox_z_stack.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.list_configurations = QListWidget() - for microscope_configuration in self.configurationManager.configurations: + for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.list_configurations.addItems([microscope_configuration.name]) self.list_configurations.setSelectionMode(QAbstractItemView.MultiSelection) @@ -4133,9 +4323,10 @@ def resizeEvent(self, event): class StitcherWidget(QFrame): - def __init__(self, configurationManager, contrastManager, *args, **kwargs): + def __init__(self, objectiveStore, channelConfigurationManager, contrastManager, *args, **kwargs): super(StitcherWidget, self).__init__(*args, **kwargs) - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.channelConfigurationManager = channelConfigurationManager self.contrastManager = contrastManager self.stitcherThread = None self.output_path = "" @@ -4366,7 +4557,8 @@ def __init__( streamHandler, liveController, stage: AbstractStage, - configurationManager, + objectiveStore, + channelConfigurationManager, contrastManager, wellSelectionWidget=None, show_trigger_options=True, @@ -4379,7 +4571,8 @@ def __init__( self.streamHandler = streamHandler self.liveController = liveController self.stage = stage - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.channelConfigurationManager = channelConfigurationManager self.wellSelectionWidget = wellSelectionWidget self.live_configuration = self.liveController.currentConfiguration self.image_width = 0 @@ -4451,7 +4644,7 @@ def initControlWidgets(self, show_trigger_options, show_display_options, show_au # Microscope Configuration self.dropdown_modeSelection = QComboBox() - for config in self.configurationManager.configurations: + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.dropdown_modeSelection.addItem(config.name) self.dropdown_modeSelection.setCurrentText(self.live_configuration.name) self.dropdown_modeSelection.currentTextChanged.connect(self.update_microscope_mode_by_name) @@ -4710,7 +4903,7 @@ def update_microscope_mode_by_name(self, current_microscope_mode_name): self.live_configuration = next( ( config - for config in self.configurationManager.configurations + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == current_microscope_mode_name ), None, @@ -4723,17 +4916,17 @@ def update_microscope_mode_by_name(self, current_microscope_mode_name): def update_config_exposure_time(self, new_value): self.live_configuration.exposure_time = new_value - self.configurationManager.update_configuration(self.live_configuration.id, "ExposureTime", new_value) + self.channelConfigurationManager.update_configuration(self.objectiveStore.current_objective, self.live_configuration.id, "ExposureTime", new_value) self.signal_newExposureTime.emit(new_value) def update_config_analog_gain(self, new_value): self.live_configuration.analog_gain = new_value - self.configurationManager.update_configuration(self.live_configuration.id, "AnalogGain", new_value) + self.channelConfigurationManager.update_configuration(self.objectiveStore.current_objective, self.live_configuration.id, "AnalogGain", new_value) self.signal_newAnalogGain.emit(new_value) def update_config_illumination_intensity(self, new_value): self.live_configuration.illumination_intensity = new_value - self.configurationManager.update_configuration(self.live_configuration.id, "IlluminationIntensity", new_value) + self.channelConfigurationManager.update_configuration(self.objectiveStore.current_objective, self.live_configuration.id, "IlluminationIntensity", new_value) self.liveController.set_illumination(self.live_configuration.illumination_source, new_value) def update_resolution_scaling(self, value): @@ -5482,7 +5675,8 @@ class TrackingControllerWidget(QFrame): def __init__( self, trackingController: TrackingController, - configurationManager, + objectiveStore, + channelConfigurationManager, show_configurations=True, main=None, *args, @@ -5490,7 +5684,8 @@ def __init__( ): super().__init__(*args, **kwargs) self.trackingController = trackingController - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.channelConfigurationManager = channelConfigurationManager self.base_path_is_set = False self.add_components(show_configurations) self.setFrameStyle(QFrame.Panel | QFrame.Raised) @@ -5528,7 +5723,7 @@ def add_components(self, show_configurations): self.entry_tracking_interval.setValue(0) self.list_configurations = QListWidget() - for microscope_configuration in self.configurationManager.configurations: + for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.list_configurations.addItems([microscope_configuration.name]) self.list_configurations.setSelectionMode( QAbstractItemView.MultiSelection @@ -6377,6 +6572,12 @@ def init_controller(self): self.btn_measure_displacement.setEnabled(True) self.btn_move_to_target.setEnabled(True) + def update_init_state(self): + self.btn_initialize.setChecked(self.laserAutofocusController.is_initialized) + self.btn_set_reference.setEnabled(self.laserAutofocusController.is_initialized) + self.btn_measure_displacement.setEnabled(self.laserAutofocusController.is_initialized) + self.btn_move_to_target.setEnabled(self.laserAutofocusController.is_initialized) + def move_to_target(self, target_um): self.laserAutofocusController.move_to_target(self.entry_target.value()) diff --git a/software/main_hcs.py b/software/main_hcs.py index 8c35f5edb..664e48809 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -19,11 +19,13 @@ # app specific libraries import control.gui_hcs as gui from configparser import ConfigParser -from control.widgets import ConfigEditorBackwardsCompatible, ConfigEditorForAcquisitions +from control.widgets import ConfigEditorBackwardsCompatible from control._def import CACHED_CONFIG_FILE_PATH from control._def import USE_TERMINAL_CONSOLE +from control._def import SUPPORT_LASER_AUTOFOCUS import control.utils + if USE_TERMINAL_CONSOLE: from control.console import ConsoleThread @@ -32,11 +34,12 @@ def show_config(cfp, configpath, main_gui): config_widget = ConfigEditorBackwardsCompatible(cfp, configpath, main_gui) config_widget.exec_() - +''' +# Planning to replace this with a better design def show_acq_config(cfm): acq_config_widget = ConfigEditorForAcquisitions(cfm) acq_config_widget.exec_() - +''' if __name__ == "__main__": parser = argparse.ArgumentParser() @@ -55,7 +58,8 @@ def show_acq_config(cfm): log.error("Couldn't setup logging to file!") sys.exit(1) - log.info(f"Squid Repository State: {control.utils.get_squid_repo_state_description()}") + log.info(f"Squid Repository State: { +.get_squid_repo_state_description()}") legacy_config = False cf_editor_parser = ConfigParser() @@ -72,11 +76,14 @@ def show_acq_config(cfm): win = gui.HighContentScreeningGui(is_simulation=args.simulation, live_only_mode=args.live_only) + ''' + # Planning to replace this with a better design acq_config_action = QAction("Acquisition Settings", win) acq_config_action.triggered.connect(lambda: show_acq_config(win.configurationManager)) + ''' file_menu = QMenu("File", win) - file_menu.addAction(acq_config_action) + #file_menu.addAction(acq_config_action) if not legacy_config: config_action = QAction("Microscope Settings", win)