Source code for caltrig.start_gui

from PyQt5.QtWidgets import (QApplication, QMainWindow, QStyle, QFileDialog, QMessageBox, QAction,
                            QVBoxLayout, QHBoxLayout, QWidget, QTabWidget)
from PyQt5.QtGui import QIcon
from caltrig.gui.main_widgets import (UpdateDialog, ParamDialog, VisualizeInstanceWidget, Viewer, ClusteringToolWidget,
                            GAToolWidget, CaltrigToolWidget, SDAToolWidget, ConfigFileDialog)

from .gui.genetic_algorithm_widgets import GAWindowWidget,GAGenerationScoreWindowWidget
from .gui.exploration_widgets import CaltrigWidget
from .gui.pop_up_messages import print_error
import sys
import os
import json
sys.path.insert(0, ".")
from .core.backend import DataInstance
from .gui.clustering_inspection_widgets import InspectionWidget
import dask

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

[docs]class MainWindow(QMainWindow): """ Main window of the application. It displays a window with a menu bar and a central widget, that contains the visualization of imported data and the tools to interact with it. """ def __init__(self, *args, **kwargs): self.processes = kwargs.pop("processes", True) super(MainWindow, self).__init__(*args, **kwargs) self.icon_path = os.path.join(BASE_DIR, "caltrig_icon.png") # Check if icon exists and set it with proper error handling self.icon_path = os.path.join(BASE_DIR, "caltrig_icon.png") if os.path.exists(self.icon_path): app_icon = QIcon(self.icon_path) self.setWindowIcon(app_icon) # Also set application-wide icon QApplication.setWindowIcon(app_icon) else: print(f"Warning: Icon file not found at {self.icon_path}") self.setWindowTitle("Cell Exploration Tool") self.setMinimumSize(600, 800) self.windows = {} dask.config.set({"array.slicing.split_large_chunks": True}) # Data stuff self.instances = {} self.path_list = {} self.instances_list = [] # INI file save path - persists across dialog instances self.save_ini_path = os.getcwd() # Event defaults: self.event_defaults = {"ALP": {"window": 20, "delay": 0}, "ILP": {"window": 20, "delay": 0}, "RNF": {"window": 20, "delay": 0}, "ALP_Timeout": {"window": 20, "delay": 0}, "distance_metric": "euclidean"} # Menu Bar pixmapi_folder = QStyle.StandardPixmap.SP_DirIcon button_folder = QAction(self.style().standardIcon(pixmapi_folder), "&Load Data", self) button_folder.setStatusTip("Select a Folder to load in data") button_folder.triggered.connect(self.load_data) pixmapi_save = QStyle.StandardPixmap.SP_DialogSaveButton button_save = QAction(self.style().standardIcon(pixmapi_save), "&Save", self) button_save.setStatusTip("Save current state") button_save.triggered.connect(self.save) pixmapi_load = QStyle.StandardPixmap.SP_FileDialogStart button_load = QAction(self.style().standardIcon(pixmapi_load), "&Load Saved State", self) button_load.setStatusTip("Load previously saved state") button_load.triggered.connect(self.load_saved_state) pixmapi_update = QStyle.StandardPixmap.SP_BrowserReload button_update = QAction(self.style().standardIcon(pixmapi_update), "&Update Default Parameters", self) button_update.setStatusTip("Update the default parameters of the events") button_update.triggered.connect(self.update_defaults) button_generate_ini = QAction(self.style().standardIcon(pixmapi_update), "&Generate INI File", self) button_generate_ini.setStatusTip("Generate INI File") button_generate_ini.triggered.connect(self.generate_ini_file) menu = self.menuBar() file_menu = menu.addMenu("&File") file_menu.addAction(button_folder) file_menu.addAction(button_save) file_menu.addAction(button_load) file_menu.addAction(button_update) file_menu.addAction(button_generate_ini) # Tool Widgets self.current_selection = None self.cl_tools = ClusteringToolWidget(self, self.event_defaults) self.ga_tools = GAToolWidget(self) self.e_tools = CaltrigToolWidget(self) self.sda_tools = SDAToolWidget(self) self.cl_tools.setEnabled(False) self.e_tools.setEnabled(False) self.ga_tools.setEnabled(False) self.sda_tools.setEnabled(False) # Layouts and tabs layout_central = QHBoxLayout() layout_cluster = QVBoxLayout() tabs = QTabWidget() tabs.setFixedWidth(400) self.instance_viz = VisualizeInstanceWidget(self) tabs.addTab(self.e_tools, "Caltrig") tabs.addTab(self.cl_tools, "Clustering") tabs.addTab(self.ga_tools, "Genetic Algorithm") layout_cluster.addWidget(self.instance_viz) layout_central.addLayout(layout_cluster) layout_central.addWidget(tabs) widget = QWidget() widget.setLayout(layout_central) self.setCentralWidget(widget) self.show()
[docs] def activate_params(self, viewer: Viewer): """ Activates the parameters of the selected viewer and deactivates the parameters of the previous viewer. Parameters ---------- viewer : Viewer The viewer that was selected. """ if self.current_selection is None: self.current_selection = viewer self.current_selection.change_to_red() self.cl_tools.setEnabled(True) self.e_tools.setEnabled(True) self.ga_tools.setEnabled(True) self.sda_tools.setEnabled(True) self.update_params() elif self.current_selection == viewer: if viewer.selected == True: viewer.change_to_white() self.cl_tools.setEnabled(False) self.e_tools.setEnabled(False) self.ga_tools.setEnabled(False) self.sda_tools.setEnabled(False) else: viewer.selected = True viewer.change_to_red() self.cl_tools.setEnabled(True) self.e_tools.setEnabled(True) self.ga_tools.setEnabled(True) self.sda_tools.setEnabled(True) self.update_params() else: self.current_selection.change_to_white() self.current_selection = viewer self.current_selection.change_to_red() self.cl_tools.setEnabled(True) self.e_tools.setEnabled(True) self.ga_tools.setEnabled(True) self.sda_tools.setEnabled(True) self.update_params()
[docs] def update_params(self): """ Updates the parameters of the current selection in the GUI. """ group, session, day, mouseID = self.current_selection.return_info() instance = self.instances[group][mouseID][f"{session}:{day}"] result = self.path_list[instance.config_path] if result is not None: self.cl_tools.display_params() result["no_of_clusters"] = instance.no_of_clusters self.cl_tools.update(result, instance.data["unit_ids"]) else: self.cl_tools.display_default()
[docs] def update_defaults(self): """ Opens a dialog to update the default parameters of the events. """ pdg = UpdateDialog(self.event_defaults) if pdg.exec(): result = pdg.get_result() else: return self.event_defaults = result self.cl_tools.update_defaults(self.event_defaults)
[docs] def generate_ini_file(self): """ Opens a dialog to generate an INI file for the current selection. """ config_dialog = ConfigFileDialog(save_ini_path=self.save_ini_path) if config_dialog.exec(): # Update the main window's save path if it was changed in the dialog self.save_ini_path = config_dialog.save_ini_path
[docs] def start_caltrig(self, current_selection=None): """ Starts the Caltrig window for the current selection. If a selection is not provided, the current selection of the GUI will be used. Parameters ---------- current_selection : Viewer, optional The current selection to be analyzed. If not provided, the current selection of the GUI will be used. """ current_selection = self.current_selection if current_selection is None else current_selection group, session, day, mouseID = current_selection.return_info() instance = self.instances[group][mouseID][f"{session}:{day}"] name = f"{instance.mouseID} {instance.day} {instance.session} Exploration" if name not in self.windows: wid = CaltrigWidget(instance, name, self, processes=self.processes) if wid is not None: wid.setWindowTitle(name) self.windows[name] = wid # Create a backup of E data instance.backup_data("E") wid.show()
[docs] def start_ga(self, ga): """ Starts the Genetic Algorithm window and the Generation Score window for the provided Genetic Algorithm object. Parameters ---------- ga : GeneticAlgorithm The Genetic Algorithm object to be visualized. """ name = "Genetic Algorithm" name2 = "Score" window = GAGenerationScoreWindowWidget(self,ga) window.setWindowTitle(window.name) self.windows[name2] = window window.show() if name not in self.windows: wid = GAWindowWidget(self,ga = ga) wid.setWindowTitle(name) self.windows[name] = wid wid.show()
[docs] def update_cluster(self, result): """ Updates the clustering results of the current selection. Parameters ---------- result : dict A dictionary containing the clustering results. """ no_of_clusters = result.pop("no_of_clusters") outliers = result.pop("outliers") distance_metric = result.pop("distance_metric") self.setWindowTitle("Loading...") group, session, day, mouseID = self.current_selection.return_info() instance = self.instances[group][mouseID][f"{session}:{day}"] instance.set_outliers(outliers) instance.set_distance_metric(distance_metric) instance.load_events(result.keys()) for event in result: delay, window = result[event]["delay"], result[event]["window"] instance.events[event].set_delay_and_duration(delay, window) instance.events[event].set_values() result["outliers"] = instance.outliers_list result["distance_metric"] = instance.distance_metric instance.set_vector() instance.set_no_of_clusters(no_of_clusters) instance.compute_clustering() self.path_list[instance.config_path] = result # Visualisation stuff mouseID, session, day, group, cl_result = instance.get_vis_info() self.current_selection.update_visualization(cl_result) self.setWindowTitle("Cell Exploration Tool") # Check if there is an active subwindow and update it name = f"{instance.mouseID} {instance.day} {instance.session}" if name in self.windows: self.windows[name].refresh()
[docs] def start_inspection(self, current_selection:Viewer=None): """ Initialize the clustering inspection window for the current selection. If a selection is not provided, the current GUI selection will be used. Parameters ---------- current_selection : Viewer, optional The current selection to be inspected. If not provided, the current selection of the GUI will be used. """ current_selection = self.current_selection if current_selection is None else current_selection group, session, day, mouseID = current_selection.return_info() instance = self.instances[group][mouseID][f"{session}:{day}"] name = f"{instance.mouseID} {instance.day} {instance.session} Inspection" # Clustering result will only contain basic if it wasn't computed if name not in self.windows and "all" in instance.clustering_result: wid = InspectionWidget(instance, self) wid.setWindowTitle(name) self.windows[name] = wid wid.show()
[docs] def delete_selection(self): """ Deletes the current selection from the application. Readjusts the visualization and removes all references to the selection. """ group, session, day, mouseID = self.current_selection.return_info() self.instance_viz.remove_visualization(group, mouseID, session, day) path = self.instances[group][mouseID][f"{session}:{day}"].config_path # Remove it from all references del self.path_list[path] del self.instances[group][mouseID][f"{session}:{day}"] if not self.instances[group][mouseID]: del self.instances[group][mouseID] self.cl_tools.setEnabled(False) self.e_tools.setEnabled(False) self.ga_tools.setEnabled(False) self.sda_tools.setEnabled(False) self.current_selection = None
def remove_window(self, name): del self.windows[name]
[docs] def load_data(self, _): """ Loads data from a folder and creates a DataInstance object for each file in the folder. """ fname = QFileDialog.getOpenFileName( self, "Select ini File", ) fname = fname[0] if fname != '' and fname not in self.path_list: try: self.load_instance(fname) except Exception as e: print_error((str(e), fname), extra_info="Make sure the file is a valid ini file.", severity=QMessageBox.Critical)
[docs] def load_clustering_params(self): """ Loads the clustering parameters from a dialog and starts the clustering process. """ pdg = ParamDialog(self.event_defaults) if pdg.exec(): result = pdg.get_result() else: return self.load_clustering(result)
[docs] def load_saved_state(self): """ Loads a previously saved state of the application from a JSON file. The file contains the paths of the loaded data and the default parameters for the events. """ fname = QFileDialog.getOpenFileName( self, "Open File", ) fname = fname[0] if fname[-4:] == "json": if os.path.getsize(fname) != 0: self.setWindowTitle("Loading...") with open(fname, 'r') as f: self.path_list = json.load(f) if "defaults" in self.path_list: self.event_defaults = self.path_list.pop("defaults") if "distance_metric" not in self.event_defaults: self.event_defaults["distance_metric"] = "euclidean" self.cl_tools.update_defaults(self.event_defaults) for path in self.path_list.keys(): self.load_instance(path) self.setWindowTitle("Cell Exploration Tool")
[docs] def load_clustering(self, result): """ Loads the clustering results into the current instance. Parameters ---------- result : dict A dictionary containing the clustering results. """ self.setWindowTitle("Loading...") current_selection = self.current_selection group, session, day, mouseID = current_selection.return_info() instance = self.instances[group][mouseID][f"{session}:{day}"] events = list(result.keys()) if "distance_metric" in result: events.remove("distance_metric") if "outliers" in result: events.remove("outliers") no_of_clusters = None if "no_of_clusters" in events: no_of_clusters = result["no_of_clusters"] events.remove("no_of_clusters") instance.load_events(events) if no_of_clusters is not None: instance.no_of_clusters = no_of_clusters for event in events: delay, window = result[event]["delay"], result[event]["window"] instance.events[event].set_delay_and_duration(delay, window) instance.events[event].set_values() if "outliers" in result: instance.set_outliers(result["outliers"]) if "distance_metric" in result: instance.set_distance_metric(result["distance_metric"]) instance.set_vector() instance.compute_clustering() self.path_list[instance.config_path] = result # Visualisation stuff mouseID, session, day, group, cl_result = instance.get_vis_info() # Generate the Grid self.instance_viz.add_visualization(group, mouseID, cl_result, session, day) self.cl_tools.display_params() self.current_selection.change_to_red() self.update_params() self.setWindowTitle("Cell Exploration Tool")
[docs] def load_instance(self, fname: str): """ Loads an instance of the DataInstance class and adds it to the instances dictionary. Parameters ---------- fname : str The path to the ini file that contains the data """ self.setWindowTitle("Loading...") instance = DataInstance(fname) self.instances_list.append(instance) # Check if the instance is valid if instance.check_essential_data(): mouseID, session, day, group, viz_result = instance.get_vis_info() if group not in self.instances: self.instances[group] = {} if instance.mouseID not in self.instances[group]: self.instances[group][f"{instance.mouseID}"] = {} self.instances[group][f"{instance.mouseID}"][f"{session}:{day}"] = instance self.instance_viz.add_visualization(group, mouseID, viz_result, session, day) self.path_list[fname] = None # Initially there might be no E and DFF data, check and create it if necessary instance.check_E() instance.check_DFF() self.setWindowTitle("Cell Exploration Tool")
[docs] def save(self): """ Saves the current state of the application to a JSON file. The file contains the paths of the loaded data and the default parameters for the events. """ default_dir = os.getcwd() default_filename = os.path.join(default_dir, "paths.json") filename, _ = QFileDialog.getSaveFileName( self, "Save State", default_filename, "JSON Files (*.json)" ) if filename: if self.path_list: extended_json = self.path_list.copy() extended_json["defaults"] = self.event_defaults with open(filename, 'w') as f: json.dump(extended_json, f, indent=4)
[docs] def closeEvent(self, event): """ Calls the parent closeEvent and closes all other windows. Parameters ---------- event : QCloseEvent The event that is triggered when the window is closed. """ windows_to_close = list(self.windows.values()) for window in windows_to_close: window.close() event.accept()
[docs]def start_gui(processes=True): """ Function to initiate the PyQt5 GUI. Example ------- >>> from caltrig.start_gui import start_gui >>> if __name__ == "__main__": >>> start_gui() """ app = QApplication([]) app.setStyle('Fusion') window = MainWindow(processes=processes) app.exec()