From e32c8dadec49f063d23dc005e4de0134cd90bc9b Mon Sep 17 00:00:00 2001 From: You Yan Date: Sun, 9 Feb 2025 23:36:11 -0800 Subject: [PATCH 01/24] AcquisitionConfigurationManager class and LaserAutofocusController class --- software/control/core/core.py | 379 +++++++++++++++++++++++----------- 1 file changed, 259 insertions(+), 120 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index dd85ef73..c2793c35 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -34,7 +34,7 @@ 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 @@ -3582,29 +3582,64 @@ 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"): +class AcquisitionConfigurationManager(QObject): + def __init__(self, profile: str = "default_configurations"): QObject.__init__(self) - self.config_filename = filename - self.configurations = [] - self.read_configurations() - - def save_configurations(self): - self.write_configuration(self.config_filename) - - def write_configuration(self, filename): - self.config_xml_tree.write(filename, encoding="utf-8", xml_declaration=True, pretty_print=True) - - 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( + base_path="./acquisition_configurations" + self.base_config_path = Path(base_path) + self.current_profile = profile + self.channel_configurations = {} # Dict to store configurations for each objective + self.autofocus_configurations = {} # Dict to store autofocus configs for each objective + self.available_profiles = self._get_available_profiles() + self.load_profile(profile) + + def _get_available_profiles(self) -> List[str]: + """Get list of available configuration profiles.""" + if not self.base_config_path.exists(): + os.makedirs(self.base_config_path) + 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 list of available objectives in the current 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 + self.channel_configurations.clear() + self.autofocus_configurations.clear() + + # Load configurations for each objective + for objective in self._get_available_objectives(profile_path): + objective_path = profile_path / objective + channel_config_file = objective_path / "channel_configurations.xml" + autofocus_config_file = objective_path / "autofocus_config.json" + + # Load channel configurations + if channel_config_file.exists(): + self.channel_configurations[objective] = self._load_channel_configurations(channel_config_file) + + # Load autofocus configurations + if autofocus_config_file.exists(): + with open(autofocus_config_file, 'r') as f: + self.autofocus_configurations[objective] = json.load(f) + + def _load_channel_configurations(self, config_file: Path) -> List[Configuration]: + """Load channel configurations from XML file.""" + if not os.path.isfile(config_file): + utils_config.generate_default_configuration(str(config_file)) + print(f"Generated default config file for {config_file}") + + config_xml_tree = etree.parse(str(config_file)) + config_xml_tree_root = config_xml_tree.getroot() + configurations = [] + + for mode in config_xml_tree_root.iter("mode"): + configurations.append( Configuration( mode_id=mode.get("ID"), name=mode.get("Name"), @@ -3615,33 +3650,120 @@ def read_configurations(self): 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)), ) ) + return configurations - 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) + def get_channel_configurations_for_objective(self, objective: str) -> List[Configuration]: + """Get channel configurations for a specific objective.""" + return self.channel_configurations.get(objective, []) + + def get_autofocus_configurations_for_objective(self, objective: str) -> Dict[str, Any]: + """Get autofocus configurations for a specific objective.""" + return self.autofocus_configurations.get(objective, {}) + + def get_channel_configurations(self) -> Dict[str, List[Configuration]]: + """Get channel configurations for all objectives.""" + return self.channel_configurations + + def get_autofocus_configurations(self) -> Dict[str, Dict[str, Any]]: + """Get autofocus configurations for all objectives.""" + return self.autofocus_configurations + + def update_channel_configuration(self, objective: str, configuration_id: str, attribute_name: str, new_value: Any) -> None: + """Update a specific configuration attribute in memory.""" + if objective not in self.objective_configurations: + raise ValueError(f"Objective {objective} not found") + + # Update directly in memory + for conf in self.objective_configurations[objective]: + if conf.id == configuration_id: + setattr(conf, attribute_name.lower(), new_value) + break + + def save_channel_configurations_for_objective(self, objective: str) -> None: + """Save channel configurations for a specific objective to disk.""" + if objective not in self.objective_configurations: + raise ValueError(f"Objective {objective} not found") + + profile_path = self.base_config_path / self.current_profile + config_file = profile_path / objective / "channel_configurations.xml" + + # Create XML structure from configurations + root = etree.Element("modes") + for config in self.objective_configurations[objective]: + mode = etree.SubElement(root, "mode") + # Convert configuration object attributes to XML + for attr_name, value in vars(config).items(): + if attr_name != 'id': # Handle ID separately to match original casing + xml_name = attr_name.title().replace('_', '') + mode.set(xml_name, str(value)) + else: + mode.set('ID', str(value)) + + # Write to file + tree = etree.ElementTree(root) + tree.write(str(config_file), encoding="utf-8", xml_declaration=True, pretty_print=True) + + def save_autofocus_configurations_for_objective(self, objective: str) -> None: + """Save autofocus configurations for a specific objective to disk.""" + if objective not in self.autofocus_configurations: + raise ValueError(f"No autofocus configuration found for objective {objective}") + + profile_path = self.base_config_path / self.current_profile + config_file = profile_path / objective / "autofocus_config.json" + + with open(config_file, 'w') as f: + json.dump(self.autofocus_configurations[objective], f, indent=4) + + def save_all_configurations(self) -> None: + """Write all configurations to disk.""" + for objective in self.objective_configurations: + self.save_channel_configurations_for_objective(objective) + if objective in self.autofocus_configurations: + self.save_autofocus_configurations_for_objective(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") + + # Create new profile directory + os.makedirs(new_profile_path) + + # Save current configurations to new profile + self.current_profile = profile_name + self.save_all_configurations() + + self.available_profiles = self._get_available_profiles() + + def write_configuration_selected(self, objective: str, selected_configurations: List[Configuration], filename: Path) -> None: + """ + Uses a temporary configuration tree to write selected states without affecting the original. + """ + # Create a new XML tree for temporary modifications + profile_path = self.base_config_path / self.current_profile + config_file = profile_path / objective / "channel_configurations.xml" + + # Create a fresh parse of the XML to avoid modifying the working copy + temp_tree = etree.parse(str(config_file)) + temp_root = temp_tree.getroot() + + # First set all Selected to 0 + for conf in temp_root.iter("mode"): + conf.set("Selected", "0") + + # Set selected configurations to 1 for conf in selected_configurations: - self.update_configuration_without_writing(conf.id, "Selected", 0) + conf_list = temp_root.xpath("//mode[contains(@ID," + "'" + str(conf.id) + "')]") + if conf_list: + conf_list[0].set("Selected", "1") + + # Write to the specified file + temp_tree.write(str(filename), encoding="utf-8", xml_declaration=True, pretty_print=True) def get_channel_color(self, channel): channel_info = CHANNEL_COLORS_MAP.get(self.extract_wavelength(channel), {"hex": 0xFFFFFF, "name": "gray"}) @@ -4433,25 +4555,24 @@ def get_surface_grid(self, x_range, y_range, num_points=50): class LaserAutofocusController(QObject): - image_to_display = Signal(np.ndarray) signal_displacement_um = Signal(float) def __init__( self, microcontroller: Microcontroller, - camera, - liveController, + camera: camera, + liveController: LiveController, stage: AbstractStage, - has_two_interfaces=True, - use_glass_top=True, - look_for_cache=True, + objectiveStore: Optional[ObjectiveStore] = None, + cachedLaserAFConfigurations: Optional[Dict[str, Any]] = None ): QObject.__init__(self) self.microcontroller = microcontroller self.camera = camera self.liveController = liveController self.stage = stage + self.objectiveStore = objectiveStore self.is_initialized = False self.x_reference = 0 @@ -4460,54 +4581,78 @@ def __init__( self.y_offset = 0 self.x_width = 3088 self.y_width = 2064 - - self.has_two_interfaces = has_two_interfaces # e.g. air-glass and glass water, set to false when (1) using oil immersion (2) using 1 mm thick slide (3) using metal coated slide or Si wafer - self.use_glass_top = use_glass_top + self.has_two_interfaces = False # e.g. air-glass and glass water, set to false when (1) using oil immersion (2) using 1 mm thick slide (3) using metal coated slide or Si wafer + self.use_glass_top = True + self.focus_camera_exposure_time_ms = 2 + self.focus_camera_analog_gain = 0 self.spot_spacing_pixels = None # spacing between the spots from the two interfaces (unit: pixel) - self.look_for_cache = look_for_cache - self.image = None # for saving the focus camera image for debugging when centroid cannot be found - if look_for_cache: - cache_path = "cache/laser_af_reference_plane.txt" - try: - with open(cache_path, "r") as cache_file: - for line in cache_file: - value_list = line.split(",") - x_offset = float(value_list[0]) - y_offset = float(value_list[1]) - width = int(value_list[2]) - height = int(value_list[3]) - pixel_to_um = float(value_list[4]) - x_reference = float(value_list[5]) - self.initialize_manual(x_offset, y_offset, width, height, pixel_to_um, x_reference) - break - except (FileNotFoundError, ValueError, IndexError) as e: - print("Unable to read laser AF state cache, exception below:") - print(e) - pass + # Load configurations if provided + self.laser_af_cache = cachedLaserAFConfigurations + if self.laser_af_cache is not None: + self.load_cached_configuration() - def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_reference, write_to_cache=True): - cache_string = ",".join( - [str(x_offset), str(y_offset), str(width), str(height), str(pixel_to_um), str(x_reference)] - ) - if write_to_cache: - cache_path = Path("cache/laser_af_reference_plane.txt") - cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.write_text(cache_string) - # x_reference is relative to the full sensor + def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_reference, + has_two_interfaces=self.has_two_interfaces, use_glass_top=self.use_glass_top, + focus_camera_exposure_time_ms=self.focus_camera_exposure_time_ms, + focus_camera_analog_gain=self.focus_camera_analog_gain): self.pixel_to_um = pixel_to_um self.x_offset = int((x_offset // 8) * 8) self.y_offset = int((y_offset // 2) * 2) self.width = int((width // 8) * 8) self.height = int((height // 2) * 2) - self.x_reference = x_reference - self.x_offset # self.x_reference is relative to the cropped region + self.x_reference = x_reference - self.x_offset + self.has_two_interfaces = has_two_interfaces + self.use_glass_top = use_glass_top + self.camera.set_ROI(self.x_offset, self.y_offset, self.width, self.height) + self.is_initialized = True - def initialize_auto(self): + # Update cache if objective store and laser_af_cache is available + if self.objectiveStore and self.laser_af_cache and self.objectiveStore.current_objective: + current_objective = self.objectiveStore.current_objective + if current_objective not in self.laser_af_cache: + self.laser_af_cache[current_objective] = {} + + self.laser_af_cache[current_objective].update({ + 'x_offset': x_offset, + 'y_offset': y_offset, + 'width': width, + 'height': height, + 'pixel_to_um': pixel_to_um, + 'x_reference': x_reference, + 'has_two_interfaces': has_two_interfaces, + 'use_glass_top': use_glass_top, + 'focus_camera_exposure_time_ms': focus_camera_exposure_time_ms, + 'focus_camera_analog_gain': focus_camera_analog_gain + }) + + def load_cached_configuration(self): + """Load configuration from the cache if available.""" + current_objective = self.objectiveStore.current_objective if self.objectiveStore else None + if current_objective and current_objective in self.laser_af_cache: + config = self.laser_af_cache[current_objective] + + self.focus_camera_exposure_time_ms = config.get('focus_camera_exposure_time_ms', 2), + self.focus_camera_analog_gain = config.get('focus_camera_analog_gain', 0) + self.camera.set_exposure_time(self.focus_camera_exposure_time_ms) + self.camera.set_analog_gain(self.focus_camera_analog_gain) + + self.initialize_manual( + x_offset=config.get('x_offset', 0), + y_offset=config.get('y_offset', 0), + width=config.get('width', LASER_AF_CROP_WIDTH), + height=config.get('height', LASER_AF_CROP_HEIGHT), + pixel_to_um=config.get('pixel_to_um', 1.0), + x_reference=config.get('x_reference', 0), + has_two_interfaces=config.get('has_two_interfaces', False), + use_glass_top=config.get('use_glass_top', True) + ) + def initialize_auto(self): # first find the region to crop # then calculate the convert factor @@ -4515,8 +4660,8 @@ def initialize_auto(self): self.camera.set_ROI(0, 0, None, None) # set offset first self.camera.set_ROI(0, 0, 3088, 2064) # update camera settings - self.camera.set_exposure_time(FOCUS_CAMERA_EXPOSURE_TIME_MS) - self.camera.set_analog_gain(FOCUS_CAMERA_ANALOG_GAIN) + self.camera.set_exposure_time(self.focus_camera_exposure_time_ms) + self.camera.set_analog_gain(self.focus_camera_analog_gain) # turn on the laser self.microcontroller.turn_on_AF_laser() @@ -4531,12 +4676,16 @@ def initialize_auto(self): x_offset = x - LASER_AF_CROP_WIDTH / 2 y_offset = y - LASER_AF_CROP_HEIGHT / 2 - print("laser spot location on the full sensor is (" + str(int(x)) + "," + str(int(y)) + ")") + print(f"laser spot location on the full sensor is ({int(x)},{int(y)})") # set camera crop self.initialize_manual(x_offset, y_offset, LASER_AF_CROP_WIDTH, LASER_AF_CROP_HEIGHT, 1, x) - # turn on laser + # Calibrate pixel to um conversion + self._calibrate_pixel_to_um() + + def _calibrate_pixel_to_um(self): + """Calibrate the pixel to micrometer conversion factor.""" self.microcontroller.turn_on_AF_laser() self.microcontroller.wait_till_operation_is_completed() @@ -4570,35 +4719,18 @@ def initialize_auto(self): # set reference self.x_reference = x1 - if self.look_for_cache: - cache_path = "cache/laser_af_reference_plane.txt" - try: - x_offset = None - y_offset = None - width = None - height = None - pixel_to_um = None - x_reference = None - with open(cache_path, "r") as cache_file: - for line in cache_file: - value_list = line.split(",") - x_offset = float(value_list[0]) - y_offset = float(value_list[1]) - width = int(value_list[2]) - height = int(value_list[3]) - pixel_to_um = self.pixel_to_um - x_reference = self.x_reference + self.x_offset - break - cache_string = ",".join( - [str(x_offset), str(y_offset), str(width), str(height), str(pixel_to_um), str(x_reference)] - ) - cache_path = Path("cache/laser_af_reference_plane.txt") - cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.write_text(cache_string) - except (FileNotFoundError, ValueError, IndexError) as e: - print("Unable to read laser AF state cache, exception below:") - print(e) - pass + # Update cache if objective store and laser_af_cache is available + if self.objectiveStore and self.laser_af_cache and self.objectiveStore.current_objective: + current_objective = self.objectiveStore.current_objective + if current_objective in self.laser_af_cache: + self.laser_af_cache[current_objective]['pixel_to_um'] = self.pixel_to_um + + def set_laser_af_properties(self, has_two_interfaces, use_glass_top, focus_camera_exposure_time_ms, focus_camera_analog_gain): + # These properties can be set from gui + self.has_two_interfaces = has_two_interfaces + self.use_glass_top = use_glass_top + self.focus_camera_exposure_time_ms = focus_camera_exposure_time_ms + self.focus_camera_analog_gain = focus_camera_analog_gain def measure_displacement(self): # turn on the laser @@ -4643,8 +4775,15 @@ def set_reference(self): self.x_reference = x self.signal_displacement_um.emit(0) - def _caculate_centroid(self, image): - if self.has_two_interfaces == False: + # Update cache if objective store and laser_af_cache is available + if self.objectiveStore and self.laser_af_cache and self.objectiveStore.current_objective: + current_objective = self.objectiveStore.current_objective + if current_objective in self.laser_af_cache: + self.laser_af_cache[current_objective]['x_reference'] = x + self.x_offset + + def _calculate_centroid(self, image): + """Calculate the centroid of the laser spot.""" + if not self.has_two_interfaces: h, w = image.shape x, y = np.meshgrid(range(w), range(h)) I = image.astype(float) From be36177f601b96a56b95b3e7d508de7293524840 Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 10 Feb 2025 05:49:22 -0800 Subject: [PATCH 02/24] replaced old usages --- software/control/core/core.py | 92 ++++++++++++++---------------- software/control/gui_hcs.py | 40 ++++++------- software/control/utils.py | 18 ++++++ software/control/widgets.py | 102 +++++++++++++++++----------------- software/main_hcs.py | 2 +- 5 files changed, 132 insertions(+), 122 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index c2793c35..d5ea507b 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -455,8 +455,6 @@ def __init__( illumination_source=None, illumination_intensity=None, z_offset=None, - pixel_format=None, - _pixel_format_options=None, emission_filter_position=None, ): self.id = mode_id @@ -468,12 +466,6 @@ def __init__( 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 @@ -482,7 +474,6 @@ def __init__( self, camera, microcontroller, - configurationManager, illuminationController, parent=None, control_illumination=True, @@ -493,7 +484,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 @@ -533,7 +523,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() @@ -544,7 +534,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() @@ -597,7 +587,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]) @@ -1386,7 +1376,8 @@ def __init__(self, multiPointController): self.stage: squid.abc.AbstractStage = self.multiPointController.stage self.liveController = self.multiPointController.liveController self.autofocusController = self.multiPointController.autofocusController - self.configurationManager = self.multiPointController.configurationManager + self.objectiveStore = self.multiPointController.objectiveStore + self.acquisitionConfigurationManager = self.multiPointController.acquisitionConfigurationManager self.NX = self.multiPointController.NX self.NY = self.multiPointController.NY self.NZ = self.multiPointController.NZ @@ -1763,7 +1754,7 @@ def perform_autofocus(self, region_id, fov): config_AF = next( ( config - for config in self.configurationManager.configurations + for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name_AF ) ) @@ -1785,7 +1776,7 @@ def perform_autofocus(self, region_id, fov): config_AF = next( ( config - for config in self.configurationManager.configurations + for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name_AF ) ) @@ -1896,7 +1887,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.acquisitionConfigurationManager.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_) @@ -2180,7 +2171,8 @@ def __init__( microcontroller: Microcontroller, liveController, autofocusController, - configurationManager, + objectiveStore, + acquisitionConfigurationManager, usb_spectrometer=None, scanCoordinates=None, parent=None, @@ -2194,7 +2186,8 @@ def __init__( self.microcontroller = microcontroller self.liveController = liveController self.autofocusController = autofocusController - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore, + self.acquisitionConfigurationManager = acquisitionConfigurationManager self.multiPointWorker: Optional[MultiPointWorker] = None self.thread: Optional[QThread] = None self.NX = 1 @@ -2318,9 +2311,7 @@ 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.acquisitionConfigurationManager.write_configuration_selected( self.selected_configurations, os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" ) # save the configuration for the experiment # Prepare acquisition parameters @@ -2364,7 +2355,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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == configuration_name) ) ) @@ -2636,7 +2629,8 @@ def __init__( camera, microcontroller: Microcontroller, stage: AbstractStage, - configurationManager, + objectiveStore, + acquisitionConfigurationManager, liveController: LiveController, autofocusController, imageDisplayWindow, @@ -2645,7 +2639,8 @@ def __init__( self.camera = camera self.microcontroller = microcontroller self.stage = stage - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.acquisitionConfigurationManager = acquisitionConfigurationManager self.liveController = liveController self.autofocusController = autofocusController self.imageDisplayWindow = imageDisplayWindow @@ -2751,7 +2746,7 @@ 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.acquisitionConfigurationManager.write_configuration( os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" ) # save the configuration for the experiment except: @@ -2762,8 +2757,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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == configuration_name) ) ) @@ -2863,7 +2860,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.acquisitionConfigurationManager = self.trackingController.acquisitionConfigurationManager self.imageDisplayWindow = self.trackingController.imageDisplayWindow self.crop_width = self.trackingController.crop_width self.crop_height = self.trackingController.crop_height @@ -3583,13 +3580,15 @@ def display_image(self, image, illumination_source): class AcquisitionConfigurationManager(QObject): - def __init__(self, profile: str = "default_configurations"): + def __init__(self, base_path: str = "./acquisition_configurations", profile: str = "default_configurations"): QObject.__init__(self) - base_path="./acquisition_configurations" self.base_config_path = Path(base_path) self.current_profile = profile self.channel_configurations = {} # Dict to store configurations for each objective self.autofocus_configurations = {} # Dict to store autofocus configs for each objective + if ENABLE_SPINNING_DISK_CONFOCAL: + self.confocal_configurations = {} + self.widefield_configurations = {} self.available_profiles = self._get_available_profiles() self.load_profile(profile) @@ -3628,6 +3627,19 @@ def load_profile(self, profile_name: str) -> None: with open(autofocus_config_file, 'r') as f: self.autofocus_configurations[objective] = json.load(f) + if ENABLE_SPINNING_DISK_CONFOCAL: + confocal_config_file = objective_path / "confocal_configurations.xml" + if confocal_config_file.exists(): + self.confocal_configurations[objective] = self._load_channel_configurations(confocal_config_file) + else: + self.confocal_configurations[objective] = self.channel_configurations[objective] + + widefield_config_file = objective_path / "widefield_configurations.xml" + if widefield_config_file.exists(): + self.widefield_configurations[objective] = self._load_channel_configurations(widefield_config_file) + else: + self.widefield_configurations[objective] = self.channel_configurations[objective] + def _load_channel_configurations(self, config_file: Path) -> List[Configuration]: """Load channel configurations from XML file.""" if not os.path.isfile(config_file): @@ -3765,22 +3777,6 @@ def write_configuration_selected(self, objective: str, selected_configurations: # Write to the specified file temp_tree.write(str(filename), encoding="utf-8", xml_declaration=True, pretty_print=True) - 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 - class ContrastManager: def __init__(self): @@ -4562,7 +4558,6 @@ def __init__( self, microcontroller: Microcontroller, camera: camera, - liveController: LiveController, stage: AbstractStage, objectiveStore: Optional[ObjectiveStore] = None, cachedLaserAFConfigurations: Optional[Dict[str, Any]] = None @@ -4570,7 +4565,6 @@ def __init__( QObject.__init__(self) self.microcontroller = microcontroller self.camera = camera - self.liveController = liveController self.stage = stage self.objectiveStore = objectiveStore diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index dcce7aaa..61e5bd89 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -212,11 +212,11 @@ def loadObjects(self, is_simulation): # Common object initialization self.objectiveStore = core.ObjectiveStore(parent=self) - self.configurationManager = core.ConfigurationManager(filename="./channel_configurations.xml") + self.acquisitionConfigurationManager = core.AcquisitionConfigurationManager() 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 ) if USE_PRIOR_STAGE: @@ -246,7 +246,8 @@ def loadObjects(self, is_simulation): self.camera, self.microcontroller, self.stage, - self.configurationManager, + self.objectiveStore, + self.acquisitionConfigurationManager, self.liveController, self.autofocusController, self.imageDisplayWindow, @@ -264,20 +265,17 @@ def loadObjects(self, is_simulation): self.microcontroller, self.liveController, self.autofocusController, - self.configurationManager, + self.objectiveStore, + self.acquisitionConfigurationManager, 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, @@ -288,7 +286,8 @@ def loadObjects(self, is_simulation): self.microcontroller, self.liveController, self.autofocusController, - self.configurationManager, + self.objectiveStore, + self.acquisitionConfigurationManager, scanCoordinates=self.scanCoordinates, parent=self, ) @@ -301,9 +300,8 @@ def loadObjects(self, is_simulation): self.camera_focus, self.liveController_focus_camera, self.stage, - has_two_interfaces=HAS_TWO_INTERFACES, - use_glass_top=USE_GLASS_TOP, - look_for_cache=False, + self.objectiveStore, + self.acquisitionConfigurationManager.get_autofocus_configurations() ) if USE_SQUID_FILTERWHEEL: @@ -518,7 +516,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.objectiveStore, self.acquisitionConfigurationManager) if ENABLE_NL5: import control.NL5Widget as NL5Widget @@ -541,7 +539,8 @@ def loadWidgets(self): self.liveControlWidget = widgets.LiveControlWidget( self.streamHandler, self.liveController, - self.configurationManager, + self.objectiveStore, + self.acquisitionConfigurationManager, show_display_options=True, show_autolevel=True, autolevel=True, @@ -596,7 +595,6 @@ def loadWidgets(self): self.liveControlWidget_focus_camera = widgets.LiveControlWidget( self.streamHandler_focus_camera, self.liveController_focus_camera, - self.configurationManager_focus_camera, stretch=False, ) # ,show_display_options=True) self.waveformDisplay = widgets.WaveformDisplay(N=1000, include_x=True, include_y=False) @@ -629,7 +627,7 @@ def loadWidgets(self): self.navigationViewer, self.multipointController, self.objectiveStore, - self.configurationManager, + self.acquisitionConfigurationManager, self.scanCoordinates, self.focusMapWidget, ) @@ -638,7 +636,7 @@ def loadWidgets(self): self.navigationViewer, self.multipointController, self.objectiveStore, - self.configurationManager, + self.acquisitionConfigurationManager, self.scanCoordinates, self.focusMapWidget, self.napariMosaicDisplayWidget, @@ -648,11 +646,12 @@ def loadWidgets(self): if ENABLE_TRACKING: self.trackingControlWidget = widgets.TrackingControllerWidget( self.trackingController, - self.configurationManager, + self.objectiveStore, + self.acquisitionConfigurationManager, show_configurations=TRACKING_SHOW_MICROSCOPE_CONFIGURATIONS, ) if ENABLE_STITCHER: - self.stitcherWidget = widgets.StitcherWidget(self.configurationManager, self.contrastManager) + self.stitcherWidget = widgets.StitcherWidget(self.objectiveStore, self.acquisitionConfigurationManager, self.contrastManager) self.recordTabWidget = QTabWidget() self.setupRecordTabWidget() @@ -666,7 +665,8 @@ def setupImageDisplayTabs(self): self.streamHandler, self.liveController, self.stage, - self.configurationManager, + self.objectiveStore, + self.acquisitionConfigurationManager, self.contrastManager, self.wellSelectionWidget, ) diff --git a/software/control/utils.py b/software/control/utils.py index cdcb479f..30ab4a6e 100644 --- a/software/control/utils.py +++ b/software/control/utils.py @@ -165,3 +165,21 @@ def ensure_directory_exists(raw_string_path: str): path: pathlib.Path = pathlib.Path(raw_string_path) _log.debug(f"Making sure directory '{path}' exists.") path.mkdir(parents=True, exist_ok=True) + + +def extract_wavelength_from_config_name(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 get_channel_color(self, channel): + channel_info = CHANNEL_COLORS_MAP.get(extract_wavelength_from_config_name(channel), {"hex": 0xFFFFFF, "name": "gray"}) + return channel_info["hex"] \ No newline at end of file diff --git a/software/control/widgets.py b/software/control/widgets.py index c269250c..510b0c22 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -3,7 +3,7 @@ from typing import Optional import squid.logging -from control.core.core import TrackingController +from control.core.core import TrackingController, Configuration from control.microcontroller import Microcontroller import control.utils as utils from squid.abc import AbstractStage @@ -330,9 +330,10 @@ def apply_and_exit(self): class SpinningDiskConfocalWidget(QWidget): - def __init__(self, xlight, config_manager=None): + def __init__(self, xlight, objective_store=None, config_manager=None): super(SpinningDiskConfocalWidget, self).__init__() + self.objective_store = objective_store self.config_manager = config_manager self.xlight = xlight @@ -350,14 +351,6 @@ def __init__(self, xlight, config_manager=None): 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) @@ -472,13 +465,7 @@ def toggle_disk_position(self): else: 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.enable_all_buttons() def toggle_motor(self): @@ -853,7 +840,8 @@ def __init__( self, streamHandler, liveController, - configurationManager=None, + objectiveStore=None, + acquisitionConfigurationManager=None, show_trigger_options=True, show_display_options=False, show_autolevel=False, @@ -866,16 +854,18 @@ def __init__( super().__init__(*args, **kwargs) self.liveController = liveController self.streamHandler = streamHandler - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.acquisitionConfigurationManager = acquisitionConfigurationManager 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] - + if acquisitionConfigurationManager: + self.currentConfiguration = self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)[0] + else: + self.currentConfiguration = Configuration() self.add_components(show_trigger_options, show_display_options, show_autolevel, autolevel, stretch) self.setFrameStyle(QFrame.Panel | QFrame.Raised) self.update_microscope_mode_by_name(self.currentConfiguration.name) @@ -899,11 +889,12 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_triggerFPS.setDecimals(0) # line 2: choose microscope mode / toggle live mode - self.dropdown_modeSelection = QComboBox() - for microscope_configuration in self.configurationManager.configurations: - self.dropdown_modeSelection.addItems([microscope_configuration.name]) - self.dropdown_modeSelection.setCurrentText(self.currentConfiguration.name) - self.dropdown_modeSelection.setSizePolicy(sizePolicy) + if self.acquisitionConfigurationManager: + self.dropdown_modeSelection = QComboBox() + for microscope_configuration in self.self.acquisitionConfigurationManager.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) self.btn_live = QPushButton("Start Live") self.btn_live.setCheckable(True) @@ -989,7 +980,8 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_displayFPS.valueChanged.connect(self.streamHandler.set_display_fps) self.slider_resolutionScaling.valueChanged.connect(self.streamHandler.set_display_resolution_scaling) self.slider_resolutionScaling.valueChanged.connect(self.liveController.set_display_resolution_scaling) - self.dropdown_modeSelection.currentTextChanged.connect(self.update_microscope_mode_by_name) + if self.acquisitionConfigurationManager: + self.dropdown_modeSelection.currentTextChanged.connect(self.update_microscope_mode_by_name) self.dropdown_triggerManu.currentIndexChanged.connect(self.update_trigger_mode) self.btn_live.clicked.connect(self.toggle_live) self.entry_exposureTime.valueChanged.connect(self.update_config_exposure_time) @@ -1004,7 +996,8 @@ def add_components(self, show_trigger_options, show_display_options, show_autole # layout grid_line1 = QHBoxLayout() grid_line1.addWidget(QLabel("Live Configuration")) - grid_line1.addWidget(self.dropdown_modeSelection, 2) + if self.acquisitionConfigurationManager: + grid_line1.addWidget(self.dropdown_modeSelection, 2) grid_line1.addWidget(self.btn_live, 1) grid_line2 = QHBoxLayout() @@ -1072,11 +1065,11 @@ def update_camera_settings(self): 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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)) self.currentConfiguration = next( ( config - for config in self.configurationManager.configurations + for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == current_microscope_mode_name ), None, @@ -1096,20 +1089,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.acquisitionConfigurationManager.update_channel_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.acquisitionConfigurationManager.update_channel_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.acquisitionConfigurationManager.update_channel_configuration( + self.objectiveStore.current_objective, self.currentConfiguration.id, "IlluminationIntensity", new_value ) self.liveController.set_illumination( self.currentConfiguration.illumination_source, self.currentConfiguration.illumination_intensity @@ -1874,7 +1867,7 @@ def __init__( navigationViewer, multipointController, objectiveStore, - configurationManager, + acquisitionConfigurationManager, scanCoordinates, focusMapWidget, *args, @@ -1889,7 +1882,7 @@ def __init__( self.navigationViewer = navigationViewer self.multipointController = multipointController self.objectiveStore = objectiveStore - self.configurationManager = configurationManager + self.acquisitionConfigurationManager = acquisitionConfigurationManager self.scanCoordinates = scanCoordinates self.focusMapWidget = focusMapWidget self.base_path_is_set = False @@ -2041,7 +2034,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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.list_configurations.addItems([microscope_configuration.name]) self.list_configurations.setSelectionMode( QAbstractItemView.MultiSelection @@ -3068,7 +3061,7 @@ def __init__( navigationViewer, multipointController, objectiveStore, - configurationManager, + acquisitionConfigurationManager, scanCoordinates, focusMapWidget=None, napariMosaicWidget=None, @@ -3081,7 +3074,7 @@ def __init__( self.navigationViewer = navigationViewer self.multipointController = multipointController self.objectiveStore = objectiveStore - self.configurationManager = configurationManager + self.acquisitionConfigurationManager = acquisitionConfigurationManager self.scanCoordinates = scanCoordinates self.focusMapWidget = focusMapWidget if napariMosaicWidget is None: @@ -4109,9 +4102,10 @@ def resizeEvent(self, event): class StitcherWidget(QFrame): - def __init__(self, configurationManager, contrastManager, *args, **kwargs): + def __init__(self, objectiveStore, acquisitionConfigurationManager, contrastManager, *args, **kwargs): super(StitcherWidget, self).__init__(*args, **kwargs) - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.acquisitionConfigurationManager = acquisitionConfigurationManager self.contrastManager = contrastManager self.stitcherThread = None self.output_path = "" @@ -4342,7 +4336,8 @@ def __init__( streamHandler, liveController, stage: AbstractStage, - configurationManager, + objectiveStore, + acquisitionConfigurationManager, contrastManager, wellSelectionWidget=None, show_trigger_options=True, @@ -4355,7 +4350,8 @@ def __init__( self.streamHandler = streamHandler self.liveController = liveController self.stage = stage - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.acquisitionConfigurationManager = acquisitionConfigurationManager self.wellSelectionWidget = wellSelectionWidget self.live_configuration = self.liveController.currentConfiguration self.image_width = 0 @@ -4427,7 +4423,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.acquisitionConfigurationManager.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) @@ -4686,7 +4682,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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == current_microscope_mode_name ), None, @@ -4699,17 +4695,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.acquisitionConfigurationManager.update_channel_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.acquisitionConfigurationManager.update_channel_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.acquisitionConfigurationManager.update_channel_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): @@ -5458,7 +5454,8 @@ class TrackingControllerWidget(QFrame): def __init__( self, trackingController: TrackingController, - configurationManager, + objectiveStore, + acquisitionConfigurationManager, show_configurations=True, main=None, *args, @@ -5466,7 +5463,8 @@ def __init__( ): super().__init__(*args, **kwargs) self.trackingController = trackingController - self.configurationManager = configurationManager + self.objectiveStore = objectiveStore + self.acquisitionConfigurationManager = acquisitionConfigurationManager self.base_path_is_set = False self.add_components(show_configurations) self.setFrameStyle(QFrame.Panel | QFrame.Raised) @@ -5504,7 +5502,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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.list_configurations.addItems([microscope_configuration.name]) self.list_configurations.setSelectionMode( QAbstractItemView.MultiSelection diff --git a/software/main_hcs.py b/software/main_hcs.py index d4072bce..af30593c 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -68,7 +68,7 @@ def show_acq_config(cfm): win = gui.HighContentScreeningGui(is_simulation=args.simulation, live_only_mode=args.live_only) acq_config_action = QAction("Acquisition Settings", win) - acq_config_action.triggered.connect(lambda: show_acq_config(win.configurationManager)) + acq_config_action.triggered.connect(lambda: show_acq_config(win.acquisitionConfigurationManager)) file_menu = QMenu("File", win) file_menu.addAction(acq_config_action) From 22955e9b1e5193c087e860a75bd3d82274798b8a Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 10 Feb 2025 10:29:48 -0800 Subject: [PATCH 03/24] channel configuration for confocal --- software/control/core/core.py | 19 +++++++++++++++---- software/control/gui_hcs.py | 2 +- software/control/widgets.py | 8 ++++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index d5ea507b..85ed169d 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3586,6 +3586,7 @@ def __init__(self, base_path: str = "./acquisition_configurations", profile: str self.current_profile = profile self.channel_configurations = {} # Dict to store configurations for each objective self.autofocus_configurations = {} # Dict to store autofocus configs for each objective + self.active_channel_config = None if ENABLE_SPINNING_DISK_CONFOCAL: self.confocal_configurations = {} self.widefield_configurations = {} @@ -3611,6 +3612,8 @@ def load_profile(self, profile_name: str) -> None: self.current_profile = profile_name self.channel_configurations.clear() self.autofocus_configurations.clear() + self.confocal_configurations.clear() + self.widefield_configurations.clear() # Load configurations for each objective for objective in self._get_available_objectives(profile_path): @@ -3621,12 +3624,14 @@ def load_profile(self, profile_name: str) -> None: # Load channel configurations if channel_config_file.exists(): self.channel_configurations[objective] = self._load_channel_configurations(channel_config_file) - + # Load autofocus configurations if autofocus_config_file.exists(): with open(autofocus_config_file, 'r') as f: self.autofocus_configurations[objective] = json.load(f) + self.active_channel_config = self.channel_configurations + if ENABLE_SPINNING_DISK_CONFOCAL: confocal_config_file = objective_path / "confocal_configurations.xml" if confocal_config_file.exists(): @@ -3669,7 +3674,7 @@ def _load_channel_configurations(self, config_file: Path) -> List[Configuration] def get_channel_configurations_for_objective(self, objective: str) -> List[Configuration]: """Get channel configurations for a specific objective.""" - return self.channel_configurations.get(objective, []) + return self.active_channel_config.get(objective, []) def get_autofocus_configurations_for_objective(self, objective: str) -> Dict[str, Any]: """Get autofocus configurations for a specific objective.""" @@ -3677,11 +3682,17 @@ def get_autofocus_configurations_for_objective(self, objective: str) -> Dict[str def get_channel_configurations(self) -> Dict[str, List[Configuration]]: """Get channel configurations for all objectives.""" - return self.channel_configurations + return self.active_channel_config def get_autofocus_configurations(self) -> Dict[str, Dict[str, Any]]: """Get autofocus configurations for all objectives.""" - return self.autofocus_configurations + return self.active_channel_config + + def toggle_confocal_widefield(self, confocal: bool): + if confocal: + self.active_channel_config = self.confocal_configurations + else: + self.active_channel_config = self.widefield_configurations def update_channel_configuration(self, objective: str, configuration_id: str, attribute_name: str, new_value: Any) -> None: """Update a specific configuration attribute in memory.""" diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 61e5bd89..50a27572 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -516,7 +516,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.objectiveStore, self.acquisitionConfigurationManager) + self.spinningDiskConfocalWidget = widgets.SpinningDiskConfocalWidget(self.xlight, self.acquisitionConfigurationManager) if ENABLE_NL5: import control.NL5Widget as NL5Widget diff --git a/software/control/widgets.py b/software/control/widgets.py index 510b0c22..5fe868f5 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -330,7 +330,7 @@ def apply_and_exit(self): class SpinningDiskConfocalWidget(QWidget): - def __init__(self, xlight, objective_store=None, config_manager=None): + def __init__(self, xlight, config_manager=None): super(SpinningDiskConfocalWidget, self).__init__() self.objective_store = objective_store @@ -348,6 +348,9 @@ def __init__(self, xlight, objective_store=None, config_manager=None): self.disk_position_state = self.xlight.get_disk_position() + if self.config_manager is not None: + self.config_manager.toogle_confocal_widefield(self.disk_position_state) + if self.disk_position_state == 1: self.btn_toggle_widefield.setText("Switch to Widefield") @@ -465,7 +468,8 @@ def toggle_disk_position(self): else: 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: + self.config_manager.toogle_confocal_widefield(self.disk_position_state) self.enable_all_buttons() def toggle_motor(self): From 1cc16cf32b57eb6928116495532289c19a562528 Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 10 Feb 2025 13:14:24 -0800 Subject: [PATCH 04/24] active channel flag; update write_configuration_selected --- software/control/core/core.py | 85 ++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 85ed169d..3dec43aa 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -2312,7 +2312,7 @@ def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prep # create a new folder utils.ensure_directory_exists(os.path.join(self.base_path, self.experiment_ID)) self.acquisitionConfigurationManager.write_configuration_selected( - self.selected_configurations, os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" + 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 = { @@ -3587,6 +3587,7 @@ def __init__(self, base_path: str = "./acquisition_configurations", profile: str self.channel_configurations = {} # Dict to store configurations for each objective self.autofocus_configurations = {} # Dict to store autofocus configs for each objective self.active_channel_config = None + self.active_config_flag = -1 # 0: channel_configurations, 1: confocal_configurations, 2: widefield_configurations if ENABLE_SPINNING_DISK_CONFOCAL: self.confocal_configurations = {} self.widefield_configurations = {} @@ -3631,6 +3632,7 @@ def load_profile(self, profile_name: str) -> None: self.autofocus_configurations[objective] = json.load(f) self.active_channel_config = self.channel_configurations + self.active_config_flag = 0 if ENABLE_SPINNING_DISK_CONFOCAL: confocal_config_file = objective_path / "confocal_configurations.xml" @@ -3691,31 +3693,38 @@ def get_autofocus_configurations(self) -> Dict[str, Dict[str, Any]]: def toggle_confocal_widefield(self, confocal: bool): if confocal: self.active_channel_config = self.confocal_configurations + self.active_config_flag = 1 else: self.active_channel_config = self.widefield_configurations + self.active_config_flag = 2 def update_channel_configuration(self, objective: str, configuration_id: str, attribute_name: str, new_value: Any) -> None: """Update a specific configuration attribute in memory.""" - if objective not in self.objective_configurations: + if objective not in self.active_channel_config: raise ValueError(f"Objective {objective} not found") # Update directly in memory - for conf in self.objective_configurations[objective]: + for conf in self.active_channel_config[objective]: if conf.id == configuration_id: setattr(conf, attribute_name.lower(), new_value) break def save_channel_configurations_for_objective(self, objective: str) -> None: """Save channel configurations for a specific objective to disk.""" - if objective not in self.objective_configurations: + if objective not in self.active_channel_config: raise ValueError(f"Objective {objective} not found") profile_path = self.base_config_path / self.current_profile - config_file = profile_path / objective / "channel_configurations.xml" + if self.active_config_flag == 0: + config_file = profile_path / objective / "channel_configurations.xml" + elif self.active_config_flag == 1: + config_file = profile_path / objective / "confocal_configurations.xml" + elif self.active_config_flag == 2: + config_file = profile_path / objective / "widefield_configurations.xml" # Create XML structure from configurations root = etree.Element("modes") - for config in self.objective_configurations[objective]: + for config in self.active_channel_config[objective]: mode = etree.SubElement(root, "mode") # Convert configuration object attributes to XML for attr_name, value in vars(config).items(): @@ -3742,8 +3751,20 @@ def save_autofocus_configurations_for_objective(self, objective: str) -> None: def save_all_configurations(self) -> None: """Write all configurations to disk.""" - for objective in self.objective_configurations: - self.save_channel_configurations_for_objective(objective) + profile_path = self.base_config_path / self.current_profile + for objective in self._get_available_objectives(profile_path): + if self.active_config_flag == 0: + self.save_channel_configurations_for_objective(objective) + if ENABLE_SPINNING_DISK_CONFOCAL: + # save current config + self.save_channel_configurations_for_objective(objective) + # if current config is confocal, set active config to widefield; otherwise set to confocal + is_confocal = bool(self.active_config_flag - 1) + self.toggle_confocal_widefield(is_confocal) + # save the other config + self.save_channel_configurations_for_objective(objective) + # toggle active config back + self.toggle_confocal_widefield(not is_confocal) if objective in self.autofocus_configurations: self.save_autofocus_configurations_for_objective(objective) @@ -3764,29 +3785,31 @@ def create_new_profile(self, profile_name: str) -> None: self.available_profiles = self._get_available_profiles() def write_configuration_selected(self, objective: str, selected_configurations: List[Configuration], filename: Path) -> None: - """ - Uses a temporary configuration tree to write selected states without affecting the original. - """ - # Create a new XML tree for temporary modifications - profile_path = self.base_config_path / self.current_profile - config_file = profile_path / objective / "channel_configurations.xml" - - # Create a fresh parse of the XML to avoid modifying the working copy - temp_tree = etree.parse(str(config_file)) - temp_root = temp_tree.getroot() - - # First set all Selected to 0 - for conf in temp_root.iter("mode"): - conf.set("Selected", "0") - - # Set selected configurations to 1 - for conf in selected_configurations: - conf_list = temp_root.xpath("//mode[contains(@ID," + "'" + str(conf.id) + "')]") - if conf_list: - conf_list[0].set("Selected", "1") - - # Write to the specified file - temp_tree.write(str(filename), encoding="utf-8", xml_declaration=True, pretty_print=True) + """Write selected configurations to a file.""" + if objective not in self.active_channel_config: + raise ValueError(f"Objective {objective} not found") + + # Create XML structure from configurations + root = etree.Element("modes") + selected_ids = [conf.id for conf in selected_configurations] + + # Write all configurations but mark selected ones + for config in self.active_channel_config[objective]: + mode = etree.SubElement(root, "mode") + # Convert configuration object attributes to XML + for attr_name, value in vars(config).items(): + if attr_name != 'id': + xml_name = attr_name.title().replace('_', '') + mode.set(xml_name, str(value)) + else: + mode.set('ID', str(value)) + + # Set Selected attribute + mode.set("Selected", "1" if config.id in selected_ids else "0") + + # Write to file + tree = etree.ElementTree(root) + tree.write(str(filename), encoding="utf-8", xml_declaration=True, pretty_print=True) class ContrastManager: From 76bdadbf0b5191adb5913d67b0dab80b893f525e Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 10 Feb 2025 13:18:45 -0800 Subject: [PATCH 05/24] example configuration files --- .../10x/laser_af_reference_plane.json | 12 ++++++++++++ .../20x/laser_af_reference_plane.json | 12 ++++++++++++ .../2x/laser_af_reference_plane.json | 12 ++++++++++++ .../40x/laser_af_reference_plane.json | 12 ++++++++++++ .../4x/laser_af_reference_plane.json | 12 ++++++++++++ .../50x/laser_af_reference_plane.json | 12 ++++++++++++ .../60x/laser_af_reference_plane.json | 12 ++++++++++++ 7 files changed, 84 insertions(+) create mode 100644 software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json create mode 100644 software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json create mode 100644 software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json create mode 100644 software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json create mode 100644 software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json create mode 100644 software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json create mode 100644 software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json diff --git a/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json new file mode 100644 index 00000000..99745550 --- /dev/null +++ b/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json @@ -0,0 +1,12 @@ +{ + "x_offset": 2286.0079317619884, + "y_offset": 921.7816593175237, + "width": 1536, + "height": 256, + "pixel_to_um": 1.0, + "x_reference": 3054.0079317619884, + "has_two_interfaces": False, + "use_glass_top": True, + "focus_camera_exposure_time_ms": 2, + "focus_camera_analog_gain": 0 +} diff --git a/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json new file mode 100644 index 00000000..99745550 --- /dev/null +++ b/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json @@ -0,0 +1,12 @@ +{ + "x_offset": 2286.0079317619884, + "y_offset": 921.7816593175237, + "width": 1536, + "height": 256, + "pixel_to_um": 1.0, + "x_reference": 3054.0079317619884, + "has_two_interfaces": False, + "use_glass_top": True, + "focus_camera_exposure_time_ms": 2, + "focus_camera_analog_gain": 0 +} diff --git a/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json new file mode 100644 index 00000000..99745550 --- /dev/null +++ b/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json @@ -0,0 +1,12 @@ +{ + "x_offset": 2286.0079317619884, + "y_offset": 921.7816593175237, + "width": 1536, + "height": 256, + "pixel_to_um": 1.0, + "x_reference": 3054.0079317619884, + "has_two_interfaces": False, + "use_glass_top": True, + "focus_camera_exposure_time_ms": 2, + "focus_camera_analog_gain": 0 +} diff --git a/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json new file mode 100644 index 00000000..99745550 --- /dev/null +++ b/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json @@ -0,0 +1,12 @@ +{ + "x_offset": 2286.0079317619884, + "y_offset": 921.7816593175237, + "width": 1536, + "height": 256, + "pixel_to_um": 1.0, + "x_reference": 3054.0079317619884, + "has_two_interfaces": False, + "use_glass_top": True, + "focus_camera_exposure_time_ms": 2, + "focus_camera_analog_gain": 0 +} diff --git a/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json new file mode 100644 index 00000000..99745550 --- /dev/null +++ b/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json @@ -0,0 +1,12 @@ +{ + "x_offset": 2286.0079317619884, + "y_offset": 921.7816593175237, + "width": 1536, + "height": 256, + "pixel_to_um": 1.0, + "x_reference": 3054.0079317619884, + "has_two_interfaces": False, + "use_glass_top": True, + "focus_camera_exposure_time_ms": 2, + "focus_camera_analog_gain": 0 +} diff --git a/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json new file mode 100644 index 00000000..99745550 --- /dev/null +++ b/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json @@ -0,0 +1,12 @@ +{ + "x_offset": 2286.0079317619884, + "y_offset": 921.7816593175237, + "width": 1536, + "height": 256, + "pixel_to_um": 1.0, + "x_reference": 3054.0079317619884, + "has_two_interfaces": False, + "use_glass_top": True, + "focus_camera_exposure_time_ms": 2, + "focus_camera_analog_gain": 0 +} diff --git a/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json new file mode 100644 index 00000000..99745550 --- /dev/null +++ b/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json @@ -0,0 +1,12 @@ +{ + "x_offset": 2286.0079317619884, + "y_offset": 921.7816593175237, + "width": 1536, + "height": 256, + "pixel_to_um": 1.0, + "x_reference": 3054.0079317619884, + "has_two_interfaces": False, + "use_glass_top": True, + "focus_camera_exposure_time_ms": 2, + "focus_camera_analog_gain": 0 +} From 46e6ce3a1dad4b440dfb0521e68afddf26302e8b Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 10 Feb 2025 13:21:35 -0800 Subject: [PATCH 06/24] fix json --- .../default_configurations/10x/laser_af_reference_plane.json | 4 ++-- .../default_configurations/20x/laser_af_reference_plane.json | 4 ++-- .../default_configurations/2x/laser_af_reference_plane.json | 4 ++-- .../default_configurations/40x/laser_af_reference_plane.json | 4 ++-- .../default_configurations/4x/laser_af_reference_plane.json | 4 ++-- .../default_configurations/50x/laser_af_reference_plane.json | 4 ++-- .../default_configurations/60x/laser_af_reference_plane.json | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json index 99745550..317e6c19 100644 --- a/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json +++ b/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json @@ -5,8 +5,8 @@ "height": 256, "pixel_to_um": 1.0, "x_reference": 3054.0079317619884, - "has_two_interfaces": False, - "use_glass_top": True, + "has_two_interfaces": false, + "use_glass_top": true, "focus_camera_exposure_time_ms": 2, "focus_camera_analog_gain": 0 } diff --git a/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json index 99745550..317e6c19 100644 --- a/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json +++ b/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json @@ -5,8 +5,8 @@ "height": 256, "pixel_to_um": 1.0, "x_reference": 3054.0079317619884, - "has_two_interfaces": False, - "use_glass_top": True, + "has_two_interfaces": false, + "use_glass_top": true, "focus_camera_exposure_time_ms": 2, "focus_camera_analog_gain": 0 } diff --git a/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json index 99745550..317e6c19 100644 --- a/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json +++ b/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json @@ -5,8 +5,8 @@ "height": 256, "pixel_to_um": 1.0, "x_reference": 3054.0079317619884, - "has_two_interfaces": False, - "use_glass_top": True, + "has_two_interfaces": false, + "use_glass_top": true, "focus_camera_exposure_time_ms": 2, "focus_camera_analog_gain": 0 } diff --git a/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json index 99745550..317e6c19 100644 --- a/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json +++ b/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json @@ -5,8 +5,8 @@ "height": 256, "pixel_to_um": 1.0, "x_reference": 3054.0079317619884, - "has_two_interfaces": False, - "use_glass_top": True, + "has_two_interfaces": false, + "use_glass_top": true, "focus_camera_exposure_time_ms": 2, "focus_camera_analog_gain": 0 } diff --git a/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json index 99745550..317e6c19 100644 --- a/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json +++ b/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json @@ -5,8 +5,8 @@ "height": 256, "pixel_to_um": 1.0, "x_reference": 3054.0079317619884, - "has_two_interfaces": False, - "use_glass_top": True, + "has_two_interfaces": false, + "use_glass_top": true, "focus_camera_exposure_time_ms": 2, "focus_camera_analog_gain": 0 } diff --git a/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json index 99745550..317e6c19 100644 --- a/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json +++ b/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json @@ -5,8 +5,8 @@ "height": 256, "pixel_to_um": 1.0, "x_reference": 3054.0079317619884, - "has_two_interfaces": False, - "use_glass_top": True, + "has_two_interfaces": false, + "use_glass_top": true, "focus_camera_exposure_time_ms": 2, "focus_camera_analog_gain": 0 } diff --git a/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json index 99745550..317e6c19 100644 --- a/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json +++ b/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json @@ -5,8 +5,8 @@ "height": 256, "pixel_to_um": 1.0, "x_reference": 3054.0079317619884, - "has_two_interfaces": False, - "use_glass_top": True, + "has_two_interfaces": false, + "use_glass_top": true, "focus_camera_exposure_time_ms": 2, "focus_camera_analog_gain": 0 } From 19cc63c97fc20dfe68170397e1f0889f2df1122a Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 10 Feb 2025 14:26:30 -0800 Subject: [PATCH 07/24] ui components for configuration profile management --- software/control/widgets.py | 89 ++++++++++++++++++++++++++++++++++--- software/main_hcs.py | 10 +++-- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/software/control/widgets.py b/software/control/widgets.py index 5fe868f5..0d849298 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__() @@ -200,7 +201,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): @@ -877,14 +878,26 @@ def __init__( self.is_switching_mode = False # flag used to prevent from settings being set by twice - from both mode change slot and value change slot; another way is to use blockSignals(True) def add_components(self, show_trigger_options, show_display_options, show_autolevel, autolevel, stretch): - # line 0: trigger mode + # line 0: acquisition configuration profile management + self.dropdown_profiles = QComboBox() + if self.acquisitionConfigurationManager: + self.dropdown_profiles.addItems(self.acquisitionConfigurationManager.available_profiles) + self.dropdown_profiles.setCurrentText(self.acquisitionConfigurationManager.current_profile) + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.dropdown_profiles.setSizePolicy(sizePolicy) + + self.btn_loadProfile = QPushButton("Load") + self.btn_saveProfile = QPushButton("Save") + self.btn_newProfile = QPushButton("Create") + + # line 1: trigger mode self.triggerMode = None self.dropdown_triggerManu = QComboBox() self.dropdown_triggerManu.addItems([TriggerMode.SOFTWARE, TriggerMode.HARDWARE, TriggerMode.CONTINUOUS]) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.dropdown_triggerManu.setSizePolicy(sizePolicy) - # line 1: fps + # line 2: fps self.entry_triggerFPS = QDoubleSpinBox() self.entry_triggerFPS.setMinimum(0.02) self.entry_triggerFPS.setMaximum(1000) @@ -892,7 +905,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_triggerFPS.setValue(self.fps_trigger) self.entry_triggerFPS.setDecimals(0) - # line 2: choose microscope mode / toggle live mode + # line 3: choose microscope mode / toggle live mode if self.acquisitionConfigurationManager: self.dropdown_modeSelection = QComboBox() for microscope_configuration in self.self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): @@ -907,7 +920,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.btn_live.setStyleSheet("background-color: #C2C2FF") self.btn_live.setSizePolicy(sizePolicy) - # line 3: exposure time and analog gain associated with the current mode + # line 4: exposure time and analog gain associated with the current mode self.entry_exposureTime = QDoubleSpinBox() self.entry_exposureTime.setMinimum(self.liveController.camera.EXPOSURE_TIME_MS_MIN) self.entry_exposureTime.setMaximum(self.liveController.camera.EXPOSURE_TIME_MS_MAX) @@ -939,7 +952,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_illuminationIntensity.setSuffix("%") self.entry_illuminationIntensity.setValue(100) - # line 4: display fps and resolution scaling + # line 5: display fps and resolution scaling self.entry_displayFPS = QDoubleSpinBox() self.entry_displayFPS.setMinimum(1) self.entry_displayFPS.setMaximum(240) @@ -980,6 +993,10 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.btn_autolevel.setFixedWidth(max_width) # connections + self.btn_loadProfile.clicked.connect(self.load_profile) + self.btn_saveProfile.clicked.connect(self.save_profile) + self.btn_newProfile.clicked.connect(self.create_new_profile) + self.entry_triggerFPS.valueChanged.connect(self.liveController.set_trigger_fps) self.entry_displayFPS.valueChanged.connect(self.streamHandler.set_display_fps) self.slider_resolutionScaling.valueChanged.connect(self.streamHandler.set_display_resolution_scaling) @@ -998,6 +1015,14 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.btn_autolevel.toggled.connect(self.signal_autoLevelSetting.emit) # layout + # Profile management layout + grid_line_profile = QHBoxLayout() + grid_line_profile.addWidget(QLabel("Configuration Profile")) + grid_line_profile.addWidget(self.dropdown_profiles, 2) + grid_line_profile.addWidget(self.btn_loadProfile) + grid_line_profile.addWidget(self.btn_saveProfile) + grid_line_profile.addWidget(self.btn_newProfile) + grid_line1 = QHBoxLayout() grid_line1.addWidget(QLabel("Live Configuration")) if self.acquisitionConfigurationManager: @@ -1040,6 +1065,8 @@ def add_components(self, show_trigger_options, show_display_options, show_autole grid_line05.addWidget(self.label_resolutionScaling) self.grid = QVBoxLayout() + if self.acquisitionConfigurationManager: + self.grid.addLayout(grid_line_profile) if show_trigger_options: self.grid.addLayout(grid_line0) self.grid.addLayout(grid_line1) @@ -1051,6 +1078,54 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.grid.addStretch() self.setLayout(self.grid) + def load_profile(self): + """Load the selected profile.""" + if self.acquisitionConfigurationManager: + profile_name = self.dropdown_profiles.currentText() + # Load the profile + self.acquisitionConfigurationManager.load_profile(profile_name) + # Update the mode selection dropdown + self.dropdown_modeSelection.clear() + for microscope_configuration in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + self.dropdown_modeSelection.addItem(microscope_configuration.name) + # Update to first configuration + if self.dropdown_modeSelection.count() > 0: + self.update_microscope_mode_by_name(self.dropdown_modeSelection.currentText()) + + def save_profile(self): + """Save current configurations to selected profile with confirmation.""" + if self.acquisitionConfigurationManager: + profile_name = self.dropdown_profiles.currentText() + msg = QMessageBox() + msg.setIcon(QMessageBox.Question) + msg.setText(f"Save current configurations to profile '{profile_name}'?") + msg.setWindowTitle("Save Profile") + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + + if msg.exec_() == QMessageBox.Yes: + self.acquisitionConfigurationManager.save_all_configurations() + + def create_new_profile(self): + """Create a new profile with current configurations.""" + if self.acquisitionConfigurationManager: + dialog = QInputDialog() + profile_name, ok = dialog.getText( + self, + "New Profile", + "Enter new profile name:", + QLineEdit.Normal, + "" + ) + + if ok and profile_name: + try: + self.acquisitionConfigurationManager.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 toggle_live(self, pressed): if pressed: self.liveController.start_live() diff --git a/software/main_hcs.py b/software/main_hcs.py index af30593c..50f3deca 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -29,11 +29,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() @@ -67,11 +68,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.acquisitionConfigurationManager)) + ''' 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) From c6fbba0f62b9729118ee3821bbf9cad376e90922 Mon Sep 17 00:00:00 2001 From: You Yan Date: Mon, 10 Feb 2025 15:43:16 -0800 Subject: [PATCH 08/24] bug fix --- software/control/core/core.py | 18 ++++++++++-------- software/control/utils.py | 6 ++++-- software/control/widgets.py | 4 ++-- software/main_hcs.py | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 3dec43aa..5ed5a649 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3613,8 +3613,9 @@ def load_profile(self, profile_name: str) -> None: self.current_profile = profile_name self.channel_configurations.clear() self.autofocus_configurations.clear() - self.confocal_configurations.clear() - self.widefield_configurations.clear() + if ENABLE_SPINNING_DISK_CONFOCAL: + self.confocal_configurations.clear() + self.widefield_configurations.clear() # Load configurations for each objective for objective in self._get_available_objectives(profile_path): @@ -3662,7 +3663,7 @@ def _load_channel_configurations(self, config_file: Path) -> List[Configuration] Configuration( mode_id=mode.get("ID"), name=mode.get("Name"), - color=self.get_channel_color(mode.get("Name")), + color=utils.get_channel_color(mode.get("Name")), exposure_time=float(mode.get("ExposureTime")), analog_gain=float(mode.get("AnalogGain")), illumination_source=int(mode.get("IlluminationSource")), @@ -4591,7 +4592,7 @@ class LaserAutofocusController(QObject): def __init__( self, microcontroller: Microcontroller, - camera: camera, + camera, stage: AbstractStage, objectiveStore: Optional[ObjectiveStore] = None, cachedLaserAFConfigurations: Optional[Dict[str, Any]] = None @@ -4623,9 +4624,8 @@ def __init__( self.load_cached_configuration() def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_reference, - has_two_interfaces=self.has_two_interfaces, use_glass_top=self.use_glass_top, - focus_camera_exposure_time_ms=self.focus_camera_exposure_time_ms, - focus_camera_analog_gain=self.focus_camera_analog_gain): + has_two_interfaces=False, use_glass_top=True, + focus_camera_exposure_time_ms=2, focus_camera_analog_gain=0): self.pixel_to_um = pixel_to_um self.x_offset = int((x_offset // 8) * 8) self.y_offset = int((y_offset // 2) * 2) @@ -4677,7 +4677,9 @@ def load_cached_configuration(self): pixel_to_um=config.get('pixel_to_um', 1.0), x_reference=config.get('x_reference', 0), has_two_interfaces=config.get('has_two_interfaces', False), - use_glass_top=config.get('use_glass_top', True) + use_glass_top=config.get('use_glass_top', True), + focus_camera_exposure_time_ms=config.get('focus_camera_exposure_time_ms', 2), + focus_camera_analog_gain=config.get('focus_camera_analog_gain', 0), ) def initialize_auto(self): diff --git a/software/control/utils.py b/software/control/utils.py index 30ab4a6e..1e3af85d 100644 --- a/software/control/utils.py +++ b/software/control/utils.py @@ -6,6 +6,8 @@ from scipy.ndimage import label import os +from control._def import CHANNEL_COLORS_MAP + import squid.logging _log = squid.logging.get_logger("control.utils") @@ -167,7 +169,7 @@ def ensure_directory_exists(raw_string_path: str): path.mkdir(parents=True, exist_ok=True) -def extract_wavelength_from_config_name(self, name): +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: @@ -180,6 +182,6 @@ def extract_wavelength_from_config_name(self, name): return None -def get_channel_color(self, channel): +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"] \ No newline at end of file diff --git a/software/control/widgets.py b/software/control/widgets.py index 0d849298..dccd39fa 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -908,7 +908,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole # line 3: choose microscope mode / toggle live mode if self.acquisitionConfigurationManager: self.dropdown_modeSelection = QComboBox() - for microscope_configuration in self.self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + for microscope_configuration in self.acquisitionConfigurationManager.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) @@ -3269,7 +3269,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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.list_configurations.addItems([microscope_configuration.name]) self.list_configurations.setSelectionMode(QAbstractItemView.MultiSelection) diff --git a/software/main_hcs.py b/software/main_hcs.py index 50f3deca..6f87a9d5 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -19,7 +19,7 @@ # 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 if USE_TERMINAL_CONSOLE: From 92f505fe1c687d73fa1946b1fe0f53223d5f9765 Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 11 Feb 2025 16:06:04 -0800 Subject: [PATCH 09/24] extract ChannelConfigurationManager and LaserAFConfigurationManager from ACM class --- .../10x/laser_af_reference_plane.json | 12 - .../20x/laser_af_reference_plane.json | 12 - .../2x/laser_af_reference_plane.json | 12 - .../40x/laser_af_reference_plane.json | 12 - .../4x/laser_af_reference_plane.json | 12 - .../50x/laser_af_reference_plane.json | 12 - .../60x/laser_af_reference_plane.json | 12 - software/control/core/core.py | 367 ++++++++++-------- 8 files changed, 196 insertions(+), 255 deletions(-) delete mode 100644 software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json delete mode 100644 software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json delete mode 100644 software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json delete mode 100644 software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json delete mode 100644 software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json delete mode 100644 software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json delete mode 100644 software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json diff --git a/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json deleted file mode 100644 index 317e6c19..00000000 --- a/software/acquisition_configurations/default_configurations/10x/laser_af_reference_plane.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "x_offset": 2286.0079317619884, - "y_offset": 921.7816593175237, - "width": 1536, - "height": 256, - "pixel_to_um": 1.0, - "x_reference": 3054.0079317619884, - "has_two_interfaces": false, - "use_glass_top": true, - "focus_camera_exposure_time_ms": 2, - "focus_camera_analog_gain": 0 -} diff --git a/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json deleted file mode 100644 index 317e6c19..00000000 --- a/software/acquisition_configurations/default_configurations/20x/laser_af_reference_plane.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "x_offset": 2286.0079317619884, - "y_offset": 921.7816593175237, - "width": 1536, - "height": 256, - "pixel_to_um": 1.0, - "x_reference": 3054.0079317619884, - "has_two_interfaces": false, - "use_glass_top": true, - "focus_camera_exposure_time_ms": 2, - "focus_camera_analog_gain": 0 -} diff --git a/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json deleted file mode 100644 index 317e6c19..00000000 --- a/software/acquisition_configurations/default_configurations/2x/laser_af_reference_plane.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "x_offset": 2286.0079317619884, - "y_offset": 921.7816593175237, - "width": 1536, - "height": 256, - "pixel_to_um": 1.0, - "x_reference": 3054.0079317619884, - "has_two_interfaces": false, - "use_glass_top": true, - "focus_camera_exposure_time_ms": 2, - "focus_camera_analog_gain": 0 -} diff --git a/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json deleted file mode 100644 index 317e6c19..00000000 --- a/software/acquisition_configurations/default_configurations/40x/laser_af_reference_plane.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "x_offset": 2286.0079317619884, - "y_offset": 921.7816593175237, - "width": 1536, - "height": 256, - "pixel_to_um": 1.0, - "x_reference": 3054.0079317619884, - "has_two_interfaces": false, - "use_glass_top": true, - "focus_camera_exposure_time_ms": 2, - "focus_camera_analog_gain": 0 -} diff --git a/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json deleted file mode 100644 index 317e6c19..00000000 --- a/software/acquisition_configurations/default_configurations/4x/laser_af_reference_plane.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "x_offset": 2286.0079317619884, - "y_offset": 921.7816593175237, - "width": 1536, - "height": 256, - "pixel_to_um": 1.0, - "x_reference": 3054.0079317619884, - "has_two_interfaces": false, - "use_glass_top": true, - "focus_camera_exposure_time_ms": 2, - "focus_camera_analog_gain": 0 -} diff --git a/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json deleted file mode 100644 index 317e6c19..00000000 --- a/software/acquisition_configurations/default_configurations/50x/laser_af_reference_plane.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "x_offset": 2286.0079317619884, - "y_offset": 921.7816593175237, - "width": 1536, - "height": 256, - "pixel_to_um": 1.0, - "x_reference": 3054.0079317619884, - "has_two_interfaces": false, - "use_glass_top": true, - "focus_camera_exposure_time_ms": 2, - "focus_camera_analog_gain": 0 -} diff --git a/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json b/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json deleted file mode 100644 index 317e6c19..00000000 --- a/software/acquisition_configurations/default_configurations/60x/laser_af_reference_plane.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "x_offset": 2286.0079317619884, - "y_offset": 921.7816593175237, - "width": 1536, - "height": 256, - "pixel_to_um": 1.0, - "x_reference": 3054.0079317619884, - "has_two_interfaces": false, - "use_glass_top": true, - "focus_camera_exposure_time_ms": 2, - "focus_camera_analog_gain": 0 -} diff --git a/software/control/core/core.py b/software/control/core/core.py index 5ed5a649..837be51d 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3579,78 +3579,59 @@ def display_image(self, image, illumination_source): self.graphics_widget_4.img.setImage(image, autoLevels=False) -class AcquisitionConfigurationManager(QObject): - def __init__(self, base_path: str = "./acquisition_configurations", profile: str = "default_configurations"): - QObject.__init__(self) - self.base_config_path = Path(base_path) - self.current_profile = profile - self.channel_configurations = {} # Dict to store configurations for each objective - self.autofocus_configurations = {} # Dict to store autofocus configs for each objective +class ChannelConfigurationManager: + """Manages XML-based channel configurations.""" + def __init__(self): self.active_channel_config = None - self.active_config_flag = -1 # 0: channel_configurations, 1: confocal_configurations, 2: widefield_configurations - if ENABLE_SPINNING_DISK_CONFOCAL: - self.confocal_configurations = {} - self.widefield_configurations = {} - self.available_profiles = self._get_available_profiles() - self.load_profile(profile) - - def _get_available_profiles(self) -> List[str]: - """Get list of available configuration profiles.""" - if not self.base_config_path.exists(): - os.makedirs(self.base_config_path) - 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 list of available objectives in the current 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.active_config_xml_tree = None + self.active_config_xml_tree_root = None + self.active_config_flag = -1 # 0: channel, 1: confocal, 2: widefield + self.current_profile = None + + if ENABLE_SPINNING_DISK: + self.confocal_configurations = {} # Dict[str, List[Configuration]] + self.confocal_config_xml_tree = {} # Dict[str, etree.ElementTree] + self.confocal_config_xml_tree_root = {} # Dict[str, etree.ElementTree] + self.widefield_configurations = {} # Dict[str, List[Configuration]] + self.widefield_config_xml_tree = {} # Dict[str, etree.ElementTree] + self.widefield_config_xml_tree_root = {} # Dict[str, etree.ElementTree] + else: + self.channel_configurations = {} # Dict[str, List[Configuration]] + self.channel_config_xml_tree = {} # Dict[str, etree.ElementTree] + self.channel_config_xml_tree_root = {} # Dict[str, etree.ElementTree] + def set_profile_name(self, profile_name: str) -> None: self.current_profile = profile_name - self.channel_configurations.clear() - self.autofocus_configurations.clear() - if ENABLE_SPINNING_DISK_CONFOCAL: - self.confocal_configurations.clear() - self.widefield_configurations.clear() - # Load configurations for each objective - for objective in self._get_available_objectives(profile_path): - objective_path = profile_path / objective - channel_config_file = objective_path / "channel_configurations.xml" - autofocus_config_file = objective_path / "autofocus_config.json" + def load_configurations(self, objective: str) -> None: + """Load channel configurations for a specific objective.""" + objective_path = self.current_profile / objective + + # Load spinning disk configurations if enabled + if ENABLE_SPINNING_DISK: + confocal_config_file = objective_path / "confocal_configurations.xml" + if confocal_config_file.exists(): + self.confocal_configurations[objective], self.confocal_config_xml_tree[objective], self.confocal_config_xml_tree_root[objective] + = self._load_xml_config(confocal_config_file) + + widefield_config_file = objective_path / "widefield_configurations.xml" + if widefield_config_file.exists(): + self.widefield_configurations[objective], self.widefield_config_xml_tree[objective], self.widefield_config_xml_tree_root[objective] + = self._load_xml_config(widefield_config_file) - # Load channel configurations + else: + channel_config_file = objective_path / "channel_configurations.xml" if channel_config_file.exists(): - self.channel_configurations[objective] = self._load_channel_configurations(channel_config_file) - - # Load autofocus configurations - if autofocus_config_file.exists(): - with open(autofocus_config_file, 'r') as f: - self.autofocus_configurations[objective] = json.load(f) - - self.active_channel_config = self.channel_configurations - self.active_config_flag = 0 - - if ENABLE_SPINNING_DISK_CONFOCAL: - confocal_config_file = objective_path / "confocal_configurations.xml" - if confocal_config_file.exists(): - self.confocal_configurations[objective] = self._load_channel_configurations(confocal_config_file) - else: - self.confocal_configurations[objective] = self.channel_configurations[objective] - - widefield_config_file = objective_path / "widefield_configurations.xml" - if widefield_config_file.exists(): - self.widefield_configurations[objective] = self._load_channel_configurations(widefield_config_file) - else: - self.widefield_configurations[objective] = self.channel_configurations[objective] - - def _load_channel_configurations(self, config_file: Path) -> List[Configuration]: - """Load channel configurations from XML file.""" - if not os.path.isfile(config_file): + self.channel_configurations[objective], self.channel_config_xml_tree[objective], self.channel_config_xml_tree_root[objective] + = self._load_xml_config(channel_config_file) + self.active_channel_config = self.channel_configurations + self.active_config_xml_tree = self.channel_config_xml_tree + self.active_config_xml_tree_root = self.channel_config_xml_tree_root + self.active_config_flag = 0 + + def _load_xml_config(self, config_file: Path) -> List[Configuration]: + """Parse XML and create Configuration objects.""" + if not config_file.is_file(): utils_config.generate_default_configuration(str(config_file)) print(f"Generated default config file for {config_file}") @@ -3673,145 +3654,189 @@ def _load_channel_configurations(self, config_file: Path) -> List[Configuration] emission_filter_position=int(mode.get("EmissionFilterPosition", 1)), ) ) - return configurations + return configurations, config_xml_tree, config_xml_tree_root + + def save_configurations(self, objective: str) -> None: + """Save channel configurations for a specific objective.""" + if ENABLE_SPINNING_DISK: + # Store current state + current_tree = self.active_config_xml_tree + # If we're in confocal mode + if self.active_config_flag == 1: + self._save_xml_config(objective, self.current_profile / objective / "confocal_configurations.xml") + self.active_config_xml_tree = self.widefield_configurations + self._save_xml_config(objective, self.current_profile / objective / "widefield_configurations.xml") + # If we're in widefield mode + elif self.active_config_flag == 2: + self._save_xml_config(objective, self.current_profile / objective / "widefield_configurations.xml") + self.active_config_xml_tree = self.confocal_configurations + self._save_xml_config(objective, self.current_profile / objective / "confocal_configurations.xml") + # Restore original state + self.active_config_xml_tree = current_tree + else: + self._save_xml_config(objective, self.current_profile / objective / "channel_configurations.xml") - def get_channel_configurations_for_objective(self, objective: str) -> List[Configuration]: - """Get channel configurations for a specific objective.""" + def get_configurations(self, objective: str) -> List[Configuration]: + """Get configurations for the current active mode.""" return self.active_channel_config.get(objective, []) - def get_autofocus_configurations_for_objective(self, objective: str) -> Dict[str, Any]: - """Get autofocus configurations for a specific objective.""" - return self.autofocus_configurations.get(objective, {}) + def update_configuration(self, objective: str, config_id: str, attr_name: str, value: Any) -> None: + """Update a specific configuration in the current active mode.""" + if objective not in self.active_channel_config: + return - def get_channel_configurations(self) -> Dict[str, List[Configuration]]: - """Get channel configurations for all objectives.""" - return self.active_channel_config + conf_list = self.active_config_xml_tree_root[objective].xpath("//mode[contains(@ID," + "'" + str(config_id) + "')]") + mode_to_update = conf_list[0] + mode_to_update.set(attr_name, str(value)) - def get_autofocus_configurations(self) -> Dict[str, Dict[str, Any]]: - """Get autofocus configurations for all objectives.""" - return self.active_channel_config + if self.active_config_flag == 0: + config_file = self.current_profile / objective / "channel_configurations.xml" + elif self.active_config_flag == 1: + config_file = self.current_profile / objective / "confocal_configurations.xml" + elif self.active_config_flag == 2: + config_file = self.current_profile / objective / "widefield_configurations.xml" + self._save_xml_config(objective, config_file) + + def _save_xml_config(self, objective: str, filename: Path) -> None: + if not filename.parent.exists(): + os.makedirs(filename.parent) + self.active_config_xml_tree[objective].write(filename, encoding="utf-8", xml_declaration=True, pretty_print=True) - def toggle_confocal_widefield(self, confocal: bool): + def write_configuration_selected(self, objective: str, selected_configurations: List[Configuration], filename: Path) -> None: + """Write selected configurations to a file.""" + if objective not in self.active_channel_config: + raise ValueError(f"Objective {objective} not found") + + for conf in self.configurations: + self.update_configuration(conf.id, "Selected", 0) + for conf in selected_configurations: + self.update_configuration(conf.id, "Selected", 1) + self._save_xml_config(objective, filename) + for conf in selected_configurations: + self.update_configuration(conf.id, "Selected", 0) + + def toggle_spinning_disk_mode(self, confocal: bool) -> None: + """Toggle between confocal and widefield configurations.""" + if not ENABLE_SPINNING_DISK: + return + if confocal: self.active_channel_config = self.confocal_configurations + self.active_config_xml_tree = self.confocal_config_xml_tree + self.active_config_xml_tree_root = self.confocal_config_xml_tree_root self.active_config_flag = 1 else: self.active_channel_config = self.widefield_configurations + self.active_config_xml_tree = self.widefield_config_xml_tree + self.active_config_xml_tree_root = self.widefield_config_xml_tree_root self.active_config_flag = 2 - def update_channel_configuration(self, objective: str, configuration_id: str, attribute_name: str, new_value: Any) -> None: - """Update a specific configuration attribute in memory.""" - if objective not in self.active_channel_config: - raise ValueError(f"Objective {objective} not found") - - # Update directly in memory - for conf in self.active_channel_config[objective]: - if conf.id == configuration_id: - setattr(conf, attribute_name.lower(), new_value) - break - - def save_channel_configurations_for_objective(self, objective: str) -> None: - """Save channel configurations for a specific objective to disk.""" - if objective not in self.active_channel_config: - raise ValueError(f"Objective {objective} not found") +class LaserAFConfigurationManager: + """Manages JSON-based laser autofocus configurations.""" + def __init__(self): + self.autofocus_configurations = {} # Dict[str, Dict[str, Any]] + self.current_profile = None - profile_path = self.base_config_path / self.current_profile - if self.active_config_flag == 0: - config_file = profile_path / objective / "channel_configurations.xml" - elif self.active_config_flag == 1: - config_file = profile_path / objective / "confocal_configurations.xml" - elif self.active_config_flag == 2: - config_file = profile_path / objective / "widefield_configurations.xml" - - # Create XML structure from configurations - root = etree.Element("modes") - for config in self.active_channel_config[objective]: - mode = etree.SubElement(root, "mode") - # Convert configuration object attributes to XML - for attr_name, value in vars(config).items(): - if attr_name != 'id': # Handle ID separately to match original casing - xml_name = attr_name.title().replace('_', '') - mode.set(xml_name, str(value)) - else: - mode.set('ID', str(value)) + def set_profile_name(self, profile_name: str) -> None: + self.current_profile = profile_name - # Write to file - tree = etree.ElementTree(root) - tree.write(str(config_file), encoding="utf-8", xml_declaration=True, pretty_print=True) + def load_configurations(self, objective: str) -> None: + """Load autofocus configurations for a specific objective.""" + config_file = self.current_profile / objective / "laser_af_cache.json" + if config_file.exists(): + with open(config_file, 'r') as f: + self.autofocus_configurations[objective] = json.load(f) - def save_autofocus_configurations_for_objective(self, objective: str) -> None: - """Save autofocus configurations for a specific objective to disk.""" + def save_configurations(self, objective: str) -> None: + """Save autofocus configurations for a specific objective.""" if objective not in self.autofocus_configurations: - raise ValueError(f"No autofocus configuration found for objective {objective}") - - profile_path = self.base_config_path / self.current_profile - config_file = profile_path / objective / "autofocus_config.json" + return + if not self.current_profile/objective.exists(): + os.makedirs(self.current_profile / objective) + config_file = self.current_profile / objective / "laser_af_cache.json" with open(config_file, 'w') as f: json.dump(self.autofocus_configurations[objective], f, indent=4) - def save_all_configurations(self) -> None: - """Write all configurations to disk.""" - profile_path = self.base_config_path / self.current_profile + def get_configurations(self, objective: str) -> Dict[str, Any]: + return self.autofocus_configurations.get(objective, {}) + + def update_configuration(self, objective: str, updates: Dict[str, Any]) -> None: + if objective not in self.autofocus_configurations: + self.autofocus_configurations[objective] = {} + self.autofocus_configurations[objective].update(updates) + +class ConfigurationManager(QObject): + """Main configuration manager that coordinates channel and autofocus configurations.""" + def __init__(self, + base_config_path: Path = Path("acquisition_configurations"), + profile: str = "default_profile", + channel_manager: ChannelConfigurationManager, + af_manager: Optional[LaserAFCacheManager] = None): + 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]: + 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]: + 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_name(profile_name) + if self.laser_af_manager: + self.laser_af_manager.set_profile_name(profile_name) + + # Load configurations for each objective for objective in self._get_available_objectives(profile_path): - if self.active_config_flag == 0: - self.save_channel_configurations_for_objective(objective) - if ENABLE_SPINNING_DISK_CONFOCAL: - # save current config - self.save_channel_configurations_for_objective(objective) - # if current config is confocal, set active config to widefield; otherwise set to confocal - is_confocal = bool(self.active_config_flag - 1) - self.toggle_confocal_widefield(is_confocal) - # save the other config - self.save_channel_configurations_for_objective(objective) - # toggle active config back - self.toggle_confocal_widefield(not is_confocal) - if objective in self.autofocus_configurations: - self.save_autofocus_configurations_for_objective(objective) + 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") - - # Create new profile directory os.makedirs(new_profile_path) - # Save current configurations to new profile + objectives = self.channel_manager.objective_configurations.keys() + self.current_profile = profile_name - self.save_all_configurations() + if self.channel_manager: + self.channel_manager.set_profile_name(profile_name) + if self.laser_af_manager: + self.laser_af_manager.set_profile_name(profile_name) + + 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() - def write_configuration_selected(self, objective: str, selected_configurations: List[Configuration], filename: Path) -> None: - """Write selected configurations to a file.""" - if objective not in self.active_channel_config: - raise ValueError(f"Objective {objective} not found") - - # Create XML structure from configurations - root = etree.Element("modes") - selected_ids = [conf.id for conf in selected_configurations] - - # Write all configurations but mark selected ones - for config in self.active_channel_config[objective]: - mode = etree.SubElement(root, "mode") - # Convert configuration object attributes to XML - for attr_name, value in vars(config).items(): - if attr_name != 'id': - xml_name = attr_name.title().replace('_', '') - mode.set(xml_name, str(value)) - else: - mode.set('ID', str(value)) - - # Set Selected attribute - mode.set("Selected", "1" if config.id in selected_ids else "0") - - # Write to file - tree = etree.ElementTree(root) - tree.write(str(filename), encoding="utf-8", xml_declaration=True, pretty_print=True) - class ContrastManager: def __init__(self): From f77a27acc05bf317ecfea42dc55cc258d9660043 Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 11 Feb 2025 20:25:47 -0800 Subject: [PATCH 10/24] update channel configuration and laser af cache usages everywhere --- software/control/core/core.py | 93 ++++++++++++++++------------- software/control/gui_hcs.py | 37 ++++++++---- software/control/widgets.py | 109 ++++++++++++++++------------------ 3 files changed, 128 insertions(+), 111 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 837be51d..b06b46ad 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -1377,7 +1377,7 @@ def __init__(self, multiPointController): self.liveController = self.multiPointController.liveController self.autofocusController = self.multiPointController.autofocusController self.objectiveStore = self.multiPointController.objectiveStore - self.acquisitionConfigurationManager = self.multiPointController.acquisitionConfigurationManager + self.channelConfigurationManager = self.multiPointController.channelConfigurationManager self.NX = self.multiPointController.NX self.NY = self.multiPointController.NY self.NZ = self.multiPointController.NZ @@ -1754,7 +1754,7 @@ def perform_autofocus(self, region_id, fov): config_AF = next( ( config - for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name_AF ) ) @@ -1776,7 +1776,7 @@ def perform_autofocus(self, region_id, fov): config_AF = next( ( config - for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name_AF ) ) @@ -1887,7 +1887,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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + 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_) @@ -2172,7 +2172,7 @@ def __init__( liveController, autofocusController, objectiveStore, - acquisitionConfigurationManager, + channelConfigurationManager, usb_spectrometer=None, scanCoordinates=None, parent=None, @@ -2187,7 +2187,7 @@ def __init__( self.liveController = liveController self.autofocusController = autofocusController self.objectiveStore = objectiveStore, - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.channelConfigurationManager = channelConfigurationManager self.multiPointWorker: Optional[MultiPointWorker] = None self.thread: Optional[QThread] = None self.NX = 1 @@ -2311,7 +2311,7 @@ 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)) - self.acquisitionConfigurationManager.write_configuration_selected( + 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 @@ -2356,7 +2356,7 @@ def set_selected_configurations(self, selected_configurations_name): self.selected_configurations.append( next( (config - for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name) ) ) @@ -2630,7 +2630,7 @@ def __init__( microcontroller: Microcontroller, stage: AbstractStage, objectiveStore, - acquisitionConfigurationManager, + channelConfigurationManager, liveController: LiveController, autofocusController, imageDisplayWindow, @@ -2640,7 +2640,7 @@ def __init__( self.microcontroller = microcontroller self.stage = stage self.objectiveStore = objectiveStore - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.channelConfigurationManager = channelConfigurationManager self.liveController = liveController self.autofocusController = autofocusController self.imageDisplayWindow = imageDisplayWindow @@ -2746,7 +2746,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.acquisitionConfigurationManager.write_configuration( + self.channelConfigurationManager._save_xml_config( + self.objectiveStore.current_objective, os.path.join(self.base_path, self.experiment_ID) + "/configurations.xml" ) # save the configuration for the experiment except: @@ -2759,7 +2760,7 @@ def set_selected_configurations(self, selected_configurations_name): self.selected_configurations.append( next(( config - for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == configuration_name) ) ) @@ -2860,7 +2861,7 @@ def __init__(self, trackingController: TrackingController): self.microcontroller = self.trackingController.microcontroller self.liveController = self.trackingController.liveController self.autofocusController = self.trackingController.autofocusController - self.acquisitionConfigurationManager = self.trackingController.acquisitionConfigurationManager + self.channelConfigurationManager = self.trackingController.channelConfigurationManager self.imageDisplayWindow = self.trackingController.imageDisplayWindow self.crop_width = self.trackingController.crop_width self.crop_height = self.trackingController.crop_height @@ -3714,8 +3715,11 @@ def write_configuration_selected(self, objective: str, selected_configurations: self._save_xml_config(objective, filename) for conf in selected_configurations: self.update_configuration(conf.id, "Selected", 0) + + def get_channel_configurations_for_objective(self, objective: str) -> List[Configuration]: + return self.active_channel_config.get(objective, []) - def toggle_spinning_disk_mode(self, confocal: bool) -> None: + def toggle_confocal_widefield(self, confocal: bool) -> None: """Toggle between confocal and widefield configurations.""" if not ENABLE_SPINNING_DISK: return @@ -3731,7 +3735,7 @@ def toggle_spinning_disk_mode(self, confocal: bool) -> None: self.active_config_xml_tree_root = self.widefield_config_xml_tree_root self.active_config_flag = 2 -class LaserAFConfigurationManager: +class LaserAFCacheManager: """Manages JSON-based laser autofocus configurations.""" def __init__(self): self.autofocus_configurations = {} # Dict[str, Dict[str, Any]] @@ -3758,10 +3762,13 @@ def save_configurations(self, objective: str) -> None: with open(config_file, 'w') as f: json.dump(self.autofocus_configurations[objective], f, indent=4) - def get_configurations(self, objective: str) -> Dict[str, Any]: + def get_cache_for_objective(self, objective: str) -> Dict[str, Any]: return self.autofocus_configurations.get(objective, {}) - - def update_configuration(self, objective: str, updates: Dict[str, Any]) -> None: + + def get_laser_af_cache(self) -> Dict[str, Any]: + return self.autofocus_configurations + + def update_laser_af_cache(self, objective: str, updates: Dict[str, Any]) -> None: if objective not in self.autofocus_configurations: self.autofocus_configurations[objective] = {} self.autofocus_configurations[objective].update(updates) @@ -3769,10 +3776,10 @@ def update_configuration(self, objective: str, updates: Dict[str, Any]) -> None: class ConfigurationManager(QObject): """Main configuration manager that coordinates channel and autofocus configurations.""" def __init__(self, - base_config_path: Path = Path("acquisition_configurations"), - profile: str = "default_profile", channel_manager: ChannelConfigurationManager, - af_manager: Optional[LaserAFCacheManager] = None): + af_manager: Optional[LaserAFCacheManager] = 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 @@ -4620,13 +4627,14 @@ def __init__( camera, stage: AbstractStage, objectiveStore: Optional[ObjectiveStore] = None, - cachedLaserAFConfigurations: Optional[Dict[str, Any]] = None + laserAFCacheManager: Optional[LaserAFCacheManager] = None ): QObject.__init__(self) self.microcontroller = microcontroller self.camera = camera self.stage = stage self.objectiveStore = objectiveStore + self.laserAFCacheManager = laserAFCacheManager self.is_initialized = False self.x_reference = 0 @@ -4644,8 +4652,8 @@ def __init__( self.image = None # for saving the focus camera image for debugging when centroid cannot be found # Load configurations if provided - self.laser_af_cache = cachedLaserAFConfigurations - if self.laser_af_cache is not None: + if self.laserAFCacheManager: + self.laser_af_cache = self.laserAFCacheManager.get_laser_af_cache() self.load_cached_configuration() def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_reference, @@ -4665,12 +4673,8 @@ def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_re self.is_initialized = True # Update cache if objective store and laser_af_cache is available - if self.objectiveStore and self.laser_af_cache and self.objectiveStore.current_objective: - current_objective = self.objectiveStore.current_objective - if current_objective not in self.laser_af_cache: - self.laser_af_cache[current_objective] = {} - - self.laser_af_cache[current_objective].update({ + if self.objectiveStore and self.laserAFCacheManager and self.objectiveStore.current_objective: + self.laserAFCacheManager.update_laser_af_cache(self.objectiveStore.current_objective, { 'x_offset': x_offset, 'y_offset': y_offset, 'width': width, @@ -4687,7 +4691,7 @@ def load_cached_configuration(self): """Load configuration from the cache if available.""" current_objective = self.objectiveStore.current_objective if self.objectiveStore else None if current_objective and current_objective in self.laser_af_cache: - config = self.laser_af_cache[current_objective] + config = self.laserAFCacheManager.get_cache_for_objective(current_objective) self.focus_camera_exposure_time_ms = config.get('focus_camera_exposure_time_ms', 2), self.focus_camera_analog_gain = config.get('focus_camera_analog_gain', 0) @@ -4739,6 +4743,8 @@ def initialize_auto(self): # Calibrate pixel to um conversion self._calibrate_pixel_to_um() + self.laserAFCacheManager.save_configurations(self.objectiveStore.current_objective) + def _calibrate_pixel_to_um(self): """Calibrate the pixel to micrometer conversion factor.""" self.microcontroller.turn_on_AF_laser() @@ -4774,11 +4780,10 @@ def _calibrate_pixel_to_um(self): # set reference self.x_reference = x1 - # Update cache if objective store and laser_af_cache is available - if self.objectiveStore and self.laser_af_cache and self.objectiveStore.current_objective: - current_objective = self.objectiveStore.current_objective - if current_objective in self.laser_af_cache: - self.laser_af_cache[current_objective]['pixel_to_um'] = self.pixel_to_um + # Update cache + self.laserAFCacheManager.update_laser_af_cache(self.objectiveStore.current_objective, { + 'pixel_to_um': self.pixel_to_um + }) def set_laser_af_properties(self, has_two_interfaces, use_glass_top, focus_camera_exposure_time_ms, focus_camera_analog_gain): # These properties can be set from gui @@ -4787,6 +4792,8 @@ def set_laser_af_properties(self, has_two_interfaces, use_glass_top, focus_camer self.focus_camera_exposure_time_ms = focus_camera_exposure_time_ms self.focus_camera_analog_gain = focus_camera_analog_gain + self.is_initialized = False + def measure_displacement(self): # turn on the laser self.microcontroller.turn_on_AF_laser() @@ -4830,11 +4837,15 @@ def set_reference(self): self.x_reference = x self.signal_displacement_um.emit(0) - # Update cache if objective store and laser_af_cache is available - if self.objectiveStore and self.laser_af_cache and self.objectiveStore.current_objective: - current_objective = self.objectiveStore.current_objective - if current_objective in self.laser_af_cache: - self.laser_af_cache[current_objective]['x_reference'] = x + self.x_offset + # Update cache + self.laserAFCacheManager.update_laser_af_cache(self.objectiveStore.current_objective, { + 'x_reference': x + self.x_offset + }) + self.laserAFCacheManager.save_configurations(self.objectiveStore.current_objective) + + def on_objective_changed(self): + self.is_initialized = False + self.load_cached_configurations() def _calculate_centroid(self, image): """Calculate the centroid of the laser spot.""" diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 50a27572..ee2676be 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -212,7 +212,10 @@ def loadObjects(self, is_simulation): # Common object initialization self.objectiveStore = core.ObjectiveStore(parent=self) - self.acquisitionConfigurationManager = core.AcquisitionConfigurationManager() + self.channelConfigurationManager = core.ChannelConfigurationManager() + if SUPPORT_LASER_AUTOFOCUS: + self.laserAFCacheManager = core.LaserAFCacheManager() + self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, af_manager=self.laserAFCacheManager) self.contrastManager = core.ContrastManager() self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP / 100) self.liveController = core.LiveController( @@ -247,7 +250,7 @@ def loadObjects(self, is_simulation): self.microcontroller, self.stage, self.objectiveStore, - self.acquisitionConfigurationManager, + self.channelConfigurationManager, self.liveController, self.autofocusController, self.imageDisplayWindow, @@ -266,7 +269,7 @@ def loadObjects(self, is_simulation): self.liveController, self.autofocusController, self.objectiveStore, - self.acquisitionConfigurationManager, + self.channelConfigurationManager, scanCoordinates=self.scanCoordinates, parent=self, ) @@ -287,7 +290,7 @@ def loadObjects(self, is_simulation): self.liveController, self.autofocusController, self.objectiveStore, - self.acquisitionConfigurationManager, + self.channelConfigurationManager, scanCoordinates=self.scanCoordinates, parent=self, ) @@ -301,7 +304,7 @@ def loadObjects(self, is_simulation): self.liveController_focus_camera, self.stage, self.objectiveStore, - self.acquisitionConfigurationManager.get_autofocus_configurations() + self.laserAFCacheManager ) if USE_SQUID_FILTERWHEEL: @@ -516,7 +519,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.acquisitionConfigurationManager) + self.spinningDiskConfocalWidget = widgets.SpinningDiskConfocalWidget(self.xlight, self.channelConfigurationManager) if ENABLE_NL5: import control.NL5Widget as NL5Widget @@ -540,7 +543,7 @@ def loadWidgets(self): self.streamHandler, self.liveController, self.objectiveStore, - self.acquisitionConfigurationManager, + self.configurationManager, show_display_options=True, show_autolevel=True, autolevel=True, @@ -627,7 +630,7 @@ def loadWidgets(self): self.navigationViewer, self.multipointController, self.objectiveStore, - self.acquisitionConfigurationManager, + self.channelConfigurationManager, self.scanCoordinates, self.focusMapWidget, ) @@ -636,7 +639,7 @@ def loadWidgets(self): self.navigationViewer, self.multipointController, self.objectiveStore, - self.acquisitionConfigurationManager, + self.channelConfigurationManager, self.scanCoordinates, self.focusMapWidget, self.napariMosaicDisplayWidget, @@ -647,11 +650,11 @@ def loadWidgets(self): self.trackingControlWidget = widgets.TrackingControllerWidget( self.trackingController, self.objectiveStore, - self.acquisitionConfigurationManager, + self.channelConfigurationManager, show_configurations=TRACKING_SHOW_MICROSCOPE_CONFIGURATIONS, ) if ENABLE_STITCHER: - self.stitcherWidget = widgets.StitcherWidget(self.objectiveStore, self.acquisitionConfigurationManager, self.contrastManager) + self.stitcherWidget = widgets.StitcherWidget(self.objectiveStore, self.channelConfigurationManager, self.contrastManager) self.recordTabWidget = QTabWidget() self.setupRecordTabWidget() @@ -666,7 +669,7 @@ def setupImageDisplayTabs(self): self.liveController, self.stage, self.objectiveStore, - self.acquisitionConfigurationManager, + self.channelConfigurationManager, self.contrastManager, self.wellSelectionWidget, ) @@ -943,6 +946,16 @@ def makeConnections(self): self.objectivesWidget.signal_objective_changed.connect(self.wellplateMultiPointWidget.update_coordinates) if SUPPORT_LASER_AUTOFOCUS: + def connect_objective_changed_laser_af(self): + self.laserAutofocusController.on_objective_changed() + self.laserAutofocusControlWidget.update_init_state() + + self.objectivesWidget.signal_objective_changed.connect( + self.connect_objective_changed_laser_af + ) + self.objectivesWidget.signal_objective_changed.connect( + self.laserAutofocusControlWidget.update_init_state + ) self.liveControlWidget_focus_camera.signal_newExposureTime.connect( self.cameraSettingWidget_focus_camera.set_exposure_time ) diff --git a/software/control/widgets.py b/software/control/widgets.py index dccd39fa..3ee5f726 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -350,7 +350,7 @@ def __init__(self, xlight, config_manager=None): self.disk_position_state = self.xlight.get_disk_position() if self.config_manager is not None: - self.config_manager.toogle_confocal_widefield(self.disk_position_state) + self.config_manager.toggle_confocal_widefield(self.disk_position_state) if self.disk_position_state == 1: self.btn_toggle_widefield.setText("Switch to Widefield") @@ -470,7 +470,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: - self.config_manager.toogle_confocal_widefield(self.disk_position_state) + self.config_manager.toggle_confocal_widefield(self.disk_position_state) self.enable_all_buttons() def toggle_motor(self): @@ -846,7 +846,7 @@ def __init__( streamHandler, liveController, objectiveStore=None, - acquisitionConfigurationManager=None, + configurationManager=None, show_trigger_options=True, show_display_options=False, show_autolevel=False, @@ -860,15 +860,17 @@ def __init__( self.liveController = liveController self.streamHandler = streamHandler self.objectiveStore = objectiveStore - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.configurationManager = configurationManager + if self.configurationManager: + self.channelConfigurationManager = self.configurationManager.channel_manager 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 - if acquisitionConfigurationManager: - self.currentConfiguration = self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)[0] + if self.channelConfigurationManager: + self.currentConfiguration = self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)[0] else: self.currentConfiguration = Configuration() self.add_components(show_trigger_options, show_display_options, show_autolevel, autolevel, stretch) @@ -880,15 +882,14 @@ def __init__( def add_components(self, show_trigger_options, show_display_options, show_autolevel, autolevel, stretch): # line 0: acquisition configuration profile management self.dropdown_profiles = QComboBox() - if self.acquisitionConfigurationManager: - self.dropdown_profiles.addItems(self.acquisitionConfigurationManager.available_profiles) - self.dropdown_profiles.setCurrentText(self.acquisitionConfigurationManager.current_profile) + if self.configurationManager: + 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_saveProfile = QPushButton("Save") - self.btn_newProfile = QPushButton("Create") + self.btn_newProfile = QPushButton("Save As") # line 1: trigger mode self.triggerMode = None @@ -906,9 +907,9 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_triggerFPS.setDecimals(0) # line 3: choose microscope mode / toggle live mode - if self.acquisitionConfigurationManager: + if self.channelConfigurationManager: self.dropdown_modeSelection = QComboBox() - for microscope_configuration in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + 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) @@ -994,14 +995,13 @@ def add_components(self, show_trigger_options, show_display_options, show_autole # connections self.btn_loadProfile.clicked.connect(self.load_profile) - self.btn_saveProfile.clicked.connect(self.save_profile) self.btn_newProfile.clicked.connect(self.create_new_profile) self.entry_triggerFPS.valueChanged.connect(self.liveController.set_trigger_fps) self.entry_displayFPS.valueChanged.connect(self.streamHandler.set_display_fps) self.slider_resolutionScaling.valueChanged.connect(self.streamHandler.set_display_resolution_scaling) self.slider_resolutionScaling.valueChanged.connect(self.liveController.set_display_resolution_scaling) - if self.acquisitionConfigurationManager: + if self.channelConfigurationManager: self.dropdown_modeSelection.currentTextChanged.connect(self.update_microscope_mode_by_name) self.dropdown_triggerManu.currentIndexChanged.connect(self.update_trigger_mode) self.btn_live.clicked.connect(self.toggle_live) @@ -1025,7 +1025,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole grid_line1 = QHBoxLayout() grid_line1.addWidget(QLabel("Live Configuration")) - if self.acquisitionConfigurationManager: + if self.channelConfigurationManager: grid_line1.addWidget(self.dropdown_modeSelection, 2) grid_line1.addWidget(self.btn_live, 1) @@ -1065,7 +1065,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole grid_line05.addWidget(self.label_resolutionScaling) self.grid = QVBoxLayout() - if self.acquisitionConfigurationManager: + if self.configurationManager: self.grid.addLayout(grid_line_profile) if show_trigger_options: self.grid.addLayout(grid_line0) @@ -1080,34 +1080,21 @@ def add_components(self, show_trigger_options, show_display_options, show_autole def load_profile(self): """Load the selected profile.""" - if self.acquisitionConfigurationManager: + if self.configurationManager: profile_name = self.dropdown_profiles.currentText() # Load the profile - self.acquisitionConfigurationManager.load_profile(profile_name) + self.configurationManager.load_profile(profile_name) # Update the mode selection dropdown self.dropdown_modeSelection.clear() - for microscope_configuration in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.dropdown_modeSelection.addItem(microscope_configuration.name) # Update to first configuration if self.dropdown_modeSelection.count() > 0: self.update_microscope_mode_by_name(self.dropdown_modeSelection.currentText()) - def save_profile(self): - """Save current configurations to selected profile with confirmation.""" - if self.acquisitionConfigurationManager: - profile_name = self.dropdown_profiles.currentText() - msg = QMessageBox() - msg.setIcon(QMessageBox.Question) - msg.setText(f"Save current configurations to profile '{profile_name}'?") - msg.setWindowTitle("Save Profile") - msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - - if msg.exec_() == QMessageBox.Yes: - self.acquisitionConfigurationManager.save_all_configurations() - def create_new_profile(self): """Create a new profile with current configurations.""" - if self.acquisitionConfigurationManager: + if self.configurationManager: dialog = QInputDialog() profile_name, ok = dialog.getText( self, @@ -1119,7 +1106,7 @@ def create_new_profile(self): if ok and profile_name: try: - self.acquisitionConfigurationManager.create_new_profile(profile_name) + self.configurationManager.create_new_profile(profile_name) # Update profile dropdown self.dropdown_profiles.addItem(profile_name) self.dropdown_profiles.setCurrentText(profile_name) @@ -1144,11 +1131,11 @@ def update_camera_settings(self): 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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)) + # 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.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == current_microscope_mode_name ), None, @@ -1168,19 +1155,19 @@ 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.acquisitionConfigurationManager.update_channel_configuration(self.objectiveStore.current_objective, 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.acquisitionConfigurationManager.update_channel_configuration(self.objectiveStore.current_objective, 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.acquisitionConfigurationManager.update_channel_configuration( + self.channelConfigurationManager.update_configuration( self.objectiveStore.current_objective, self.currentConfiguration.id, "IlluminationIntensity", new_value ) self.liveController.set_illumination( @@ -1946,7 +1933,7 @@ def __init__( navigationViewer, multipointController, objectiveStore, - acquisitionConfigurationManager, + channelConfigurationManager, scanCoordinates, focusMapWidget, *args, @@ -1961,7 +1948,7 @@ def __init__( self.navigationViewer = navigationViewer self.multipointController = multipointController self.objectiveStore = objectiveStore - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.channelConfigurationManager = channelConfigurationManager self.scanCoordinates = scanCoordinates self.focusMapWidget = focusMapWidget self.base_path_is_set = False @@ -2113,7 +2100,7 @@ def add_components(self): self.entry_Nt.setFixedWidth(max_num_width) self.list_configurations = QListWidget() - for microscope_configuration in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + 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 @@ -3140,7 +3127,7 @@ def __init__( navigationViewer, multipointController, objectiveStore, - acquisitionConfigurationManager, + channelConfigurationManager, scanCoordinates, focusMapWidget=None, napariMosaicWidget=None, @@ -3153,7 +3140,7 @@ def __init__( self.navigationViewer = navigationViewer self.multipointController = multipointController self.objectiveStore = objectiveStore - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.channelConfigurationManager = channelConfigurationManager self.scanCoordinates = scanCoordinates self.focusMapWidget = focusMapWidget if napariMosaicWidget is None: @@ -3269,7 +3256,7 @@ def add_components(self): self.combobox_z_stack.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.list_configurations = QListWidget() - for microscope_configuration in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + 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) @@ -4181,10 +4168,10 @@ def resizeEvent(self, event): class StitcherWidget(QFrame): - def __init__(self, objectiveStore, acquisitionConfigurationManager, contrastManager, *args, **kwargs): + def __init__(self, objectiveStore, channelConfigurationManager, contrastManager, *args, **kwargs): super(StitcherWidget, self).__init__(*args, **kwargs) self.objectiveStore = objectiveStore - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.channelConfigurationManager = channelConfigurationManager self.contrastManager = contrastManager self.stitcherThread = None self.output_path = "" @@ -4416,7 +4403,7 @@ def __init__( liveController, stage: AbstractStage, objectiveStore, - acquisitionConfigurationManager, + channelConfigurationManager, contrastManager, wellSelectionWidget=None, show_trigger_options=True, @@ -4430,7 +4417,7 @@ def __init__( self.liveController = liveController self.stage = stage self.objectiveStore = objectiveStore - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.channelConfigurationManager = channelConfigurationManager self.wellSelectionWidget = wellSelectionWidget self.live_configuration = self.liveController.currentConfiguration self.image_width = 0 @@ -4502,7 +4489,7 @@ def initControlWidgets(self, show_trigger_options, show_display_options, show_au # Microscope Configuration self.dropdown_modeSelection = QComboBox() - for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + 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) @@ -4761,7 +4748,7 @@ def update_microscope_mode_by_name(self, current_microscope_mode_name): self.live_configuration = next( ( config - for config in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) if config.name == current_microscope_mode_name ), None, @@ -4774,17 +4761,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.acquisitionConfigurationManager.update_channel_configuration(self.objectiveStore.current_objective, 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.acquisitionConfigurationManager.update_channel_configuration(self.objectiveStore.current_objective, 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.acquisitionConfigurationManager.update_channel_configuration(self.objectiveStore.current_objective, 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): @@ -5534,7 +5521,7 @@ def __init__( self, trackingController: TrackingController, objectiveStore, - acquisitionConfigurationManager, + channelConfigurationManager, show_configurations=True, main=None, *args, @@ -5543,7 +5530,7 @@ def __init__( super().__init__(*args, **kwargs) self.trackingController = trackingController self.objectiveStore = objectiveStore - self.acquisitionConfigurationManager = acquisitionConfigurationManager + self.channelConfigurationManager = channelConfigurationManager self.base_path_is_set = False self.add_components(show_configurations) self.setFrameStyle(QFrame.Panel | QFrame.Raised) @@ -5581,7 +5568,7 @@ def add_components(self, show_configurations): self.entry_tracking_interval.setValue(0) self.list_configurations = QListWidget() - for microscope_configuration in self.acquisitionConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + 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 @@ -6430,6 +6417,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()) From b175456b5372d9160549371eb7a6e3ba83cf788a Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 11 Feb 2025 20:48:25 -0800 Subject: [PATCH 11/24] gui laser af setting --- software/control/widgets.py | 65 +++++++++++++++++++++++++++++++++++++ software/main_hcs.py | 16 +++++++-- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/software/control/widgets.py b/software/control/widgets.py index 3ee5f726..b06db47b 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -330,6 +330,71 @@ def apply_and_exit(self): self.close() +class LaserAutofocusSettingsWidget(QDialog): + def __init__(self, laser_af_controller): + super().__init__() + self.laser_af_controller = laser_af_controller + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout() + + # Two interfaces checkbox + self.has_two_interfaces_cb = QCheckBox("Has Two Interfaces") + layout.addWidget(self.has_two_interfaces_cb) + + # Glass top checkbox + self.use_glass_top_cb = QCheckBox("Use Glass Top") + layout.addWidget(self.use_glass_top_cb) + + # Exposure time + 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) + layout.addLayout(exposure_layout) + + # Analog gain + gain_layout = QHBoxLayout() + gain_layout.addWidget(QLabel("Focus Camera Analog Gain:")) + self.gain_spinbox = QSpinBox() + self.gain_spinbox.setRange(0, 100) + self.gain_spinbox.setValue(0) + gain_layout.addWidget(self.gain_spinbox) + layout.addLayout(gain_layout) + + # 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") + self.apply_button.clicked.connect(self.apply_settings) + layout.addWidget(self.apply_button) + + self.setLayout(layout) + + 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(), + focus_camera_exposure_time_ms=self.exposure_spinbox.value(), + focus_camera_analog_gain=self.gain_spinbox.value() + ) + self.laser_af_controller.initialize_auto() + + class SpinningDiskConfocalWidget(QWidget): def __init__(self, xlight, config_manager=None): super(SpinningDiskConfocalWidget, self).__init__() diff --git a/software/main_hcs.py b/software/main_hcs.py index 6f87a9d5..cef84263 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -19,9 +19,10 @@ # app specific libraries import control.gui_hcs as gui from configparser import ConfigParser -from control.widgets import ConfigEditorBackwardsCompatible +from control.widgets import ConfigEditorBackwardsCompatible, LaserAutofocusSettingsWidget from control._def import CACHED_CONFIG_FILE_PATH from control._def import USE_TERMINAL_CONSOLE +from control._def import SUPPORT_LASER_AUTOFOCUS if USE_TERMINAL_CONSOLE: from control.console import ConsoleThread @@ -29,6 +30,10 @@ def show_config(cfp, configpath, main_gui): config_widget = ConfigEditorBackwardsCompatible(cfp, configpath, main_gui) config_widget.exec_() +def show_laser_af_settings(laser_af_controller): + laser_af_widget = LaserAutofocusSettingsWidget(laser_af_controller) + laser_af_widget.exec_() + ''' # Planning to replace this with a better design def show_acq_config(cfm): @@ -71,12 +76,19 @@ def show_acq_config(cfm): ''' # 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.acquisitionConfigurationManager)) + acq_config_action.triggered.connect(lambda: show_acq_config(win.configurationManager)) ''' file_menu = QMenu("File", win) #file_menu.addAction(acq_config_action) + if SUPPORT_LASER_AUTOFOCUS: + af_settings_action = QAction("Laser Autofocus Settings", win) + af_settings_action.triggered.connect( + lambda: show_laser_af_settings(win.laserAutofocusController) + ) + file_menu.addAction(af_settings_action) + if not legacy_config: config_action = QAction("Microscope Settings", win) config_action.triggered.connect(lambda: show_config(cf_editor_parser, config_files[0], win)) From 735fd478f09bd00282def85fd1f31b5c8d4b2d0a Mon Sep 17 00:00:00 2001 From: You Yan Date: Wed, 12 Feb 2025 01:48:52 -0800 Subject: [PATCH 12/24] bug fix --- software/control/core/core.py | 82 +++++++++++++++++------------------ software/control/gui_hcs.py | 4 +- software/control/widgets.py | 75 +++++++++++++++++++------------- 3 files changed, 88 insertions(+), 73 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index b06b46ad..2a2496cd 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3587,9 +3587,9 @@ def __init__(self): self.active_config_xml_tree = None self.active_config_xml_tree_root = None self.active_config_flag = -1 # 0: channel, 1: confocal, 2: widefield - self.current_profile = None + self.current_profile_path = None - if ENABLE_SPINNING_DISK: + if ENABLE_SPINNING_DISK_CONFOCAL: self.confocal_configurations = {} # Dict[str, List[Configuration]] self.confocal_config_xml_tree = {} # Dict[str, etree.ElementTree] self.confocal_config_xml_tree_root = {} # Dict[str, etree.ElementTree] @@ -3601,34 +3601,31 @@ def __init__(self): self.channel_config_xml_tree = {} # Dict[str, etree.ElementTree] self.channel_config_xml_tree_root = {} # Dict[str, etree.ElementTree] - def set_profile_name(self, profile_name: str) -> None: - self.current_profile = profile_name + def set_profile_path(self, profile_path: Path) -> None: + self.current_profile_path = profile_path def load_configurations(self, objective: str) -> None: """Load channel configurations for a specific objective.""" - objective_path = self.current_profile / objective - + objective_path = self.current_profile_path / objective + # Load spinning disk configurations if enabled - if ENABLE_SPINNING_DISK: + if ENABLE_SPINNING_DISK_CONFOCAL: confocal_config_file = objective_path / "confocal_configurations.xml" - if confocal_config_file.exists(): - self.confocal_configurations[objective], self.confocal_config_xml_tree[objective], self.confocal_config_xml_tree_root[objective] - = self._load_xml_config(confocal_config_file) - + self.confocal_configurations[objective], self.confocal_config_xml_tree[objective], self.confocal_config_xml_tree_root[objective] \ + = self._load_xml_config(confocal_config_file) + widefield_config_file = objective_path / "widefield_configurations.xml" - if widefield_config_file.exists(): - self.widefield_configurations[objective], self.widefield_config_xml_tree[objective], self.widefield_config_xml_tree_root[objective] - = self._load_xml_config(widefield_config_file) + self.widefield_configurations[objective], self.widefield_config_xml_tree[objective], self.widefield_config_xml_tree_root[objective] \ + = self._load_xml_config(widefield_config_file) else: channel_config_file = objective_path / "channel_configurations.xml" - if channel_config_file.exists(): - self.channel_configurations[objective], self.channel_config_xml_tree[objective], self.channel_config_xml_tree_root[objective] - = self._load_xml_config(channel_config_file) - self.active_channel_config = self.channel_configurations - self.active_config_xml_tree = self.channel_config_xml_tree - self.active_config_xml_tree_root = self.channel_config_xml_tree_root - self.active_config_flag = 0 + self.channel_configurations[objective], self.channel_config_xml_tree[objective], self.channel_config_xml_tree_root[objective] \ + = self._load_xml_config(channel_config_file) + self.active_channel_config = self.channel_configurations + self.active_config_xml_tree = self.channel_config_xml_tree + self.active_config_xml_tree_root = self.channel_config_xml_tree_root + self.active_config_flag = 0 def _load_xml_config(self, config_file: Path) -> List[Configuration]: """Parse XML and create Configuration objects.""" @@ -3659,23 +3656,23 @@ def _load_xml_config(self, config_file: Path) -> List[Configuration]: def save_configurations(self, objective: str) -> None: """Save channel configurations for a specific objective.""" - if ENABLE_SPINNING_DISK: + if ENABLE_SPINNING_DISK_CONFOCAL: # Store current state current_tree = self.active_config_xml_tree # If we're in confocal mode if self.active_config_flag == 1: - self._save_xml_config(objective, self.current_profile / objective / "confocal_configurations.xml") + self._save_xml_config(objective, self.current_profile_path / "confocal_configurations.xml") self.active_config_xml_tree = self.widefield_configurations - self._save_xml_config(objective, self.current_profile / objective / "widefield_configurations.xml") + self._save_xml_config(objective, self.current_profile_path / "widefield_configurations.xml") # If we're in widefield mode elif self.active_config_flag == 2: - self._save_xml_config(objective, self.current_profile / objective / "widefield_configurations.xml") + self._save_xml_config(objective, self.current_profile_path / "widefield_configurations.xml") self.active_config_xml_tree = self.confocal_configurations - self._save_xml_config(objective, self.current_profile / objective / "confocal_configurations.xml") + self._save_xml_config(objective, self.current_profile_path / "confocal_configurations.xml") # Restore original state self.active_config_xml_tree = current_tree else: - self._save_xml_config(objective, self.current_profile / objective / "channel_configurations.xml") + self._save_xml_config(objective, self.current_profile_path / "channel_configurations.xml") def get_configurations(self, objective: str) -> List[Configuration]: """Get configurations for the current active mode.""" @@ -3691,11 +3688,11 @@ def update_configuration(self, objective: str, config_id: str, attr_name: str, v mode_to_update.set(attr_name, str(value)) if self.active_config_flag == 0: - config_file = self.current_profile / objective / "channel_configurations.xml" + config_file = self.current_profile_path / "channel_configurations.xml" elif self.active_config_flag == 1: - config_file = self.current_profile / objective / "confocal_configurations.xml" + config_file = self.current_profile_path / "confocal_configurations.xml" elif self.active_config_flag == 2: - config_file = self.current_profile / objective / "widefield_configurations.xml" + config_file = self.current_profile_path / "widefield_configurations.xml" self._save_xml_config(objective, config_file) def _save_xml_config(self, objective: str, filename: Path) -> None: @@ -3739,14 +3736,14 @@ class LaserAFCacheManager: """Manages JSON-based laser autofocus configurations.""" def __init__(self): self.autofocus_configurations = {} # Dict[str, Dict[str, Any]] - self.current_profile = None + self.current_profile_path = None - def set_profile_name(self, profile_name: str) -> None: - self.current_profile = profile_name + 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 / objective / "laser_af_cache.json" + config_file = self.current_profile_path / objective / "laser_af_cache.json" if config_file.exists(): with open(config_file, 'r') as f: self.autofocus_configurations[objective] = json.load(f) @@ -3756,9 +3753,9 @@ def save_configurations(self, objective: str) -> None: if objective not in self.autofocus_configurations: return - if not self.current_profile/objective.exists(): - os.makedirs(self.current_profile / objective) - config_file = self.current_profile / objective / "laser_af_cache.json" + if not self.current_profile_path / objective.exists(): + os.makedirs(self.current_profile_path / objective) + config_file = self.current_profile_path / objective / "laser_af_cache.json" with open(config_file, 'w') as f: json.dump(self.autofocus_configurations[objective], f, indent=4) @@ -3777,7 +3774,7 @@ class ConfigurationManager(QObject): """Main configuration manager that coordinates channel and autofocus configurations.""" def __init__(self, channel_manager: ChannelConfigurationManager, - af_manager: Optional[LaserAFCacheManager] = None, + laser_af_manager: Optional[LaserAFCacheManager] = None, base_config_path: Path = Path("acquisition_configurations"), profile: str = "default_profile"): super().__init__() @@ -3809,9 +3806,9 @@ def load_profile(self, profile_name: str) -> None: self.current_profile = profile_name if self.channel_manager: - self.channel_manager.set_profile_name(profile_name) + self.channel_manager.set_profile_path(profile_path) if self.laser_af_manager: - self.laser_af_manager.set_profile_name(profile_name) + self.laser_af_manager.set_profile_path(profile_path) # Load configurations for each objective for objective in self._get_available_objectives(profile_path): @@ -3831,9 +3828,9 @@ def create_new_profile(self, profile_name: str) -> None: self.current_profile = profile_name if self.channel_manager: - self.channel_manager.set_profile_name(profile_name) + self.channel_manager.set_profile_path(profile_path) if self.laser_af_manager: - self.laser_af_manager.set_profile_name(profile_name) + self.laser_af_manager.set_profile_path(profile_path) for objective in objectives: os.makedirs(new_profile_path / objective) @@ -4625,6 +4622,7 @@ def __init__( self, microcontroller: Microcontroller, camera, + liveController, stage: AbstractStage, objectiveStore: Optional[ObjectiveStore] = None, laserAFCacheManager: Optional[LaserAFCacheManager] = None diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index ee2676be..168862d6 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -215,7 +215,9 @@ def loadObjects(self, is_simulation): self.channelConfigurationManager = core.ChannelConfigurationManager() if SUPPORT_LASER_AUTOFOCUS: self.laserAFCacheManager = core.LaserAFCacheManager() - self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, af_manager=self.laserAFCacheManager) + self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, laser_af_manager=self.laserAFCacheManager) + else: + self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager) self.contrastManager = core.ContrastManager() self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP / 100) self.liveController = core.LiveController( diff --git a/software/control/widgets.py b/software/control/widgets.py index b06db47b..1af72ec0 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -934,7 +934,7 @@ def __init__( self.streamHandler.set_display_fps(self.fps_display) self.triggerMode = TriggerMode.SOFTWARE - if self.channelConfigurationManager: + if self.configurationManager and self.channelConfigurationManager: self.currentConfiguration = self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)[0] else: self.currentConfiguration = Configuration() @@ -972,7 +972,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_triggerFPS.setDecimals(0) # line 3: choose microscope mode / toggle live mode - if self.channelConfigurationManager: + if self.configurationManager and self.channelConfigurationManager: self.dropdown_modeSelection = QComboBox() for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.dropdown_modeSelection.addItems([microscope_configuration.name]) @@ -1066,7 +1066,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_displayFPS.valueChanged.connect(self.streamHandler.set_display_fps) self.slider_resolutionScaling.valueChanged.connect(self.streamHandler.set_display_resolution_scaling) self.slider_resolutionScaling.valueChanged.connect(self.liveController.set_display_resolution_scaling) - if self.channelConfigurationManager: + if self.configurationManager and self.channelConfigurationManager: self.dropdown_modeSelection.currentTextChanged.connect(self.update_microscope_mode_by_name) self.dropdown_triggerManu.currentIndexChanged.connect(self.update_trigger_mode) self.btn_live.clicked.connect(self.toggle_live) @@ -1085,12 +1085,12 @@ def add_components(self, show_trigger_options, show_display_options, show_autole grid_line_profile.addWidget(QLabel("Configuration Profile")) grid_line_profile.addWidget(self.dropdown_profiles, 2) grid_line_profile.addWidget(self.btn_loadProfile) - grid_line_profile.addWidget(self.btn_saveProfile) + grid_line_profile.addWidget(self.btn_newProfile) grid_line1 = QHBoxLayout() grid_line1.addWidget(QLabel("Live Configuration")) - if self.channelConfigurationManager: + if self.configurationManager and self.channelConfigurationManager: grid_line1.addWidget(self.dropdown_modeSelection, 2) grid_line1.addWidget(self.btn_live, 1) @@ -1197,22 +1197,31 @@ def update_camera_settings(self): 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.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)) - self.currentConfiguration = next( - ( - config - for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) - if config.name == current_microscope_mode_name + if self.configurationManager and self.channelConfigurationManager: + self.currentConfiguration = next( + ( + config + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == current_microscope_mode_name ), None, - ) - self.signal_live_configuration.emit(self.currentConfiguration) - # update the microscope to the current configuration - self.liveController.set_microscope_mode(self.currentConfiguration) - # update the exposure time and analog gain settings according to the selected configuration - self.entry_exposureTime.setValue(self.currentConfiguration.exposure_time) - self.entry_analogGain.setValue(self.currentConfiguration.analog_gain) - self.entry_illuminationIntensity.setValue(self.currentConfiguration.illumination_intensity) - self.is_switching_mode = False + ) + self.signal_live_configuration.emit(self.currentConfiguration) + # update the microscope to the current configuration + self.liveController.set_microscope_mode(self.currentConfiguration) + # update the exposure time and analog gain settings according to the selected configuration + self.entry_exposureTime.setValue(self.currentConfiguration.exposure_time) + self.entry_analogGain.setValue(self.currentConfiguration.analog_gain) + self.entry_illuminationIntensity.setValue(self.currentConfiguration.illumination_intensity) + self.is_switching_mode = False + + else: + # laser autofocus live control + self.currentConfiguration = Configuration(exposure_time=100, analog_gain=0.0) + # update the exposure time and analog gain settings according to the selected configuration + self.entry_exposureTime.setValue(self.currentConfiguration.exposure_time) + self.entry_analogGain.setValue(self.currentConfiguration.analog_gain) + self.is_switching_mode = False def update_trigger_mode(self): self.liveController.set_trigger_mode(self.dropdown_triggerManu.currentText()) @@ -4810,19 +4819,25 @@ def set_microscope_mode(self, config): self.dropdown_modeSelection.setCurrentText(config.name) def update_microscope_mode_by_name(self, current_microscope_mode_name): - self.live_configuration = next( - ( - config - for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) - if config.name == current_microscope_mode_name - ), - None, - ) - if self.live_configuration: - self.liveController.set_microscope_mode(self.live_configuration) + if self.configurationManager and self.channelConfigurationManager: + self.live_configuration = next( + ( + config + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == current_microscope_mode_name + ), + None, + ) + if self.live_configuration: + self.liveController.set_microscope_mode(self.live_configuration) + self.entry_exposureTime.setValue(self.live_configuration.exposure_time) + self.entry_analogGain.setValue(self.live_configuration.analog_gain) + self.slider_illuminationIntensity.setValue(int(self.live_configuration.illumination_intensity)) + else: + # laser autofocus live control + self.live_configuration = Configuration(exposure_time=100, analog_gain=0.0) self.entry_exposureTime.setValue(self.live_configuration.exposure_time) self.entry_analogGain.setValue(self.live_configuration.analog_gain) - self.slider_illuminationIntensity.setValue(int(self.live_configuration.illumination_intensity)) def update_config_exposure_time(self, new_value): self.live_configuration.exposure_time = new_value From a6b259343fd3db96d94c5098daa6894e6b7679b7 Mon Sep 17 00:00:00 2001 From: You Yan Date: Wed, 12 Feb 2025 17:42:52 -0800 Subject: [PATCH 13/24] bug fix --- software/control/gui_hcs.py | 2 +- software/control/widgets.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 168862d6..fa3771fe 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -953,7 +953,7 @@ def connect_objective_changed_laser_af(self): self.laserAutofocusControlWidget.update_init_state() self.objectivesWidget.signal_objective_changed.connect( - self.connect_objective_changed_laser_af + connect_objective_changed_laser_af ) self.objectivesWidget.signal_objective_changed.connect( self.laserAutofocusControlWidget.update_init_state diff --git a/software/control/widgets.py b/software/control/widgets.py index 1af72ec0..c4d9b6b8 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -399,7 +399,6 @@ class SpinningDiskConfocalWidget(QWidget): def __init__(self, xlight, config_manager=None): super(SpinningDiskConfocalWidget, self).__init__() - self.objective_store = objective_store self.config_manager = config_manager self.xlight = xlight @@ -1206,7 +1205,7 @@ def update_microscope_mode_by_name(self, current_microscope_mode_name): ), None, ) - self.signal_live_configuration.emit(self.currentConfiguration) + self.signal_live_configuration.emit(self.currentConfiguration) # update the microscope to the current configuration self.liveController.set_microscope_mode(self.currentConfiguration) # update the exposure time and analog gain settings according to the selected configuration From f9f3a62482808142469d37a4667153069ca7ca3c Mon Sep 17 00:00:00 2001 From: You Yan Date: Fri, 14 Feb 2025 22:33:06 -0800 Subject: [PATCH 14/24] new laser af configuration widget --- software/control/gui_hcs.py | 18 +++--- software/control/widgets.py | 121 ++++++++++++++++++++++++++++-------- software/main_hcs.py | 13 +--- 3 files changed, 103 insertions(+), 49 deletions(-) diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index fa3771fe..70424d70 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -597,9 +597,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.laserAutofocusController, stretch=False, ) # ,show_display_options=True) self.waveformDisplay = widgets.WaveformDisplay(N=1000, include_x=True, include_y=False) @@ -712,9 +713,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() @@ -955,16 +956,11 @@ def connect_objective_changed_laser_af(self): self.objectivesWidget.signal_objective_changed.connect( connect_objective_changed_laser_af ) - self.objectivesWidget.signal_objective_changed.connect( - self.laserAutofocusControlWidget.update_init_state - ) - self.liveControlWidget_focus_camera.signal_newExposureTime.connect( + self.focusCameraControlWidget.signal_newExposureTime.connect( self.cameraSettingWidget_focus_camera.set_exposure_time ) - self.liveControlWidget_focus_camera.signal_newAnalogGain.connect( - self.cameraSettingWidget_focus_camera.set_analog_gain - ) - 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/widgets.py b/software/control/widgets.py index c4d9b6b8..52659604 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -330,24 +330,43 @@ def apply_and_exit(self): self.close() -class LaserAutofocusSettingsWidget(QDialog): - def __init__(self, laser_af_controller): +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) - # Two interfaces checkbox - self.has_two_interfaces_cb = QCheckBox("Has Two Interfaces") - layout.addWidget(self.has_two_interfaces_cb) + # Live control group + live_group = QFrame() + live_group.setFrameStyle(QFrame.Panel | QFrame.Raised) + live_layout = QVBoxLayout() - # Glass top checkbox - self.use_glass_top_cb = QCheckBox("Use Glass Top") - layout.addWidget(self.use_glass_top_cb) + # Live button + self.btn_live = QPushButton("Start Live") + self.btn_live.setCheckable(True) + self.btn_live.setStyleSheet("background-color: #C2C2FF") - # Exposure time + # Exposure time control exposure_layout = QHBoxLayout() exposure_layout.addWidget(QLabel("Focus Camera Exposure (ms):")) self.exposure_spinbox = QDoubleSpinBox() @@ -355,16 +374,22 @@ def init_ui(self): self.exposure_spinbox.setValue(2) self.exposure_spinbox.setDecimals(1) exposure_layout.addWidget(self.exposure_spinbox) - layout.addLayout(exposure_layout) - # Analog gain - gain_layout = QHBoxLayout() - gain_layout.addWidget(QLabel("Focus Camera Analog Gain:")) - self.gain_spinbox = QSpinBox() - self.gain_spinbox.setRange(0, 100) - self.gain_spinbox.setValue(0) - gain_layout.addWidget(self.gain_spinbox) - layout.addLayout(gain_layout) + # 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 ''' @@ -380,17 +405,41 @@ def init_ui(self): # Apply button self.apply_button = QPushButton("Apply and Initialize") - self.apply_button.clicked.connect(self.apply_settings) - layout.addWidget(self.apply_button) + # 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(), - focus_camera_exposure_time_ms=self.exposure_spinbox.value(), - focus_camera_analog_gain=self.gain_spinbox.value() ) self.laser_af_controller.initialize_auto() @@ -938,7 +987,7 @@ def __init__( else: self.currentConfiguration = Configuration() self.add_components(show_trigger_options, show_display_options, show_autolevel, autolevel, stretch) - self.setFrameStyle(QFrame.Panel | QFrame.Raised) + self.setFrameStyle(QFrame.NoFrame) self.update_microscope_mode_by_name(self.currentConfiguration.name) self.is_switching_mode = False # flag used to prevent from settings being set by twice - from both mode change slot and value change slot; another way is to use blockSignals(True) @@ -1128,9 +1177,22 @@ def add_components(self, show_trigger_options, show_display_options, show_autole else: grid_line05.addWidget(self.label_resolutionScaling) - self.grid = QVBoxLayout() + # Create separate frames for each section + self.profile_frame = QFrame() + self.profile_frame.setFrameStyle(QFrame.Panel | QFrame.Raised) + self.control_frame = QFrame() + self.control_frame.setFrameStyle(QFrame.Panel | QFrame.Raised) + + # First section layout + self.grid0 = QVBoxLayout() if self.configurationManager: - self.grid.addLayout(grid_line_profile) + self.grid0.addLayout(grid_line_profile) + if not stretch: + self.grid0.addStretch() + self.profile_frame.setLayout(self.grid0) + + # Second section layout + self.grid = QVBoxLayout() if show_trigger_options: self.grid.addLayout(grid_line0) self.grid.addLayout(grid_line1) @@ -1140,7 +1202,14 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.grid.addLayout(grid_line05) if not stretch: self.grid.addStretch() - self.setLayout(self.grid) + self.control_frame.setLayout(self.grid) + + # Main layout to hold both frames + main_layout = QVBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(self.profile_frame) + main_layout.addWidget(self.control_frame) + self.setLayout(main_layout) def load_profile(self): """Load the selected profile.""" diff --git a/software/main_hcs.py b/software/main_hcs.py index cef84263..2b9c9c1e 100644 --- a/software/main_hcs.py +++ b/software/main_hcs.py @@ -19,7 +19,7 @@ # app specific libraries import control.gui_hcs as gui from configparser import ConfigParser -from control.widgets import ConfigEditorBackwardsCompatible, LaserAutofocusSettingsWidget +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 @@ -30,10 +30,6 @@ def show_config(cfp, configpath, main_gui): config_widget = ConfigEditorBackwardsCompatible(cfp, configpath, main_gui) config_widget.exec_() -def show_laser_af_settings(laser_af_controller): - laser_af_widget = LaserAutofocusSettingsWidget(laser_af_controller) - laser_af_widget.exec_() - ''' # Planning to replace this with a better design def show_acq_config(cfm): @@ -82,13 +78,6 @@ def show_acq_config(cfm): file_menu = QMenu("File", win) #file_menu.addAction(acq_config_action) - if SUPPORT_LASER_AUTOFOCUS: - af_settings_action = QAction("Laser Autofocus Settings", win) - af_settings_action.triggered.connect( - lambda: show_laser_af_settings(win.laserAutofocusController) - ) - file_menu.addAction(af_settings_action) - if not legacy_config: config_action = QAction("Microscope Settings", win) config_action.triggered.connect(lambda: show_config(cf_editor_parser, config_files[0], win)) From ef493496f8e6f7513a8c9571ba4b5f4ef8e6a384 Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 15 Feb 2025 19:26:42 -0800 Subject: [PATCH 15/24] use pydantic-xml --- software/control/core/core.py | 246 ++++++++++++++++------------------ 1 file changed, 115 insertions(+), 131 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 2a2496cd..76767402 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -39,11 +39,12 @@ from threading import Thread, Lock from pathlib import Path from datetime import datetime +from enum import Enum +from pydantic_xml import BaseXmlModel, attr, element import time import subprocess import shutil import itertools -from lxml import etree import json import math import random @@ -3580,157 +3581,140 @@ def display_image(self, image, illumination_source): self.graphics_widget_4.img.setImage(image, autoLevels=False) -class ChannelConfigurationManager: - """Manages XML-based channel configurations.""" - def __init__(self): - self.active_channel_config = None - self.active_config_xml_tree = None - self.active_config_xml_tree_root = None - self.active_config_flag = -1 # 0: channel, 1: confocal, 2: widefield - self.current_profile_path = None +class ConfigType(Enum): + CHANNEL = "channel" + CONFOCAL = "confocal" + WIDEFIELD = "widefield" + + +class ChannelMode(BaseXmlModel, tag='mode'): + """Channel configuration model""" + mode_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 + + def __init__(self, **data): + super().__init__(**data) + self.color = utils.get_channel_color(self.name) + + +class ChannelConfig(BaseXmlModel, tag='configurations'): + """Root configuration file model""" + modes: List[ChannelMode] = element(tag='mode') - if ENABLE_SPINNING_DISK_CONFOCAL: - self.confocal_configurations = {} # Dict[str, List[Configuration]] - self.confocal_config_xml_tree = {} # Dict[str, etree.ElementTree] - self.confocal_config_xml_tree_root = {} # Dict[str, etree.ElementTree] - self.widefield_configurations = {} # Dict[str, List[Configuration]] - self.widefield_config_xml_tree = {} # Dict[str, etree.ElementTree] - self.widefield_config_xml_tree_root = {} # Dict[str, etree.ElementTree] - else: - self.channel_configurations = {} # Dict[str, List[Configuration]] - self.channel_config_xml_tree = {} # Dict[str, etree.ElementTree] - self.channel_config_xml_tree_root = {} # Dict[str, etree.ElementTree] + +class ChannelConfigurationManager: + def __init__(self, config_root: Path): + self.config_root = config_root + 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 set_profile_path(self, profile_path: Path) -> None: - self.current_profile_path = profile_path + """Set the root path for configurations""" + self.config_root = profile_path - def load_configurations(self, objective: str) -> None: - """Load channel configurations for a specific objective.""" - objective_path = self.current_profile_path / objective + 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" - # Load spinning disk configurations if enabled + if not config_file.exists(): + utils_config.generate_default_configuration(str(config_file)) + + self.all_configs[config_type][objective] = ChannelConfig.parse_file(config_file) + + def load_configurations(self, objective: str) -> None: + """Load available configurations for an objective""" if ENABLE_SPINNING_DISK_CONFOCAL: - confocal_config_file = objective_path / "confocal_configurations.xml" - self.confocal_configurations[objective], self.confocal_config_xml_tree[objective], self.confocal_config_xml_tree_root[objective] \ - = self._load_xml_config(confocal_config_file) + # 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) - widefield_config_file = objective_path / "widefield_configurations.xml" - self.widefield_configurations[objective], self.widefield_config_xml_tree[objective], self.widefield_config_xml_tree_root[objective] \ - = self._load_xml_config(widefield_config_file) + 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 - else: - channel_config_file = objective_path / "channel_configurations.xml" - self.channel_configurations[objective], self.channel_config_xml_tree[objective], self.channel_config_xml_tree_root[objective] \ - = self._load_xml_config(channel_config_file) - self.active_channel_config = self.channel_configurations - self.active_config_xml_tree = self.channel_config_xml_tree - self.active_config_xml_tree_root = self.channel_config_xml_tree_root - self.active_config_flag = 0 - - def _load_xml_config(self, config_file: Path) -> List[Configuration]: - """Parse XML and create Configuration objects.""" - if not config_file.is_file(): - utils_config.generate_default_configuration(str(config_file)) - print(f"Generated default config file for {config_file}") - - config_xml_tree = etree.parse(str(config_file)) - config_xml_tree_root = config_xml_tree.getroot() - configurations = [] - - for mode in config_xml_tree_root.iter("mode"): - configurations.append( - Configuration( - mode_id=mode.get("ID"), - name=mode.get("Name"), - color=utils.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")), - emission_filter_position=int(mode.get("EmissionFilterPosition", 1)), - ) - ) - return configurations, config_xml_tree, config_xml_tree_root + 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 save_configurations(self, objective: str) -> None: - """Save channel configurations for a specific objective.""" + """Save configurations based on spinning disk configuration""" if ENABLE_SPINNING_DISK_CONFOCAL: - # Store current state - current_tree = self.active_config_xml_tree - # If we're in confocal mode - if self.active_config_flag == 1: - self._save_xml_config(objective, self.current_profile_path / "confocal_configurations.xml") - self.active_config_xml_tree = self.widefield_configurations - self._save_xml_config(objective, self.current_profile_path / "widefield_configurations.xml") - # If we're in widefield mode - elif self.active_config_flag == 2: - self._save_xml_config(objective, self.current_profile_path / "widefield_configurations.xml") - self.active_config_xml_tree = self.confocal_configurations - self._save_xml_config(objective, self.current_profile_path / "confocal_configurations.xml") - # Restore original state - self.active_config_xml_tree = current_tree + # Save both confocal and widefield configurations + self._save_xml_config(objective, ConfigType.CONFOCAL) + self._save_xml_config(objective, ConfigType.WIDEFIELD) else: - self._save_xml_config(objective, self.current_profile_path / "channel_configurations.xml") + # Save only channel configurations + self._save_xml_config(objective, ConfigType.CHANNEL) - def get_configurations(self, objective: str) -> List[Configuration]: - """Get configurations for the current active mode.""" - return self.active_channel_config.get(objective, []) + 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 the current active mode.""" - if objective not in self.active_channel_config: + """Update a specific configuration in current active type""" + config = self.all_configs[self.active_config_type].get(objective) + if not config: return - conf_list = self.active_config_xml_tree_root[objective].xpath("//mode[contains(@ID," + "'" + str(config_id) + "')]") - mode_to_update = conf_list[0] - mode_to_update.set(attr_name, str(value)) - - if self.active_config_flag == 0: - config_file = self.current_profile_path / "channel_configurations.xml" - elif self.active_config_flag == 1: - config_file = self.current_profile_path / "confocal_configurations.xml" - elif self.active_config_flag == 2: - config_file = self.current_profile_path / "widefield_configurations.xml" - self._save_xml_config(objective, config_file) - - def _save_xml_config(self, objective: str, filename: Path) -> None: - if not filename.parent.exists(): - os.makedirs(filename.parent) - self.active_config_xml_tree[objective].write(filename, encoding="utf-8", xml_declaration=True, pretty_print=True) - - def write_configuration_selected(self, objective: str, selected_configurations: List[Configuration], filename: Path) -> None: - """Write selected configurations to a file.""" - if objective not in self.active_channel_config: + for mode in config.modes: + if mode.mode_id == config_id: + setattr(mode, attr_name.lower(), value) + break + + self.save_configurations(objective) + + def write_configuration_selected(self, objective: str, selected_configurations: List[ChannelMode], filename: Path) -> 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") - for conf in self.configurations: - self.update_configuration(conf.id, "Selected", 0) - for conf in selected_configurations: - self.update_configuration(conf.id, "Selected", 1) - self._save_xml_config(objective, filename) - for conf in selected_configurations: - self.update_configuration(conf.id, "Selected", 0) - - def get_channel_configurations_for_objective(self, objective: str) -> List[Configuration]: - return self.active_channel_config.get(objective, []) + # Update selected status + for mode in config.modes: + mode.selected = any(conf.mode_id == mode.mode_id for conf in selected_configurations) + + # Save to specified file + xml_str = config.to_xml(pretty_print=True, encoding='utf-8') + 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.""" - if not ENABLE_SPINNING_DISK: - return - - if confocal: - self.active_channel_config = self.confocal_configurations - self.active_config_xml_tree = self.confocal_config_xml_tree - self.active_config_xml_tree_root = self.confocal_config_xml_tree_root - self.active_config_flag = 1 - else: - self.active_channel_config = self.widefield_configurations - self.active_config_xml_tree = self.widefield_config_xml_tree - self.active_config_xml_tree_root = self.widefield_config_xml_tree_root - self.active_config_flag = 2 + """Toggle between confocal and widefield configurations""" + self.active_config_type = ConfigType.CONFOCAL if confocal else ConfigType.WIDEFIELD + class LaserAFCacheManager: """Manages JSON-based laser autofocus configurations.""" From e85068c31e329e300b7352bb314fe0352431c70e Mon Sep 17 00:00:00 2001 From: You Yan Date: Sun, 16 Feb 2025 09:08:44 -0800 Subject: [PATCH 16/24] update usages --- software/control/core/core.py | 11 +-- software/control/widgets.py | 162 ++++++++++++++-------------------- 2 files changed, 74 insertions(+), 99 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 76767402..62714051 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -13,6 +13,7 @@ from qtpy.QtCore import * from qtpy.QtWidgets import * from qtpy.QtGui import * +QMetaType.registerType(ChannelMode) # control from control._def import * @@ -2154,7 +2155,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) @@ -2623,7 +2624,7 @@ 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, @@ -2851,7 +2852,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) @@ -3812,9 +3813,9 @@ def create_new_profile(self, profile_name: str) -> None: self.current_profile = profile_name if self.channel_manager: - self.channel_manager.set_profile_path(profile_path) + self.channel_manager.set_profile_path(new_profile_path) if self.laser_af_manager: - self.laser_af_manager.set_profile_path(profile_path) + self.laser_af_manager.set_profile_path(new_profile_path) for objective in objectives: os.makedirs(new_profile_path / objective) diff --git a/software/control/widgets.py b/software/control/widgets.py index 52659604..49b5389d 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -3,7 +3,7 @@ from typing import Optional import squid.logging -from control.core.core import TrackingController, Configuration +from control.core.core import TrackingController from control.microcontroller import Microcontroller import control.utils as utils from squid.abc import AbstractStage @@ -958,8 +958,8 @@ def __init__( self, streamHandler, liveController, - objectiveStore=None, - configurationManager=None, + objectiveStore, + configurationManager, show_trigger_options=True, show_display_options=False, show_autolevel=False, @@ -974,18 +974,14 @@ def __init__( self.streamHandler = streamHandler self.objectiveStore = objectiveStore self.configurationManager = configurationManager - if self.configurationManager: - self.channelConfigurationManager = self.configurationManager.channel_manager + self.channelConfigurationManager = self.configurationManager.channel_manager 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 - if self.configurationManager and self.channelConfigurationManager: - self.currentConfiguration = self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)[0] - else: - self.currentConfiguration = Configuration() + 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.NoFrame) self.update_microscope_mode_by_name(self.currentConfiguration.name) @@ -995,9 +991,8 @@ def __init__( def add_components(self, show_trigger_options, show_display_options, show_autolevel, autolevel, stretch): # line 0: acquisition configuration profile management self.dropdown_profiles = QComboBox() - if self.configurationManager: - self.dropdown_profiles.addItems(self.configurationManager.available_profiles) - self.dropdown_profiles.setCurrentText(self.configurationManager.current_profile) + 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) @@ -1020,12 +1015,11 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_triggerFPS.setDecimals(0) # line 3: choose microscope mode / toggle live mode - if self.configurationManager and self.channelConfigurationManager: - self.dropdown_modeSelection = QComboBox() - 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) + self.dropdown_modeSelection = QComboBox() + 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) self.btn_live = QPushButton("Start Live") self.btn_live.setCheckable(True) @@ -1114,8 +1108,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_displayFPS.valueChanged.connect(self.streamHandler.set_display_fps) self.slider_resolutionScaling.valueChanged.connect(self.streamHandler.set_display_resolution_scaling) self.slider_resolutionScaling.valueChanged.connect(self.liveController.set_display_resolution_scaling) - if self.configurationManager and self.channelConfigurationManager: - self.dropdown_modeSelection.currentTextChanged.connect(self.update_microscope_mode_by_name) + self.dropdown_modeSelection.currentTextChanged.connect(self.update_microscope_mode_by_name) self.dropdown_triggerManu.currentIndexChanged.connect(self.update_trigger_mode) self.btn_live.clicked.connect(self.toggle_live) self.entry_exposureTime.valueChanged.connect(self.update_config_exposure_time) @@ -1138,8 +1131,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole grid_line1 = QHBoxLayout() grid_line1.addWidget(QLabel("Live Configuration")) - if self.configurationManager and self.channelConfigurationManager: - grid_line1.addWidget(self.dropdown_modeSelection, 2) + grid_line1.addWidget(self.dropdown_modeSelection, 2) grid_line1.addWidget(self.btn_live, 1) grid_line2 = QHBoxLayout() @@ -1185,8 +1177,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole # First section layout self.grid0 = QVBoxLayout() - if self.configurationManager: - self.grid0.addLayout(grid_line_profile) + self.grid0.addLayout(grid_line_profile) if not stretch: self.grid0.addStretch() self.profile_frame.setLayout(self.grid0) @@ -1213,38 +1204,36 @@ def add_components(self, show_trigger_options, show_display_options, show_autole def load_profile(self): """Load the selected profile.""" - if self.configurationManager: - profile_name = self.dropdown_profiles.currentText() - # Load the profile - self.configurationManager.load_profile(profile_name) - # Update the mode selection dropdown - 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) - # Update to first configuration - if self.dropdown_modeSelection.count() > 0: - self.update_microscope_mode_by_name(self.dropdown_modeSelection.currentText()) + profile_name = self.dropdown_profiles.currentText() + # Load the profile + self.configurationManager.load_profile(profile_name) + # Update the mode selection dropdown + 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) + # Update to first configuration + if self.dropdown_modeSelection.count() > 0: + self.update_microscope_mode_by_name(self.dropdown_modeSelection.currentText()) def create_new_profile(self): """Create a new profile with current configurations.""" - if self.configurationManager: - dialog = QInputDialog() - profile_name, ok = dialog.getText( - self, - "New Profile", - "Enter new profile name:", - QLineEdit.Normal, - "" - ) + 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)) + 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 toggle_live(self, pressed): if pressed: @@ -1265,31 +1254,22 @@ def update_camera_settings(self): 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.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)) - if self.configurationManager and self.channelConfigurationManager: - self.currentConfiguration = next( - ( - config - for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) - if config.name == current_microscope_mode_name - ), - None, - ) - self.signal_live_configuration.emit(self.currentConfiguration) - # update the microscope to the current configuration - self.liveController.set_microscope_mode(self.currentConfiguration) - # update the exposure time and analog gain settings according to the selected configuration - self.entry_exposureTime.setValue(self.currentConfiguration.exposure_time) - self.entry_analogGain.setValue(self.currentConfiguration.analog_gain) - self.entry_illuminationIntensity.setValue(self.currentConfiguration.illumination_intensity) - self.is_switching_mode = False - - else: - # laser autofocus live control - self.currentConfiguration = Configuration(exposure_time=100, analog_gain=0.0) - # update the exposure time and analog gain settings according to the selected configuration - self.entry_exposureTime.setValue(self.currentConfiguration.exposure_time) - self.entry_analogGain.setValue(self.currentConfiguration.analog_gain) - self.is_switching_mode = False + self.currentConfiguration = next( + ( + config + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == current_microscope_mode_name + ), + None, + ) + self.signal_live_configuration.emit(self.currentConfiguration) + # update the microscope to the current configuration + self.liveController.set_microscope_mode(self.currentConfiguration) + # update the exposure time and analog gain settings according to the selected configuration + self.entry_exposureTime.setValue(self.currentConfiguration.exposure_time) + self.entry_analogGain.setValue(self.currentConfiguration.analog_gain) + self.entry_illuminationIntensity.setValue(self.currentConfiguration.illumination_intensity) + self.is_switching_mode = False def update_trigger_mode(self): self.liveController.set_trigger_mode(self.dropdown_triggerManu.currentText()) @@ -4887,25 +4867,19 @@ def set_microscope_mode(self, config): self.dropdown_modeSelection.setCurrentText(config.name) def update_microscope_mode_by_name(self, current_microscope_mode_name): - if self.configurationManager and self.channelConfigurationManager: - self.live_configuration = next( - ( - config - for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) - if config.name == current_microscope_mode_name - ), - None, - ) - if self.live_configuration: - self.liveController.set_microscope_mode(self.live_configuration) - self.entry_exposureTime.setValue(self.live_configuration.exposure_time) - self.entry_analogGain.setValue(self.live_configuration.analog_gain) - self.slider_illuminationIntensity.setValue(int(self.live_configuration.illumination_intensity)) - else: - # laser autofocus live control - self.live_configuration = Configuration(exposure_time=100, analog_gain=0.0) + self.live_configuration = next( + ( + config + for config in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective) + if config.name == current_microscope_mode_name + ), + None, + ) + if self.live_configuration: + self.liveController.set_microscope_mode(self.live_configuration) self.entry_exposureTime.setValue(self.live_configuration.exposure_time) self.entry_analogGain.setValue(self.live_configuration.analog_gain) + self.slider_illuminationIntensity.setValue(int(self.live_configuration.illumination_intensity)) def update_config_exposure_time(self, new_value): self.live_configuration.exposure_time = new_value From 513f84d1210797ae62ea854dfd0dd0ff949d7030 Mon Sep 17 00:00:00 2001 From: You Yan Date: Tue, 18 Feb 2025 00:18:59 -0800 Subject: [PATCH 17/24] addressed comments --- software/control/core/core.py | 98 +++++++++++++++-------------------- software/control/gui_hcs.py | 2 +- software/control/widgets.py | 2 +- 3 files changed, 44 insertions(+), 58 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 62714051..6e229f02 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -13,7 +13,6 @@ from qtpy.QtCore import * from qtpy.QtWidgets import * from qtpy.QtGui import * -QMetaType.registerType(ChannelMode) # control from control._def import * @@ -57,6 +56,33 @@ import squid.abc +class ChannelMode(BaseXmlModel, tag='mode'): + """Channel configuration model""" + mode_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 + + def __init__(self, **data): + super().__init__(**data) + self.color = utils.get_channel_color(self.name) + + +QMetaType.registerType(ChannelMode) + + +class ChannelConfig(BaseXmlModel, tag='configurations'): + """Root configuration file model""" + modes: List[ChannelMode] = element(tag='mode') + + class ObjectiveStore: def __init__(self, objectives_dict=OBJECTIVES, default_objective=DEFAULT_OBJECTIVE, parent=None): self.objectives_dict = objectives_dict @@ -445,32 +471,6 @@ 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, - 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.emission_filter_position = emission_filter_position - - class LiveController(QObject): def __init__( self, @@ -1353,7 +1353,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) @@ -2748,7 +2748,7 @@ 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.channelConfigurationManager._save_xml_config( + 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 @@ -3588,32 +3588,9 @@ class ConfigType(Enum): WIDEFIELD = "widefield" -class ChannelMode(BaseXmlModel, tag='mode'): - """Channel configuration model""" - mode_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 - - def __init__(self, **data): - super().__init__(**data) - self.color = utils.get_channel_color(self.name) - - -class ChannelConfig(BaseXmlModel, tag='configurations'): - """Root configuration file model""" - modes: List[ChannelMode] = element(tag='mode') - - class ChannelConfigurationManager: def __init__(self, config_root: Path): + self._log = squid.logging.get_logger(self.__class__.__name__) self.config_root = config_root self.all_configs: Dict[ConfigType, Dict[str, ChannelConfig]] = { ConfigType.CHANNEL: {}, @@ -3633,7 +3610,8 @@ def _load_xml_config(self, objective: str, config_type: ConfigType) -> None: if not config_file.exists(): utils_config.generate_default_configuration(str(config_file)) - self.all_configs[config_type][objective] = ChannelConfig.parse_file(config_file) + xml_content = config_file.read_text() + self.all_configs[config_type][objective] = ChannelConfig.model_validate_xml(xml_content) def load_configurations(self, objective: str) -> None: """Load available configurations for an objective""" @@ -3669,6 +3647,12 @@ def save_configurations(self, objective: str) -> None: # 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) @@ -3680,6 +3664,7 @@ def update_configuration(self, objective: str, config_id: str, attr_name: str, v """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: @@ -4613,6 +4598,7 @@ def __init__( laserAFCacheManager: Optional[LaserAFCacheManager] = None ): QObject.__init__(self) + self._log = squid.logging.get_logger(self.__class__.__name__) self.microcontroller = microcontroller self.camera = camera self.stage = stage @@ -4647,7 +4633,7 @@ def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_re self.y_offset = int((y_offset // 2) * 2) self.width = int((width // 8) * 8) self.height = int((height // 2) * 2) - self.x_reference = x_reference - self.x_offset + self.x_reference = x_reference - self.x_offset # self.x_reference is relative to the cropped region self.has_two_interfaces = has_two_interfaces self.use_glass_top = use_glass_top @@ -4718,7 +4704,7 @@ def initialize_auto(self): x_offset = x - LASER_AF_CROP_WIDTH / 2 y_offset = y - LASER_AF_CROP_HEIGHT / 2 - print(f"laser spot location on the full sensor is ({int(x)},{int(y)})") + self._log.error(f"laser spot location on the full sensor is ({int(x)},{int(y)})") # set camera crop self.initialize_manual(x_offset, y_offset, LASER_AF_CROP_WIDTH, LASER_AF_CROP_HEIGHT, 1, x) diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 70424d70..40bd30d5 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -215,9 +215,9 @@ def loadObjects(self, is_simulation): self.channelConfigurationManager = core.ChannelConfigurationManager() if SUPPORT_LASER_AUTOFOCUS: self.laserAFCacheManager = core.LaserAFCacheManager() - self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, laser_af_manager=self.laserAFCacheManager) else: self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager) + self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, laser_af_manager=self.laserAFCacheManager) self.contrastManager = core.ContrastManager() self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP / 100) self.liveController = core.LiveController( diff --git a/software/control/widgets.py b/software/control/widgets.py index 49b5389d..2f0a26e5 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -462,7 +462,7 @@ def __init__(self, xlight, config_manager=None): self.disk_position_state = self.xlight.get_disk_position() - if self.config_manager is not None: + if self.config_manager: self.config_manager.toggle_confocal_widefield(self.disk_position_state) if self.disk_position_state == 1: From 15bbc0ee15d0b88e3f09ab7fe8f1ca9a4f2198e8 Mon Sep 17 00:00:00 2001 From: You Yan Date: Thu, 20 Feb 2025 15:48:18 -0800 Subject: [PATCH 18/24] bug fix; create default profile with pydantic-xml --- software/control/core/core.py | 66 ++-- software/control/gui_hcs.py | 6 +- software/control/utils_config.py | 503 +++++++++++++++---------------- 3 files changed, 275 insertions(+), 300 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 6e229f02..f4801186 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -40,7 +40,7 @@ from pathlib import Path from datetime import datetime from enum import Enum -from pydantic_xml import BaseXmlModel, attr, element +from control.utils_config import ChannelConfig, ChannelMode import time import subprocess import shutil @@ -56,33 +56,6 @@ import squid.abc -class ChannelMode(BaseXmlModel, tag='mode'): - """Channel configuration model""" - mode_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 - - def __init__(self, **data): - super().__init__(**data) - self.color = utils.get_channel_color(self.name) - - -QMetaType.registerType(ChannelMode) - - -class ChannelConfig(BaseXmlModel, tag='configurations'): - """Root configuration file model""" - modes: List[ChannelMode] = element(tag='mode') - - class ObjectiveStore: def __init__(self, objectives_dict=OBJECTIVES, default_objective=DEFAULT_OBJECTIVE, parent=None): self.objectives_dict = objectives_dict @@ -2188,7 +2161,7 @@ def __init__( self.microcontroller = microcontroller self.liveController = liveController self.autofocusController = autofocusController - self.objectiveStore = objectiveStore, + self.objectiveStore = objectiveStore self.channelConfigurationManager = channelConfigurationManager self.multiPointWorker: Optional[MultiPointWorker] = None self.thread: Optional[QThread] = None @@ -3589,9 +3562,9 @@ class ConfigType(Enum): class ChannelConfigurationManager: - def __init__(self, config_root: Path): + def __init__(self): self._log = squid.logging.get_logger(self.__class__.__name__) - self.config_root = config_root + self.config_root = None self.all_configs: Dict[ConfigType, Dict[str, ChannelConfig]] = { ConfigType.CHANNEL: {}, ConfigType.CONFOCAL: {}, @@ -3610,8 +3583,8 @@ def _load_xml_config(self, objective: str, config_type: ConfigType) -> None: if not config_file.exists(): utils_config.generate_default_configuration(str(config_file)) - xml_content = config_file.read_text() - self.all_configs[config_type][objective] = ChannelConfig.model_validate_xml(xml_content) + 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""" @@ -3668,13 +3641,13 @@ def update_configuration(self, objective: str, config_id: str, attr_name: str, v return for mode in config.modes: - if mode.mode_id == config_id: - setattr(mode, attr_name.lower(), value) + 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: Path) -> None: + 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: @@ -3682,12 +3655,13 @@ def write_configuration_selected(self, objective: str, selected_configurations: # Update selected status for mode in config.modes: - mode.selected = any(conf.mode_id == mode.mode_id for conf in selected_configurations) - + 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 @@ -3723,9 +3697,10 @@ def save_configurations(self, objective: str) -> None: if objective not in self.autofocus_configurations: return - if not self.current_profile_path / objective.exists(): - os.makedirs(self.current_profile_path / objective) - config_file = self.current_profile_path / objective / "laser_af_cache.json" + objective_path = self.current_profile_path / objective + if not objective_path.exists(): + objective_path.mkdir(parents=True) + config_file = objective_path / "laser_af_cache.json" with open(config_file, 'w') as f: json.dump(self.autofocus_configurations[objective], f, indent=4) @@ -3794,7 +3769,7 @@ def create_new_profile(self, profile_name: str) -> None: raise ValueError(f"Profile {profile_name} already exists") os.makedirs(new_profile_path) - objectives = self.channel_manager.objective_configurations.keys() + objectives = OBJECTIVES self.current_profile = profile_name if self.channel_manager: @@ -4602,6 +4577,7 @@ def __init__( self.microcontroller = microcontroller self.camera = camera self.stage = stage + self.liveController = liveController self.objectiveStore = objectiveStore self.laserAFCacheManager = laserAFCacheManager @@ -4814,7 +4790,7 @@ def set_reference(self): def on_objective_changed(self): self.is_initialized = False - self.load_cached_configurations() + self.load_cached_configuration() def _calculate_centroid(self, image): """Calculate the centroid of the laser spot.""" @@ -4893,7 +4869,7 @@ def _get_laser_spot_centroid(self): if LASER_AF_DISPLAY_SPOT_IMAGE: self.image_to_display.emit(image) # calculate centroid - x, y = self._caculate_centroid(image) + x, y = self._calculate_centroid(image) tmp_x = tmp_x + x tmp_y = tmp_y + y x = tmp_x / LASER_AF_AVERAGING_N diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 40bd30d5..7a669d9d 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -948,8 +948,12 @@ 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: - def connect_objective_changed_laser_af(self): + def connect_objective_changed_laser_af(): self.laserAutofocusController.on_objective_changed() self.laserAutofocusControlWidget.update_init_state() diff --git a/software/control/utils_config.py b/software/control/utils_config.py index 4eb2b8a1..59438e01 100644 --- a/software/control/utils_config.py +++ b/software/control/utils_config.py @@ -1,254 +1,249 @@ -from lxml import etree as ET - -top = ET.Element("modes") - - -def generate_default_configuration(filename): - - 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") - - 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") - - 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") - - 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") - - 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) +from pydantic_xml import BaseXmlModel, element, attr +from typing import List, Optional +from pathlib import Path +import control.utils as utils + + +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 + + def __init__(self, **data): + super().__init__(**data) + self.color = utils.get_channel_color(self.name) + +class ChannelConfig(BaseXmlModel, tag='modes'): + """Root configuration file model""" + modes: List[ChannelMode] = element(tag='mode') + +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] + +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 + ) + ] + + 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 From 7e51a52a8e5899d8e44a90bd06ac6ceff8040923 Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 22 Feb 2025 17:55:48 -0800 Subject: [PATCH 19/24] use pydantic for laser af cache --- software/control/core/core.py | 47 ++++++++++++++++++-------------- software/control/gui_hcs.py | 2 +- software/control/utils_config.py | 14 ++++++++++ 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index f4801186..87970d27 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -40,7 +40,7 @@ from pathlib import Path from datetime import datetime from enum import Enum -from control.utils_config import ChannelConfig, ChannelMode +from control.utils_config import ChannelConfig, ChannelMode, LaserAFConfig import time import subprocess import shutil @@ -3679,7 +3679,7 @@ def toggle_confocal_widefield(self, confocal: bool) -> None: class LaserAFCacheManager: """Manages JSON-based laser autofocus configurations.""" def __init__(self): - self.autofocus_configurations = {} # Dict[str, Dict[str, Any]] + 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: @@ -3690,7 +3690,8 @@ def load_configurations(self, objective: str) -> None: config_file = self.current_profile_path / objective / "laser_af_cache.json" if config_file.exists(): with open(config_file, 'r') as f: - self.autofocus_configurations[objective] = json.load(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.""" @@ -3701,19 +3702,25 @@ def save_configurations(self, objective: str) -> None: if not objective_path.exists(): objective_path.mkdir(parents=True) config_file = objective_path / "laser_af_cache.json" + + config_dict = self.autofocus_configurations[objective].model_dump() with open(config_file, 'w') as f: - json.dump(self.autofocus_configurations[objective], f, indent=4) + json.dump(config_dict, f, indent=4) def get_cache_for_objective(self, objective: str) -> Dict[str, Any]: - return self.autofocus_configurations.get(objective, {}) - + 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_cache(self) -> Dict[str, Any]: return self.autofocus_configurations def update_laser_af_cache(self, objective: str, updates: Dict[str, Any]) -> None: if objective not in self.autofocus_configurations: - self.autofocus_configurations[objective] = {} - self.autofocus_configurations[objective].update(updates) + self.autofocus_configurations[objective] = LaserAFConfig(**updates) + else: + config = self.autofocus_configurations[objective] + self.autofocus_configurations[objective] = config.model_copy(update=updates) class ConfigurationManager(QObject): """Main configuration manager that coordinates channel and autofocus configurations.""" @@ -4638,22 +4645,22 @@ def load_cached_configuration(self): if current_objective and current_objective in self.laser_af_cache: config = self.laserAFCacheManager.get_cache_for_objective(current_objective) - self.focus_camera_exposure_time_ms = config.get('focus_camera_exposure_time_ms', 2), - self.focus_camera_analog_gain = config.get('focus_camera_analog_gain', 0) + self.focus_camera_exposure_time_ms = config.focus_camera_exposure_time_ms + self.focus_camera_analog_gain = config.focus_camera_analog_gain self.camera.set_exposure_time(self.focus_camera_exposure_time_ms) self.camera.set_analog_gain(self.focus_camera_analog_gain) self.initialize_manual( - x_offset=config.get('x_offset', 0), - y_offset=config.get('y_offset', 0), - width=config.get('width', LASER_AF_CROP_WIDTH), - height=config.get('height', LASER_AF_CROP_HEIGHT), - pixel_to_um=config.get('pixel_to_um', 1.0), - x_reference=config.get('x_reference', 0), - has_two_interfaces=config.get('has_two_interfaces', False), - use_glass_top=config.get('use_glass_top', True), - focus_camera_exposure_time_ms=config.get('focus_camera_exposure_time_ms', 2), - focus_camera_analog_gain=config.get('focus_camera_analog_gain', 0), + x_offset=config.x_offset, + y_offset=config.y_offset, + width=config.width, + height=config.height, + pixel_to_um=config.pixel_to_um, + x_reference=config.x_reference, + has_two_interfaces=config.has_two_interfaces, + use_glass_top=config.use_glass_top, + focus_camera_exposure_time_ms=config.focus_camera_exposure_time_ms, + focus_camera_analog_gain=config.focus_camera_analog_gain, ) def initialize_auto(self): diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 7a669d9d..5aeec915 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -216,7 +216,7 @@ def loadObjects(self, is_simulation): if SUPPORT_LASER_AUTOFOCUS: self.laserAFCacheManager = core.LaserAFCacheManager() else: - self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager) + self.laserAFCacheManager = None self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, laser_af_manager=self.laserAFCacheManager) self.contrastManager = core.ContrastManager() self.streamHandler = core.StreamHandler(display_resolution_scaling=DEFAULT_DISPLAY_CROP / 100) diff --git a/software/control/utils_config.py b/software/control/utils_config.py index 59438e01..5cb0f161 100644 --- a/software/control/utils_config.py +++ b/software/control/utils_config.py @@ -1,9 +1,23 @@ +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 +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 + class ChannelMode(BaseXmlModel, tag='mode'): """Channel configuration model""" id: str = attr(name='ID') From d5fe609b1481d2328fc11c4c704f2802a438982e Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 22 Feb 2025 20:00:33 -0800 Subject: [PATCH 20/24] ProfileWidget --- software/control/gui_hcs.py | 6 +- software/control/widgets.py | 176 +++++++++++++++++++----------------- 2 files changed, 96 insertions(+), 86 deletions(-) diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 5aeec915..766400b6 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -541,11 +541,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.objectiveStore, - self.configurationManager, + self.channelConfigurationManager, show_display_options=True, show_autolevel=True, autolevel=True, @@ -778,6 +779,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) @@ -881,6 +883,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: diff --git a/software/control/widgets.py b/software/control/widgets.py index 2f0a26e5..b2bc960c 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -946,6 +946,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) @@ -959,7 +1031,7 @@ def __init__( streamHandler, liveController, objectiveStore, - configurationManager, + channelConfigurationManager, show_trigger_options=True, show_display_options=False, show_autolevel=False, @@ -973,8 +1045,7 @@ def __init__( self.liveController = liveController self.streamHandler = streamHandler self.objectiveStore = objectiveStore - self.configurationManager = configurationManager - self.channelConfigurationManager = self.configurationManager.channel_manager + self.channelConfigurationManager = channelConfigurationManager self.fps_trigger = 10 self.fps_display = 10 self.liveController.set_trigger_fps(self.fps_trigger) @@ -982,31 +1053,22 @@ def __init__( self.triggerMode = TriggerMode.SOFTWARE 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.NoFrame) + self.setFrameStyle(QFrame.Panel | QFrame.Raised) self.update_microscope_mode_by_name(self.currentConfiguration.name) self.is_switching_mode = False # flag used to prevent from settings being set by twice - from both mode change slot and value change slot; another way is to use blockSignals(True) def add_components(self, show_trigger_options, show_display_options, show_autolevel, autolevel, stretch): - # line 0: acquisition configuration profile management - 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") - - # line 1: trigger mode + # line 0: trigger mode self.triggerMode = None self.dropdown_triggerManu = QComboBox() self.dropdown_triggerManu.addItems([TriggerMode.SOFTWARE, TriggerMode.HARDWARE, TriggerMode.CONTINUOUS]) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.dropdown_triggerManu.setSizePolicy(sizePolicy) - # line 2: fps + # line 1: fps self.entry_triggerFPS = QDoubleSpinBox() self.entry_triggerFPS.setMinimum(0.02) self.entry_triggerFPS.setMaximum(1000) @@ -1014,7 +1076,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_triggerFPS.setValue(self.fps_trigger) self.entry_triggerFPS.setDecimals(0) - # line 3: choose microscope mode / toggle live mode + # line 2: choose microscope mode / toggle live mode self.dropdown_modeSelection = QComboBox() for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): self.dropdown_modeSelection.addItems([microscope_configuration.name]) @@ -1028,7 +1090,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.btn_live.setStyleSheet("background-color: #C2C2FF") self.btn_live.setSizePolicy(sizePolicy) - # line 4: exposure time and analog gain associated with the current mode + # line 3: exposure time and analog gain associated with the current mode self.entry_exposureTime = QDoubleSpinBox() self.entry_exposureTime.setMinimum(self.liveController.camera.EXPOSURE_TIME_MS_MIN) self.entry_exposureTime.setMaximum(self.liveController.camera.EXPOSURE_TIME_MS_MAX) @@ -1060,7 +1122,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.entry_illuminationIntensity.setSuffix("%") self.entry_illuminationIntensity.setValue(100) - # line 5: display fps and resolution scaling + # line 4: display fps and resolution scaling self.entry_displayFPS = QDoubleSpinBox() self.entry_displayFPS.setMinimum(1) self.entry_displayFPS.setMaximum(240) @@ -1101,9 +1163,6 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.btn_autolevel.setFixedWidth(max_width) # connections - self.btn_loadProfile.clicked.connect(self.load_profile) - self.btn_newProfile.clicked.connect(self.create_new_profile) - self.entry_triggerFPS.valueChanged.connect(self.liveController.set_trigger_fps) self.entry_displayFPS.valueChanged.connect(self.streamHandler.set_display_fps) self.slider_resolutionScaling.valueChanged.connect(self.streamHandler.set_display_resolution_scaling) @@ -1121,14 +1180,6 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.btn_autolevel.toggled.connect(self.signal_autoLevelSetting.emit) # layout - # Profile management layout - grid_line_profile = QHBoxLayout() - grid_line_profile.addWidget(QLabel("Configuration Profile")) - grid_line_profile.addWidget(self.dropdown_profiles, 2) - grid_line_profile.addWidget(self.btn_loadProfile) - - grid_line_profile.addWidget(self.btn_newProfile) - grid_line1 = QHBoxLayout() grid_line1.addWidget(QLabel("Live Configuration")) grid_line1.addWidget(self.dropdown_modeSelection, 2) @@ -1169,20 +1220,6 @@ def add_components(self, show_trigger_options, show_display_options, show_autole else: grid_line05.addWidget(self.label_resolutionScaling) - # Create separate frames for each section - self.profile_frame = QFrame() - self.profile_frame.setFrameStyle(QFrame.Panel | QFrame.Raised) - self.control_frame = QFrame() - self.control_frame.setFrameStyle(QFrame.Panel | QFrame.Raised) - - # First section layout - self.grid0 = QVBoxLayout() - self.grid0.addLayout(grid_line_profile) - if not stretch: - self.grid0.addStretch() - self.profile_frame.setLayout(self.grid0) - - # Second section layout self.grid = QVBoxLayout() if show_trigger_options: self.grid.addLayout(grid_line0) @@ -1193,47 +1230,7 @@ def add_components(self, show_trigger_options, show_display_options, show_autole self.grid.addLayout(grid_line05) if not stretch: self.grid.addStretch() - self.control_frame.setLayout(self.grid) - - # Main layout to hold both frames - main_layout = QVBoxLayout() - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(self.profile_frame) - main_layout.addWidget(self.control_frame) - self.setLayout(main_layout) - - def load_profile(self): - """Load the selected profile.""" - profile_name = self.dropdown_profiles.currentText() - # Load the profile - self.configurationManager.load_profile(profile_name) - # Update the mode selection dropdown - 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) - # Update to first configuration - if self.dropdown_modeSelection.count() > 0: - self.update_microscope_mode_by_name(self.dropdown_modeSelection.currentText()) - - 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)) + self.setLayout(self.grid) def toggle_live(self, pressed): if pressed: @@ -1251,6 +1248,15 @@ 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.clear() + for microscope_configuration in self.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective): + self.dropdown_modeSelection.addItem(microscope_configuration.name) + # 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.channelConfigurationManager.get_channel_configurations_for_objective(self.objectiveStore.current_objective)) From df06321d2c65993306081fb260ab6166122f1abf Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 22 Feb 2025 20:46:12 -0800 Subject: [PATCH 21/24] bug fix: block signals when adding items to channel mode dropdown --- software/control/core/core.py | 2 +- software/control/widgets.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 87970d27..ed04dbda 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3722,7 +3722,7 @@ def update_laser_af_cache(self, objective: str, updates: Dict[str, Any]) -> None config = self.autofocus_configurations[objective] self.autofocus_configurations[objective] = config.model_copy(update=updates) -class ConfigurationManager(QObject): +class ConfigurationManager: """Main configuration manager that coordinates channel and autofocus configurations.""" def __init__(self, channel_manager: ChannelConfigurationManager, diff --git a/software/control/widgets.py b/software/control/widgets.py index b2bc960c..aa8b6224 100644 --- a/software/control/widgets.py +++ b/software/control/widgets.py @@ -1250,9 +1250,12 @@ def update_camera_settings(self): 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()) From 413fd590decdae5dbacf1e94572b02f2df502ffc Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 22 Feb 2025 21:14:08 -0800 Subject: [PATCH 22/24] updated naming --- software/control/core/core.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index ed04dbda..9b9c71c8 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3687,7 +3687,7 @@ def set_profile_path(self, profile_path: Path) -> None: def load_configurations(self, objective: str) -> None: """Load autofocus configurations for a specific objective.""" - config_file = self.current_profile_path / objective / "laser_af_cache.json" + 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) @@ -3701,21 +3701,21 @@ def save_configurations(self, objective: str) -> None: objective_path = self.current_profile_path / objective if not objective_path.exists(): objective_path.mkdir(parents=True) - config_file = objective_path / "laser_af_cache.json" + 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_cache_for_objective(self, objective: str) -> Dict[str, Any]: + 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_cache(self) -> Dict[str, Any]: + def get_laser_af_settings(self) -> Dict[str, Any]: return self.autofocus_configurations - - def update_laser_af_cache(self, objective: str, updates: Dict[str, Any]) -> None: + + 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: @@ -4605,7 +4605,7 @@ def __init__( # Load configurations if provided if self.laserAFCacheManager: - self.laser_af_cache = self.laserAFCacheManager.get_laser_af_cache() + self.laser_af_settings = self.laserAFCacheManager.get_laser_af_settings() self.load_cached_configuration() def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_reference, @@ -4624,9 +4624,9 @@ def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_re self.is_initialized = True - # Update cache if objective store and laser_af_cache is available + # Update cache if objective store and laser_af_settings is available if self.objectiveStore and self.laserAFCacheManager and self.objectiveStore.current_objective: - self.laserAFCacheManager.update_laser_af_cache(self.objectiveStore.current_objective, { + self.laserAFCacheManager.update_laser_af_settings(self.objectiveStore.current_objective, { 'x_offset': x_offset, 'y_offset': y_offset, 'width': width, @@ -4642,8 +4642,8 @@ def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_re def load_cached_configuration(self): """Load configuration from the cache if available.""" current_objective = self.objectiveStore.current_objective if self.objectiveStore else None - if current_objective and current_objective in self.laser_af_cache: - config = self.laserAFCacheManager.get_cache_for_objective(current_objective) + if current_objective and current_objective in self.laser_af_settings: + config = self.laserAFCacheManager.get_settings_for_objective(current_objective) self.focus_camera_exposure_time_ms = config.focus_camera_exposure_time_ms self.focus_camera_analog_gain = config.focus_camera_analog_gain @@ -4733,7 +4733,7 @@ def _calibrate_pixel_to_um(self): self.x_reference = x1 # Update cache - self.laserAFCacheManager.update_laser_af_cache(self.objectiveStore.current_objective, { + self.laserAFCacheManager.update_laser_af_settings(self.objectiveStore.current_objective, { 'pixel_to_um': self.pixel_to_um }) @@ -4790,7 +4790,7 @@ def set_reference(self): self.signal_displacement_um.emit(0) # Update cache - self.laserAFCacheManager.update_laser_af_cache(self.objectiveStore.current_objective, { + self.laserAFCacheManager.update_laser_af_settings(self.objectiveStore.current_objective, { 'x_reference': x + self.x_offset }) self.laserAFCacheManager.save_configurations(self.objectiveStore.current_objective) From b16ee0c354d1175a85f8fa515c63f686c7debe26 Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 22 Feb 2025 21:31:15 -0800 Subject: [PATCH 23/24] updated naming --- software/control/core/core.py | 26 +++++++++++++------------- software/control/gui_hcs.py | 8 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/software/control/core/core.py b/software/control/core/core.py index 9b9c71c8..54535cd4 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3676,7 +3676,7 @@ def toggle_confocal_widefield(self, confocal: bool) -> None: self.active_config_type = ConfigType.CONFOCAL if confocal else ConfigType.WIDEFIELD -class LaserAFCacheManager: +class LaserAFSettingManager: """Manages JSON-based laser autofocus configurations.""" def __init__(self): self.autofocus_configurations: Dict[str, LaserAFConfig] = {} # Dict[str, Dict[str, Any]] @@ -3726,7 +3726,7 @@ class ConfigurationManager: """Main configuration manager that coordinates channel and autofocus configurations.""" def __init__(self, channel_manager: ChannelConfigurationManager, - laser_af_manager: Optional[LaserAFCacheManager] = None, + laser_af_manager: Optional[LaserAFSettingManager] = None, base_config_path: Path = Path("acquisition_configurations"), profile: str = "default_profile"): super().__init__() @@ -4577,7 +4577,7 @@ def __init__( liveController, stage: AbstractStage, objectiveStore: Optional[ObjectiveStore] = None, - laserAFCacheManager: Optional[LaserAFCacheManager] = None + laserAFSettingManager: Optional[LaserAFSettingManager] = None ): QObject.__init__(self) self._log = squid.logging.get_logger(self.__class__.__name__) @@ -4586,7 +4586,7 @@ def __init__( self.stage = stage self.liveController = liveController self.objectiveStore = objectiveStore - self.laserAFCacheManager = laserAFCacheManager + self.laserAFSettingManager = laserAFSettingManager self.is_initialized = False self.x_reference = 0 @@ -4604,8 +4604,8 @@ def __init__( self.image = None # for saving the focus camera image for debugging when centroid cannot be found # Load configurations if provided - if self.laserAFCacheManager: - self.laser_af_settings = self.laserAFCacheManager.get_laser_af_settings() + if self.laserAFSettingManager: + self.laser_af_settings = self.laserAFSettingManager.get_laser_af_settings() self.load_cached_configuration() def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_reference, @@ -4625,8 +4625,8 @@ def initialize_manual(self, x_offset, y_offset, width, height, pixel_to_um, x_re self.is_initialized = True # Update cache if objective store and laser_af_settings is available - if self.objectiveStore and self.laserAFCacheManager and self.objectiveStore.current_objective: - self.laserAFCacheManager.update_laser_af_settings(self.objectiveStore.current_objective, { + if self.objectiveStore and self.laserAFSettingManager and self.objectiveStore.current_objective: + self.laserAFSettingManager.update_laser_af_settings(self.objectiveStore.current_objective, { 'x_offset': x_offset, 'y_offset': y_offset, 'width': width, @@ -4643,7 +4643,7 @@ def load_cached_configuration(self): """Load configuration from the cache if available.""" current_objective = self.objectiveStore.current_objective if self.objectiveStore else None if current_objective and current_objective in self.laser_af_settings: - config = self.laserAFCacheManager.get_settings_for_objective(current_objective) + config = self.laserAFSettingManager.get_settings_for_objective(current_objective) self.focus_camera_exposure_time_ms = config.focus_camera_exposure_time_ms self.focus_camera_analog_gain = config.focus_camera_analog_gain @@ -4695,7 +4695,7 @@ def initialize_auto(self): # Calibrate pixel to um conversion self._calibrate_pixel_to_um() - self.laserAFCacheManager.save_configurations(self.objectiveStore.current_objective) + self.laserAFSettingManager.save_configurations(self.objectiveStore.current_objective) def _calibrate_pixel_to_um(self): """Calibrate the pixel to micrometer conversion factor.""" @@ -4733,7 +4733,7 @@ def _calibrate_pixel_to_um(self): self.x_reference = x1 # Update cache - self.laserAFCacheManager.update_laser_af_settings(self.objectiveStore.current_objective, { + self.laserAFSettingManager.update_laser_af_settings(self.objectiveStore.current_objective, { 'pixel_to_um': self.pixel_to_um }) @@ -4790,10 +4790,10 @@ def set_reference(self): self.signal_displacement_um.emit(0) # Update cache - self.laserAFCacheManager.update_laser_af_settings(self.objectiveStore.current_objective, { + self.laserAFSettingManager.update_laser_af_settings(self.objectiveStore.current_objective, { 'x_reference': x + self.x_offset }) - self.laserAFCacheManager.save_configurations(self.objectiveStore.current_objective) + self.laserAFSettingManager.save_configurations(self.objectiveStore.current_objective) def on_objective_changed(self): self.is_initialized = False diff --git a/software/control/gui_hcs.py b/software/control/gui_hcs.py index 766400b6..b468ec0f 100644 --- a/software/control/gui_hcs.py +++ b/software/control/gui_hcs.py @@ -214,10 +214,10 @@ def loadObjects(self, is_simulation): self.objectiveStore = core.ObjectiveStore(parent=self) self.channelConfigurationManager = core.ChannelConfigurationManager() if SUPPORT_LASER_AUTOFOCUS: - self.laserAFCacheManager = core.LaserAFCacheManager() + self.laserAFSettingManager = core.LaserAFSettingManager() else: - self.laserAFCacheManager = None - self.configurationManager = core.ConfigurationManager(channel_manager=self.channelConfigurationManager, laser_af_manager=self.laserAFCacheManager) + 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( @@ -306,7 +306,7 @@ def loadObjects(self, is_simulation): self.liveController_focus_camera, self.stage, self.objectiveStore, - self.laserAFCacheManager + self.laserAFSettingManager ) if USE_SQUID_FILTERWHEEL: From c146bae7ddf374dd565fbd6ad63aebc2bb2f35a1 Mon Sep 17 00:00:00 2001 From: You Yan Date: Sat, 22 Feb 2025 21:35:25 -0800 Subject: [PATCH 24/24] documentation --- software/control/core/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/software/control/core/core.py b/software/control/core/core.py index 54535cd4..01195e7b 100644 --- a/software/control/core/core.py +++ b/software/control/core/core.py @@ -3740,6 +3740,7 @@ def __init__(self, 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") @@ -3748,6 +3749,7 @@ def _get_available_profiles(self) -> List[str]: 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: