From ff08c4ac152a2776591a3c3b0a12532d8e4b41ab Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 3 Dec 2023 10:14:21 +0100 Subject: [PATCH 01/57] Added: Edit MUs class --- openhdemg/gui/__init__.py | 5 +- openhdemg/gui/edit_mus.py | 7 + openhdemg/gui/openhdemg_gui.py | 261 +++++++++++++++++---------------- 3 files changed, 147 insertions(+), 126 deletions(-) create mode 100644 openhdemg/gui/edit_mus.py diff --git a/openhdemg/gui/__init__.py b/openhdemg/gui/__init__.py index 108304c..7028f86 100644 --- a/openhdemg/gui/__init__.py +++ b/openhdemg/gui/__init__.py @@ -1 +1,4 @@ -__all__ = ["openhdemg_gui"] +__all__ = ["openhdemg_gui", "edit_mus"] + +from openhdemg.gui.openhdemg_gui import * +from openhdemg.gui.edit_mus import * diff --git a/openhdemg/gui/edit_mus.py b/openhdemg/gui/edit_mus.py new file mode 100644 index 0000000..e241eb4 --- /dev/null +++ b/openhdemg/gui/edit_mus.py @@ -0,0 +1,7 @@ +import tkinter as tk +from tkinter import ttk, StringVar, N, S, W, E +import os +import openhdemg.library as openhdemg +from openhdemg.gui import openhdemg_gui + + diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index a057995..56bdd16 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -24,7 +24,7 @@ import openhdemg.library as openhdemg -class emgGUI: +class emgGUI(tk.Tk): """ A class representing a Tkinter TK instance. @@ -331,6 +331,7 @@ def __init__(self, master): master: tk tk class object """ + tk.Tk.__init__(self) # Set up GUI self.master = master self.master.title("openhdemg") @@ -438,7 +439,7 @@ def __init__(self, master): separator2.grid(column=0, columnspan=3, row=9, sticky=(W, E)) # Remove Motor Units - remove_mus = ttk.Button(self.left, text="Remove MUs", command=self.remove_mus) + remove_mus = ttk.Button(self.left, text="Remove MUs", command=self.show_removal_window) remove_mus.grid(column=0, row=10, sticky=W) # COMMENT: This is commented because it is not fully functional @@ -828,7 +829,7 @@ def load_file(): # End progress progress.stop() progress.grid_remove() - + # Indicate Progress progress = ttk.Progressbar(self.left, mode="indeterminate") progress.grid(row=4, column=0) @@ -876,8 +877,6 @@ def on_filetype_change(self, *args): def decompose_file(self): pass - import threading - def save_emgfile(self): """ Instance method to save the edited emgfile. Results are saved in a .json file. @@ -1256,127 +1255,10 @@ def sort_mus(self): # ----------------------------------------------------------------------------------------------- # Removal of single motor units + def show_removal_window(self): + removal_window = MU_Removal_Window(parent=self, resdict=self.resdict) - def remove_mus(self): - """ - Instance method to open "Motor Unit Removal Window". Further option to select and - remove MUs are displayed. - - Executed when button "Remove MUs" in master GUI window is pressed. - - Raises - ------ - AttributeError - When no file is loaded prior to analysis. - """ - if hasattr(self, "resdict"): - # Create new window - self.head = tk.Toplevel(bg="LightBlue4") - self.head.title("Motor Unit Removal Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.head.grab_set() - - # Select Motor Unit - ttk.Label(self.head, text="Select MU:").grid( - column=1, row=0, padx=5, pady=5, sticky=W - ) - - self.mu_to_remove = StringVar() - removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] - removed_mu = ttk.Combobox( - self.head, width=10, textvariable=self.mu_to_remove - ) - removed_mu["values"] = removed_mu_value - removed_mu["state"] = "readonly" - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) - - # Remove Motor unit - remove = ttk.Button(self.head, text="Remove MU", command=self.remove) - remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) - - # Remove empty MUs - remove_empty = ttk.Button(self.head, text="Remove empty MUs", command=self.remove_empty) - remove_empty.grid(column=2, row=2, padx=5, pady=5) - else: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - def remove(self): - """ - Instance method that actually removes a selected motor unit based on user specification. - - Executed when button "Remove MU" in Motor Unit Removal Window is pressed. - The emgfile and the plot are subsequently updated. - - See Also - -------- - delete_mus in library. - """ - try: - # Get resdict with MU removed - self.resdict = openhdemg.delete_mus( - emgfile=self.resdict, munumber=int(self.mu_to_remove.get()) - ) - # Upate MU number - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) - ) - - # Update selection field - self.mu_to_remove = StringVar() - removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] - removed_mu = ttk.Combobox( - self.head, width=10, textvariable=self.mu_to_remove - ) - removed_mu["values"] = removed_mu_value - removed_mu["state"] = "readonly" - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) - - # Update plot - if hasattr(self, "fig"): - self.in_gui_plotting() - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - def remove_empty(self): - """ - Instance method that removes all empty MUs. - - Executed when button "Remove empty MUs" in Motor Unit Removal Window is pressed. - The emgfile and the plot are subsequently updated. - - See Also - -------- - delete_empty_mus in library. - """ - try: - # Get resdict with MU removed - self.resdict = openhdemg.delete_empty_mus(self.resdict) - - # Upate MU number - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) - ) - - # Update selection field - self.mu_to_remove = StringVar() - removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] - removed_mu = ttk.Combobox( - self.head, width=10, textvariable=self.mu_to_remove - ) - removed_mu["values"] = removed_mu_value - removed_mu["state"] = "readonly" - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) - - # Update plot - if hasattr(self, "fig"): - self.in_gui_plotting() - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - + # ----------------------------------------------------------------------------------------------- # Editing of single motor Units @@ -3252,6 +3134,135 @@ def display_results(self, input_df): table.show() + +class MU_Removal_Window(): + """ + Instance method to open "Motor Unit Removal Window". Further option to select and + remove MUs are displayed. + + Executed when button "Remove MUs" in master GUI window is pressed. + + Raises + ------ + AttributeError + When no file is loaded prior to analysis. + """ + + def __init__(self, parent, resdict): + super().__init__() + self.resdict = resdict + # Create new window + self.head = tk.Toplevel() + self.head.title("Motor Unit Removal Window") + self.head.iconbitmap( + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" + ) + self.head.grab_set() + + # Select Motor Unit + ttk.Label(self.head, text="Select MU:").grid( + column=1, row=0, padx=5, pady=5, sticky=W + ) + + self.mu_to_remove = StringVar() + removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] + removed_mu = ttk.Combobox( + self.head, width=10, textvariable=self.mu_to_remove + ) + removed_mu["values"] = removed_mu_value + removed_mu["state"] = "readonly" + removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + + # Remove Motor unit + remove = ttk.Button(self.head, text="Remove MU", command=self.remove) + remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) + + # Remove empty MUs + remove_empty = ttk.Button(self.head, text="Remove empty MUs", command=self.remove_empty) + remove_empty.grid(column=2, row=2, padx=5, pady=5) + + def remove(self): + """ + Instance method that actually removes a selected motor unit based on user specification. + + Executed when button "Remove MU" in Motor Unit Removal Window is pressed. + The emgfile and the plot are subsequently updated. + + See Also + -------- + delete_mus in library. + """ + try: + # Get resdict with MU removed + print(self.mu_to_remove.get()) + self.resdict = openhdemg.delete_mus( + emgfile=self.resdict, munumber=int(self.mu_to_remove.get()) + ) + # Upate MU number + ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( + column=2, row=3, sticky=(W, E) + ) + + # Update selection field + self.mu_to_remove = StringVar() + removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] + removed_mu = ttk.Combobox( + self.head, width=10, textvariable=self.mu_to_remove + ) + removed_mu["values"] = removed_mu_value + removed_mu["state"] = "readonly" + removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + + # Update plot + if hasattr(self, "fig"): + self.in_gui_plotting() + + except AttributeError: + tk.messagebox.showerror("Information", "Make sure a file is loaded.") + + def remove_empty(self): + """ + Instance method that removes all empty MUs. + + Executed when button "Remove empty MUs" in Motor Unit Removal Window is pressed. + The emgfile and the plot are subsequently updated. + + See Also + -------- + delete_empty_mus in library. + """ + try: + # Get resdict with MU removed + self.resdict = openhdemg.delete_empty_mus(self.resdict) + + # Upate MU number + ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( + column=2, row=3, sticky=(W, E) + ) + + # Update selection field + self.mu_to_remove = StringVar() + removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] + removed_mu = ttk.Combobox( + self.head, width=10, textvariable=self.mu_to_remove + ) + removed_mu["values"] = removed_mu_value + removed_mu["state"] = "readonly" + removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + + # Update plot + if hasattr(self, "fig"): + self.in_gui_plotting() + + except AttributeError: + tk.messagebox.showerror("Information", "Make sure a file is loaded.") + + + + + + + # ----------------------------------------------------------------------------------------------- def run_main(): # Run GUI upon calling From a69edc5ee27a516c22bb2499e8983fa68d88075a Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 3 Dec 2023 17:14:44 +0100 Subject: [PATCH 02/57] Added: GUI_modules --- openhdemg/gui/__init__.py | 5 +- openhdemg/gui/edit_mus.py | 7 -- openhdemg/gui/gui_modules/__init__.py | 3 + openhdemg/gui/gui_modules/edit_mus.py | 128 +++++++++++++++++++++ openhdemg/gui/openhdemg_gui.py | 160 +++----------------------- 5 files changed, 148 insertions(+), 155 deletions(-) delete mode 100644 openhdemg/gui/edit_mus.py create mode 100644 openhdemg/gui/gui_modules/__init__.py create mode 100644 openhdemg/gui/gui_modules/edit_mus.py diff --git a/openhdemg/gui/__init__.py b/openhdemg/gui/__init__.py index 7028f86..17035a0 100644 --- a/openhdemg/gui/__init__.py +++ b/openhdemg/gui/__init__.py @@ -1,4 +1,3 @@ -__all__ = ["openhdemg_gui", "edit_mus"] +__all__ = ["openhdemg_gui"] -from openhdemg.gui.openhdemg_gui import * -from openhdemg.gui.edit_mus import * +from openhdemg.gui.openhdemg_gui import * \ No newline at end of file diff --git a/openhdemg/gui/edit_mus.py b/openhdemg/gui/edit_mus.py deleted file mode 100644 index e241eb4..0000000 --- a/openhdemg/gui/edit_mus.py +++ /dev/null @@ -1,7 +0,0 @@ -import tkinter as tk -from tkinter import ttk, StringVar, N, S, W, E -import os -import openhdemg.library as openhdemg -from openhdemg.gui import openhdemg_gui - - diff --git a/openhdemg/gui/gui_modules/__init__.py b/openhdemg/gui/gui_modules/__init__.py new file mode 100644 index 0000000..695c571 --- /dev/null +++ b/openhdemg/gui/gui_modules/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["edit_mus"] + +from openhdemg.gui.gui_modules.edit_mus import * diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py new file mode 100644 index 0000000..fcbbf07 --- /dev/null +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -0,0 +1,128 @@ +"""Module containing the MU Removal GUI class""" + +import customtkinter as ctk +from tkinter import StringVar, W, E, messagebox +import os +import openhdemg.library as openhdemg + +class MU_Removal_Window: + """ + Instance method to open "Motor Unit Removal Window". Further option to select and + remove MUs are displayed. + + Executed when button "Remove MUs" in master GUI window is pressed. + + Raises + ------ + AttributeError + When no file is loaded prior to analysis. + """ + + def __init__(self, parent, resdict): + self.parent = parent + self.resdict = resdict + # Create new window + self.head = ctk.CTkToplevel(fg_color="LightBlue4") + # Set the background color of the top-level window + self.head.title("Motor Unit Removal Window") + self.head.iconbitmap( + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" + ) + self.head.grab_set() + + # Select Motor Unit + ctk.CTkLabel(self.head, text="Select MU:").grid( + column=1, row=0, padx=5, pady=5, sticky=W + ) + + self.mu_to_remove = StringVar() + removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] + removed_mu_value = list(map(str, removed_mu_value)) + removed_mu = ctk.CTkComboBox( + self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value + ) + removed_mu.configure(state="readonly") + removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + + # Remove Motor unit + remove = ctk.CTkButton(self.head, text="Remove MU", command=self.remove) + remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) + + # Remove empty MUs + remove_empty = ctk.CTkButton(self.head, text="Remove empty MUs", command=self.remove_empty) + remove_empty.grid(column=2, row=2, padx=5, pady=5) + + def remove(self): + """ + Instance method that actually removes a selected motor unit based on user specification. + + Executed when button "Remove MU" in Motor Unit Removal Window is pressed. + The emgfile and the plot are subsequently updated. + + See Also + -------- + delete_mus in library. + """ + try: + # Get resdict with MU removed + self.parent.resdict = openhdemg.delete_mus( + emgfile=self.parent.resdict, munumber=int(self.mu_to_remove.get()) + ) + # Upate MU number + ctk.CTkLabel(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"])).grid( + column=2, row=3, sticky=(W, E) + ) + + # Update selection field + self.mu_to_remove = StringVar() + removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] + removed_mu_value = list(map(str, removed_mu_value)) + removed_mu = ctk.CTkComboBox( + self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value + ) + removed_mu.configure(state="readonly") + removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + + # Update plot + if hasattr(self.parent, "fig"): + self.parent.in_gui_plotting(resdict=self.parent.resdict) + + except AttributeError: + messagebox.showerror("Information", "Make sure a file is loaded.") + + def remove_empty(self): + """ + Instance method that removes all empty MUs. + + Executed when button "Remove empty MUs" in Motor Unit Removal Window is pressed. + The emgfile and the plot are subsequently updated. + + See Also + -------- + delete_empty_mus in library. + """ + try: + # Get resdict with MU removed + self.parent.resdict = openhdemg.delete_empty_mus(self.parent.resdict) + + # Upate MU number + ctk.Label(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"])).grid( + column=2, row=3, sticky=(W, E) + ) + + # Update selection field + self.mu_to_remove = StringVar() + removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] + removed_mu_value = list(map(str, removed_mu_value)) + removed_mu = ctk.CTkComboBox( + self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value + ) + removed_mu.configure(state="readonly") + removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + + # Update plot + if hasattr(self.parent, "fig"): + self.parent.in_gui_plotting(resdict=self.parent.resdict) + + except AttributeError: + messagebox.showerror("Information", "Make sure a file is loaded.") diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index e392f40..250abfc 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -22,9 +22,10 @@ matplotlib.use("TkAgg") import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules import MU_Removal_Window -class emgGUI(tk.Tk): +class emgGUI(): """ A class representing a Tkinter TK instance. @@ -331,7 +332,6 @@ def __init__(self, master): master: tk tk class object """ - tk.Tk.__init__(self) # Set up GUI self.master = master self.master.title("openhdemg") @@ -429,7 +429,7 @@ def __init__(self, master): export.grid(column=1, row=6, sticky=(W, E)) # View Motor Unit Firings - firings = ttk.Button(self.left, text="View MUs", command=self.in_gui_plotting) + firings = ttk.Button(self.left, text="View MUs", command=lambda: (self.in_gui_plotting(resdict=self.resdict))) firings.grid(column=0, row=8, sticky=W) # Sort Motor Units @@ -1071,7 +1071,7 @@ def reset_analysis(self): # Update Plot if hasattr(self, "fig"): - self.in_gui_plotting() + self.in_gui_plotting(resdict=self.resdict) # Clear frame for output if hasattr(self, "terminal"): @@ -1201,7 +1201,7 @@ def on_matrix_none_adv(self, *args): # ----------------------------------------------------------------------------------------------- # Plotting inside of GUI - def in_gui_plotting(self, plot="idr"): + def in_gui_plotting(self, resdict, plot="idr"): """ Instance method to plot any analysis results in the GUI for inspection. Plots are updated during the analysis process. @@ -1221,19 +1221,19 @@ def in_gui_plotting(self, plot="idr"): try: if self.filetype.get() in ["OTB_REFSIG", "CUSTOMCSV_REFSIG", "DELSYS_REFSIG"]: self.fig = openhdemg.plot_refsig( - emgfile=self.resdict, showimmediately=False, tight_layout=True + emgfile=resdict, showimmediately=False, tight_layout=True ) elif plot == "idr": self.fig = openhdemg.plot_idr( - emgfile=self.resdict, showimmediately=False, tight_layout=True + emgfile=resdict, showimmediately=False, tight_layout=True ) elif plot == "refsig_fil": self.fig = openhdemg.plot_refsig( - emgfile=self.resdict, showimmediately=False, tight_layout=True + emgfile=resdict, showimmediately=False, tight_layout=True ) elif plot == "refsig_off": self.fig = openhdemg.plot_refsig( - emgfile=self.resdict, showimmediately=False, tight_layout=True + emgfile=resdict, showimmediately=False, tight_layout=True ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) @@ -1270,7 +1270,7 @@ def sort_mus(self): # Update plot if hasattr(self, "fig"): - self.in_gui_plotting() + self.in_gui_plotting(resdict=self.resdict) except AttributeError: tk.messagebox.showerror("Information", "Make sure a file is loaded.") @@ -1469,7 +1469,7 @@ def filter_refsig(self): cutoff=int(self.cutoff_freq.get()), ) # Plot filtered Refsig - self.in_gui_plotting(plot="refsig_fil") + self.in_gui_plotting(resdict=self.resdict, plot="refsig_fil") except AttributeError: tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") @@ -1498,7 +1498,7 @@ def remove_offset(self): auto=int(self.auto_eval.get()), ) # Update Plot - self.in_gui_plotting(plot="refsig_off") + self.in_gui_plotting(resdict=self.resdict, plot="refsig_off") except AttributeError: tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") @@ -1528,7 +1528,7 @@ def convert_refsig(self): self.resdict["REF_SIGNAL"] = self.resdict["REF_SIGNAL"] / self.convert_factor.get() # Update Plot - self.in_gui_plotting(plot="refsig_off") + self.in_gui_plotting(resdict=self.resdict, plot="refsig_off") except AttributeError: tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") @@ -1554,7 +1554,7 @@ def to_percent(self): self.resdict["REF_SIGNAL"] = (self.resdict["REF_SIGNAL"] * 100) / self.mvc_value.get() # Update Plot - self.in_gui_plotting() + self.in_gui_plotting(resdict=self.resdict) except AttributeError: tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") @@ -1600,7 +1600,7 @@ def resize_file(self): emgfile=self.resdict, area=[start, end] ) # Update Plot - self.in_gui_plotting() + self.in_gui_plotting(resdict=self.resdict) # Update filelength ttk.Label(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( @@ -3158,136 +3158,6 @@ def display_results(self, input_df): # Show results table.show() - - -class MU_Removal_Window(): - """ - Instance method to open "Motor Unit Removal Window". Further option to select and - remove MUs are displayed. - - Executed when button "Remove MUs" in master GUI window is pressed. - - Raises - ------ - AttributeError - When no file is loaded prior to analysis. - """ - - def __init__(self, parent, resdict): - super().__init__() - self.resdict = resdict - # Create new window - self.head = tk.Toplevel() - self.head.title("Motor Unit Removal Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.head.grab_set() - - # Select Motor Unit - ttk.Label(self.head, text="Select MU:").grid( - column=1, row=0, padx=5, pady=5, sticky=W - ) - - self.mu_to_remove = StringVar() - removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] - removed_mu = ttk.Combobox( - self.head, width=10, textvariable=self.mu_to_remove - ) - removed_mu["values"] = removed_mu_value - removed_mu["state"] = "readonly" - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) - - # Remove Motor unit - remove = ttk.Button(self.head, text="Remove MU", command=self.remove) - remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) - - # Remove empty MUs - remove_empty = ttk.Button(self.head, text="Remove empty MUs", command=self.remove_empty) - remove_empty.grid(column=2, row=2, padx=5, pady=5) - - def remove(self): - """ - Instance method that actually removes a selected motor unit based on user specification. - - Executed when button "Remove MU" in Motor Unit Removal Window is pressed. - The emgfile and the plot are subsequently updated. - - See Also - -------- - delete_mus in library. - """ - try: - # Get resdict with MU removed - print(self.mu_to_remove.get()) - self.resdict = openhdemg.delete_mus( - emgfile=self.resdict, munumber=int(self.mu_to_remove.get()) - ) - # Upate MU number - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) - ) - - # Update selection field - self.mu_to_remove = StringVar() - removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] - removed_mu = ttk.Combobox( - self.head, width=10, textvariable=self.mu_to_remove - ) - removed_mu["values"] = removed_mu_value - removed_mu["state"] = "readonly" - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) - - # Update plot - if hasattr(self, "fig"): - self.in_gui_plotting() - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - def remove_empty(self): - """ - Instance method that removes all empty MUs. - - Executed when button "Remove empty MUs" in Motor Unit Removal Window is pressed. - The emgfile and the plot are subsequently updated. - - See Also - -------- - delete_empty_mus in library. - """ - try: - # Get resdict with MU removed - self.resdict = openhdemg.delete_empty_mus(self.resdict) - - # Upate MU number - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) - ) - - # Update selection field - self.mu_to_remove = StringVar() - removed_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] - removed_mu = ttk.Combobox( - self.head, width=10, textvariable=self.mu_to_remove - ) - removed_mu["values"] = removed_mu_value - removed_mu["state"] = "readonly" - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) - - # Update plot - if hasattr(self, "fig"): - self.in_gui_plotting() - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - - - - - - # ----------------------------------------------------------------------------------------------- def run_main(): # Run GUI upon calling From 94b5de481cef554bb46f4308f85c8c4b219be59a Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 3 Dec 2023 22:01:08 +0100 Subject: [PATCH 03/57] Added: MU edit window --- openhdemg/gui/gui_modules/__init__.py | 4 +- openhdemg/gui/gui_modules/edit_mus.py | 16 +- openhdemg/gui/gui_modules/edit_refsig.py | 236 +++++++++++++++++++ openhdemg/gui/gui_modules/gui_helpers.py | 59 +++++ openhdemg/gui/openhdemg_gui.py | 283 +---------------------- 5 files changed, 313 insertions(+), 285 deletions(-) create mode 100644 openhdemg/gui/gui_modules/edit_refsig.py create mode 100644 openhdemg/gui/gui_modules/gui_helpers.py diff --git a/openhdemg/gui/gui_modules/__init__.py b/openhdemg/gui/gui_modules/__init__.py index 695c571..304e533 100644 --- a/openhdemg/gui/gui_modules/__init__.py +++ b/openhdemg/gui/gui_modules/__init__.py @@ -1,3 +1,5 @@ -__all__ = ["edit_mus"] +__all__ = ["edit_mus", "edit_refsig", "gui_helpers"] from openhdemg.gui.gui_modules.edit_mus import * +from openhdemg.gui.gui_modules.edit_refsig import * +from openhdemg.gui.gui_modules.gui_helpers import * diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index fcbbf07..1d06388 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -1,11 +1,11 @@ """Module containing the MU Removal GUI class""" -import customtkinter as ctk from tkinter import StringVar, W, E, messagebox import os +import customtkinter as ctk import openhdemg.library as openhdemg -class MU_Removal_Window: +class MURemovalWindow: """ Instance method to open "Motor Unit Removal Window". Further option to select and remove MUs are displayed. @@ -31,7 +31,7 @@ def __init__(self, parent, resdict): self.head.grab_set() # Select Motor Unit - ctk.CTkLabel(self.head, text="Select MU:").grid( + ctk.CTkLabel(self.head, text="Select MU:", font=('Segoe UI',15, 'bold')).grid( column=1, row=0, padx=5, pady=5, sticky=W ) @@ -45,11 +45,13 @@ def __init__(self, parent, resdict): removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) # Remove Motor unit - remove = ctk.CTkButton(self.head, text="Remove MU", command=self.remove) + remove = ctk.CTkButton(self.head, text="Remove MU", command=self.remove, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) # Remove empty MUs - remove_empty = ctk.CTkButton(self.head, text="Remove empty MUs", command=self.remove_empty) + remove_empty = ctk.CTkButton(self.head, text="Remove empty MUs", command=self.remove_empty, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) remove_empty.grid(column=2, row=2, padx=5, pady=5) def remove(self): @@ -69,7 +71,7 @@ def remove(self): emgfile=self.parent.resdict, munumber=int(self.mu_to_remove.get()) ) # Upate MU number - ctk.CTkLabel(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"])).grid( + ctk.CTkLabel(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"]), font=('Segoe UI',15, 'bold')).grid( column=2, row=3, sticky=(W, E) ) @@ -106,7 +108,7 @@ def remove_empty(self): self.parent.resdict = openhdemg.delete_empty_mus(self.parent.resdict) # Upate MU number - ctk.Label(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"])).grid( + ctk.Label(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"]), font=('Segoe UI',15, 'bold')).grid( column=2, row=3, sticky=(W, E) ) diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py new file mode 100644 index 0000000..c64b0d4 --- /dev/null +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -0,0 +1,236 @@ +"""Module containing the Resif editing class""" + +import os +from tkinter import ttk, W, E, StringVar, DoubleVar, messagebox +import customtkinter as ctk + +import openhdemg.library as openhdemg + +class EditRefsig: + """ + Instance method to open "Reference Signal Editing Window". Options for + refsig filtering and offset removal are displayed. + + Executed when button "RefSig Editing" in master GUI window is pressed. + """ + def __init__(self, parent): + self.parent = parent + + # Create new window + self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head.title("Reference Signal Editing Window") + self.head.iconbitmap( + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" + ) + self.head.grab_set() + self.head.resizable(width=True, height=True) + + # Filter Refsig + # Define Labels + ctk.CTkLabel(self.head, text="Filter Order", font=('Segoe UI',15, 'bold')).grid(column=1, row=0, sticky=(W, E)) + ctk.CTkLabel(self.head, text="Cutoff Freq", font=('Segoe UI',15, 'bold')).grid(column=2, row=0, sticky=(W, E)) + # Fiter button + basic = ctk.CTkButton(self.head, text="Filter Refsig", command=self.filter_refsig, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + basic.grid(column=0, row=1, sticky=W) + self.filter_order = StringVar() + order = ctk.CTkEntry(self.head, width=100, textvariable=self.filter_order) + order.grid(column=1, row=1) + self.filter_order.set(4) + + self.cutoff_freq = StringVar() + cutoff = ctk.CTkEntry(self.head, width=100, textvariable=self.cutoff_freq) + cutoff.grid(column=2, row=1) + self.cutoff_freq.set(15) + + # Remove offset of reference signal + separator2 = ttk.Separator(self.head, orient="horizontal") + separator2.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) + + ctk.CTkLabel(self.head, text="Offset Value", font=('Segoe UI',15, 'bold')).grid(column=1, row=3, sticky=(W, E)) + ctk.CTkLabel(self.head, text="Automatic Offset", font=('Segoe UI',15, 'bold')).grid( + column=2, row=3, sticky=(W, E) + ) + + # Offset removal button + basic2 = ctk.CTkButton(self.head, text="Remove Offset", command=self.remove_offset, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + basic2.grid(column=0, row=4, sticky=W) + + self.offsetval = StringVar() + offset = ctk.CTkEntry(self.head, width=100, textvariable=self.offsetval) + offset.grid(column=1, row=4) + self.offsetval.set(4) + + self.auto_eval = StringVar() + auto = ctk.CTkEntry(self.head, width=100, textvariable=self.auto_eval) + auto.grid(column=2, row=4) + self.auto_eval.set(0) + + separator3 = ttk.Separator(self.head, orient="horizontal") + separator3.grid(column=0, columnspan=3, row=5, sticky=(W, E), padx=5, pady=5) + + # Convert Reference signal + ctk.CTkLabel(self.head, text="Operator", font=('Segoe UI',15, 'bold')).grid(column=1, row=6, sticky=(W, E)) + ctk.CTkLabel(self.head, text="Factor", font=('Segoe UI',15, 'bold')).grid( + column=2, row=6, sticky=(W, E) + ) + + self.convert = StringVar() + convert = ctk.CTkComboBox(self.head, width=100, variable=self.convert, values=("Multiply", "Divide")) + convert.configure(state="readonly") + convert.grid(column=1, row=7) + self.convert.set("Multiply") + + self.convert_factor = DoubleVar() + factor = ctk.CTkEntry(self.head, width=100, textvariable=self.convert_factor) + factor.grid(column=2, row=7) + self.convert_factor.set(2.5) + + convert_button = ctk.CTkButton(self.head, text="Convert", command=self.convert_refsig, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + convert_button.grid(column=0, row=7, sticky=W) + + separator3 = ttk.Separator(self.head, orient="horizontal") + separator3.grid(column=0, columnspan=3, row=8, sticky=(W, E), padx=5, pady=5) + + # Convert to percentage + ctk.CTkLabel(self.head, text="MVC Value", font=('Segoe UI',15, 'bold')).grid(column=1, row=9, sticky=(W, E)) + + percent_button = ctk.CTkButton(self.head, text="To Percent*", command=self.to_percent, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + percent_button.grid(column=0, row=10, sticky=W) + + self.mvc_value = DoubleVar() + mvc = ctk.CTkEntry(self.head, width=100, textvariable=self.mvc_value) + mvc.grid(column=1, row=10) + + + ctk.CTkLabel(self.head, + text= "*Use this button \nonly if your Refsig \nis in absolute values!", + font=("Arial", 8)).grid( + column=2, row=9, rowspan=2 + ) + + # Add padding to all children widgets of head + for child in self.head.winfo_children(): + child.grid_configure(padx=5, pady=5) + + ### Define functions for Refsig editing + + def filter_refsig(self): + """ + Instance method that filters the refig based on user selected specs. + + Executed when button "Filter Refsig" in Reference Signal Editing Window is pressed. + The emgfile and the GUI plot are updated. + + Raises + ------ + AttributeError + When no reference signal file is available. + + See Also + -------- + filter_refsig in library. + """ + try: + # Filter refsig + self.parent.resdict = openhdemg.filter_refsig( + emgfile=self.parent.resdict, + order=int(self.filter_order.get()), + cutoff=int(self.cutoff_freq.get()), + ) + # Plot filtered Refsig + self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_fil") + + except AttributeError: + messagebox.showerror("Information", "Make sure a Refsig file is loaded.") + + def remove_offset(self): + """ + Instance Method that removes user specified/selected Refsig offset. + + Executed when button "Remove offset" in Reference Signal Editing Window is pressed. + The emgfile and the GUI plot are updated. + + Raises + ------ + AttributeError + When no reference signal file is available + + See Also + -------- + remove_offset in library. + """ + try: + # Remove offset + self.parent.resdict = openhdemg.remove_offset( + emgfile=self.parent.resdict, + offsetval=int(self.offsetval.get()), + auto=int(self.auto_eval.get()), + ) + # Update Plot + self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") + + except AttributeError: + messagebox.showerror("Information", "Make sure a Refsig file is loaded.") + + except ValueError: + messagebox.showerror("Information", "Make sure to specify valid filtering or offset values.") + + def convert_refsig(self): + """ + Instance Method that converts Refsig by multiplication or division. + + Executed when button "Convert" in Reference Signal Editing Window is pressed. + The emgfile and the GUI plot are updated. + + Raises + ------ + AttributeError + When no reference signal file is available + ValueError + When invalid conversion factor is specified + + """ + try: + if self.convert.get() == "Multiply": + self.parent.resdict["REF_SIGNAL"] = self.parent.resdict["REF_SIGNAL"] * self.convert_factor.get() + elif self.convert.get() == "Divide": + self.parent.resdict["REF_SIGNAL"] = self.parent.resdict["REF_SIGNAL"] / self.convert_factor.get() + + # Update Plot + self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") + + except AttributeError: + messagebox.showerror("Information", "Make sure a Refsig file is loaded.") + + except ValueError: + messagebox.showerror("Information", "Make sure to specify a valid conversion factor.") + + def to_percent(self): + """ + Instance Method that converts Refsig to a percentag value. Should only be used when the Refsig is in absolute values. + + Executed when button "To Percen" in Reference Signal Editing Window is pressed. + The emgfile and the GUI plot are updated. + + Raises + ------ + AttributeError + When no reference signal file is available + ValueError + When invalid conversion factor is specified + """ + try: + self.parent.resdict["REF_SIGNAL"] = (self.parent.resdict["REF_SIGNAL"] * 100) / self.mvc_value.get() + + # Update Plot + self.parent.in_gui_plotting(resdict=self.parent.resdict) + + except AttributeError: + messagebox.showerror("Information", "Make sure a Refsig file is loaded.") + + except ValueError: + messagebox.showerror("Information", "Make sure to specify a valid conversion factor.") \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py new file mode 100644 index 0000000..47c2587 --- /dev/null +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -0,0 +1,59 @@ +"""Module that contains all helper functions for the GUI""" + +import os +from tkinter import ttk, W, E, messagebox +import customtkinter as ctk + +import openhdemg.library as openhdemg + +class GUIHelpers: + """ + """ + def __init__(self, parent): + self.parent=parent + + def resize_file(self): + """ + Instance method to get resize area from user specification on plot and + resize emgfile. + + Executed when button "Select Resize" is pressed in Resize file window. + The emgfile and the GUI plot are updated. + + Raises + ------ + AttributeError + When no file is loaded prior to analysis. + + See Also + -------- + showselect, resize_emgfile in library. + """ + try: + # Open selection window for user + points = openhdemg.showselect( + emgfile=self.parent.resdict, + title="Select the start/end area to resize by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + titlesize=10, + ) + start, end = points[0], points[1] + + # Delsys requires different handling for resize + if self.parent.resdict["SOURCE"] == "DELSYS": + self.parent.resdict, _, _ = openhdemg.resize_emgfile( + emgfile=self.parent.resdict, area=[start, end], accuracy="maintain" + ) + else: + self.parent.resdict, _, _ = openhdemg.resize_emgfile( + emgfile=self.parent.resdict, area=[start, end] + ) + # Update Plot + self.parent.in_gui_plotting(resdict=self.parent.resdict) + + # Update filelength + ctk.CTkLabel(self.parent.left, text=str(self.parent.resdict["EMG_LENGTH"]), font=('Segoe UI',12)).grid( + column=2, row=4, sticky=(W, E) + ) + + except AttributeError: + messagebox.showerror("Information", "Make sure a file is loaded.") diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 250abfc..aff969a 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -22,7 +22,7 @@ matplotlib.use("TkAgg") import openhdemg.library as openhdemg -from openhdemg.gui.gui_modules import MU_Removal_Window +from openhdemg.gui.gui_modules import MURemovalWindow, EditRefsig, GUIHelpers class emgGUI(): @@ -439,7 +439,7 @@ def __init__(self, master): separator2.grid(column=0, columnspan=3, row=9, sticky=(W, E)) # Remove Motor Units - remove_mus = ttk.Button(self.left, text="Remove MUs", command=self.show_removal_window) + remove_mus = ttk.Button(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self, resdict=self.resdict))) remove_mus.grid(column=0, row=10, sticky=W) # COMMENT: This is commented because it is not fully functional @@ -454,12 +454,12 @@ def __init__(self, master): # Filter Reference Signal reference = ttk.Button( - self.left, text="RefSig Editing", command=self.edit_refsig + self.left, text="RefSig Editing", command=lambda:(EditRefsig(parent=self)) ) reference.grid(column=0, row=12, sticky=W) # Resize File - resize = ttk.Button(self.left, text="Resize File", command=self.resize_file) + resize = ttk.Button(self.left, text="Resize File", command=lambda:(GUIHelpers(parent=self).resize_file())) resize.grid(column=1, row=12, sticky=(W, E)) separator4 = ttk.Separator(self.left, orient="horizontal") separator4.grid(column=0, columnspan=3, row=13, sticky=(W, E)) @@ -1278,12 +1278,7 @@ def sort_mus(self): except KeyError: tk.messagebox.showerror("Information", "Sorting not possible when ≤ 1" + "\nMU is present in the File (i.e. Refsigs)") - # ----------------------------------------------------------------------------------------------- - # Removal of single motor units - def show_removal_window(self): - removal_window = MU_Removal_Window(parent=self, resdict=self.resdict) - # ----------------------------------------------------------------------------------------------- # Editing of single motor Units @@ -1339,277 +1334,11 @@ def show_removal_window(self): # ----------------------------------------------------------------------------------------------- # Editing of Reference EMG Signal - def edit_refsig(self): - """ - Instance method to open "Reference Signal Editing Window". Options for - refsig filtering and offset removal are displayed. - - Executed when button "RefSig Editing" in master GUI window is pressed. - """ - # Create new window - self.head = tk.Toplevel(bg="LightBlue4") - self.head.title("Reference Signal Editing Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.head.grab_set() - - # Filter Refsig - # Define Labels - ttk.Label(self.head, text="Filter Order").grid(column=1, row=0, sticky=(W, E)) - ttk.Label(self.head, text="Cutoff Freq").grid(column=2, row=0, sticky=(W, E)) - # Fiter button - basic = ttk.Button(self.head, text="Filter Refsig", command=self.filter_refsig) - basic.grid(column=0, row=1, sticky=W) - - self.filter_order = StringVar() - order = ttk.Entry(self.head, width=10, textvariable=self.filter_order) - order.grid(column=1, row=1) - self.filter_order.set(4) - - self.cutoff_freq = StringVar() - cutoff = ttk.Entry(self.head, width=10, textvariable=self.cutoff_freq) - cutoff.grid(column=2, row=1) - self.cutoff_freq.set(15) - - # Remove offset of reference signal - separator2 = ttk.Separator(self.head, orient="horizontal") - separator2.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) - - ttk.Label(self.head, text="Offset Value").grid(column=1, row=3, sticky=(W, E)) - ttk.Label(self.head, text="Automatic Offset").grid( - column=2, row=3, sticky=(W, E) - ) - - # Offset removal button - basic2 = ttk.Button(self.head, text="Remove Offset", command=self.remove_offset) - basic2.grid(column=0, row=4, sticky=W) - - self.offsetval = StringVar() - offset = ttk.Entry(self.head, width=10, textvariable=self.offsetval) - offset.grid(column=1, row=4) - self.offsetval.set(4) - - self.auto_eval = StringVar() - auto = ttk.Entry(self.head, width=10, textvariable=self.auto_eval) - auto.grid(column=2, row=4) - self.auto_eval.set(0) - - separator3 = ttk.Separator(self.head, orient="horizontal") - separator3.grid(column=0, columnspan=3, row=5, sticky=(W, E), padx=5, pady=5) - - # Convert Reference signal - ttk.Label(self.head, text="Operator").grid(column=1, row=6, sticky=(W, E)) - ttk.Label(self.head, text="Factor").grid( - column=2, row=6, sticky=(W, E) - ) - - self.convert = StringVar() - convert = ttk.Combobox(self.head, width=10, textvariable=self.convert) - convert["values"] = ("Multiply", "Divide") - convert["state"] = "readonly" - convert.grid(column=1, row=7) - self.convert.set("Multiply") - - self.convert_factor = DoubleVar() - factor = ttk.Entry(self.head, width=10, textvariable=self.convert_factor) - factor.grid(column=2, row=7) - self.convert_factor.set(2.5) - - convert_button = ttk.Button(self.head, text="Convert", command=self.convert_refsig) - convert_button.grid(column=0, row=7, sticky=W) - - separator3 = ttk.Separator(self.head, orient="horizontal") - separator3.grid(column=0, columnspan=3, row=8, sticky=(W, E), padx=5, pady=5) - - # Convert to percentage - ttk.Label(self.head, text="MVC Value").grid(column=1, row=9, sticky=(W, E)) - - percent_button = ttk.Button(self.head, text="To Percent*", command=self.to_percent) - percent_button.grid(column=0, row=10, sticky=W) - - self.mvc_value = DoubleVar() - mvc = ttk.Entry(self.head, width=10, textvariable=self.mvc_value) - mvc.grid(column=1, row=10) - - - ttk.Label(self.head, - text= "*Use this button \nonly if your Refsig \nis in absolute values!", - font=("Arial", 8)).grid( - column=2, row=9, rowspan=2 - ) - - # Add padding to all children widgets of head - for child in self.head.winfo_children(): - child.grid_configure(padx=5, pady=5) - - ### Define functions for Refsig editing - - def filter_refsig(self): - """ - Instance method that filters the refig based on user selected specs. - - Executed when button "Filter Refsig" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. - - Raises - ------ - AttributeError - When no reference signal file is available. - - See Also - -------- - filter_refsig in library. - """ - try: - # Filter refsig - self.resdict = openhdemg.filter_refsig( - emgfile=self.resdict, - order=int(self.filter_order.get()), - cutoff=int(self.cutoff_freq.get()), - ) - # Plot filtered Refsig - self.in_gui_plotting(resdict=self.resdict, plot="refsig_fil") - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") - - def remove_offset(self): - """ - Instance Method that removes user specified/selected Refsig offset. - - Executed when button "Remove offset" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. - - Raises - ------ - AttributeError - When no reference signal file is available - - See Also - -------- - remove_offset in library. - """ - try: - # Remove offset - self.resdict = openhdemg.remove_offset( - emgfile=self.resdict, - offsetval=int(self.offsetval.get()), - auto=int(self.auto_eval.get()), - ) - # Update Plot - self.in_gui_plotting(resdict=self.resdict, plot="refsig_off") - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") - - except ValueError: - tk.messagebox.showerror("Information", "Make sure to specify valid filtering or offset values.") - - def convert_refsig(self): - """ - Instance Method that converts Refsig by multiplication or division. - - Executed when button "Convert" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. - - Raises - ------ - AttributeError - When no reference signal file is available - ValueError - When invalid conversion factor is specified - - """ - try: - if self.convert.get() == "Multiply": - self.resdict["REF_SIGNAL"] = self.resdict["REF_SIGNAL"] * self.convert_factor.get() - elif self.convert.get() == "Divide": - self.resdict["REF_SIGNAL"] = self.resdict["REF_SIGNAL"] / self.convert_factor.get() - - # Update Plot - self.in_gui_plotting(resdict=self.resdict, plot="refsig_off") - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") - - except ValueError: - tk.messagebox.showerror("Information", "Make sure to specify a valid conversion factor.") - - def to_percent(self): - """ - Instance Method that converts Refsig to a percentag value. Should only be used when the Refsig is in absolute values. - - Executed when button "To Percen" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. - - Raises - ------ - AttributeError - When no reference signal file is available - ValueError - When invalid conversion factor is specified - """ - try: - self.resdict["REF_SIGNAL"] = (self.resdict["REF_SIGNAL"] * 100) / self.mvc_value.get() - - # Update Plot - self.in_gui_plotting(resdict=self.resdict) - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") - - except ValueError: - tk.messagebox.showerror("Information", "Make sure to specify a valid conversion factor.") + # ----------------------------------------------------------------------------------------------- # Resize EMG File - def resize_file(self): - """ - Instance method to get resize area from user specification on plot and - resize emgfile. - - Executed when button "Select Resize" is pressed in Resize file window. - The emgfile and the GUI plot are updated. - - Raises - ------ - AttributeError - When no file is loaded prior to analysis. - - See Also - -------- - showselect, resize_emgfile in library. - """ - try: - # Open selection window for user - points = openhdemg.showselect( - emgfile=self.resdict, - title="Select the start/end area to resize by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", - titlesize=10, - ) - start, end = points[0], points[1] - - # Delsys requires different handling for resize - if self.resdict["SOURCE"] == "DELSYS": - self.resdict, _, _ = openhdemg.resize_emgfile( - emgfile=self.resdict, area=[start, end], accuracy="maintain" - ) - else: - self.resdict, _, _ = openhdemg.resize_emgfile( - emgfile=self.resdict, area=[start, end] - ) - # Update Plot - self.in_gui_plotting(resdict=self.resdict) - - # Update filelength - ttk.Label(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E) - ) - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - + # ----------------------------------------------------------------------------------------------- # Analysis of Force From 8987c7b5c7f4341bc207aac6decb4fc203a26d83 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 10 Dec 2023 10:42:47 +0100 Subject: [PATCH 04/57] Clean up --- openhdemg/gui/openhdemg_gui.py | 69 +--------------------------------- 1 file changed, 2 insertions(+), 67 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index aff969a..2a2ca78 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -409,13 +409,6 @@ def __init__(self, master): separator0 = ttk.Separator(self.left, orient="horizontal") separator0.grid(column=0, columnspan=3, row=5, sticky=(W, E)) - # COMMENT: This is commented out because it is not yet functional. - # Decompose file - # decompose = ttk.Button(self.left, - # text="Decompose", - # command=self.decompose_file) - # decompose.grid(row=3, column=0, sticky=W) - # Save File save = ttk.Button(self.left, text="Save File", command=self.save_emgfile) save.grid(column=0, row=6, sticky=W) @@ -442,13 +435,6 @@ def __init__(self, master): remove_mus = ttk.Button(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self, resdict=self.resdict))) remove_mus.grid(column=0, row=10, sticky=W) - # COMMENT: This is commented because it is not fully functional - # Edit Motor Units - # edit_mus = ttk.Button(self.left, - # text="Edit MUs", - # command=self.editing_mus) - # edit_mus.grid(column=1, row=10, sticky=W) - separator3 = ttk.Separator(self.left, orient="horizontal") separator3.grid(column=0, columnspan=3, row=11, sticky=(W, E)) @@ -1279,66 +1265,15 @@ def sort_mus(self): tk.messagebox.showerror("Information", "Sorting not possible when ≤ 1" + "\nMU is present in the File (i.e. Refsigs)") - # ----------------------------------------------------------------------------------------------- - # Editing of single motor Units - - # def editing_mus(self): - # """ - # Instance method to edit sindle motor units. For now, this contains only plotting single MUs. - # More options will be added. - - # THIS PART IS NOT YET INTEGRATED IN THE GUI. - # """ - - # # Create new window - # self.head = tk.Toplevel(bg='LightBlue4') - # self.head.title("Motor Unit Eiditing Window") - # self.head.grab_set() - - # # Select Motor Unit - # ttk.Label(self.head, text="Select MU:").grid(column=0, row=0, sticky=W, padx=5, pady=5) - - # self.mu_to_edit = StringVar() - # edit_mu_value = [*range(0, self.resdict["NUMBER_OF_MUS"])] - # edit_mu = ttk.Combobox(self.head, width=10, textvariable=self.mu_to_edit) - # edit_mu["values"] = edit_mu_value - # edit_mu["state"] = "readonly" - # edit_mu.grid(column=1, row=0, sticky=(W,E), padx=5, pady=5) - - # # Button to plot MU - # single_mu = ttk.Button(self.head, - # text="View single MU", - # command=self.view_single_mu) - # single_mu.grid(column=1, row=1, sticky=(W,E), padx=5, pady=5) - - # def view_single_mu(self): - # """ - # Instance method that plots single selected MU. - - # THIS PART IS NOT YET INTEGRATED IN THE GUI. - # """ - # # Make figure - # fig = openhdemg.plot_idr(emgfile=self.resdict, - # munumber=int(self.mu_to_edit.get()), - # showimmediately=False) - # # Create canvas and plot - # canvas = FigureCanvasTkAgg(fig, master=self.head) - # canvas_plot = canvas.get_tk_widget() - # canvas_plot.grid(column=1, row=2, sticky=(W,E)) - # # Place matplotlib toolbar - # toolbar = NavigationToolbar2Tk(canvas, self.head, pack_toolbar=False) - # toolbar.grid(row=3, column=1) - # # terminate matplotlib to ensure GUI shutdown when closed - # plt.close() # ----------------------------------------------------------------------------------------------- # Editing of Reference EMG Signal - + # ----------------------------------------------------------------------------------------------- # Resize EMG File - + # ----------------------------------------------------------------------------------------------- # Analysis of Force From f3378d66bb937f26c19edb39bfe25e12b5469e3b Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 10 Dec 2023 20:27:38 +0100 Subject: [PATCH 05/57] Added: MU properties / analyse force --- openhdemg/gui/gui_modules/__init__.py | 5 +- openhdemg/gui/gui_modules/analyse_force.py | 123 ++++++++ openhdemg/gui/gui_modules/edit_mus.py | 18 +- openhdemg/gui/gui_modules/edit_refsig.py | 38 ++- openhdemg/gui/gui_modules/gui_helpers.py | 9 +- openhdemg/gui/gui_modules/gui_plotting.py | 1 + openhdemg/gui/gui_modules/mu_properties.py | 260 +++++++++++++++ openhdemg/gui/openhdemg_gui.py | 348 +-------------------- 8 files changed, 440 insertions(+), 362 deletions(-) create mode 100644 openhdemg/gui/gui_modules/analyse_force.py create mode 100644 openhdemg/gui/gui_modules/gui_plotting.py create mode 100644 openhdemg/gui/gui_modules/mu_properties.py diff --git a/openhdemg/gui/gui_modules/__init__.py b/openhdemg/gui/gui_modules/__init__.py index 304e533..34fb173 100644 --- a/openhdemg/gui/gui_modules/__init__.py +++ b/openhdemg/gui/gui_modules/__init__.py @@ -1,5 +1,8 @@ -__all__ = ["edit_mus", "edit_refsig", "gui_helpers"] +__all__ = ["edit_mus", "edit_refsig", "gui_helpers", "analyse_force", + "mu_properties"] from openhdemg.gui.gui_modules.edit_mus import * from openhdemg.gui.gui_modules.edit_refsig import * from openhdemg.gui.gui_modules.gui_helpers import * +from openhdemg.gui.gui_modules.analyse_force import * +from openhdemg.gui.gui_modules.mu_properties import * diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py new file mode 100644 index 0000000..351f495 --- /dev/null +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -0,0 +1,123 @@ +"""Module containing the force analysis GUI""" + +import os +from tkinter import ttk, W, E, StringVar +import customtkinter as ctk +from CTkMessagebox import CTkMessagebox +import pandas as pd +import openhdemg.library as openhdemg + + + +class AnalyseForce: + """ + Class containing the force analysis window for openhdemg + + Instance method to open "Force analysis Window". + Options to analyse force singal are displayed. + + Executed when "Analyse Force" button in master GUI window is pressed. + """ + + def __init__(self, parent): + self.parent = parent + + # Create new window + self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head.title("Force Analysis Window") + + # This after method is necessary due to a bug in customtktinter + # usually, the iconbitmap is overwritten after 200ms to a default one + self.head.iconbitmap( + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" + ) + self.head.grab_set() + + # Get MVC + get_mvf = ctk.CTkButton(self.head, text="Get MVC", command=self.get_mvc, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + get_mvf.grid(column=0, row=1, sticky=(W, E), padx=5, pady=5) + + # Get RFD + separator1 = ttk.Separator(self.head, orient="horizontal") + separator1.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) + + ctk.CTkLabel(self.head, text="RFD miliseconds", font=('Segoe UI',15, 'bold')).grid( + column=1, row=3, sticky=(W, E), padx=5, pady=5 + ) + + get_rfd = ctk.CTkButton(self.head, text="Get RFD", command=self.get_rfd, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + get_rfd.grid(column=0, row=4, sticky=(W, E), padx=5, pady=5) + + self.rfdms = StringVar() + milisecond = ctk.CTkEntry(self.head, width=100, textvariable=self.rfdms) + milisecond.grid(column=1, row=4, sticky=(W, E), padx=5, pady=5) + self.rfdms.set("50,100,150,200") + + ### Define functions for force analysis + + def get_mvc(self): + """ + Instance methof to retrieve calculated MVC based on user selection. + + Executed when button "Get MVC" in Analyze Force window is pressed. + The Results of the analysis are displayed in the results terminal. + + Raises + ------ + AttributeError + When no file is loaded prior to analysis. + + See Also + -------- + get_mvc in the library + """ + try: + # get MVC + mvc = openhdemg.get_mvc(emgfile=self.parent.resdict) + # Define dictionary for pandas + mvc_dic = {"MVC": [mvc]} + mvc_df = pd.DataFrame(data=mvc_dic) + # Display results + self.parent.display_results(input_df=mvc_df) + + except AttributeError: + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def get_rfd(self): + """ + Instance method to calculate RFD at specified timepoints based on user selection. + + Executed when button "Get RFD" in Analyze Force window is pressed. + The Results of the analysis are displayed in the results terminal. + + Raises + ------ + AttributeError + When no file is loaded prior to analysis. + + See Also + -------- + get_rfd in library + """ + try: + # Define list for RFD computation + ms = str(self.rfdms.get()) + # Split the string at , + ms_list = ms.split(",") + # Use comprehension to iterate through + ms_list = [int(i) for i in ms_list] + # Calculate rfd + rfd = openhdemg.compute_rfd(emgfile=self.parent.resdict, ms=ms_list) + # Display results + self.parent.display_results(input_df=rfd) + + except AttributeError: + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index 1d06388..eccefef 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -1,8 +1,9 @@ """Module containing the MU Removal GUI class""" -from tkinter import StringVar, W, E, messagebox +from tkinter import StringVar, W, E import os import customtkinter as ctk +from CTkMessagebox import CTkMessagebox import openhdemg.library as openhdemg class MURemovalWindow: @@ -39,9 +40,8 @@ def __init__(self, parent, resdict): removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] removed_mu_value = list(map(str, removed_mu_value)) removed_mu = ctk.CTkComboBox( - self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value + self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value, state="readonly" ) - removed_mu.configure(state="readonly") removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) # Remove Motor unit @@ -90,8 +90,11 @@ def remove(self): self.parent.in_gui_plotting(resdict=self.parent.resdict) except AttributeError: - messagebox.showerror("Information", "Make sure a file is loaded.") - + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + def remove_empty(self): """ Instance method that removes all empty MUs. @@ -127,4 +130,7 @@ def remove_empty(self): self.parent.in_gui_plotting(resdict=self.parent.resdict) except AttributeError: - messagebox.showerror("Information", "Make sure a file is loaded.") + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index c64b0d4..c731713 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -3,6 +3,7 @@ import os from tkinter import ttk, W, E, StringVar, DoubleVar, messagebox import customtkinter as ctk +from CTkMessagebox import CTkMessagebox import openhdemg.library as openhdemg @@ -145,7 +146,10 @@ def filter_refsig(self): self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_fil") except AttributeError: - messagebox.showerror("Information", "Make sure a Refsig file is loaded.") + CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") def remove_offset(self): """ @@ -174,10 +178,16 @@ def remove_offset(self): self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") except AttributeError: - messagebox.showerror("Information", "Make sure a Refsig file is loaded.") - + CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError: - messagebox.showerror("Information", "Make sure to specify valid filtering or offset values.") + CTkMessagebox(title="Info", message="Make sure to specify valid filtering or offset values.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") def convert_refsig(self): """ @@ -204,10 +214,16 @@ def convert_refsig(self): self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") except AttributeError: - messagebox.showerror("Information", "Make sure a Refsig file is loaded.") + CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except ValueError: - messagebox.showerror("Information", "Make sure to specify a valid conversion factor.") + CTkMessagebox(title="Info", message="Make sure to specify valid conversion factor.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") def to_percent(self): """ @@ -230,7 +246,13 @@ def to_percent(self): self.parent.in_gui_plotting(resdict=self.parent.resdict) except AttributeError: - messagebox.showerror("Information", "Make sure a Refsig file is loaded.") + CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except ValueError: - messagebox.showerror("Information", "Make sure to specify a valid conversion factor.") \ No newline at end of file + CTkMessagebox(title="Info", message="Make sure to specify valid conversion factor.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 47c2587..e49bbc5 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -1,8 +1,8 @@ """Module that contains all helper functions for the GUI""" -import os -from tkinter import ttk, W, E, messagebox +from tkinter import W, E import customtkinter as ctk +from CTkMessagebox import CTkMessagebox import openhdemg.library as openhdemg @@ -56,4 +56,7 @@ def resize_file(self): ) except AttributeError: - messagebox.showerror("Information", "Make sure a file is loaded.") + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -0,0 +1 @@ + diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py new file mode 100644 index 0000000..72958bf --- /dev/null +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -0,0 +1,260 @@ +"""Module containing MU propterty analysis""" + +import customtkinter as ctk +import os +from tkinter import ttk, W, E, StringVar +import customtkinter as ctk +from CTkMessagebox import CTkMessagebox +import pandas as pd +import openhdemg.library as openhdemg + + +class MuAnalysis: + """ + Instance method to open "Motor Unit Properties Window". Options to analyse motor + unit properties such as recruitement threshold, discharge rate or + basic properties computing are displayed. + + Executed when button "MU Properties" button in master GUI window is pressed. + """ + def __init__(self, parent): + # Create new window + self.parent = parent + self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head.title("Motor Unit Properties Window") + self.head.iconbitmap( + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" + ) + self.head.grab_set() + + # MVC Entry + ctk.CTkLabel(self.head, text="Enter MVC[n]:", font=('Segoe UI',15, 'bold')).grid(column=0, row=0, sticky=(W)) + self.mvc_value = StringVar() + enter_mvc = ctk.CTkEntry(self.head, width=100, textvariable=self.mvc_value) + enter_mvc.grid(column=1, row=0, sticky=(W, E)) + + # Compute MU re-/derecruitement threshold + separator = ttk.Separator(self.head, orient="horizontal") + separator.grid(column=0, columnspan=4, row=2, padx=5, pady=5) + + thresh = ctk.CTkButton( + self.head, text="Compute threshold", command=self.compute_mu_threshold, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1 + ) + thresh.grid(column=0, row=3, sticky=W) + + self.ct_event = StringVar() + ct_event_values = ("rt", "dert", "rt_dert") + ct_events_entry = ctk.CTkComboBox(self.head, width=100, variable=self.ct_event, + values=ct_event_values, state="readonly") + ct_events_entry.grid(column=1, row=3) + self.ct_event.set("Event") + + self.ct_type = StringVar() + ct_types_values = ("abs", "rel", "abs_rel") + ct_types_entry = ctk.CTkComboBox(self.head, width=100, variable=self.ct_type, + values=ct_types_values, state="readonly") + ct_types_entry.grid(column=2, row=3) + self.ct_type.set("Type") + + # Compute motor unit discharge rate + separator1 = ttk.Separator(self.head, orient="horizontal") + separator1.grid(column=0, columnspan=4, row=4, sticky=(W, E), padx=5, pady=5) + + ctk.CTkLabel(self.head, text="Firings at Rec", font=('Segoe UI',15, 'bold')).grid(column=1, row=5, sticky=(W, E)) + ctk.CTkLabel(self.head, text="Firings Start/End Steady", font=('Segoe UI',15, 'bold')).grid( + column=2, row=5, sticky=(W, E) + ) + + dr_rate = ctk.CTkButton( + self.head, text="Compute discharge rate", command=self.compute_mu_dr, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1 + ) + dr_rate.grid(column=0, row=6, sticky=W) + + self.firings_rec = StringVar() + firings_1 = ctk.CTkEntry(self.head, width=100, textvariable=self.firings_rec) + firings_1.grid(column=1, row=6) + self.firings_rec.set(4) + + self.firings_ste = StringVar() + firings_2 = ctk.CTkEntry(self.head, width=100, textvariable=self.firings_ste) + firings_2.grid(column=2, row=6) + self.firings_ste.set(10) + + self.dr_event = StringVar() + dr_events_values = ( + "rec", + "derec", + "rec_derec", + "steady", + "rec_derec_steady", + ) + dr_events_entry = ctk.CTkComboBox(self.head, width=100, variable=self.dr_event, + values=dr_events_values, state="readonly") + dr_events_entry.grid(column=3, row=6, sticky=E) + self.dr_event.set("Event") + + # Compute basic motor unit properties + separator2 = ttk.Separator(self.head, orient="horizontal") + separator2.grid(column=0, columnspan=4, row=7, sticky=(W, E), padx=5, pady=5) + + ctk.CTkLabel(self.head, text="Firings at Rec", font=('Segoe UI',15, 'bold')).grid(column=1, row=8, sticky=(W, E)) + ctk.CTkLabel(self.head, text="Firings Start/End Steady", font=('Segoe UI',15, 'bold')).grid( + column=2, row=8, sticky=(W, E) + ) + + basic = ctk.CTkButton( + self.head, text="Basic MU properties", command=self.basic_mus_properties, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1 + ) + basic.grid(column=0, row=9, sticky=W) + + self.b_firings_rec = StringVar() + b_firings_1 = ctk.CTkEntry(self.head, width=100, textvariable=self.b_firings_rec) + b_firings_1.grid(column=1, row=9) + self.b_firings_rec.set(4) + + self.b_firings_ste = StringVar() + b_firings_2 = ctk.CTkEntry(self.head, width=100, textvariable=self.b_firings_ste) + b_firings_2.grid(column=2, row=9) + self.b_firings_ste.set(10) + + for child in self.head.winfo_children(): + child.grid_configure(padx=5, pady=5) + + ### Define functions for motor unit property calculation + + def compute_mu_threshold(self): + """ + Instance method to compute the motor unit recruitement thresholds + based on user selection of events and types. + + Executed when button "Compute threshold" in Motor Unit Properties Window + is pressed. The analysis results are displayed in the result terminal. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + ValueError + When entered MVC is not valid (inexistent). + AssertionError + When types/events are not specified. + + See Also + -------- + compute_thresholds in library. + """ + try: + # Compute thresholds + self.mu_thresholds = openhdemg.compute_thresholds( + emgfile=self.resdict, + event_=self.ct_event.get(), + type_=self.ct_type.get(), + mvc=float(self.mvc_value.get()), + ) + # Display results + self.display_results(self.mu_thresholds) + + except AttributeError: + tk.messagebox.showerror("Information", "Load file prior to computation.") + + except ValueError: + tk.messagebox.showerror("Information", "Enter valid MVC.") + + except AssertionError: + tk.messagebox.showerror("Information", "Specify Event and/or Type.") + + def compute_mu_dr(self): + """ + Instance method to compute the motor unit discharge rate + based on user selection of Firings and Events. + + Executed when button "Compute discharge rate" in Motor Unit Properties Window + is pressed. The analysis results are displayed in the result terminal. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + ValueError + When entered Firings values are not valid (inexistent). + AssertionError + When types/events are not specified. + + See Also + -------- + compute_dr in library. + """ + try: + # Compute discharge rates + self.mus_dr = openhdemg.compute_dr( + emgfile=self.resdict, + n_firings_RecDerec=int(self.firings_rec.get()), + n_firings_steady=int(self.firings_ste.get()), + event_=self.dr_event.get(), + ) + # Display results + self.display_results(self.mus_dr) + + except AttributeError: + tk.messagebox.showerror("Information", "Load file prior to computation.") + + except ValueError: + tk.messagebox.showerror( + "Information", + "Enter valid Firings value or select a correct number of points." + ) + + except AssertionError: + tk.messagebox.showerror("Information", "Specify Event and/or Type.") + + def basic_mus_properties(self): + """ + Instance method to compute basic motor unit properties based in user + selection in plot. + + Executed when button "Basic MU properties" in Motor Unit Properties Window + is pressed. The analysis results are displayed in the result terminal. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + ValueError + When entered Firings values are not valid (inexistent). + AssertionError + When types/events are not specified. + UnboundLocalError + When start/end area for calculations are specified wrongly. + + See Also + -------- + basic_mus_properties in library. + """ + try: + # Calculate properties + self.exportable_df = openhdemg.basic_mus_properties( + emgfile=self.resdict, + n_firings_RecDerec=int(self.b_firings_rec.get()), + n_firings_steady=int(self.b_firings_ste.get()), + mvc=float(self.mvc_value.get()), + ) + # Display results + self.display_results(self.exportable_df) + + except AttributeError: + tk.messagebox.showerror("Information", "Load file prior to computation.") + + except ValueError: + tk.messagebox.showerror( + "Information", + "Enter valid MVC or select a correct number of points." + ) + + except AssertionError: + tk.messagebox.showerror("Information", "Specify Event and/or Type.") + + except UnboundLocalError: + tk.messagebox.showerror("Information", "Select start/end area again.") diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 2a2ca78..e9f9cf9 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -22,7 +22,7 @@ matplotlib.use("TkAgg") import openhdemg.library as openhdemg -from openhdemg.gui.gui_modules import MURemovalWindow, EditRefsig, GUIHelpers +from openhdemg.gui.gui_modules import MURemovalWindow, EditRefsig, GUIHelpers, AnalyseForce, MuAnalysis class emgGUI(): @@ -451,13 +451,13 @@ def __init__(self, master): separator4.grid(column=0, columnspan=3, row=13, sticky=(W, E)) # Force Analysis - force = ttk.Button(self.left, text="Analyse Force", command=self.analyze_force) + force = ttk.Button(self.left, text="Analyse Force", command=lambda:(AnalyseForce(parent=self))) force.grid(column=0, row=14, sticky=W) separator5 = ttk.Separator(self.left, orient="horizontal") separator5.grid(column=0, columnspan=3, row=15, sticky=(W, E)) # Motor Unit properties - mus = ttk.Button(self.left, text="MU Properties", command=self.mu_analysis) + mus = ttk.Button(self.left, text="MU Properties", command=lambda:(MuAnalysis(parent=self))) mus.grid(column=1, row=14, sticky=W) separator6 = ttk.Separator(self.left, orient="horizontal") separator6.grid(column=0, columnspan=3, row=17, sticky=(W, E)) @@ -1277,350 +1277,10 @@ def sort_mus(self): # ----------------------------------------------------------------------------------------------- # Analysis of Force - def analyze_force(self): - """ - Instance method to open "Force analysis Window". - Options to analyse force singal are displayed. - - Executed when "Analyse Force" button in master GUI window is pressed. - """ - # Create new window - self.head = tk.Toplevel(bg="LightBlue4") - self.head.title("Force Analysis Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.head.grab_set() - - # Get MVC - get_mvf = ttk.Button(self.head, text="Get MVC", command=self.get_mvc) - get_mvf.grid(column=0, row=1, sticky=(W, E), padx=5, pady=5) - - # Get RFD - separator1 = ttk.Separator(self.head, orient="horizontal") - separator1.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) - - ttk.Label(self.head, text="RFD miliseconds").grid( - column=1, row=3, sticky=(W, E), padx=5, pady=5 - ) - - get_rfd = ttk.Button(self.head, text="Get RFD", command=self.get_rfd) - get_rfd.grid(column=0, row=4, sticky=(W, E), padx=5, pady=5) - - self.rfdms = StringVar() - milisecond = ttk.Entry(self.head, width=10, textvariable=self.rfdms) - milisecond.grid(column=1, row=4, sticky=(W, E), padx=5, pady=5) - self.rfdms.set("50,100,150,200") - - ### Define functions for force analysis - - def get_mvc(self): - """ - Instance methof to retrieve calculated MVC based on user selection. - - Executed when button "Get MVC" in Analyze Force window is pressed. - The Results of the analysis are displayed in the results terminal. - - Raises - ------ - AttributeError - When no file is loaded prior to analysis. - - See Also - -------- - get_mvc in the library - """ - try: - # get MVC - self.mvc = openhdemg.get_mvc(emgfile=self.resdict) - # Define dictionary for pandas - mvc_dic = {"MVC": [self.mvc]} - self.mvc_df = pd.DataFrame(data=mvc_dic) - # Display results - self.display_results(self.mvc_df) - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - def get_rfd(self): - """ - Instance method to calculate RFD at specified timepoints based on user selection. - - Executed when button "Get RFD" in Analyze Force window is pressed. - The Results of the analysis are displayed in the results terminal. - - Raises - ------ - AttributeError - When no file is loaded prior to analysis. - - See Also - -------- - get_rfd in library - """ - try: - # Define list for RFD computation - ms = str(self.rfdms.get()) - # Split the string at , - ms_list = ms.split(",") - # Use comprehension to iterate through - ms_list = [int(i) for i in ms_list] - # Calculate rfd - self.rfd = openhdemg.compute_rfd(emgfile=self.resdict, ms=ms_list) - # Display results - self.display_results(self.rfd) - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - # ----------------------------------------------------------------------------------------------- # Analysis of motor unit properties - def mu_analysis(self): - """ - Instance method to open "Motor Unit Properties Window". Options to analyse motor - unit properties such as recruitement threshold, discharge rate or - basic properties computing are displayed. - - Executed when button "MU Properties" button in master GUI window is pressed. - """ - # Create new window - self.head = tk.Toplevel(bg="LightBlue4") - self.head.title("Motor Unit Properties Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.head.grab_set() - - # MVC Entry - ttk.Label(self.head, text="Enter MVC[n]:").grid(column=0, row=0, sticky=(W)) - self.mvc_value = StringVar() - enter_mvc = ttk.Entry(self.head, width=20, textvariable=self.mvc_value) - enter_mvc.grid(column=1, row=0, sticky=(W, E)) - - # Compute MU re-/derecruitement threshold - separator = ttk.Separator(self.head, orient="horizontal") - separator.grid(column=0, columnspan=4, row=2, sticky=(W, E), padx=5, pady=5) - - thresh = ttk.Button( - self.head, text="Compute threshold", command=self.compute_mu_threshold - ) - thresh.grid(column=0, row=3, sticky=W) - - self.ct_event = StringVar() - ct_events_entry = ttk.Combobox(self.head, width=10, textvariable=self.ct_event) - ct_events_entry["values"] = ("rt", "dert", "rt_dert") - ct_events_entry["state"] = "readonly" - ct_events_entry.grid(column=1, row=3, sticky=(W, E)) - self.ct_event.set("Event") - - self.ct_type = StringVar() - ct_types_entry = ttk.Combobox(self.head, width=10, textvariable=self.ct_type) - ct_types_entry["values"] = ("abs", "rel", "abs_rel") - ct_types_entry["state"] = "readonly" - ct_types_entry.grid(column=2, row=3, sticky=(W, E)) - self.ct_type.set("Type") - - # Compute motor unit discharge rate - separator1 = ttk.Separator(self.head, orient="horizontal") - separator1.grid(column=0, columnspan=4, row=4, sticky=(W, E), padx=5, pady=5) - - ttk.Label(self.head, text="Firings at Rec").grid(column=1, row=5, sticky=(W, E)) - ttk.Label(self.head, text="Firings Start/End Steady").grid( - column=2, row=5, sticky=(W, E) - ) - - dr_rate = ttk.Button( - self.head, text="Compute discharge rate", command=self.compute_mu_dr - ) - dr_rate.grid(column=0, row=6, sticky=W) - - self.firings_rec = StringVar() - firings_1 = ttk.Entry(self.head, width=20, textvariable=self.firings_rec) - firings_1.grid(column=1, row=6) - self.firings_rec.set(4) - - self.firings_ste = StringVar() - firings_2 = ttk.Entry(self.head, width=20, textvariable=self.firings_ste) - firings_2.grid(column=2, row=6) - self.firings_ste.set(10) - - self.dr_event = StringVar() - dr_events_entry = ttk.Combobox(self.head, width=10, textvariable=self.dr_event) - dr_events_entry["values"] = ( - "rec", - "derec", - "rec_derec", - "steady", - "rec_derec_steady", - ) - dr_events_entry["state"] = "readonly" - dr_events_entry.grid(column=3, row=6, sticky=E) - self.dr_event.set("Event") - - # Compute basic motor unit properties - separator2 = ttk.Separator(self.head, orient="horizontal") - separator2.grid(column=0, columnspan=4, row=7, sticky=(W, E), padx=5, pady=5) - - ttk.Label(self.head, text="Firings at Rec").grid(column=1, row=8, sticky=(W, E)) - ttk.Label(self.head, text="Firings Start/End Steady").grid( - column=2, row=8, sticky=(W, E) - ) - - basic = ttk.Button( - self.head, text="Basic MU properties", command=self.basic_mus_properties - ) - basic.grid(column=0, row=9, sticky=W) - - self.b_firings_rec = StringVar() - b_firings_1 = ttk.Entry(self.head, width=20, textvariable=self.b_firings_rec) - b_firings_1.grid(column=1, row=9) - self.b_firings_rec.set(4) - - self.b_firings_ste = StringVar() - b_firings_2 = ttk.Entry(self.head, width=20, textvariable=self.b_firings_ste) - b_firings_2.grid(column=2, row=9) - self.b_firings_ste.set(10) - - for child in self.head.winfo_children(): - child.grid_configure(padx=5, pady=5) - - ### Define functions for motor unit property calculation - - def compute_mu_threshold(self): - """ - Instance method to compute the motor unit recruitement thresholds - based on user selection of events and types. - - Executed when button "Compute threshold" in Motor Unit Properties Window - is pressed. The analysis results are displayed in the result terminal. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - ValueError - When entered MVC is not valid (inexistent). - AssertionError - When types/events are not specified. - - See Also - -------- - compute_thresholds in library. - """ - try: - # Compute thresholds - self.mu_thresholds = openhdemg.compute_thresholds( - emgfile=self.resdict, - event_=self.ct_event.get(), - type_=self.ct_type.get(), - mvc=float(self.mvc_value.get()), - ) - # Display results - self.display_results(self.mu_thresholds) - - except AttributeError: - tk.messagebox.showerror("Information", "Load file prior to computation.") - - except ValueError: - tk.messagebox.showerror("Information", "Enter valid MVC.") - - except AssertionError: - tk.messagebox.showerror("Information", "Specify Event and/or Type.") - - def compute_mu_dr(self): - """ - Instance method to compute the motor unit discharge rate - based on user selection of Firings and Events. - - Executed when button "Compute discharge rate" in Motor Unit Properties Window - is pressed. The analysis results are displayed in the result terminal. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - ValueError - When entered Firings values are not valid (inexistent). - AssertionError - When types/events are not specified. - - See Also - -------- - compute_dr in library. - """ - try: - # Compute discharge rates - self.mus_dr = openhdemg.compute_dr( - emgfile=self.resdict, - n_firings_RecDerec=int(self.firings_rec.get()), - n_firings_steady=int(self.firings_ste.get()), - event_=self.dr_event.get(), - ) - # Display results - self.display_results(self.mus_dr) - - except AttributeError: - tk.messagebox.showerror("Information", "Load file prior to computation.") - - except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid Firings value or select a correct number of points." - ) - - except AssertionError: - tk.messagebox.showerror("Information", "Specify Event and/or Type.") - - def basic_mus_properties(self): - """ - Instance method to compute basic motor unit properties based in user - selection in plot. - - Executed when button "Basic MU properties" in Motor Unit Properties Window - is pressed. The analysis results are displayed in the result terminal. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - ValueError - When entered Firings values are not valid (inexistent). - AssertionError - When types/events are not specified. - UnboundLocalError - When start/end area for calculations are specified wrongly. - - See Also - -------- - basic_mus_properties in library. - """ - try: - # Calculate properties - self.exportable_df = openhdemg.basic_mus_properties( - emgfile=self.resdict, - n_firings_RecDerec=int(self.b_firings_rec.get()), - n_firings_steady=int(self.b_firings_ste.get()), - mvc=float(self.mvc_value.get()), - ) - # Display results - self.display_results(self.exportable_df) - - except AttributeError: - tk.messagebox.showerror("Information", "Load file prior to computation.") - - except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid MVC or select a correct number of points." - ) - - except AssertionError: - tk.messagebox.showerror("Information", "Specify Event and/or Type.") - - except UnboundLocalError: - tk.messagebox.showerror("Information", "Select start/end area again.") - + # ----------------------------------------------------------------------------------------------- # Plot EMG From 5648f227c10ad95068e3837d046c8df6d1612c8b Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 31 Dec 2023 10:38:48 +0100 Subject: [PATCH 06/57] Added: GUI Plotting Module --- openhdemg/gui/gui_modules/__init__.py | 3 +- openhdemg/gui/gui_modules/analyse_force.py | 10 +- openhdemg/gui/gui_modules/edit_mus.py | 4 +- openhdemg/gui/gui_modules/edit_refsig.py | 6 +- openhdemg/gui/gui_modules/gui_plotting.py | 747 +++++++++++++++++++++ openhdemg/gui/gui_modules/mu_properties.py | 89 +-- openhdemg/gui/openhdemg_gui.py | 715 +------------------- 7 files changed, 808 insertions(+), 766 deletions(-) diff --git a/openhdemg/gui/gui_modules/__init__.py b/openhdemg/gui/gui_modules/__init__.py index 34fb173..d50a7b7 100644 --- a/openhdemg/gui/gui_modules/__init__.py +++ b/openhdemg/gui/gui_modules/__init__.py @@ -1,8 +1,9 @@ __all__ = ["edit_mus", "edit_refsig", "gui_helpers", "analyse_force", - "mu_properties"] + "mu_properties", "gui_plotting"] from openhdemg.gui.gui_modules.edit_mus import * from openhdemg.gui.gui_modules.edit_refsig import * from openhdemg.gui.gui_modules.gui_helpers import * from openhdemg.gui.gui_modules.analyse_force import * from openhdemg.gui.gui_modules.mu_properties import * +from openhdemg.gui.gui_modules.gui_plotting import * diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index 351f495..bd6c3a4 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -1,14 +1,11 @@ """Module containing the force analysis GUI""" -import os from tkinter import ttk, W, E, StringVar import customtkinter as ctk from CTkMessagebox import CTkMessagebox import pandas as pd import openhdemg.library as openhdemg - - class AnalyseForce: """ Class containing the force analysis window for openhdemg @@ -25,12 +22,7 @@ def __init__(self, parent): # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Force Analysis Window") - - # This after method is necessary due to a bug in customtktinter - # usually, the iconbitmap is overwritten after 200ms to a default one - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) + self.head.wm_iconbitmap() self.head.grab_set() # Get MVC diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index eccefef..2ef4aac 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -26,9 +26,7 @@ def __init__(self, parent, resdict): self.head = ctk.CTkToplevel(fg_color="LightBlue4") # Set the background color of the top-level window self.head.title("Motor Unit Removal Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) + self.head.wm_iconbitmap() self.head.grab_set() # Select Motor Unit diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index c731713..4848b14 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -1,7 +1,7 @@ """Module containing the Resif editing class""" import os -from tkinter import ttk, W, E, StringVar, DoubleVar, messagebox +from tkinter import ttk, W, E, StringVar, DoubleVar import customtkinter as ctk from CTkMessagebox import CTkMessagebox @@ -20,9 +20,7 @@ def __init__(self, parent): # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Reference Signal Editing Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) + self.head.wm_iconbitmap() self.head.grab_set() self.head.resizable(width=True, height=True) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 8b13789..b832fad 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -1 +1,748 @@ +"""This modules contains all plotting functionalities for the GUI""" +import os +import webbrowser +from tkinter import ttk, W, E, StringVar, PhotoImage +from PIL import Image + +import customtkinter as ctk +from CTkMessagebox import CTkMessagebox +import openhdemg.library as openhdemg + +class PlotEmg: + """ + Instance method to open "Plot Window". Options to create + several plots from the emgfile are displayed. + + Executed when button "Plot EMG" in master GUI window is pressed. + The plots are displayed in seperate windows. + """ + def __init__(self, parent): + + try: + self.parent = parent + self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head.title("Plot Window") + self.head.wm_iconbitmap() + self.head.grab_set() + + # define tk variables for later use + self.matrix_rc = StringVar() # Matrix rows columns + self.mat_label = ttk.Label() # Label for matriy rows columns + self.row_cols_entry = ttk.Entry() # Entry for matrix rows columns + + # Reference signal + ctk.CTkLabel(self.head, text="Reference signal", font=('Segoe UI',15, 'bold')).grid( + column=0, row=0, sticky=W + ) + self.ref_but = StringVar() + ref_button = ctk.CTkCheckBox( + self.head, + variable=self.ref_but, + bg_color="LightBlue4", + onvalue="True", + offvalue="False", + text="" + ) + ref_button.grid(column=1, row=0, sticky=(W)) + self.ref_but.set(False) + + # Time + ctk.CTkLabel(self.head, text="Time in seconds", font=('Segoe UI',15, 'bold')).grid(column=0, row=1, sticky=W) + self.time_sec = StringVar() + time_button = ctk.CTkCheckBox( + self.head, + variable=self.time_sec, + bg_color="LightBlue4", + onvalue="True", + offvalue="False", + text="" + ) + time_button.grid(column=1, row=1, sticky=W) + self.time_sec.set(False) + + # Figure Size + ctk.CTkLabel(self.head, text="Figure size in cm (h,w)", font=('Segoe UI',15, 'bold')).grid(column=0, row=2) + self.size_fig = StringVar() + fig_entry = ctk.CTkEntry(self.head, width=100, textvariable=self.size_fig) + self.size_fig.set("20,15") + fig_entry.grid(column=1, row=2, sticky=W) + + # Plot emgsig + plt_emgsig = ctk.CTkButton( + self.head, text="Plot EMGsig", command=self.plt_emgsignal, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plt_emgsig.grid(column=0, row=3, sticky=W) + + self.channels = StringVar() + channel_entry_values = ("0", "0,1,2", "0,1,2,3") + channel_entry = ctk.CTkComboBox( + self.head, width=150, variable=self.channels, + values=channel_entry_values + ) + channel_entry.grid(column=1, row=3, sticky=(W, E)) + self.channels.set("Channel Numbers") + + # Plot refsig + plt_refsig = ctk.CTkButton( + self.head, text="Plot RefSig", command=self.plt_refsignal, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plt_refsig.grid(column=0, row=4, sticky=W) + + # Plot motor unit pulses + plt_pulses = ctk.CTkButton( + self.head, text="Plot MUpulses", command=self.plt_mupulses, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plt_pulses.grid(column=0, row=5, sticky=W) + + # Define Linewidth for plot + self.linewidth = StringVar() + linewidth_entry_values = ("0.25", "0.5", "0.75", "1") + linewidth_entry = ctk.CTkComboBox( + self.head, width=15, variable=self.linewidth, + values=linewidth_entry_values + ) + linewidth_entry.grid(column=1, row=5, sticky=(W, E)) + self.linewidth.set("Linewidth") + + # Plot impulse train + plt_ipts_but = ctk.CTkButton(self.head, text="Plot Source", command=self.plt_ipts, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plt_ipts_but.grid(column=0, row=6, sticky=W) + + self.mu_numb = StringVar() + munumb_entry_values = ("0", "0,1,2", "0,1,2,3", "all") + munumb_entry = ctk.CTkComboBox(self.head, width=15, variable=self.mu_numb, + values=munumb_entry_values) + munumb_entry.grid(column=1, row=6, sticky=(W, E)) + self.mu_numb.set("MU Number") + + # Plot instantaneous discharge rate + plt_idr_but = ctk.CTkButton(self.head, text="Plot IDR", command=self.plt_idr, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plt_idr_but.grid(column=0, row=7, sticky=W) + + self.mu_numb_idr = StringVar() + munumb_entry_idr_values = ("0", "0,1,2", "0,1,2,3", "all") + munumb_entry_idr = ctk.CTkComboBox( + self.head, width=15, variable=self.mu_numb_idr, + values=munumb_entry_idr_values + ) + munumb_entry_idr.grid(column=1, row=7, sticky=(W, E)) + self.mu_numb_idr.set("MU Number") + + # This section containes the code for column 3++ + # Separator + ttk.Separator(self.head, orient="vertical").grid( + row=3, column=2, rowspan=6, ipady=120 + ) + + # Matrix code + ctk.CTkLabel(self.head, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid(row=0, column=3, sticky=(W)) + + self.mat_code = StringVar() + matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "None") + matrix_code = ctk.CTkComboBox(self.head, width=100, variable=self.mat_code, + values=matrix_code_values, state="readonly") + matrix_code.grid(row=0, column=4, sticky=(W, E)) + self.mat_code.set("GR08MM1305") + + # Trace matrix code value + self.mat_code.trace_add("write", self.on_matrix_none) + + # Matrix Orientation + ctk.CTkLabel(self.head, text="Orientation", font=('Segoe UI',15, 'bold')).grid(row=1, column=3, sticky=(W)) + self.mat_orientation = StringVar() + orientation_values = ("0", "180") + orientation = ctk.CTkComboBox( + self.head, width=15, variable=self.mat_orientation, + values=orientation_values, state="readonly" + ) + orientation.grid(row=1, column=4, sticky=(W, E)) + self.mat_orientation.set("180") + # Disable the orientation setting for DELSYS files + if self.parent.resdict["SOURCE"] == "DELSYS": + orientation.config(state="disabled") + + # Plot derivation + # Button + deriv_button = ctk.CTkButton( + self.head, text="Plot Derivation", command=self.plot_derivation, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + deriv_button.grid(row=3, column=3, sticky=W) + + # Combobox Config + self.deriv_config = StringVar() + configuration_values = ("Single differential", "Double differential") + configuration = ctk.CTkComboBox( + self.head, width=15, variable=self.deriv_config, + values=configuration_values, state="readonly" + ) + configuration.grid(row=3, column=4, sticky=(W, E)) + self.deriv_config.set("Configuration") + + # Combobox Matrix + self.deriv_matrix = StringVar() + mat_column_values = ("col0", "col1", "col2", "col3", "col4") + mat_column = ctk.CTkComboBox( + self.head, width=100, variable=self.deriv_matrix, + values=mat_column_values, state="readonly" + ) + mat_column.grid(row=3, column=5, sticky=(W, E)) + self.deriv_matrix.set("Matrix Column") + + # Motor unit action potential + # Button + muap_button = ctk.CTkButton( + self.head, text="Plot MUAPs", command=self.plot_muaps, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + muap_button.grid(row=4, column=3, sticky=W) + + # Combobox Config + self.muap_config = StringVar() + config_muap_values = ( + "Monopolar", + "Single differential", + "Double differential", + ) + config_muap = ctk.CTkComboBox( + self.head, width=15, variable=self.muap_config, + values=config_muap_values, state="readonly" + ) + config_muap.grid(row=4, column=4, sticky=(W, E)) + self.muap_config.set("Configuration") + # Disable config for DELSYS files + if self.parent.resdict["SOURCE"] == "DELSYS": + config_muap.config(state="disabled") + + # Combobox MU Number + self.muap_munum = StringVar() + mu_numbers = tuple(str(number) for number in range(0, self.parent.resdict["NUMBER_OF_MUS"])) + muap_munum = ctk.CTkComboBox(self.head, width=15, variable=self.muap_munum, + values=mu_numbers, state="readonly") + muap_munum.grid(row=4, column=5, sticky=(W, E)) + self.muap_munum.set("MU Number") + + # Combobox Timewindow + self.muap_time = StringVar() + timewindow_values = ("25", "50", "100", "200") + timewindow = ctk.CTkComboBox(self.head, width=120, variable=self.muap_time, + values=timewindow_values) + timewindow.grid(row=4, column=6, sticky=(W, E)) + self.muap_time.set("Timewindow (ms)") + # Disable Timewindow for DELSYS files + if self.parent.resdict["SOURCE"] == "DELSYS": + timewindow.config(state="disabled") + + # Matrix Illustration Graphic + matrix_canvas = ctk.CTkCanvas(self.head, height=150, width=600, bg="white") + matrix_canvas.grid(row=5, column=3, rowspan=5, columnspan=5) + parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.matrix = PhotoImage( + file= parent_dir + "/gui_files/Matrix.png" + ) + matrix_canvas.create_image(0, 0, anchor="nw", image=self.matrix) + # Information Button + self.info = ctk.CTkImage( + light_image=Image.open(parent_dir + "/gui_files/Info.png"), + size = (30, 30) + ) + info_button = ctk.CTkButton( + self.head, + image=self.info, + text="", + width=30, + height=30, + bg_color="LightBlue4", + fg_color="LightBlue4", + command=lambda: ( + ( + webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_basics/#plot-motor-units") + ), + ), + ) + info_button.grid(row=0, column=6, sticky=E) + + for child in self.head.winfo_children(): + child.grid_configure(padx=5, pady=5) + + except AttributeError: + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + self.head.destroy() + + ### Define functions for motor unit plotting + def on_matrix_none(self, *args): + """ + This function is called when the value of the mat_code variable is changed. + + When the variable is set to "None" it will create an Entrybox on the grid at column 1 and row 6, + and when the mat_code is set to something else it will remove the entrybox from the grid. + """ + if self.mat_code.get() == "None": + # Place label defined in init + self.mat_label = ttk.Label(self.head, text="Rows, Columns:") # Label for matriy rows columns + self.mat_label.grid(row=0, column=5, sticky=E) + + # Column entry only when specified matrix code + self.row_cols_entry = ttk.Entry(self.head, width=8, textvariable= self.matrix_rc) + self.row_cols_entry.grid(row=0, column=6, sticky = W, padx=5) + self.matrix_rc.set("13,5") + + else: + if hasattr(self, "row_cols_entry"): + self.row_cols_entry.grid_forget() + self.mat_label.grid_forget() + + + self.head.update_idletasks() + + + def plt_emgsignal(self): + """ + Instance method to plot the raw emg signal in an seperate plot window. + The channels selected by the user are plotted. The plot can be saved and + partly edited using the matplotlib options. + + Executed when button "Plot EMGsig" in Plot Window is pressed. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + ValueError + When entered channel number is not valid (inexistent). + KeyError + When entered channel number is out of bounds. + + See Also + -------- + plot_emgsignal in library. + """ + try: + # Create list of channels to be plotted + channels = self.channels.get() + # Create list of figsize + figsize = [int(i) for i in self.size_fig.get().split(",")] + + if len(channels) > 1: + chan_list = channels.split(",") + chan_list = [int(i) for i in chan_list] + + # Plot raw emg signal + openhdemg.plot_emgsig( + emgfile=self.parent.resdict, + channels=chan_list, + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + else: + # Plot raw emg signal + openhdemg.plot_emgsig( + emgfile=self.parent.resdict, + channels=int(channels), + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + except ValueError: + CTkMessagebox(title="Info", message="Enter valid channel number or non-negative figure size.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except KeyError: + CTkMessagebox(title="Info", message="Enter valid channel number.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except IndexError: + CTkMessagebox(title="Info", message="Enter valid figure size. Must be non negative and tuple of (heigth, width).", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def plt_refsignal(self): + """ + Instance method to plot the reference signal in an seperate plot window. + The plot can be saved and partly edited using the matplotlib options. + + Executed when button "Plot REFsig" in Plot Window is pressed. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + + See Also + -------- + plot_refsig in library. + """ + # Create list of figsize + figsize = [int(i) for i in self.size_fig.get().split(",")] + + # Plot reference signal + openhdemg.plot_refsig( + emgfile=self.parent.resdict, + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + def plt_mupulses(self): + """ + Instance method to plot the mu pulses in an seperate plot window. + The linewidth selected by the user is used. The plot can be saved and + partly edited using the matplotlib options. + + Executed when button "Plot MUpulses" in Plot Window is pressed. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + ValueError + When entered channel number is not valid (inexistent). + + See Also + -------- + plot_mupulses in library. + """ + try: + # Create list of figsize + figsize = [int(i) for i in self.size_fig.get().split(",")] + + # Plot motor unit pulses + openhdemg.plot_mupulses( + emgfile=self.parent.resdict, + linewidths=float(self.linewidth.get()), + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + except ValueError: + CTkMessagebox(title="Info", message="Enter valid linewidth number.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def plt_ipts(self): + """ + Instance method to plot the motor unit pulse train in an seperate plot window. + The motor units selected by the user are plotted. The plot can be saved and + partly edited using the matplotlib options. + + Executed when button "Plot Source" in Plot Window is pressed. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + ValueError + When entered motor unit number is not valid (inexistent). + KeyError + When entered motor number is out of bounds. + + See Also + -------- + plot_ipts in library. + """ + try: + # Create list contaning motor units to be plotted + mu_numb = self.mu_numb.get() + # Create list of figsize + figsize = [int(i) for i in self.size_fig.get().split(",")] + + if mu_numb == "all": + # Plot motor unit puls train in default + openhdemg.plot_ipts( + emgfile=self.parent.resdict, + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + elif len(mu_numb) > 2: + # Split at , + mu_list = mu_numb.split(",") + # Use comprehension to loop troug mu_list + mu_list = [int(i) for i in mu_list] + # Plot motor unit puls train in default + openhdemg.plot_ipts( + emgfile=self.parent.resdict, + munumber=mu_list, + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + else: + # Plot motor unit puls train in default + openhdemg.plot_ipts( + emgfile=self.parent.resdict, + munumber=int(mu_numb), + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + except ValueError: + CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except KeyError: + CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def plt_idr(self): + """ + Instance method to plot the instanteous discharge rate in an seperate plot window. + The motor units selected by the user are plotted. The plot can be saved and + partly edited using the matplotlib options. + + Executed when button "Plot IDR" in Plot Window is pressed. + + Raises + ------ + AttributeError + When no file is loaded prior to calculation. + ValueError + When entered channel number is not valid (inexistent). + KeyError + When entered channel number is out of bounds. + + See Also + -------- + plot_idr in library. + """ + try: + mu_idr = self.mu_numb_idr.get() + # Create list of figsize + figsize = [int(i) for i in self.size_fig.get().split(",")] + + if mu_idr == "all": + # Plot instanteous discharge rate + openhdemg.plot_idr( + emgfile=self.parent.resdict, + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + elif len(mu_idr) > 2: + mu_list_idr = mu_idr.split(",") + mu_list_idr = [int(mu) for mu in mu_list_idr] + # Plot instanteous discharge rate + openhdemg.plot_idr( + emgfile=self.parent.resdict, + munumber=mu_list_idr, + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + else: + # Plot instanteous discharge rate + openhdemg.plot_idr( + emgfile=self.parent.resdict, + munumber=int(mu_idr), + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + + except ValueError: + CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except KeyError: + CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def plot_derivation(self): + """ + Instance method to plot the differential derivation of the RAW_SIGNAL by matrix column. + + Both the single and the double differencials can be plotted. + This function is used to plot also the sorted RAW_SIGNAL. + """ + try: + if self.mat_code.get() == "None": + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.matrix_rc.get().split(",")] + + try: + # Sort emg file + sorted_file = openhdemg.sort_rawemg( + emgfile=self.parent.resdict, + code=self.mat_code.get(), + orientation=int(self.mat_orientation.get()), + n_rows=list_rcs[0], + n_cols=list_rcs[1] + ) + + except ValueError: + CTkMessagebox(title="Info", message="Number of specified rows and columns must match" + + "\nnumber of channels.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + return + + else: + # Sort emg file + sorted_file = openhdemg.sort_rawemg( + emgfile=self.parent.resdict, + code=self.mat_code.get(), + orientation=int(self.mat_orientation.get()), + ) + + # calcualte derivation + if self.deriv_config.get() == "Single differential": + diff_file = openhdemg.diff(sorted_rawemg=sorted_file) + + elif self.deriv_config.get() == "Double differential": + diff_file = openhdemg.double_diff(sorted_rawemg=sorted_file) + + # Create list of figsize + figsize = [int(i) for i in self.size_fig.get().split(",")] + + # Plot derivation + openhdemg.plot_differentials( + emgfile=self.parent.resdict, + differential=diff_file, + column=self.deriv_matrix.get(), + addrefsig=eval(self.ref_but.get()), + timeinseconds=eval(self.time_sec.get()), + figsize=figsize, + ) + except ValueError: + CTkMessagebox(title="Info", message="Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Rows, Columns arguments", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except UnboundLocalError: + CTkMessagebox(title="Info", message="Enter valid Configuration and Matrix Column.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except KeyError: + CTkMessagebox(title="Info", message="Enter valid Matrix Column.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def plot_muaps(self): + """ + Instance methos to plot motor unit action potenital obtained from STA from one or + multiple MUs. Except for DELSYS files, where the STA is not comupted. + + There is no limit to the number of MUs and STA files that can be overplotted. + ``Remember: the different STAs should be matched`` with same number of electrode, + processing (i.e., differential) and computed on the same timewindow. + """ + try: + # DELSYS requires different MUAPS plot + if self.parent.resdict["SOURCE"] == "DELSYS": + figsize = [int(i) for i in self.size_fig.get().split(",")] + muaps_dict = openhdemg.extract_delsys_muaps(self.parent.resdict) + openhdemg.plot_muaps(muaps_dict[int(self.muap_munum.get())], figsize=figsize) + + else: + if self.mat_code.get() == "None": + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.matrix_rc.get().split(",")] + + try: + # Sort emg file + sorted_file = openhdemg.sort_rawemg( + emgfile=self.parent.resdict, + code=self.mat_code.get(), + orientation=int(self.mat_orientation.get()), + n_rows=list_rcs[0], + n_cols=list_rcs[1] + ) + + except ValueError: + CTkMessagebox(title="Info", message="Number of specified rows and columns must match" + + "\nnumber of channels.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + return + + else: + # Sort emg file + sorted_file = openhdemg.sort_rawemg( + emgfile=self.parent.resdict, + code=self.mat_code.get(), + orientation=int(self.mat_orientation.get()), + ) + + # calcualte derivation + if self.muap_config.get() == "Single differential": + diff_file = openhdemg.diff(sorted_rawemg=sorted_file) + + elif self.muap_config.get() == "Double differential": + diff_file = openhdemg.double_diff(sorted_rawemg=sorted_file) + + elif self.muap_config.get() == "Monopolar": + diff_file = sorted_file + + # Calculate STA dictionary + # Plot deviation + sta_dict = openhdemg.sta( + emgfile=self.parent.resdict, + sorted_rawemg=diff_file, + firings="all", + timewindow=int(self.muap_time.get()), + ) + + # Create list of figsize + figsize = [int(i) for i in self.size_fig.get().split(",")] + + # Plot MUAPS + openhdemg.plot_muaps(sta_dict[int(self.muap_munum.get())], figsize=figsize) + + except ValueError: + CTkMessagebox(title="Info", message="Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Timewindow" + + "\n - MU Number" + + "\n - Rows, Columns arguments", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except UnboundLocalError: + CTkMessagebox(title="Info", message="Enter valid Configuration.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except KeyError: + CTkMessagebox(title="Info", message="Enter valid Matrix Column.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index 72958bf..de08365 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -1,14 +1,10 @@ """Module containing MU propterty analysis""" -import customtkinter as ctk -import os from tkinter import ttk, W, E, StringVar import customtkinter as ctk from CTkMessagebox import CTkMessagebox -import pandas as pd import openhdemg.library as openhdemg - class MuAnalysis: """ Instance method to open "Motor Unit Properties Window". Options to analyse motor @@ -22,9 +18,7 @@ def __init__(self, parent): self.parent = parent self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Motor Unit Properties Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) + self.head.wm_iconbitmap() self.head.grab_set() # MVC Entry @@ -148,23 +142,30 @@ def compute_mu_threshold(self): """ try: # Compute thresholds - self.mu_thresholds = openhdemg.compute_thresholds( - emgfile=self.resdict, + mu_thresholds = openhdemg.compute_thresholds( + emgfile=self.parent.resdict, event_=self.ct_event.get(), type_=self.ct_type.get(), mvc=float(self.mvc_value.get()), ) # Display results - self.display_results(self.mu_thresholds) + self.parent.display_results(mu_thresholds) except AttributeError: - tk.messagebox.showerror("Information", "Load file prior to computation.") - + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except ValueError: - tk.messagebox.showerror("Information", "Enter valid MVC.") - + CTkMessagebox(title="Info", message="Enter valid MVC.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except AssertionError: - tk.messagebox.showerror("Information", "Specify Event and/or Type.") + CTkMessagebox(title="Info", message="Specify Event and/or Type.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") def compute_mu_dr(self): """ @@ -189,26 +190,30 @@ def compute_mu_dr(self): """ try: # Compute discharge rates - self.mus_dr = openhdemg.compute_dr( - emgfile=self.resdict, + mus_dr = openhdemg.compute_dr( + emgfile=self.parent.resdict, n_firings_RecDerec=int(self.firings_rec.get()), n_firings_steady=int(self.firings_ste.get()), event_=self.dr_event.get(), ) # Display results - self.display_results(self.mus_dr) + self.parent.display_results(mus_dr) except AttributeError: - tk.messagebox.showerror("Information", "Load file prior to computation.") - + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid Firings value or select a correct number of points." - ) - + CTkMessagebox(title="Info", message="Enter valid Firings value or select a correct number of points.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except AssertionError: - tk.messagebox.showerror("Information", "Specify Event and/or Type.") + CTkMessagebox(title="Info", message="Specify Event and/or Type.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") def basic_mus_properties(self): """ @@ -235,26 +240,32 @@ def basic_mus_properties(self): """ try: # Calculate properties - self.exportable_df = openhdemg.basic_mus_properties( - emgfile=self.resdict, + exportable_df = openhdemg.basic_mus_properties( + emgfile=self.parent.resdict, n_firings_RecDerec=int(self.b_firings_rec.get()), n_firings_steady=int(self.b_firings_ste.get()), mvc=float(self.mvc_value.get()), ) # Display results - self.display_results(self.exportable_df) + self.parent.display_results(exportable_df) except AttributeError: - tk.messagebox.showerror("Information", "Load file prior to computation.") - + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid MVC or select a correct number of points." - ) - + CTkMessagebox(title="Info", message="Enter valid MVC value or select a correct number of points.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except AssertionError: - tk.messagebox.showerror("Information", "Specify Event and/or Type.") - + CTkMessagebox(title="Info", message="Specify Event and/or Type.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except UnboundLocalError: - tk.messagebox.showerror("Information", "Select start/end area again.") + CTkMessagebox(title="Info", message="Select start/end area again.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 72448fd..fe00f6d 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -22,7 +22,7 @@ matplotlib.use("TkAgg") import openhdemg.library as openhdemg -from openhdemg.gui.gui_modules import MURemovalWindow, EditRefsig, GUIHelpers, AnalyseForce, MuAnalysis +from openhdemg.gui.gui_modules import MURemovalWindow, EditRefsig, GUIHelpers, AnalyseForce, MuAnalysis, PlotEmg class emgGUI(): @@ -395,7 +395,7 @@ def __init__(self, master): signal_entry.grid(column=0, row=1, sticky=(W, E)) self.filetype.set("Type of file") # Trace filetype to apply function when changeing - self.filetype.trace("w", self.on_filetype_change) + self.filetype.trace_add("write", self.on_filetype_change) # Load file load = ttk.Button(self.left, text="Load File", command=self.get_file_input) @@ -463,7 +463,7 @@ def __init__(self, master): separator6.grid(column=0, columnspan=3, row=17, sticky=(W, E)) # Plot EMG - plots = ttk.Button(self.left, text="Plot EMG", command=self.plot_emg) + plots = ttk.Button(self.left, text="Plot EMG", command=lambda:(PlotEmg(parent=self))) plots.grid(column=0, row=16, sticky=W) separator7 = ttk.Separator(self.left, orient="horizontal") separator7.grid(column=0, columnspan=3, row=19, sticky=(W, E)) @@ -915,9 +915,6 @@ def on_filetype_change(self, *args): ) self.csv_entry.grid(column=0, row=2, sticky=(W, E), padx=5) - # TODO remove this and any reference to it - def decompose_file(self): - pass def save_emgfile(self): """ @@ -1190,7 +1187,7 @@ def open_advanced_tools(self): self.mat_code_adv.set("GR08MM1305") # Trace variabel for updating window - self.mat_code_adv.trace("w", self.on_matrix_none_adv) + self.mat_code_adv.trace_add("write", self.on_matrix_none_adv) # Analysis Button adv_button = ttk.Button( @@ -1309,713 +1306,11 @@ def sort_mus(self): except KeyError: tk.messagebox.showerror("Information", "Sorting not possible when ≤ 1" + "\nMU is present in the File (i.e. Refsigs)") - - - # ----------------------------------------------------------------------------------------------- - # Editing of Reference EMG Signal - - - # ----------------------------------------------------------------------------------------------- - # Resize EMG File - - - # ----------------------------------------------------------------------------------------------- - # Analysis of Force - - # ----------------------------------------------------------------------------------------------- - # Analysis of motor unit properties - # ----------------------------------------------------------------------------------------------- # Plot EMG - def plot_emg(self): - """ - Instance method to open "Plot Window". Options to create - several plots from the emgfile are displayed. - - Executed when button "Plot EMG" in master GUI window is pressed. - The plots are displayed in seperate windows. - """ - try: - # Create new window - self.head = tk.Toplevel(bg="LightBlue4") - self.head.title("Plot Window") - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.head.grab_set() - - # Reference signal - ttk.Label(self.head, text="Reference signal").grid( - column=0, row=0, sticky=W - ) - self.ref_but = StringVar() - ref_button = tk.Checkbutton( - self.head, - variable=self.ref_but, - bg="LightBlue4", - onvalue="True", - offvalue="False", - ) - ref_button.grid(column=1, row=0, sticky=(W)) - self.ref_but.set(False) - - # Time - ttk.Label(self.head, text="Time in seconds").grid(column=0, row=1, sticky=W) - self.time_sec = StringVar() - time_button = tk.Checkbutton( - self.head, - variable=self.time_sec, - bg="LightBlue4", - onvalue="True", - offvalue="False", - ) - time_button.grid(column=1, row=1, sticky=W) - self.time_sec.set(False) - - # Figure Size - ttk.Label(self.head, text="Figure size in cm (h,w)").grid(column=0, row=2) - self.size_fig = StringVar() - fig_entry = ttk.Entry(self.head, width=7, textvariable=self.size_fig) - self.size_fig.set("20,15") - fig_entry.grid(column=1, row=2, sticky=W) - - # Plot emgsig - plt_emgsig = ttk.Button( - self.head, text="Plot EMGsig", command=self.plt_emgsignal - ) - plt_emgsig.grid(column=0, row=3, sticky=W) - - self.channels = StringVar() - channel_entry = ttk.Combobox( - self.head, width=15, textvariable=self.channels - ) - channel_entry["values"] = ("0", "0,1,2", "0,1,2,3") - channel_entry.grid(column=1, row=3, sticky=(W, E)) - self.channels.set("Channel Numbers") - - # Plot refsig - plt_refsig = ttk.Button( - self.head, text="Plot RefSig", command=self.plt_refsignal - ) - plt_refsig.grid(column=0, row=4, sticky=W) - - # Plot motor unit pulses - plt_pulses = ttk.Button( - self.head, text="Plot MUpulses", command=self.plt_mupulses - ) - plt_pulses.grid(column=0, row=5, sticky=W) - - self.linewidth = StringVar() - linewidth_entry = ttk.Combobox( - self.head, width=15, textvariable=self.linewidth - ) - linewidth_entry["values"] = ("0.25", "0.5", "0.75", "1") - linewidth_entry.grid(column=1, row=5, sticky=(W, E)) - self.linewidth.set("Linewidth") - - # Plot impulse train - plt_ipts = ttk.Button(self.head, text="Plot Source", command=self.plt_ipts) - plt_ipts.grid(column=0, row=6, sticky=W) - - self.mu_numb = StringVar() - munumb_entry = ttk.Combobox(self.head, width=15, textvariable=self.mu_numb) - munumb_entry["values"] = ("0", "0,1,2", "0,1,2,3", "all") - munumb_entry.grid(column=1, row=6, sticky=(W, E)) - self.mu_numb.set("MU Number") - - # Plot instantaneous discharge rate - plt_idr = ttk.Button(self.head, text="Plot IDR", command=self.plt_idr) - plt_idr.grid(column=0, row=7, sticky=W) - - self.mu_numb_idr = StringVar() - munumb_entry_idr = ttk.Combobox( - self.head, width=15, textvariable=self.mu_numb_idr - ) - munumb_entry_idr["values"] = ("0", "0,1,2", "0,1,2,3", "all") - munumb_entry_idr.grid(column=1, row=7, sticky=(W, E)) - self.mu_numb_idr.set("MU Number") - - # This section containes the code for column 3++ - - # Separator - ttk.Separator(self.head, orient="vertical").grid( - row=3, column=2, rowspan=6, ipady=120 - ) - - # Matrix code - ttk.Label(self.head, text="Matrix Code").grid(row=0, column=3, sticky=(W)) - self.mat_code = StringVar() - matrix_code = ttk.Combobox(self.head, width=15, textvariable=self.mat_code) - matrix_code["values"] = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "None") - matrix_code["state"] = "readonly" - matrix_code.grid(row=0, column=4, sticky=(W, E)) - self.mat_code.set("GR08MM1305") - - # Trace matrix code value - self.mat_code.trace("w", self.on_matrix_none) - - # Matrix Orientation - ttk.Label(self.head, text="Orientation").grid(row=1, column=3, sticky=(W)) - self.mat_orientation = StringVar() - orientation = ttk.Combobox( - self.head, width=15, textvariable=self.mat_orientation - ) - orientation["values"] = ("0", "180") - orientation["state"] = "readonly" - orientation.grid(row=1, column=4, sticky=(W, E)) - self.mat_orientation.set("180") - if self.resdict["SOURCE"] == "DELSYS": - orientation.config(state="disabled") - - # Plot derivation - # Button - deriv_button = ttk.Button( - self.head, text="Plot Derivation", command=self.plot_derivation - ) - deriv_button.grid(row=3, column=3) - - # Combobox Config - self.deriv_config = StringVar() - configuration = ttk.Combobox( - self.head, width=15, textvariable=self.deriv_config - ) - configuration["values"] = ("Single differential", "Double differential") - configuration["state"] = "readonly" - configuration.grid(row=3, column=4, sticky=(W, E)) - self.deriv_config.set("Configuration") - - # Combobox Matrix - self.deriv_matrix = StringVar() - mat_column = ttk.Combobox( - self.head, width=15, textvariable=self.deriv_matrix - ) - mat_column["values"] = ("col0", "col1", "col2", "col3", "col4") - mat_column["state"] = "readonly" - mat_column.grid(row=3, column=5, sticky=(W, E)) - self.deriv_matrix.set("Matrix column") - - # Motor unit action potential - # Button - muap_button = ttk.Button( - self.head, text="Plot MUAPs", command=self.plot_muaps - ) - muap_button.grid(row=4, column=3) - - # Combobox Config - self.muap_config = StringVar() - config_muap = ttk.Combobox( - self.head, width=15, textvariable=self.muap_config - ) - config_muap["values"] = ( - "Monopolar", - "Single differential", - "Double differential", - ) - config_muap["state"] = "readonly" - config_muap.grid(row=4, column=4, sticky=(W, E)) - self.muap_config.set("Configuration") - # Disable config for DELSYS files - if self.resdict["SOURCE"] == "DELSYS": - config_muap.config(state="disabled") - - # Combobox MU Number - self.muap_munum = StringVar() - muap_munum = ttk.Combobox(self.head, width=15, textvariable=self.muap_munum) - mu_numbers = [*range(0, self.resdict["NUMBER_OF_MUS"])] - muap_munum["values"] = mu_numbers - muap_munum["state"] = "readonly" - muap_munum.grid(row=4, column=5, sticky=(W, E)) - self.muap_munum.set("MU Number") - - # Combobox Timewindow - self.muap_time = StringVar() - timewindow = ttk.Combobox(self.head, width=15, textvariable=self.muap_time) - timewindow["values"] = ("25", "50", "100", "200") - timewindow.grid(row=4, column=6, sticky=(W, E)) - self.muap_time.set("Timewindow (ms)") - if self.resdict["SOURCE"] == "DELSYS": - timewindow.config(state="disabled") - - # Matrix Illustration Graphic - matrix_canvas = Canvas(self.head, height=150, width=600, bg="white") - matrix_canvas.grid(row=5, column=3, rowspan=5, columnspan=5) - self.matrix = tk.PhotoImage( - file=os.path.dirname(os.path.abspath(__file__)) - + "/gui_files/Matrix.png" - ) - matrix_canvas.create_image(0, 0, anchor="nw", image=self.matrix) - - # Information Button - self.info = customtkinter.CTkImage( - light_image=Image.open(os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Info.png"), - size = (30, 30) - ) - info_button = customtkinter.CTkButton( - self.head, - image=self.info, - text="", - width=30, - height=30, - bg_color="LightBlue4", - fg_color="LightBlue4", - command=lambda: ( - ( - webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_basics/#plot-motor-units") - ), - ), - ) - info_button.grid(row=0, column=6, sticky=E) - - for child in self.head.winfo_children(): - child.grid_configure(padx=5, pady=5) - - except AttributeError: - tk.messagebox.showerror("Information", "Load file prior to computation.") - self.head.destroy() - - ### Define functions for motor unit plotting - - def on_matrix_none(self, *args): - """ - This function is called when the value of the mat_code variable is changed. - - When the variable is set to "None" it will create an Entrybox on the grid at column 1 and row 6, - and when the mat_code is set to something else it will remove the entrybox from the grid. - """ - if self.mat_code.get() == "None": - - self.mat_label = ttk.Label(self.head, text="Rows, Columns:") - self.mat_label.grid(row=0, column=5, sticky=E) - - self.matrix_rc = StringVar() - self.row_cols_entry = ttk.Entry(self.head, width=8, textvariable= self.matrix_rc) - self.row_cols_entry.grid(row=0, column=6, sticky = W, padx=5) - self.matrix_rc.set("13,5") - - else: - if hasattr(self, "row_cols_entry"): - self.row_cols_entry.grid_forget() - self.mat_label.grid_forget() - - - self.head.update_idletasks() - - - def plt_emgsignal(self): - """ - Instance method to plot the raw emg signal in an seperate plot window. - The channels selected by the user are plotted. The plot can be saved and - partly edited using the matplotlib options. - - Executed when button "Plot EMGsig" in Plot Window is pressed. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - ValueError - When entered channel number is not valid (inexistent). - KeyError - When entered channel number is out of bounds. - - See Also - -------- - plot_emgsignal in library. - """ - try: - # Create list of channels to be plotted - channels = self.channels.get() - # Create list of figsize - figsize = [int(i) for i in self.size_fig.get().split(",")] - - if len(channels) > 1: - chan_list = channels.split(",") - chan_list = [int(i) for i in chan_list] - - # Plot raw emg signal - openhdemg.plot_emgsig( - emgfile=self.resdict, - channels=chan_list, - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - else: - # Plot raw emg signal - openhdemg.plot_emgsig( - emgfile=self.resdict, - channels=int(channels), - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - except ValueError: - tk.messagebox.showerror("Information", "Enter valid channel number.") - - except KeyError: - tk.messagebox.showerror("Information", "Enter valid channel number.") - - def plt_refsignal(self): - """ - Instance method to plot the reference signal in an seperate plot window. - The plot can be saved and partly edited using the matplotlib options. - - Executed when button "Plot REFsig" in Plot Window is pressed. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - - See Also - -------- - plot_refsig in library. - """ - # Create list of figsize - figsize = [int(i) for i in self.size_fig.get().split(",")] - - # Plot reference signal - openhdemg.plot_refsig( - emgfile=self.resdict, - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - def plt_mupulses(self): - """ - Instance method to plot the mu pulses in an seperate plot window. - The linewidth selected by the user is used. The plot can be saved and - partly edited using the matplotlib options. - - Executed when button "Plot MUpulses" in Plot Window is pressed. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - ValueError - When entered channel number is not valid (inexistent). - - See Also - -------- - plot_mupulses in library. - """ - try: - # Create list of figsize - figsize = [int(i) for i in self.size_fig.get().split(",")] - - # Plot motor unig pulses - openhdemg.plot_mupulses( - emgfile=self.resdict, - linewidths=float(self.linewidth.get()), - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - except ValueError: - tk.messagebox.showerror("Information", "Enter valid linewidth number.") - - def plt_ipts(self): - """ - Instance method to plot the motor unit pulse train in an seperate plot window. - The motor units selected by the user are plotted. The plot can be saved and - partly edited using the matplotlib options. - - Executed when button "Plot Source" in Plot Window is pressed. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - ValueError - When entered motor unit number is not valid (inexistent). - KeyError - When entered motor number is out of bounds. - - See Also - -------- - plot_ipts in library. - """ - try: - # Create list contaning motor units to be plotted - mu_numb = self.mu_numb.get() - # Create list of figsize - figsize = [int(i) for i in self.size_fig.get().split(",")] - - if mu_numb == "all": - # Plot motor unit puls train in default - openhdemg.plot_ipts( - emgfile=self.resdict, - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - elif len(mu_numb) > 2: - # Split at , - mu_list = mu_numb.split(",") - # Use comprehension to loop troug mu_list - mu_list = [int(i) for i in mu_list] - # Plot motor unit puls train in default - openhdemg.plot_ipts( - emgfile=self.resdict, - munumber=mu_list, - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - else: - # Plot motor unit puls train in default - openhdemg.plot_ipts( - emgfile=self.resdict, - munumber=int(mu_numb), - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - except ValueError: - tk.messagebox.showerror("Information", "Enter valid motor unit number.") - - except KeyError: - tk.messagebox.showerror("Information", "Enter valid motor unit number.") - - def plt_idr(self): - """ - Instance method to plot the instanteous discharge rate in an seperate plot window. - The motor units selected by the user are plotted. The plot can be saved and - partly edited using the matplotlib options. - - Executed when button "Plot IDR" in Plot Window is pressed. - - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - ValueError - When entered channel number is not valid (inexistent). - KeyError - When entered channel number is out of bounds. - - See Also - -------- - plot_idr in library. - """ - try: - mu_idr = self.mu_numb_idr.get() - # Create list of figsize - figsize = [int(i) for i in self.size_fig.get().split(",")] - - if mu_idr == "all": - # Plot instanteous discharge rate - openhdemg.plot_idr( - emgfile=self.resdict, - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - elif len(mu_idr) > 2: - mu_list_idr = mu_idr.split(",") - mu_list_idr = [int(mu) for mu in mu_list_idr] - # Plot instanteous discharge rate - openhdemg.plot_idr( - emgfile=self.resdict, - munumber=mu_list_idr, - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - else: - # Plot instanteous discharge rate - openhdemg.plot_idr( - emgfile=self.resdict, - munumber=int(mu_idr), - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - - except ValueError: - tk.messagebox.showerror("Information", "Enter valid motor unit number.") - - except KeyError: - tk.messagebox.showerror("Information", "Enter valid motor unit number.") - - def plot_derivation(self): - """ - Instance method to plot the differential derivation of the RAW_SIGNAL by matrix column. - - Both the single and the double differencials can be plotted. - This function is used to plot also the sorted RAW_SIGNAL. - """ - try: - if self.mat_code.get() == "None": - # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.matrix_rc.get().split(",")] - - try: - # Sort emg file - sorted_file = openhdemg.sort_rawemg( - emgfile=self.resdict, - code=self.mat_code.get(), - orientation=int(self.mat_orientation.get()), - n_rows=list_rcs[0], - n_cols=list_rcs[1] - ) - - except ValueError: - tk.messagebox.showerror( - "Information", - "Number of specified rows and columns must match" + - "\nnumber of channels." - ) - return - - else: - # Sort emg file - sorted_file = openhdemg.sort_rawemg( - emgfile=self.resdict, - code=self.mat_code.get(), - orientation=int(self.mat_orientation.get()), - ) - - # calcualte derivation - if self.deriv_config.get() == "Single differential": - diff_file = openhdemg.diff(sorted_rawemg=sorted_file) - - elif self.deriv_config.get() == "Double differential": - diff_file = openhdemg.double_diff(sorted_rawemg=sorted_file) - - # Create list of figsize - figsize = [int(i) for i in self.size_fig.get().split(",")] - - # Plot derivation - openhdemg.plot_differentials( - emgfile=self.resdict, - differential=diff_file, - column=self.deriv_matrix.get(), - addrefsig=eval(self.ref_but.get()), - timeinseconds=eval(self.time_sec.get()), - figsize=figsize, - ) - except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Rows, Columns arguments", - ) - except UnboundLocalError: - tk.messagebox.showerror( - "Information", "Enter valid Configuration and Matrix Column." - ) - - except KeyError: - tk.messagebox.showerror("Information", "Enter valid Matrix Column.") - - def plot_muaps(self): - """ - Instance methos to plot motor unit action potenital obtained from STA from one or - multiple MUs. Except for DELSYS files, where the STA is not comupted. - - There is no limit to the number of MUs and STA files that can be overplotted. - ``Remember: the different STAs should be matched`` with same number of electrode, - processing (i.e., differential) and computed on the same timewindow. - """ - try: - # DELSYS requires different MUAPS plot - if self.resdict["SOURCE"] == "DELSYS": - figsize = [int(i) for i in self.size_fig.get().split(",")] - muaps_dict = openhdemg.extract_delsys_muaps(self.resdict) - openhdemg.plot_muaps(muaps_dict[int(self.muap_munum.get())], figsize=figsize) - - else: - if self.mat_code.get() == "None": - # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.matrix_rc.get().split(",")] - - try: - # Sort emg file - sorted_file = openhdemg.sort_rawemg( - emgfile=self.resdict, - code=self.mat_code.get(), - orientation=int(self.mat_orientation.get()), - n_rows=list_rcs[0], - n_cols=list_rcs[1] - ) - - except ValueError: - tk.messagebox.showerror( - "Information", - "Number of specified rows and columns must match" - + "\nnumber of channels." - ) - return - - else: - # Sort emg file - sorted_file = openhdemg.sort_rawemg( - emgfile=self.resdict, - code=self.mat_code.get(), - orientation=int(self.mat_orientation.get()), - ) - - # calcualte derivation - if self.muap_config.get() == "Single differential": - diff_file = openhdemg.diff(sorted_rawemg=sorted_file) - - elif self.muap_config.get() == "Double differential": - diff_file = openhdemg.double_diff(sorted_rawemg=sorted_file) - - elif self.muap_config.get() == "Monopolar": - diff_file = sorted_file - - # Calculate STA dictionary - # Plot deviation - sta_dict = openhdemg.sta( - emgfile=self.resdict, - sorted_rawemg=diff_file, - firings="all", - timewindow=int(self.muap_time.get()), - ) - - # Create list of figsize - figsize = [int(i) for i in self.size_fig.get().split(",")] - - # Plot MUAPS - openhdemg.plot_muaps(sta_dict[int(self.muap_munum.get())], figsize=figsize) - - except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Timewindow" - + "\n - MU Number" - + "\n - Rows, Columns arguments", - ) - - except UnboundLocalError: - tk.messagebox.showerror("Information", "Enter valid Configuration.") - - except KeyError: - tk.messagebox.showerror("Information", "Enter valid Matrix Column.") - # ----------------------------------------------------------------------------------------------- # Advanced Analysis @@ -2050,7 +1345,7 @@ def advanced_analysis(self): signal_entry["state"] = "readonly" signal_entry.grid(column=0, row=1, sticky=(W, E)) self.filetype_adv.set("Type of file") - self.filetype_adv.trace("w", self.on_filetype_change_adv) + self.filetype_adv.trace_add("write", self.on_filetype_change_adv) # Load file load1 = ttk.Button(self.head, text="Load File 1", command=self.open_emgfile1) From b94fe6e58b2e106fab49741cde2f9a42213f957d Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 31 Dec 2023 11:58:00 +0100 Subject: [PATCH 07/57] Fixed: Docstrings --- openhdemg/gui/gui_modules/analyse_force.py | 61 ++++++++- openhdemg/gui/gui_modules/edit_mus.py | 68 +++++++++- openhdemg/gui/gui_modules/edit_refsig.py | 80 +++++++++++- openhdemg/gui/gui_modules/gui_helpers.py | 53 +++++++- openhdemg/gui/gui_modules/gui_plotting.py | 144 ++++++++++++++++++--- openhdemg/gui/gui_modules/mu_properties.py | 81 +++++++++++- openhdemg/gui/openhdemg_gui.py | 6 - 7 files changed, 447 insertions(+), 46 deletions(-) diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index bd6c3a4..1308836 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -8,18 +8,67 @@ class AnalyseForce: """ - Class containing the force analysis window for openhdemg + A class for conducting force analysis in an openhdemg GUI application. + + This class provides a window for analyzing force signals. It includes functionalities + for calculating Maximum Voluntary Contraction (MVC) and Rate of Force Development (RFD). + The class is activated through the "Analyse Force" button in the main GUI window. + + Attributes + ---------- + parent : object + The parent widget, typically the main application window, to which this AnalyseForce + instance belongs. + head : CTkToplevel + The top-level widget for the Force Analysis window. + rfdms : StringVar + Tkinter StringVar to store the milliseconds value for Rate of Force Development (RFD) analysis. + + Methods + ------- + __init__(self, parent) + Initialize a new instance of the AnalyseForce class. + get_mvc(self) + Calculate and display the Maximum Voluntary Contraction (MVC). + get_rfd(self) + Calculate and display the Rate of Force Development (RFD) over specified milliseconds. - Instance method to open "Force analysis Window". - Options to analyse force singal are displayed. - - Executed when "Analyse Force" button in master GUI window is pressed. + Examples + -------- + >>> main_window = Tk() + >>> force_analysis = AnalyseForce(main_window) + >>> force_analysis.head.mainloop() + + Notes + ----- + The class is designed to be a part of a larger GUI application and interacts with force + signal data accessible via the `parent` widget. """ def __init__(self, parent): - self.parent = parent + """ + Initialize a new instance of the AnalyseForce class. + This method sets up the GUI components for the Force Analysis Window. It includes buttons + for calculating MVC and RFD, and an entry field for specifying RFD milliseconds. The method + configures and places various widgets such as labels, buttons, and entry fields in a grid + layout for user interaction. + + Parameters + ---------- + parent : object + The parent widget, typically the main application window, to which this AnalyseForce + instance belongs. The parent is used for accessing shared resources and data. + + Raises + ------ + AttributeError + If certain widgets or properties are not properly instantiated due to missing + parent configurations or resources. + + """ # Create new window + self.parent = parent self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Force Analysis Window") self.head.wm_iconbitmap() diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index 2ef4aac..4039c31 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -8,18 +8,76 @@ class MURemovalWindow: """ - Instance method to open "Motor Unit Removal Window". Further option to select and - remove MUs are displayed. - - Executed when button "Remove MUs" in master GUI window is pressed. + A class for managing the removal of motor units (MUs) in a GUI application. + + This class creates a window that offers options to select and remove specific MUs. + It is activated from the main GUI window and is intended to provide functionalities + for manipulating motor unit data. The class raises an AttributeError if it is instantiated + without a loaded file for analysis. + + Attributes + ---------- + parent : object + The parent widget, typically the main application window, to which this MURemovalWindow + instance belongs. + resdict : dict + A dictionary containing relevant data and settings, including the number of MUs. + head : CTkToplevel + The top-level widget for the Motor Unit Removal window. + mu_to_remove : StringVar + Tkinter StringVar to store the ID of the motor unit selected for removal. + + Methods + ------- + __init__(self, parent, resdict) + Initialize a new instance of the MURemovalWindow class. + remove(self) + Remove the selected motor unit from the analysis. + remove_empty(self) + Remove all motor units that are empty or have no data. + + Examples + -------- + >>> main_window = Tk() + >>> resdict = {"NUMBER_OF_MUS": 10} # Example resdict + >>> mu_removal_window = MURemovalWindow(main_window, resdict) + >>> mu_removal_window.head.mainloop() Raises ------ AttributeError When no file is loaded prior to analysis. - """ + Notes + ----- + The class is designed to interact with the data structure provided by the `resdict` + attribute, which is expected to contain specific keys and values relevant to the MU analysis. + + """ def __init__(self, parent, resdict): + """ + Initialize a new instance of the MURemovalWindow class. + + This method sets up the GUI components for the Motor Unit Removal Window. It includes + a dropdown menu to select a motor unit (MU) for removal and buttons to remove either + the selected MU or all empty MUs. The method configures and places various widgets such + as labels, comboboxes, and buttons in a grid layout for user interaction. + + Parameters + ---------- + parent : object + The parent widget, typically the main application window, to which this MURemovalWindow + instance belongs. The parent is used for accessing shared resources and data. + resdict : dict + A dictionary containing relevant data and settings for the motor unit analysis, + including the number of MUs. + + Raises + ------ + AttributeError + If certain widgets or properties are not properly instantiated due to missing + parent configurations or resources. + """ self.parent = parent self.resdict = resdict # Create new window diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index 4848b14..2edf599 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -1,6 +1,5 @@ """Module containing the Resif editing class""" -import os from tkinter import ttk, W, E, StringVar, DoubleVar import customtkinter as ctk from CTkMessagebox import CTkMessagebox @@ -9,16 +8,85 @@ class EditRefsig: """ - Instance method to open "Reference Signal Editing Window". Options for - refsig filtering and offset removal are displayed. + A class to manage editing of the reference signal in a GUI application. + + This class creates a window that offers various options for editing the reference signal. + It includes functionalities for filtering the signal, removing offset, converting the signal, + and transforming it to a percentage value. The class is instantiated when the "RefSig Editing" + button in the master GUI window is pressed. + + Attributes + ---------- + parent : object + The parent widget, typically the main application window, to which this EditRefsig + instance belongs. + head : CTkToplevel + The top-level widget for the Reference Signal Editing window. + filter_order : StringVar + Tkinter StringVar to store the filter order for reference signal filtering. + cutoff_freq : StringVar + Tkinter StringVar to store the cutoff frequency for reference signal filtering. + offsetval : StringVar + Tkinter StringVar to store the offset value to be removed from the reference signal. + auto_eval : StringVar + Tkinter StringVar to store the value for automatic offset evaluation. + convert : StringVar + Tkinter StringVar to store the operation (Multiply/Divide) for reference signal conversion. + convert_factor : DoubleVar + Tkinter DoubleVar to store the factor for reference signal conversion. + mvc_value : DoubleVar + Tkinter DoubleVar to store the MVC (Maximum Voluntary Contraction) value for percentage conversion. + + Methods + ------- + __init__(self, parent) + Initialize a new instance of the EditRefsig class. + filter_refsig(self) + Apply filtering to the reference signal based on the specified order and cutoff frequency. + remove_offset(self) + Remove or adjust the offset of the reference signal based on the specified value or automatic evaluation. + convert_refsig(self) + Convert the reference signal using the specified operation (Multiply/Divide) and factor. + to_percent(self) + Convert the reference signal to a percentage value based on the specified MVC value. + + Examples + -------- + >>> main_window = Tk() + >>> edit_refsig = EditRefsig(main_window) + >>> edit_refsig.head.mainloop() + + Notes + ----- + This class relies on the `ctk` and `ttk` modules from the `tkinter` library. The class is designed + to be instantiated from within a larger GUI application and operates on the reference signal data + that is accessible via the `parent` widget. - Executed when button "RefSig Editing" in master GUI window is pressed. """ def __init__(self, parent): - self.parent = parent - + """ + Initialize a new instance of the EditRefsig class. + + This method sets up the GUI components for the Reference Signal Editing Window. It includes + controls for filtering the reference signal, removing its offset, converting it, and + transforming it to a percentage value. The method configures and places various widgets + such as labels, entries, buttons, and combo boxes in a grid layout for user interaction. + + Parameters + ---------- + parent : object + The parent widget, typically the main application window, to which this EditRefsig + instance belongs. The parent is used for accessing shared resources and data. + + Raises + ------ + AttributeError + If certain widgets or properties are not properly instantiated due to missing + parent configurations or resources. + """ # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.parent = parent self.head.title("Reference Signal Editing Window") self.head.wm_iconbitmap() self.head.grab_set() diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index e49bbc5..2c99096 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -8,9 +8,60 @@ class GUIHelpers: """ + A utility class to provide additional functionalities in an openhdemg GUI application. + + This class includes helper functions to enhance user interaction with the application, + such as resizing files based on user input. It is designed to work in conjunction with + the main GUI components and relies on the parent widget for accessing shared data and resources. + + Attributes + ---------- + parent : object + The parent widget, typically the main application window, to which this GUIHelpers + instance belongs. + + Methods + ------- + __init__(self, parent) + Initialize a new instance of the GUIHelpers class. + resize_file(self) + Resize the EMG file based on user-defined areas selected in the GUI plot. + + Examples + -------- + >>> main_window = Tk() + >>> gui_helpers = GUIHelpers(main_window) + # Usage of gui_helpers methods as required + + Raises + ------ + AttributeError + When no file is loaded prior to analysis or certain operations are attempted + without the necessary context or data. + + Notes + ----- + The class's methods interact with other components of the openhdemg application, + such as file handling and plotting utilities, and are dependent on the state of the + parent widget. + """ + def __init__(self, parent): - self.parent=parent + """ + Initialize a new instance of the GUIHelpers class. + + Sets up a reference to the parent widget, which is used for accessing shared + resources and functionalities within the application. + + Parameters + ---------- + parent : object + The parent widget, usually the main application window, which provides + necessary context and data for the helper functions. + + """ + self.parent = parent def resize_file(self): """ diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index b832fad..18afa36 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -11,14 +11,107 @@ class PlotEmg: """ - Instance method to open "Plot Window". Options to create - several plots from the emgfile are displayed. + A class to manage and display EMG (Electromyography) signal plots in a GUI application. + + This class creates a plotting window with various options to create and display plots + from an EMG file. It provides functionalities to plot EMG signals, reference signals, + motor unit pulses, and more, with customizable settings. + + Attributes + ---------- + parent : object + The parent widget, typically the main application window that this PlotEmg instance belongs to. + head : CTkToplevel + The top-level widget for the plotting window. + matrix_rc : StringVar + Tkinter StringVar for storing matrix rows and columns information. + mat_label : ttk.Label + Label widget for displaying matrix rows and columns information. + row_cols_entry : ttk.Entry + Entry widget for inputting matrix rows and columns. + ref_but : StringVar + Variable to track the state of the reference signal checkbox. + time_sec : StringVar + Variable to track the time selection for the plot. + size_fig : StringVar + Variable to store the specified figure size. + channels : StringVar + Variable to store the selected EMG channels. + linewidth : StringVar + Variable to store the selected line width for plotting. + mu_numb : StringVar + Variable to store the selected motor unit number. + mat_code : StringVar + Variable to store the selected matrix code. + mat_orientation : StringVar + Variable to store the orientation of the matrix. + deriv_config : StringVar + Variable to store the configuration for the derivation plot. + deriv_matrix : StringVar + Variable to store the selected matrix column for derivation. + muap_config : StringVar + Variable to store the configuration for the MUAP plot. + muap_munum : StringVar + Variable to store the selected motor unit number for MUAP plot. + muap_time : StringVar + Variable to store the time window for MUAP plot. + matrix : PhotoImage + Image variable to store and display the matrix illustration graphic. + info : CTkImage + Image variable for the information button. + + Methods + ------- + __init__(self, parent) + Initialize a new instance of the PlotEmg class. + plt_emgsignal(self) + Plot the EMG signal based on the current settings. + plt_refsignal(self) + Plot the reference signal. + plt_mupulses(self) + Plot motor unit pulses. + plt_ipts(self) + Plot the impulse train. + plt_idr(self) + Plot the instantaneous discharge rate. + plot_derivation(self) + Plot the derivation based on the selected configuration. + plot_muaps(self) + Plot the motor unit action potentials. + on_matrix_none(self, *args) + Handle changes in the matrix code selection. + + Examples + -------- + >>> main_window = Tk() + >>> plot_emg = PlotEmg(main_window) + >>> plot_emg.head.mainloop() + + Notes + ----- + This class is dependent on the `ctk` and `ttk` modules from the `tkinter` library. + Some attributes and methods are conditional based on the `parent`'s properties. - Executed when button "Plot EMG" in master GUI window is pressed. - The plots are displayed in seperate windows. """ def __init__(self, parent): + """ + Initialize a new instance of the PlotEmg class. + + This method sets up the GUI components of the PlotEmg window, including labels, + entries, checkboxes, and buttons for various plot settings and options. + + Parameters + ---------- + parent : object + The parent widget, typically the main application window, to which this PlotEmg instance belongs. + + Raises + ------ + AttributeError + If certain widgets are not properly instantiated due to missing parent resdict. + """ + # Try is block is necessary as some widgets depend on parent resdict try: self.parent = parent self.head = ctk.CTkToplevel(fg_color="LightBlue4") @@ -310,13 +403,12 @@ def plt_emgsignal(self): Raises ------ - AttributeError - When no file is loaded prior to calculation. ValueError When entered channel number is not valid (inexistent). KeyError When entered channel number is out of bounds. - + IndexError + When negative figure size is specified. See Also -------- plot_emgsignal in library. @@ -373,11 +465,6 @@ def plt_refsignal(self): Executed when button "Plot REFsig" in Plot Window is pressed. - Raises - ------ - AttributeError - When no file is loaded prior to calculation. - See Also -------- plot_refsig in library. @@ -402,8 +489,6 @@ def plt_mupulses(self): Raises ------ - AttributeError - When no file is loaded prior to calculation. ValueError When entered channel number is not valid (inexistent). @@ -440,8 +525,6 @@ def plt_ipts(self): Raises ------ - AttributeError - When no file is loaded prior to calculation. ValueError When entered motor unit number is not valid (inexistent). KeyError @@ -512,8 +595,6 @@ def plt_idr(self): Raises ------ - AttributeError - When no file is loaded prior to calculation. ValueError When entered channel number is not valid (inexistent). KeyError @@ -577,6 +658,19 @@ def plot_derivation(self): Both the single and the double differencials can be plotted. This function is used to plot also the sorted RAW_SIGNAL. + + Raises + ------ + ValueError + When entered channel number is not valid (inexistent). + UnboundLocalError + When matrix configuration and / or column number are invalid. + KeyError + When entered channel number is out of bounds. + + See Also + -------- + plot_differentials in library. """ try: if self.mat_code.get() == "None": @@ -658,6 +752,20 @@ def plot_muaps(self): There is no limit to the number of MUs and STA files that can be overplotted. ``Remember: the different STAs should be matched`` with same number of electrode, processing (i.e., differential) and computed on the same timewindow. + + Raises + ------ + ValueError + When entered channel number is not valid (inexistent). + UnboundLocalError + When matrix configuration and / or column number are invalid. + KeyError + When entered channel number is out of bounds. + + See Also + -------- + plot_muaps in library. + """ try: # DELSYS requires different MUAPS plot diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index de08365..f0becbf 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -7,13 +7,86 @@ class MuAnalysis: """ - Instance method to open "Motor Unit Properties Window". Options to analyse motor - unit properties such as recruitement threshold, discharge rate or - basic properties computing are displayed. + A class for analyzing motor unit (MU) properties within a GUI application. + + This class creates a window for analyzing various MU properties such as recruitment + threshold, discharge rate, and other basic properties. It is activated from the main + GUI window and allows for input and computation of MU-related metrics. + + Attributes + ---------- + parent : object + The parent widget, typically the main application window that this MuAnalysis instance belongs to. + head : CTkToplevel + The top-level widget for the MU properties analysis window. + mvc_value : StringVar + Tkinter StringVar for storing the Maximum Voluntary Contraction (MVC) value. + ct_event : StringVar + Variable to store the chosen event type for computing MU thresholds. + ct_type : StringVar + Variable to store the type of computation (absolute, relative, or both) for MU thresholds. + firings_rec : StringVar + Variable to store the number of firings at recruitment. + firings_ste : StringVar + Variable to store the number of firings at the start/end of steady phase. + dr_event : StringVar + Variable to store the chosen event type for computing MU discharge rate. + b_firings_rec : StringVar + Variable to store the number of firings at recruitment for basic MU properties computation. + b_firings_ste : StringVar + Variable to store the number of firings at the start/end of steady phase for basic MU properties computation. + + Methods + ------- + __init__(self, parent) + Initialize a new instance of the MuAnalysis class. + compute_mu_threshold(self) + Compute the motor unit recruitment/derecruitment threshold. + compute_mu_dr(self) + Compute the motor unit discharge rate. + basic_mus_properties(self) + Compute basic motor unit properties. + + Examples + -------- + >>> main_window = Tk() + >>> mu_analysis = MuAnalysis(main_window) + >>> mu_analysis.head.mainloop() + + Notes + ----- + This class is dependent on the `ctk` and `ttk` modules from the `tkinter` library. + Some attributes and methods are conditional based on the `parent`'s properties. - Executed when button "MU Properties" button in master GUI window is pressed. """ def __init__(self, parent): + """ + Initialize a new instance of the MuAnalysis class. + + This method sets up the GUI components of the Motor Unit Properties window. It includes + input fields for MVC (Maximum Voluntary Contraction) value, buttons and dropdown menus + to compute MU thresholds, discharge rates, and basic MU properties. Each component is + configured and placed in the window grid. + + Parameters + ---------- + parent : object + The parent widget, typically the main application window, to which this MuAnalysis + instance belongs. The parent is used for accessing shared resources and data. + + Raises + ------ + AttributeError + If certain widgets or properties are not properly instantiated due to missing + parent configurations or resources. + + Notes + ----- + The creation of the GUI components involves setting up various Tkinter and custom widgets + (like CTkLabel, CTkEntry, CTkButton, CTkComboBox). Each widget is configured with specific + properties like size, color, and variable bindings and placed in a grid layout. + + """ # Create new window self.parent = parent self.head = ctk.CTkToplevel(fg_color="LightBlue4") diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index fe00f6d..8d836ed 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -1306,11 +1306,7 @@ def sort_mus(self): except KeyError: tk.messagebox.showerror("Information", "Sorting not possible when ≤ 1" + "\nMU is present in the File (i.e. Refsigs)") - - # ----------------------------------------------------------------------------------------------- - # Plot EMG - # ----------------------------------------------------------------------------------------------- # Advanced Analysis @@ -1318,7 +1314,6 @@ def advanced_analysis(self): """ Open top-level windows based on the selected advanced method. """ - if self.advanced_method.get() == "Motor Unit Tracking": head_title = "MUs Tracking Window" @@ -1326,7 +1321,6 @@ def advanced_analysis(self): head_title = "Conduction Velocity Window" else: head_title = "Duplicate Removal Window" - self.head = tk.Toplevel(bg="LightBlue4") self.head.title(head_title) From fdbd09ff6f13f881d92547746e83fb3abb9e5db2 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 31 Dec 2023 16:02:32 +0100 Subject: [PATCH 08/57] Added: Docstrigs and Helpers --- openhdemg/gui/gui_modules/analyse_force.py | 8 +- openhdemg/gui/gui_modules/gui_helpers.py | 102 ++++++++++++++++++++- openhdemg/gui/gui_modules/mu_properties.py | 12 +-- openhdemg/gui/openhdemg_gui.py | 99 +------------------- 4 files changed, 116 insertions(+), 105 deletions(-) diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index 1308836..663c38d 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -119,9 +119,9 @@ def get_mvc(self): mvc = openhdemg.get_mvc(emgfile=self.parent.resdict) # Define dictionary for pandas mvc_dic = {"MVC": [mvc]} - mvc_df = pd.DataFrame(data=mvc_dic) + self.parent.mvc_df = pd.DataFrame(data=mvc_dic) # Display results - self.parent.display_results(input_df=mvc_df) + self.parent.display_results(input_df=self.parent.mvc_df) except AttributeError: CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", @@ -153,9 +153,9 @@ def get_rfd(self): # Use comprehension to iterate through ms_list = [int(i) for i in ms_list] # Calculate rfd - rfd = openhdemg.compute_rfd(emgfile=self.parent.resdict, ms=ms_list) + self.parent.rfd = openhdemg.compute_rfd(emgfile=self.parent.resdict, ms=ms_list) # Display results - self.parent.display_results(input_df=rfd) + self.parent.display_results(input_df=self.parent.rfd) except AttributeError: CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 2c99096..0a590a7 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -1,8 +1,9 @@ """Module that contains all helper functions for the GUI""" -from tkinter import W, E +from tkinter import W, E, filedialog import customtkinter as ctk from CTkMessagebox import CTkMessagebox +import pandas as pd import openhdemg.library as openhdemg @@ -26,6 +27,10 @@ class GUIHelpers: Initialize a new instance of the GUIHelpers class. resize_file(self) Resize the EMG file based on user-defined areas selected in the GUI plot. + export_to_excel(self) + Save an analaysis dataframe to a csv file. + sort_mus(self) + Sort motor units according to recriuitement order. Examples -------- @@ -111,3 +116,98 @@ def resize_file(self): bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def export_to_excel(self): + """ + Instnace method to export any prior analysis results. Results are saved in an excel sheet + in a directory specified by the user. + + Executed when button "Save Results" in master GUI window is pressed. + + Raises + ------ + IndexError + When no analysis has been performed prior to attempted savig. + AttributeError + When no file was loaded in the GUI. + """ + try: + # Ask user to select the directory + path = filedialog.askdirectory() + + # Define excel writer + writer = pd.ExcelWriter(path + "/Results_" + self.parent.filename + ".xlsx") + + # Check for attributes and write sheets + if hasattr(self.parent, "mvc_df"): + self.parent.mvc_df.to_excel(writer, sheet_name="MVC") + + if hasattr(self.parent, "rfd"): + self.parent.rfd.to_excel(writer, sheet_name="RFD") + + if hasattr(self.parent, "exportable_df"): + self.parent.mu_prop_df.to_excel(writer, sheet_name="Basic MU Properties") + + if hasattr(self.parent, "mus_dr"): + self.parent.mus_dr.to_excel(writer, sheet_name="MU Discharge Rate") + + if hasattr(self.parent, "mu_thresholds"): + self.mu_thresholds.to_excel(writer, sheet_name="MU Thresholds") + + writer.close() + + except IndexError: + CTkMessagebox(title="Info", message="Please conduct at least one analysis before saving.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except AttributeError: + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except PermissionError: + CTkMessagebox(title="Info", message="If /Results.xlsx already opened, please close." + + "\nOtherwise ignore as you propably canceled the saving progress.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def sort_mus(self): + """ + Instance method to sort motor units ascending according to recruitement order. + + Executed when button "Sort MUs" in master GUI window is pressed. The plot of the MUs + and the emgfile are subsequently updated. + + Raises + ------ + AttributeError + When no file was loaded in the GUI. + + See Also + -------- + sort_mus in library. + """ + try: + # Sort emgfile + self.parent.resdict = openhdemg.sort_mus(emgfile=self.parent.resdict) + + # Update plot + if hasattr(self.parent, "fig"): + self.parent.in_gui_plotting(resdict=self.parent.resdict) + + except AttributeError: + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except KeyError: + CTkMessagebox(title="Info", message="Sorting not possible when ≤ 1" + + "\nMU is present in the File (i.e. Refsigs)", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index f0becbf..163feac 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -215,14 +215,14 @@ def compute_mu_threshold(self): """ try: # Compute thresholds - mu_thresholds = openhdemg.compute_thresholds( + self.parent.mu_thresholds = openhdemg.compute_thresholds( emgfile=self.parent.resdict, event_=self.ct_event.get(), type_=self.ct_type.get(), mvc=float(self.mvc_value.get()), ) # Display results - self.parent.display_results(mu_thresholds) + self.parent.display_results(self.parent.mu_thresholds) except AttributeError: CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", @@ -263,14 +263,14 @@ def compute_mu_dr(self): """ try: # Compute discharge rates - mus_dr = openhdemg.compute_dr( + self.parent.mus_dr = openhdemg.compute_dr( emgfile=self.parent.resdict, n_firings_RecDerec=int(self.firings_rec.get()), n_firings_steady=int(self.firings_ste.get()), event_=self.dr_event.get(), ) # Display results - self.parent.display_results(mus_dr) + self.parent.display_results(self.parent.mus_dr) except AttributeError: CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", @@ -313,14 +313,14 @@ def basic_mus_properties(self): """ try: # Calculate properties - exportable_df = openhdemg.basic_mus_properties( + self.parent.mu_prop_df = openhdemg.basic_mus_properties( emgfile=self.parent.resdict, n_firings_RecDerec=int(self.b_firings_rec.get()), n_firings_steady=int(self.b_firings_ste.get()), mvc=float(self.mvc_value.get()), ) # Display results - self.parent.display_results(exportable_df) + self.parent.display_results(self.parent.mu_prop_df) except AttributeError: CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 8d836ed..756dbaa 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -229,18 +229,12 @@ class emgGUI(): save_emgfile() Saves the edited emgfile dictionary to a .json file. Executed when button "Save File" in master GUI window pressed. - export_to_excel() - Saves the analysis results to a .xlsx file. - Executed when button "Save Results" in master GUI window pressed. - reset_analysis() + reset_analysis() Resets the whole analysis, restores the original input file and the graph. Executed when button "Reset analysis" in master GUI window pressed. in_gui_plotting() Method used for creating plot inside the GUI (on the GUI canvas). Executed when button "View MUs" in master GUI window pressed. - sort_mus() - Method used to sort motor units in Plot according to recruitement order. - Executed when button "Sort MUs" in master GUI window pressed. remove_mus() Opens seperate window to select motor units to be removed. Executed when button "Remove MUs" in master GUI window pressed. @@ -417,7 +411,7 @@ def __init__(self, master): # Export to Excel export = ttk.Button( - self.left, text="Save Results", command=self.export_to_excel + self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()) ) export.grid(column=1, row=6, sticky=(W, E)) @@ -426,7 +420,7 @@ def __init__(self, master): firings.grid(column=0, row=8, sticky=W) # Sort Motor Units - sorting = ttk.Button(self.left, text="Sort MUs", command=self.sort_mus) + sorting = ttk.Button(self.left, text="Sort MUs", command=lambda: (GUIHelpers(parent=self).sort_mus())) sorting.grid(column=1, row=8, sticky=(W, E)) separator2 = ttk.Separator(self.left, orient="horizontal") separator2.grid(column=0, columnspan=3, row=9, sticky=(W, E)) @@ -967,59 +961,7 @@ def save_file(): save_thread = threading.Thread(target=save_file) save_thread.start() - def export_to_excel(self): - """ - Instnace method to export any prior analysis results. Results are saved in an excel sheet - in a directory specified by the user. - - Executed when button "Save Results" in master GUI window is pressed. - - Raises - ------ - IndexError - When no analysis has been performed prior to attempted savig. - AttributeError - When no file was loaded in the GUI. - """ - try: - # Ask user to select the directory - path = filedialog.askdirectory() - - # Define excel writer - writer = pd.ExcelWriter(path + "/Results_" + self.filename + ".xlsx") - - # Check for attributes and write sheets - if hasattr(self, "mvc_df"): - self.mvc_df.to_excel(writer, sheet_name="MVC") - - if hasattr(self, "rfd"): - self.rfd.to_excel(writer, sheet_name="RFD") - - if hasattr(self, "exportable_df"): - self.exportable_df.to_excel(writer, sheet_name="Basic MU Properties") - - if hasattr(self, "mus_dr"): - self.mus_dr.to_excel(writer, sheet_name="MU Discharge Rate") - - if hasattr(self, "mu_thresholds"): - self.mu_thresholds.to_excel(writer, sheet_name="MU Thresholds") - - writer.close() - - except IndexError: - tk.messagebox.showerror( - "Information", "Please conduct at least one analysis before saving" - ) - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - except PermissionError: - tk.messagebox.showerror( - "Information", - "If /Results.xlsx already opened, please close." - + "\nOtherwise ignore as you propably canceled the saving progress.", - ) + def reset_analysis(self): """ @@ -1126,7 +1068,6 @@ def open_advanced_tools(self): "Information", "Advanced Tools for Delsys are only accessible from the library.", ) - # NOTE I would show an error message return # Open window @@ -1276,37 +1217,7 @@ def in_gui_plotting(self, resdict, plot="idr"): # ----------------------------------------------------------------------------------------------- # Sorting of motor units - def sort_mus(self): - """ - Instance method to sort motor units ascending according to recruitement order. - - Executed when button "Sort MUs" in master GUI window is pressed. The plot of the MUs - and the emgfile are subsequently updated. - - Raises - ------ - AttributeError - When no file was loaded in the GUI. - - See Also - -------- - sort_mus in library. - """ - try: - # Sort emgfile - self.resdict = openhdemg.sort_mus(emgfile=self.resdict) - - # Update plot - if hasattr(self, "fig"): - self.in_gui_plotting(resdict=self.resdict) - - except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - except KeyError: - tk.messagebox.showerror("Information", "Sorting not possible when ≤ 1" - + "\nMU is present in the File (i.e. Refsigs)") - + # ----------------------------------------------------------------------------------------------- # Advanced Analysis From 6d25338a3ddae0bdfcfee989c50c13538736182b Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:16:33 +0100 Subject: [PATCH 09/57] Fix website --- docs/cite-us.md | 2 +- docs/isek_jek_tutorials.md | 6 +++++- mkdocs.yml | 8 ++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/cite-us.md b/docs/cite-us.md index 0e3d83e..5416566 100644 --- a/docs/cite-us.md +++ b/docs/cite-us.md @@ -1,4 +1,4 @@ -If you use *openhdemg* for your reaserch, please cite our [tutorial article](/isek_jek_tutorials/#jek-tutorial-article). Any citation will help us to continue our work. +If you use *openhdemg* for your reaserch, please cite our [tutorial article](/isek_jek_tutorials#jek-tutorial-article). Any citation will help us to continue our work. Cite us as: diff --git a/docs/isek_jek_tutorials.md b/docs/isek_jek_tutorials.md index 975f057..c69dec6 100644 --- a/docs/isek_jek_tutorials.md +++ b/docs/isek_jek_tutorials.md @@ -18,7 +18,11 @@ You can download the sample files and the sample scripts [here](https://drive.go
- + + +

Your web browser doesn't have a PDF plugin. Instead, you can click here to download the PDF file.

+
+

diff --git a/mkdocs.yml b/mkdocs.yml index 8d818aa..8f7cea4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -108,8 +108,8 @@ markdown_extensions: - pymdownx.caret - pymdownx.details - pymdownx.emoji: - emoji_generator: !!python/name:materialx.emoji.to_svg - emoji_index: !!python/name:materialx.emoji.twemoji + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: anchor_linenums: true line_spans: __span @@ -118,8 +118,8 @@ markdown_extensions: - pymdownx.keys - pymdownx.magiclink: repo_url_shorthand: true - user: squidfunk - repo: mkdocs-material + user: GiacomoValliPhD + repo: openhdemg - pymdownx.mark - pymdownx.smartsymbols - pymdownx.superfences: From f9b5d6375b746b09d5858e9f4e6abf88bcb7f4dd Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 7 Jan 2024 20:35:08 +0100 Subject: [PATCH 10/57] Added: Advanced Analysis --- openhdemg/gui/gui_modules/__init__.py | 3 +- .../gui/gui_modules/advanced_analyses.py | 712 ++++++++++++++++++ openhdemg/gui/gui_modules/gui_helpers.py | 2 +- openhdemg/gui/openhdemg_gui.py | 593 +-------------- 4 files changed, 726 insertions(+), 584 deletions(-) create mode 100644 openhdemg/gui/gui_modules/advanced_analyses.py diff --git a/openhdemg/gui/gui_modules/__init__.py b/openhdemg/gui/gui_modules/__init__.py index d50a7b7..964c773 100644 --- a/openhdemg/gui/gui_modules/__init__.py +++ b/openhdemg/gui/gui_modules/__init__.py @@ -1,5 +1,5 @@ __all__ = ["edit_mus", "edit_refsig", "gui_helpers", "analyse_force", - "mu_properties", "gui_plotting"] + "mu_properties", "gui_plotting", "advanced_analyses"] from openhdemg.gui.gui_modules.edit_mus import * from openhdemg.gui.gui_modules.edit_refsig import * @@ -7,3 +7,4 @@ from openhdemg.gui.gui_modules.analyse_force import * from openhdemg.gui.gui_modules.mu_properties import * from openhdemg.gui.gui_modules.gui_plotting import * +from openhdemg.gui.gui_modules.advanced_analyses import * \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py new file mode 100644 index 0000000..704beab --- /dev/null +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -0,0 +1,712 @@ +"""Module containing the advanced analysis options""" + +from tkinter import ttk, W, E, N, S, StringVar, BooleanVar +import customtkinter as ctk +from CTkMessagebox import CTkMessagebox +from pandastable import Table +import openhdemg.library as openhdemg + +class AdvancedAnalysis: + """ + A class to manage advanced analysis tools in an openhdemg GUI application. + + This class provides a window for conducting advanced analyses on EMG data. + It allows users to select and utilize different analysis tools and set parameters + for these tools. The class supports functionalities like motor unit tracking, + duplicate removal, and conduction velocity analysis. + + Attributes + ---------- + parent : object + The parent widget, typically the main application window, to which this + AdvancedAnalysis instance belongs. + matrix_rc_adv : StringVar + Tkinter StringVar for storing matrix rows and columns information. + filetype_adv : StringVar + Tkinter StringVar for storing the file type information. + threshold_adv : StringVar + Tkinter StringVar for storing the threshold value for analysis. + time_window : StringVar + Tkinter StringVar for storing the time window for analysis. + exclude_thres : BooleanVar + Tkinter BooleanVar to indicate if a threshold is to be excluded. + filter_adv : BooleanVar + Tkinter BooleanVar to indicate if filtering is applied. + show_adv : BooleanVar + Tkinter BooleanVar to indicate if advanced results are to be shown. + which_adv : StringVar + Tkinter StringVar for specifying which advanced analysis to perform. + emgfile1 : dict + Dictionary to store the first EMG file's data. + emgfile2 : dict + Dictionary to store the second EMG file's data. + extension_factor_adv : StringVar + Tkinter StringVar for storing the extension factor for analysis. + + Methods + ------- + __init__(self, parent) + Initialize a new instance of the AdvancedAnalysis class. + advanced_analysis(self) + Perform the selected advanced analysis based on user-defined parameters. + on_matrix_none_adv(self, *args) + Callback function for handling changes in matrix code selection. + + Examples + -------- + >>> main_window = Tk() + >>> advanced_analysis = AdvancedAnalysis(main_window) + # Usage of advanced_analysis methods as required + + Notes + ----- + The class is designed to be a part of a larger GUI application and interacts with EMG + data and analysis tools. It depends on the state and data of the `parent` widget. + + """ + def __init__(self, parent): + """ + Initialize a new instance of the AdvancedAnalysis class. + + Sets up a window with various controls for performing advanced EMG data analyses. + The method configures and places widgets for tool selection, matrix orientation, + matrix code, and an analysis button in a grid layout. It also initializes + several Tkinter StringVars and BooleanVars for user inputs and settings. + + Parameters + ---------- + parent : object + The parent widget, usually the main application window, which provides + necessary context and data for the analysis functions. + + Raises + ------ + AttributeError + If certain operations are attempted without a loaded file or necessary data. + + """ + # Define attributes for later usage not defined in init + self.matrix_rc_adv = StringVar() + self.filetype_adv = StringVar() + self.threshold_adv = StringVar() + self.time_window = StringVar() + self.exclude_thres = BooleanVar() + self.filter_adv = BooleanVar() + self.show_adv = BooleanVar() + self.which_adv = StringVar() + self.emgfile1 = {} + self.emgfile2 = {} + self.extension_factor_adv = StringVar() + + # Set parent, most probable emgGUI + self.parent = parent + + # Disable config for DELSYS files + try: + if self.parent.resdict["SOURCE"] == "DELSYS": + CTkMessagebox(title="Info", message="Advanced Tools for Delsys are only accessible from the library.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + return + except AttributeError: + CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", + bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + return + + # Open window + self.a_window = ctk.CTkToplevel(fg_color="LightBlue4") + self.a_window.title("Advanced Tools Window") + self.a_window.wm_iconbitmap() + self.a_window.grab_set() + + # Add Label + ctk.CTkLabel( + self.a_window, text="Select tool and matrix:", font=('Segoe UI',15, 'bold'), + text_color="#000000", anchor="w" + ).grid(row=0, column=0) + + # Analysis Tool + ctk.CTkLabel(self.a_window, text="Analysis Tool", font=('Segoe UI',15, 'bold')).grid( + row=2, column=0, sticky=(W, E)) + + # Add Selection Combobox + adv_box_values = ( + "Motor Unit Tracking", + "Duplicate Removal", + "Conduction Velocity", + ) + self.advanced_method = StringVar() + adv_box = ctk.CTkComboBox( + self.a_window, width=170, variable=self.advanced_method, + values=adv_box_values, state="readonly") + adv_box.grid(row=2, column=1, sticky=(W, E)) + self.advanced_method.set("Motor Unit Tracking") + + # Matrix Orientation + ctk.CTkLabel(self.a_window, text="Matrix Orientation", font=('Segoe UI',15, 'bold')).grid( + row=3, column=0, sticky=(W, E)) + self.mat_orientation_adv = StringVar() + orientation = ctk.CTkComboBox( + self.a_window, width=100, variable=self.mat_orientation_adv, + values=("0", "180"), state="readonly") + orientation.grid(row=3, column=1, sticky=(W, E)) + self.mat_orientation_adv.set("180") + + # Matrix code + ctk.CTkLabel(self.a_window, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid( + row=4, column=0, sticky=(W, E)) + self.mat_code_adv = StringVar() + matrix_code_vals = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "None") + matrix_code = ctk.CTkComboBox( + self.a_window, width=150, variable=self.mat_code_adv, + values=matrix_code_vals, state="readonly") + matrix_code.grid(row=4, column=1, sticky=(W, E)) + self.mat_code_adv.set("GR08MM1305") + + # Trace variabel for updating window + self.mat_code_adv.trace_add("write", self.on_matrix_none_adv) + + # Analysis Button + adv_button = ctk.CTkButton( + self.a_window, + text="Advanced Analysis", + command=self.advanced_analysis, + fg_color="#000000", text_color="white", border_color="white", border_width=1) + adv_button.grid(column=0, row=7) + + # Add padding to widgets + for child in self.a_window.winfo_children(): + child.grid_configure(padx=5, pady=5) + + # ----------------------------------------------------------------------------------------------- + # Advanced Analysis functionalities + + def on_matrix_none_adv(self, *args): + """ + Handle changes in the matrix code selection in the AdvancedAnalysis GUI. + + This callback function is triggered when the `mat_code_adv` variable changes. + It dynamically updates the GUI to add or remove an entry box for specifying + matrix rows and columns. When 'None' is selected for the matrix code, it creates + an entry box for the user to input the rows and columns. Otherwise, it removes + this entry box. + + Parameters + ---------- + *args : tuple + The arguments passed to the callback function. Not used in the function but + required for compatibility with Tkinter's trace mechanism. + + Notes + ----- + The method is part of the AdvancedAnalysis class and interacts with the GUI elements + specific to advanced analysis options. It ensures that the GUI is responsive to user + selections and updates the interface accordingly. + + """ + # Necessary to distinguish between None and other + if self.mat_code_adv.get() == "None": + + # Set label for matrix rows and columns + mat_label_adv = ctk.CTkLabel(self.a_window, text="Rows, Columns:", font=('Segoe UI',15, 'bold')) + mat_label_adv.grid(row=5, column=1, sticky = W) + + row_cols_entry_adv = ctk.CTkEntry(self.a_window, width=8, textvariable= self.matrix_rc_adv) + row_cols_entry_adv.grid(row=6, column=1, sticky = W, padx=5, pady=2) + self.matrix_rc_adv.set("13,5") + + else: + if hasattr(self, "row_cols_entry_adv"): + row_cols_entry_adv.grid_forget() + mat_label_adv.grid_forget() + + # Update main advanced window based on selection + self.a_window.update_idletasks() + + def advanced_analysis(self): + """ + Open a top-level window based on the selected advanced analysis method. + + This method is responsible for generating different GUI windows depending on the + advanced analysis option chosen by the user. It dynamically creates GUI elements + like dropdowns, buttons, and checkboxes specific to the selected analysis tool, + such as 'Motor Unit Tracking', 'Conduction Velocity', or 'Duplicate Removal'. + + Raises + ------ + AttributeError + If a required file is not loaded prior to performing the analysis or if invalid + rows and columns arguments are entered for the 'Conduction Velocity' analysis. + + Notes + ----- + The method is part of the AdvancedAnalysis class and interacts with other GUI elements + and functionalities of the application. It ensures that the GUI adapts to the user's + choice of analysis, providing relevant options and settings for each analysis type. + """ + if self.advanced_method.get() == "Motor Unit Tracking": + head_title = "MUs Tracking Window" + elif self.advanced_method.get() == "Conduction Velocity": + head_title = "Conduction Velocity Window" + else: + head_title = "Duplicate Removal Window" + + self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head.title(head_title) + self.head.wm_iconbitmap() + self.head.grab_set() + + # Specify Signal + signal_value = ("OTB", "DEMUSE", "OPENHDEMG", "CUSTOMCSV") + signal_entry = ctk.CTkComboBox( + self.head, width=150, variable=self.filetype_adv, + values=signal_value, state="readonly") + signal_entry.grid(column=0, row=1, sticky=(W, E)) + self.filetype_adv.set("Type of file") + self.filetype_adv.trace_add("write", self.on_filetype_change_adv) + + # Load file + load1 = ctk.CTkButton(self.head, text="Load File 1", command=self.open_emgfile1, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + load1.grid(column=0, row=2, sticky=(W, E)) + + # Load file + load2 = ctk.CTkButton(self.head, text="Load File 2", command=self.open_emgfile2, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + load2.grid(column=0, row=3, sticky=(W, E)) + + # Threshold label + threshold_label = ctk.CTkLabel(self.head, text="Threshold:", font=('Segoe UI',15, 'bold')) + threshold_label.grid(column=0, row=9) + + # Combobox for threshold + threshold_combobox = ctk.CTkComboBox( + self.head, + values=("0.6", "0.7", "0.8", "0.9"), + variable=self.threshold_adv, + state="readonly", + width=100, + ) + threshold_combobox.grid(column=1, row=9) + self.threshold_adv.set("0.8") + + # Time Label + time_window_label = ctk.CTkLabel(self.head, text="Time window:", font=('Segoe UI',15, 'bold')) + time_window_label.grid(column=0, row=10) + + # Time Combobox + time_combobox = ctk.CTkComboBox( + self.head, + values=("25", "50"), + variable=self.time_window, + state="readonly", + width=100, + ) + time_combobox.grid(column=1, row=10) + self.time_window.set("25") + + # Exclude below threshold + exclude_label = ctk.CTkLabel(self.head, text="Exclude below threshold", font=('Segoe UI',15, 'bold')) + exclude_label.grid(column=0, row=11) + + # Add exclude checkbox + exclude_checkbox = ctk.CTkCheckBox( + self.head, variable=self.exclude_thres, bg_color="LightBlue4", + onvalue="True", offvalue="False", text="" + ) + exclude_checkbox.grid(column=1, row=11) + self.exclude_thres.set(True) + + # Filter + filter_label = ctk.CTkLabel(self.head, text="Filter", font=('Segoe UI',15, 'bold')) + filter_label.grid(column=0, row=12) + + # Add filter checkbox + filter_checkbox = ctk.CTkCheckBox( + self.head, variable=self.filter_adv, bg_color="LightBlue4", + onvalue="True", offvalue="False", text="" + ) + filter_checkbox.grid(column=1, row=12) + self.filter_adv.set(True) + + # Exclude below threshold + show_label = ctk.CTkLabel(self.head, text="Show", font=('Segoe UI',15, 'bold')) + show_label.grid(column=0, row=13) + + # Add exclude checkbox + show_checkbox = ctk.CTkCheckBox( + self.head, variable=self.show_adv, bg_color="LightBlue4", + onvalue="True", offvalue="False", text="" + ) + show_checkbox.grid(column=1, row=13) + + # Add button to execute MU tracking + track_button = ctk.CTkButton(self.head, text="Track", command=self.track_mus, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + track_button.grid(column=0, row=15, columnspan=2, sticky=(W, E)) + + # Add padding + for child in self.head.winfo_children(): + child.grid_configure(padx=5, pady=5) + + # Add Which widget and update the track button + # to match functionalities required for duplicate removal + if self.advanced_method.get() == "Duplicate Removal": + + # Add Which label + ctk.CTkLabel(self.head, text="Which", font=('Segoe UI',15, 'bold')).grid(column=0, row=14) + + # Combobox for Which option + which_combobox = ctk.CTkComboBox( + self.head, + values=["munumber", "accuracy"], + variable=self.which_adv, + state="readonly", + width=150, + ) + which_combobox.grid(row=14, column=1, padx=5, pady=5) + self.which_adv.set("munumber") + + # Add button to execute MU tracking + track_button.configure( + text="Remove Duplicates", command=self.remove_duplicates_between + ) + + if self.advanced_method.get() == "Conduction Velocity": + try: + # Destroy unnecessary pop-ups + self.head.destroy() + self.a_window.destroy() + + if self.mat_code_adv.get() == "None": + + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.matrix_rc_adv.get().split(",")] + + try: + # Sort emg file + sorted_rawemg = openhdemg.sort_rawemg( + self.parent.resdict, + code=self.mat_code_adv.get(), + orientation=int(self.mat_orientation_adv.get()), + n_rows=list_rcs[0], + n_cols=list_rcs[1] + ) + except ValueError: + CTkMessagebox(title="Info", message="Number of specified rows and columns must match" + "\nnumber of channels.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + return + # # DELSYS conduction velocity not available + # elif self.mat_code_adv.get() == "Trigno Galileo Sensor": + # tk.messagebox.showerror( + # "Information", + # "MUs conduction velocity estimation is not available for this matrix." + # ) + # return + + else: + # Sort emg file + sorted_rawemg = openhdemg.sort_rawemg( + self.parent.resdict, + code=self.mat_code_adv.get(), + orientation=int(self.mat_orientation_adv.get()), + ) + + openhdemg.MUcv_gui( + emgfile=self.parent.resdict, + sorted_rawemg=sorted_rawemg, + ) + + except AttributeError: + CTkMessagebox(title="Info", message="Please make sure to load a file prior to Conduction velocity calculation.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + self.head.destroy() + + except ValueError: + CTkMessagebox(title="Info", message="Please make sure to enter valid Rows, Columns arguments." + + "\nArguments must be non-negative and seperated by `,`.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + self.head.destroy() + + + # Destroy first window to avoid too many pop-ups + self.a_window.destroy() + + ### Define function for advanced analysis tools + def open_emgfile1(self): + """ + Open EMG file based on the selected file type and extension factor. + + This function is used to open and store the first emgfile that is + required for the MU tracking. As both files required are loaded by + different buttons, two functions storing the two files were created. + + See Also + -------- + open_emgfile1(), openhdemg.askopenfile() + """ + try: + # Open OTB file + if self.filetype_adv.get() == "OTB": + self.emgfile1 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + otb_ext_factor=int(self.extension_factor_adv.get()), + ) + # Open all other filetypes + else: + self.emgfile1 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + ) + + # Add filename to GUI + ctk.CTkLabel(self.head, text="File 1 loaded", font=('Segoe UI',15, 'bold')).grid(column=1, row=2) + + except ValueError: + CTkMessagebox(title="Info", message="Make sure to specify a valid filetype or extension factor.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def open_emgfile2(self): + """ + Open EMG file based on the selected file type and extension factor. + + This function is used to open and store the first emgfile that is + required for the MU tracking. As both files required are loaded by + different buttons, two functions storing the two files were created. + + See Also + -------- + open_emgfile1(), openhdemg.askopenfile() + """ + try: + # Open OTB file + if self.filetype_adv.get() == "OTB": + self.emgfile2 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + otb_ext_factor=int(self.extension_factor_adv.get()), + ) + # Open all other filetypes + else: + self.emgfile2 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + ) + + # Add filename to GUI + ctk.CTkLabel(self.head, text="File 2 loaded", font=('Segoe UI',15, 'bold')).grid(column=1, row=3) + + except ValueError: + CTkMessagebox(title="Info", message="Make sure to specify a valid filetype or extension factor.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def on_filetype_change_adv(self, *args): + """ + Handle changes in the file type selection in the AdvancedAnalysis GUI. + + This callback function is triggered when the `filetype_adv` variable changes. + Specifically, it updates the GUI to add or remove a combobox for specifying + the OTB (OpenToBe) extension factors when 'OTB' is selected as the file type. + For other file types, this additional combobox is removed. + + Parameters + ---------- + *args : tuple + The arguments passed to the callback function. Not used in the function but + required for compatibility with Tkinter's trace mechanism. + + Notes + ----- + The method is part of the AdvancedAnalysis class and interacts with the GUI elements + specific to file type selection. It ensures that the GUI is responsive to user + selections and updates the interface accordingly. + """ + # Add a combobox containing the OTB extension factors + # in case an OTB file is loaded + if self.filetype_adv.get() == "OTB": + self.otb_combobox = ctk.CTkComboBox( + self.head, + values=["8", "9", "10", "11", "12", "13", "14", "15", "16"], + width=8, + variable=self.extension_factor_adv, + state="readonly", + ) + self.otb_combobox.grid(column=1, row=1, sticky=(W, E), padx=5) + self.otb_combobox.set("Extension Factor") + + # Forget the widget in case the filetype is changed + else: + if hasattr(self, "otb_combobox"): + self.otb_combobox.grid_forget() + + def track_mus(self): + """ + Perform MUs tracking on the loaded EMG files. + + Notes + ----- + The function uses the openhdemg.tracking + function to perform the tracking of MUs. + + Raises + ------ + AttributeError + If the required EMG files have not been loaded. + ValueError + If the input parameters are not valid. + + See Also + -------- + openhdemg.tracking() + """ + try: + if self.mat_code_adv.get() == "None": + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.matrix_rc_adv.get().split(",")] + n_rows = list_rcs[0] + n_cols = list_rcs[1] + else: + n_rows = None + n_cols = None + except ValueError: + CTkMessagebox(title="Info", message="Verify that Rows and Columns are separated by ','", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + try: + # Track motor units + tracking_res = openhdemg.tracking( + emgfile1=self.emgfile1, + emgfile2=self.emgfile2, + threshold=float(self.threshold_adv.get()), + timewindow=int(self.time_window.get()), + matrixcode=self.mat_code_adv.get(), + orientation=int(self.mat_orientation_adv.get()), + n_rows=n_rows, + n_cols=n_cols, + exclude_belowthreshold=self.exclude_thres.get(), + filter=self.filter_adv.get(), + show=self.show_adv.get(), + ) + + # Add result terminal + track_terminal = ttk.LabelFrame( + self.head, text="MUs Tracking Result", height=100, relief="ridge" + ) + track_terminal.grid( + column=2, + row=0, + columnspan=2, + rowspan=12, + pady=8, + padx=10, + sticky=(N, S, W, E), + ) + + # Add table containing results to the label frame + track_table = Table(track_terminal, dataframe=tracking_res) + track_table.show() + + except AttributeError: + CTkMessagebox(title="Info", message="Make sure to load all required EMG files prior to tracking.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except ValueError: + CTkMessagebox(title="Info", message= + "Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Extension Factor (in case of OTB file)" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Threshold" + + "\n - Rows, Columns", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + def remove_duplicates_between(self): + """ + Perform duplicate removal between two EMG files. + + Notes + ----- + The function uses the openhdemg.remove_duplicates_between function to remove duplicates between two EMG files. + If the required parameters are not provided, the function will raise an AttributeError or ValueError. + + Raises + ------ + AttributeError + If the required EMG files have not been loaded. + ValueError + If the input parameters are not valid. + + See Also + -------- + openhdemg.remove_duplicates_between(), openhdemg.asksavefile() + """ + try: + if self.mat_code_adv.get() == "None": + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.matrix_rc_adv.get().split(",")] + n_rows = list_rcs[0] + n_cols = list_rcs[1] + else: + n_rows = None + n_cols = None + except ValueError: + CTkMessagebox(title="Info", message="Verify that Rows and Columns are separated by ','", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + try: + # Remove motor unit duplicates + emg_file1, emg_file2, _ = openhdemg.remove_duplicates_between( + emgfile1=self.emgfile1, + emgfile2=self.emgfile2, + threshold=float(self.threshold_adv.get()), + timewindow=int(self.time_window.get()), + matrixcode=self.mat_code_adv.get(), + orientation=int(self.mat_orientation_adv.get()), + n_rows=n_rows, + n_cols=n_cols, + filter=self.filter_adv.get(), + show=self.show_adv.get(), + which=self.which_adv.get(), + ) + + # Save files + openhdemg.asksavefile(emg_file1) + openhdemg.asksavefile(emg_file2) + + except AttributeError: + CTkMessagebox(title="Info", message="Make sure to load all required EMG files prior to tracking.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + except ValueError: + CTkMessagebox(title="Info", message="Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Extension Factor (in case of OTB file)" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Threshold" + + "\n - Which" + + "\n - Rows, Columns", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 0a590a7..8cd146b 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -51,7 +51,7 @@ class GUIHelpers: parent widget. """ - + def __init__(self, parent): """ Initialize a new instance of the GUIHelpers class. diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 756dbaa..ec4f2d3 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -8,7 +8,7 @@ import threading import webbrowser from tkinter import ttk, filedialog, Canvas -from tkinter import StringVar, Tk, N, S, W, E, DoubleVar +from tkinter import StringVar, Tk, N, S, W, E from pandastable import Table, config from PIL import Image @@ -17,12 +17,12 @@ import matplotlib from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from matplotlib.figure import Figure -import pandas as pd matplotlib.use("TkAgg") import openhdemg.library as openhdemg -from openhdemg.gui.gui_modules import MURemovalWindow, EditRefsig, GUIHelpers, AnalyseForce, MuAnalysis, PlotEmg +from openhdemg.gui.gui_modules import (MURemovalWindow, EditRefsig, GUIHelpers, + AnalyseForce, MuAnalysis, PlotEmg, AdvancedAnalysis) class emgGUI(): @@ -338,6 +338,8 @@ def __init__(self, master): # Create left side framing for functionalities self.left = ttk.Frame(self.master, padding="10 10 12 12") self.left.grid(column=0, row=0, sticky="nsew") + + # Columnconfigure allows resizable frames self.left.columnconfigure(0, weight=1) self.left.columnconfigure(1, weight=1) self.left.columnconfigure(2, weight=1) @@ -481,7 +483,7 @@ def __init__(self, master): advanced = ttk.Button( self.left, - command=self.open_advanced_tools, + command=lambda:(AdvancedAnalysis(self)), text="Advanced Tools", style="B.TButton", ) @@ -1058,115 +1060,9 @@ def reset_analysis(self): except FileNotFoundError: tk.messagebox.showerror("Information", "Make sure a file is loaded.") - def open_advanced_tools(self): - """ - Open a window for advanced analysis tools. - """ - # Disable config for DELSYS files - if self.resdict["SOURCE"] == "DELSYS": - tk.messagebox.showerror( - "Information", - "Advanced Tools for Delsys are only accessible from the library.", - ) - return - - # Open window - self.a_window = tk.Toplevel(bg="LightBlue4", height=200) - self.a_window.title("Advanced Tools Window") - self.a_window.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.a_window.grab_set() - - # Add Label - ttk.Label( - self.a_window, text="Select tool and matrix:", font=("Verdana", 14, "bold") - ).grid(row=0, column=0) - - # Analysis Tool - ttk.Label(self.a_window, text="Analysis Tool").grid( - row=2, column=0, sticky=(W, E) - ) - # Add Selection Combobox - self.advanced_method = StringVar() - adv_box = ttk.Combobox( - self.a_window, width=15, textvariable=self.advanced_method - ) - adv_box["values"] = ( - "Motor Unit Tracking", - "Duplicate Removal", - "Conduction Velocity", - ) - adv_box["state"] = "readonly" - adv_box.grid(row=2, column=1, sticky=(W, E)) - adv_box.set("Motor Unit Tracking") - - # Matrix Orientation - ttk.Label(self.a_window, text="Matrix Orientation").grid( - row=3, column=0, sticky=(W, E) - ) - self.mat_orientation_adv = StringVar() - orientation = ttk.Combobox( - self.a_window, width=8, textvariable=self.mat_orientation_adv - ) - orientation["values"] = ("0", "180") - orientation["state"] = "readonly" - orientation.grid(row=3, column=1, sticky=(W, E)) - self.mat_orientation_adv.set("180") - - # Matrix code - ttk.Label(self.a_window, text="Matrix Code").grid( - row=4, column=0, sticky=(W, E) - ) - self.mat_code_adv = StringVar() - matrix_code = ttk.Combobox( - self.a_window, width=10, textvariable=self.mat_code_adv - ) - matrix_code["values"] = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "None") - matrix_code["state"] = "readonly" - matrix_code.grid(row=4, column=1, sticky=(W, E)) - self.mat_code_adv.set("GR08MM1305") - - # Trace variabel for updating window - self.mat_code_adv.trace_add("write", self.on_matrix_none_adv) - - # Analysis Button - adv_button = ttk.Button( - self.a_window, - text="Advanced Analysis", - command=self.advanced_analysis, - style="B.TButton", - ) - adv_button.grid(column=0, row=7) - - # Add padding to widgets - for child in self.a_window.winfo_children(): - child.grid_configure(padx=5, pady=5) - - def on_matrix_none_adv(self, *args): - """ - This function is called when the value of the mat_code_adv variable is changed. - - When the variable is set to "None" it will create an Entrybox on the grid at column 1 and row 6, - and when the mat_code_adv is set to something else it will remove the entrybox from the grid. - """ - if self.mat_code_adv.get() == "None": - - self.mat_label_adv = ttk.Label(self.a_window, text="Rows, Columns:") - self.mat_label_adv.grid(row=5, column=1, sticky = W) - - self.matrix_rc_adv = StringVar() - self.row_cols_entry_adv = ttk.Entry(self.a_window, width=8, textvariable= self.matrix_rc_adv) - self.row_cols_entry_adv.grid(row=6, column=1, sticky = W, padx=5, pady=2) - self.matrix_rc_adv.set("13,5") - - else: - if hasattr(self, "row_cols_entry_adv"): - self.row_cols_entry_adv.grid_forget() - self.mat_label_adv.grid_forget() - - self.a_window.update_idletasks() + + # ----------------------------------------------------------------------------------------------- # Plotting inside of GUI @@ -1218,476 +1114,9 @@ def in_gui_plotting(self, resdict, plot="idr"): # Sorting of motor units - # ----------------------------------------------------------------------------------------------- - # Advanced Analysis - - def advanced_analysis(self): - """ - Open top-level windows based on the selected advanced method. - """ - - if self.advanced_method.get() == "Motor Unit Tracking": - head_title = "MUs Tracking Window" - elif self.advanced_method.get() == "Conduction Velocity": - head_title = "Conduction Velocity Window" - else: - head_title = "Duplicate Removal Window" - - self.head = tk.Toplevel(bg="LightBlue4") - self.head.title(head_title) - self.head.iconbitmap( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Icon.ico" - ) - self.head.grab_set() - - # Specify Signal - self.filetype_adv = StringVar() - signal_value = ("OTB", "DEMUSE", "OPENHDEMG", "CUSTOMCSV") - signal_entry = ttk.Combobox( - self.head, text="Signal", width=8, textvariable=self.filetype_adv - ) - signal_entry["values"] = signal_value - signal_entry["state"] = "readonly" - signal_entry.grid(column=0, row=1, sticky=(W, E)) - self.filetype_adv.set("Type of file") - self.filetype_adv.trace_add("write", self.on_filetype_change_adv) - - # Load file - load1 = ttk.Button(self.head, text="Load File 1", command=self.open_emgfile1) - load1.grid(column=0, row=2, sticky=(W, E)) - - # Load file - load2 = ttk.Button(self.head, text="Load File 2", command=self.open_emgfile2) - load2.grid(column=0, row=3, sticky=(W, E)) - - # Threshold label - threshold_label = ttk.Label(self.head, text="Threshold:") - threshold_label.grid(column=0, row=9) - - # Combobox for threshold - self.threshold_adv = StringVar() - threshold_combobox = ttk.Combobox( - self.head, - values=[0.6, 0.7, 0.8, 0.9], - textvariable=self.threshold_adv, - state="readonly", - width=8, - ) - threshold_combobox.grid(column=1, row=9) - self.threshold_adv.set(0.8) - - # Time Label - time_window_label = ttk.Label(self.head, text="Time window:") - time_window_label.grid(column=0, row=10) - - # Time Combobox - self.time_window = StringVar() - time_combobox = ttk.Combobox( - self.head, - values=[25, 50], - textvariable=self.time_window, - state="readonly", - width=8, - ) - time_combobox.grid(column=1, row=10) - self.time_window.set(25) - - # Exclude below threshold - exclude_label = ttk.Label(self.head, text="Exclude below threshold") - exclude_label.grid(column=0, row=11) - - # Add exclude checkbox - self.exclude_thres = tk.BooleanVar() - exclude_checkbox = tk.Checkbutton( - self.head, variable=self.exclude_thres, bg="LightBlue4" - ) - exclude_checkbox.grid(column=1, row=11) - self.exclude_thres.set(True) - - # Filter - filter_label = ttk.Label(self.head, text="Filter") - filter_label.grid(column=0, row=12) - - # Add filter checkbox - self.filter_adv = tk.BooleanVar() - filter_checkbox = tk.Checkbutton( - self.head, variable=self.filter_adv, bg="LightBlue4" - ) - filter_checkbox.grid(column=1, row=12) - self.filter_adv.set(True) - - # Exclude below threshold - show_label = ttk.Label(self.head, text="Show") - show_label.grid(column=0, row=13) - - # Add exclude checkbox - self.show_adv = tk.BooleanVar() - show_checkbox = tk.Checkbutton( - self.head, variable=self.show_adv, bg="LightBlue4" - ) - show_checkbox.grid(column=1, row=13) - - # Add button to execute MU tracking - track_button = ttk.Button(self.head, text="Track", command=self.track_mus) - track_button.grid(column=0, row=15, columnspan=2, sticky=(W, E)) - - # Add padding - for child in self.head.winfo_children(): - child.grid_configure(padx=5, pady=5) - - # Add Which widget and update the track button - # to match functionalities required for duplicate removal - if self.advanced_method.get() == "Duplicate Removal": - - # Add Which label - ttk.Label(self.head, text="Which").grid(column=0, row=14) - # Combobox for Which option - self.which_adv = StringVar() - which_combobox = ttk.Combobox( - self.head, - values=["munumber", "accuracy"], - textvariable=self.which_adv, - state="readonly", - width=8, - ) - which_combobox.grid(row=14, column=1, padx=5, pady=5) - self.which_adv.set("munumber") - - # Add button to execute MU tracking - track_button.config( - text="Remove Duplicates", command=self.remove_duplicates_between - ) - - if self.advanced_method.get() == "Conduction Velocity": - try: - # Destroy unnecessary pop-ups - self.head.destroy() - self.a_window.destroy() - - if self.mat_code_adv.get() == "None": - - # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.matrix_rc_adv.get().split(",")] - - try: - # Sort emg file - sorted_rawemg = openhdemg.sort_rawemg( - self.resdict, - code=self.mat_code_adv.get(), - orientation=int(self.mat_orientation_adv.get()), - n_rows=list_rcs[0], - n_cols=list_rcs[1] - ) - except ValueError: - tk.messagebox.showerror( - "Information", - "Number of specified rows and columns must match" - + "\nnumber of channels." - ) - return - # # DELSYS conduction velocity not available - # elif self.mat_code_adv.get() == "Trigno Galileo Sensor": - # tk.messagebox.showerror( - # "Information", - # "MUs conduction velocity estimation is not available for this matrix." - # ) - # return - - else: - # Sort emg file - sorted_rawemg = openhdemg.sort_rawemg( - self.resdict, - code=self.mat_code_adv.get(), - orientation=int(self.mat_orientation_adv.get()), - ) - - openhdemg.MUcv_gui( - emgfile=self.resdict, - sorted_rawemg=sorted_rawemg, - ) - - except AttributeError: - tk.messagebox.showerror( - "Information", - "Please make sure to load a file" - + "prior to Conduction velocity calculation.", - ) - self.head.destroy() - - except ValueError: - tk.messagebox.showerror( - "Information", - "Please make sure to enter valid Rows, Columns arguments." - + "\nArguments must be non-negative and seperated by `,`.", - ) - self.head.destroy() - - - # Destroy first window to avoid too many pop-ups - self.a_window.destroy() - - ### Define function for advanced analysis tools - def open_emgfile1(self): - """ - Open EMG file based on the selected file type and extension factor. - - This function is used to open and store the first emgfile that is - required for the MU tracking. As both files required are loaded by - different buttons, two functions storing the two files were created. - - See Also - -------- - open_emgfile1(), openhdemg.askopenfile() - """ - try: - # Open OTB file - if self.filetype_adv.get() == "OTB": - self.emgfile1 = openhdemg.askopenfile( - filesource=self.filetype_adv.get(), - otb_ext_factor=int(self.extension_factor_adv.get()), - ) - # Open all other filetypes - else: - self.emgfile1 = openhdemg.askopenfile( - filesource=self.filetype_adv.get(), - ) - - # Add filename to GUI - ttk.Label(self.head, text="File 1 loaded").grid(column=1, row=2) - - except ValueError: - tk.messagebox.showerror( - "Information", - "Make sure to specify a valid extension factor.", - ) - - def open_emgfile2(self): - """ - Open EMG file based on the selected file type and extension factor. - - This function is used to open and store the first emgfile that is - required for the MU tracking. As both files required are loaded by - different buttons, two functions storing the two files were created. - - See Also - -------- - open_emgfile1(), openhdemg.askopenfile() - """ - # Open OTB file - if self.filetype_adv.get() == "OTB": - self.emgfile2 = openhdemg.askopenfile( - filesource=self.filetype_adv.get(), - otb_ext_factor=int(self.extension_factor_adv.get()), - ) - # Open all other filetypes - else: - self.emgfile2 = openhdemg.askopenfile( - filesource=self.filetype_adv.get(), - ) - - # Add filename to GUI - ttk.Label(self.head, text="File 2 loaded").grid(column=1, row=3) - - def on_filetype_change_adv(self, *args): - """ - This function is called when the value of the filetype variable is changed. - When the filetype is set to "OTB" it will create a second combobox on the grid at column 0 and row 2, - and when the filetype is set to something else it will remove the second combobox from the grid. - """ - # Add a combobox containing the OTB extension factors - # in case an OTB file is loaded - if self.filetype_adv.get() == "OTB": - self.extension_factor_adv = StringVar() - self.otb_combobox = ttk.Combobox( - self.head, - values=["8", "9", "10", "11", "12", "13", "14", "15", "16"], - width=8, - textvariable=self.extension_factor_adv, - state="readonly", - ) - self.otb_combobox.grid(column=1, row=1, sticky=(W, E), padx=5) - self.otb_combobox.set("Extension Factor") - - # Forget the widget in case the filetype is changed - else: - if hasattr(self, "otb_combobox"): - self.otb_combobox.grid_forget() - - def track_mus(self): - """ - Perform MUs tracking on the loaded EMG files. - - Notes - ----- - The function uses the openhdemg.tracking - function to perform the tracking of MUs. - - Raises - ------ - AttributeError - If the required EMG files have not been loaded. - ValueError - If the input parameters are not valid. - - See Also - -------- - openhdemg.tracking() - """ - try: - if self.mat_code_adv.get() == "None": - # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.matrix_rc_adv.get().split(",")] - n_rows = list_rcs[0] - n_cols = list_rcs[1] - else: - n_rows = None - n_cols = None - except ValueError: - tk.messagebox.showerror( - "Information", - "Verify that Rows and Columns are separated by ','", - ) - - try: - # Track motor units - tracking_res = openhdemg.tracking( - emgfile1=self.emgfile1, - emgfile2=self.emgfile2, - threshold=float(self.threshold_adv.get()), - timewindow=int(self.time_window.get()), - matrixcode=self.mat_code_adv.get(), - orientation=int(self.mat_orientation_adv.get()), - n_rows=n_rows, - n_cols=n_cols, - exclude_belowthreshold=self.exclude_thres.get(), - filter=self.filter_adv.get(), - show=self.show_adv.get(), - ) - - # Add result terminal - track_terminal = ttk.LabelFrame( - self.head, text="MUs Tracking Result", height=100, relief="ridge" - ) - track_terminal.grid( - column=2, - row=0, - columnspan=2, - rowspan=12, - pady=8, - padx=10, - sticky=(N, S, W, E), - ) - - # Add table containing results to the label frame - track_table = Table(track_terminal, dataframe=tracking_res) - track_table.show() - - except AttributeError: - tk.messagebox.showerror( - "Information", - "Make sure to load all required EMG files prior to tracking.", - ) - - except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Extension Factor (in case of OTB file)" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Threshold" - + "\n - Rows, Columns", - ) - - def remove_duplicates_between(self): - """ - Perform duplicate removal between two EMG files. - - Notes - ----- - The function uses the openhdemg.remove_duplicates_between function to remove duplicates between two EMG files. - If the required parameters are not provided, the function will raise an AttributeError or ValueError. - - Raises - ------ - AttributeError - If the required EMG files have not been loaded. - ValueError - If the input parameters are not valid. - - See Also - -------- - openhdemg.remove_duplicates_between(), openhdemg.asksavefile() - """ - try: - if self.mat_code_adv.get() == "None": - # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.matrix_rc_adv.get().split(",")] - n_rows = list_rcs[0] - n_cols = list_rcs[1] - else: - n_rows = None - n_cols = None - except ValueError: - tk.messagebox.showerror( - "Information", - "Verify that Rows and Columns are separated by ','", - ) - - try: - # Remove motor unit duplicates - emg_file1, emg_file2, _ = openhdemg.remove_duplicates_between( - emgfile1=self.emgfile1, - emgfile2=self.emgfile2, - threshold=float(self.threshold_adv.get()), - timewindow=int(self.time_window.get()), - matrixcode=self.mat_code_adv.get(), - orientation=int(self.mat_orientation_adv.get()), - n_rows=n_rows, - n_cols=n_cols, - filter=self.filter_adv.get(), - show=self.show_adv.get(), - which=self.which_adv.get(), - ) - - # Save files - openhdemg.asksavefile(emg_file1) - openhdemg.asksavefile(emg_file2) - - except AttributeError: - tk.messagebox.showerror( - "Information", - "Make sure to load all required EMG files prior to tracking.", - ) - - except ValueError: - tk.messagebox.showerror( - "Information", - "Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Extension Factor (in case of OTB file)" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Threshold" - + "\n - Which" - + "\n - Rows, Columns", - ) - - def calculate_conduct_vel(self): - # Add result terminal - track_terminal = ttk.LabelFrame( - self.head, text="Conduction Velocity", height=100, relief="ridge" - ) - track_terminal.grid( - column=2, - row=0, - columnspan=2, - rowspan=12, - pady=8, - padx=10, - sticky=(N, S, W, E), - ) + + + # ----------------------------------------------------------------------------------------------- # Analysis results display From c82ad8ed00fcddc6de4a4379e3c71441df6aa70a Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 9 Jan 2024 22:05:08 +0100 Subject: [PATCH 11/57] Added: Main GUI --- openhdemg/gui/gui_modules/edit_refsig.py | 2 +- openhdemg/gui/gui_modules/gui_helpers.py | 2 + openhdemg/gui/openhdemg_gui.py | 484 +++++------------------ 3 files changed, 106 insertions(+), 382 deletions(-) diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index 2edf599..8429c08 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -153,7 +153,7 @@ def __init__(self, parent): factor = ctk.CTkEntry(self.head, width=100, textvariable=self.convert_factor) factor.grid(column=2, row=7) self.convert_factor.set(2.5) - + convert_button = ctk.CTkButton(self.head, text="Convert", command=self.convert_refsig, fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) convert_button.grid(column=0, row=7, sticky=W) diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 8cd146b..a72c9f8 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -211,3 +211,5 @@ def sort_mus(self): bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + + \ No newline at end of file diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index ec4f2d3..4734db0 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -3,13 +3,13 @@ """ import os +import customtkinter as ctk import tkinter as tk -import customtkinter import threading import webbrowser -from tkinter import ttk, filedialog, Canvas -from tkinter import StringVar, Tk, N, S, W, E +from tkinter import ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E from pandastable import Table, config +import customtkinter from PIL import Image @@ -18,12 +18,10 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from matplotlib.figure import Figure -matplotlib.use("TkAgg") - import openhdemg.library as openhdemg from openhdemg.gui.gui_modules import (MURemovalWindow, EditRefsig, GUIHelpers, AnalyseForce, MuAnalysis, PlotEmg, AdvancedAnalysis) - +matplotlib.use("TkAgg") class emgGUI(): """ @@ -34,44 +32,12 @@ class emgGUI(): Attributes ---------- - self.auto_eval : int, default 0 - If auto > 0, the script automatically removes the offset based on the number - of samples passed in input. - self.b_firings_rec : int, default 4 - The number of firings at recruitment and derecruitment to consider for the - calculation of the DR. - self.b_firings_ste : int, default 10 - The number of firings to consider for the calculation of the DR at the start and at the end - of the steady-state phase. self.canvas : matplotlib.backends.backend_tkagg Canvas for plotting figures inside the GUI. self.channels : int or list The channel (int) or channels (list of int) to plot. The list can be passed as a manually-written with: "0,1,2,3,4,5...,n", channels is expected to be with base 0. - self.convert : str, default Multiply - The kind of conversion applied to the Refsig during Refsig conversion. Can be "Multiply" or "Divide". - self.convert_factor : float, default 2.5 - Factore used during Refsig converison when multiplication or division is applied. - self.cutoff_freq : int, default 20 - The cut-off frequency in Hz. - self.ct_event : str, default "rt_dert" - When to calculate the thresholds. Input parameters for event_ are: - "rt_dert" means that both recruitment and derecruitment tresholds will be calculated. - "rt" means that only recruitment tresholds will be calculated. - "dert" means that only derecruitment tresholds will be calculated. - self.dr_event : str, default "rec_derec_steady" - When to calculate the DR. Input parameters for event_ are: - "rec_derec_steady" means that the DR is calculated at recruitment, derecruitment and - during the steady-state phase. - "rec" means that the DR is calculated at recruitment. - "derec" means that the DR is calculated at derecruitment. - "rec_derec" means that the DR is calculated at recruitment and derecruitment. - "steady" means that the DR is calculated during the steady-state phase. - self.end_area, self.start_area : int - The start and end of the selection for file resizing (can be used for code automation). - self.exportable_df : pd.DataFrame - A pd.DataFrame containing the results of the analysis. self.fig : matplotlib.figure Figure to be plotted on Canvas. self.filename : str @@ -81,96 +47,20 @@ class emgGUI(): self.filetype : str String containing the filetype of import EMG file. Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG". - self.filter_order : int, default 4 - The filter order. - self.firings_rec : int, default 4 - The number of firings at recruitment and derecruitment to consider for the calculation - of the DR. - self.firings_ste : int, default 10 - The number of firings to consider for the calculation of the DR at the start and at the end - of the steady-state phase. - self.first_fig : matplotlib.figure - Figure frame determinining size of all figures that are plotted on canvas. - self.head : tk.toplevel - New tk.toplevel instance created everytime upon opnening a new window. This is needed - for having a seperate window open. self.left : tk.frame Left frame inside of master that contains all buttons and filespecs. - self.linewidth : float, default 0.5 - The width of the vertical lines representing the MU firing. - self.logo : + self.logo : String containing the path to image file containing logo of openhdemg. self.logo_canvas : tk.canvas Canvas to display logo of Open_HG-EMG when openend. self.master: tk TK master window containing all widget children for this GUI. - self.mus_dr: pd.DataFrame - A pd.DataFrame containing the requested DR. - self.mu_numb : int or list, default "all" - By default, IPTS of all the MUs is plotted. - Otherwise, a single MU (int) or multiple MUs (list of int) can be specified. - The list can be passed as a manually-written list: "0,1,2,3,4,5,...,n", - self.m_numb is expected to be with base 0 (i.e., the first MU in the file is the number 0). - self.mu_numb_idr : int or list, default "all" - By default, IDR of all the MUs is plotted. - Otherwise, a single MU (int) or multiple MUs (list of int) can be specified. - The list can be passed as a manually-written list: "0,1,2,3,4,5,...,n", - self.m_numb_idr is expected to be with base 0 (i.e., the first MU in the - file is the number 0). - self.mu_to_remove : int - The MUs to remove. If a single MU has to be removed, this should be an - int (number of the MU). - self.mu_to_remove is expected to be with base 0 - (i.e., the first MU in the file is the number 0). - self.mu_to_edit : int - The MUs to edit singularly. If a single MU has to be edited, this should be an - int (number of the MU). - self.mu_to_edit is expected to be with base 0 - (i.e., the first MU in the file is the number 0). - self.mu_thresholds : pd.DataFrame - A DataFrame containing the requested thresholds. - self.mvc : float - The MVC value in the original unit of measurement. - self.mvc_df : pd.DataFrame - A Dataframe containing the detected MVC value. - self.mvc_value : float - The MVC value specified during Refsig conversion. - self.offsetval: float, default 0 - Value of the offset. If offsetval is 0 (default), the user will be asked to manually - select an aerea to compute the offset value. - Otherwise, the value passed to offsetval will be used. Negative offsetval can be passed. self.resdict : dict Dictionary derived from input EMG file for further analysis. - self.rfd_df : pd.DataFrame - A Dataframe containing the calculated RFD values. - self.rfdms : list, default [50, 100, 150, 200] - Milliseconds (ms). A list containing the ranges in ms to calculate the RFD. self.right : tk.frame Left frame inside of master that contains plotting canvas. self.terminal : ttk.Labelframe Tkinter labelframe that is used to display the results table in the GUI. - self.mat_code : str - The code containing the matrix identification number. - self.mat_orientation : int - The orientation of the matrix in degrees. Can be 0, 180. - self.deriv_config : str - The Method used to calculate the MUs deviation. - self.muap_config : str - The Method used to calculate the MUs deviation. - self.deriv_matrix : str - Column of the matrix to be plotted. - self.size_fig : str, default [20,15] - Size of the figure to be plotted in centimeter. - self.ref_but : str, default "False" - String value used to determine if reference signal should be - added to the plot. - self.time_but : str, default "False" - String value used to determine if time in seconds should be used - in x-axis of plotting. - muap_munum : int - Number of motor unit to be plotted. - muap_time : int - Time window to be plotted. self.info : tk.PhotoImage Information Icon displayed in GUI. self.online : tk.Photoimage @@ -188,36 +78,6 @@ class emgGUI(): Stringvariable containing the self.extension_factor : tk.StringVar() Stringvariable containing the OTB extension factor value. - self.advanced_method : tk.Stringvar() - Stringvariable containing the selected method of advanced - analysis. - self.matrix_rc : tk.StringVar() - String containing the channel number of emgfile when - matri codes are bypassed. Used in plot window. - self.matrix_rc_adv : tk.StringVar() - String containing the channel number of emgfile when - matri codes are bypassed. Used in advanced window. - self.emgfile1 : pd.Dataframe - Dataframe object containing the loaded first emgfile used - for MU tracking. - self.emgfile2 : pd.Dataframe - Dataframe object containing the loaded first emgfile used - for MU tracking. - self.thresh_adv : tk.Stringvar() - Stringvariable containing the selected threshold for MU tracking. - self.filter_adv : tk.Boolenvar() - Boolean determining whether filtering should be applied during MU - tracking. - self.show_adv : tk.Boolenvar() - Boolean determining whether results of MU tracking should be plotted. - self.exclude_thres : tk.Boolenvar() - Boolean determining whether values below treshold should be excluded - during MU tracking. - self.which_adv : tk.Stringvar() - Stringvariable determining how MU duplicates are removed. - self.time_window : tk.Stringvar() - Stringvariable determining the time window for duplicate removal - and tracking. Methods ------- @@ -229,82 +89,18 @@ class emgGUI(): save_emgfile() Saves the edited emgfile dictionary to a .json file. Executed when button "Save File" in master GUI window pressed. - reset_analysis() + reset_analysis() Resets the whole analysis, restores the original input file and the graph. Executed when button "Reset analysis" in master GUI window pressed. in_gui_plotting() Method used for creating plot inside the GUI (on the GUI canvas). Executed when button "View MUs" in master GUI window pressed. - remove_mus() - Opens seperate window to select motor units to be removed. - Executed when button "Remove MUs" in master GUI window pressed. - remove() - Method used to remove single motor units. - remove_empty() - Method that removes all empty MUs. - edit_refsig() - Opens seperate window to edit emg reference signal. - Executed when button "RefSig Editing" in master GUI window pressed. - filter_refsig() - Method used to filter the emg reference signal. - remove_offset() - Method used to remove offset of emg reference signal. - resize_file() - Opens seperate window to resize emg file / reference signal. - Executed when button "Resize File" in master GUI window pressed. - analyze_force() - Opens seperate window to analyze force signal/values. - Executed when button "Analyze force" in master GUI window pressed. - get_mvc() - Method used to calculate/select MVC. - det_rfd() - Method used to calculated RFD based on selected startpoint. mu_analysis() Opens seperate window to calculated specific motor unit properties. Executed when button "MU properties" in master GUI window pressed. - compute_mu_threshold() - Method used to calculate motor unit recruitement thresholds. - compute_mu_dr() - Method used to calculate motor unit discharge rate. - basic_mus_analysis() - Method used to calculate basic motor unit properties. - plot_emg() - Opens seperate window to plot emgsignal/motor unit properties. - Executed when button "Plot EMG" in master GUI window pressed. - plt_emgsignal() - Method used to plot emgsignal. - plt_idr() - Method used to plot instanteous discharge rate. - plt_ipts() - Method used to plot the motor unit puls train (i.e., non-binary firing) - plt_refsignal() - Method used to plot the motor unit reference signal. - plt_mupulses() - Method used to plot the motor unit pulses. - plot_derivation() - Method to plot the differential derivation of the RAW_SIGNAL - by matrix column. - plot_muaps() - Method to plot motor unit action potenital obtained from STA - from one or multiple MUs. - advanced_analysis() - Method to open top-level windows based on the selected advanced method. - on_filetype_change() - Method do display extension factor combobx when filetype loaded is - OTB. - open_emgfile1() - Method to open EMG file based on the selected file type and extension factor. - open_emgfile2() - Method to open EMG file based on the selected file type and extension factor. - track_mus() - Method to perform MUs tracking on the loaded EMG files. display_results() Method used to display result table containing analysis results. - to_percent() - Method that converts Refsig to a percentag value. Should only be used when the Refsig is in absolute values. - convert_refsig() - Method that converts Refsig by multiplication or division. - + Notes ----- Please note that altough we created a GUI class, the included methods/ @@ -316,7 +112,6 @@ class emgGUI(): the library. In the section "See Also" at each instance method, the reader is referred to the corresponding function and extensive documentation in the library. """ - def __init__(self, master): """ Initialization of master GUI window upon calling. @@ -332,176 +127,134 @@ def __init__(self, master): master_path = os.path.dirname(os.path.abspath(__file__)) iconpath = master_path + "/gui_files/Icon.ico" self.master.iconbitmap(iconpath) + + # Necessary for resizing self.master.columnconfigure(0, weight=1) self.master.rowconfigure(0, weight=1) # Create left side framing for functionalities - self.left = ttk.Frame(self.master, padding="10 10 12 12") + self.left = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0) self.left.grid(column=0, row=0, sticky="nsew") - # Columnconfigure allows resizable frames - self.left.columnconfigure(0, weight=1) - self.left.columnconfigure(1, weight=1) - self.left.columnconfigure(2, weight=1) - self.left.columnconfigure(3, weight=1) - self.left.rowconfigure(0, weight=1) - self.left.rowconfigure(1, weight=1) - self.left.rowconfigure(2, weight=1) - self.left.rowconfigure(3, weight=1) - self.left.rowconfigure(4, weight=1) - self.left.rowconfigure(5, weight=1) - self.left.rowconfigure(6, weight=1) - self.left.rowconfigure(7, weight=1) - self.left.rowconfigure(8, weight=1) - self.left.rowconfigure(9, weight=1) - self.left.rowconfigure(10, weight=1) - self.left.rowconfigure(11, weight=1) - self.left.rowconfigure(12, weight=1) - self.left.rowconfigure(13, weight=1) - self.left.rowconfigure(14, weight=1) - self.left.rowconfigure(15, weight=1) - self.left.rowconfigure(16, weight=1) - self.left.rowconfigure(17, weight=1) - self.left.rowconfigure(18, weight=1) - - # Style - style = ttk.Style() - style.theme_use("clam") - style.configure("TToplevel", background="LightBlue4") - style.configure("TFrame", background="LightBlue4") - style.configure( - "TLabel", - font=("Lucida Sans", 12), - foreground="black", - background="LightBlue4", - ) - style.configure("TButton", foreground="black", font=("Lucida Sans", 11)) - style.configure("TEntry", font=("Lucida Sans", 12), foreground="black") - style.configure("TCombobox", background="LightBlue4", foreground="black") - style.configure("TLabelFrame", foreground="black", font=("Lucida Sans", 16)) - style.configure("TProgressbar", foreground="#FFBF00", background="#FFBF00") - # Specify Signal + # Configure columns with a loop + for col in range(4): + self.left.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(19): + self.left.rowconfigure(row, weight=1) + + # Specify filetype self.filetype = StringVar() - signal_value = ("OPENHDEMG", "DEMUSE","OTB", "OTB_REFSIG", "DELSYS", "DELSYS_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG") - signal_entry = ttk.Combobox( - self.left, text="Signal", width=10, textvariable=self.filetype - ) - signal_entry["values"] = signal_value - signal_entry["state"] = "readonly" - signal_entry.grid(column=0, row=1, sticky=(W, E)) + signal_value = ["OPENHDEMG", "DEMUSE", "OTB", "OTB_REFSIG", "DELSYS", "DELSYS_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG"] + signal_entry = ctk.CTkComboBox(self.left, width=150, variable=self.filetype, values=signal_value, state="readonly") + signal_entry.grid(column=0, row=1, sticky=(ctk.W, ctk.E)) self.filetype.set("Type of file") # Trace filetype to apply function when changeing self.filetype.trace_add("write", self.on_filetype_change) # Load file - load = ttk.Button(self.left, text="Load File", command=self.get_file_input) - load.grid(column=0, row=3, sticky=W) + load = ctk.CTkButton(self.left, text="Load File", command=self.get_file_input, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + load.grid(column=0, row=3, sticky=ctk.W) # File specifications - ttk.Label(self.left, text="Filespecs:").grid(column=1, row=1, sticky=(W, E)) - ttk.Label(self.left, text="N Channels:").grid(column=1, row=2, sticky=(W, E)) - ttk.Label(self.left, text="N of MUs:").grid(column=1, row=3, sticky=(W, E)) - ttk.Label(self.left, text="File length:").grid(column=1, row=4, sticky=(W, E)) + ctk.CTkLabel(self.left, text="Filespecs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=1, sticky=(W)) + ctk.CTkLabel(self.left, text="N Channels:", font=('Segoe UI',15, 'bold')).grid(column=1, row=2, sticky=(W)) + ctk.CTkLabel(self.left, text="N of MUs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=3, sticky=(W)) + ctk.CTkLabel(self.left, text="File length:", font=('Segoe UI',15, 'bold')).grid(column=1, row=4, sticky=(W)) separator0 = ttk.Separator(self.left, orient="horizontal") separator0.grid(column=0, columnspan=3, row=5, sticky=(W, E)) # Save File - save = ttk.Button(self.left, text="Save File", command=self.save_emgfile) + save = ctk.CTkButton(self.left, text="Save File", command=self.save_emgfile, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) save.grid(column=0, row=6, sticky=W) separator1 = ttk.Separator(self.left, orient="horizontal") separator1.grid(column=0, columnspan=3, row=7, sticky=(W, E)) # Export to Excel - export = ttk.Button( - self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()) - ) + export = ctk.CTkButton(self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) export.grid(column=1, row=6, sticky=(W, E)) # View Motor Unit Firings - firings = ttk.Button(self.left, text="View MUs", command=lambda: (self.in_gui_plotting(resdict=self.resdict))) + firings = ctk.CTkButton(self.left, text="View MUs", command=lambda: (self.in_gui_plotting(resdict=self.resdict)), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) firings.grid(column=0, row=8, sticky=W) # Sort Motor Units - sorting = ttk.Button(self.left, text="Sort MUs", command=lambda: (GUIHelpers(parent=self).sort_mus())) + sorting = ctk.CTkButton(self.left, text="Sort MUs", command=lambda: (GUIHelpers(parent=self).sort_mus()), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) sorting.grid(column=1, row=8, sticky=(W, E)) separator2 = ttk.Separator(self.left, orient="horizontal") separator2.grid(column=0, columnspan=3, row=9, sticky=(W, E)) # Remove Motor Units - remove_mus = ttk.Button(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self, resdict=self.resdict))) + remove_mus = ctk.CTkButton(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self, resdict=self.resdict)), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) remove_mus.grid(column=0, row=10, sticky=W) separator3 = ttk.Separator(self.left, orient="horizontal") separator3.grid(column=0, columnspan=3, row=11, sticky=(W, E)) # Filter Reference Signal - reference = ttk.Button( - self.left, text="RefSig Editing", command=lambda:(EditRefsig(parent=self)) - ) + reference = ctk.CTkButton(self.left, text="RefSig Editing", command=lambda:(EditRefsig(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) reference.grid(column=0, row=12, sticky=W) # Resize File - resize = ttk.Button(self.left, text="Resize File", command=lambda:(GUIHelpers(parent=self).resize_file())) + resize = ctk.CTkButton(self.left, text="Resize File", command=lambda:(GUIHelpers(parent=self).resize_file()), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) resize.grid(column=1, row=12, sticky=(W, E)) separator4 = ttk.Separator(self.left, orient="horizontal") separator4.grid(column=0, columnspan=3, row=13, sticky=(W, E)) # Force Analysis - force = ttk.Button(self.left, text="Analyse Force", command=lambda:(AnalyseForce(parent=self))) + force = ctk.CTkButton(self.left, text="Analyse Force", command=lambda:(AnalyseForce(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) force.grid(column=0, row=14, sticky=W) separator5 = ttk.Separator(self.left, orient="horizontal") separator5.grid(column=0, columnspan=3, row=15, sticky=(W, E)) # Motor Unit properties - mus = ttk.Button(self.left, text="MU Properties", command=lambda:(MuAnalysis(parent=self))) + mus = ctk.CTkButton(self.left, text="MU Properties", command=lambda:(MuAnalysis(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) mus.grid(column=1, row=14, sticky=W) separator6 = ttk.Separator(self.left, orient="horizontal") separator6.grid(column=0, columnspan=3, row=17, sticky=(W, E)) # Plot EMG - plots = ttk.Button(self.left, text="Plot EMG", command=lambda:(PlotEmg(parent=self))) + plots = ctk.CTkButton(self.left, text="Plot EMG", command=lambda:(PlotEmg(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) plots.grid(column=0, row=16, sticky=W) separator7 = ttk.Separator(self.left, orient="horizontal") separator7.grid(column=0, columnspan=3, row=19, sticky=(W, E)) # Reset Analysis - reset = ttk.Button( - self.left, text="Reset Analysis", command=self.reset_analysis - ) + reset = ctk.CTkButton(self.left, text="Reset Analysis", command=self.reset_analysis, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) reset.grid(column=1, row=18, sticky=(W, E)) # Advanced tools - # Create seperate style for this button - advanced_button_style = ttk.Style() - advanced_button_style.theme_use("clam") - advanced_button_style.configure( - "B.TButton", - foreground="white", - background="black", - font=("Lucida Sans", 11), - ) - - advanced = ttk.Button( - self.left, - command=lambda:(AdvancedAnalysis(self)), - text="Advanced Tools", - style="B.TButton", - ) + advanced = ctk.CTkButton(self.left, text="Advanced Tools", command=lambda:(AdvancedAnalysis(self)), + fg_color="#000000", text_color="white", border_color="white", border_width=1, + hover_color="#FFBF00") advanced.grid(row=20, column=0, columnspan=2, sticky=(W, E)) # Create right side framing for functionalities - self.right = ttk.Frame(self.master, padding="10 10 12 12") + self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0) self.right.grid(column=1, row=0, sticky=(N, S, E, W)) - self.right.columnconfigure(0, weight=1) - self.right.columnconfigure(1, weight=1) - self.right.rowconfigure(0, weight=1) - self.right.rowconfigure(1, weight=1) - self.right.rowconfigure(2, weight=1) - self.right.rowconfigure(3, weight=1) - self.right.rowconfigure(4, weight=1) + # Configure columns with a loop + for col in range(2): + self.right.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(5): + self.right.rowconfigure(row, weight=1) # Create empty figure - self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54)) + self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=False) self.canvas = FigureCanvasTkAgg(self.first_fig, master=self.right) self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=5) @@ -529,10 +282,7 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - ( - webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/") - ), - ), + (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/"))), ) info_button.grid(row=0, column=1, sticky=E) @@ -549,10 +299,7 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - ( - webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/") - ), - ), + (webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/"))), ) online_button.grid(row=1, column=1, sticky=E) @@ -568,11 +315,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ( - ( - webbrowser.open("https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers") - ), - ), + command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers"))), ) redirect_button.grid(row=2, column=1, sticky=E) @@ -588,11 +331,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ( - ( - webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/") - ), - ), + command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/"))), ) contact_button.grid(row=3, column=1, sticky=E) @@ -608,12 +347,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ( - # Check user OS for pdf opening - ( - webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/") - ), - ), + command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/"))), ) cite_button.grid(row=4, column=1, sticky=E) @@ -648,13 +382,13 @@ def load_file(): ext_factor=int(self.extension_factor.get()), ) # Add filespecs - ttk.Label( + ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( + ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( column=2, row=3, sticky=(W, E) ) - ttk.Label(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( + ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( column=2, row=4, sticky=(W, E) ) @@ -667,13 +401,13 @@ def load_file(): # load file self.resdict = openhdemg.emg_from_demuse(filepath=self.file_path) # Add filespecs - ttk.Label( + ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( + ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( column=2, row=3, sticky=(W, E) ) - ttk.Label(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( + ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( column=2, row=4, sticky=(W, E) ) elif self.filetype.get() == "DELSYS": @@ -692,11 +426,11 @@ def load_file(): self.resdict = openhdemg.emg_from_delsys(rawemg_filepath=self.file_path, mus_directory=self.mus_path) # Add filespecs - ttk.Label(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( + ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E)) + ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( column=2, row=3, sticky=(W, E) ) - ttk.Label(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( + ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( column=2, row=4, sticky=(W, E) ) @@ -713,16 +447,16 @@ def load_file(): # files, these could contain also the reference signal only. Therefore line 2 # and 3 will crash. I temporarily fixed it, please review it for next release. if self.resdict["SOURCE"] in ["DEMUSE", "OTB", "CUSTOMCSV", "DELSYS"]: - ttk.Label(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid(column=2, row=3, sticky=(W, E)) - ttk.Label(self.left, text=str(self.resdict["EMG_LENGTH"])).grid(column=2, row=4, sticky=(W, E)) + ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E)) + ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid(column=2, row=3, sticky=(W, E)) + ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid(column=2, row=4, sticky=(W, E)) else: # Reconfigure labels for refsig - ttk.Label( + ctk.CTkLabel( self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) - ttk.Label(self.left, text=" ").grid( + ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) + ctk.CTkLabel(self.left, text=" ").grid( column=2, row=4, sticky=(W, E) ) else: @@ -738,9 +472,9 @@ def load_file(): fsamp=float(self.fsamp.get()), ) # Add filespecs - ttk.Label(self.left, text="Custom CSV").grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text="").grid(column=2, row=3, sticky=(W, E)) - ttk.Label(self.left, text="").grid(column=2, row=4, sticky=(W, E)) + ctk.CTkLabel(self.left, text="Custom CSV").grid(column=2, row=2, sticky=(W, E)) + ctk.CTkLabel(self.left, text="").grid(column=2, row=3, sticky=(W, E)) + ctk.CTkLabel(self.left, text="").grid(column=2, row=4, sticky=(W, E)) # Get filename filename = os.path.splitext(os.path.basename(file_path))[0] @@ -792,11 +526,11 @@ def load_file(): self.master.title(self.filename) # Reconfigure labels for refsig - ttk.Label( + ctk.CTkLabel( self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) - ttk.Label(self.left, text=" ").grid( + ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) + ctk.CTkLabel(self.left, text=" ").grid( column=2, row=4, sticky=(W, E) ) @@ -847,9 +581,10 @@ def load_file(): progress.grid_remove() # Indicate Progress - progress = ttk.Progressbar(self.left, mode="indeterminate") + progress = ctk.CTkProgressBar(self.left, mode="indeterminate", fg_color="#585858", + width=100, progress_color="#FFBF00") progress.grid(row=4, column=0) - progress.start(1) + progress.start() # Create a thread to run the load_file function save_thread = threading.Thread(target=load_file) @@ -880,7 +615,7 @@ def on_filetype_change(self, *args): # in case an OTB file is loaded if self.filetype.get() == "OTB": self.extension_factor = StringVar() - self.otb_combobox = ttk.Combobox( + self.otb_combobox = ctk.CTkComboBox( self.left, values=[ "8", @@ -894,7 +629,7 @@ def on_filetype_change(self, *args): "16", ], width=8, - textvariable=self.extension_factor, + variable=self.extension_factor, state="readonly", ) self.otb_combobox.grid(column=0, row=2, sticky=(W, E), padx=5) @@ -904,7 +639,7 @@ def on_filetype_change(self, *args): # Please check if this can be done better. elif self.filetype.get() in ["CUSTOMCSV", "CUSTOMCSV_REFSIG"]: self.fsamp = StringVar(value="Fsamp") - self.csv_entry = ttk.Entry( + self.csv_entry = ctk.CTkEntry( self.left, width=8, textvariable=self.fsamp, @@ -955,7 +690,7 @@ def save_file(): tk.messagebox.showerror("Information", "Make sure a file is loaded.") # Indicate Progress - progress = ttk.Progressbar(self.left, mode="indeterminate") + progress = ctk.CTkProgressbar(self.left, mode="indeterminate") progress.grid(row=4, column=0) progress.start(1) @@ -963,8 +698,6 @@ def save_file(): save_thread = threading.Thread(target=save_file) save_thread.start() - - def reset_analysis(self): """ Instance method to restore the GUI to base data. Any analysis progress will be deleted by @@ -1010,13 +743,13 @@ def reset_analysis(self): self.resdict = openhdemg.emg_from_delsys(rawemg_filepath=self.file_path, mus_directory=self.mus_path) # Update Filespecs - ttk.Label( + ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( + ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( column=2, row=3, sticky=(W, E) ) - ttk.Label(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( + ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( column=2, row=4, sticky=(W, E) ) @@ -1028,11 +761,11 @@ def reset_analysis(self): self.resdict = openhdemg.refsig_from_customcsv(filepath=self.file_path) # Recondifgure labels for refsig - ttk.Label( + ctk.CTkLabel( self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ttk.Label(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) - ttk.Label(self.left, text=" ").grid( + ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) + ctk.CTkLabel(self.left, text=" ").grid( column=2, row=4, sticky=(W, E) ) @@ -1060,9 +793,6 @@ def reset_analysis(self): except FileNotFoundError: tk.messagebox.showerror("Information", "Make sure a file is loaded.") - - - # ----------------------------------------------------------------------------------------------- # Plotting inside of GUI @@ -1110,14 +840,6 @@ def in_gui_plotting(self, resdict, plot="idr"): except AttributeError: tk.messagebox.showerror("Information", "Make sure a file is loaded.") - # ----------------------------------------------------------------------------------------------- - # Sorting of motor units - - - - - - # ----------------------------------------------------------------------------------------------- # Analysis results display @@ -1135,7 +857,7 @@ def display_results(self, input_df): """ # Create frame for output self.terminal = ttk.LabelFrame( - root, text="Result Output", height=100, relief="ridge" + self.master, text="Result Output", height=100, relief="ridge", ) self.terminal.grid( column=0, row=21, columnspan=2, pady=8, padx=10, sticky=(N, S, W, E) From 917cbba6e9a10361564676e01cc53e7de7f56715 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 12 Jan 2024 22:01:32 +0100 Subject: [PATCH 12/57] Added: Error Messages --- .../gui/gui_modules/advanced_analyses.py | 2 +- openhdemg/gui/gui_modules/mu_properties.py | 2 +- openhdemg/gui/openhdemg_gui.py | 128 ++++++++++-------- 3 files changed, 75 insertions(+), 57 deletions(-) diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index 704beab..ac65784 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -504,7 +504,7 @@ def open_emgfile2(self): # Add filename to GUI ctk.CTkLabel(self.head, text="File 2 loaded", font=('Segoe UI',15, 'bold')).grid(column=1, row=3) - + except ValueError: CTkMessagebox(title="Info", message="Make sure to specify a valid filetype or extension factor.", icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index 163feac..164a0c0 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -230,7 +230,7 @@ def compute_mu_threshold(self): button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except ValueError: - CTkMessagebox(title="Info", message="Enter valid MVC.", icon="info", + CTkMessagebox(title="Info", message="Enter valid MVC, Event or Type.", icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 4734db0..6c98415 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -3,13 +3,13 @@ """ import os -import customtkinter as ctk import tkinter as tk import threading import webbrowser from tkinter import ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E +import customtkinter as ctk +from CTkMessagebox import CTkMessagebox from pandastable import Table, config -import customtkinter from PIL import Image @@ -25,11 +25,13 @@ class emgGUI(): """ - A class representing a Tkinter TK instance. - This class is used to create a graphical user interface for the openhdemg library. + Within this class and corresponding childs, most functionalities + of the ophdemg library are packed in a GUI. Howebver, the library is more + comprehensive and much more adaptable to the users needs. + Attributes ---------- self.canvas : matplotlib.backends.backend_tkagg @@ -155,7 +157,7 @@ def __init__(self, master): # Load file load = ctk.CTkButton(self.left, text="Load File", command=self.get_file_input, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) load.grid(column=0, row=3, sticky=ctk.W) # File specifications @@ -168,14 +170,14 @@ def __init__(self, master): # Save File save = ctk.CTkButton(self.left, text="Save File", command=self.save_emgfile, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) save.grid(column=0, row=6, sticky=W) separator1 = ttk.Separator(self.left, orient="horizontal") separator1.grid(column=0, columnspan=3, row=7, sticky=(W, E)) # Export to Excel export = ctk.CTkButton(self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) export.grid(column=1, row=6, sticky=(W, E)) # View Motor Unit Firings @@ -192,7 +194,7 @@ def __init__(self, master): # Remove Motor Units remove_mus = ctk.CTkButton(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self, resdict=self.resdict)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) remove_mus.grid(column=0, row=10, sticky=W) separator3 = ttk.Separator(self.left, orient="horizontal") @@ -200,19 +202,19 @@ def __init__(self, master): # Filter Reference Signal reference = ctk.CTkButton(self.left, text="RefSig Editing", command=lambda:(EditRefsig(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) reference.grid(column=0, row=12, sticky=W) # Resize File resize = ctk.CTkButton(self.left, text="Resize File", command=lambda:(GUIHelpers(parent=self).resize_file()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) resize.grid(column=1, row=12, sticky=(W, E)) separator4 = ttk.Separator(self.left, orient="horizontal") separator4.grid(column=0, columnspan=3, row=13, sticky=(W, E)) # Force Analysis force = ctk.CTkButton(self.left, text="Analyse Force", command=lambda:(AnalyseForce(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) force.grid(column=0, row=14, sticky=W) separator5 = ttk.Separator(self.left, orient="horizontal") separator5.grid(column=0, columnspan=3, row=15, sticky=(W, E)) @@ -226,26 +228,26 @@ def __init__(self, master): # Plot EMG plots = ctk.CTkButton(self.left, text="Plot EMG", command=lambda:(PlotEmg(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) plots.grid(column=0, row=16, sticky=W) separator7 = ttk.Separator(self.left, orient="horizontal") separator7.grid(column=0, columnspan=3, row=19, sticky=(W, E)) # Reset Analysis reset = ctk.CTkButton(self.left, text="Reset Analysis", command=self.reset_analysis, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) reset.grid(column=1, row=18, sticky=(W, E)) # Advanced tools advanced = ctk.CTkButton(self.left, text="Advanced Tools", command=lambda:(AdvancedAnalysis(self)), - fg_color="#000000", text_color="white", border_color="white", border_width=1, - hover_color="#FFBF00") + fg_color="#000000", text_color="white", border_color="white", border_width=1, + hover_color="#FFBF00") advanced.grid(row=20, column=0, columnspan=2, sticky=(W, E)) # Create right side framing for functionalities self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0) self.right.grid(column=1, row=0, sticky=(N, S, E, W)) - # Configure columns with a loop + # Configure columns with a loop for col in range(2): self.right.columnconfigure(col, weight=1) @@ -271,9 +273,9 @@ def __init__(self, master): # Create info button # Information Button info_path = master_path + "/gui_files/Info.png" # Get infor button path - self.info = customtkinter.CTkImage(light_image=Image.open(info_path), - size=(30,30)) - info_button = customtkinter.CTkButton( + self.info = ctk.CTkImage(light_image=Image.open(info_path), + size=(30,30)) + info_button = ctk.CTkButton( self.right, image=self.info, text="", @@ -288,9 +290,9 @@ def __init__(self, master): # Button for online tutorials online_path = master_path + "/gui_files/Online.png" - self.online = customtkinter.CTkImage(light_image=Image.open(online_path), - size=(30,30)) - online_button = customtkinter.CTkButton( + self.online = ctk.CTkImage(light_image=Image.open(online_path), + size=(30,30)) + online_button = ctk.CTkButton( self.right, image=self.online, text="", @@ -305,9 +307,9 @@ def __init__(self, master): # Button for dev information redirect_path = master_path + "/gui_files/Redirect.png" - self.redirect = customtkinter.CTkImage(light_image=Image.open(redirect_path), - size=(30,30)) - redirect_button = customtkinter.CTkButton( + self.redirect = ctk.CTkImage(light_image=Image.open(redirect_path), + size=(30,30)) + redirect_button = ctk.CTkButton( self.right, image=self.redirect, text="", @@ -321,9 +323,9 @@ def __init__(self, master): # Button for contact information contact_path = master_path + "/gui_files/Contact.png" - self.contact = customtkinter.CTkImage(light_image=Image.open(contact_path), - size=(30,30)) - contact_button = customtkinter.CTkButton( + self.contact = ctk.CTkImage(light_image=Image.open(contact_path), + size=(30,30)) + contact_button = ctk.CTkButton( self.right, image=self.contact, text="", @@ -337,9 +339,9 @@ def __init__(self, master): # Button for citatoin information cite_path = master_path + "/gui_files/Cite.png" - self.cite = customtkinter.CTkImage(light_image=Image.open(cite_path), - size=(30,30)) - cite_button = customtkinter.CTkButton( + self.cite = ctk.CTkImage(light_image=Image.open(cite_path), + size=(30,30)) + cite_button = ctk.CTkButton( self.right, image=self.cite, text="", @@ -539,14 +541,14 @@ def load_file(): progress.grid_remove() except ValueError: - tk.messagebox.showerror( - "Information", - "When an OTB file is loaded, make sure to " + CTkMessagebox(title="Info", message= "When an OTB file is loaded, make sure to " + "\nspecify an extension factor (number) first." - + "\n" - + "When a DELSYS file is loaded, make sure to " - + "\nspecify the correct folder." - ) + + "\nWhen a DELSYS file is loaded, make sure to " + + "\nspecify the correct folder.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + # End progress progress.stop() progress.grid_remove() @@ -557,21 +559,23 @@ def load_file(): progress.grid_remove() except TypeError: - tk.messagebox.showerror( - "Information", - "Make sure to load correct file" + CTkMessagebox(title="Info", message="Make sure to load correct file" + "\naccording to your specification.", - ) + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + # End progress progress.stop() progress.grid_remove() except KeyError: - tk.messagebox.showerror( - "Information", - "Make sure to load correct file" + CTkMessagebox(title="Info", message="Make sure to load correct file" + "\naccording to your specification.", - ) + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + # End progress progress.stop() progress.grid_remove() @@ -687,12 +691,15 @@ def save_file(): progress.grid_remove() except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") + CTkMessagebox(title="Info", message="Make sure a file is loaded.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") # Indicate Progress - progress = ctk.CTkProgressbar(self.left, mode="indeterminate") + progress = ctk.CTkProgressBar(self.left, mode="indeterminate") progress.grid(row=4, column=0) - progress.start(1) + progress.start() # Create a thread to run the save_file function save_thread = threading.Thread(target=save_file) @@ -714,8 +721,11 @@ def reset_analysis(self): When no file was loaded in the GUI. """ # Get user input and check whether analysis wants to be truly resetted - if tk.messagebox.askokcancel( - "Attention", "Do you really want to reset the analysis?" + if CTkMessagebox( + title="Attention",message="Do you really want to reset the analysis?", + icon="warning", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF" ): # user decided to rest analysis try: @@ -788,11 +798,16 @@ def reset_analysis(self): ) except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") + CTkMessagebox(title="Info", message="Make sure a file is loaded.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") except FileNotFoundError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") - + CTkMessagebox(title="Info", message="Make sure a file is loaded.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") # ----------------------------------------------------------------------------------------------- # Plotting inside of GUI @@ -838,7 +853,10 @@ def in_gui_plotting(self, resdict, plot="idr"): plt.close() except AttributeError: - tk.messagebox.showerror("Information", "Make sure a file is loaded.") + CTkMessagebox(title="Info", message="Make sure a file is loaded.", + icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", + button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", + font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") # ----------------------------------------------------------------------------------------------- # Analysis results display From 9bb5ac78695b3db68617a7484ffa1d25feed6421 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 3 Feb 2024 22:51:37 +0100 Subject: [PATCH 13/57] Added: Tracebacks and Settings --- openhdemg/gui/__init__.py | 5 +- openhdemg/gui/gui_files/gear.png | Bin 0 -> 6350 bytes openhdemg/gui/gui_modules/Error.png | Bin 0 -> 14484 bytes openhdemg/gui/gui_modules/__init__.py | 6 +- .../gui/gui_modules/advanced_analyses.py | 132 +++++--------- openhdemg/gui/gui_modules/analyse_force.py | 17 +- openhdemg/gui/gui_modules/edit_mus.py | 91 +++++----- openhdemg/gui/gui_modules/edit_refsig.py | 59 ++---- openhdemg/gui/gui_modules/error_handler.py | 125 +++++++++++++ openhdemg/gui/gui_modules/gui_helpers.py | 52 ++---- openhdemg/gui/gui_modules/gui_plotting.py | 169 +++++++----------- openhdemg/gui/gui_modules/mu_properties.py | 79 +++----- openhdemg/gui/openhdemg_gui.py | 151 ++++++++++------ openhdemg/gui/settings.py | 15 ++ 14 files changed, 466 insertions(+), 435 deletions(-) create mode 100644 openhdemg/gui/gui_files/gear.png create mode 100644 openhdemg/gui/gui_modules/Error.png create mode 100644 openhdemg/gui/gui_modules/error_handler.py create mode 100644 openhdemg/gui/settings.py diff --git a/openhdemg/gui/__init__.py b/openhdemg/gui/__init__.py index 17035a0..8a84820 100644 --- a/openhdemg/gui/__init__.py +++ b/openhdemg/gui/__init__.py @@ -1,3 +1,4 @@ -__all__ = ["openhdemg_gui"] +__all__ = ["openhdemg_gui", "settings"] -from openhdemg.gui.openhdemg_gui import * \ No newline at end of file +from openhdemg.gui.openhdemg_gui import * +from openhdemg.gui.settings import * \ No newline at end of file diff --git a/openhdemg/gui/gui_files/gear.png b/openhdemg/gui/gui_files/gear.png new file mode 100644 index 0000000000000000000000000000000000000000..cea51cb4140d630f7d2977ae230f5d9727857265 GIT binary patch literal 6350 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 T=7Im@Lz9CkN)@~|{PKSQyVtkk literal 0 HcmV?d00001 diff --git a/openhdemg/gui/gui_modules/__init__.py b/openhdemg/gui/gui_modules/__init__.py index 964c773..0eb910f 100644 --- a/openhdemg/gui/gui_modules/__init__.py +++ b/openhdemg/gui/gui_modules/__init__.py @@ -1,5 +1,6 @@ __all__ = ["edit_mus", "edit_refsig", "gui_helpers", "analyse_force", - "mu_properties", "gui_plotting", "advanced_analyses"] + "mu_properties", "gui_plotting", "advanced_analyses", + "error_handler"] from openhdemg.gui.gui_modules.edit_mus import * from openhdemg.gui.gui_modules.edit_refsig import * @@ -7,4 +8,5 @@ from openhdemg.gui.gui_modules.analyse_force import * from openhdemg.gui.gui_modules.mu_properties import * from openhdemg.gui.gui_modules.gui_plotting import * -from openhdemg.gui.gui_modules.advanced_analyses import * \ No newline at end of file +from openhdemg.gui.gui_modules.advanced_analyses import * +from openhdemg.gui.gui_modules.error_handler import * diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index ac65784..536f212 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -2,9 +2,9 @@ from tkinter import ttk, W, E, N, S, StringVar, BooleanVar import customtkinter as ctk -from CTkMessagebox import CTkMessagebox from pandastable import Table import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules.error_handler import show_error_dialog class AdvancedAnalysis: """ @@ -104,16 +104,10 @@ def __init__(self, parent): # Disable config for DELSYS files try: if self.parent.resdict["SOURCE"] == "DELSYS": - CTkMessagebox(title="Info", message="Advanced Tools for Delsys are only accessible from the library.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + show_error_dialog(parent=self, error=None, solution=str("Advanced Tools for Delsys are only accessible from the library.")) return - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) return # Open window @@ -395,12 +389,8 @@ def advanced_analysis(self): n_rows=list_rcs[0], n_cols=list_rcs[1] ) - except ValueError: - CTkMessagebox(title="Info", message="Number of specified rows and columns must match" + "\nnumber of channels.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Number of specified rows and columns must match number of channels.")) return # # DELSYS conduction velocity not available # elif self.mat_code_adv.get() == "Trigno Galileo Sensor": @@ -409,7 +399,7 @@ def advanced_analysis(self): # "MUs conduction velocity estimation is not available for this matrix." # ) # return - + else: # Sort emg file sorted_rawemg = openhdemg.sort_rawemg( @@ -423,22 +413,15 @@ def advanced_analysis(self): sorted_rawemg=sorted_rawemg, ) - except AttributeError: - CTkMessagebox(title="Info", message="Please make sure to load a file prior to Conduction velocity calculation.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Please make sure to load a file prior to Conduction velocity calculation.")) self.head.destroy() - except ValueError: - CTkMessagebox(title="Info", message="Please make sure to enter valid Rows, Columns arguments." - + "\nArguments must be non-negative and seperated by `,`.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Please make sure to enter valid Rows, Columns arguments." + + "Arguments must be non-negative and seperated by `,`.")) self.head.destroy() - # Destroy first window to avoid too many pop-ups self.a_window.destroy() @@ -471,11 +454,8 @@ def open_emgfile1(self): # Add filename to GUI ctk.CTkLabel(self.head, text="File 1 loaded", font=('Segoe UI',15, 'bold')).grid(column=1, row=2) - except ValueError: - CTkMessagebox(title="Info", message="Make sure to specify a valid filetype or extension factor.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to specify a valid filetype or extension factor.")) def open_emgfile2(self): """ @@ -505,11 +485,8 @@ def open_emgfile2(self): # Add filename to GUI ctk.CTkLabel(self.head, text="File 2 loaded", font=('Segoe UI',15, 'bold')).grid(column=1, row=3) - except ValueError: - CTkMessagebox(title="Info", message="Make sure to specify a valid filetype or extension factor.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to specify a valid filetype or extension factor.")) def on_filetype_change_adv(self, *args): """ @@ -579,11 +556,9 @@ def track_mus(self): else: n_rows = None n_cols = None - except ValueError: - CTkMessagebox(title="Info", message="Verify that Rows and Columns are separated by ','", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Verify that Rows and Columns are separated by ','")) + try: # Track motor units tracking_res = openhdemg.tracking( @@ -618,24 +593,19 @@ def track_mus(self): track_table = Table(track_terminal, dataframe=tracking_res) track_table.show() - except AttributeError: - CTkMessagebox(title="Info", message="Make sure to load all required EMG files prior to tracking.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - - except ValueError: - CTkMessagebox(title="Info", message= - "Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Extension Factor (in case of OTB file)" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Threshold" - + "\n - Rows, Columns", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to load all required EMG files prior to tracking.")) + + except ValueError as e: + show_error_dialog(parent=self, error=e, + solution=str("Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Extension Factor (in case of OTB file)" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Threshold" + + "\n - Rows, Columns")) + def remove_duplicates_between(self): """ @@ -666,11 +636,8 @@ def remove_duplicates_between(self): else: n_rows = None n_cols = None - except ValueError: - CTkMessagebox(title="Info", message="Verify that Rows and Columns are separated by ','", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Verify that Rows and Columns are separated by ','")) try: # Remove motor unit duplicates @@ -692,21 +659,16 @@ def remove_duplicates_between(self): openhdemg.asksavefile(emg_file1) openhdemg.asksavefile(emg_file2) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure to load all required EMG files prior to tracking.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - - except ValueError: - CTkMessagebox(title="Info", message="Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Extension Factor (in case of OTB file)" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Threshold" - + "\n - Which" - + "\n - Rows, Columns", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to load all required EMG files prior to tracking.")) + + except ValueError as e: + show_error_dialog(parent=self, error=e, + solution=str("Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Extension Factor (in case of OTB file)" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Threshold" + + "\n - Which" + + "\n - Rows, Columns",)) diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index 663c38d..3b201d2 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -2,9 +2,10 @@ from tkinter import ttk, W, E, StringVar import customtkinter as ctk -from CTkMessagebox import CTkMessagebox import pandas as pd import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules.error_handler import show_error_dialog + class AnalyseForce: """ @@ -123,11 +124,8 @@ def get_mvc(self): # Display results self.parent.display_results(input_df=self.parent.mvc_df) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) def get_rfd(self): """ @@ -157,8 +155,5 @@ def get_rfd(self): # Display results self.parent.display_results(input_df=self.parent.rfd) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") \ No newline at end of file + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index 4039c31..530f3f6 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -1,10 +1,10 @@ """Module containing the MU Removal GUI class""" from tkinter import StringVar, W, E -import os import customtkinter as ctk -from CTkMessagebox import CTkMessagebox import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules.error_handler import show_error_dialog + class MURemovalWindow: """ @@ -54,7 +54,7 @@ class MURemovalWindow: attribute, which is expected to contain specific keys and values relevant to the MU analysis. """ - def __init__(self, parent, resdict): + def __init__(self, parent): """ Initialize a new instance of the MURemovalWindow class. @@ -68,9 +68,6 @@ def __init__(self, parent, resdict): parent : object The parent widget, typically the main application window, to which this MURemovalWindow instance belongs. The parent is used for accessing shared resources and data. - resdict : dict - A dictionary containing relevant data and settings for the motor unit analysis, - including the number of MUs. Raises ------ @@ -78,37 +75,41 @@ def __init__(self, parent, resdict): If certain widgets or properties are not properly instantiated due to missing parent configurations or resources. """ - self.parent = parent - self.resdict = resdict - # Create new window - self.head = ctk.CTkToplevel(fg_color="LightBlue4") - # Set the background color of the top-level window - self.head.title("Motor Unit Removal Window") - self.head.wm_iconbitmap() - self.head.grab_set() - - # Select Motor Unit - ctk.CTkLabel(self.head, text="Select MU:", font=('Segoe UI',15, 'bold')).grid( - column=1, row=0, padx=5, pady=5, sticky=W - ) - - self.mu_to_remove = StringVar() - removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] - removed_mu_value = list(map(str, removed_mu_value)) - removed_mu = ctk.CTkComboBox( - self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value, state="readonly" - ) - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) - - # Remove Motor unit - remove = ctk.CTkButton(self.head, text="Remove MU", command=self.remove, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) - - # Remove empty MUs - remove_empty = ctk.CTkButton(self.head, text="Remove empty MUs", command=self.remove_empty, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - remove_empty.grid(column=2, row=2, padx=5, pady=5) + try: + self.parent = parent + # Create new window + self.head = ctk.CTkToplevel(fg_color="LightBlue4") + # Set the background color of the top-level window + self.head.title("Motor Unit Removal Window") + self.head.wm_iconbitmap() + self.head.grab_set() + + # Select Motor Unit + ctk.CTkLabel(self.head, text="Select MU:", font=('Segoe UI',15, 'bold')).grid( + column=1, row=0, padx=5, pady=5, sticky=W + ) + + self.mu_to_remove = StringVar() + removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] + removed_mu_value = list(map(str, removed_mu_value)) + removed_mu = ctk.CTkComboBox( + self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value, state="readonly" + ) + removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + + # Remove Motor unit + remove = ctk.CTkButton(self.head, text="Remove MU", command=self.remove, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) + + # Remove empty MUs + remove_empty = ctk.CTkButton(self.head, text="Remove empty MUs", command=self.remove_empty, + fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + remove_empty.grid(column=2, row=2, padx=5, pady=5) + + except AttributeError as e: + self.head.destroy() + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) def remove(self): """ @@ -145,12 +146,9 @@ def remove(self): if hasattr(self.parent, "fig"): self.parent.in_gui_plotting(resdict=self.parent.resdict) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + def remove_empty(self): """ Instance method that removes all empty MUs. @@ -185,8 +183,5 @@ def remove_empty(self): if hasattr(self.parent, "fig"): self.parent.in_gui_plotting(resdict=self.parent.resdict) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") \ No newline at end of file + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index 8429c08..7e722a6 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -2,9 +2,10 @@ from tkinter import ttk, W, E, StringVar, DoubleVar import customtkinter as ctk -from CTkMessagebox import CTkMessagebox import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules.error_handler import show_error_dialog + class EditRefsig: """ @@ -211,11 +212,8 @@ def filter_refsig(self): # Plot filtered Refsig self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_fil") - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) def remove_offset(self): """ @@ -243,17 +241,11 @@ def remove_offset(self): # Update Plot self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - - except ValueError: - CTkMessagebox(title="Info", message="Make sure to specify valid filtering or offset values.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) + + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to specify valid filtering or offset values.")) def convert_refsig(self): """ @@ -275,21 +267,15 @@ def convert_refsig(self): self.parent.resdict["REF_SIGNAL"] = self.parent.resdict["REF_SIGNAL"] * self.convert_factor.get() elif self.convert.get() == "Divide": self.parent.resdict["REF_SIGNAL"] = self.parent.resdict["REF_SIGNAL"] / self.convert_factor.get() - + # Update Plot self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) - except ValueError: - CTkMessagebox(title="Info", message="Make sure to specify valid conversion factor.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to specify valid conversion factor.")) def to_percent(self): """ @@ -307,18 +293,11 @@ def to_percent(self): """ try: self.parent.resdict["REF_SIGNAL"] = (self.parent.resdict["REF_SIGNAL"] * 100) / self.mvc_value.get() - # Update Plot self.parent.in_gui_plotting(resdict=self.parent.resdict) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a Refsig file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - - except ValueError: - CTkMessagebox(title="Info", message="Make sure to specify valid conversion factor.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) + + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to specify valid conversion factor.")) diff --git a/openhdemg/gui/gui_modules/error_handler.py b/openhdemg/gui/gui_modules/error_handler.py new file mode 100644 index 0000000..6e55054 --- /dev/null +++ b/openhdemg/gui/gui_modules/error_handler.py @@ -0,0 +1,125 @@ +"""Module containing the error message designs""" + +import os +import traceback +import customtkinter as ctk +from PIL import Image + + +class ErrorDialog: + """ + A dialog window for displaying error messages and solutions to the user. + + This class creates a custom dialog window using customtkinter (ctk) components to show an error message + and a potential solution. The dialog includes an information icon, labels for the error and solution, + and is styled with specific foreground and background colors. + + Parameters + ---------- + parent : tk.Tk or ctk.CTk + The parent window to which this dialog is attached. It can be either a Tkinter root window + or another customtkinter component. + error : str + The error message to be displayed in the dialog. This should describe what went wrong. + solution : str + A message providing a potential solution or workaround for the error described. + + Attributes + ---------- + parent : tk.Tk or ctk.CTk + The parent window of the dialog. + head : ctk.CTkToplevel + The top-level window component of the dialog. + content_frame : ctk.CTkFrame + A frame widget that holds the content of the dialog, including the error and solution messages. + info_photo : ctk.CTkImage + The photo image of the information icon displayed in the dialog. + icon : ctk.CTkLabel + A label widget used to display the information icon. + icon_info : ctk.CTkLabel + A label widget displaying the text "INFORMATION" below the icon. + solution_label : ctk.CTkLabel + A label widget used to display the solution message. + error_label : ctk.CTkLabel + A label widget used to display the error message. + + Methods + ------- + __init__(self, parent, error, solution) + Initializes the ErrorDialog with the parent window, error message, and solution message. + + Example + ------- + >>> import tkinter as tk + >>> import customtkinter as ctk + >>> root = tk.Tk() + >>> error = "This is a sample error message." + >>> solution = "Try restarting the application." + >>> ErrorDialog(root, error, solution) + >>> root.mainloop() + """ + def __init__(self, parent, error, solution): + self.parent = parent + self.head = ctk.CTkToplevel(fg_color="#FFBF00") + self.head.title("Error Dialog") + self.head.geometry("500x300") # Adjust the size as needed + path = os.path.dirname(os.path.abspath(__file__)) + + # Create a frame for the content with blue background, placed in the middle + self.content_frame = ctk.CTkFrame(self.head, corner_radius=10, fg_color="LightBlue4", bg_color="#FFBF00") + self.content_frame.pack(padx=50, expand=True, fill="both") + + # Load an information icon and display it + self.info_photo = ctk.CTkImage(light_image=Image.open(path + "/Error.png"), size=(50, 50)) + self.icon = ctk.CTkLabel(self.content_frame, text="", + image=self.info_photo, bg_color="LightBlue4") + self.icon.pack(pady=5) + self.icon_info = ctk.CTkLabel(self.content_frame, + text="INFORMATION", font=("Arial", 16, "bold"), + text_color="#123456") + self.icon_info.pack(pady=5) + + # Error solution label (larger, bold), placed below the icon + self.solution_label = ctk.CTkLabel(self.content_frame, text=solution, + font=("Arial", 14, "bold"), wraplength=400, fg_color="LightBlue4") + self.solution_label.pack(pady=(10, 5)) + + # Error traceback label (smaller) + self.error_label = ctk.CTkLabel(self.content_frame, text=error, + font=("Arial", 10), wraplength=400, fg_color="LightBlue4") + self.error_label.pack(pady=(5, 10), expand=True, fill='both') + + # Make window modal + self.head.grab_set() # Redirect all events to this window + + +def show_error_dialog(parent, error, solution): + """ + Displays an ErrorDialog with a formatted error traceback and a solution message. + + This function takes an exception as input, formats its traceback, and then creates + an ErrorDialog to display the formatted traceback along with a solution message. + + Parameters + ---------- + parent : tk.Tk or ctk.CTk + The parent window to which this dialog will be attached. + error : Exception + The exception object from which the error message will be derived. + solution : str + A message providing a potential solution or workaround for the error. + + Example + ------- + >>> import tkinter as tk + >>> import customtkinter as ctk + >>> root = tk.Tk() + >>> try: + ... # Some operation that raises an exception + ... raise ValueError("A sample error.") + ... except Exception as e: + ... show_error_dialog(root, e, "Check the value and try again.") + >>> root.mainloop() + """ + error_message = "".join(traceback.format_exception(type(error), value=error, tb=error.__traceback__)) + ErrorDialog(parent, error_message, solution) diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index a72c9f8..b05e99c 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -2,10 +2,11 @@ from tkinter import W, E, filedialog import customtkinter as ctk -from CTkMessagebox import CTkMessagebox import pandas as pd import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules.error_handler import show_error_dialog + class GUIHelpers: """ @@ -111,11 +112,8 @@ def resize_file(self): column=2, row=4, sticky=(W, E) ) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) def export_to_excel(self): """ @@ -156,24 +154,14 @@ def export_to_excel(self): writer.close() - except IndexError: - CTkMessagebox(title="Info", message="Please conduct at least one analysis before saving.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - - except PermissionError: - CTkMessagebox(title="Info", message="If /Results.xlsx already opened, please close." - + "\nOtherwise ignore as you propably canceled the saving progress.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except IndexError as e: + show_error_dialog(parent=self, error=e, solution=str("Please conduct at least one analysis before saving.")) + + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + + except PermissionError as e: + show_error_dialog(parent=self, error=e, solution=str("If /Results.xlsx already opened, please close.")) def sort_mus(self): """ @@ -199,17 +187,11 @@ def sort_mus(self): if hasattr(self.parent, "fig"): self.parent.in_gui_plotting(resdict=self.parent.resdict) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) except KeyError: - CTkMessagebox(title="Info", message="Sorting not possible when ≤ 1" - + "\nMU is present in the File (i.e. Refsigs)", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + show_error_dialog(parent=self, error=e, + solution=str("Sorting not possible when ≤ 1" + + "\nMU is present in the File (i.e. Refsigs)")) \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 18afa36..0ef9e01 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -6,8 +6,9 @@ from PIL import Image import customtkinter as ctk -from CTkMessagebox import CTkMessagebox import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules.error_handler import show_error_dialog + class PlotEmg: """ @@ -359,11 +360,8 @@ def __init__(self, parent): for child in self.head.winfo_children(): child.grid_configure(padx=5, pady=5) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) self.head.destroy() ### Define functions for motor unit plotting @@ -442,21 +440,14 @@ def plt_emgsignal(self): figsize=figsize, ) - except ValueError: - CTkMessagebox(title="Info", message="Enter valid channel number or non-negative figure size.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except KeyError: - CTkMessagebox(title="Info", message="Enter valid channel number.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except IndexError: - CTkMessagebox(title="Info", message="Enter valid figure size. Must be non negative and tuple of (heigth, width).", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid channel number or non-negative figure size.")) + + except KeyError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid channel number.")) + + except IndexError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid figure size. Must be non negative and tuple of (heigth, width).")) def plt_refsignal(self): """ @@ -509,11 +500,8 @@ def plt_mupulses(self): figsize=figsize, ) - except ValueError: - CTkMessagebox(title="Info", message="Enter valid linewidth number.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid linewidth number.")) def plt_ipts(self): """ @@ -573,17 +561,11 @@ def plt_ipts(self): figsize=figsize, ) - except ValueError: - CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) - except KeyError: - CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except KeyError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) def plt_idr(self): """ @@ -640,17 +622,11 @@ def plt_idr(self): figsize=figsize, ) - except ValueError: - CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) - except KeyError: - CTkMessagebox(title="Info", message="Enter valid motor unit number.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except KeyError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) def plot_derivation(self): """ @@ -687,13 +663,8 @@ def plot_derivation(self): n_cols=list_rcs[1] ) - except ValueError: - CTkMessagebox(title="Info", message="Number of specified rows and columns must match" + - "\nnumber of channels.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Number of specified rows and columns must match number of channels.")) return else: @@ -723,26 +694,20 @@ def plot_derivation(self): timeinseconds=eval(self.time_sec.get()), figsize=figsize, ) - except ValueError: - CTkMessagebox(title="Info", message="Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Rows, Columns arguments", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except UnboundLocalError: - CTkMessagebox(title="Info", message="Enter valid Configuration and Matrix Column.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except KeyError: - CTkMessagebox(title="Info", message="Enter valid Matrix Column.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, + solution=str("Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Rows, Columns arguments")) + + except UnboundLocalError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid Configuration and Matrix Column.")) + + except KeyError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid Matrix Column.")) def plot_muaps(self): """ @@ -789,12 +754,8 @@ def plot_muaps(self): n_cols=list_rcs[1] ) - except ValueError: - CTkMessagebox(title="Info", message="Number of specified rows and columns must match" - + "\nnumber of channels.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Number of specified rows and columns must match")) return else: @@ -830,27 +791,29 @@ def plot_muaps(self): # Plot MUAPS openhdemg.plot_muaps(sta_dict[int(self.muap_munum.get())], figsize=figsize) - except ValueError: - CTkMessagebox(title="Info", message="Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Timewindow" - + "\n - MU Number" - + "\n - Rows, Columns arguments", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - - except UnboundLocalError: - CTkMessagebox(title="Info", message="Enter valid Configuration.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except KeyError: - CTkMessagebox(title="Info", message="Enter valid Matrix Column.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - \ No newline at end of file + except ValueError as e: + show_error_dialog(parent=self, error=e, + solution=str("Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Timewindow" + + "\n - MU Number" + + "\n - Rows, Columns arguments")) + + show_error_dialog(parent=self, error=e, + solution=str("Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Timewindow" + + "\n - MU Number" + + "\n - Rows, Columns arguments")) + + except UnboundLocalError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid Configuration.")) + + except KeyError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid Matrix Column.")) diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index 164a0c0..6fa4cdf 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -2,8 +2,8 @@ from tkinter import ttk, W, E, StringVar import customtkinter as ctk -from CTkMessagebox import CTkMessagebox import openhdemg.library as openhdemg +from openhdemg.gui.gui_modules.error_handler import show_error_dialog class MuAnalysis: """ @@ -224,21 +224,14 @@ def compute_mu_threshold(self): # Display results self.parent.display_results(self.parent.mu_thresholds) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except ValueError: - CTkMessagebox(title="Info", message="Enter valid MVC, Event or Type.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except AssertionError: - CTkMessagebox(title="Info", message="Specify Event and/or Type.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid MVC, Event or Type.")) + + except AssertionError as e: + show_error_dialog(parent=self, error=e, solution=str("Specify Event and/or Type.")) def compute_mu_dr(self): """ @@ -272,21 +265,14 @@ def compute_mu_dr(self): # Display results self.parent.display_results(self.parent.mus_dr) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except ValueError: - CTkMessagebox(title="Info", message="Enter valid Firings value or select a correct number of points.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except AssertionError: - CTkMessagebox(title="Info", message="Specify Event and/or Type.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid Firings value or select a correct number of points.")) + + except AssertionError as e: + show_error_dialog(parent=self, error=e, solution=str("Specify Event and/or Type.")) def basic_mus_properties(self): """ @@ -322,23 +308,14 @@ def basic_mus_properties(self): # Display results self.parent.display_results(self.parent.mu_prop_df) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except ValueError: - CTkMessagebox(title="Info", message="Enter valid MVC value or select a correct number of points.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except AssertionError: - CTkMessagebox(title="Info", message="Specify Event and/or Type.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - except UnboundLocalError: - CTkMessagebox(title="Info", message="Select start/end area again.", icon="info", - bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("Enter valid MVC value or select a correct number of points.")) + + except AssertionError as e: + show_error_dialog(parent=self, error=e, solution=str("Specify Event and/or Type.")) + + except UnboundLocalError as e: + show_error_dialog(parent=self, error=e, solution=str("Select start/end area again.")) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 6c98415..4f9d658 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -3,6 +3,10 @@ """ import os +import sys +import subprocess +import importlib + import tkinter as tk import threading import webbrowser @@ -20,7 +24,11 @@ import openhdemg.library as openhdemg from openhdemg.gui.gui_modules import (MURemovalWindow, EditRefsig, GUIHelpers, - AnalyseForce, MuAnalysis, PlotEmg, AdvancedAnalysis) + AnalyseForce, MuAnalysis, PlotEmg, AdvancedAnalysis, + show_error_dialog) +# This loads the settings file directly +import settings + matplotlib.use("TkAgg") class emgGUI(): @@ -123,6 +131,9 @@ def __init__(self, master): master: tk tk class object """ + # Load settings + self.load_settings() + # Set up GUI self.master = master self.master.title("openhdemg") @@ -135,7 +146,7 @@ def __init__(self, master): self.master.rowconfigure(0, weight=1) # Create left side framing for functionalities - self.left = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0) + self.left = ctk.CTkFrame(self.master, fg_color=self.settings['background_color'], corner_radius=0) self.left.grid(column=0, row=0, sticky="nsew") # Configure columns with a loop @@ -167,7 +178,7 @@ def __init__(self, master): ctk.CTkLabel(self.left, text="File length:", font=('Segoe UI',15, 'bold')).grid(column=1, row=4, sticky=(W)) separator0 = ttk.Separator(self.left, orient="horizontal") separator0.grid(column=0, columnspan=3, row=5, sticky=(W, E)) - + # Save File save = ctk.CTkButton(self.left, text="Save File", command=self.save_emgfile, fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) @@ -193,7 +204,7 @@ def __init__(self, master): separator2.grid(column=0, columnspan=3, row=9, sticky=(W, E)) # Remove Motor Units - remove_mus = ctk.CTkButton(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self, resdict=self.resdict)), + remove_mus = ctk.CTkButton(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self)), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) remove_mus.grid(column=0, row=10, sticky=W) @@ -258,11 +269,11 @@ def __init__(self, master): # Create empty figure self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=False) self.canvas = FigureCanvasTkAgg(self.first_fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=5) + self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6) # Create logo figure self.logo_canvas = Canvas(self.right, height=590, width=800, bg="white") - self.logo_canvas.grid(row=0, column=0, rowspan=5) + self.logo_canvas.grid(row=0, column=0, rowspan=6) logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) @@ -270,7 +281,20 @@ def __init__(self, master): # self.matrix = tk.PhotoImage(file="Matrix_illustration.png") self.logo_canvas.create_image(400, 300, anchor="center", image=self.logo) - # Create info button + # Create info buttons + # Settings button + gear_path = master_path + "/gui_files/gear.png" + self.gear = ctk.CTkImage(light_image=Image.open(gear_path), + size=(30,30)) + + settings_b = ctk.CTkButton(self.right, text="", image=self.gear, + command=self.open_settings, + width=30, + height=30, + bg_color="LightBlue4", + fg_color="LightBlue4") + settings_b.grid(column=1, row=0, sticky=E) + # Information Button info_path = master_path + "/gui_files/Info.png" # Get infor button path self.info = ctk.CTkImage(light_image=Image.open(info_path), @@ -286,7 +310,7 @@ def __init__(self, master): command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/"))), ) - info_button.grid(row=0, column=1, sticky=E) + info_button.grid(row=1, column=1, sticky=E) # Button for online tutorials online_path = master_path + "/gui_files/Online.png" @@ -303,7 +327,7 @@ def __init__(self, master): command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/"))), ) - online_button.grid(row=1, column=1, sticky=E) + online_button.grid(row=2, column=1, sticky=E) # Button for dev information redirect_path = master_path + "/gui_files/Redirect.png" @@ -319,7 +343,7 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers"))), ) - redirect_button.grid(row=2, column=1, sticky=E) + redirect_button.grid(row=3, column=1, sticky=E) # Button for contact information contact_path = master_path + "/gui_files/Contact.png" @@ -335,7 +359,7 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/"))), ) - contact_button.grid(row=3, column=1, sticky=E) + contact_button.grid(row=4, column=1, sticky=E) # Button for citatoin information cite_path = master_path + "/gui_files/Cite.png" @@ -351,12 +375,46 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/"))), ) - cite_button.grid(row=4, column=1, sticky=E) + cite_button.grid(row=5, column=1, sticky=E, pady=0) for child in self.left.winfo_children(): child.grid_configure(padx=5, pady=5) ## Define functionalities for buttons used in GUI master window + def load_settings(self): + + # Load settings file + importlib.reload(settings) + self.settings = settings.settings + self.update_gui_variables() + + def open_settings(self): + """ + Instance Method to load the setting file for. + + Executed when the button "Settings" in master GUI window is pressed. + A python file is openend containing a dictionary with relevant variables + that users should be able to customize. + """ + # Determine relative filepath + file_path = "settings.py" + + # Check for operating system and open in default editor + if sys.platform.startswith('darwin'): # macOS + subprocess.run(['open', file_path]) + elif sys.platform.startswith('win32'): # Windows + os.startfile(file_path) + else: # Linux or other + subprocess.run(['xdg-open', file_path]) + + + # Unused (yet) + def update_gui_variables(self): + """ + Method to update variables changes in the settings file + """ + pass + def get_file_input(self): """ @@ -540,45 +598,36 @@ def load_file(): progress.stop() progress.grid_remove() - except ValueError: - CTkMessagebox(title="Info", message= "When an OTB file is loaded, make sure to " - + "\nspecify an extension factor (number) first." - + "\nWhen a DELSYS file is loaded, make sure to " - + "\nspecify the correct folder.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + except ValueError as e: + show_error_dialog(parent=self, error=e, solution=str("When an OTB file is loaded, make sure to " + + "specify an extension factor (number) first." + + "\nWhen a DELSYS file is loaded, make sure to " + + "specify the correct folder.")) # End progress progress.stop() progress.grid_remove() - except FileNotFoundError: + except FileNotFoundError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to load correct file" + + "according to your specification.")) # End progress progress.stop() progress.grid_remove() - except TypeError: - CTkMessagebox(title="Info", message="Make sure to load correct file" - + "\naccording to your specification.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + except TypeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to load correct file" + + "according to your specification.")) # End progress progress.stop() progress.grid_remove() - except KeyError: - CTkMessagebox(title="Info", message="Make sure to load correct file" - + "\naccording to your specification.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + except KeyError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure to load correct file" + + "according to your specification.")) # End progress progress.stop() progress.grid_remove() + except: # End progress progress.stop() @@ -601,10 +650,6 @@ def on_filetype_change(self, *args): create a second combobox on the grid at column 0 and row 2 and when the filetype is set to something else it will remove the second combobox from the grid. """ - # Forget previous widget when filetype is changes - # NOTE I had to separate them and put them on top of the function to - # ensure that changing file type consecutively would not miss the - # previous entry or combobox. if self.filetype.get() not in ["OTB"]: if hasattr(self, "otb_combobox"): self.otb_combobox.grid_forget() @@ -639,8 +684,6 @@ def on_filetype_change(self, *args): self.otb_combobox.grid(column=0, row=2, sticky=(W, E), padx=5) self.otb_combobox.set("Extension Factor") - # NOTE I forgot to mention, but people should be able to select fsamp for .csv files. - # Please check if this can be done better. elif self.filetype.get() in ["CUSTOMCSV", "CUSTOMCSV_REFSIG"]: self.fsamp = StringVar(value="Fsamp") self.csv_entry = ctk.CTkEntry( @@ -690,7 +733,8 @@ def save_file(): progress.stop() progress.grid_remove() - except AttributeError: + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) CTkMessagebox(title="Info", message="Make sure a file is loaded.", icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", @@ -797,17 +841,11 @@ def reset_analysis(self): sticky=(N, S, W, E), ) - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) - except FileNotFoundError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except FileNotFoundError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) # ----------------------------------------------------------------------------------------------- # Plotting inside of GUI @@ -852,11 +890,8 @@ def in_gui_plotting(self, resdict, plot="idr"): toolbar.grid(row=5, column=0) plt.close() - except AttributeError: - CTkMessagebox(title="Info", message="Make sure a file is loaded.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") + except AttributeError as e: + show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) # ----------------------------------------------------------------------------------------------- # Analysis results display diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py new file mode 100644 index 0000000..4c1a230 --- /dev/null +++ b/openhdemg/gui/settings.py @@ -0,0 +1,15 @@ +""" +Module docstring explaining how to change the GUI settings. +""" + + + + + + +settings = { + 'background_color':"LightBlue4" + + + + } From 2d9f71265dc28035e8427d0036423e088f5e8185 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 4 Feb 2024 09:25:46 +0100 Subject: [PATCH 14/57] Fixed: Reset analysis --- openhdemg/gui/openhdemg_gui.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 4f9d658..191845a 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -10,7 +10,7 @@ import tkinter as tk import threading import webbrowser -from tkinter import ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E +from tkinter import messagebox, ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E import customtkinter as ctk from CTkMessagebox import CTkMessagebox from pandastable import Table, config @@ -735,11 +735,7 @@ def save_file(): except AttributeError as e: show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) - CTkMessagebox(title="Info", message="Make sure a file is loaded.", - icon="info", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF") - + # Indicate Progress progress = ctk.CTkProgressBar(self.left, mode="indeterminate") progress.grid(row=4, column=0) @@ -765,12 +761,8 @@ def reset_analysis(self): When no file was loaded in the GUI. """ # Get user input and check whether analysis wants to be truly resetted - if CTkMessagebox( - title="Attention",message="Do you really want to reset the analysis?", - icon="warning", bg_color="#fdbc00", fg_color="LightBlue4", title_color="#000000", - button_color="#E5E4E2", button_text_color="#000000", button_hover_color="#1e52fe", - font=('Segoe UI',15, 'bold'), text_color="#FFFFFF" - ): + if messagebox.askokcancel(title="Attention", message="Do you really want to reset the analysis?", + icon="warning"): # user decided to rest analysis try: # reload original file From a4b87f6ef3334c526a5516ea30272990ef9b1dd6 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 4 Feb 2024 10:35:21 +0100 Subject: [PATCH 15/57] Fixed: settings & resize --- .../gui/gui_modules/advanced_analyses.py | 5 +- openhdemg/gui/gui_modules/analyse_force.py | 6 +- openhdemg/gui/gui_modules/edit_mus.py | 3 + openhdemg/gui/gui_modules/edit_refsig.py | 5 +- openhdemg/gui/gui_modules/gui_helpers.py | 2 + openhdemg/gui/gui_modules/gui_plotting.py | 8 +- openhdemg/gui/gui_modules/mu_properties.py | 4 +- openhdemg/gui/openhdemg_gui.py | 78 ++++++++++--------- openhdemg/gui/settings.py | 5 +- 9 files changed, 70 insertions(+), 46 deletions(-) diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index 536f212..48aa506 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -98,8 +98,9 @@ def __init__(self, parent): self.emgfile2 = {} self.extension_factor_adv = StringVar() - # Set parent, most probable emgGUI + # Initialize parent and load parent settings self.parent = parent + self.parent.load_settings() # Disable config for DELSYS files try: @@ -153,7 +154,7 @@ def __init__(self, parent): ctk.CTkLabel(self.a_window, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid( row=4, column=0, sticky=(W, E)) self.mat_code_adv = StringVar() - matrix_code_vals = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "None") + matrix_code_vals = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings['delsys_sensor_label'], "None") matrix_code = ctk.CTkComboBox( self.a_window, width=150, variable=self.mat_code_adv, values=matrix_code_vals, state="readonly") diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index 3b201d2..d48f30c 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -68,8 +68,12 @@ def __init__(self, parent): parent configurations or resources. """ - # Create new window + # Initialize parent and load parent settings + self.parent = parent + self.parent.load_settings() + + # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Force Analysis Window") self.head.wm_iconbitmap() diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index 530f3f6..67b8e41 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -76,7 +76,10 @@ def __init__(self, parent): parent configurations or resources. """ try: + # Initialize parent and load parent settings self.parent = parent + self.parent.load_settings() + # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") # Set the background color of the top-level window diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index 7e722a6..521c0f0 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -85,9 +85,12 @@ def __init__(self, parent): If certain widgets or properties are not properly instantiated due to missing parent configurations or resources. """ + # Initialize parent and load parent settings + self.parent = parent + self.parent.load_settings() + # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") - self.parent = parent self.head.title("Reference Signal Editing Window") self.head.wm_iconbitmap() self.head.grab_set() diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index b05e99c..244fa1a 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -67,7 +67,9 @@ def __init__(self, parent): necessary context and data for the helper functions. """ + # Initialize parent and load parent settings self.parent = parent + self.parent.load_settings() def resize_file(self): """ diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 0ef9e01..e88d6a1 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -114,7 +114,10 @@ def __init__(self, parent): """ # Try is block is necessary as some widgets depend on parent resdict try: + # Initialize parent and load parent settings self.parent = parent + self.parent.load_settings() + self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Plot Window") self.head.wm_iconbitmap() @@ -235,7 +238,7 @@ def __init__(self, parent): ctk.CTkLabel(self.head, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid(row=0, column=3, sticky=(W)) self.mat_code = StringVar() - matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "None") + matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings["delsys_sensor_label"], "None") matrix_code = ctk.CTkComboBox(self.head, width=100, variable=self.mat_code, values=matrix_code_values, state="readonly") matrix_code.grid(row=0, column=4, sticky=(W, E)) @@ -386,8 +389,7 @@ def on_matrix_none(self, *args): if hasattr(self, "row_cols_entry"): self.row_cols_entry.grid_forget() self.mat_label.grid_forget() - - + self.head.update_idletasks() diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index 6fa4cdf..ba8d48a 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -87,8 +87,10 @@ def __init__(self, parent): properties like size, color, and variable bindings and placed in a grid layout. """ - # Create new window + # Initialize parent and load parent settings self.parent = parent + self.parent.load_settings() + # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Motor Unit Properties Window") self.head.wm_iconbitmap() diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 191845a..62aa44c 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -12,7 +12,6 @@ import webbrowser from tkinter import messagebox, ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E import customtkinter as ctk -from CTkMessagebox import CTkMessagebox from pandastable import Table, config from PIL import Image @@ -147,7 +146,7 @@ def __init__(self, master): # Create left side framing for functionalities self.left = ctk.CTkFrame(self.master, fg_color=self.settings['background_color'], corner_radius=0) - self.left.grid(column=0, row=0, sticky="nsew") + self.left.grid(column=0, row=0, sticky=(N, S, E, W)) # Configure columns with a loop for col in range(4): @@ -161,7 +160,7 @@ def __init__(self, master): self.filetype = StringVar() signal_value = ["OPENHDEMG", "DEMUSE", "OTB", "OTB_REFSIG", "DELSYS", "DELSYS_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG"] signal_entry = ctk.CTkComboBox(self.left, width=150, variable=self.filetype, values=signal_value, state="readonly") - signal_entry.grid(column=0, row=1, sticky=(ctk.W, ctk.E)) + signal_entry.grid(column=0, row=1, sticky=(N, S, E, W)) self.filetype.set("Type of file") # Trace filetype to apply function when changeing self.filetype.trace_add("write", self.on_filetype_change) @@ -169,7 +168,7 @@ def __init__(self, master): # Load file load = ctk.CTkButton(self.left, text="Load File", command=self.get_file_input, fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - load.grid(column=0, row=3, sticky=ctk.W) + load.grid(column=0, row=3, sticky=(N, S, E, W)) # File specifications ctk.CTkLabel(self.left, text="Filespecs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=1, sticky=(W)) @@ -177,83 +176,83 @@ def __init__(self, master): ctk.CTkLabel(self.left, text="N of MUs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=3, sticky=(W)) ctk.CTkLabel(self.left, text="File length:", font=('Segoe UI',15, 'bold')).grid(column=1, row=4, sticky=(W)) separator0 = ttk.Separator(self.left, orient="horizontal") - separator0.grid(column=0, columnspan=3, row=5, sticky=(W, E)) + separator0.grid(column=0, columnspan=3, row=5, sticky=(E, W)) # Save File save = ctk.CTkButton(self.left, text="Save File", command=self.save_emgfile, fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - save.grid(column=0, row=6, sticky=W) + save.grid(column=0, row=6, sticky=(N, S, E, W)) separator1 = ttk.Separator(self.left, orient="horizontal") - separator1.grid(column=0, columnspan=3, row=7, sticky=(W, E)) + separator1.grid(column=0, columnspan=3, row=7, sticky=(E, W)) # Export to Excel export = ctk.CTkButton(self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - export.grid(column=1, row=6, sticky=(W, E)) + export.grid(column=1, row=6, sticky=(N, S, E, W)) # View Motor Unit Firings firings = ctk.CTkButton(self.left, text="View MUs", command=lambda: (self.in_gui_plotting(resdict=self.resdict)), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - firings.grid(column=0, row=8, sticky=W) + firings.grid(column=0, row=8, sticky=(N, S, E, W)) # Sort Motor Units sorting = ctk.CTkButton(self.left, text="Sort MUs", command=lambda: (GUIHelpers(parent=self).sort_mus()), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - sorting.grid(column=1, row=8, sticky=(W, E)) + sorting.grid(column=1, row=8, sticky=(N, S, E, W)) separator2 = ttk.Separator(self.left, orient="horizontal") - separator2.grid(column=0, columnspan=3, row=9, sticky=(W, E)) + separator2.grid(column=0, columnspan=3, row=9, sticky=(E, W)) # Remove Motor Units remove_mus = ctk.CTkButton(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self)), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - remove_mus.grid(column=0, row=10, sticky=W) + remove_mus.grid(column=0, row=10, sticky=(N, S, E, W)) separator3 = ttk.Separator(self.left, orient="horizontal") - separator3.grid(column=0, columnspan=3, row=11, sticky=(W, E)) + separator3.grid(column=0, columnspan=3, row=11, sticky=(E, W)) # Filter Reference Signal reference = ctk.CTkButton(self.left, text="RefSig Editing", command=lambda:(EditRefsig(parent=self)), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - reference.grid(column=0, row=12, sticky=W) + reference.grid(column=0, row=12, sticky=(N, S, E, W)) # Resize File resize = ctk.CTkButton(self.left, text="Resize File", command=lambda:(GUIHelpers(parent=self).resize_file()), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - resize.grid(column=1, row=12, sticky=(W, E)) + resize.grid(column=1, row=12, sticky=(N, S, E, W)) separator4 = ttk.Separator(self.left, orient="horizontal") - separator4.grid(column=0, columnspan=3, row=13, sticky=(W, E)) + separator4.grid(column=0, columnspan=3, row=13, sticky=(E, W)) # Force Analysis force = ctk.CTkButton(self.left, text="Analyse Force", command=lambda:(AnalyseForce(parent=self)), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - force.grid(column=0, row=14, sticky=W) + force.grid(column=0, row=14, sticky=(N, S, E, W)) separator5 = ttk.Separator(self.left, orient="horizontal") - separator5.grid(column=0, columnspan=3, row=15, sticky=(W, E)) + separator5.grid(column=0, columnspan=3, row=15, sticky=(E, W)) # Motor Unit properties mus = ctk.CTkButton(self.left, text="MU Properties", command=lambda:(MuAnalysis(parent=self)), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - mus.grid(column=1, row=14, sticky=W) + mus.grid(column=1, row=14, sticky=(N, S, E, W)) separator6 = ttk.Separator(self.left, orient="horizontal") - separator6.grid(column=0, columnspan=3, row=17, sticky=(W, E)) + separator6.grid(column=0, columnspan=3, row=17, sticky=(E, W)) # Plot EMG plots = ctk.CTkButton(self.left, text="Plot EMG", command=lambda:(PlotEmg(parent=self)), fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - plots.grid(column=0, row=16, sticky=W) + plots.grid(column=0, row=16, sticky=(N, S, E, W)) separator7 = ttk.Separator(self.left, orient="horizontal") - separator7.grid(column=0, columnspan=3, row=19, sticky=(W, E)) + separator7.grid(column=0, columnspan=3, row=19, sticky=(E, W)) # Reset Analysis reset = ctk.CTkButton(self.left, text="Reset Analysis", command=self.reset_analysis, fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) - reset.grid(column=1, row=18, sticky=(W, E)) + reset.grid(column=1, row=18, sticky=(N, S, E, W)) # Advanced tools advanced = ctk.CTkButton(self.left, text="Advanced Tools", command=lambda:(AdvancedAnalysis(self)), fg_color="#000000", text_color="white", border_color="white", border_width=1, hover_color="#FFBF00") - advanced.grid(row=20, column=0, columnspan=2, sticky=(W, E)) + advanced.grid(row=20, column=0, columnspan=2, sticky=(N, S, E, W)) # Create right side framing for functionalities self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0) @@ -269,16 +268,15 @@ def __init__(self, master): # Create empty figure self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=False) self.canvas = FigureCanvasTkAgg(self.first_fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6) + self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) # Create logo figure self.logo_canvas = Canvas(self.right, height=590, width=800, bg="white") - self.logo_canvas.grid(row=0, column=0, rowspan=6) + self.logo_canvas.grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) - # self.matrix = tk.PhotoImage(file="Matrix_illustration.png") self.logo_canvas.create_image(400, 300, anchor="center", image=self.logo) # Create info buttons @@ -293,7 +291,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4") - settings_b.grid(column=1, row=0, sticky=E) + settings_b.grid(column=1, row=0, sticky=E, pady=(0, 20)) # Information Button info_path = master_path + "/gui_files/Info.png" # Get infor button path @@ -310,7 +308,7 @@ def __init__(self, master): command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/"))), ) - info_button.grid(row=1, column=1, sticky=E) + info_button.grid(row=1, column=1, sticky=E, pady=(0, 20)) # Button for online tutorials online_path = master_path + "/gui_files/Online.png" @@ -327,7 +325,7 @@ def __init__(self, master): command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/"))), ) - online_button.grid(row=2, column=1, sticky=E) + online_button.grid(row=2, column=1, sticky=E, pady=(0, 20)) # Button for dev information redirect_path = master_path + "/gui_files/Redirect.png" @@ -343,7 +341,7 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers"))), ) - redirect_button.grid(row=3, column=1, sticky=E) + redirect_button.grid(row=3, column=1, sticky=E, pady=(0, 20)) # Button for contact information contact_path = master_path + "/gui_files/Contact.png" @@ -359,7 +357,7 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/"))), ) - contact_button.grid(row=4, column=1, sticky=E) + contact_button.grid(row=4, column=1, sticky=E, pady=(0, 20)) # Button for citatoin information cite_path = master_path + "/gui_files/Cite.png" @@ -375,14 +373,20 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/"))), ) - cite_button.grid(row=5, column=1, sticky=E, pady=0) + cite_button.grid(row=5, column=1, sticky=E, pady=(0, 20)) for child in self.left.winfo_children(): child.grid_configure(padx=5, pady=5) ## Define functionalities for buttons used in GUI master window def load_settings(self): - + """ + Instance Method to load the setting file for. + + Executed each time when the GUI or a toplevel is openened. + The settings specified by the user will then be transferred + to the code and used. + """ # Load settings file importlib.reload(settings) self.settings = settings.settings @@ -390,7 +394,7 @@ def load_settings(self): def open_settings(self): """ - Instance Method to load the setting file for. + Instance Method to open the setting file for. Executed when the button "Settings" in master GUI window is pressed. A python file is openend containing a dictionary with relevant variables @@ -858,7 +862,7 @@ def in_gui_plotting(self, resdict, plot="idr"): -------- plot_refsig, plot_idr in the library. """ - try: # NOTE as I previously said, OPENHDEMG (.json) files can contain anything. better check SOURCE. + try: if self.resdict["SOURCE"] in ["OTB_REFSIG", "CUSTOMCSV_REFSIG", "DELSYS_REFSIG"]: self.fig = openhdemg.plot_refsig( emgfile=resdict, showimmediately=False, tight_layout=True @@ -877,7 +881,7 @@ def in_gui_plotting(self, resdict, plot="idr"): ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=5) + self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6) toolbar = NavigationToolbar2Tk(self.canvas, self.right, pack_toolbar=False) toolbar.grid(row=5, column=0) plt.close() diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index 4c1a230..8f55c9f 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -8,8 +8,11 @@ settings = { - 'background_color':"LightBlue4" + # Graphic parameters changed after restart + 'background_color':"LightBlue4", + # Code parameters adapted without restart + 'delsys_sensor_label':"Trigno Galileo Sensor" } From 7660b691b20a820a5ed81eb26febdda74afe1d02 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 10 Feb 2024 10:56:07 +0100 Subject: [PATCH 16/57] Fixed: GUI resizing --- openhdemg/gui/openhdemg_gui.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 62aa44c..1013ed5 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -142,6 +142,7 @@ def __init__(self, master): # Necessary for resizing self.master.columnconfigure(0, weight=1) + self.master.columnconfigure(1, weight=1) self.master.rowconfigure(0, weight=1) # Create left side framing for functionalities @@ -149,11 +150,11 @@ def __init__(self, master): self.left.grid(column=0, row=0, sticky=(N, S, E, W)) # Configure columns with a loop - for col in range(4): + for col in range(3): self.left.columnconfigure(col, weight=1) # Configure rows with a loop - for row in range(19): + for row in range(21): self.left.rowconfigure(row, weight=1) # Specify filetype @@ -257,10 +258,10 @@ def __init__(self, master): # Create right side framing for functionalities self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0) self.right.grid(column=1, row=0, sticky=(N, S, E, W)) - # Configure columns with a loop - for col in range(2): - self.right.columnconfigure(col, weight=1) + # Configure columns, plot is weighted more icons are not configured + self.right.columnconfigure(0, weight=10) + self.right.columnconfigure(1, weight=0) # Configure rows with a loop for row in range(5): self.right.rowconfigure(row, weight=1) @@ -507,9 +508,6 @@ def load_file(): # load OPENHDEMG (.json) self.resdict = openhdemg.emg_from_json(filepath=self.file_path) # Add filespecs - # NOTE this is not correct because when the user asks to load OPENHDEMG (.json) - # files, these could contain also the reference signal only. Therefore line 2 - # and 3 will crash. I temporarily fixed it, please review it for next release. if self.resdict["SOURCE"] in ["DEMUSE", "OTB", "CUSTOMCSV", "DELSYS"]: ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E)) ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid(column=2, row=3, sticky=(W, E)) @@ -597,6 +595,9 @@ def load_file(): ctk.CTkLabel(self.left, text=" ").grid( column=2, row=4, sticky=(W, E) ) + + for child in self.left.winfo_children(): + child.grid_configure(padx=5, pady=5) # End progress progress.stop() @@ -881,9 +882,9 @@ def in_gui_plotting(self, resdict, plot="idr"): ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6) + self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N,S,W,E)) toolbar = NavigationToolbar2Tk(self.canvas, self.right, pack_toolbar=False) - toolbar.grid(row=5, column=0) + toolbar.grid(row=5, column=0, sticky=S) plt.close() except AttributeError as e: From 4d028b88baba9ab9b63f1b2cf3a60502d31ecbbb Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 10 Feb 2024 11:46:35 +0100 Subject: [PATCH 17/57] Fixed: settings import --- openhdemg/gui/__init__.py | 3 --- openhdemg/gui/gui_modules/advanced_analyses.py | 4 ++-- openhdemg/gui/openhdemg_gui.py | 16 ++++++++-------- openhdemg/gui/settings.py | 17 ++++------------- setup.py | 4 ++-- 5 files changed, 16 insertions(+), 28 deletions(-) diff --git a/openhdemg/gui/__init__.py b/openhdemg/gui/__init__.py index 8a84820..8b13789 100644 --- a/openhdemg/gui/__init__.py +++ b/openhdemg/gui/__init__.py @@ -1,4 +1 @@ -__all__ = ["openhdemg_gui", "settings"] -from openhdemg.gui.openhdemg_gui import * -from openhdemg.gui.settings import * \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index 48aa506..49f5489 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -98,7 +98,7 @@ def __init__(self, parent): self.emgfile2 = {} self.extension_factor_adv = StringVar() - # Initialize parent and load parent settings + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() @@ -154,7 +154,7 @@ def __init__(self, parent): ctk.CTkLabel(self.a_window, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid( row=4, column=0, sticky=(W, E)) self.mat_code_adv = StringVar() - matrix_code_vals = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings['delsys_sensor_label'], "None") + matrix_code_vals = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings.delsys_sensor_label, "None") matrix_code = ctk.CTkComboBox( self.a_window, width=150, variable=self.mat_code_adv, values=matrix_code_vals, state="readonly") diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 1013ed5..fd45636 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -22,11 +22,11 @@ from matplotlib.figure import Figure import openhdemg.library as openhdemg +import openhdemg.gui.settings as settings from openhdemg.gui.gui_modules import (MURemovalWindow, EditRefsig, GUIHelpers, AnalyseForce, MuAnalysis, PlotEmg, AdvancedAnalysis, show_error_dialog) -# This loads the settings file directly -import settings + matplotlib.use("TkAgg") @@ -146,7 +146,7 @@ def __init__(self, master): self.master.rowconfigure(0, weight=1) # Create left side framing for functionalities - self.left = ctk.CTkFrame(self.master, fg_color=self.settings['background_color'], corner_radius=0) + self.left = ctk.CTkFrame(self.master, fg_color=self.settings.background_color, corner_radius=0) self.left.grid(column=0, row=0, sticky=(N, S, E, W)) # Configure columns with a loop @@ -388,9 +388,9 @@ def load_settings(self): The settings specified by the user will then be transferred to the code and used. """ - # Load settings file - importlib.reload(settings) - self.settings = settings.settings + # If not previously imported, just import it + global settings + self.settings = importlib.reload(settings) self.update_gui_variables() def open_settings(self): @@ -402,7 +402,7 @@ def open_settings(self): that users should be able to customize. """ # Determine relative filepath - file_path = "settings.py" + file_path = "openhdemg/gui/settings.py" # Check for operating system and open in default editor if sys.platform.startswith('darwin'): # macOS @@ -598,7 +598,7 @@ def load_file(): for child in self.left.winfo_children(): child.grid_configure(padx=5, pady=5) - + # End progress progress.stop() progress.grid_remove() diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index 8f55c9f..16aa06f 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -2,17 +2,8 @@ Module docstring explaining how to change the GUI settings. """ +# Graphic parameters changed after restart +background_color = "LightBlue4" - - - - -settings = { - # Graphic parameters changed after restart - 'background_color':"LightBlue4", - - - # Code parameters adapted without restart - 'delsys_sensor_label':"Trigno Galileo Sensor" - - } +# Code parameters adapted without restart +delsys_sensor_label = "Trigno Galileo Sensor" diff --git a/setup.py b/setup.py index 5903ef1..469055f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ INSTALL_REQUIRES = [ "customtkinter==5.2.1", - "CTkMessagebox==2.5" + "CTkMessagebox==2.5", "matplotlib==3.8.1", "numpy==1.26.1", "openpyxl==3.1.2", @@ -28,7 +28,7 @@ "openhdemg.compatibility", "openhdemg.gui", "openhdemg.gui.gui_files", - "openhdemg.gui.gui_helpers" + "openhdemg.gui.gui_modules" ] CLASSIFIERS = [ From 7786f0ea99d349c17271980f6cae4be78fe95c7b Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 12 Feb 2024 08:56:58 +0100 Subject: [PATCH 18/57] Initialise the test_suite --- openhdemg/tests/__init__.py | 0 openhdemg/tests/test_openfiles.py | 2 ++ openhdemg/tests/test_suite.py | 39 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openhdemg/tests/__init__.py create mode 100644 openhdemg/tests/test_openfiles.py create mode 100644 openhdemg/tests/test_suite.py diff --git a/openhdemg/tests/__init__.py b/openhdemg/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openhdemg/tests/test_openfiles.py b/openhdemg/tests/test_openfiles.py new file mode 100644 index 0000000..0909d10 --- /dev/null +++ b/openhdemg/tests/test_openfiles.py @@ -0,0 +1,2 @@ +import unittest +from openhdemg.library.openfiles import \ No newline at end of file diff --git a/openhdemg/tests/test_suite.py b/openhdemg/tests/test_suite.py new file mode 100644 index 0000000..cd8ed82 --- /dev/null +++ b/openhdemg/tests/test_suite.py @@ -0,0 +1,39 @@ +""" +Use this module to run all the tests. This will take a while. + +First, you should dowload all the files necessary for the testing. These might +occupy gigabytes and the dowload might be slow. The files are available at: +https://drive.google.com/drive/folders/1suCZSils8rSCs2E3_K25vRCbN3AFDI7F?usp=sharing + +IMPORTANT: Do not alter the content of the dowloaded folder! + +Since the library's functions perform complex tasks and return complex data +structures, these tests can verify that no critical errors occur, but the +accuracy of each function must be assessed independently upon creation, or at +each revision of the code. +""" + + +def test_library_modules(): + pass + + +def test_compatibility_modules(): + pass + + +def test_gui_modules(): + pass + + +if __name__ == "__main__": + # Change the testfiles_dir as needed. + testfiles_dir = "C:/Users/Giacomo/Desktop/PhD Padova/Papers Unipd/Open_HD-EMG Docs and Extras/openhdemg_testfiles" + + print("\nTest Started, wait for its completition.\n") + + test_library_modules() + test_compatibility_modules() + test_gui_modules() + + print("\nTest Finished!\n") From ed2ee7a89040d476203bfc2c237d6b23d2f99a94 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:39:41 +0100 Subject: [PATCH 19/57] Update website and docs --- docs/api_muap.md | 7 +++++++ docs/cite-us.md | 2 +- mkdocs.yml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/api_muap.md b/docs/api_muap.md index f8c25c7..b7e8fb3 100644 --- a/docs/api_muap.md +++ b/docs/api_muap.md @@ -82,6 +82,13 @@ This module contains functions to produce and analyse MUs anction potentials
+::: openhdemg.library.muap.estimate_cv_via_mle + options: + show_root_full_path: False + show_root_heading: True + +
+ ::: openhdemg.library.muap.MUcv_gui options: show_root_full_path: False diff --git a/docs/cite-us.md b/docs/cite-us.md index 5416566..0917449 100644 --- a/docs/cite-us.md +++ b/docs/cite-us.md @@ -1,4 +1,4 @@ -If you use *openhdemg* for your reaserch, please cite our [tutorial article](/isek_jek_tutorials#jek-tutorial-article). Any citation will help us to continue our work. +If you use *openhdemg* for your reaserch, please cite our [tutorial article](isek_jek_tutorials.md#jek-tutorial-article). Any citation will help us to continue our work. Cite us as: diff --git a/mkdocs.yml b/mkdocs.yml index 8f7cea4..eb7a54b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,7 @@ site_description: >- repo_name: openhdemg repo_url: https://github.com/GiacomoValliPhD/openhdemg -copyright: Copyright © 2022 - 2023. The openhdemg community +copyright: Copyright © 2022 - 2024. The openhdemg community theme: name: material From 5c1d31dbb2bf78b9939804a430a40832381165f7 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:40:36 +0100 Subject: [PATCH 20/57] Updated citeus --- openhdemg/library/info.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openhdemg/library/info.py b/openhdemg/library/info.py index f7afc01..a7dc713 100644 --- a/openhdemg/library/info.py +++ b/openhdemg/library/info.py @@ -295,13 +295,17 @@ def citeus(self): >>> emg.info().citeus() """ - cite = { - "Journal": "Waiting for publication...", - "Title": "Thinking about it...", - } + cite = ( + "Valli G, Ritsche P, Casolo A, Negro F, De Vito G. " + + "Tutorial: Analysis of central and peripheral motor unit " + + "properties from decomposed High-Density surface EMG signals " + + "with openhdemg. J Electromyogr Kinesiol. 2024 Feb;74:102850. " + + "doi: 10.1016/j.jelekin.2023.102850. Epub 2023 Nov 30." + ) # Pretty dict printing print("\nCite Us:\n") - print(json.dumps(cite, indent=4)) + print(cite) + print("\n") return cite From 5a098cebfa6ab957d01c5153b90dbf204b68f9c5 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:41:54 +0100 Subject: [PATCH 21/57] New CV implementation Faster implementation of CV estimation via MLE and new function estimate_cv_via_mle() to estimate CV with only 1 line of code. --- openhdemg/library/mathtools.py | 130 +++++++++++----------------- openhdemg/library/muap.py | 153 ++++++++++++++++++++++++--------- 2 files changed, 166 insertions(+), 117 deletions(-) diff --git a/openhdemg/library/mathtools.py b/openhdemg/library/mathtools.py index 5360887..a40849c 100644 --- a/openhdemg/library/mathtools.py +++ b/openhdemg/library/mathtools.py @@ -439,7 +439,7 @@ def derivatives_beamforming(sig, row, teta): Parameters ---------- - sig : pd.Dataframe + sig : np.ndarray The source signal to be used for the calculation. Different channels should be organised in different rows. row : int @@ -454,22 +454,21 @@ def derivatives_beamforming(sig, row, teta): See also -------- - mle_cv_est : Estimate conduction velocity (CV) via maximum likelihood - estimation. + - estimate_cv_via_mle : Estimate signal conduction velocity via maximum + likelihood estimation. + - MUcv_gui : Graphical user interface for the estimation of MUs conduction + velocity. """ - # To implement with nympy arrays instead of pd.Series for performance? - # Check also the use of pandas in row-major. - # Define some necessary variables - total_rows = len(sig) + total_rows = np.shape(sig)[0] m = total_rows - 1 - total_columns = len(sig.columns) - half_of_the_columns = pd.Series(np.arange((round(total_columns/2)))) + 1 + total_columns = np.shape(sig)[1] + half_of_the_columns = np.arange((round(total_columns/2))) + 1 # Create a custom position index with negative and mirrored values for # index < row. - position = np.zeros(len(sig.index)) + position = np.zeros(np.shape(sig)[0]) position[0] = row + 1 # + 1 used to overcome base 0 here and following for i in range(1, row+1): @@ -481,35 +480,30 @@ def derivatives_beamforming(sig, row, teta): position = np.delete(position, 0) # Shift sig and move the value contained in sig[row] to sig[0] - this_row = pd.DataFrame(sig.iloc[row]).transpose() - sig = sig.drop(row).reset_index(drop=True) - sig = pd.concat([this_row, sig], ignore_index=True) + this_row = sig[row, :] + sig = np.delete(sig, (row), axis=0) # axis=0 to delete rows + sig = np.insert(sig, 0, this_row, axis=0) # Calculate fft row-wise (for each signal) - sigfft = pd.DataFrame(0, index=np.arange(total_rows), columns=sig.columns) + sigfft = np.zeros_like(sig, dtype=np.complex128) # Specify dtype as complex for i in range(total_rows): - sigfft.iloc[i] = fft(sig.iloc[i].to_numpy()) + sigfft[i, :] = fft(sig[i, :]) # Create the series used to store the terms of the derivatives - term_de1 = pd.Series(0, np.arange(len(half_of_the_columns))) - term_de2 = pd.Series(0, np.arange(len(half_of_the_columns))) - term_de12 = pd.Series(0, np.arange(len(half_of_the_columns))) - term_de22 = pd.Series(0, np.arange(len(half_of_the_columns))) + term_de1 = np.zeros(np.shape(half_of_the_columns)[0]) + term_de2 = np.zeros(np.shape(half_of_the_columns)[0]) + term_de12 = np.zeros(np.shape(half_of_the_columns)[0]) + term_de22 = np.zeros(np.shape(half_of_the_columns)[0]) # Calculate the first term of the first derivative for i in range(m): for u in range(i+1, m): - s_fft = sigfft.iloc[i+1, list(half_of_the_columns)] - s_conj = np.conj(sigfft.iloc[u+1, list(half_of_the_columns)]) + s_fft = sigfft[i+1, half_of_the_columns] + s_conj = np.conj(sigfft[u+1, half_of_the_columns]) s_exp = np.exp(1j * 2 * np.pi * half_of_the_columns * (position[i]-position[u]) * teta / total_columns) s_last = 2 * np.pi * half_of_the_columns * (position[i]-position[u]) / total_columns - s_fft = s_fft.reset_index(drop=True) - s_conj = s_conj.reset_index(drop=True) - s_exp = s_exp.reset_index(drop=True) - s_last = s_last.reset_index(drop=True) - image = np.imag(s_fft * s_conj * s_exp * s_last) term_de1 = term_de1-image @@ -519,18 +513,13 @@ def derivatives_beamforming(sig, row, teta): # Calculate the second term of the first derivative for i in range(m): - s_fft = sigfft.iloc[i+1, list(half_of_the_columns)] + s_fft = sigfft[i+1, half_of_the_columns] s_exp = np.exp(1j * 2 * np.pi * half_of_the_columns * position[i] * teta / total_columns) s_last = 2 * np.pi * half_of_the_columns * position[i] / total_columns - s_fft = s_fft.reset_index(drop=True) - s_exp = s_exp.reset_index(drop=True) - s_last = s_last.reset_index(drop=True) - term_de2 = term_de2 + (s_fft * s_exp * s_last) - s_conj = np.conj(sigfft.iloc[0, list(half_of_the_columns)]) - s_conj = s_conj.reset_index(drop=True) + s_conj = np.conj(sigfft[0, half_of_the_columns]) term_de2 = 2 * np.imag(s_conj * term_de2) / m @@ -541,16 +530,11 @@ def derivatives_beamforming(sig, row, teta): for i in range(m): for u in range(i+1, m): - s_fft = sigfft.iloc[i+1, list(half_of_the_columns)] - s_conj = np.conj(sigfft.iloc[u+1, list(half_of_the_columns)]) + s_fft = sigfft[i+1, half_of_the_columns] + s_conj = np.conj(sigfft[u+1, half_of_the_columns]) s_exp = np.exp(1j * 2 * np.pi * half_of_the_columns * (position[i]-position[u]) * teta / total_columns) s_last = 2 * np.pi * half_of_the_columns * (position[i]-position[u]) / total_columns - s_fft = s_fft.reset_index(drop=True) - s_conj = s_conj.reset_index(drop=True) - s_exp = s_exp.reset_index(drop=True) - s_last = s_last.reset_index(drop=True) - term_de12 = term_de12 - np.real(s_fft * s_conj * s_exp * (s_last**2)) term_de12 = (term_de12 * 2) / (m ** 2) @@ -558,18 +542,13 @@ def derivatives_beamforming(sig, row, teta): # Calculate the second term of the second derivative for i in range(m): - s_fft = sigfft.iloc[i+1, list(half_of_the_columns)] + s_fft = sigfft[i+1, half_of_the_columns] s_exp = np.exp(1j * 2 * np.pi * half_of_the_columns * position[i] * teta / total_columns) s_last = 2 * np.pi * half_of_the_columns * position[i] / total_columns - s_fft = s_fft.reset_index(drop=True) - s_exp = s_exp.reset_index(drop=True) - s_last = s_last.reset_index(drop=True) - term_de22 = term_de22 + (s_fft * s_exp * (s_last**2)) - s_conj = np.conj(sigfft.iloc[0, list(half_of_the_columns)]) - s_conj = s_conj.reset_index(drop=True) + s_conj = np.conj(sigfft[0, half_of_the_columns]) term_de22 = 2 * np.real(s_conj * term_de22) / m @@ -585,7 +564,7 @@ def mle_cv_est(sig, initial_teta, ied, fsamp): Parameters ---------- - sig : pd.Dataframe + sig : np.ndarray The source signal to be used for the calculation. Different channels should be organised in different rows. initial_teta : int @@ -604,7 +583,8 @@ def mle_cv_est(sig, initial_teta, ied, fsamp): See also -------- - - find_teta : Find the starting value for teta. + - estimate_cv_via_mle : Estimate signal conduction velocity via maximum + likelihood estimation. - MUcv_gui : Graphical user interface for the estimation of MUs conduction velocity. @@ -613,9 +593,6 @@ def mle_cv_est(sig, initial_teta, ied, fsamp): Refer to the examples of find_teta to obtain sig and initial_teta. """ - # Set index to 0 - sig = sig.reset_index(drop=True) - # Calculate ied in meters ied = ied / 1000 @@ -627,14 +604,14 @@ def mle_cv_est(sig, initial_teta, ied, fsamp): eps = sys.float_info.epsilon while abs(teta - t) >= 5e-5 and trial < 30: - trial = trial+1 + trial = trial + 1 teta = t # Initialize the first and second derivatives de1 = 0 de2 = 0 # Calculate the first and second derivatives - for row in range(len(sig)): + for row in range(np.shape(sig)[0]): de1t, de2t = derivatives_beamforming(sig=sig, row=row, teta=teta) de1 = de1 + de1t + eps de2 = de2 + de2t + eps @@ -667,12 +644,12 @@ def find_teta(sig1, sig2, ied, fsamp): Parameters ---------- - sig1, sig2 : pd.Series + sig1, sig2 : np.ndarray The two signals based on which to calculate teta. - These must be pd.Series, i.e., 1-dimensional data structures. - ied : int + These must be 1-dimensional arrays where the data is contained in a row. + ied : int or float Interelectrode distance (mm). - fsamp : int + fsamp : int or float Sampling frequency (Hz). Returns @@ -682,23 +659,24 @@ def find_teta(sig1, sig2, ied, fsamp): See also -------- - - mle_cv_est : Estimate conduction velocity (CV) via maximum likelihood - estimation. + - estimate_cv_via_mle : Estimate signal conduction velocity via maximum + likelihood estimation. - MUcv_gui : Graphical user interface for the estimation of MUs conduction velocity. Examples -------- Calculate the starting point for the maximum likelihood estimation. - In this example we calculate teta for the first MU on the channels 5,6,7 - in the third column ("col3") of the double differential representation of - the MUAPs. + In this example, we calculate teta for the first MU (number 0) on the + channels 31, 32, 34, 34 that are contained in the second column ("col2") + of the double differential representation of the MUAPs. First, obtain the spike-triggered average of the double differential derivation. >>> import openhdemg.library as emg >>> emgfile = emg.askopenfile(filesource="OTB", otb_ext_factor=8) - ... sorted_rawemg = emg.sort_rawemg( + >>> emgfile = emg.filter_rawemg(emgfile) + >>> sorted_rawemg = emg.sort_rawemg( ... emgfile, ... code="GR08MM1305", ... orientation=180, @@ -717,17 +695,17 @@ def find_teta(sig1, sig2, ied, fsamp): each row and all the instants are contained in columns. For this reason, the original content of the spike-triggered average has to be transposed. After that, the 1D signals used to estimate teta are defined based on the - number of available channels. - - >>> sig = sta[1]["col3"].transpose() - >>> sig = sig.iloc[[5,6,7], :] - >>> sig = sig.reset_index(drop=True) - >>> if len(sig) > 3: - >>> sig1 = sig.iloc[1] - >>> sig2 = sig.iloc[2] + number of available channels. Please note that the original signal + contained in a pandas DataFrame has to be convertedn in a numpy array. + + >>> sig = sta[0]["col2"].loc[:, 31:34] + >>> sig = sig.to_numpy() + >>> if np.shape(sig)[0] > 3: + ... sig1 = sig[1, :] + ... sig2 = sig[2, :] >>> else: - >>> sig1 = sig.iloc[0] - >>> sig2 = sig.iloc[1] + ... sig1 = sig[0, :] + ... sig2 = sig[1, :] Third, estimate teta. @@ -747,10 +725,6 @@ def find_teta(sig1, sig2, ied, fsamp): teta_min = math.floor(ied / max_cv * fsamp) teta_max = math.ceil(ied / min_cv * fsamp) - # Work with numpy arrays for better performance - sig1 = sig1.to_numpy() - sig2 = sig2.to_numpy() - # Verify that the input is a 1D array. If not, it will affect the # calculation of corrpos. if sig1.ndim != 1: diff --git a/openhdemg/library/muap.py b/openhdemg/library/muap.py index 6b29502..631a152 100644 --- a/openhdemg/library/muap.py +++ b/openhdemg/library/muap.py @@ -1329,6 +1329,101 @@ def xcc_sta(sta): return xcc_sta +def estimate_cv_via_mle(emgfile, signal): + """ + Estimate signal conduction velocity via maximum likelihood estimation. + + This function can be used for the estimation of conduction velocity for 2 + or more signals. These can be either MUAPs or global EMG signals. + + Parameters + ---------- + emgfile : dict + The dictionary containing the emgfile from whic "signal" has been + extracted. This is used to know IED and FSAMP. + signal : pd.DataFrame + A dataframe containing the signals on which to estimate CV. The signals + should be organised in colums. + + Returns + ------- + cv : float + The conduction velocity value in M/s. + + See also + -------- + - MUcv_gui : Graphical user interface for the estimation of MUs conduction + velocity. + + Examples + -------- + Calculate the CV for the first MU (number 0) on the channels 31, 32, 34, + 34 that are contained in the second column ("col2") of the double + differential representation of the MUAPs. First, obtain the spike- + triggered average of the double differential derivation. + + >>> import openhdemg.library as emg + >>> emgfile = emg.askopenfile(filesource="OTB", otb_ext_factor=8) + >>> emgfile = emg.filter_rawemg(emgfile) + >>> sorted_rawemg = emg.sort_rawemg( + ... emgfile, + ... code="GR08MM1305", + ... orientation=180, + ... dividebycolumn=True + ... ) + >>> dd = emg.double_diff(sorted_rawemg=sorted_rawemg) + >>> sta = emg.sta( + ... emgfile=emgfile, + ... sorted_rawemg=sorted_rawemg, + ... firings=[0,50], + ... timewindow=50, + ... ) + + Second, extract the channels of interest and estimate CV. + + >>> signal = sta[0]["col2"].loc[:, 31:34] + >>> cv = estimate_cv_via_mle(emgfile=emgfile, signal=signal) + """ + + """ + sta_mu is a pandas dataframe containing the signals where to estimate CV + sta = emg.sta(emgfile, dd) + sta_mu = sta[0]["col2"].loc[:, 31:34] + """ + ied = emgfile["IED"] + fsamp = emgfile["FSAMP"] + + # Work with numpy vectorised operations for better performance + sig = signal.to_numpy() + sig = sig.T + + # Prepare the input 1D signals for find_teta + if np.shape(sig)[0] > 3: + sig1 = sig[1, :] + sig2 = sig[2, :] + else: + sig1 = sig[0, :] + sig2 = sig[1, :] + + teta = find_teta( + sig1=sig1, + sig2=sig2, + ied=ied, + fsamp=fsamp, + ) + + cv, teta = mle_cv_est( + sig=sig, + initial_teta=teta, + ied=ied, + fsamp=fsamp, + ) + + cv = abs(cv) + + return cv + + class MUcv_gui(): """ Graphical user interface for the estimation of MUs conduction velocity. @@ -1339,7 +1434,7 @@ class MUcv_gui(): Parameters ---------- emgfile : dict - The dictionary containing the first emgfile. + The dictionary containing the emgfile. sorted_rawemg : dict A dict containing the sorted electrodes. Every key of the dictionary represents a different column of the @@ -1354,11 +1449,18 @@ class MUcv_gui(): muaps_timewindow : int, default 50 Timewindow to compute ST MUAPs in milliseconds. + See also + -------- + - estimate_cv_via_mle : Estimate signal conduction velocity via maximum + likelihood estimation. + Examples -------- Call the GUI. + >>> import openhdemg.library as emg >>> emgfile = emg.askopenfile(filesource="OTB", otb_ext_factor=8) + >>> emgfile = emg.filter_rawemg(emgfile) >>> sorted_rawemg = emg.sort_rawemg( ... emgfile, ... code="GR08MM1305", @@ -1379,7 +1481,7 @@ def __init__( sorted_rawemg, n_firings=[0, 50], muaps_timewindow=50, - figsize=[20, 15], + figsize=[25, 20], ): # On start, compute the necessary information self.emgfile = emgfile @@ -1534,8 +1636,7 @@ def __init__( # Define functions necessary for the GUI # Use empty docstrings to hide the functions from the documentation. def gui_plot(self): - """ - """ # Plot the MUAPs used to estimate CV. + # Plot the MUAPs used to estimate CV. # Get MU number mu = int(self.selectmu_cb.get()) @@ -1555,58 +1656,32 @@ def gui_plot(self): plt.close() def copy_to_clipboard(self): - """ - """ # Copy the dataframe to clipboard in csv format. + # Copy the dataframe to clipboard in csv format. pyperclip.copy(self.res_df.to_csv(index=False, sep='\t')) # Define functions for cv estimation def compute_cv(self): - """ - """ # Compute conduction velocity. + # Compute conduction velocity. - # Get the muaps of the selected columns and represent them in - # different rows (as requested by the functions find_teta and - # mle_cv_est). - sig = self.st[int(self.selectmu_cb.get())][self.col_cb.get()].transpose() + # Get the muaps of the selected columns. + sig = self.st[int(self.selectmu_cb.get())][self.col_cb.get()] col_list = list(range(int(self.start_cb.get()), int(self.stop_cb.get())+1)) - sig = sig.iloc[col_list, :] - sig = sig.reset_index(drop=True) + sig = sig.iloc[:, col_list] # Verify that the signal is correcly oriented - if len(sig) > len(sig.columns): + if len(sig) < len(sig.columns): raise ValueError( "The number of signals exceeds the number of samples. Verify that each row represents a signal" ) - # Prepare the input 1D signals for find_teta - if len(sig) > 3: - sig1 = sig.iloc[1] - sig2 = sig.iloc[2] - else: - sig1 = sig.iloc[0] - sig2 = sig.iloc[1] - - initial_teta = find_teta( - sig1=sig1, - sig2=sig2, - ied=self.ied, - fsamp=self.fsamp, - ) - - # Calculate CV (and return only positive values) - cv, teta = mle_cv_est( - sig=sig, - initial_teta=initial_teta, - ied=self.ied, - fsamp=self.fsamp, - ) - cv = abs(cv) + # Estimate CV + cv = estimate_cv_via_mle(emgfile=self.emgfile, signal=sig) # Calculate RMS sig = sig.to_numpy() - rms = np.mean(np.sqrt((np.mean(sig**2, axis=1)))) + rms = np.mean(np.sqrt((np.mean(sig**2, axis=0)))) # Update the self.res_df and the self.textbox mu = int(self.selectmu_cb.get()) From a72d1639d27336b32535406c2fdc80d225e3783c Mon Sep 17 00:00:00 2001 From: Paul Ritsche Date: Thu, 22 Feb 2024 17:45:26 +0100 Subject: [PATCH 22/57] Fixed: Magic windows... --- openhdemg/gui/gui_files/Icon2.ico | Bin 0 -> 3126 bytes .../gui/gui_modules/advanced_analyses.py | 41 +++++++- openhdemg/gui/gui_modules/analyse_force.py | 22 ++++- openhdemg/gui/gui_modules/edit_mus.py | 20 +++- openhdemg/gui/gui_modules/edit_refsig.py | 23 ++++- openhdemg/gui/gui_modules/error_handler.py | 15 ++- openhdemg/gui/gui_modules/gui_plotting.py | 25 ++++- openhdemg/gui/gui_modules/mu_properties.py | 20 +++- openhdemg/gui/openhdemg_gui.py | 90 ++++++++++-------- 9 files changed, 200 insertions(+), 56 deletions(-) create mode 100644 openhdemg/gui/gui_files/Icon2.ico diff --git a/openhdemg/gui/gui_files/Icon2.ico b/openhdemg/gui/gui_files/Icon2.ico new file mode 100644 index 0000000000000000000000000000000000000000..500424d8fca13c462b064f8e436dcca4551adbc5 GIT binary patch literal 3126 zcmc&${cjUx9DiCnhi;kMa01hbgc(2$FoXeP8bny4K@(&A#Sr*O-Y3y{BFqsnPsALFX9)k7 zlfAbsH2Rr#``_vKPAugJ+$D(|Nm8QG266V|@4u>-{w6BbPjELj@r)}l`B5UZE-4Mn zn69URP{4mBaOGrwe|scVI>fp016e%V%8b@3BJ`jF#F1Qj<3hNTy|y!W6}FhL7CttH zIe}?=5#iZN1eHD;o7nG@TZFq^e${+ynN}_ZqM_RwEA+4u9PGoi*UfDd?ldMI9rVTx zf~UpDFsh0n5V49C^~^+tTfpv8vWGpfE=B_^Fps!-Y@EvCCtrWwDon1VHzYQ%h(#}- zB2qCWER!{Q#KU)nelv8glNS&LU^XOXPJjEw*6u;mx+9(8YXGU2=M9sMyN;ivFLF2c`iGAVGR=wU0x@^=eBh8zIC?(V zH{jmFr`!3|{gaAyFHRdp6_8UJ}DQg$?%v;f#y&a;jwM(?;Vj_ z#|M0RMGR9I-WDCNlQVTn=B+^n;#|r?2Rmk(z|E9=X!!>lD-cC|#MdQfb_IUv;QlCi zviNoxJ<@=^XW|PBCHxA^@<)}zx((9&Zsw=W{B0W+1d*RJ@umDS(dC%sdyO9Oil~Bp zesyE)jtxE-<5?45u2w1K$i`ly`~1mfVfuhiYKq^t$49d@R?YZ(m`iQ3@q^w(gE(Vv zvV|`#+7rVBjlLSVjO;q>5fBA?7Rx`F7ghUJg^M)0BY1TyceBS6MJn3h129^QFE?8w z{&x2I%b{O;+)<}uml7&krbR6`1>^!Z2% zodH!~Z*L-ZN*OWp2Y>8Cdlr>G7ac>#JLHMm5W_jaWwUH=I7Mu;RHZG#6k1%jSG2(w z^Cx1J6W=*BWy7UX6mQ>v=XIZ=H=l~^vL03X6f<7(O!*K6C zKF%>@*I4}gs=OGNa`wX?zS+%Os!juNq`K__&1&i;mri~6<*vY$Dg-DfBuQ<{IW(z< zdu36p6qDVW$fk=xCgt_dgY_=xgVqFgOOQt2{qbBopVsv^&c#I%D$bGh;uF#rOE(24 h2nHN_6lBRu&IG=4GXx`vd(;vfe?OjHKk9$U@egWB2fhFR literal 0 HcmV?d00001 diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index 49f5489..b42e87b 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -2,6 +2,8 @@ from tkinter import ttk, W, E, N, S, StringVar, BooleanVar import customtkinter as ctk +import os +from sys import platform from pandastable import Table import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog @@ -114,9 +116,25 @@ def __init__(self, parent): # Open window self.a_window = ctk.CTkToplevel(fg_color="LightBlue4") self.a_window.title("Advanced Tools Window") - self.a_window.wm_iconbitmap() + + # Set window icon + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) + self.a_window.grab_set() + # Set resizable window + # Configure columns with a loop + for col in range(3): + self.head.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(8): + self.head.rowconfigure(row, weight=1) + # Add Label ctk.CTkLabel( self.a_window, text="Select tool and matrix:", font=('Segoe UI',15, 'bold'), @@ -251,8 +269,25 @@ def advanced_analysis(self): self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title(head_title) - self.head.wm_iconbitmap() - self.head.grab_set() + + # Set window icon + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) + + self.a_window.grab_set() + + # Set resizable window + # Configure columns with a loop + for col in range(3): + self.head.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(17): + self.head.rowconfigure(row, weight=1) + # Specify Signal signal_value = ("OTB", "DEMUSE", "OPENHDEMG", "CUSTOMCSV") diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index d48f30c..8e2b478 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -1,6 +1,8 @@ """Module containing the force analysis GUI""" from tkinter import ttk, W, E, StringVar +from sys import platform +import os import customtkinter as ctk import pandas as pd import openhdemg.library as openhdemg @@ -76,9 +78,25 @@ def __init__(self, parent): # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Force Analysis Window") - self.head.wm_iconbitmap() + + # Set window icon + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) + self.head.grab_set() - + + # Set resizable window + # Configure columns with a loop + for col in range(3): + self.head.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(10): + self.head.rowconfigure(row, weight=1) + # Get MVC get_mvf = ctk.CTkButton(self.head, text="Get MVC", command=self.get_mvc, fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index 67b8e41..0c5ce46 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -1,6 +1,8 @@ """Module containing the MU Removal GUI class""" from tkinter import StringVar, W, E +import os +from sys import platform import customtkinter as ctk import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog @@ -84,9 +86,25 @@ def __init__(self, parent): self.head = ctk.CTkToplevel(fg_color="LightBlue4") # Set the background color of the top-level window self.head.title("Motor Unit Removal Window") - self.head.wm_iconbitmap() + + # Set the icon for the window + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) + self.head.grab_set() + # Set resizable window + # Configure columns with a loop + for col in range(3): + self.head.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(10): + self.head.rowconfigure(row, weight=1) + # Select Motor Unit ctk.CTkLabel(self.head, text="Select MU:", font=('Segoe UI',15, 'bold')).grid( column=1, row=0, padx=5, pady=5, sticky=W diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index 521c0f0..22cb520 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -2,7 +2,8 @@ from tkinter import ttk, W, E, StringVar, DoubleVar import customtkinter as ctk - +import os +from sys import platform import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog @@ -92,10 +93,24 @@ def __init__(self, parent): # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Reference Signal Editing Window") - self.head.wm_iconbitmap() + + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) + self.head.grab_set() - self.head.resizable(width=True, height=True) - + + # Set resizable window + # Configure columns with a loop + for col in range(3): + self.head.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(10): + self.head.rowconfigure(row, weight=1) + # Filter Refsig # Define Labels ctk.CTkLabel(self.head, text="Filter Order", font=('Segoe UI',15, 'bold')).grid(column=1, row=0, sticky=(W, E)) diff --git a/openhdemg/gui/gui_modules/error_handler.py b/openhdemg/gui/gui_modules/error_handler.py index 6e55054..986bf89 100644 --- a/openhdemg/gui/gui_modules/error_handler.py +++ b/openhdemg/gui/gui_modules/error_handler.py @@ -1,6 +1,7 @@ """Module containing the error message designs""" import os +from sys import platform import traceback import customtkinter as ctk from PIL import Image @@ -63,6 +64,14 @@ def __init__(self, parent, error, solution): self.head = ctk.CTkToplevel(fg_color="#FFBF00") self.head.title("Error Dialog") self.head.geometry("500x300") # Adjust the size as needed + + # Set window icon + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) + path = os.path.dirname(os.path.abspath(__file__)) # Create a frame for the content with blue background, placed in the middle @@ -121,5 +130,9 @@ def show_error_dialog(parent, error, solution): ... show_error_dialog(root, e, "Check the value and try again.") >>> root.mainloop() """ - error_message = "".join(traceback.format_exception(type(error), value=error, tb=error.__traceback__)) + if error is None: + error_message = "".join(traceback.format_exception(type(error), value=error, tb=None)) + else: + error_message = "".join(traceback.format_exception(type(error), value=error, tb=error.__traceback__)) + ErrorDialog(parent, error_message, solution) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index e88d6a1..671acb0 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -2,8 +2,9 @@ import os import webbrowser -from tkinter import ttk, W, E, StringVar, PhotoImage +from tkinter import ttk, W, E,N, S, StringVar, PhotoImage from PIL import Image +from sys import platform import customtkinter as ctk import openhdemg.library as openhdemg @@ -120,8 +121,22 @@ def __init__(self, parent): self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Plot Window") - self.head.wm_iconbitmap() - self.head.grab_set() + + # Set window icon + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) + + # Set resizable window + # Configure columns with a loop + for col in range(7): + self.head.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(21): + self.head.rowconfigure(row, weight=1) # define tk variables for later use self.matrix_rc = StringVar() # Matrix rows columns @@ -238,7 +253,7 @@ def __init__(self, parent): ctk.CTkLabel(self.head, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid(row=0, column=3, sticky=(W)) self.mat_code = StringVar() - matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings["delsys_sensor_label"], "None") + matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings.delsys_sensor_label, "None") matrix_code = ctk.CTkComboBox(self.head, width=100, variable=self.mat_code, values=matrix_code_values, state="readonly") matrix_code.grid(row=0, column=4, sticky=(W, E)) @@ -361,7 +376,7 @@ def __init__(self, parent): info_button.grid(row=0, column=6, sticky=E) for child in self.head.winfo_children(): - child.grid_configure(padx=5, pady=5) + child.grid_configure(padx=5, pady=5, ) except AttributeError as e: show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index ba8d48a..28158e4 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -1,6 +1,8 @@ """Module containing MU propterty analysis""" from tkinter import ttk, W, E, StringVar +from sys import platform +import os import customtkinter as ctk import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog @@ -93,9 +95,25 @@ def __init__(self, parent): # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Motor Unit Properties Window") - self.head.wm_iconbitmap() + + # Set window icon + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + iconpath = head_path + "/gui_files/Icon2.ico" + self.head.iconbitmap(default=iconpath) + if platform.startswith("win"): + self.head.after(200, lambda: self.head.iconbitmap(iconpath)) self.head.grab_set() + # Set resizable window + # Configure columns with a loop + for col in range(3): + self.head.columnconfigure(col, weight=1) + + # Configure rows with a loop + for row in range(21): + self.head.rowconfigure(row, weight=1) + + # MVC Entry ctk.CTkLabel(self.head, text="Enter MVC[n]:", font=('Segoe UI',15, 'bold')).grid(column=0, row=0, sticky=(W)) self.mvc_value = StringVar() diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index fd45636..9ec80ee 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -256,7 +256,7 @@ def __init__(self, master): advanced.grid(row=20, column=0, columnspan=2, sticky=(N, S, E, W)) # Create right side framing for functionalities - self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0) + self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0, bg_color="LightBlue4") self.right.grid(column=1, row=0, sticky=(N, S, E, W)) # Configure columns, plot is weighted more icons are not configured @@ -267,7 +267,7 @@ def __init__(self, master): self.right.rowconfigure(row, weight=1) # Create empty figure - self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=False) + self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=True) self.canvas = FigureCanvasTkAgg(self.first_fig, master=self.right) self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) @@ -292,7 +292,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4") - settings_b.grid(column=1, row=0, sticky=E, pady=(0, 20)) + settings_b.grid(column=1, row=0, sticky=W, pady=(0, 20)) # Information Button info_path = master_path + "/gui_files/Info.png" # Get infor button path @@ -309,7 +309,7 @@ def __init__(self, master): command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/"))), ) - info_button.grid(row=1, column=1, sticky=E, pady=(0, 20)) + info_button.grid(row=1, column=1, sticky=W, pady=(0, 20)) # Button for online tutorials online_path = master_path + "/gui_files/Online.png" @@ -326,7 +326,7 @@ def __init__(self, master): command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/"))), ) - online_button.grid(row=2, column=1, sticky=E, pady=(0, 20)) + online_button.grid(row=2, column=1, sticky=W, pady=(0, 20)) # Button for dev information redirect_path = master_path + "/gui_files/Redirect.png" @@ -342,7 +342,7 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers"))), ) - redirect_button.grid(row=3, column=1, sticky=E, pady=(0, 20)) + redirect_button.grid(row=3, column=1, sticky=W, pady=(0, 20)) # Button for contact information contact_path = master_path + "/gui_files/Contact.png" @@ -358,7 +358,7 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/"))), ) - contact_button.grid(row=4, column=1, sticky=E, pady=(0, 20)) + contact_button.grid(row=4, column=1, sticky=W, pady=(0, 20)) # Button for citatoin information cite_path = master_path + "/gui_files/Cite.png" @@ -374,7 +374,7 @@ def __init__(self, master): fg_color="LightBlue4", command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/"))), ) - cite_button.grid(row=5, column=1, sticky=E, pady=(0, 20)) + cite_button.grid(row=5, column=1, sticky=W, pady=(0, 20)) for child in self.left.winfo_children(): child.grid_configure(padx=5, pady=5) @@ -402,7 +402,7 @@ def open_settings(self): that users should be able to customize. """ # Determine relative filepath - file_path = "openhdemg/gui/settings.py" + file_path = os.path.dirname(os.path.abspath(__file__)) + "/settings.py" # Check for operating system and open in default editor if sys.platform.startswith('darwin'): # macOS @@ -449,12 +449,12 @@ def load_file(): # Add filespecs ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E)) + ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) + column=2, row=3, sticky=(W, E), padx=5, pady=5 ) ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E) + column=2, row=4, sticky=(W, E), padx=5, pady=5 ) elif self.filetype.get() == "DEMUSE": @@ -468,12 +468,12 @@ def load_file(): # Add filespecs ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E)) + ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) + column=2, row=3, sticky=(W, E), padx=5, pady=5 ) ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E) + column=2, row=4, sticky=(W, E), padx=5, pady=5 ) elif self.filetype.get() == "DELSYS": # Ask user to select the file @@ -491,12 +491,12 @@ def load_file(): self.resdict = openhdemg.emg_from_delsys(rawemg_filepath=self.file_path, mus_directory=self.mus_path) # Add filespecs - ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E)) + ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) + column=2, row=3, sticky=(W, E), padx=5, pady=5 ) ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E) + column=2, row=4, sticky=(W, E), padx=5, pady=5 ) elif self.filetype.get() == "OPENHDEMG": @@ -509,17 +509,17 @@ def load_file(): self.resdict = openhdemg.emg_from_json(filepath=self.file_path) # Add filespecs if self.resdict["SOURCE"] in ["DEMUSE", "OTB", "CUSTOMCSV", "DELSYS"]: - ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid(column=2, row=3, sticky=(W, E)) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid(column=2, row=4, sticky=(W, E)) + ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) else: # Reconfigure labels for refsig ctk.CTkLabel( self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) + ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) ctk.CTkLabel(self.left, text=" ").grid( - column=2, row=4, sticky=(W, E) + column=2, row=4, sticky=(W, E), padx=5, pady=5 ) else: # Ask user to select the file @@ -534,9 +534,9 @@ def load_file(): fsamp=float(self.fsamp.get()), ) # Add filespecs - ctk.CTkLabel(self.left, text="Custom CSV").grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel(self.left, text="").grid(column=2, row=3, sticky=(W, E)) - ctk.CTkLabel(self.left, text="").grid(column=2, row=4, sticky=(W, E)) + ctk.CTkLabel(self.left, text="Custom CSV").grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text="").grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text="").grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) # Get filename filename = os.path.splitext(os.path.basename(file_path))[0] @@ -545,6 +545,11 @@ def load_file(): # Add filename to label self.master.title(self.filename) + # End progress + progress.grid_remove() + progress.stop() + + # This sections is used for refsig loading as they required not the # the filespecs to be loaded. else: @@ -578,7 +583,7 @@ def load_file(): self.resdict = openhdemg.refsig_from_customcsv( filepath=self.file_path, fsamp=float(self.fsamp.get()), - ) # NOTE please check that I used correctly self.fsamp.get() here and above. + ) # Get filename filename = os.path.splitext(os.path.basename(file_path))[0] @@ -590,18 +595,20 @@ def load_file(): # Reconfigure labels for refsig ctk.CTkLabel( self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) + ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) ctk.CTkLabel(self.left, text=" ").grid( - column=2, row=4, sticky=(W, E) + column=2, row=4, sticky=(W, E), padx=5, pady=5 ) - for child in self.left.winfo_children(): - child.grid_configure(padx=5, pady=5) + # for child in self.left.winfo_children(): + # child.grid_configure(padx=5, pady=5) # End progress - progress.stop() progress.grid_remove() + progress.stop() + + return except ValueError as e: show_error_dialog(parent=self, error=e, solution=str("When an OTB file is loaded, make sure to " @@ -635,9 +642,10 @@ def load_file(): except: # End progress - progress.stop() progress.grid_remove() - + progress.stop() + + # Indicate Progress progress = ctk.CTkProgressBar(self.left, mode="indeterminate", fg_color="#585858", width=100, progress_color="#FFBF00") @@ -737,10 +745,14 @@ def save_file(): # End progress progress.stop() progress.grid_remove() - + + return + except AttributeError as e: show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) - + + + # Indicate Progress progress = ctk.CTkProgressBar(self.left, mode="indeterminate") progress.grid(row=4, column=0) @@ -882,7 +894,7 @@ def in_gui_plotting(self, resdict, plot="idr"): ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N,S,W,E)) + self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N,S,E,W), padx=5) toolbar = NavigationToolbar2Tk(self.canvas, self.right, pack_toolbar=False) toolbar.grid(row=5, column=0, sticky=S) plt.close() From fe618e4c3911c4aa0c88eb72c37b7d9aaaa963db Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 26 Feb 2024 09:12:46 +0100 Subject: [PATCH 23/57] Update basic structure of the test_modules --- .gitignore | 2 + openhdemg/tests/{ => fixtures}/__init__.py | 0 openhdemg/tests/integration/__init__.py | 0 openhdemg/tests/test_openfiles.py | 2 - openhdemg/tests/test_suite.py | 39 ------ openhdemg/tests/unit/__init__.py | 0 .../tests/unit/functions_for_unit_test.py | 125 ++++++++++++++++++ openhdemg/tests/unit/test_openfiles.py | 112 ++++++++++++++++ 8 files changed, 239 insertions(+), 41 deletions(-) rename openhdemg/tests/{ => fixtures}/__init__.py (100%) create mode 100644 openhdemg/tests/integration/__init__.py delete mode 100644 openhdemg/tests/test_openfiles.py delete mode 100644 openhdemg/tests/test_suite.py create mode 100644 openhdemg/tests/unit/__init__.py create mode 100644 openhdemg/tests/unit/functions_for_unit_test.py create mode 100644 openhdemg/tests/unit/test_openfiles.py diff --git a/.gitignore b/.gitignore index eeb908e..0ba1c27 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ openhdemg.egg-info/ openhdemg/.DS_Store prove.py prove_storage.py +openhdemg/tests/fixtures/* +!openhdemg/tests/fixtures/__init__.py diff --git a/openhdemg/tests/__init__.py b/openhdemg/tests/fixtures/__init__.py similarity index 100% rename from openhdemg/tests/__init__.py rename to openhdemg/tests/fixtures/__init__.py diff --git a/openhdemg/tests/integration/__init__.py b/openhdemg/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openhdemg/tests/test_openfiles.py b/openhdemg/tests/test_openfiles.py deleted file mode 100644 index 0909d10..0000000 --- a/openhdemg/tests/test_openfiles.py +++ /dev/null @@ -1,2 +0,0 @@ -import unittest -from openhdemg.library.openfiles import \ No newline at end of file diff --git a/openhdemg/tests/test_suite.py b/openhdemg/tests/test_suite.py deleted file mode 100644 index cd8ed82..0000000 --- a/openhdemg/tests/test_suite.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Use this module to run all the tests. This will take a while. - -First, you should dowload all the files necessary for the testing. These might -occupy gigabytes and the dowload might be slow. The files are available at: -https://drive.google.com/drive/folders/1suCZSils8rSCs2E3_K25vRCbN3AFDI7F?usp=sharing - -IMPORTANT: Do not alter the content of the dowloaded folder! - -Since the library's functions perform complex tasks and return complex data -structures, these tests can verify that no critical errors occur, but the -accuracy of each function must be assessed independently upon creation, or at -each revision of the code. -""" - - -def test_library_modules(): - pass - - -def test_compatibility_modules(): - pass - - -def test_gui_modules(): - pass - - -if __name__ == "__main__": - # Change the testfiles_dir as needed. - testfiles_dir = "C:/Users/Giacomo/Desktop/PhD Padova/Papers Unipd/Open_HD-EMG Docs and Extras/openhdemg_testfiles" - - print("\nTest Started, wait for its completition.\n") - - test_library_modules() - test_compatibility_modules() - test_gui_modules() - - print("\nTest Finished!\n") diff --git a/openhdemg/tests/unit/__init__.py b/openhdemg/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openhdemg/tests/unit/functions_for_unit_test.py b/openhdemg/tests/unit/functions_for_unit_test.py new file mode 100644 index 0000000..67251cf --- /dev/null +++ b/openhdemg/tests/unit/functions_for_unit_test.py @@ -0,0 +1,125 @@ +import os +import pandas as pd +import numpy as np + + +def get_directories(folder, subfolder, filename): + """ + Get the directory of the specified file. + + Parameters: + ----------- + folder : str + The name of the folder containing the folder with the specified file + (e.g., library). + The folder should be inside "tests/fixtures/..." + subfolder : str + The name of the folder containing the specified file (e.g., demuse). + The folder should be inside "tests/fixtures/...folder..." + filename : str + The name of the file to open including the file extension. The file to + open should be inside "tests/fixtures/...folder.../...subfolder". + + Returns + ------- + filepath : str + The full path to the file. + """ + + # Get the absolute path of the current Python file + current_file_path = os.path.abspath(__file__) + + # Get the directory containing the current Python file + current_directory = os.path.dirname(os.path.dirname(current_file_path)) + + filepath = os.path.join( + current_directory, "fixtures", folder, subfolder, filename, + ) + + return filepath + + +def validate_emgfile_content(tc, emgfile): + """ + Verify the emgfile content (instances and shapes). + + Parameters: + ----------- + tc : class + The unittest.TestCase instance (this should be "self"). + emgfile : dict + The loaded emgfile (with decomposition outcome). + """ + + # Verify instances + tc.assertIsInstance(emgfile, dict) + tc.assertIsInstance(emgfile["SOURCE"], str) + tc.assertIsInstance(emgfile["FILENAME"], str) + tc.assertIsInstance(emgfile["RAW_SIGNAL"], pd.DataFrame) + tc.assertIsInstance(emgfile["REF_SIGNAL"], pd.DataFrame) + tc.assertIsInstance(emgfile["ACCURACY"], pd.DataFrame) + tc.assertIsInstance(emgfile["IPTS"], pd.DataFrame) + tc.assertIsInstance(emgfile["MUPULSES"], list) + tc.assertIsInstance(emgfile["MUPULSES"][0], np.ndarray) + tc.assertIsInstance(emgfile["FSAMP"], float) + tc.assertIsInstance(emgfile["IED"], float) + tc.assertIsInstance(emgfile["EMG_LENGTH"], int) + tc.assertIsInstance(emgfile["NUMBER_OF_MUS"], int) + tc.assertIsInstance(emgfile["BINARY_MUS_FIRING"], pd.DataFrame) + tc.assertIsInstance(emgfile["EXTRAS"], pd.DataFrame) + + # Verify shapes + tc.assertEqual(len(emgfile.keys()), 13) + tc.assertTrue( + emgfile["RAW_SIGNAL"].shape[0] > emgfile["RAW_SIGNAL"].shape[1] + ) + tc.assertTrue( + emgfile["REF_SIGNAL"].shape[0] > emgfile["REF_SIGNAL"].shape[1] + ) + tc.assertTrue( + emgfile["ACCURACY"].shape[0] >= emgfile["ACCURACY"].shape[1] + ) # >= to manage the exception of a single MU + tc.assertTrue( + emgfile["IPTS"].shape[0] > emgfile["IPTS"].shape[1] + ) + tc.assertTrue( + emgfile["BINARY_MUS_FIRING"].shape[0] > emgfile["BINARY_MUS_FIRING"].shape[1] + ) + + # Verify congruent sizes + tc.assertTrue(emgfile["RAW_SIGNAL"].shape[0] == emgfile["IPTS"].shape[0]) + tc.assertTrue( + emgfile["IPTS"].shape[0] == emgfile["BINARY_MUS_FIRING"].shape[0] + ) + tc.assertTrue(emgfile["IPTS"].shape[1] == emgfile["NUMBER_OF_MUS"]) + tc.assertTrue( + emgfile["BINARY_MUS_FIRING"].shape[1] == emgfile["NUMBER_OF_MUS"] + ) + tc.assertTrue(len(emgfile["MUPULSES"]) == emgfile["NUMBER_OF_MUS"]) + + +def validate_emg_refsig_content(tc, emg_refsig): + """ + Verify the emg_refsig file content (instances and shapes). + + Parameters: + ----------- + tc : class + The unittest.TestCase instance (this should be "self"). + emg_refsig : dict + The loaded emg_refsig. + """ + + # Verify instances + tc.assertIsInstance(emg_refsig, dict) + tc.assertIsInstance(emg_refsig["SOURCE"], str) + tc.assertIsInstance(emg_refsig["FILENAME"], str) + tc.assertIsInstance(emg_refsig["REF_SIGNAL"], pd.DataFrame) + tc.assertIsInstance(emg_refsig["FSAMP"], float) + tc.assertIsInstance(emg_refsig["EXTRAS"], pd.DataFrame) + + # Verify shapes + tc.assertEqual(len(emg_refsig.keys()), 5) + tc.assertTrue( + emg_refsig["REF_SIGNAL"].shape[0] > emg_refsig["REF_SIGNAL"].shape[1] + ) diff --git a/openhdemg/tests/unit/test_openfiles.py b/openhdemg/tests/unit/test_openfiles.py new file mode 100644 index 0000000..dc3ad76 --- /dev/null +++ b/openhdemg/tests/unit/test_openfiles.py @@ -0,0 +1,112 @@ +""" +To run the tests using unittest, execute from the openhdemg/tests directory: + python -m unittest discover + +First, you should dowload all the files necessary for the testing and store them inside +openhdemg/tests/fixtures. The files are available at: +https://drive.google.com/drive/folders/1suCZSils8rSCs2E3_K25vRCbN3AFDI7F?usp=sharing + +IMPORTANT: Do not alter the content of the dowloaded folder! + +WARNING!!! Since the library's functions perform complex tasks and return +complex data structures, these tests can verify that no critical errors occur, +but the accuracy of each function must be assessed independently upon creation, +or at each revision of the code. +""" + + +import pandas as pd +import numpy as np +import unittest +from openhdemg.tests.unit.functions_for_unit_test import get_directories as getd +from openhdemg.tests.unit.functions_for_unit_test import ( + validate_emgfile_content, validate_emg_refsig_content, +) +from openhdemg.library.openfiles import ( + emg_from_demuse, emg_from_otb, refsig_from_otb, emg_from_delsys, + refsig_from_delsys, emg_from_customcsv, refsig_from_customcsv, + save_json_emgfile, emg_from_json, askopenfile, asksavefile, + emg_from_samplefile, +) + + +class TestOpenfiles(unittest.TestCase): + """ + Test the functions/classes in the openfiles module. + """ + + def test_from_demuse(self): + """ + Test loading various decomposed files saved from the DEMUSE software. + """ + + # Load decomposed file with multiple MUs and reference signal + demuse_D_R_mMU = emg_from_demuse( + filepath=getd("library", "demuse", "DEMUSE_D_R_mMU.mat"), + ) + validate_emgfile_content(self, demuse_D_R_mMU) + + # Load decomposed file with only 1 MU and reference signal + demuse_D_R_1MU = emg_from_demuse( + filepath=getd("library", "demuse", "DEMUSE_D_R_1MU.mat"), + ) + validate_emgfile_content(self, demuse_D_R_1MU) + + # Load decomposed file with multiple MUs (some empty) and reference + # signal + demuse_D_R_E_mMU = emg_from_demuse( + filepath=getd("library", "demuse", "demuse_D_R_E_mMU.mat"), + ) + validate_emgfile_content(self, demuse_D_R_E_mMU) + + def test_from_otb(self): + """ + Test loading various files saved from the OTBiolab+ software. + """ + + # Load decomposed file with multiple MUs and reference signal + otb_D_R_mMU = emg_from_otb( + filepath=getd("library", "otb", "OTB_D_R_mMU.mat"), + ) + validate_emgfile_content(self, otb_D_R_mMU) + + # Load decomposed file with multiple MUs, reference signal and EXTRAS + otb_D_R_EX_mMU = emg_from_otb( + filepath=getd("library", "otb", "OTB_D_R_EX_mMU.mat"), + extras="requested|AUX Force" + ) + validate_emgfile_content(self, otb_D_R_EX_mMU) + + # Load decomposed file with only 1 MU and reference signal + otb_D_R_1_MU = emg_from_otb( + filepath=getd("library", "otb", "OTB_D_R_1MU.mat"), + ) + validate_emgfile_content(self, otb_D_R_1_MU) + + # Load file with only the reference signal + otb_R = refsig_from_otb( + filepath=getd("library", "otb", "OTB_R.mat"), + ) + validate_emg_refsig_content(self, otb_R) + + # Load file with only the reference signal and EXTRAS + otb_R_EX = refsig_from_otb( + filepath=getd("library", "otb", "OTB_R_EX.mat"), + extras="requested path", + ) + validate_emg_refsig_content(self, otb_R_EX) + + def test_from_delsys(self): + """ + Test loading various files saved from the Delsys software. + """ + + # Load decomposed file with multiple MUs and reference signal + otb_D_R_mMU = emg_from_otb( + filepath=getd("library", "otb", "OTB_D_R_mMU.mat"), + ) + validate_emgfile_content(self, otb_D_R_mMU) + + +if __name__ == '__main__': + unittest.main() From 5e19799ecdc4dd370a140199ec8475aae5afe2eb Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 4 Mar 2024 08:02:40 +0100 Subject: [PATCH 24/57] Update tests --- .../tests/unit/functions_for_unit_test.py | 64 ++++++--- openhdemg/tests/unit/test_openfiles.py | 122 ++++++++++++++++-- 2 files changed, 159 insertions(+), 27 deletions(-) diff --git a/openhdemg/tests/unit/functions_for_unit_test.py b/openhdemg/tests/unit/functions_for_unit_test.py index 67251cf..97e9dc2 100644 --- a/openhdemg/tests/unit/functions_for_unit_test.py +++ b/openhdemg/tests/unit/functions_for_unit_test.py @@ -13,9 +13,10 @@ def get_directories(folder, subfolder, filename): The name of the folder containing the folder with the specified file (e.g., library). The folder should be inside "tests/fixtures/..." - subfolder : str - The name of the folder containing the specified file (e.g., demuse). - The folder should be inside "tests/fixtures/...folder..." + subfolder : str or list + A sting or list of the subfolder/s containing the specified file + (e.g., "demuse" or ["delsys", "4pin"]). + The folder/s should be inside "tests/fixtures/...folder..." filename : str The name of the file to open including the file extension. The file to open should be inside "tests/fixtures/...folder.../...subfolder". @@ -32,8 +33,11 @@ def get_directories(folder, subfolder, filename): # Get the directory containing the current Python file current_directory = os.path.dirname(os.path.dirname(current_file_path)) + if isinstance(subfolder, str): + subfolder = [subfolder] + filepath = os.path.join( - current_directory, "fixtures", folder, subfolder, filename, + current_directory, "fixtures", folder, *subfolder, filename, ) return filepath @@ -73,29 +77,51 @@ def validate_emgfile_content(tc, emgfile): tc.assertTrue( emgfile["RAW_SIGNAL"].shape[0] > emgfile["RAW_SIGNAL"].shape[1] ) - tc.assertTrue( - emgfile["REF_SIGNAL"].shape[0] > emgfile["REF_SIGNAL"].shape[1] - ) + try: # Manage excpetion of no reference signal + tc.assertTrue( + emgfile["REF_SIGNAL"].shape[0] > emgfile["REF_SIGNAL"].shape[1] + ) + except AssertionError: + tc.assertTrue(emgfile["REF_SIGNAL"].shape[0] == 0) tc.assertTrue( emgfile["ACCURACY"].shape[0] >= emgfile["ACCURACY"].shape[1] ) # >= to manage the exception of a single MU - tc.assertTrue( - emgfile["IPTS"].shape[0] > emgfile["IPTS"].shape[1] - ) + if emgfile["SOURCE"] != "DELSYS": + tc.assertTrue( + emgfile["IPTS"].shape[0] > emgfile["IPTS"].shape[1] + ) + else: + tc.assertTrue( + emgfile["IPTS"].shape[0] == 0 + ) tc.assertTrue( emgfile["BINARY_MUS_FIRING"].shape[0] > emgfile["BINARY_MUS_FIRING"].shape[1] ) # Verify congruent sizes - tc.assertTrue(emgfile["RAW_SIGNAL"].shape[0] == emgfile["IPTS"].shape[0]) - tc.assertTrue( - emgfile["IPTS"].shape[0] == emgfile["BINARY_MUS_FIRING"].shape[0] - ) - tc.assertTrue(emgfile["IPTS"].shape[1] == emgfile["NUMBER_OF_MUS"]) - tc.assertTrue( - emgfile["BINARY_MUS_FIRING"].shape[1] == emgfile["NUMBER_OF_MUS"] - ) - tc.assertTrue(len(emgfile["MUPULSES"]) == emgfile["NUMBER_OF_MUS"]) + if emgfile["SOURCE"] != "DELSYS": + tc.assertTrue( + emgfile["RAW_SIGNAL"].shape[0] == emgfile["IPTS"].shape[0] + ) + tc.assertTrue( + emgfile["IPTS"].shape[0] == emgfile["BINARY_MUS_FIRING"].shape[0] + ) + tc.assertTrue(emgfile["IPTS"].shape[1] == emgfile["NUMBER_OF_MUS"]) + tc.assertTrue( + emgfile["BINARY_MUS_FIRING"].shape[1] == emgfile["NUMBER_OF_MUS"] + ) + tc.assertTrue(len(emgfile["MUPULSES"]) == emgfile["NUMBER_OF_MUS"]) + else: + tc.assertTrue( + emgfile["RAW_SIGNAL"].shape[0] == emgfile["BINARY_MUS_FIRING"].shape[0] + ) + tc.assertTrue( + emgfile["BINARY_MUS_FIRING"].shape[1] == emgfile["NUMBER_OF_MUS"] + ) + tc.assertTrue(len(emgfile["MUPULSES"]) == emgfile["NUMBER_OF_MUS"]) + tc.assertTrue( + emgfile["EXTRAS"].shape[1] == emgfile["NUMBER_OF_MUS"] * 4 + ) def validate_emg_refsig_content(tc, emg_refsig): diff --git a/openhdemg/tests/unit/test_openfiles.py b/openhdemg/tests/unit/test_openfiles.py index dc3ad76..f008a4a 100644 --- a/openhdemg/tests/unit/test_openfiles.py +++ b/openhdemg/tests/unit/test_openfiles.py @@ -2,8 +2,8 @@ To run the tests using unittest, execute from the openhdemg/tests directory: python -m unittest discover -First, you should dowload all the files necessary for the testing and store them inside -openhdemg/tests/fixtures. The files are available at: +First, you should dowload all the files necessary for the testing and store +them inside openhdemg/tests/fixtures. The files are available at: https://drive.google.com/drive/folders/1suCZSils8rSCs2E3_K25vRCbN3AFDI7F?usp=sharing IMPORTANT: Do not alter the content of the dowloaded folder! @@ -15,8 +15,6 @@ """ -import pandas as pd -import numpy as np import unittest from openhdemg.tests.unit.functions_for_unit_test import get_directories as getd from openhdemg.tests.unit.functions_for_unit_test import ( @@ -30,7 +28,7 @@ ) -class TestOpenfiles(unittest.TestCase): +class TestOpenFiles(unittest.TestCase): """ Test the functions/classes in the openfiles module. """ @@ -101,11 +99,119 @@ def test_from_delsys(self): Test loading various files saved from the Delsys software. """ + # Load decomposed file with multiple MUs, reference signal and MUAPs + delsys_D_R_MUAPs_mMU = emg_from_delsys( + rawemg_filepath=getd( + "library", + ["delsys", "4pin", "DELSYS_D_R_MUAPs_mMU"], + "Raw_EMG_signal_withFakeRef.mat" + ), + mus_directory=getd( + "library", + ["delsys", "4pin", "DELSYS_D_R_MUAPs_mMU"], + "Bicep_Brachii_Motor_Units (Sensor 1)" + ), + ) + validate_emgfile_content(self, delsys_D_R_MUAPs_mMU) + + # Load decomposed file with multiple MUs and MUAPs + delsys_D_MUAPs_mMU = emg_from_delsys( + rawemg_filepath=getd( + "library", + ["delsys", "4pin", "DELSYS_D_MUAPs_mMU"], + "Raw_EMG_signal.mat" + ), + mus_directory=getd( + "library", + ["delsys", "4pin", "DELSYS_D_MUAPs_mMU"], + "Bicep_Brachii_Motor_Units (Sensor 1)" + ), + ) + validate_emgfile_content(self, delsys_D_MUAPs_mMU) + + # Load file with only the reference signal + delsys_R = refsig_from_delsys( + filepath=getd( + "library", + ["delsys", "4pin", "DELSYS_D_R_MUAPs_mMU"], + "Raw_EMG_signal_withFakeRef.mat", + ) + ) + validate_emg_refsig_content(self, delsys_R) + + def test_from_customcsv(self): + """ + Test loading various files saved in a custom .csv file. + """ + + # Load decomposed file with multiple MUs, reference signal and EXTRAS + custom_csv_D_R_EX_mMU = emg_from_customcsv( + filepath=getd("library", "custom_csv", "C_CSV_D_R_EX_mMU.csv"), + ) + validate_emgfile_content(self, custom_csv_D_R_EX_mMU) + + # Load decomposed file with multiple MUs, reference signal and EXTRAS + custom_csv_D_R_EX_1MU = emg_from_customcsv( + filepath=getd("library", "custom_csv", "C_CSV_D_R_EX_1MU.csv"), + ) + validate_emgfile_content(self, custom_csv_D_R_EX_1MU) + + # Load file with only the reference signal EXTRAS + custom_csv_R_EX = refsig_from_customcsv( + filepath=getd("library", "custom_csv", "C_CSV_R_EX.csv"), + ) + validate_emg_refsig_content(self, custom_csv_R_EX) + + def test_from_samplefile(self): + """ + Test loading the decomposed sample file. + """ + # Load decomposed file with multiple MUs and reference signal - otb_D_R_mMU = emg_from_otb( - filepath=getd("library", "otb", "OTB_D_R_mMU.mat"), + emgfile = emg_from_samplefile() + validate_emgfile_content(self, emgfile) + + def test_from_json(self): + """ + Test loading various files saved in .json. + """ + + # Load decomposed file with multiple MUs and reference signal + openhdemg_D_R_mMU = emg_from_json( + filepath=getd("library", "openhdemg", "OPENHD_D_R_mMU.json"), ) - validate_emgfile_content(self, otb_D_R_mMU) + validate_emgfile_content(self, openhdemg_D_R_mMU) + + # Load decomposed file with multiple MUs, reference signal and EXTRAS + openhdemg_D_R_EX_mMU = emg_from_json( + filepath=getd("library", "openhdemg", "OPENHD_D_R_EX_mMU.json"), + ) + validate_emgfile_content(self, openhdemg_D_R_EX_mMU) + + # Load file with only reference signal + openhdemg_R = emg_from_json( + filepath=getd("library", "openhdemg", "OPENHD_R.json"), + ) + validate_emg_refsig_content(self, openhdemg_R) + + # Load file with only reference signal and EXTRAS + openhdemg_R_EX = emg_from_json( + filepath=getd("library", "openhdemg", "OPENHD_R_EX.json"), + ) + validate_emg_refsig_content(self, openhdemg_R_EX) + + def test_saving_and_reloading(self): + """ + Test saving (various) and reloading (.json) files. + """ + + # Test with sample file + emgfile = emg_from_samplefile() + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + + if __name__ == '__main__': From 98b1b30d9bf5cac3161d3c3e298f8161a8a978de Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:43:48 +0100 Subject: [PATCH 25/57] Update test_openfiles.py --- openhdemg/tests/unit/test_openfiles.py | 207 +++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/openhdemg/tests/unit/test_openfiles.py b/openhdemg/tests/unit/test_openfiles.py index f008a4a..8151afb 100644 --- a/openhdemg/tests/unit/test_openfiles.py +++ b/openhdemg/tests/unit/test_openfiles.py @@ -12,6 +12,8 @@ complex data structures, these tests can verify that no critical errors occur, but the accuracy of each function must be assessed independently upon creation, or at each revision of the code. + +WARNING!!! - UNTESTED FUNCTIONS: askopenfile, asksavefile """ @@ -210,8 +212,213 @@ def test_saving_and_reloading(self): save_json_emgfile( emgfile, filepath=getd("library", "saved", "temp.json") ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Test with DEMUSE files + # Load decomposed file with multiple MUs and reference signal + emgfile = emg_from_demuse( + filepath=getd("library", "demuse", "DEMUSE_D_R_mMU.mat"), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Load decomposed file with only 1 MU and reference signal + emgfile = emg_from_demuse( + filepath=getd("library", "demuse", "DEMUSE_D_R_1MU.mat"), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Load decomposed file with multiple MUs (some empty) and reference + # signal + emgfile = emg_from_demuse( + filepath=getd("library", "demuse", "demuse_D_R_E_mMU.mat"), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Test with OTB files + # Load decomposed file with multiple MUs and reference signal + emgfile = emg_from_otb( + filepath=getd("library", "otb", "OTB_D_R_mMU.mat"), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Load decomposed file with multiple MUs, reference signal and EXTRAS + emgfile = emg_from_otb( + filepath=getd("library", "otb", "OTB_D_R_EX_mMU.mat"), + extras="requested|AUX Force" + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + # Load decomposed file with only 1 MU and reference signal + emgfile = emg_from_otb( + filepath=getd("library", "otb", "OTB_D_R_1MU.mat"), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + # Load file with only the reference signal + emg_refsig = refsig_from_otb( + filepath=getd("library", "otb", "OTB_R.mat"), + ) + save_json_emgfile( + emg_refsig, filepath=getd("library", "saved", "temp.json") + ) + emg_refsig = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emg_refsig_content(self, emg_refsig) + + # Load file with only the reference signal and EXTRAS + emg_refsig = refsig_from_otb( + filepath=getd("library", "otb", "OTB_R_EX.mat"), + extras="requested path", + ) + save_json_emgfile( + emg_refsig, filepath=getd("library", "saved", "temp.json") + ) + emg_refsig = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emg_refsig_content(self, emg_refsig) + + # Test with Delsys files + # Load decomposed file with multiple MUs, reference signal and MUAPs + emgfile = emg_from_delsys( + rawemg_filepath=getd( + "library", + ["delsys", "4pin", "DELSYS_D_R_MUAPs_mMU"], + "Raw_EMG_signal_withFakeRef.mat" + ), + mus_directory=getd( + "library", + ["delsys", "4pin", "DELSYS_D_R_MUAPs_mMU"], + "Bicep_Brachii_Motor_Units (Sensor 1)" + ), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile=emgfile) + + # Load decomposed file with multiple MUs and MUAPs + emgfile = emg_from_delsys( + rawemg_filepath=getd( + "library", + ["delsys", "4pin", "DELSYS_D_MUAPs_mMU"], + "Raw_EMG_signal.mat" + ), + mus_directory=getd( + "library", + ["delsys", "4pin", "DELSYS_D_MUAPs_mMU"], + "Bicep_Brachii_Motor_Units (Sensor 1)" + ), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile=emgfile) + + # Load file with only the reference signal + emg_refsig = refsig_from_delsys( + filepath=getd( + "library", + ["delsys", "4pin", "DELSYS_D_R_MUAPs_mMU"], + "Raw_EMG_signal_withFakeRef.mat", + ) + ) + save_json_emgfile( + emg_refsig, filepath=getd("library", "saved", "temp.json") + ) + emg_refsig = emg_from_json( + filepath=getd("library", "saved", "temp.json"), + ) + validate_emg_refsig_content(self, emg_refsig) + + # Test with CUSTOMCSV files + # Load decomposed file with multiple MUs, reference signal and EXTRAS + emgfile = emg_from_customcsv( + filepath=getd("library", "custom_csv", "C_CSV_D_R_EX_mMU.csv"), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Load decomposed file with multiple MUs, reference signal and EXTRAS + emgfile = emg_from_customcsv( + filepath=getd("library", "custom_csv", "C_CSV_D_R_EX_1MU.csv"), + ) + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json") + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Load file with only the reference signal, with EXTRAS + emg_refsig = refsig_from_customcsv( + filepath=getd("library", "custom_csv", "C_CSV_R_EX.csv"), + ) + save_json_emgfile( + emg_refsig, filepath=getd("library", "saved", "temp.json") + ) + emg_refsig = emg_from_json( + filepath=getd("library", "saved", "temp.json"), + ) + validate_emg_refsig_content(self, emg_refsig) + + def test_compression_level(self): + """ + Test saving (various) and reloading (.json) files. + """ + + # Test with sample file, no compression + emgfile = emg_from_samplefile() + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json"), + compresslevel=0, + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Normal compression + emgfile = emg_from_samplefile() + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json"), + compresslevel=4, + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) + + # Maximum compression + emgfile = emg_from_samplefile() + save_json_emgfile( + emgfile, filepath=getd("library", "saved", "temp.json"), + compresslevel=9, + ) + emgfile = emg_from_json(filepath=getd("library", "saved", "temp.json")) + validate_emgfile_content(self, emgfile) if __name__ == '__main__': From c0a51a60b9e13d3980a8f8799dba01731e7981c0 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:46:55 +0100 Subject: [PATCH 26/57] Update accuracy estimation Update compute_sil() and compute_pnr() --- openhdemg/library/mathtools.py | 241 ++++++++++++++++++++------------- 1 file changed, 148 insertions(+), 93 deletions(-) diff --git a/openhdemg/library/mathtools.py b/openhdemg/library/mathtools.py index a40849c..63d356a 100644 --- a/openhdemg/library/mathtools.py +++ b/openhdemg/library/mathtools.py @@ -235,7 +235,7 @@ def norm_twod_xcorr(df1, df2, mode="full"): return normxcorr_df, normxcorr_max -def compute_sil(ipts, mupulses): +def compute_sil(ipts, mupulses, ignore_negative_ipts=True): """ Calculate the Silhouette score for a single MU. @@ -250,6 +250,9 @@ def compute_sil(ipts, mupulses): interest. mupulses : ndarray The time of firing (MUPULSES[mu]) of the MU of interest. + ignore_negative_ipts : bool, default True + Only use positive ipts during peak and noise clustering. This is + particularly important for sources with large negative components. Returns ------- @@ -261,12 +264,18 @@ def compute_sil(ipts, mupulses): - compute_pnr : to calculate the Pulse to Noise ratio of a single MU. """ - # Manage exception of no firings (e.g., as can happen in files from DEMUSE) + # Manage exception of no firings if len(mupulses) == 0: return np.nan # Extract source and peaks and align source and peaks based on IPTS source = ipts.to_numpy() + + if ignore_negative_ipts: + # Ignore negative values, this is particularly needed for negative + # unbalanced sources. + source = source * np.abs(source) + peaks_idxs = mupulses - ipts.index[0] # Create clusters @@ -300,7 +309,13 @@ def compute_sil(ipts, mupulses): return sil -def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=True): +def compute_pnr( + ipts, + mupulses, + fsamp, + constrain_pulses=[True, 3], + separate_paired_firings=True, +): """ Calculate the pulse to noise ratio for a single MU. @@ -311,12 +326,19 @@ def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=True): interest. mupulses : ndarray The time of firing (MUPULSES[mu]) of the MU of interest. + constrain_pulses : list, default [True, 3] + If constrain_pulses[0] == True, the times of firing are considered + those in mupulses +- the number of samples specified in + constrain_pulses[1]. + If constrain_pulses[0] == False, the times of firing are estimated via + a heuristic penality funtion (see Notes). + constrain_pulses[1] must be an integer (see Notes for instructions on + how to set the appropriate value). separate_paired_firings : bool, default False Whether to treat differently paired and non-paired firings during - the estimation of the signal/noise threshold. According to Holobar - 2012, this is common in pathological tremor. The user is encouraged to - use the default value (True) to increase the robustness of the - estimation. + the estimation of the signal/noise threshold (heuristic penality + funtion, see Notes). This is relevant only if + constrain_pulses[0] == False. Otherwise, this argument is ignored. Returns ------- @@ -326,105 +348,138 @@ def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=True): See also -------- - compute_sil : to calculate the Silhouette score for a single MU. + + Notes + ----- + The behaviour of the compute_pnr() function is determined by the argument + constrain_pulses. + + If constrain_pulses[0] == True, the times of firing are considered those + in mupulses +- a number of samples specified in constrain_pulses[1]. + The inclusion of the samples around the mupulses values allows to capture + the full ipts corresponding to the time of firing (e.g., including also + the raising and falling wedges). The appropriate value of + constrain_pulses[1] must be determined by the user and depends on the + sampling frequency. It is suggested to use 3 when the sampling frequency is + 2000 or 2048 Hz and increase it if the sampling frequency is higher (e.g. + use 6 at 4000 or 4096 Hz). With this approach, the PNR estimation is not + related to the variability of the firings. + + If constrain_pulses[0] == False, the ipts values are classified as firings + or noise based on a threshold value (named "Pi" or "r") estimated from the + euristic penality funtion described in Holobar 2012, as proposed in + Holobar 2014. If the variability of the firings is relevant, this + apoproach should be preferred. Specifically: + Pi = D · χ[3,50](D) + CoVIDI + CoVpIDI + Where: + D is the median of the low-pass filtered instantaneous motor unit discharge + rate (first-order Butterworth filter, cut-off frequency 3 Hz). + χ[3,50](D) stands for an indicator function that penalizes motor units + with filtered discharge rate D below 3 pulses per second (pps) or above + 50 pps: + χ[3,50](D) = 0 if D is between 3 and 50 or D if D is not between 3 and 50. + Two separate coefficients of variation for inter-discharge interval (IDI) + calculated as standard deviation (SD) of IDI divided by the mean IDI, + are used. CoVIDI is the coefficient of variation for IDI of non-paired + MUs discharges only, whereas CoVpIDI is the coefficient of variation for + IDI of paired MUs discharges. + Holobar 2012 considered MUs discharges paired whenever the second + discharge was within 50 ms of the first. + Paired discharges are typical in pathological tremor and the use of both + CoVIDI and CoVpIDI accounts for this condition. + However, this heuristic penality function penalizes MUs firing during + specific types of contractions like explosive contractions + (MUs discharge up to 200 pps). + Therefore, in this implementation of the PNR, we did not include the + penality based on MUs discharge. + Additionally, the user can decide whether to adopt the two coefficients + of variations to estimate Pi or not. + If both are used, Pi would be calculated as: + Pi = CoVIDI + CoVpIDI + Otherwise, Pi would be calculated as: + Pi = CoV_all_IDI """ - # According to Holobar 2014, the PNR is calculated as: - # 10 * log10((mean of firings) / (mean of noise)) - # Where instants in the decomposed source are classified as firings or - # noise based on a threshold value named "Pi" or "r". - # - # Pi is calculated via a heuristic penalty funtion described in Holobar - # 2012 as: - # Pi = D · χ[3,50](D) + CoVIDI + CoVpIDI - # Where: - # D is the median of the low-pass filtered instantaneous motor unit - # discharge rate (first-order Butterworth filter, cut-off frequency 3 Hz) - # χ[3,50](D) stands for an indicator function that penalizes motor units - # with filtered discharge rate D below 3 pulses per second (pps) or above - # 50 pps: - # χ[3,50](D) = 0 if D is between 3 and 50 or D if D is not between 3 and 50. - # Two separate coefficients of variation for inter-discharge interval (IDI) - # calculated as standard deviation (SD) of IDI divided by the mean IDI, - # are used. CoVIDI is the coefficient of variation for IDI of non-paired - # MUs discharges only, whereas CoVpIDI is the coefficient of variation for - # IDI of paired MUs discharges. - # Holobar 2012 considered MUs discharges paired whenever the second - # discharge was within 50 ms of the first. - # Paired discharges are typical in pathological tremor and the use of both - # CoVIDI and CoVpIDI accounts for this condition. - # - # However, this heuristic penalty function penalizes MUs firing during - # specific types of contractions like explosive contractions - # (MUs discharge up to 200 pps). - # Therefore, in this implementation of the PNR estimation we did not use a - # penality based on MUs discharge. - # Additionally, the user can decide whether to adopt the two coefficients - # of variations to estimate Pi or not. - # If both are used, Pi would be calculated as: - # Pi = CoVIDI + CoVpIDI - # Otherwise, Pi would be calculated as: - # Pi = CoV_all_IDI - - # Calculate IDI - idi = np.diff(mupulses) - - # In order to increase robustness to outlier values, remove values outside - # mean +- 3 * STD in the idi array and the firings happening with more - # than 500ms of difference between each others. - idi = idi[idi <= (fsamp * 0.5)] - - mean, std = np.mean(idi), np.std(idi) - upper_bound = mean + 3*std - lower_bound = mean - 3*std - - idi = idi[idi <= upper_bound] - idi = idi[idi >= lower_bound] - - # Calculate Pi - if separate_paired_firings is False: - # Calculate Pi on all IDI - CoV_all_IDI = np.std(idi) / np.mean(idi) - - if math.isnan(CoV_all_IDI): - CoV_all_IDI = 0 - - Pi = CoV_all_IDI + # Manage exception of no firings + if len(mupulses) == 0: + return np.nan - else: - # Divide paired and non-paired IDIs before calculating specific CoV. - # De Luca 1985 considered dublets < 10 ms, Holobar < 50 ms - idinonp = idi[idi >= (fsamp * 0.05)] - idip = idi[idi < (fsamp * 0.05)] + # Extract the source + source = ipts.to_numpy() - if len(idinonp) > 1: - CoVIDI = np.std(idinonp) / np.mean(idinonp) - else: - CoVIDI = 0 + # Normalise source + source = source / np.mean(source[mupulses]) - if len(idip) > 1: - CoVpIDI = np.std(idip) / np.mean(idip) - else: - CoVpIDI = 0 + # Check how to estimate PNR + if constrain_pulses[0] is True: + # Estimate by mupulses + start, stop = -int(constrain_pulses[1]), int(constrain_pulses[1]) + extended_mupulses = np.concatenate( + [mupulses + t for t in range(start, stop+1)] + ) + + # Consider noise what outside the extenbded mupulses + noise_times = np.setdiff1d( + np.arange(mupulses[0], mupulses[-1]+1), extended_mupulses, + ) + + # Create clusters + peak_cluster = source[mupulses] + noise_cluster = source[noise_times] + noise_cluster = noise_cluster[~np.isnan(noise_cluster)] + noise_cluster = noise_cluster[noise_cluster >= 0] + + elif constrain_pulses[0] is False: + # Estimate by Pi + # Calculate IDI + idi = np.diff(mupulses) + + # In order to increase robustness to outlier values, remove values + # with more than 500ms of difference between each others. + idi = idi[idi <= (fsamp * 0.5)] # Calculate Pi - Pi = CoVIDI + CoVpIDI + if separate_paired_firings is False: + # Calculate Pi on all IDI + CoV_all_IDI = np.std(idi) / np.mean(idi) - # Extract the source - source = ipts.to_numpy() + if math.isnan(CoV_all_IDI): + CoV_all_IDI = 0 - # Use only absolute values from the source and normalise the source. - # This step is fundamental for the OTBiolab+ output. - source = np.abs(source) - source = (source - np.min(source)) / (np.max(source) - np.min(source)) + Pi = CoV_all_IDI - # Create clusters - peak_cluster = source[source >= Pi] - noise_cluster = source[source < Pi] + else: + # Divide paired and non-paired IDIs before calculating specific + # CoV. De Luca 1985 considered dublets < 10 ms, Holobar < 50 ms. + idinonp = idi[idi >= (fsamp * 0.05)] + idip = idi[idi < (fsamp * 0.05)] - peak_cluster = np.square(peak_cluster) - noise_cluster = np.square(noise_cluster) + if len(idinonp) > 1: + CoVIDI = np.std(idinonp) / np.mean(idinonp) + else: + CoVIDI = 0 + + if len(idip) > 1: + CoVpIDI = np.std(idip) / np.mean(idip) + else: + CoVpIDI = 0 + + # Calculate Pi + Pi = CoVIDI + CoVpIDI + + # Create clusters + peak_cluster = source[source >= Pi] + noise_cluster = source[source < Pi] + + else: + raise ValueError( + "constrain_pulses[0] can only be True or False. " + + f"{constrain_pulses[0]} was passed instead." + ) # Calculate PNR + peak_cluster = np.square(peak_cluster) + noise_cluster = np.square(noise_cluster) pnr = 10 * np.log10(np.mean(peak_cluster) / np.mean(noise_cluster)) return pnr From 9c1e0485a45d80d5329eef3cead9e721e5bd928e Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:33:09 +0100 Subject: [PATCH 27/57] Update accuracy estimation --- openhdemg/library/mathtools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openhdemg/library/mathtools.py b/openhdemg/library/mathtools.py index 63d356a..ae3d08e 100644 --- a/openhdemg/library/mathtools.py +++ b/openhdemg/library/mathtools.py @@ -331,12 +331,12 @@ def compute_pnr( those in mupulses +- the number of samples specified in constrain_pulses[1]. If constrain_pulses[0] == False, the times of firing are estimated via - a heuristic penality funtion (see Notes). + a heuristic penalty funtion (see Notes). constrain_pulses[1] must be an integer (see Notes for instructions on how to set the appropriate value). separate_paired_firings : bool, default False Whether to treat differently paired and non-paired firings during - the estimation of the signal/noise threshold (heuristic penality + the estimation of the signal/noise threshold (heuristic penalty funtion, see Notes). This is relevant only if constrain_pulses[0] == False. Otherwise, this argument is ignored. @@ -367,7 +367,7 @@ def compute_pnr( If constrain_pulses[0] == False, the ipts values are classified as firings or noise based on a threshold value (named "Pi" or "r") estimated from the - euristic penality funtion described in Holobar 2012, as proposed in + euristic penalty funtion described in Holobar 2012, as proposed in Holobar 2014. If the variability of the firings is relevant, this apoproach should be preferred. Specifically: Pi = D · χ[3,50](D) + CoVIDI + CoVpIDI @@ -387,11 +387,11 @@ def compute_pnr( discharge was within 50 ms of the first. Paired discharges are typical in pathological tremor and the use of both CoVIDI and CoVpIDI accounts for this condition. - However, this heuristic penality function penalizes MUs firing during + However, this heuristic penalty function penalizes MUs firing during specific types of contractions like explosive contractions (MUs discharge up to 200 pps). Therefore, in this implementation of the PNR, we did not include the - penality based on MUs discharge. + penalty based on MUs discharge. Additionally, the user can decide whether to adopt the two coefficients of variations to estimate Pi or not. If both are used, Pi would be calculated as: From 16c7a69e621cd9a61dd4f3951e8bce742b377499 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:04:20 +0100 Subject: [PATCH 28/57] Update askopenfile() --- openhdemg/library/openfiles.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openhdemg/library/openfiles.py b/openhdemg/library/openfiles.py index 36089b3..58c8cb8 100644 --- a/openhdemg/library/openfiles.py +++ b/openhdemg/library/openfiles.py @@ -1959,6 +1959,11 @@ def askopenfile(initialdir="/", filesource="OPENHDEMG", **kwargs): If your specific version is not available in the tested versions, trying with the closer one usually works, but please double check the results. Ignore if loading other files. + otb_extras : None or str, default None + Extras is used to store additional custom values. These information + will be stored in a pd.DataFrame with columns named as in the .mat + file. If not None, pass a regex pattern unequivocally identifying the + variable in the .mat file to load as extras. delsys_emg_sensor_name : str, default "Galileo sensor" The name of the EMG sensor used to collect the data. We currently support only the "Galileo sensor". @@ -2160,7 +2165,8 @@ def askopenfile(initialdir="/", filesource="OPENHDEMG", **kwargs): filepath=file_toOpen, ext_factor=kwargs.get("otb_ext_factor", 8), refsig=kwargs.get("otb_refsig_type", [True, "fullsampled"]), - version=kwargs.get("otb_version", "1.5.9.3") + version=kwargs.get("otb_version", "1.5.9.3"), + extras=kwargs.get("otb_extras", None), ) elif filesource == "OTB_REFSIG": ref = kwargs.get("otb_refsig_type", [True, "fullsampled"]) @@ -2168,6 +2174,7 @@ def askopenfile(initialdir="/", filesource="OPENHDEMG", **kwargs): filepath=file_toOpen, refsig=ref[1], version=kwargs.get("otb_version", "1.5.9.3"), + extras=kwargs.get("otb_extras", None), ) elif filesource == "DELSYS": emgfile = emg_from_delsys( From 88812e24f672374686973fcf2391ccd9bb7ac255 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:06:04 +0100 Subject: [PATCH 29/57] Update electrodes.py Updated sort_rawemg() and possibility to pass custom channels sorting orders --- openhdemg/library/electrodes.py | 176 +++++++++++++++++++++----------- 1 file changed, 116 insertions(+), 60 deletions(-) diff --git a/openhdemg/library/electrodes.py b/openhdemg/library/electrodes.py index 899b8c6..8e36dbc 100644 --- a/openhdemg/library/electrodes.py +++ b/openhdemg/library/electrodes.py @@ -6,6 +6,7 @@ import numpy as np import copy +import itertools OTBelectrodes_tuple = ( "GR04MM1305", @@ -143,6 +144,7 @@ def sort_rawemg( dividebycolumn=True, n_rows=None, n_cols=None, + custom_sorting_order=None, ): """ Sort RAW_SIGNAL based on matrix type and orientation. @@ -150,10 +152,11 @@ def sort_rawemg( To date, built-in sorting functions have been implemented for the matrices: Code (Orientation) - GR08MM1305 (0, 180), - GR04MM1305 (0, 180), - GR10MM0808 (0, 180), - Trigno Galileo Sensor (na). + GR08MM1305 (0, 180) + GR04MM1305 (0, 180) + GR10MM0808 (0, 180) + Trigno Galileo Sensor (na) + Custom order (any) Parameters ---------- @@ -166,29 +169,43 @@ def sort_rawemg( ``GR04MM1305`` ``GR10MM0808`` ``Trigno Galileo Sensor`` + ``Custom order`` + ``None`` If "None", the electodes are not sorted but n_rows and n_cols must be specified when dividebycolumn == True. + If "Custom order", the electrodes are sorted based on + custom_sorting_order. orientation : int {0, 180}, default 180 Orientation in degree of the matrix. E.g. 180 corresponds to the matrix connection toward the researcher or the ground (depending on the limb). Ignore if using the "Trigno Galileo Sensor". In this case, channels will be oriented as in the Delsys Neuromap Explorer software. + This Parameter is ignored if code=="Custom order" or code=="None". dividebycolumn = bool, default True Whether to return the sorted channels classified by matrix column. n_rows : None or int, default None The number of rows of the matrix. This parameter is used to divide the - channels based on the matrix shape. These are normally inferred by the - matrix code and must be specified only if code == None. + channels based on the matrix shape. These are inferred by the matrix + code and must be specified only if code==None. n_cols : None or int, default None The number of columns of the matrix. This parameter is used to divide - the channels based on the matrix shape. These are normally inferred by - the matrix code and must be specified only if code == None. + the channels based on the matrix shape. These are inferred by the + matrix code and must be specified only if code==None. + custom_sorting_order : None or list, default None + If code=="Custom order", custom_sorting_order will be used for + channels sorting. In this case, custom_sorting_order must be a list of + lists containing the order of the matrix channels. + Specifically, the number of columns are defined by + len(custom_sorting_order) while the number of rows by + len(custom_sorting_order[0]). np.nan can be used to specify empty + channels. Please refer to the Examples section for the structure of + the custom sorting order. Returns ------- sorted_rawemg : dict or pd.DataFrame - If dividebycolumn == True a dict containing the sorted electrodes is + If dividebycolumn == True, a dict containing the sorted electrodes is returned. Every key of the dictionary represents a different column of the matrix. Rows are stored in the dict as a pd.DataFrame. If dividebycolumn == False a pd.DataFrame containing the sorted @@ -201,7 +218,8 @@ def sort_rawemg( Examples -------- - Sort emgfile RAW_SIGNAL and divide it by columns. + Sort emgfile RAW_SIGNAL and divide it by columns with built-in sorting + orders. >>> import openhdemg.library as emg >>> emgfile = emg.askopenfile(filesource="OTB", otb_ext_factor=8) @@ -251,7 +269,9 @@ def sort_rawemg( Avoid RAW_SIGNAL sorting but divide it by columns. >>> emgfile = emg.askopenfile(filesource="CUSTOM") - >>> sorted_rawemg = emg.sort_rawemg(emgfile, code="None", n_cols=5, n_rows=13) + >>> sorted_rawemg = emg.sort_rawemg( + ... emgfile, code="None", n_cols=5, n_rows=13, + ... ) >>> sorted_rawemg["col0"] 0 1 2 ... 10 11 12 0 0.008138 0.001017 0.002035 ... 0.005595 0.008647 NaN @@ -265,6 +285,35 @@ def sort_rawemg( 62461 0.001526 0.009664 0.000000 ... 0.001526 0.025940 NaN 62462 0.033061 0.037130 0.027974 ... 0.022380 0.049845 NaN 62463 0.020854 0.028992 0.017802 ... 0.013733 0.037638 NaN + + Sort RAW_SIGNAL based on a custom order and divide it by columns. + The custom_sorting_order refers to a grid of 13 rows and 5 columns with the + empty channel in last position. + + >>> import openhdemg.library as emg + >>> emgfile = emg.askopenfile(filesource="OTB", otb_ext_factor=8) + >>> custom_sorting_order = [ + ... [63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51,], + ... [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,], + ... [37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25,], + ... [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,], + ... [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, np.nan,], + ... ] # 13 rows and 5 columns + >>> sorted_rawemg = emg.sort_rawemg( + ... emgfile=emgfile, + ... code="Custom order", + ... dividebycolumn=True, + ... custom_sorting_order=custom_sorting_order, + ... ) + >>> sorted_rawemg["col0"] + 0 1 2 ... 10 11 12 + 0 2.034505 -9.663899 -5.086263 ... -26.957193 -8.138021 -2.034505 + 1 1.017253 -8.646647 -3.560384 ... -26.957193 -8.138021 -9.663899 + 2 17.293295 6.612142 11.189778 ... -13.224284 9.663899 6.612142 + 3 21.362305 14.750163 22.888184 ... -9.155273 17.293295 12.715657 + ... ... ... ... ... ... ... ... + 63483 -15.258789 -20.345053 -8.646647 ... -20.853678 -15.767415 -10.681152 + 63484 -13.732910 -19.327799 -7.120768 ... -21.362305 -17.801920 -14.241536 """ valid_codes = [ @@ -273,6 +322,7 @@ def sort_rawemg( "GR10MM0808", "Trigno Galileo Sensor", "None", + "Custom order", ] if code not in valid_codes: return ValueError("Unsupported code in sort_rawemg()") @@ -280,8 +330,19 @@ def sort_rawemg( # Work on a copy of the RAW_SIGNAL rawemg = copy.deepcopy(emgfile["RAW_SIGNAL"]) - # Get sorting order by matrix - if code in ["GR08MM1305", "GR04MM1305"]: + # Get sorting order by matrix code + if code == "Custom order": + # Theck that custom_sorting_order has been specified + if not isinstance(custom_sorting_order, list): + raise ValueError( + "In sort_rawemg(), custom_sorting_order must be a list of " + + "lists when code=='Custom order'" + ) + + # Get custom sorting order + base0_sorting_order = custom_sorting_order + + elif code in ["GR08MM1305", "GR04MM1305"]: # Get sorting order by matrix orientation if orientation == 0: """ @@ -311,11 +372,11 @@ def sort_rawemg( 12 52 51 26 25 NaN """ base0_sorting_order = [ - 63,62,61,60,59,58,57,56,55,54,53,52, 51, - 38,39,40,41,42,43,44,45,46,47,48,49, 50, - 37,36,35,34,33,32,31,30,29,28,27,26, 25, - 12,13,14,15,16,17,18,19,20,21,22,23, 24, - 11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,np.nan + [63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51], + [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50], + [37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25], + [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, np.nan], ] elif orientation == 180: @@ -337,11 +398,11 @@ def sort_rawemg( 12 12 13 38 39 64 """ base0_sorting_order = [ - np.nan,0, 1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10,11, - 24 ,23,22,21,20,19,18,17,16,15,14,13,12, - 25 ,26,27,28,29,30,31,32,33,34,35,36,37, - 50 ,49,48,47,46,45,44,43,42,41,40,39,38, - 51 ,52,53,54,55,56,57,58,59,60,61,62,63 + [np.nan, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + [24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12], + [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37], + [50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38], + [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63], ] elif code == "GR10MM0808": @@ -359,14 +420,14 @@ def sort_rawemg( 7 64 56 48 40 32 24 16 8 """ base0_sorting_order = [ - 56, 57, 58, 59, 60, 61, 62, 63, - 48, 49, 50, 51, 52, 53, 54, 55, - 40, 41, 42, 43, 44, 45, 46, 47, - 33, 33, 34, 35, 36, 37, 38, 39, - 24, 25, 26, 27, 28, 29, 30, 31, - 16, 17, 18, 19, 20, 21, 22, 23, - 8, 9, 10, 11, 12, 13, 14, 15, - 0, 1, 2, 3, 4, 5, 6, 7, + [56, 57, 58, 59, 60, 61, 62, 63], + [48, 49, 50, 51, 52, 53, 54, 55], + [40, 41, 42, 43, 44, 45, 46, 47], + [33, 33, 34, 35, 36, 37, 38, 39], + [24, 25, 26, 27, 28, 29, 30, 31], + [16, 17, 18, 19, 20, 21, 22, 23], + [8, 9, 10, 11, 12, 13, 14, 15], + [0, 1, 2, 3, 4, 5, 6, 7], ] elif orientation == 180: @@ -383,14 +444,14 @@ def sort_rawemg( 7 1 9 17 25 33 41 49 57 """ base0_sorting_order = [ - 7, 6, 5, 4, 3, 2, 1, 0, - 15, 14, 13, 12, 11, 10, 9, 8, - 23, 22, 21, 20, 19, 18, 17, 16, - 31, 30, 29, 28, 27, 26, 25, 24, - 39, 38, 37, 36, 35, 34, 33, 32, - 47, 46, 45, 44, 43, 42, 41, 40, - 55, 54, 53, 52, 51, 50, 49, 48, - 63, 62, 61, 60, 59, 58, 57, 56, + [7, 6, 5, 4, 3, 2, 1, 0], + [15, 14, 13, 12, 11, 10, 9, 8], + [23, 22, 21, 20, 19, 18, 17, 16], + [31, 30, 29, 28, 27, 26, 25, 24], + [39, 38, 37, 36, 35, 34, 33, 32], + [47, 46, 45, 44, 43, 42, 41, 40], + [55, 54, 53, 52, 51, 50, 49, 48], + [63, 62, 61, 60, 59, 58, 57, 56], ] elif code == "Trigno Galileo Sensor": @@ -408,53 +469,48 @@ def sort_rawemg( 2 3 3 4 """ - base0_sorting_order = [0, 1, 2, 3] + base0_sorting_order = [[0, 1, 2, 3]] - else: + else: # elif code == "None": pass # Once the order to sort channels has been retrieved, # Sort the channels based on pre-specified order and reset columns - if code != "None": - sorted_rawemg = rawemg.reindex(columns=base0_sorting_order) + if code not in [None, "None"]: + flattened_base0_sorting_order = list( + itertools.chain(*base0_sorting_order), + ) + sorted_rawemg = rawemg.reindex(columns=flattened_base0_sorting_order) sorted_rawemg.columns = range(sorted_rawemg.columns.size) else: # Always allow a way to avoid electrodes sorting. # Return a copy of the RAW_SIGNAL - sorted_rawemg = copy.deepcopy(emgfile["RAW_SIGNAL"]) + sorted_rawemg = rawemg # Check if we need the sorted RAW_SIGNAL divided by column if dividebycolumn: - if code in ["GR08MM1305", "GR04MM1305"]: - if orientation in [0, 180]: - n_rows = 13 - n_cols = 5 - - elif code == "GR10MM0808": - if orientation in [0, 180]: - n_rows = 8 - n_cols = 8 - - elif code == "Trigno Galileo Sensor": - n_rows = 4 - n_cols = 1 + if code not in [None, "None"]: + n_cols = len(base0_sorting_order) + n_rows = len(base0_sorting_order[0]) else: # Check if n_rows and n_cols have been passed if not isinstance(n_rows, int): raise ValueError( - "In sort_rawemg(), n_rows and n_cols must be integers when code == 'None'" + "In sort_rawemg(), n_rows and n_cols must be integers " + + "when code == 'None'" ) if not isinstance(n_cols, int): raise ValueError( - "In sort_rawemg(), n_rows and n_cols must be integers when code == 'None'" + "In sort_rawemg(), n_rows and n_cols must be integers " + + "when code == 'None'" ) # Create the empty dict to fill with the sorted_rawemg divided by # columns. But first check for missing empty channel. if n_rows * n_cols != sorted_rawemg.shape[1]: raise ValueError( - "Number of specified rows and columns must match the number of channels." + "Number of rows * columns must match the number of channels." ) empty_dict = {f"col{n}": None for n in range(n_cols)} From 96842d6da74aca7fa5fb16e9fd446270d198c8d4 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:01:53 +0100 Subject: [PATCH 30/57] Update accuracy estimation The accuracy estimation is now customisable in various functions and, by default, it should provide the same results as in the previous release (for backward compatibility) --- openhdemg/library/analysis.py | 20 ++++++++++++++++++ openhdemg/library/mathtools.py | 8 ++++---- openhdemg/library/openfiles.py | 37 ++++++++++++++++++++++++++++++---- openhdemg/library/tools.py | 14 ++++++++++++- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/openhdemg/library/analysis.py b/openhdemg/library/analysis.py index dd6edd9..f7c1a9b 100644 --- a/openhdemg/library/analysis.py +++ b/openhdemg/library/analysis.py @@ -445,6 +445,8 @@ def basic_mus_properties( start_steady=-1, end_steady=-1, accuracy="default", + ignore_negative_ipts=False, + constrain_pulses=[True, 3], mvc=0, ): """ @@ -494,6 +496,20 @@ def basic_mus_properties( ``SIL_PNR`` Both the Silhouette score and the pulse to noise ratio are computed. + ignore_negative_ipts : bool, default False + This parameter determines the silhouette score estimation. If True, + only positive ipts values are used during peak and noise clustering. + This is particularly important for compensating sources with large + negative components. This parameter is considered only when + accuracy=="SIL" or accuracy=="SIL_PNR". + constrain_pulses : list, default [True, 3] + This parameter determines the PNR estimation. If + constrain_pulses[0]==True, the times of firing are considered those in + mupulses +- the number of samples specified in constrain_pulses[1]. + If constrain_pulses[0]==False, the times of firing are estimated via + a heuristic penalty funtion (see Notes in compute_pnr()). + constrain_pulses[1] must be an integer (see Notes in compute_pnr() for + instructions on how to set the appropriate value). mvc : float, default 0 The maximum voluntary contraction (MVC). It is suggest to report MVC in Newton (N). If 0 (default), the user will be asked to imput it @@ -604,6 +620,7 @@ def basic_mus_properties( sil = compute_sil( ipts=emgfile["IPTS"][mu], mupulses=emgfile["MUPULSES"][mu], + ignore_negative_ipts=ignore_negative_ipts, ) toappend.append({"SIL": sil}) toappend = pd.DataFrame(toappend) @@ -623,6 +640,7 @@ def basic_mus_properties( ipts=emgfile["IPTS"][mu], mupulses=emgfile["MUPULSES"][mu], fsamp=emgfile["FSAMP"], + constrain_pulses=constrain_pulses, ) toappend.append({"PNR": pnr}) toappend = pd.DataFrame(toappend) @@ -641,6 +659,7 @@ def basic_mus_properties( sil = compute_sil( ipts=emgfile["IPTS"][mu], mupulses=emgfile["MUPULSES"][mu], + ignore_negative_ipts=ignore_negative_ipts, ) toappend.append({"SIL": sil}) toappend = pd.DataFrame(toappend) @@ -659,6 +678,7 @@ def basic_mus_properties( ipts=emgfile["IPTS"][mu], mupulses=emgfile["MUPULSES"][mu], fsamp=emgfile["FSAMP"], + constrain_pulses=constrain_pulses, ) toappend.append({"PNR": pnr}) toappend = pd.DataFrame(toappend) diff --git a/openhdemg/library/mathtools.py b/openhdemg/library/mathtools.py index ae3d08e..39eee73 100644 --- a/openhdemg/library/mathtools.py +++ b/openhdemg/library/mathtools.py @@ -235,7 +235,7 @@ def norm_twod_xcorr(df1, df2, mode="full"): return normxcorr_df, normxcorr_max -def compute_sil(ipts, mupulses, ignore_negative_ipts=True): +def compute_sil(ipts, mupulses, ignore_negative_ipts=False): """ Calculate the Silhouette score for a single MU. @@ -250,9 +250,9 @@ def compute_sil(ipts, mupulses, ignore_negative_ipts=True): interest. mupulses : ndarray The time of firing (MUPULSES[mu]) of the MU of interest. - ignore_negative_ipts : bool, default True - Only use positive ipts during peak and noise clustering. This is - particularly important for sources with large negative components. + ignore_negative_ipts : bool, default False + If True, only use positive ipts during peak and noise clustering. This + is particularly important for sources with large negative components. Returns ------- diff --git a/openhdemg/library/openfiles.py b/openhdemg/library/openfiles.py index 58c8cb8..396cf9d 100644 --- a/openhdemg/library/openfiles.py +++ b/openhdemg/library/openfiles.py @@ -102,7 +102,7 @@ # --------------------------------------------------------------------- # # Function to open decomposed files coming from DEMUSE. -def emg_from_demuse(filepath): +def emg_from_demuse(filepath, ignore_negative_ipts=False): """ Import the .mat file decomposed in DEMUSE. @@ -112,6 +112,11 @@ def emg_from_demuse(filepath): The directory and the name of the file to load (including file extension .mat). This can be a simple string, the use of Path is not necessary. + ignore_negative_ipts : bool, default False + This parameter determines the silhouette score estimation. If True, + only positive ipts values are used during peak and noise clustering. + This is particularly important for compensating sources with large + negative components. Returns ------- @@ -261,7 +266,11 @@ def emg_from_demuse(filepath): if NUMBER_OF_MUS > 0: to_append = [] for mu in range(NUMBER_OF_MUS): - sil = compute_sil(ipts=IPTS[mu], mupulses=MUPULSES[mu]) + sil = compute_sil( + ipts=IPTS[mu], + mupulses=MUPULSES[mu], + ignore_negative_ipts=ignore_negative_ipts, + ) to_append.append(sil) ACCURACY = pd.DataFrame(to_append) @@ -538,6 +547,7 @@ def emg_from_otb( refsig=[True, "fullsampled"], version="1.5.9.3", extras=None, + ignore_negative_ipts=False, ): """ Import the .mat file exportable from OTBiolab+. @@ -575,6 +585,11 @@ def emg_from_otb( will be stored in a pd.DataFrame with columns named as in the .mat file. If not None, pass a regex pattern unequivocally identifying the variable in the .mat file to load as extras. + ignore_negative_ipts : bool, default False + This parameter determines the silhouette score estimation. If True, + only positive ipts values are used during peak and noise clustering. + This is particularly important for compensating sources with large + negative components. Returns ------- @@ -717,7 +732,11 @@ def emg_from_otb( if NUMBER_OF_MUS > 0: to_append = [] for mu in range(NUMBER_OF_MUS): - sil = compute_sil(ipts=IPTS[mu], mupulses=MUPULSES[mu]) + sil = compute_sil( + ipts=IPTS[mu], + mupulses=MUPULSES[mu], + ignore_negative_ipts=ignore_negative_ipts, + ) to_append.append(sil) ACCURACY = pd.DataFrame(to_append) @@ -1936,6 +1955,12 @@ def askopenfile(initialdir="/", filesource="OPENHDEMG", **kwargs): (.mat). ``CUSTOMCSV_REFSIG`` Custom file format (.csv) containing only the reference signal. + ignore_negative_ipts : bool, default False + This parameter determines the silhouette score estimation. If True, + only positive ipts values are used during peak and noise clustering. + This is particularly important for compensating sources with large + negative components. Currently, this parameter is used when loading + files decomposed in DEMUSE or OTB. otb_ext_factor : int, default 8 The extension factor used for the decomposition in the OTbiolab+ software. @@ -2159,7 +2184,10 @@ def askopenfile(initialdir="/", filesource="OPENHDEMG", **kwargs): # Open file depending on file origin if filesource == "DEMUSE": - emgfile = emg_from_demuse(filepath=file_toOpen) + emgfile = emg_from_demuse( + filepath=file_toOpen, + ignore_negative_ipts=kwargs.get("ignore_negative_ipts", False), + ) elif filesource == "OTB": emgfile = emg_from_otb( filepath=file_toOpen, @@ -2167,6 +2195,7 @@ def askopenfile(initialdir="/", filesource="OPENHDEMG", **kwargs): refsig=kwargs.get("otb_refsig_type", [True, "fullsampled"]), version=kwargs.get("otb_version", "1.5.9.3"), extras=kwargs.get("otb_extras", None), + ignore_negative_ipts=kwargs.get("ignore_negative_ipts", False), ) elif filesource == "OTB_REFSIG": ref = kwargs.get("otb_refsig_type", [True, "fullsampled"]) diff --git a/openhdemg/library/tools.py b/openhdemg/library/tools.py index 712b5af..b04ab9b 100644 --- a/openhdemg/library/tools.py +++ b/openhdemg/library/tools.py @@ -152,7 +152,12 @@ def mupulses_from_binary(binarymusfiring): return MUPULSES -def resize_emgfile(emgfile, area=None, accuracy="recalculate"): +def resize_emgfile( + emgfile, + area=None, + accuracy="recalculate", + ignore_negative_ipts=False, +): """ Resize all the emgfile. @@ -174,6 +179,12 @@ def resize_emgfile(emgfile, area=None, accuracy="recalculate"): ``maintain`` The original accuracy measure already contained in the emgfile is returned without any computation. + ignore_negative_ipts : bool, default False + This parameter determines the silhouette score estimation. If True, + only positive ipts values are used during peak and noise clustering. + This is particularly important for compensating sources with large + negative components. This parameter is considered only if + accuracy=="recalculate". Returns ------- @@ -263,6 +274,7 @@ def resize_emgfile(emgfile, area=None, accuracy="recalculate"): res = compute_sil( ipts=rs_emgfile["IPTS"][mu], mupulses=rs_emgfile["MUPULSES"][mu], + ignore_negative_ipts=ignore_negative_ipts, ) to_append.append(res) rs_emgfile["ACCURACY"] = pd.DataFrame(to_append) From f18442f8f872078cd4ddf69cf3866dc6ba98f317 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:54:11 +0100 Subject: [PATCH 31/57] Update resize_emgfile() and showselect() Possibility to resize files based on the reference signal or on the mean EMG signal --- openhdemg/library/tools.py | 89 +++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 11 deletions(-) diff --git a/openhdemg/library/tools.py b/openhdemg/library/tools.py index b04ab9b..99d9902 100644 --- a/openhdemg/library/tools.py +++ b/openhdemg/library/tools.py @@ -14,19 +14,28 @@ from openhdemg.library.mathtools import compute_sil -def showselect(emgfile, title="", titlesize=12, nclic=2): +def showselect(emgfile, how="ref_signal", title="", titlesize=12, nclic=2): """ - Select a part of the recording. + Visually select a part of the recording. - The area can be selected (based on the REF_SIGNAL) with any letter or - number in the keyboard, wrong points can be removed by pressing the - right mouse button. Once finished, press enter to continue. + The area can be selected based on the reference signal or based on the + mean EMG signal. The selection can be performed with any letter or number + in the keyboard, wrong points can be removed by pressing the right mouse + button. Once finished, press enter to continue. Parameters ---------- emgfile : dict - The dictionary containing the emgfile and in particular the REF_SIGNAL - (which is used for the selection). + The dictionary containing the emgfile. + how : str {"ref_signal", "mean_emg"}, default "ref_signal" + What to display in the figure used to visually select the area to + resize. + + ``ref_signal`` + Visualise the reference signal to select the area to resize. + + ``mean_emg`` + Visualise the mean EMG signal to select the area to resize. title : str The title of the plot. It is optional but strongly recommended. It should describe the task to do. @@ -48,23 +57,46 @@ def showselect(emgfile, title="", titlesize=12, nclic=2): Examples -------- - Load the EMG file and select the points. + Load the EMG file and select the points based on the reference signal. >>> import openhdemg.library as emg >>> emgfile = emg.askopenfile(filesource="OTB_REFSIG") >>> points = emg.showselect( ... emgfile, + ... how="ref_signal", ... title="Select 2 points", ... nclic=2, ... ) >>> points [16115, 40473] + + Load the EMG file and select the points based on the mean EMG signal. + + >>> import openhdemg.library as emg + >>> emgfile = emg.askopenfile(filesource="OPENHDEMG") + >>> points = emg.showselect( + ... emgfile, + ... how="mean_emg", + ... title="Select 2 points", + ... nclic=2, + ... ) + >>> points + [135, 26598] """ + + # Get the data to plot + if how == "ref_signal": + data_to_plot = emgfile["REF_SIGNAL"][0] + y_label = "Reference signal" + elif how == "mean_emg": + data_to_plot = emgfile["RAW_SIGNAL"].mean(axis=1) + y_label = "Mean EMG signal" + # Show the signal for the selection plt.figure(num="Fig_ginput") - plt.plot(emgfile["REF_SIGNAL"][0]) + plt.plot(data_to_plot) plt.xlabel("Samples") - plt.ylabel("MVC") + plt.ylabel(y_label) plt.title(title, fontweight="bold", fontsize=titlesize) ginput_res = plt.ginput(n=-1, timeout=0, mouse_add=False, show_clicks=True) @@ -155,11 +187,12 @@ def mupulses_from_binary(binarymusfiring): def resize_emgfile( emgfile, area=None, + how="ref_signal", accuracy="recalculate", ignore_negative_ipts=False, ): """ - Resize all the emgfile. + Resize all the components in the emgfile. This function can be useful to compute the various parameters only in the area of interest. @@ -172,10 +205,21 @@ def resize_emgfile( The resizing area. If already known, it can be passed in samples, as a list (e.g., [120,2560]). If None, the user can select the area of interest manually. + how : str {"ref_signal", "mean_emg"}, default "ref_signal" + If area==None, allow the user to visually select the area to resize + based on how. + + ``ref_signal`` + Visualise the reference signal to select the area to resize. + + ``mean_emg`` + Visualise the mean EMG signal to select the area to resize. accuracy : str {"recalculate", "maintain"}, default "recalculate" + ``recalculate`` The Silhouette score is computed in the new resized file. This can be done only if IPTS is present. + ``maintain`` The original accuracy measure already contained in the emgfile is returned without any computation. @@ -196,6 +240,28 @@ def resize_emgfile( Notes ----- Suggested names for the returned objects: rs_emgfile, start_, end_. + + Examples + -------- + Manually select the area to resize the emgfile based on mean EMG signal + and recalculate the silhouette score in the new portion of the signal. + + >>> emgfile = emg.askopenfile(filesource="DEMUSE", ignore_negative_ipts=True) + >>> rs_emgfile, start_, end_ = emg.resize_emgfile( + ... emgfile, + ... how="mean_emg", + ... accuracy="recalculate", + ... ) + + Automatically resize the emgfile in the pre-specified area. Do not + recalculate the silhouette score in the new portion of the signal. + + >>> emgfile = emg.askopenfile(filesource="CUSTOMCSV") + >>> rs_emgfile, start_, end_ = emg.resize_emgfile( + ... emgfile, + ... area=[120, 25680], + ... accuracy="maintain", + ... ) """ # Identify the area of interest @@ -207,6 +273,7 @@ def resize_emgfile( # Visualise and select the area to resize points = showselect( emgfile, + how=how, title="Select the start/end area to resize by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", titlesize=10, ) From 49bff0d90b050157673ae31e6d1e4667885f5c0d Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:58:20 +0100 Subject: [PATCH 32/57] First draft of GUI settings --- openhdemg/gui/gui_modules/gui_plotting.py | 3 +- openhdemg/gui/settings.py | 104 +++++++++++++++++++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 671acb0..7ec865e 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -253,7 +253,8 @@ def __init__(self, parent): ctk.CTkLabel(self.head, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid(row=0, column=3, sticky=(W)) self.mat_code = StringVar() - matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings.delsys_sensor_label, "None") + # NOTE the matrix codes can only be those accepted by sort_rawemg(). matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings.delsys_sensor_label, "None") + matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "Custom order", "None") matrix_code = ctk.CTkComboBox(self.head, width=100, variable=self.mat_code, values=matrix_code_values, state="readonly") matrix_code.grid(row=0, column=4, sticky=(W, E)) diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index 16aa06f..1b84c87 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -1,9 +1,107 @@ """ Module docstring explaining how to change the GUI settings. + +The settings can be related to both the GUI appearence and the analyses +functions. Parameters for the analyses functions are clustered based on the +openhdemg library's modules (here described as #----- MODULE NAME -----) and +can be better known from the API section of the openhdemg website. + +Each parameter for the analyses functions is named FunctionName__Parameter. +An extensive explanation of each "Parameter" can be found in the specific +API module and in the specific "FunctionName". + +A tutorial on how to use this settings file is available at: +# TODO add link to docs tutorial """ -# Graphic parameters changed after restart +# These graphic parameters are updated only after restarting the GUI +# +# --------------------------------- GUI LOOK ---------------------------------- background_color = "LightBlue4" +# Options are #TODO Paul please explain what colors can be used, I will add these in the tutorial +#TODO we also must rename this file in something more specific like gui_settings.py + + +# The following parameters are updated without restarting the GUI +# +# --------------------------------- openfiles --------------------------------- + +# in emg_from_demuse() +emg_from_demuse__ignore_negative_ipts = False + +# in emg_from_otb() +emg_from_otb__ext_factor = 8 +emg_from_otb__refsig = [True, "fullsampled"] +emg_from_otb__extras = None +emg_from_otb__ignore_negative_ipts = False + +# in refsig_from_otb() +refsig_from_otb__refsig = "fullsampled" +refsig_from_otb__extras = None + +# in emg_from_delsys() +emg_from_delsys__emg_sensor_name = "Galileo sensor" +emg_from_delsys__refsig_sensor_name = "Trigno Load Cell" +emg_from_delsys__filename_from = "mus_directory" + +# in refsig_from_delsys() +refsig_from_delsys__refsig_sensor_name = "Trigno Load Cell" + +# in emg_from_customcsv() +emg_from_customcsv__ref_signal = "REF_SIGNAL" +emg_from_customcsv__raw_signal = "RAW_SIGNAL" +emg_from_customcsv__ipts = "IPTS" +emg_from_customcsv__mupulses = "MUPULSES" +emg_from_customcsv__binary_mus_firing = "BINARY_MUS_FIRING" +emg_from_customcsv__accuracy = "ACCURACY" +emg_from_customcsv__extras = "EXTRAS" +emg_from_customcsv__fsamp = 2048 +emg_from_customcsv__ied = 8 +# TODO in main window and in advanced tools, when selecting OTB, delsys and custom CSV write to check settings file + +# in refsig_from_customcsv() +refsig_from_customcsv__ref_signal = "REF_SIGNAL" +refsig_from_customcsv__extras = "EXTRAS" + +# in save_json_emgfile() +save_json_emgfile__compresslevel = 4 + + +# ---------------------------------- analysis --------------------------------- + +# in compute_thresholds() +compute_thresholds__n_firings = 1 + +# in basic_mus_properties() +basic_mus_properties__n_firings_rt_dert = 1 +basic_mus_properties__accuracy = "default" +basic_mus_properties__ignore_negative_ipts = False +basic_mus_properties__constrain_pulses = [True, 3] + + +# ----------------------------------- tools ----------------------------------- + +# in resize_emgfile() +resize_emgfile__how = "ref_signal" +resize_emgfile__accuracy = "recalculate" +resize_emgfile__ignore_negative_ipts = False + + +# ------------------------------------ muap ----------------------------------- +# TODO missing custom order (2 variables) in traking and duplicates + +# in tracking() +tracking__firings = "all" +tracking__derivation = "sd" + +# in remove_duplicates_between() +remove_duplicates_between__firings = "all" +remove_duplicates_between__derivation = "sd" + +# in MUcv_gui() +MUcv_gui__n_firings = [0, 50] +MUcv_gui__muaps_timewindow = 50 +MUcv_gui__figsize = [25, 20] -# Code parameters adapted without restart -delsys_sensor_label = "Trigno Galileo Sensor" +# --------------------------------- electrodes -------------------------------- +sort_rawemg__custom_sorting_order = None From 63b0a18416cfffcb87dc4e0a0e22d7ce381612fc Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:54:12 +0100 Subject: [PATCH 33/57] Update muap.py --- openhdemg/library/muap.py | 110 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 5 deletions(-) diff --git a/openhdemg/library/muap.py b/openhdemg/library/muap.py index 631a152..79a18b9 100644 --- a/openhdemg/library/muap.py +++ b/openhdemg/library/muap.py @@ -729,6 +729,7 @@ def tracking( orientation=180, n_rows=None, n_cols=None, + custom_sorting_order=None, custom_muaps=None, exclude_belowthreshold=True, filter=True, @@ -765,12 +766,17 @@ def tracking( ``GR08MM1305`` ``GR04MM1305`` ``GR10MM0808`` - This is necessary to sort the channels in - the correct order. If matrixcode="None", the electrodes are not sorted. + ``Custom order`` + ``None`` + This is necessary to sort the channels in the correct order. + If matrixcode="None", the electrodes are not sorted. In this case, n_rows and n_cols must be specified. + If "Custom order", the electrodes are sorted based on + custom_sorting_order. orientation : int {0, 180}, default 180 Orientation in degree of the matrix (same as in OTBiolab). E.g. 180 corresponds to the matrix connection toward the user. + This Parameter is ignored if code=="Custom order" or code=="None". n_rows : None or int, default None The number of rows of the matrix. This parameter is used to divide the channels based on the matrix shape. These are normally inferred by the @@ -779,6 +785,15 @@ def tracking( The number of columns of the matrix. This parameter is used to divide the channels based on the matrix shape. These are normally inferred by the matrix code and must be specified only if code == None. + custom_sorting_order : None or list, default None + If code=="Custom order", custom_sorting_order will be used for + channels sorting. In this case, custom_sorting_order must be a list of + lists containing the order of the matrix channels. + Specifically, the number of columns are defined by + len(custom_sorting_order) while the number of rows by + len(custom_sorting_order[0]). np.nan can be used to specify empty + channels. Please refer to the Examples section for the structure of + the custom sorting order. custom_muaps : None or list, default None With this parameter, it is possible to perform MUs tracking on MUAPs computed with custom techniques. If this parameter is None (default), @@ -859,6 +874,35 @@ def tracking( 8 19 10 0.802635 9 21 14 0.896419 10 22 16 0.836356 + + Track MUs between two files where channels are sorted with a custom order. + + >>> import openhdemg.library as emg + >>> emgfile1 = emg.askopenfile(filesource="CUSTOMCSV") + >>> emgfile2 = emg.askopenfile(filesource="CUSTOMCSV") + >>> custom_sorting_order = [ + ... [63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51,], + ... [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,], + ... [37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25,], + ... [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,], + ... [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, np.nan,], + ... ] # 13 rows and 5 columns + >>> tracking_res = emg.tracking( + ... emgfile1=emgfile1, + ... emgfile2=emgfile2, + ... firings="all", + ... derivation="sd", + ... timewindow=50, + ... threshold=0.8, + ... matrixcode="Custom order", + ... orientation=180, + ... n_rows=None, + ... n_cols=None, + ... custom_sorting_order=custom_sorting_order, + ... exclude_belowthreshold=True, + ... filter=True, + ... show=False, + ... ) """ # Obtain STAs @@ -870,6 +914,7 @@ def tracking( orientation=orientation, n_rows=n_rows, n_cols=n_cols, + custom_sorting_order=custom_sorting_order, ) emgfile2_sorted = sort_rawemg( emgfile2, @@ -877,6 +922,7 @@ def tracking( orientation=orientation, n_rows=n_rows, n_cols=n_cols, + custom_sorting_order=custom_sorting_order, ) # Calculate the derivation if needed @@ -1070,6 +1116,7 @@ def remove_duplicates_between( orientation=180, n_rows=None, n_cols=None, + custom_sorting_order=None, custom_muaps=None, filter=True, show=False, @@ -1100,13 +1147,23 @@ def remove_duplicates_between( threshold : float, default 0.9 The 2-dimensional cross-correlation minimum value to consider two MUs to be the same. Ranges 0-1. - matrixcode : str {"GR08MM1305", "GR04MM1305", "GR10MM0808", "None"}, default "GR08MM1305" - The code of the matrix used. This is necessary to sort the channels in - the correct order. If matrixcode="None", the electrodes are not sorted. + matrixcode : str, default "GR08MM1305" + The code of the matrix used. It can be one of: + + ``GR08MM1305`` + ``GR04MM1305`` + ``GR10MM0808`` + ``Custom order`` + ``None`` + This is necessary to sort the channels in the correct order. + If matrixcode="None", the electrodes are not sorted. In this case, n_rows and n_cols must be specified. + If "Custom order", the electrodes are sorted based on + custom_sorting_order. orientation : int {0, 180}, default 180 Orientation in degree of the matrix (same as in OTBiolab). E.g. 180 corresponds to the matrix connection toward the user. + This Parameter is ignored if code=="Custom order" or code=="None". n_rows : None or int, default None The number of rows of the matrix. This parameter is used to divide the channels based on the matrix shape. These are normally inferred by the @@ -1115,6 +1172,15 @@ def remove_duplicates_between( The number of columns of the matrix. This parameter is used to divide the channels based on the matrix shape. These are normally inferred by the matrix code and must be specified only if code == None. + custom_sorting_order : None or list, default None + If code=="Custom order", custom_sorting_order will be used for + channels sorting. In this case, custom_sorting_order must be a list of + lists containing the order of the matrix channels. + Specifically, the number of columns are defined by + len(custom_sorting_order) while the number of rows by + len(custom_sorting_order[0]). np.nan can be used to specify empty + channels. Please refer to the Examples section for the structure of + the custom sorting order. custom_muaps : None or list, default None With this parameter, it is possible to perform MUs tracking on MUAPs computed with custom techniques. If this parameter is None (default), @@ -1181,6 +1247,39 @@ def remove_duplicates_between( ... ) >>> emg.asksavefile(emgfile1) >>> emg.asksavefile(emgfile2) + + Remove duplicated MUs between two files where channels are sorted with a + custom order and save the emgfiles without duplicates. Of the 2 duplicated + MUs, the one with the lowest accuracy is removed. + + >>> import openhdemg.library as emg + >>> emgfile1 = emg.askopenfile(filesource="CUSTOMCSV") + >>> emgfile2 = emg.askopenfile(filesource="CUSTOMCSV") + >>> custom_sorting_order = [ + ... [63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51,], + ... [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,], + ... [37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25,], + ... [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,], + ... [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, np.nan,], + ... ] # 13 rows and 5 columns + >>> emgfile1, emgfile2, tracking_res = emg.remove_duplicates_between( + ... emgfile1, + ... emgfile2, + ... firings="all", + ... derivation="sd", + ... timewindow=50, + ... threshold=0.9, + ... matrixcode="Custom order", + ... orientation=180, + ... n_rows=None, + ... n_cols=None, + ... custom_sorting_order=custom_sorting_order, + ... filter=True, + ... show=False, + ... which="accuracy", + ... ) + >>> emg.asksavefile(emgfile1) + >>> emg.asksavefile(emgfile2) """ # Work on deepcopies @@ -1199,6 +1298,7 @@ def remove_duplicates_between( orientation=orientation, n_rows=n_rows, n_cols=n_cols, + custom_sorting_order=custom_sorting_order, custom_muaps=custom_muaps, exclude_belowthreshold=True, filter=filter, From 3f8ce494d8ed6755263c3b1cbca68e18f2cad573 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 14 Mar 2024 14:54:18 +0100 Subject: [PATCH 34/57] Update openfiles.py --- openhdemg/library/openfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhdemg/library/openfiles.py b/openhdemg/library/openfiles.py index 396cf9d..4a39999 100644 --- a/openhdemg/library/openfiles.py +++ b/openhdemg/library/openfiles.py @@ -1171,7 +1171,7 @@ def emg_from_delsys( # --------------------------------------------------------------------- # # Function to open the reference signal from Delsys. -def refsig_from_delsys(filepath, refsig_sensor_name="Trigno Load Cell",): +def refsig_from_delsys(filepath, refsig_sensor_name="Trigno Load Cell"): """ Import the reference signal in the .mat file exportable by Delsys Neuromap. From 785076305d1ffc8c11d40cae444bc8f2ebd6bb07 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:39:22 +0100 Subject: [PATCH 35/57] Formatted gui_plotting.py --- openhdemg/gui/gui_modules/gui_plotting.py | 404 +++++++++++++++------- 1 file changed, 281 insertions(+), 123 deletions(-) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 7ec865e..b8cb8a4 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -2,7 +2,7 @@ import os import webbrowser -from tkinter import ttk, W, E,N, S, StringVar, PhotoImage +from tkinter import ttk, W, E, N, S, StringVar, PhotoImage from PIL import Image from sys import platform @@ -13,16 +13,19 @@ class PlotEmg: """ - A class to manage and display EMG (Electromyography) signal plots in a GUI application. + A class to manage and display EMG (Electromyography) signal plots in a GUI + application. - This class creates a plotting window with various options to create and display plots - from an EMG file. It provides functionalities to plot EMG signals, reference signals, - motor unit pulses, and more, with customizable settings. + This class creates a plotting window with various options to create and + display plots from an EMG file. It provides functionalities to plot EMG + signals, reference signals, motor unit pulses, and more, with customizable + settings. Attributes ---------- parent : object - The parent widget, typically the main application window that this PlotEmg instance belongs to. + The parent widget, typically the main application window that this + PlotEmg instance belongs to. head : CTkToplevel The top-level widget for the plotting window. matrix_rc : StringVar @@ -82,7 +85,7 @@ class PlotEmg: Plot the motor unit action potentials. on_matrix_none(self, *args) Handle changes in the matrix code selection. - + Examples -------- >>> main_window = Tk() @@ -91,31 +94,38 @@ class PlotEmg: Notes ----- - This class is dependent on the `ctk` and `ttk` modules from the `tkinter` library. - Some attributes and methods are conditional based on the `parent`'s properties. + This class is dependent on the `ctk` and `ttk` modules from the `tkinter` + library. + Some attributes and methods are conditional based on the `parent`'s + properties. """ + def __init__(self, parent): """ Initialize a new instance of the PlotEmg class. - This method sets up the GUI components of the PlotEmg window, including labels, - entries, checkboxes, and buttons for various plot settings and options. + This method sets up the GUI components of the PlotEmg window, + including labels, entries, checkboxes, and buttons for various plot + settings and options. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this PlotEmg instance belongs. + The parent widget, typically the main application window, to which + this PlotEmg instance belongs. Raises ------ AttributeError - If certain widgets are not properly instantiated due to missing parent resdict. + If certain widgets are not properly instantiated due to missing + parent resdict. """ + # Try is block is necessary as some widgets depend on parent resdict try: - # Initialize parent and load parent settings + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() @@ -123,12 +133,14 @@ def __init__(self, parent): self.head.title("Plot Window") # Set window icon - head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + head_path = os.path.dirname( + os.path.dirname(os.path.abspath(__file__)) + ) iconpath = head_path + "/gui_files/Icon2.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - + # Set resizable window # Configure columns with a loop for col in range(7): @@ -138,15 +150,17 @@ def __init__(self, parent): for row in range(21): self.head.rowconfigure(row, weight=1) - # define tk variables for later use - self.matrix_rc = StringVar() # Matrix rows columns - self.mat_label = ttk.Label() # Label for matriy rows columns - self.row_cols_entry = ttk.Entry() # Entry for matrix rows columns + # Define tk variables for later use + self.matrix_rc = StringVar() # Matrix rows columns + self.mat_label = ttk.Label() # Label for matriy rows columns + self.row_cols_entry = ttk.Entry() # Entry for matrix rows columns # Reference signal - ctk.CTkLabel(self.head, text="Reference signal", font=('Segoe UI',15, 'bold')).grid( - column=0, row=0, sticky=W - ) + ctk.CTkLabel( + self.head, + text="Reference signal", + font=('Segoe UI', 15, 'bold'), + ).grid(column=0, row=0, sticky=W) self.ref_but = StringVar() ref_button = ctk.CTkCheckBox( self.head, @@ -160,7 +174,11 @@ def __init__(self, parent): self.ref_but.set(False) # Time - ctk.CTkLabel(self.head, text="Time in seconds", font=('Segoe UI',15, 'bold')).grid(column=0, row=1, sticky=W) + ctk.CTkLabel( + self.head, + text="Time in seconds", + font=('Segoe UI', 15, 'bold'), + ).grid(column=0, row=1, sticky=W) self.time_sec = StringVar() time_button = ctk.CTkCheckBox( self.head, @@ -174,16 +192,24 @@ def __init__(self, parent): self.time_sec.set(False) # Figure Size - ctk.CTkLabel(self.head, text="Figure size in cm (h,w)", font=('Segoe UI',15, 'bold')).grid(column=0, row=2) + ctk.CTkLabel( + self.head, + text="Figure size in cm (h,w)", + font=('Segoe UI', 15, 'bold'), + ).grid(column=0, row=2) self.size_fig = StringVar() - fig_entry = ctk.CTkEntry(self.head, width=100, textvariable=self.size_fig) + fig_entry = ctk.CTkEntry( + self.head, width=100, textvariable=self.size_fig + ) self.size_fig.set("20,15") fig_entry.grid(column=1, row=2, sticky=W) # Plot emgsig plt_emgsig = ctk.CTkButton( self.head, text="Plot EMGsig", command=self.plt_emgsignal, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) plt_emgsig.grid(column=0, row=3, sticky=W) self.channels = StringVar() @@ -198,13 +224,17 @@ def __init__(self, parent): # Plot refsig plt_refsig = ctk.CTkButton( self.head, text="Plot RefSig", command=self.plt_refsignal, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) plt_refsig.grid(column=0, row=4, sticky=W) # Plot motor unit pulses plt_pulses = ctk.CTkButton( self.head, text="Plot MUpulses", command=self.plt_mupulses, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) plt_pulses.grid(column=0, row=5, sticky=W) # Define Linewidth for plot @@ -218,20 +248,28 @@ def __init__(self, parent): self.linewidth.set("Linewidth") # Plot impulse train - plt_ipts_but = ctk.CTkButton(self.head, text="Plot Source", command=self.plt_ipts, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plt_ipts_but = ctk.CTkButton( + self.head, text="Plot Source", command=self.plt_ipts, + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) plt_ipts_but.grid(column=0, row=6, sticky=W) self.mu_numb = StringVar() munumb_entry_values = ("0", "0,1,2", "0,1,2,3", "all") - munumb_entry = ctk.CTkComboBox(self.head, width=15, variable=self.mu_numb, - values=munumb_entry_values) + munumb_entry = ctk.CTkComboBox( + self.head, width=15, variable=self.mu_numb, + values=munumb_entry_values, + ) munumb_entry.grid(column=1, row=6, sticky=(W, E)) self.mu_numb.set("MU Number") # Plot instantaneous discharge rate - plt_idr_but = ctk.CTkButton(self.head, text="Plot IDR", command=self.plt_idr, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plt_idr_but = ctk.CTkButton( + self.head, text="Plot IDR", command=self.plt_idr, + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) plt_idr_but.grid(column=0, row=7, sticky=W) self.mu_numb_idr = StringVar() @@ -250,13 +288,24 @@ def __init__(self, parent): ) # Matrix code - ctk.CTkLabel(self.head, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid(row=0, column=3, sticky=(W)) + ctk.CTkLabel( + self.head, text="Matrix Code", font=('Segoe UI', 15, 'bold') + ).grid(row=0, column=3, sticky=(W)) self.mat_code = StringVar() # NOTE the matrix codes can only be those accepted by sort_rawemg(). matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings.delsys_sensor_label, "None") - matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", "Custom order", "None") - matrix_code = ctk.CTkComboBox(self.head, width=100, variable=self.mat_code, - values=matrix_code_values, state="readonly") + matrix_code_values = ( + "GR08MM1305", + "GR04MM1305", + "GR10MM0808", + "Trigno Galileo Sensor", + "Custom order", + "None", + ) + matrix_code = ctk.CTkComboBox( + self.head, width=100, variable=self.mat_code, + values=matrix_code_values, state="readonly", + ) matrix_code.grid(row=0, column=4, sticky=(W, E)) self.mat_code.set("GR08MM1305") @@ -264,7 +313,9 @@ def __init__(self, parent): self.mat_code.trace_add("write", self.on_matrix_none) # Matrix Orientation - ctk.CTkLabel(self.head, text="Orientation", font=('Segoe UI',15, 'bold')).grid(row=1, column=3, sticky=(W)) + ctk.CTkLabel( + self.head, text="Orientation", font=('Segoe UI', 15, 'bold'), + ).grid(row=1, column=3, sticky=(W)) self.mat_orientation = StringVar() orientation_values = ("0", "180") orientation = ctk.CTkComboBox( @@ -280,13 +331,17 @@ def __init__(self, parent): # Plot derivation # Button deriv_button = ctk.CTkButton( - self.head, text="Plot Derivation", command=self.plot_derivation, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + self.head, text="Plot Derivation", + command=self.plot_derivation, fg_color="#E5E4E2", + text_color="black", border_color="black", border_width=1, + ) deriv_button.grid(row=3, column=3, sticky=W) # Combobox Config self.deriv_config = StringVar() - configuration_values = ("Single differential", "Double differential") + configuration_values = ( + "Single differential", "Double differential" + ) configuration = ctk.CTkComboBox( self.head, width=15, variable=self.deriv_config, values=configuration_values, state="readonly" @@ -308,7 +363,9 @@ def __init__(self, parent): # Button muap_button = ctk.CTkButton( self.head, text="Plot MUAPs", command=self.plot_muaps, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) muap_button.grid(row=4, column=3, sticky=W) # Combobox Config @@ -330,35 +387,45 @@ def __init__(self, parent): # Combobox MU Number self.muap_munum = StringVar() - mu_numbers = tuple(str(number) for number in range(0, self.parent.resdict["NUMBER_OF_MUS"])) - muap_munum = ctk.CTkComboBox(self.head, width=15, variable=self.muap_munum, - values=mu_numbers, state="readonly") + mu_numbers = tuple( + str(number) for number in range(0, self.parent.resdict["NUMBER_OF_MUS"]) + ) + muap_munum = ctk.CTkComboBox( + self.head, width=15, variable=self.muap_munum, + values=mu_numbers, state="readonly", + ) muap_munum.grid(row=4, column=5, sticky=(W, E)) self.muap_munum.set("MU Number") # Combobox Timewindow self.muap_time = StringVar() timewindow_values = ("25", "50", "100", "200") - timewindow = ctk.CTkComboBox(self.head, width=120, variable=self.muap_time, - values=timewindow_values) + timewindow = ctk.CTkComboBox( + self.head, width=120, variable=self.muap_time, + values=timewindow_values, + ) timewindow.grid(row=4, column=6, sticky=(W, E)) self.muap_time.set("Timewindow (ms)") - # Disable Timewindow for DELSYS files + # Disable Timewindow for DELSYS files if self.parent.resdict["SOURCE"] == "DELSYS": timewindow.config(state="disabled") # Matrix Illustration Graphic - matrix_canvas = ctk.CTkCanvas(self.head, height=150, width=600, bg="white") + matrix_canvas = ctk.CTkCanvas( + self.head, height=150, width=600, bg="white", + ) matrix_canvas.grid(row=5, column=3, rowspan=5, columnspan=5) - parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + parent_dir = os.path.dirname( + os.path.dirname(os.path.abspath(__file__)) + ) self.matrix = PhotoImage( - file= parent_dir + "/gui_files/Matrix.png" + file=parent_dir + "/gui_files/Matrix.png" ) matrix_canvas.create_image(0, 0, anchor="nw", image=self.matrix) # Information Button self.info = ctk.CTkImage( light_image=Image.open(parent_dir + "/gui_files/Info.png"), - size = (30, 30) + size=(30, 30), ) info_button = ctk.CTkButton( self.head, @@ -370,7 +437,9 @@ def __init__(self, parent): fg_color="LightBlue4", command=lambda: ( ( - webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_basics/#plot-motor-units") + webbrowser.open( + "https://www.giacomovalli.com/openhdemg/gui_basics/#plot-motor-units" + ) ), ), ) @@ -380,25 +449,34 @@ def __init__(self, parent): child.grid_configure(padx=5, pady=5, ) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) self.head.destroy() ### Define functions for motor unit plotting def on_matrix_none(self, *args): """ - This function is called when the value of the mat_code variable is changed. + This function is called when the value of the mat_code variable is + changed. - When the variable is set to "None" it will create an Entrybox on the grid at column 1 and row 6, - and when the mat_code is set to something else it will remove the entrybox from the grid. + When the variable is set to "None" it will create an Entrybox on the + grid at column 1 and row 6, and when the mat_code is set to something + else it will remove the entrybox from the grid. """ + if self.mat_code.get() == "None": # Place label defined in init - self.mat_label = ttk.Label(self.head, text="Rows, Columns:") # Label for matriy rows columns + # Label for matriy rows columns + self.mat_label = ttk.Label(self.head, text="Rows, Columns:") self.mat_label.grid(row=0, column=5, sticky=E) # Column entry only when specified matrix code - self.row_cols_entry = ttk.Entry(self.head, width=8, textvariable= self.matrix_rc) - self.row_cols_entry.grid(row=0, column=6, sticky = W, padx=5) + self.row_cols_entry = ttk.Entry( + self.head, width=8, textvariable=self.matrix_rc, + ) + self.row_cols_entry.grid(row=0, column=6, sticky=W, padx=5) self.matrix_rc.set("13,5") else: @@ -408,12 +486,11 @@ def on_matrix_none(self, *args): self.head.update_idletasks() - def plt_emgsignal(self): """ Instance method to plot the raw emg signal in an seperate plot window. - The channels selected by the user are plotted. The plot can be saved and - partly edited using the matplotlib options. + The channels selected by the user are plotted. The plot can be saved + and partly edited using the matplotlib options. Executed when button "Plot EMGsig" in Plot Window is pressed. @@ -429,6 +506,7 @@ def plt_emgsignal(self): -------- plot_emgsignal in library. """ + try: # Create list of channels to be plotted channels = self.channels.get() @@ -459,17 +537,31 @@ def plt_emgsignal(self): ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid channel number or non-negative figure size.")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Enter valid channel number or non-negative figure size." + ), + ) except KeyError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid channel number.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid channel number."), + ) except IndexError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid figure size. Must be non negative and tuple of (heigth, width).")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Enter valid figure size. Must be non negative and tuple of (heigth, width)." + ), + ) def plt_refsignal(self): """ - Instance method to plot the reference signal in an seperate plot window. + Instance method to plot the reference signal in an seperate plot + window. The plot can be saved and partly edited using the matplotlib options. Executed when button "Plot REFsig" in Plot Window is pressed. @@ -478,6 +570,7 @@ def plt_refsignal(self): -------- plot_refsig in library. """ + # Create list of figsize figsize = [int(i) for i in self.size_fig.get().split(",")] @@ -505,6 +598,7 @@ def plt_mupulses(self): -------- plot_mupulses in library. """ + try: # Create list of figsize figsize = [int(i) for i in self.size_fig.get().split(",")] @@ -519,13 +613,16 @@ def plt_mupulses(self): ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid linewidth number.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid linewidth number."), + ) def plt_ipts(self): """ - Instance method to plot the motor unit pulse train in an seperate plot window. - The motor units selected by the user are plotted. The plot can be saved and - partly edited using the matplotlib options. + Instance method to plot the motor unit pulse train in an seperate plot + window. The motor units selected by the user are plotted. The plot can + be saved and partly edited using the matplotlib options. Executed when button "Plot Source" in Plot Window is pressed. @@ -540,6 +637,7 @@ def plt_ipts(self): -------- plot_ipts in library. """ + try: # Create list contaning motor units to be plotted mu_numb = self.mu_numb.get() @@ -580,16 +678,22 @@ def plt_ipts(self): ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid motor unit number."), + ) except KeyError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid motor unit number."), + ) def plt_idr(self): """ - Instance method to plot the instanteous discharge rate in an seperate plot window. - The motor units selected by the user are plotted. The plot can be saved and - partly edited using the matplotlib options. + Instance method to plot the instanteous discharge rate in an seperate + plot window. The motor units selected by the user are plotted. The + plot can be saved and partly edited using the matplotlib options. Executed when button "Plot IDR" in Plot Window is pressed. @@ -604,6 +708,7 @@ def plt_idr(self): -------- plot_idr in library. """ + try: mu_idr = self.mu_numb_idr.get() # Create list of figsize @@ -641,18 +746,25 @@ def plt_idr(self): ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid motor unit number."), + ) except KeyError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid motor unit number.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid motor unit number."), + ) def plot_derivation(self): """ - Instance method to plot the differential derivation of the RAW_SIGNAL by matrix column. + Instance method to plot the differential derivation of the RAW_SIGNAL + by matrix column. Both the single and the double differencials can be plotted. This function is used to plot also the sorted RAW_SIGNAL. - + Raises ------ ValueError @@ -666,6 +778,7 @@ def plot_derivation(self): -------- plot_differentials in library. """ + try: if self.mat_code.get() == "None": # Get rows and columns and turn into list @@ -682,7 +795,12 @@ def plot_derivation(self): ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Number of specified rows and columns must match number of channels.")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Number of specified rows and columns must match number of channels." + ), + ) return else: @@ -713,28 +831,41 @@ def plot_derivation(self): figsize=figsize, ) except ValueError as e: - show_error_dialog(parent=self, error=e, - solution=str("Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Rows, Columns arguments")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Rows, Columns arguments" + ), + ) except UnboundLocalError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid Configuration and Matrix Column.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid Configuration and Matrix Column."), + ) except KeyError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid Matrix Column.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid Matrix Column."), + ) def plot_muaps(self): """ - Instance methos to plot motor unit action potenital obtained from STA from one or - multiple MUs. Except for DELSYS files, where the STA is not comupted. + Instance methos to plot motor unit action potenital obtained from STA + from one or multiple MUs. Except for DELSYS files, where the STA is + not comupted. - There is no limit to the number of MUs and STA files that can be overplotted. - ``Remember: the different STAs should be matched`` with same number of electrode, - processing (i.e., differential) and computed on the same timewindow. + There is no limit to the number of MUs and STA files that can be + overplotted. + ``Remember: the different STAs should be matched`` with same number of + electrode, processing (i.e., differential) and computed on the same + timewindow. Raises ------ @@ -754,8 +885,12 @@ def plot_muaps(self): # DELSYS requires different MUAPS plot if self.parent.resdict["SOURCE"] == "DELSYS": figsize = [int(i) for i in self.size_fig.get().split(",")] - muaps_dict = openhdemg.extract_delsys_muaps(self.parent.resdict) - openhdemg.plot_muaps(muaps_dict[int(self.muap_munum.get())], figsize=figsize) + muaps_dict = openhdemg.extract_delsys_muaps( + self.parent.resdict, + ) + openhdemg.plot_muaps( + muaps_dict[int(self.muap_munum.get())], figsize=figsize, + ) else: if self.mat_code.get() == "None": @@ -773,7 +908,12 @@ def plot_muaps(self): ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Number of specified rows and columns must match")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Number of specified rows and columns must match" + ), + ) return else: @@ -789,7 +929,9 @@ def plot_muaps(self): diff_file = openhdemg.diff(sorted_rawemg=sorted_file) elif self.muap_config.get() == "Double differential": - diff_file = openhdemg.double_diff(sorted_rawemg=sorted_file) + diff_file = openhdemg.double_diff( + sorted_rawemg=sorted_file, + ) elif self.muap_config.get() == "Monopolar": diff_file = sorted_file @@ -807,31 +949,47 @@ def plot_muaps(self): figsize = [int(i) for i in self.size_fig.get().split(",")] # Plot MUAPS - openhdemg.plot_muaps(sta_dict[int(self.muap_munum.get())], figsize=figsize) + openhdemg.plot_muaps( + sta_dict[int(self.muap_munum.get())], figsize=figsize, + ) except ValueError as e: - show_error_dialog(parent=self, error=e, - solution=str("Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Timewindow" - + "\n - MU Number" - + "\n - Rows, Columns arguments")) - - show_error_dialog(parent=self, error=e, - solution=str("Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Timewindow" - + "\n - MU Number" - + "\n - Rows, Columns arguments")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Timewindow" + + "\n - MU Number" + + "\n - Rows, Columns arguments" + ), + ) + + show_error_dialog( + parent=self, error=e, + solution=str( + "Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Figure size arguments" + + "\n - Timewindow" + + "\n - MU Number" + + "\n - Rows, Columns arguments" + ), + ) except UnboundLocalError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid Configuration.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid Configuration."), + ) except KeyError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid Matrix Column.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid Matrix Column."), + ) From 5fd6ffcf4eeea72850b5ad13b718a71b1a0f5ee5 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:33:21 +0100 Subject: [PATCH 36/57] Progressing with formatting and settings Formatted gui_helpers.py and added/verified settings in gui_helpers.py and gui_plotting.py --- openhdemg/gui/gui_modules/gui_helpers.py | 125 ++++++++++++++-------- openhdemg/gui/gui_modules/gui_plotting.py | 1 + 2 files changed, 83 insertions(+), 43 deletions(-) diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 244fa1a..8c85dce 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -10,29 +10,32 @@ class GUIHelpers: """ - A utility class to provide additional functionalities in an openhdemg GUI application. + A utility class to provide additional functionalities in an openhdemg GUI + application. - This class includes helper functions to enhance user interaction with the application, - such as resizing files based on user input. It is designed to work in conjunction with - the main GUI components and relies on the parent widget for accessing shared data and resources. + This class includes helper functions to enhance user interaction with the + application, such as resizing files based on user input. It is designed to + work in conjunction with the main GUI components and relies on the parent + widget for accessing shared data and resources. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this GUIHelpers - instance belongs. + The parent widget, typically the main application window, to which + this GUIHelpers instance belongs. Methods ------- __init__(self, parent) Initialize a new instance of the GUIHelpers class. resize_file(self) - Resize the EMG file based on user-defined areas selected in the GUI plot. + Resize the EMG file based on user-defined areas selected in the GUI + plot. export_to_excel(self) - Save an analaysis dataframe to a csv file. + Save an analaysis dataframe to a csv file. sort_mus(self) Sort motor units according to recriuitement order. - + Examples -------- >>> main_window = Tk() @@ -42,14 +45,14 @@ class GUIHelpers: Raises ------ AttributeError - When no file is loaded prior to analysis or certain operations are attempted - without the necessary context or data. + When no file is loaded prior to analysis or certain operations are + attempted without the necessary context or data. Notes ----- - The class's methods interact with other components of the openhdemg application, - such as file handling and plotting utilities, and are dependent on the state of the - parent widget. + The class's methods interact with other components of the openhdemg + application, such as file handling and plotting utilities, and are + dependent on the state of the parent widget. """ @@ -57,17 +60,18 @@ def __init__(self, parent): """ Initialize a new instance of the GUIHelpers class. - Sets up a reference to the parent widget, which is used for accessing shared - resources and functionalities within the application. + Sets up a reference to the parent widget, which is used for accessing + shared resources and functionalities within the application. Parameters ---------- parent : object - The parent widget, usually the main application window, which provides - necessary context and data for the helper functions. + The parent widget, usually the main application window, which + provides necessary context and data for the helper functions. """ - # Initialize parent and load parent settings + + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() @@ -88,10 +92,12 @@ def resize_file(self): -------- showselect, resize_emgfile in library. """ + try: # Open selection window for user points = openhdemg.showselect( emgfile=self.parent.resdict, + how=self.parent.settings.resize_emgfile__how, title="Select the start/end area to resize by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", titlesize=10, ) @@ -100,27 +106,34 @@ def resize_file(self): # Delsys requires different handling for resize if self.parent.resdict["SOURCE"] == "DELSYS": self.parent.resdict, _, _ = openhdemg.resize_emgfile( - emgfile=self.parent.resdict, area=[start, end], accuracy="maintain" + emgfile=self.parent.resdict, area=[start, end], + accuracy="maintain" ) else: self.parent.resdict, _, _ = openhdemg.resize_emgfile( - emgfile=self.parent.resdict, area=[start, end] + emgfile=self.parent.resdict, area=[start, end], + accuracy=self.parent.settings.resize_emgfile__accuracy, + ignore_negative_ipts=self.parent.settings.resize_emgfile__ignore_negative_ipts, ) # Update Plot self.parent.in_gui_plotting(resdict=self.parent.resdict) # Update filelength - ctk.CTkLabel(self.parent.left, text=str(self.parent.resdict["EMG_LENGTH"]), font=('Segoe UI',12)).grid( - column=2, row=4, sticky=(W, E) - ) + ctk.CTkLabel( + self.parent.left, text=str(self.parent.resdict["EMG_LENGTH"]), + font=('Segoe UI', 12) + ).grid(column=2, row=4, sticky=(W, E)) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) def export_to_excel(self): """ - Instnace method to export any prior analysis results. Results are saved in an excel sheet - in a directory specified by the user. + Instnace method to export any prior analysis results. Results are + saved in an excel sheet in a directory specified by the user. Executed when button "Save Results" in master GUI window is pressed. @@ -131,12 +144,15 @@ def export_to_excel(self): AttributeError When no file was loaded in the GUI. """ + try: # Ask user to select the directory path = filedialog.askdirectory() # Define excel writer - writer = pd.ExcelWriter(path + "/Results_" + self.parent.filename + ".xlsx") + writer = pd.ExcelWriter( + path + "/Results_" + self.parent.filename + ".xlsx" + ) # Check for attributes and write sheets if hasattr(self.parent, "mvc_df"): @@ -146,10 +162,14 @@ def export_to_excel(self): self.parent.rfd.to_excel(writer, sheet_name="RFD") if hasattr(self.parent, "exportable_df"): - self.parent.mu_prop_df.to_excel(writer, sheet_name="Basic MU Properties") + self.parent.mu_prop_df.to_excel( + writer, sheet_name="Basic MU Properties" + ) if hasattr(self.parent, "mus_dr"): - self.parent.mus_dr.to_excel(writer, sheet_name="MU Discharge Rate") + self.parent.mus_dr.to_excel( + writer, sheet_name="MU Discharge Rate" + ) if hasattr(self.parent, "mu_thresholds"): self.mu_thresholds.to_excel(writer, sheet_name="MU Thresholds") @@ -157,20 +177,30 @@ def export_to_excel(self): writer.close() except IndexError as e: - show_error_dialog(parent=self, error=e, solution=str("Please conduct at least one analysis before saving.")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Please conduct at least one analysis before saving." + ), + ) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded.")) except PermissionError as e: - show_error_dialog(parent=self, error=e, solution=str("If /Results.xlsx already opened, please close.")) + show_error_dialog( + parent=self, error=e, + solution=str("If /Results.xlsx already opened, please close.")) def sort_mus(self): """ - Instance method to sort motor units ascending according to recruitement order. + Instance method to sort motor units ascending according to + recruitement order. - Executed when button "Sort MUs" in master GUI window is pressed. The plot of the MUs - and the emgfile are subsequently updated. + Executed when button "Sort MUs" in master GUI window is pressed. The + plot of the MUs and the emgfile are subsequently updated. Raises ------ @@ -181,19 +211,28 @@ def sort_mus(self): -------- sort_mus in library. """ + try: # Sort emgfile - self.parent.resdict = openhdemg.sort_mus(emgfile=self.parent.resdict) + self.parent.resdict = openhdemg.sort_mus( + emgfile=self.parent.resdict, + ) # Update plot if hasattr(self.parent, "fig"): self.parent.in_gui_plotting(resdict=self.parent.resdict) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) - except KeyError: - show_error_dialog(parent=self, error=e, - solution=str("Sorting not possible when ≤ 1" - + "\nMU is present in the File (i.e. Refsigs)")) - \ No newline at end of file + except KeyError as e: + show_error_dialog( + parent=self, error=e, + solution=str( + "Sorting not possible when ≤ 1" + + "\nMU is present in the File (i.e. Refsigs)" + ), + ) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index b8cb8a4..6178d1b 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -922,6 +922,7 @@ def plot_muaps(self): emgfile=self.parent.resdict, code=self.mat_code.get(), orientation=int(self.mat_orientation.get()), + custom_sorting_order=self.parent.settings.sort_rawemg__custom_sorting_order, ) # calcualte derivation From 132a8069d05a8e8ad89d78b98003628d1ba4e2e2 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:42:54 +0100 Subject: [PATCH 37/57] Progressing with formatting and settings Formatted and included/tested settings in mu_properties.py --- openhdemg/gui/gui_modules/mu_properties.py | 225 ++++++++++++++------- 1 file changed, 155 insertions(+), 70 deletions(-) diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index 28158e4..a6c8bd6 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -7,36 +7,45 @@ import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog + class MuAnalysis: """ A class for analyzing motor unit (MU) properties within a GUI application. - This class creates a window for analyzing various MU properties such as recruitment - threshold, discharge rate, and other basic properties. It is activated from the main - GUI window and allows for input and computation of MU-related metrics. + This class creates a window for analyzing various MU properties such as + recruitment threshold, discharge rate, and other basic properties. It is + activated from the main GUI window and allows for input and computation of + MU-related metrics. Attributes ---------- parent : object - The parent widget, typically the main application window that this MuAnalysis instance belongs to. + The parent widget, typically the main application window that this + MuAnalysis instance belongs to. head : CTkToplevel The top-level widget for the MU properties analysis window. mvc_value : StringVar - Tkinter StringVar for storing the Maximum Voluntary Contraction (MVC) value. + Tkinter StringVar for storing the Maximum Voluntary Contraction (MVC) + value. ct_event : StringVar Variable to store the chosen event type for computing MU thresholds. ct_type : StringVar - Variable to store the type of computation (absolute, relative, or both) for MU thresholds. + Variable to store the type of computation (absolute, relative, or both) + for MU thresholds. firings_rec : StringVar Variable to store the number of firings at recruitment. firings_ste : StringVar - Variable to store the number of firings at the start/end of steady phase. + Variable to store the number of firings at the start/end of steady + phase. dr_event : StringVar - Variable to store the chosen event type for computing MU discharge rate. + Variable to store the chosen event type for computing MU discharge + rate. b_firings_rec : StringVar - Variable to store the number of firings at recruitment for basic MU properties computation. + Variable to store the number of firings at recruitment for basic MU + properties computation. b_firings_ste : StringVar - Variable to store the number of firings at the start/end of steady phase for basic MU properties computation. + Variable to store the number of firings at the start/end of steady + phase for basic MU properties computation. Methods ------- @@ -48,7 +57,7 @@ class MuAnalysis: Compute the motor unit discharge rate. basic_mus_properties(self) Compute basic motor unit properties. - + Examples -------- >>> main_window = Tk() @@ -57,45 +66,49 @@ class MuAnalysis: Notes ----- - This class is dependent on the `ctk` and `ttk` modules from the `tkinter` library. - Some attributes and methods are conditional based on the `parent`'s properties. + This class is dependent on the `ctk` and `ttk` modules from the `tkinter` + library. Some attributes and methods are conditional based on the + `parent`'s properties. """ def __init__(self, parent): """ Initialize a new instance of the MuAnalysis class. - This method sets up the GUI components of the Motor Unit Properties window. It includes - input fields for MVC (Maximum Voluntary Contraction) value, buttons and dropdown menus - to compute MU thresholds, discharge rates, and basic MU properties. Each component is + This method sets up the GUI components of the Motor Unit Properties + window. It includes input fields for MVC (Maximum Voluntary + Contraction) value, buttons and dropdown menus to compute MU + thresholds, discharge rates, and basic MU properties. Each component is configured and placed in the window grid. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this MuAnalysis - instance belongs. The parent is used for accessing shared resources and data. + The parent widget, typically the main application window, to which + this MuAnalysis instance belongs. The parent is used for accessing + shared resources and data. Raises ------ AttributeError - If certain widgets or properties are not properly instantiated due to missing - parent configurations or resources. + If certain widgets or properties are not properly instantiated due + to missing parent configurations or resources. Notes ----- - The creation of the GUI components involves setting up various Tkinter and custom widgets - (like CTkLabel, CTkEntry, CTkButton, CTkComboBox). Each widget is configured with specific - properties like size, color, and variable bindings and placed in a grid layout. + The creation of the GUI components involves setting up various Tkinter + and custom widgets (like CTkLabel, CTkEntry, CTkButton, CTkComboBox). + Each widget is configured with specific properties like size, color, + and variable bindings and placed in a grid layout. """ - # Initialize parent and load parent settings + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() # Create new window self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title("Motor Unit Properties Window") - + # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" @@ -113,11 +126,14 @@ def __init__(self, parent): for row in range(21): self.head.rowconfigure(row, weight=1) - # MVC Entry - ctk.CTkLabel(self.head, text="Enter MVC[n]:", font=('Segoe UI',15, 'bold')).grid(column=0, row=0, sticky=(W)) + ctk.CTkLabel( + self.head, text="Enter MVC[n]:", font=('Segoe UI', 15, 'bold'), + ).grid(column=0, row=0, sticky=(W)) self.mvc_value = StringVar() - enter_mvc = ctk.CTkEntry(self.head, width=100, textvariable=self.mvc_value) + enter_mvc = ctk.CTkEntry( + self.head, width=100, textvariable=self.mvc_value, + ) enter_mvc.grid(column=1, row=0, sticky=(W, E)) # Compute MU re-/derecruitement threshold @@ -125,47 +141,62 @@ def __init__(self, parent): separator.grid(column=0, columnspan=4, row=2, padx=5, pady=5) thresh = ctk.CTkButton( - self.head, text="Compute threshold", command=self.compute_mu_threshold, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1 + self.head, text="Compute threshold", + command=self.compute_mu_threshold, fg_color="#E5E4E2", + text_color="black", border_color="black", border_width=1, ) thresh.grid(column=0, row=3, sticky=W) self.ct_event = StringVar() ct_event_values = ("rt", "dert", "rt_dert") - ct_events_entry = ctk.CTkComboBox(self.head, width=100, variable=self.ct_event, - values=ct_event_values, state="readonly") + ct_events_entry = ctk.CTkComboBox( + self.head, width=100, variable=self.ct_event, + values=ct_event_values, state="readonly", + ) ct_events_entry.grid(column=1, row=3) self.ct_event.set("Event") self.ct_type = StringVar() ct_types_values = ("abs", "rel", "abs_rel") - ct_types_entry = ctk.CTkComboBox(self.head, width=100, variable=self.ct_type, - values=ct_types_values, state="readonly") + ct_types_entry = ctk.CTkComboBox( + self.head, width=100, variable=self.ct_type, + values=ct_types_values, state="readonly", + ) ct_types_entry.grid(column=2, row=3) self.ct_type.set("Type") # Compute motor unit discharge rate separator1 = ttk.Separator(self.head, orient="horizontal") - separator1.grid(column=0, columnspan=4, row=4, sticky=(W, E), padx=5, pady=5) - - ctk.CTkLabel(self.head, text="Firings at Rec", font=('Segoe UI',15, 'bold')).grid(column=1, row=5, sticky=(W, E)) - ctk.CTkLabel(self.head, text="Firings Start/End Steady", font=('Segoe UI',15, 'bold')).grid( - column=2, row=5, sticky=(W, E) + separator1.grid( + column=0, columnspan=4, row=4, sticky=(W, E), padx=5, pady=5, ) + ctk.CTkLabel( + self.head, text="Firings at Rec", font=('Segoe UI', 15, 'bold'), + ).grid(column=1, row=5, sticky=(W, E)) + ctk.CTkLabel( + self.head, text="Firings Start/End Steady", + font=('Segoe UI', 15, 'bold'), + ).grid(column=2, row=5, sticky=(W, E)) + dr_rate = ctk.CTkButton( - self.head, text="Compute discharge rate", command=self.compute_mu_dr, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1 + self.head, text="Compute discharge rate", + command=self.compute_mu_dr, fg_color="#E5E4E2", text_color="black", + border_color="black", border_width=1, ) dr_rate.grid(column=0, row=6, sticky=W) self.firings_rec = StringVar() - firings_1 = ctk.CTkEntry(self.head, width=100, textvariable=self.firings_rec) + firings_1 = ctk.CTkEntry( + self.head, width=100, textvariable=self.firings_rec, + ) firings_1.grid(column=1, row=6) self.firings_rec.set(4) self.firings_ste = StringVar() - firings_2 = ctk.CTkEntry(self.head, width=100, textvariable=self.firings_ste) + firings_2 = ctk.CTkEntry( + self.head, width=100, textvariable=self.firings_ste, + ) firings_2.grid(column=2, row=6) self.firings_ste.set(10) @@ -177,33 +208,46 @@ def __init__(self, parent): "steady", "rec_derec_steady", ) - dr_events_entry = ctk.CTkComboBox(self.head, width=100, variable=self.dr_event, - values=dr_events_values, state="readonly") + dr_events_entry = ctk.CTkComboBox( + self.head, width=100, variable=self.dr_event, + values=dr_events_values, state="readonly", + ) dr_events_entry.grid(column=3, row=6, sticky=E) self.dr_event.set("Event") # Compute basic motor unit properties separator2 = ttk.Separator(self.head, orient="horizontal") - separator2.grid(column=0, columnspan=4, row=7, sticky=(W, E), padx=5, pady=5) - - ctk.CTkLabel(self.head, text="Firings at Rec", font=('Segoe UI',15, 'bold')).grid(column=1, row=8, sticky=(W, E)) - ctk.CTkLabel(self.head, text="Firings Start/End Steady", font=('Segoe UI',15, 'bold')).grid( - column=2, row=8, sticky=(W, E) + separator2.grid( + column=0, columnspan=4, row=7, sticky=(W, E), padx=5, pady=5, ) + ctk.CTkLabel( + self.head, text="Firings at Rec", + font=('Segoe UI', 15, 'bold'), + ).grid(column=1, row=8, sticky=(W, E)) + ctk.CTkLabel( + self.head, text="Firings Start/End Steady", + font=('Segoe UI', 15, 'bold'), + ).grid(column=2, row=8, sticky=(W, E)) + basic = ctk.CTkButton( - self.head, text="Basic MU properties", command=self.basic_mus_properties, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1 + self.head, text="Basic MU properties", + command=self.basic_mus_properties, fg_color="#E5E4E2", + text_color="black", border_color="black", border_width=1, ) basic.grid(column=0, row=9, sticky=W) self.b_firings_rec = StringVar() - b_firings_1 = ctk.CTkEntry(self.head, width=100, textvariable=self.b_firings_rec) + b_firings_1 = ctk.CTkEntry( + self.head, width=100, textvariable=self.b_firings_rec, + ) b_firings_1.grid(column=1, row=9) self.b_firings_rec.set(4) self.b_firings_ste = StringVar() - b_firings_2 = ctk.CTkEntry(self.head, width=100, textvariable=self.b_firings_ste) + b_firings_2 = ctk.CTkEntry( + self.head, width=100, textvariable=self.b_firings_ste, + ) b_firings_2.grid(column=2, row=9) self.b_firings_ste.set(10) @@ -217,8 +261,9 @@ def compute_mu_threshold(self): Instance method to compute the motor unit recruitement thresholds based on user selection of events and types. - Executed when button "Compute threshold" in Motor Unit Properties Window - is pressed. The analysis results are displayed in the result terminal. + Executed when button "Compute threshold" in Motor Unit Properties + Window is pressed. The analysis results are displayed in the result + terminal. Raises ------ @@ -233,33 +278,45 @@ def compute_mu_threshold(self): -------- compute_thresholds in library. """ + try: # Compute thresholds self.parent.mu_thresholds = openhdemg.compute_thresholds( emgfile=self.parent.resdict, event_=self.ct_event.get(), type_=self.ct_type.get(), + n_firings=self.parent.settings.compute_thresholds__n_firings, mvc=float(self.mvc_value.get()), ) # Display results self.parent.display_results(self.parent.mu_thresholds) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid MVC, Event or Type.")) + show_error_dialog( + parent=self, error=e, + solution=str("Enter valid MVC, Event or Type."), + ) except AssertionError as e: - show_error_dialog(parent=self, error=e, solution=str("Specify Event and/or Type.")) + show_error_dialog( + parent=self, error=e, + solution=str("Specify Event and/or Type."), + ) def compute_mu_dr(self): """ Instance method to compute the motor unit discharge rate based on user selection of Firings and Events. - Executed when button "Compute discharge rate" in Motor Unit Properties Window - is pressed. The analysis results are displayed in the result terminal. + Executed when button "Compute discharge rate" in Motor Unit Properties + Window is pressed. The analysis results are displayed in the result + terminal. Raises ------ @@ -274,6 +331,7 @@ def compute_mu_dr(self): -------- compute_dr in library. """ + try: # Compute discharge rates self.parent.mus_dr = openhdemg.compute_dr( @@ -286,21 +344,32 @@ def compute_mu_dr(self): self.parent.display_results(self.parent.mus_dr) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded.")) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid Firings value or select a correct number of points.")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Enter valid Firings value or select a correct number of points." + ), + ) except AssertionError as e: - show_error_dialog(parent=self, error=e, solution=str("Specify Event and/or Type.")) + show_error_dialog( + parent=self, error=e, + solution=str("Specify Event and/or Type."), + ) def basic_mus_properties(self): """ Instance method to compute basic motor unit properties based in user selection in plot. - Executed when button "Basic MU properties" in Motor Unit Properties Window - is pressed. The analysis results are displayed in the result terminal. + Executed when button "Basic MU properties" in Motor Unit Properties + Window is pressed. The analysis results are displayed in the result + terminal. Raises ------ @@ -317,25 +386,41 @@ def basic_mus_properties(self): -------- basic_mus_properties in library. """ + try: # Calculate properties self.parent.mu_prop_df = openhdemg.basic_mus_properties( emgfile=self.parent.resdict, + n_firings_rt_dert=self.parent.settings.basic_mus_properties__n_firings_rt_dert, n_firings_RecDerec=int(self.b_firings_rec.get()), n_firings_steady=int(self.b_firings_ste.get()), + accuracy=self.parent.settings.basic_mus_properties__accuracy, + ignore_negative_ipts=self.parent.settings.basic_mus_properties__ignore_negative_ipts, + constrain_pulses=self.parent.settings.basic_mus_properties__constrain_pulses, mvc=float(self.mvc_value.get()), ) # Display results self.parent.display_results(self.parent.mu_prop_df) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded.")) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Enter valid MVC value or select a correct number of points.")) + show_error_dialog( + parent=self, error=e, + solution=str( + "Enter valid MVC value or select a correct number of points." + ), + ) except AssertionError as e: - show_error_dialog(parent=self, error=e, solution=str("Specify Event and/or Type.")) + show_error_dialog( + parent=self, error=e, + solution=str("Specify Event and/or Type.")) except UnboundLocalError as e: - show_error_dialog(parent=self, error=e, solution=str("Select start/end area again.")) + show_error_dialog( + parent=self, error=e, + solution=str("Select start/end area again.")) From f35a57d9ef8190706305d1d17ec04c7b36487caf Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 21 Mar 2024 22:43:44 +0100 Subject: [PATCH 38/57] Update settings.py --- openhdemg/gui/openhdemg_gui.py | 5 ++++- openhdemg/gui/settings.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 9ec80ee..d6b58f3 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -146,7 +146,7 @@ def __init__(self, master): self.master.rowconfigure(0, weight=1) # Create left side framing for functionalities - self.left = ctk.CTkFrame(self.master, fg_color=self.settings.background_color, corner_radius=0) + self.left = ctk.CTkFrame(self.master, fg_color=self.settings.gui_background_color, corner_radius=0) self.left.grid(column=0, row=0, sticky=(N, S, E, W)) # Configure columns with a loop @@ -663,6 +663,9 @@ def on_filetype_change(self, *args): create a second combobox on the grid at column 0 and row 2 and when the filetype is set to something else it will remove the second combobox from the grid. """ + # TODO here, instead of showing the boxes, we shopuld simply write a text to tell them to check in the gui_settings + # File if the settings are appropriate for their file. e.g., extension factor and fsamp. + # A text "Verify openfiles settings" might be appropriate. To be displayed similary to "Ignored for DEMUSE files". if self.filetype.get() not in ["OTB"]: if hasattr(self, "otb_combobox"): self.otb_combobox.grid_forget() diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index 1b84c87..c8ff3fe 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -14,12 +14,15 @@ # TODO add link to docs tutorial """ +import numpy as np + # These graphic parameters are updated only after restarting the GUI # # --------------------------------- GUI LOOK ---------------------------------- -background_color = "LightBlue4" +gui_background_color = "LightBlue4" # Options are #TODO Paul please explain what colors can be used, I will add these in the tutorial #TODO we also must rename this file in something more specific like gui_settings.py +# Please insert gui_background_color were needed in the .py GUI files, I didn't do that # The following parameters are updated without restarting the GUI @@ -58,6 +61,8 @@ emg_from_customcsv__fsamp = 2048 emg_from_customcsv__ied = 8 # TODO in main window and in advanced tools, when selecting OTB, delsys and custom CSV write to check settings file +# because I want to remove the dropdown for the otb extension factor and the dropdown for the discharge rate and only allow +# To specify these from the settings. # in refsig_from_customcsv() refsig_from_customcsv__ref_signal = "REF_SIGNAL" @@ -67,19 +72,19 @@ save_json_emgfile__compresslevel = 4 -# ---------------------------------- analysis --------------------------------- +# ---------------------------------- analysis --------------------------------- # DONE and it works # in compute_thresholds() compute_thresholds__n_firings = 1 # in basic_mus_properties() basic_mus_properties__n_firings_rt_dert = 1 -basic_mus_properties__accuracy = "default" +basic_mus_properties__accuracy = "SIL_PNR" basic_mus_properties__ignore_negative_ipts = False basic_mus_properties__constrain_pulses = [True, 3] -# ----------------------------------- tools ----------------------------------- +# ----------------------------------- tools ----------------------------------- # DONE and it works # in resize_emgfile() resize_emgfile__how = "ref_signal" @@ -103,5 +108,5 @@ MUcv_gui__muaps_timewindow = 50 MUcv_gui__figsize = [25, 20] -# --------------------------------- electrodes -------------------------------- +# --------------------------------- electrodes -------------------------------- # DONE only in plot window, it works sort_rawemg__custom_sorting_order = None From a8951e5eff777d07d5226059b08aca76f63b57ac Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:20:44 +0100 Subject: [PATCH 39/57] Implementing settings --- openhdemg/gui/gui_modules/error_handler.py | 84 +++-- openhdemg/gui/openhdemg_gui.py | 395 ++++++++++++++------- openhdemg/gui/settings.py | 14 +- 3 files changed, 320 insertions(+), 173 deletions(-) diff --git a/openhdemg/gui/gui_modules/error_handler.py b/openhdemg/gui/gui_modules/error_handler.py index 986bf89..c6db5d7 100644 --- a/openhdemg/gui/gui_modules/error_handler.py +++ b/openhdemg/gui/gui_modules/error_handler.py @@ -11,19 +11,22 @@ class ErrorDialog: """ A dialog window for displaying error messages and solutions to the user. - This class creates a custom dialog window using customtkinter (ctk) components to show an error message - and a potential solution. The dialog includes an information icon, labels for the error and solution, - and is styled with specific foreground and background colors. + This class creates a custom dialog window using customtkinter (ctk) + components to show an error message and a potential solution. The dialog + includes an information icon, labels for the error and solution, and is + styled with specific foreground and background colors. Parameters ---------- parent : tk.Tk or ctk.CTk - The parent window to which this dialog is attached. It can be either a Tkinter root window - or another customtkinter component. + The parent window to which this dialog is attached. It can be either a + Tkinter root window or another customtkinter component. error : str - The error message to be displayed in the dialog. This should describe what went wrong. + The error message to be displayed in the dialog. This should describe + what went wrong. solution : str - A message providing a potential solution or workaround for the error described. + A message providing a potential solution or workaround for the error + described. Attributes ---------- @@ -32,7 +35,8 @@ class ErrorDialog: head : ctk.CTkToplevel The top-level window component of the dialog. content_frame : ctk.CTkFrame - A frame widget that holds the content of the dialog, including the error and solution messages. + A frame widget that holds the content of the dialog, including the + error and solution messages. info_photo : ctk.CTkImage The photo image of the information icon displayed in the dialog. icon : ctk.CTkLabel @@ -47,7 +51,8 @@ class ErrorDialog: Methods ------- __init__(self, parent, error, solution) - Initializes the ErrorDialog with the parent window, error message, and solution message. + Initializes the ErrorDialog with the parent window, error message, and + solution message. Example ------- @@ -64,38 +69,51 @@ def __init__(self, parent, error, solution): self.head = ctk.CTkToplevel(fg_color="#FFBF00") self.head.title("Error Dialog") self.head.geometry("500x300") # Adjust the size as needed - + # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - + path = os.path.dirname(os.path.abspath(__file__)) - # Create a frame for the content with blue background, placed in the middle - self.content_frame = ctk.CTkFrame(self.head, corner_radius=10, fg_color="LightBlue4", bg_color="#FFBF00") + # Create a frame for the content with blue background, placed in the + # middle. + self.content_frame = ctk.CTkFrame( + self.head, corner_radius=10, fg_color="LightBlue4", + bg_color="#FFBF00", + ) self.content_frame.pack(padx=50, expand=True, fill="both") # Load an information icon and display it - self.info_photo = ctk.CTkImage(light_image=Image.open(path + "/Error.png"), size=(50, 50)) - self.icon = ctk.CTkLabel(self.content_frame, text="", - image=self.info_photo, bg_color="LightBlue4") + self.info_photo = ctk.CTkImage( + light_image=Image.open(path + "/Error.png"), size=(50, 50), + ) + self.icon = ctk.CTkLabel( + self.content_frame, text="", image=self.info_photo, + bg_color="LightBlue4", + ) self.icon.pack(pady=5) - self.icon_info = ctk.CTkLabel(self.content_frame, - text="INFORMATION", font=("Arial", 16, "bold"), - text_color="#123456") + self.icon_info = ctk.CTkLabel( + self.content_frame, text="INFORMATION", font=("Arial", 16, "bold"), + text_color="#123456", + ) self.icon_info.pack(pady=5) # Error solution label (larger, bold), placed below the icon - self.solution_label = ctk.CTkLabel(self.content_frame, text=solution, - font=("Arial", 14, "bold"), wraplength=400, fg_color="LightBlue4") + self.solution_label = ctk.CTkLabel( + self.content_frame, text=solution, font=("Arial", 14, "bold"), + wraplength=400, fg_color="LightBlue4", + ) self.solution_label.pack(pady=(10, 5)) # Error traceback label (smaller) - self.error_label = ctk.CTkLabel(self.content_frame, text=error, - font=("Arial", 10), wraplength=400, fg_color="LightBlue4") + self.error_label = ctk.CTkLabel( + self.content_frame, text=error, font=("Arial", 10), wraplength=400, + fg_color="LightBlue4", + ) self.error_label.pack(pady=(5, 10), expand=True, fill='both') # Make window modal @@ -104,10 +122,12 @@ def __init__(self, parent, error, solution): def show_error_dialog(parent, error, solution): """ - Displays an ErrorDialog with a formatted error traceback and a solution message. + Displays an ErrorDialog with a formatted error traceback and a solution + message. - This function takes an exception as input, formats its traceback, and then creates - an ErrorDialog to display the formatted traceback along with a solution message. + This function takes an exception as input, formats its traceback, and then + creates an ErrorDialog to display the formatted traceback along with a + solution message. Parameters ---------- @@ -131,8 +151,14 @@ def show_error_dialog(parent, error, solution): >>> root.mainloop() """ if error is None: - error_message = "".join(traceback.format_exception(type(error), value=error, tb=None)) + error_message = "".join( + traceback.format_exception(type(error), value=error, tb=None) + ) else: - error_message = "".join(traceback.format_exception(type(error), value=error, tb=error.__traceback__)) - + error_message = "".join( + traceback.format_exception( + type(error), value=error, tb=error.__traceback__, + ) + ) + ErrorDialog(parent, error_message, solution) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index d6b58f3..c7f300d 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -10,7 +10,9 @@ import tkinter as tk import threading import webbrowser -from tkinter import messagebox, ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E +from tkinter import ( + messagebox, ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E, +) import customtkinter as ctk from pandastable import Table, config @@ -18,25 +20,29 @@ import matplotlib.pyplot as plt import matplotlib -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk +from matplotlib.backends.backend_tkagg import ( + FigureCanvasTkAgg, NavigationToolbar2Tk, +) from matplotlib.figure import Figure import openhdemg.library as openhdemg import openhdemg.gui.settings as settings -from openhdemg.gui.gui_modules import (MURemovalWindow, EditRefsig, GUIHelpers, - AnalyseForce, MuAnalysis, PlotEmg, AdvancedAnalysis, - show_error_dialog) +from openhdemg.gui.gui_modules import ( + MURemovalWindow, EditRefsig, GUIHelpers, AnalyseForce, MuAnalysis, PlotEmg, + AdvancedAnalysis, show_error_dialog, +) matplotlib.use("TkAgg") + class emgGUI(): """ - This class is used to create a graphical user interface for - the openhdemg library. + This class is used to create a graphical user interface for the openhdemg + library. - Within this class and corresponding childs, most functionalities - of the ophdemg library are packed in a GUI. Howebver, the library is more + Within this class and corresponding childs, most functionalities of the + openhdemg library are packed in a GUI. However, the library is more comprehensive and much more adaptable to the users needs. Attributes @@ -55,7 +61,8 @@ class emgGUI(): String containing the path to EMG file selected for analysis. self.filetype : str String containing the filetype of import EMG file. - Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG". + Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", + "CUSTOMCSV", "CUSTOMCSV_REFSIG". self.left : tk.frame Left frame inside of master that contains all buttons and filespecs. self.logo : @@ -69,7 +76,8 @@ class emgGUI(): self.right : tk.frame Left frame inside of master that contains plotting canvas. self.terminal : ttk.Labelframe - Tkinter labelframe that is used to display the results table in the GUI. + Tkinter labelframe that is used to display the results table in the + GUI. self.info : tk.PhotoImage Information Icon displayed in GUI. self.online : tk.Photoimage @@ -81,9 +89,8 @@ class emgGUI(): self.cite : tk.PhotoImage Citation Icon displayed in GUI. self.otb_combobox : ttk.Combobox - Combobox appearing in main GUI window or advanced - analysis window when OTB files are loaded. Contains - the extension factor for OTB files. + Combobox appearing in main GUI window or advanced analysis window when + OTB files are loaded. Contains the extension factor for OTB files. Stringvariable containing the self.extension_factor : tk.StringVar() Stringvariable containing the OTB extension factor value. @@ -99,7 +106,8 @@ class emgGUI(): Saves the edited emgfile dictionary to a .json file. Executed when button "Save File" in master GUI window pressed. reset_analysis() - Resets the whole analysis, restores the original input file and the graph. + Resets the whole analysis, restores the original input file and the + graph. Executed when button "Reset analysis" in master GUI window pressed. in_gui_plotting() Method used for creating plot inside the GUI (on the GUI canvas). @@ -109,18 +117,21 @@ class emgGUI(): Executed when button "MU properties" in master GUI window pressed. display_results() Method used to display result table containing analysis results. - + Notes ----- Please note that altough we created a GUI class, the included methods/ - instances are highly specific. We did not conceptualize the methods/instances - to be used seperately. Similar functionalities are available in the library - and were specifically coded to be used seperately/singularly. - - Most instance methods of this class heavily rely on the functions provided in - the library. In the section "See Also" at each instance method, the reader is - referred to the corresponding function and extensive documentation in the library. + instances are highly specific. We did not conceptualize the + methods/instances to be used seperately. Similar functionalities are + available in the library and were specifically coded to be used + seperately/singularly. + + Most instance methods of this class heavily rely on the functions provided + in the library. In the section "See Also" at each instance method, the + reader is referred to the corresponding function and extensive + documentation in the library. """ + def __init__(self, master): """ Initialization of master GUI window upon calling. @@ -130,6 +141,7 @@ def __init__(self, master): master: tk tk class object """ + # Load settings self.load_settings() @@ -146,7 +158,10 @@ def __init__(self, master): self.master.rowconfigure(0, weight=1) # Create left side framing for functionalities - self.left = ctk.CTkFrame(self.master, fg_color=self.settings.gui_background_color, corner_radius=0) + self.left = ctk.CTkFrame( + self.master, fg_color=self.settings.gui_background_color, + corner_radius=0, + ) self.left.grid(column=0, row=0, sticky=(N, S, E, W)) # Configure columns with a loop @@ -159,104 +174,170 @@ def __init__(self, master): # Specify filetype self.filetype = StringVar() - signal_value = ["OPENHDEMG", "DEMUSE", "OTB", "OTB_REFSIG", "DELSYS", "DELSYS_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG"] - signal_entry = ctk.CTkComboBox(self.left, width=150, variable=self.filetype, values=signal_value, state="readonly") - signal_entry.grid(column=0, row=1, sticky=(N, S, E, W)) + signal_value = [ + "OPENHDEMG", "DEMUSE", "OTB", "OTB_REFSIG", "DELSYS", + "DELSYS_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG", + ] + signal_entry = ctk.CTkComboBox( + self.left, width=8, variable=self.filetype, values=signal_value, + state="readonly", + ) + signal_entry.grid(column=0, row=1, sticky=(W, E)) self.filetype.set("Type of file") - # Trace filetype to apply function when changeing + # Trace filetype to apply function when changing self.filetype.trace_add("write", self.on_filetype_change) # Load file - load = ctk.CTkButton(self.left, text="Load File", command=self.get_file_input, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + load = ctk.CTkButton( + self.left, text="Load File", command=self.get_file_input, + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) load.grid(column=0, row=3, sticky=(N, S, E, W)) # File specifications - ctk.CTkLabel(self.left, text="Filespecs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=1, sticky=(W)) - ctk.CTkLabel(self.left, text="N Channels:", font=('Segoe UI',15, 'bold')).grid(column=1, row=2, sticky=(W)) - ctk.CTkLabel(self.left, text="N of MUs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=3, sticky=(W)) - ctk.CTkLabel(self.left, text="File length:", font=('Segoe UI',15, 'bold')).grid(column=1, row=4, sticky=(W)) + ctk.CTkLabel( + self.left, text="Filespecs:", font=('Segoe UI', 15, 'bold'), + ).grid(column=1, row=1, sticky=(W)) + ctk.CTkLabel( + self.left, text="N Channels:", font=('Segoe UI', 15, 'bold'), + ).grid(column=1, row=2, sticky=(W)) + ctk.CTkLabel( + self.left, text="N of MUs:", font=('Segoe UI', 15, 'bold'), + ).grid(column=1, row=3, sticky=(W)) + ctk.CTkLabel( + self.left, text="File length:", font=('Segoe UI', 15, 'bold'), + ).grid(column=1, row=4, sticky=(W)) separator0 = ttk.Separator(self.left, orient="horizontal") separator0.grid(column=0, columnspan=3, row=5, sticky=(E, W)) - + # Save File - save = ctk.CTkButton(self.left, text="Save File", command=self.save_emgfile, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + save = ctk.CTkButton( + self.left, text="Save File", command=self.save_emgfile, + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) save.grid(column=0, row=6, sticky=(N, S, E, W)) separator1 = ttk.Separator(self.left, orient="horizontal") separator1.grid(column=0, columnspan=3, row=7, sticky=(E, W)) # Export to Excel - export = ctk.CTkButton(self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + export = ctk.CTkButton( + self.left, text="Save Results", + command=lambda: (GUIHelpers(parent=self).export_to_excel()), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) export.grid(column=1, row=6, sticky=(N, S, E, W)) # View Motor Unit Firings - firings = ctk.CTkButton(self.left, text="View MUs", command=lambda: (self.in_gui_plotting(resdict=self.resdict)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + firings = ctk.CTkButton( + self.left, text="View MUs", + command=lambda: (self.in_gui_plotting(resdict=self.resdict)), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) firings.grid(column=0, row=8, sticky=(N, S, E, W)) # Sort Motor Units - sorting = ctk.CTkButton(self.left, text="Sort MUs", command=lambda: (GUIHelpers(parent=self).sort_mus()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + sorting = ctk.CTkButton( + self.left, text="Sort MUs", + command=lambda: (GUIHelpers(parent=self).sort_mus()), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) sorting.grid(column=1, row=8, sticky=(N, S, E, W)) separator2 = ttk.Separator(self.left, orient="horizontal") separator2.grid(column=0, columnspan=3, row=9, sticky=(E, W)) # Remove Motor Units - remove_mus = ctk.CTkButton(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + remove_mus = ctk.CTkButton( + self.left, text="Remove MUs", + command=lambda: (MURemovalWindow(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) remove_mus.grid(column=0, row=10, sticky=(N, S, E, W)) separator3 = ttk.Separator(self.left, orient="horizontal") separator3.grid(column=0, columnspan=3, row=11, sticky=(E, W)) # Filter Reference Signal - reference = ctk.CTkButton(self.left, text="RefSig Editing", command=lambda:(EditRefsig(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + reference = ctk.CTkButton( + self.left, text="RefSig Editing", + command=lambda: (EditRefsig(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) reference.grid(column=0, row=12, sticky=(N, S, E, W)) # Resize File - resize = ctk.CTkButton(self.left, text="Resize File", command=lambda:(GUIHelpers(parent=self).resize_file()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + resize = ctk.CTkButton( + self.left, text="Resize File", + command=lambda: (GUIHelpers(parent=self).resize_file()), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) resize.grid(column=1, row=12, sticky=(N, S, E, W)) separator4 = ttk.Separator(self.left, orient="horizontal") separator4.grid(column=0, columnspan=3, row=13, sticky=(E, W)) # Force Analysis - force = ctk.CTkButton(self.left, text="Analyse Force", command=lambda:(AnalyseForce(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + force = ctk.CTkButton( + self.left, text="Analyse Force", + command=lambda: (AnalyseForce(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) force.grid(column=0, row=14, sticky=(N, S, E, W)) separator5 = ttk.Separator(self.left, orient="horizontal") separator5.grid(column=0, columnspan=3, row=15, sticky=(E, W)) # Motor Unit properties - mus = ctk.CTkButton(self.left, text="MU Properties", command=lambda:(MuAnalysis(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + mus = ctk.CTkButton( + self.left, text="MU Properties", + command=lambda: (MuAnalysis(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) mus.grid(column=1, row=14, sticky=(N, S, E, W)) separator6 = ttk.Separator(self.left, orient="horizontal") separator6.grid(column=0, columnspan=3, row=17, sticky=(E, W)) # Plot EMG - plots = ctk.CTkButton(self.left, text="Plot EMG", command=lambda:(PlotEmg(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plots = ctk.CTkButton( + self.left, text="Plot EMG", + command=lambda: (PlotEmg(parent=self)), + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) plots.grid(column=0, row=16, sticky=(N, S, E, W)) separator7 = ttk.Separator(self.left, orient="horizontal") separator7.grid(column=0, columnspan=3, row=19, sticky=(E, W)) # Reset Analysis - reset = ctk.CTkButton(self.left, text="Reset Analysis", command=self.reset_analysis, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + reset = ctk.CTkButton( + self.left, text="Reset Analysis", + command=self.reset_analysis, + fg_color="#E5E4E2", text_color="black", border_color="black", + border_width=1, + ) reset.grid(column=1, row=18, sticky=(N, S, E, W)) # Advanced tools - advanced = ctk.CTkButton(self.left, text="Advanced Tools", command=lambda:(AdvancedAnalysis(self)), - fg_color="#000000", text_color="white", border_color="white", border_width=1, - hover_color="#FFBF00") + advanced = ctk.CTkButton( + self.left, text="Advanced Tools", + command=lambda: (AdvancedAnalysis(self)), + fg_color="#000000", text_color="white", border_color="white", + border_width=1, hover_color="#FFBF00", + ) advanced.grid(row=20, column=0, columnspan=2, sticky=(N, S, E, W)) # Create right side framing for functionalities - self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0, bg_color="LightBlue4") + self.right = ctk.CTkFrame( + self.master, fg_color="LightBlue4", corner_radius=0, + bg_color="LightBlue4", + ) self.right.grid(column=1, row=0, sticky=(N, S, E, W)) # Configure columns, plot is weighted more icons are not configured @@ -269,35 +350,41 @@ def __init__(self, master): # Create empty figure self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=True) self.canvas = FigureCanvasTkAgg(self.first_fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) + self.canvas.get_tk_widget().grid( + row=0, column=0, rowspan=6, sticky=(N, S, E, W) + ) # Create logo figure - self.logo_canvas = Canvas(self.right, height=590, width=800, bg="white") + self.logo_canvas = Canvas( + self.right, height=590, width=800, bg="white", + ) self.logo_canvas.grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) - self.logo_canvas.create_image(400, 300, anchor="center", image=self.logo) + self.logo_canvas.create_image( + 400, 300, anchor="center", image=self.logo, + ) # Create info buttons # Settings button gear_path = master_path + "/gui_files/gear.png" - self.gear = ctk.CTkImage(light_image=Image.open(gear_path), - size=(30,30)) - - settings_b = ctk.CTkButton(self.right, text="", image=self.gear, - command=self.open_settings, - width=30, - height=30, - bg_color="LightBlue4", - fg_color="LightBlue4") + self.gear = ctk.CTkImage( + light_image=Image.open(gear_path), size=(30, 30), + ) + + settings_b = ctk.CTkButton( + self.right, text="", image=self.gear, command=self.open_settings, + width=30, height=30, bg_color="LightBlue4", fg_color="LightBlue4", + ) settings_b.grid(column=1, row=0, sticky=W, pady=(0, 20)) # Information Button - info_path = master_path + "/gui_files/Info.png" # Get infor button path - self.info = ctk.CTkImage(light_image=Image.open(info_path), - size=(30,30)) + info_path = master_path + "/gui_files/Info.png" # Get info button path + self.info = ctk.CTkImage( + light_image=Image.open(info_path), size=(30, 30), + ) info_button = ctk.CTkButton( self.right, image=self.info, @@ -307,14 +394,18 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/"))), + (webbrowser.open( + "https://www.giacomovalli.com/openhdemg/gui_intro/" + )) + ), ) info_button.grid(row=1, column=1, sticky=W, pady=(0, 20)) # Button for online tutorials online_path = master_path + "/gui_files/Online.png" - self.online = ctk.CTkImage(light_image=Image.open(online_path), - size=(30,30)) + self.online = ctk.CTkImage( + light_image=Image.open(online_path), size=(30, 30), + ) online_button = ctk.CTkButton( self.right, image=self.online, @@ -324,14 +415,18 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - (webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/"))), + (webbrowser.open( + "https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/" + )) + ), ) online_button.grid(row=2, column=1, sticky=W, pady=(0, 20)) # Button for dev information redirect_path = master_path + "/gui_files/Redirect.png" - self.redirect = ctk.CTkImage(light_image=Image.open(redirect_path), - size=(30,30)) + self.redirect = ctk.CTkImage( + light_image=Image.open(redirect_path), size=(30, 30), + ) redirect_button = ctk.CTkButton( self.right, image=self.redirect, @@ -340,14 +435,19 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers"))), + command=lambda: ( + (webbrowser.open( + "https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers" + )) + ), ) redirect_button.grid(row=3, column=1, sticky=W, pady=(0, 20)) # Button for contact information contact_path = master_path + "/gui_files/Contact.png" - self.contact = ctk.CTkImage(light_image=Image.open(contact_path), - size=(30,30)) + self.contact = ctk.CTkImage( + light_image=Image.open(contact_path), size=(30, 30), + ) contact_button = ctk.CTkButton( self.right, image=self.contact, @@ -356,14 +456,19 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/"))), + command=lambda: ( + (webbrowser.open( + "https://www.giacomovalli.com/openhdemg/contacts/" + )) + ), ) contact_button.grid(row=4, column=1, sticky=W, pady=(0, 20)) # Button for citatoin information cite_path = master_path + "/gui_files/Cite.png" - self.cite = ctk.CTkImage(light_image=Image.open(cite_path), - size=(30,30)) + self.cite = ctk.CTkImage( + light_image=Image.open(cite_path), size=(30, 30), + ) cite_button = ctk.CTkButton( self.right, image=self.cite, @@ -372,14 +477,18 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/"))), + command=lambda: ( + (webbrowser.open( + "https://www.giacomovalli.com/openhdemg/cite-us/" + )) + ), ) cite_button.grid(row=5, column=1, sticky=W, pady=(0, 20)) for child in self.left.winfo_children(): child.grid_configure(padx=5, pady=5) - ## Define functionalities for buttons used in GUI master window + # Define functionalities for buttons used in GUI master window def load_settings(self): """ Instance Method to load the setting file for. @@ -388,6 +497,7 @@ def load_settings(self): The settings specified by the user will then be transferred to the code and used. """ + # If not previously imported, just import it global settings self.settings = importlib.reload(settings) @@ -398,9 +508,10 @@ def open_settings(self): Instance Method to open the setting file for. Executed when the button "Settings" in master GUI window is pressed. - A python file is openend containing a dictionary with relevant variables - that users should be able to customize. + A python file is openend containing a dictionary with relevant + variables that users should be able to customize. """ + # Determine relative filepath file_path = os.path.dirname(os.path.abspath(__file__)) + "/settings.py" @@ -412,25 +523,27 @@ def open_settings(self): else: # Linux or other subprocess.run(['xdg-open', file_path]) - # Unused (yet) def update_gui_variables(self): """ Method to update variables changes in the settings file """ - pass + pass def get_file_input(self): """ - Instance Method to load the file for analysis. The user is asked to select the file. + Instance Method to load the file for analysis. The user is asked to + select the file. Executed when the button "Load File" in master GUI window is pressed. See Also -------- - emg_from_demuse, emg_from_otb, refsig_from_otb and emg_from_json in library. + emg_from_otb, emg_from_demuse, emg_from_delsys, emg_from_customcsv, + refsig_from_otb, refsig_from_delsys, refsig_from_customcsv in library. """ + def load_file(): try: if self.filetype.get() in ["OTB", "DEMUSE", "OPENHDEMG", "CUSTOMCSV", "DELSYS"]: @@ -438,43 +551,54 @@ def load_file(): if self.filetype.get() == "OTB": # Ask user to select the decomposed file file_path = filedialog.askopenfilename( - title="Open OTB file to load", filetypes=[("MATLAB files", "*.mat")] + title="Open decomposed OTB file to load", + filetypes=[("MATLAB files", "*.mat")] ) self.file_path = file_path # Load file self.resdict = openhdemg.emg_from_otb( filepath=self.file_path, ext_factor=int(self.extension_factor.get()), + refsig=self.settings.emg_from_otb__refsig, + extras=self.settings.emg_from_otb__extras, + ignore_negative_ipts=self.settings.emg_from_otb__ignore_negative_ipts, ) # Add filespecs ctk.CTkLabel( - self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) + self.left, + text=str(len(self.resdict["RAW_SIGNAL"].columns)), ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 - ) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 - ) + ctk.CTkLabel( + self.left, text=str(self.resdict["NUMBER_OF_MUS"]), + ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["EMG_LENGTH"]), + ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) elif self.filetype.get() == "DEMUSE": # Ask user to select the file file_path = filedialog.askopenfilename( - title="Open DEMUSE file to load", filetypes=[("MATLAB files", "*.mat")] + title="Open DEMUSE file to load", + filetypes=[("MATLAB files", "*.mat")], ) self.file_path = file_path # load file - self.resdict = openhdemg.emg_from_demuse(filepath=self.file_path) + self.resdict = openhdemg.emg_from_demuse( + filepath=self.file_path, + ignore_negative_ipts=self.settings.emg_from_demuse__ignore_negative_ipts, + ) # Add filespecs ctk.CTkLabel( - self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) + self.left, + text=str(len(self.resdict["RAW_SIGNAL"].columns)), ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 - ) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 - ) + ctk.CTkLabel( + self.left, text=str(self.resdict["NUMBER_OF_MUS"]) + ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["EMG_LENGTH"]) + ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) + elif self.filetype.get() == "DELSYS": # Ask user to select the file file_path = filedialog.askopenfilename( @@ -605,7 +729,7 @@ def load_file(): # child.grid_configure(padx=5, pady=5) # End progress - progress.grid_remove() + progress.grid_remove() # NOTE does it matter the order of grid_remove and stop? They are used with mixed orders! progress.stop() return @@ -653,8 +777,8 @@ def load_file(): progress.start() # Create a thread to run the load_file function - save_thread = threading.Thread(target=load_file) - save_thread.start() + load_thread = threading.Thread(target=load_file) + load_thread.start() def on_filetype_change(self, *args): """ @@ -683,15 +807,9 @@ def on_filetype_change(self, *args): self.otb_combobox = ctk.CTkComboBox( self.left, values=[ - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", + "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", + "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", + "25", "26", "27", "28", "29", "30", "31", "32", ], width=8, variable=self.extension_factor, @@ -709,12 +827,13 @@ def on_filetype_change(self, *args): ) self.csv_entry.grid(column=0, row=2, sticky=(W, E), padx=5) - def save_emgfile(self): """ - Instance method to save the edited emgfile. Results are saved in a .json file. + Instance method to save the edited emgfile. Results are saved in a + .json file. - Executed when the "Save File" button in the master GUI window is pressed. + Executed when the "Save File" button in the master GUI window is + pressed. Raises ------ @@ -725,6 +844,7 @@ def save_emgfile(self): -------- save_json_emgfile in library. """ + def save_file(): try: # Ask user to select the directory and file name @@ -743,19 +863,23 @@ def save_file(): save_emg = self.resdict # Save json file - openhdemg.save_json_emgfile(emgfile=save_emg, filepath=save_filepath) + openhdemg.save_json_emgfile( + emgfile=save_emg, filepath=save_filepath, + compresslevel=self.settings.save_json_emgfile__compresslevel, + ) # End progress progress.stop() progress.grid_remove() - + return - + except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) - - - + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) + # Indicate Progress progress = ctk.CTkProgressBar(self.left, mode="indeterminate") progress.grid(row=4, column=0) @@ -945,6 +1069,7 @@ def display_results(self, input_df): # Show results table.show() + # ----------------------------------------------------------------------------------------------- def run_main(): # Run GUI upon calling diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index c8ff3fe..a0ad3bc 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -29,11 +29,10 @@ # # --------------------------------- openfiles --------------------------------- -# in emg_from_demuse() +# in emg_from_demuse() # DONE and it works emg_from_demuse__ignore_negative_ipts = False -# in emg_from_otb() -emg_from_otb__ext_factor = 8 +# in emg_from_otb() # DONE and it works emg_from_otb__refsig = [True, "fullsampled"] emg_from_otb__extras = None emg_from_otb__ignore_negative_ipts = False @@ -58,18 +57,15 @@ emg_from_customcsv__binary_mus_firing = "BINARY_MUS_FIRING" emg_from_customcsv__accuracy = "ACCURACY" emg_from_customcsv__extras = "EXTRAS" -emg_from_customcsv__fsamp = 2048 emg_from_customcsv__ied = 8 -# TODO in main window and in advanced tools, when selecting OTB, delsys and custom CSV write to check settings file -# because I want to remove the dropdown for the otb extension factor and the dropdown for the discharge rate and only allow -# To specify these from the settings. +# TODO in main window and in advanced tools, when selecting OTB, delsys and custom CSV (both emgifle and refsig) write to check settings file # in refsig_from_customcsv() refsig_from_customcsv__ref_signal = "REF_SIGNAL" refsig_from_customcsv__extras = "EXTRAS" # in save_json_emgfile() -save_json_emgfile__compresslevel = 4 +save_json_emgfile__compresslevel = 4 # DONE and it works # ---------------------------------- analysis --------------------------------- # DONE and it works @@ -79,7 +75,7 @@ # in basic_mus_properties() basic_mus_properties__n_firings_rt_dert = 1 -basic_mus_properties__accuracy = "SIL_PNR" +basic_mus_properties__accuracy = "default" basic_mus_properties__ignore_negative_ipts = False basic_mus_properties__constrain_pulses = [True, 3] From a8e92c67edad5c6d4f1f71cbb0b377561d728447 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 23 Mar 2024 15:43:03 +0100 Subject: [PATCH 40/57] merge commit --- openhdemg/gui/openhdemg_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index fd45636..411f458 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -402,7 +402,7 @@ def open_settings(self): that users should be able to customize. """ # Determine relative filepath - file_path = "openhdemg/gui/settings.py" + file_path = os.path.dirname(os.path.abspath(__file__)) + "/settings.py" # Check for operating system and open in default editor if sys.platform.startswith('darwin'): # macOS From 8422049bc68463c2d4beb4b145a8c5ffe83faee5 Mon Sep 17 00:00:00 2001 From: Paul Ritsche Date: Sat, 23 Mar 2024 17:17:53 +0100 Subject: [PATCH 41/57] commit --- openhdemg/gui/openhdemg_gui.py | 731 ++++++++++++++++++++++----------- 1 file changed, 488 insertions(+), 243 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 9ec80ee..2201196 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -23,104 +23,113 @@ import openhdemg.library as openhdemg import openhdemg.gui.settings as settings -from openhdemg.gui.gui_modules import (MURemovalWindow, EditRefsig, GUIHelpers, - AnalyseForce, MuAnalysis, PlotEmg, AdvancedAnalysis, - show_error_dialog) +from openhdemg.gui.gui_modules import ( + MURemovalWindow, + EditRefsig, + GUIHelpers, + AnalyseForce, + MuAnalysis, + PlotEmg, + AdvancedAnalysis, + show_error_dialog, +) matplotlib.use("TkAgg") -class emgGUI(): + +class emgGUI: """ - This class is used to create a graphical user interface for - the openhdemg library. - - Within this class and corresponding childs, most functionalities - of the ophdemg library are packed in a GUI. Howebver, the library is more - comprehensive and much more adaptable to the users needs. - - Attributes - ---------- - self.canvas : matplotlib.backends.backend_tkagg - Canvas for plotting figures inside the GUI. - self.channels : int or list - The channel (int) or channels (list of int) to plot. - The list can be passed as a manually-written with: "0,1,2,3,4,5...,n", - channels is expected to be with base 0. - self.fig : matplotlib.figure - Figure to be plotted on Canvas. - self.filename : str - String and name of the file to be analysed. - self.filepath : str - String containing the path to EMG file selected for analysis. - self.filetype : str - String containing the filetype of import EMG file. - Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG". - self.left : tk.frame - Left frame inside of master that contains all buttons and filespecs. - self.logo : - String containing the path to image file containing logo of openhdemg. - self.logo_canvas : tk.canvas - Canvas to display logo of Open_HG-EMG when openend. - self.master: tk - TK master window containing all widget children for this GUI. - self.resdict : dict - Dictionary derived from input EMG file for further analysis. - self.right : tk.frame - Left frame inside of master that contains plotting canvas. - self.terminal : ttk.Labelframe - Tkinter labelframe that is used to display the results table in the GUI. - self.info : tk.PhotoImage - Information Icon displayed in GUI. - self.online : tk.Photoimage - Online Icon displayed in GUI. - self.redirect : tk.PhotoImage - Redirection Icon displayed in GUI. - self.contact : tk.PhotoImage - Contact Icon displayed in GUI. - self.cite : tk.PhotoImage - Citation Icon displayed in GUI. - self.otb_combobox : ttk.Combobox - Combobox appearing in main GUI window or advanced - analysis window when OTB files are loaded. Contains - the extension factor for OTB files. - Stringvariable containing the - self.extension_factor : tk.StringVar() - Stringvariable containing the OTB extension factor value. - - Methods - ------- - __init__(master) - Initializes GUI class and main GUI window (master). - get_file_input() - Gets emgfile location and respective file is loaded. - Executed when button "Load File" in master GUI window pressed. - save_emgfile() - Saves the edited emgfile dictionary to a .json file. - Executed when button "Save File" in master GUI window pressed. - reset_analysis() - Resets the whole analysis, restores the original input file and the graph. - Executed when button "Reset analysis" in master GUI window pressed. - in_gui_plotting() - Method used for creating plot inside the GUI (on the GUI canvas). - Executed when button "View MUs" in master GUI window pressed. - mu_analysis() - Opens seperate window to calculated specific motor unit properties. - Executed when button "MU properties" in master GUI window pressed. - display_results() - Method used to display result table containing analysis results. - - Notes - ----- - Please note that altough we created a GUI class, the included methods/ - instances are highly specific. We did not conceptualize the methods/instances - to be used seperately. Similar functionalities are available in the library - and were specifically coded to be used seperately/singularly. - - Most instance methods of this class heavily rely on the functions provided in - the library. In the section "See Also" at each instance method, the reader is - referred to the corresponding function and extensive documentation in the library. + This class is used to create a graphical user interface for + the openhdemg library. + + Within this class and corresponding childs, most functionalities + of the ophdemg library are packed in a GUI. Howebver, the library is more + comprehensive and much more adaptable to the users needs. + + Attributes + ---------- + self.canvas : matplotlib.backends.backend_tkagg + Canvas for plotting figures inside the GUI. + self.channels : int or list + The channel (int) or channels (list of int) to plot. + The list can be passed as a manually-written with: "0,1,2,3,4,5...,n", + channels is expected to be with base 0. + self.fig : matplotlib.figure + Figure to be plotted on Canvas. + self.filename : str + String and name of the file to be analysed. + self.filepath : str + String containing the path to EMG file selected for analysis. + self.filetype : str + String containing the filetype of import EMG file. + Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG". + self.left : tk.frame + Left frame inside of master that contains all buttons and filespecs. + self.logo : + String containing the path to image file containing logo of openhdemg. + self.logo_canvas : tk.canvas + Canvas to display logo of Open_HG-EMG when openend. + self.master: tk + TK master window containing all widget children for this GUI. + self.resdict : dict + Dictionary derived from input EMG file for further analysis. + self.right : tk.frame + Left frame inside of master that contains plotting canvas. + self.terminal : ttk.Labelframe + Tkinter labelframe that is used to display the results table in the GUI. + self.info : tk.PhotoImage + Information Icon displayed in GUI. + self.online : tk.Photoimage + Online Icon displayed in GUI. + self.redirect : tk.PhotoImage + Redirection Icon displayed in GUI. + self.contact : tk.PhotoImage + Contact Icon displayed in GUI. + self.cite : tk.PhotoImage + Citation Icon displayed in GUI. + self.otb_combobox : ttk.Combobox + Combobox appearing in main GUI window or advanced + analysis window when OTB files are loaded. Contains + the extension factor for OTB files. + Stringvariable containing the + self.extension_factor : tk.StringVar() + Stringvariable containing the OTB extension factor value. + + Methods + ------- + __init__(master) + Initializes GUI class and main GUI window (master). + get_file_input() + Gets emgfile location and respective file is loaded. + Executed when button "Load File" in master GUI window pressed. + save_emgfile() + Saves the edited emgfile dictionary to a .json file. + Executed when button "Save File" in master GUI window pressed. + reset_analysis() + Resets the whole analysis, restores the original input file and the graph. + Executed when button "Reset analysis" in master GUI window pressed. + in_gui_plotting() + Method used for creating plot inside the GUI (on the GUI canvas). + Executed when button "View MUs" in master GUI window pressed. + mu_analysis() + Opens seperate window to calculated specific motor unit properties. + Executed when button "MU properties" in master GUI window pressed. + display_results() + Method used to display result table containing analysis results. + + Notes + ----- + Please note that altough we created a GUI class, the included methods/ + instances are highly specific. We did not conceptualize the methods/instances + to be used seperately. Similar functionalities are available in the library + and were specifically coded to be used seperately/singularly. + + Most instance methods of this class heavily rely on the functions provided in + the library. In the section "See Also" at each instance method, the reader is + referred to the corresponding function and extensive documentation in the library. """ + def __init__(self, master): """ Initialization of master GUI window upon calling. @@ -146,7 +155,9 @@ def __init__(self, master): self.master.rowconfigure(0, weight=1) # Create left side framing for functionalities - self.left = ctk.CTkFrame(self.master, fg_color=self.settings.background_color, corner_radius=0) + self.left = ctk.CTkFrame( + self.master, fg_color=self.settings.background_color, corner_radius=0 + ) self.left.grid(column=0, row=0, sticky=(N, S, E, W)) # Configure columns with a loop @@ -159,104 +170,220 @@ def __init__(self, master): # Specify filetype self.filetype = StringVar() - signal_value = ["OPENHDEMG", "DEMUSE", "OTB", "OTB_REFSIG", "DELSYS", "DELSYS_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG"] - signal_entry = ctk.CTkComboBox(self.left, width=150, variable=self.filetype, values=signal_value, state="readonly") + signal_value = [ + "OPENHDEMG", + "DEMUSE", + "OTB", + "OTB_REFSIG", + "DELSYS", + "DELSYS_REFSIG", + "CUSTOMCSV", + "CUSTOMCSV_REFSIG", + ] + signal_entry = ctk.CTkComboBox( + self.left, + width=150, + variable=self.filetype, + values=signal_value, + state="readonly", + ) signal_entry.grid(column=0, row=1, sticky=(N, S, E, W)) self.filetype.set("Type of file") # Trace filetype to apply function when changeing self.filetype.trace_add("write", self.on_filetype_change) # Load file - load = ctk.CTkButton(self.left, text="Load File", command=self.get_file_input, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + load = ctk.CTkButton( + self.left, + text="Load File", + command=self.get_file_input, + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) load.grid(column=0, row=3, sticky=(N, S, E, W)) # File specifications - ctk.CTkLabel(self.left, text="Filespecs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=1, sticky=(W)) - ctk.CTkLabel(self.left, text="N Channels:", font=('Segoe UI',15, 'bold')).grid(column=1, row=2, sticky=(W)) - ctk.CTkLabel(self.left, text="N of MUs:", font=('Segoe UI',15, 'bold')).grid(column=1, row=3, sticky=(W)) - ctk.CTkLabel(self.left, text="File length:", font=('Segoe UI',15, 'bold')).grid(column=1, row=4, sticky=(W)) + ctk.CTkLabel(self.left, text="Filespecs:", font=("Segoe UI", 15, "bold")).grid( + column=1, row=1, sticky=(W) + ) + ctk.CTkLabel(self.left, text="N Channels:", font=("Segoe UI", 15, "bold")).grid( + column=1, row=2, sticky=(W) + ) + ctk.CTkLabel(self.left, text="N of MUs:", font=("Segoe UI", 15, "bold")).grid( + column=1, row=3, sticky=(W) + ) + ctk.CTkLabel( + self.left, text="File length:", font=("Segoe UI", 15, "bold") + ).grid(column=1, row=4, sticky=(W)) separator0 = ttk.Separator(self.left, orient="horizontal") separator0.grid(column=0, columnspan=3, row=5, sticky=(E, W)) - + # Save File - save = ctk.CTkButton(self.left, text="Save File", command=self.save_emgfile, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + save = ctk.CTkButton( + self.left, + text="Save File", + command=self.save_emgfile, + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) save.grid(column=0, row=6, sticky=(N, S, E, W)) separator1 = ttk.Separator(self.left, orient="horizontal") separator1.grid(column=0, columnspan=3, row=7, sticky=(E, W)) # Export to Excel - export = ctk.CTkButton(self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + export = ctk.CTkButton( + self.left, + text="Save Results", + command=lambda: (GUIHelpers(parent=self).export_to_excel()), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) export.grid(column=1, row=6, sticky=(N, S, E, W)) # View Motor Unit Firings - firings = ctk.CTkButton(self.left, text="View MUs", command=lambda: (self.in_gui_plotting(resdict=self.resdict)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + firings = ctk.CTkButton( + self.left, + text="View MUs", + command=lambda: (self.in_gui_plotting(resdict=self.resdict)), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) firings.grid(column=0, row=8, sticky=(N, S, E, W)) # Sort Motor Units - sorting = ctk.CTkButton(self.left, text="Sort MUs", command=lambda: (GUIHelpers(parent=self).sort_mus()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + sorting = ctk.CTkButton( + self.left, + text="Sort MUs", + command=lambda: (GUIHelpers(parent=self).sort_mus()), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) sorting.grid(column=1, row=8, sticky=(N, S, E, W)) separator2 = ttk.Separator(self.left, orient="horizontal") separator2.grid(column=0, columnspan=3, row=9, sticky=(E, W)) # Remove Motor Units - remove_mus = ctk.CTkButton(self.left, text="Remove MUs", command=lambda:(MURemovalWindow(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + remove_mus = ctk.CTkButton( + self.left, + text="Remove MUs", + command=lambda: (MURemovalWindow(parent=self)), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) remove_mus.grid(column=0, row=10, sticky=(N, S, E, W)) separator3 = ttk.Separator(self.left, orient="horizontal") separator3.grid(column=0, columnspan=3, row=11, sticky=(E, W)) # Filter Reference Signal - reference = ctk.CTkButton(self.left, text="RefSig Editing", command=lambda:(EditRefsig(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + reference = ctk.CTkButton( + self.left, + text="RefSig Editing", + command=lambda: (EditRefsig(parent=self)), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) reference.grid(column=0, row=12, sticky=(N, S, E, W)) # Resize File - resize = ctk.CTkButton(self.left, text="Resize File", command=lambda:(GUIHelpers(parent=self).resize_file()), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + resize = ctk.CTkButton( + self.left, + text="Resize File", + command=lambda: (GUIHelpers(parent=self).resize_file()), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) resize.grid(column=1, row=12, sticky=(N, S, E, W)) separator4 = ttk.Separator(self.left, orient="horizontal") separator4.grid(column=0, columnspan=3, row=13, sticky=(E, W)) # Force Analysis - force = ctk.CTkButton(self.left, text="Analyse Force", command=lambda:(AnalyseForce(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + force = ctk.CTkButton( + self.left, + text="Analyse Force", + command=lambda: (AnalyseForce(parent=self)), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) force.grid(column=0, row=14, sticky=(N, S, E, W)) separator5 = ttk.Separator(self.left, orient="horizontal") separator5.grid(column=0, columnspan=3, row=15, sticky=(E, W)) # Motor Unit properties - mus = ctk.CTkButton(self.left, text="MU Properties", command=lambda:(MuAnalysis(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + mus = ctk.CTkButton( + self.left, + text="MU Properties", + command=lambda: (MuAnalysis(parent=self)), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) mus.grid(column=1, row=14, sticky=(N, S, E, W)) separator6 = ttk.Separator(self.left, orient="horizontal") separator6.grid(column=0, columnspan=3, row=17, sticky=(E, W)) # Plot EMG - plots = ctk.CTkButton(self.left, text="Plot EMG", command=lambda:(PlotEmg(parent=self)), - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + plots = ctk.CTkButton( + self.left, + text="Plot EMG", + command=lambda: (PlotEmg(parent=self)), + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) plots.grid(column=0, row=16, sticky=(N, S, E, W)) separator7 = ttk.Separator(self.left, orient="horizontal") separator7.grid(column=0, columnspan=3, row=19, sticky=(E, W)) # Reset Analysis - reset = ctk.CTkButton(self.left, text="Reset Analysis", command=self.reset_analysis, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + reset = ctk.CTkButton( + self.left, + text="Reset Analysis", + command=self.reset_analysis, + fg_color="#E5E4E2", + text_color="black", + border_color="black", + border_width=1, + ) reset.grid(column=1, row=18, sticky=(N, S, E, W)) # Advanced tools - advanced = ctk.CTkButton(self.left, text="Advanced Tools", command=lambda:(AdvancedAnalysis(self)), - fg_color="#000000", text_color="white", border_color="white", border_width=1, - hover_color="#FFBF00") + advanced = ctk.CTkButton( + self.left, + text="Advanced Tools", + command=lambda: (AdvancedAnalysis(self)), + fg_color="#000000", + text_color="white", + border_color="white", + border_width=1, + hover_color="#FFBF00", + ) advanced.grid(row=20, column=0, columnspan=2, sticky=(N, S, E, W)) # Create right side framing for functionalities - self.right = ctk.CTkFrame(self.master, fg_color="LightBlue4", corner_radius=0, bg_color="LightBlue4") + self.right = ctk.CTkFrame( + self.master, fg_color="LightBlue4", corner_radius=0, bg_color="LightBlue4" + ) self.right.grid(column=1, row=0, sticky=(N, S, E, W)) # Configure columns, plot is weighted more icons are not configured @@ -269,7 +396,9 @@ def __init__(self, master): # Create empty figure self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=True) self.canvas = FigureCanvasTkAgg(self.first_fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) + self.canvas.get_tk_widget().grid( + row=0, column=0, rowspan=6, sticky=(N, S, E, W) + ) # Create logo figure self.logo_canvas = Canvas(self.right, height=590, width=800, bg="white") @@ -283,21 +412,23 @@ def __init__(self, master): # Create info buttons # Settings button gear_path = master_path + "/gui_files/gear.png" - self.gear = ctk.CTkImage(light_image=Image.open(gear_path), - size=(30,30)) + self.gear = ctk.CTkImage(light_image=Image.open(gear_path), size=(30, 30)) - settings_b = ctk.CTkButton(self.right, text="", image=self.gear, - command=self.open_settings, - width=30, - height=30, - bg_color="LightBlue4", - fg_color="LightBlue4") - settings_b.grid(column=1, row=0, sticky=W, pady=(0, 20)) + settings_b = ctk.CTkButton( + self.right, + text="", + image=self.gear, + command=self.open_settings, + width=30, + height=30, + bg_color="LightBlue4", + fg_color="LightBlue4", + ) + settings_b.grid(column=1, row=0, sticky=W, pady=(0, 20)) # Information Button info_path = master_path + "/gui_files/Info.png" # Get infor button path - self.info = ctk.CTkImage(light_image=Image.open(info_path), - size=(30,30)) + self.info = ctk.CTkImage(light_image=Image.open(info_path), size=(30, 30)) info_button = ctk.CTkButton( self.right, image=self.info, @@ -307,14 +438,14 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/"))), + (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/")) + ), ) info_button.grid(row=1, column=1, sticky=W, pady=(0, 20)) # Button for online tutorials online_path = master_path + "/gui_files/Online.png" - self.online = ctk.CTkImage(light_image=Image.open(online_path), - size=(30,30)) + self.online = ctk.CTkImage(light_image=Image.open(online_path), size=(30, 30)) online_button = ctk.CTkButton( self.right, image=self.online, @@ -324,14 +455,20 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - (webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/"))), + ( + webbrowser.open( + "https://www.giacomovalli.com/openhdemg/tutorials/setup_working_env/" + ) + ) + ), ) online_button.grid(row=2, column=1, sticky=W, pady=(0, 20)) # Button for dev information redirect_path = master_path + "/gui_files/Redirect.png" - self.redirect = ctk.CTkImage(light_image=Image.open(redirect_path), - size=(30,30)) + self.redirect = ctk.CTkImage( + light_image=Image.open(redirect_path), size=(30, 30) + ) redirect_button = ctk.CTkButton( self.right, image=self.redirect, @@ -340,14 +477,19 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers"))), + command=lambda: ( + ( + webbrowser.open( + "https://www.giacomovalli.com/openhdemg/about-us/#meet-the-developers" + ) + ) + ), ) redirect_button.grid(row=3, column=1, sticky=W, pady=(0, 20)) # Button for contact information contact_path = master_path + "/gui_files/Contact.png" - self.contact = ctk.CTkImage(light_image=Image.open(contact_path), - size=(30,30)) + self.contact = ctk.CTkImage(light_image=Image.open(contact_path), size=(30, 30)) contact_button = ctk.CTkButton( self.right, image=self.contact, @@ -356,14 +498,15 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/"))), + command=lambda: ( + (webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/")) + ), ) contact_button.grid(row=4, column=1, sticky=W, pady=(0, 20)) # Button for citatoin information cite_path = master_path + "/gui_files/Cite.png" - self.cite = ctk.CTkImage(light_image=Image.open(cite_path), - size=(30,30)) + self.cite = ctk.CTkImage(light_image=Image.open(cite_path), size=(30, 30)) cite_button = ctk.CTkButton( self.right, image=self.cite, @@ -372,7 +515,9 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", - command=lambda: ((webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/"))), + command=lambda: ( + (webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/")) + ), ) cite_button.grid(row=5, column=1, sticky=W, pady=(0, 20)) @@ -385,7 +530,7 @@ def load_settings(self): Instance Method to load the setting file for. Executed each time when the GUI or a toplevel is openened. - The settings specified by the user will then be transferred + The settings specified by the user will then be transferred to the code and used. """ # If not previously imported, just import it @@ -405,14 +550,13 @@ def open_settings(self): file_path = os.path.dirname(os.path.abspath(__file__)) + "/settings.py" # Check for operating system and open in default editor - if sys.platform.startswith('darwin'): # macOS - subprocess.run(['open', file_path]) - elif sys.platform.startswith('win32'): # Windows + if sys.platform.startswith("darwin"): # macOS + subprocess.run(["open", file_path]) + elif sys.platform.startswith("win32"): # Windows os.startfile(file_path) else: # Linux or other - subprocess.run(['xdg-open', file_path]) + subprocess.run(["xdg-open", file_path]) - # Unused (yet) def update_gui_variables(self): """ @@ -420,7 +564,6 @@ def update_gui_variables(self): """ pass - def get_file_input(self): """ Instance Method to load the file for analysis. The user is asked to select the file. @@ -431,14 +574,22 @@ def get_file_input(self): -------- emg_from_demuse, emg_from_otb, refsig_from_otb and emg_from_json in library. """ + def load_file(): try: - if self.filetype.get() in ["OTB", "DEMUSE", "OPENHDEMG", "CUSTOMCSV", "DELSYS"]: + if self.filetype.get() in [ + "OTB", + "DEMUSE", + "OPENHDEMG", + "CUSTOMCSV", + "DELSYS", + ]: # Check filetype for processing if self.filetype.get() == "OTB": # Ask user to select the decomposed file file_path = filedialog.askopenfilename( - title="Open OTB file to load", filetypes=[("MATLAB files", "*.mat")] + title="Open OTB file to load", + filetypes=[("MATLAB files", "*.mat")], ) self.file_path = file_path # Load file @@ -450,36 +601,39 @@ def load_file(): ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 - ) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 - ) + ctk.CTkLabel( + self.left, text=str(self.resdict["NUMBER_OF_MUS"]) + ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["EMG_LENGTH"]) + ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) elif self.filetype.get() == "DEMUSE": # Ask user to select the file file_path = filedialog.askopenfilename( - title="Open DEMUSE file to load", filetypes=[("MATLAB files", "*.mat")] + title="Open DEMUSE file to load", + filetypes=[("MATLAB files", "*.mat")], ) self.file_path = file_path # load file - self.resdict = openhdemg.emg_from_demuse(filepath=self.file_path) + self.resdict = openhdemg.emg_from_demuse( + filepath=self.file_path + ) # Add filespecs ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 - ) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 - ) + ctk.CTkLabel( + self.left, text=str(self.resdict["NUMBER_OF_MUS"]) + ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["EMG_LENGTH"]) + ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) elif self.filetype.get() == "DELSYS": # Ask user to select the file file_path = filedialog.askopenfilename( title="Select a DELSYS file with raw EMG to load", - filetypes=[("MATLAB files", "*.mat")] + filetypes=[("MATLAB files", "*.mat")], ) # Ask user to open the Delsys decompostition self.mus_path = filedialog.askdirectory( @@ -488,36 +642,55 @@ def load_file(): self.file_path = file_path # load DELSYS - self.resdict = openhdemg.emg_from_delsys(rawemg_filepath=self.file_path, - mus_directory=self.mus_path) - # Add filespecs - ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 - ) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 + self.resdict = openhdemg.emg_from_delsys( + rawemg_filepath=self.file_path, mus_directory=self.mus_path ) + # Add filespecs + ctk.CTkLabel( + self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) + ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["NUMBER_OF_MUS"]) + ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["EMG_LENGTH"]) + ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) elif self.filetype.get() == "OPENHDEMG": # Ask user to select the file file_path = filedialog.askopenfilename( - title="Open JSON file to load", filetypes=[("JSON files", "*.json")] + title="Open JSON file to load", + filetypes=[("JSON files", "*.json")], ) self.file_path = file_path # load OPENHDEMG (.json) self.resdict = openhdemg.emg_from_json(filepath=self.file_path) # Add filespecs - if self.resdict["SOURCE"] in ["DEMUSE", "OTB", "CUSTOMCSV", "DELSYS"]: - ctk.CTkLabel(self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns))).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) + if self.resdict["SOURCE"] in [ + "DEMUSE", + "OTB", + "CUSTOMCSV", + "DELSYS", + ]: + ctk.CTkLabel( + self.left, + text=str(len(self.resdict["RAW_SIGNAL"].columns)), + ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["NUMBER_OF_MUS"]) + ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel( + self.left, text=str(self.resdict["EMG_LENGTH"]) + ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) else: # Reconfigure labels for refsig ctk.CTkLabel( - self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) + self.left, + text=str(len(self.resdict["REF_SIGNAL"].columns)), ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text="NA").grid( + column=2, row=3, sticky=(W, E), padx=5, pady=5 + ) ctk.CTkLabel(self.left, text=" ").grid( column=2, row=4, sticky=(W, E), padx=5, pady=5 ) @@ -534,9 +707,15 @@ def load_file(): fsamp=float(self.fsamp.get()), ) # Add filespecs - ctk.CTkLabel(self.left, text="Custom CSV").grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text="").grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text="").grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text="Custom CSV").grid( + column=2, row=2, sticky=(W, E), padx=5, pady=5 + ) + ctk.CTkLabel(self.left, text="").grid( + column=2, row=3, sticky=(W, E), padx=5, pady=5 + ) + ctk.CTkLabel(self.left, text="").grid( + column=2, row=4, sticky=(W, E), padx=5, pady=5 + ) # Get filename filename = os.path.splitext(os.path.basename(file_path))[0] @@ -548,9 +727,8 @@ def load_file(): # End progress progress.grid_remove() progress.stop() - - # This sections is used for refsig loading as they required not the + # This sections is used for refsig loading as they required not the # the filespecs to be loaded. else: if self.filetype.get() == "OTB_REFSIG": @@ -560,18 +738,22 @@ def load_file(): ) self.file_path = file_path # load refsig - self.resdict = openhdemg.refsig_from_otb(filepath=self.file_path) + self.resdict = openhdemg.refsig_from_otb( + filepath=self.file_path + ) elif self.filetype.get() == "DELSYS_REFSIG": # Ask user to select the file file_path = filedialog.askopenfilename( title="Select a DELSYS_REFSIG file with raw EMG to load", - filetypes=[("MATLAB files", "*.mat")] + filetypes=[("MATLAB files", "*.mat")], ) self.file_path = file_path # load DELSYS - self.resdict = openhdemg.refsig_from_delsys(filepath=self.file_path) + self.resdict = openhdemg.refsig_from_delsys( + filepath=self.file_path + ) elif self.filetype.get() == "CUSTOMCSV_REFSIG": file_path = filedialog.askopenfilename( @@ -583,7 +765,7 @@ def load_file(): self.resdict = openhdemg.refsig_from_customcsv( filepath=self.file_path, fsamp=float(self.fsamp.get()), - ) + ) # Get filename filename = os.path.splitext(os.path.basename(file_path))[0] @@ -596,11 +778,13 @@ def load_file(): ctk.CTkLabel( self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) + ctk.CTkLabel(self.left, text="NA").grid( + column=2, row=3, sticky=(W, E), padx=5, pady=5 + ) ctk.CTkLabel(self.left, text=" ").grid( column=2, row=4, sticky=(W, E), padx=5, pady=5 ) - + # for child in self.left.winfo_children(): # child.grid_configure(padx=5, pady=5) @@ -611,44 +795,72 @@ def load_file(): return except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("When an OTB file is loaded, make sure to " - + "specify an extension factor (number) first." - + "\nWhen a DELSYS file is loaded, make sure to " - + "specify the correct folder.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "When an OTB file is loaded, make sure to " + + "specify an extension factor (number) first." + + "\nWhen a DELSYS file is loaded, make sure to " + + "specify the correct folder." + ), + ) # End progress progress.stop() progress.grid_remove() except FileNotFoundError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to load correct file" - + "according to your specification.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Make sure to load correct file" + + "according to your specification." + ), + ) # End progress progress.stop() progress.grid_remove() except TypeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to load correct file" - + "according to your specification.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Make sure to load correct file" + + "according to your specification." + ), + ) # End progress progress.stop() progress.grid_remove() except KeyError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to load correct file" - + "according to your specification.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Make sure to load correct file" + + "according to your specification." + ), + ) # End progress progress.stop() progress.grid_remove() - except: + except: # End progress progress.grid_remove() progress.stop() - - + # Indicate Progress - progress = ctk.CTkProgressBar(self.left, mode="indeterminate", fg_color="#585858", - width=100, progress_color="#FFBF00") + progress = ctk.CTkProgressBar( + self.left, + mode="indeterminate", + fg_color="#585858", + width=100, + progress_color="#FFBF00", + ) progress.grid(row=4, column=0) progress.start() @@ -706,7 +918,6 @@ def on_filetype_change(self, *args): ) self.csv_entry.grid(column=0, row=2, sticky=(W, E), padx=5) - def save_emgfile(self): """ Instance method to save the edited emgfile. Results are saved in a .json file. @@ -722,6 +933,7 @@ def save_emgfile(self): -------- save_json_emgfile in library. """ + def save_file(): try: # Ask user to select the directory and file name @@ -745,14 +957,14 @@ def save_file(): # End progress progress.stop() progress.grid_remove() - + return - + except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) - - - + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) + # Indicate Progress progress = ctk.CTkProgressBar(self.left, mode="indeterminate") progress.grid(row=4, column=0) @@ -778,12 +990,21 @@ def reset_analysis(self): When no file was loaded in the GUI. """ # Get user input and check whether analysis wants to be truly resetted - if messagebox.askokcancel(title="Attention", message="Do you really want to reset the analysis?", - icon="warning"): + if messagebox.askokcancel( + title="Attention", + message="Do you really want to reset the analysis?", + icon="warning", + ): # user decided to rest analysis try: # reload original file - if self.filetype.get() in ["OTB", "DEMUSE", "OPENHDEMG", "CUSTOMCSV", "DELSYS"]: + if self.filetype.get() in [ + "OTB", + "DEMUSE", + "OPENHDEMG", + "CUSTOMCSV", + "DELSYS", + ]: if self.filetype.get() == "OTB": self.resdict = openhdemg.emg_from_otb( filepath=self.file_path, @@ -803,15 +1024,16 @@ def reset_analysis(self): filepath=self.file_path ) elif self.filetype.get() == "DELSYS": - self.resdict = openhdemg.emg_from_delsys(rawemg_filepath=self.file_path, - mus_directory=self.mus_path) + self.resdict = openhdemg.emg_from_delsys( + rawemg_filepath=self.file_path, mus_directory=self.mus_path + ) # Update Filespecs ctk.CTkLabel( self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel(self.left, text=str(self.resdict["NUMBER_OF_MUS"])).grid( - column=2, row=3, sticky=(W, E) - ) + ctk.CTkLabel( + self.left, text=str(self.resdict["NUMBER_OF_MUS"]) + ).grid(column=2, row=3, sticky=(W, E)) ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( column=2, row=4, sticky=(W, E) ) @@ -819,15 +1041,21 @@ def reset_analysis(self): else: # load refsig if self.filetype.get() == "OTB_REFSIG": - self.resdict = openhdemg.refsig_from_otb(filepath=self.file_path) + self.resdict = openhdemg.refsig_from_otb( + filepath=self.file_path + ) else: # CUSTOMCSV_REFSIG - self.resdict = openhdemg.refsig_from_customcsv(filepath=self.file_path) + self.resdict = openhdemg.refsig_from_customcsv( + filepath=self.file_path + ) # Recondifgure labels for refsig ctk.CTkLabel( self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) ).grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel(self.left, text="NA").grid(column=2, row=3, sticky=(W, E)) + ctk.CTkLabel(self.left, text="NA").grid( + column=2, row=3, sticky=(W, E) + ) ctk.CTkLabel(self.left, text=" ").grid( column=2, row=4, sticky=(W, E) ) @@ -851,10 +1079,15 @@ def reset_analysis(self): ) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) except FileNotFoundError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) + # ----------------------------------------------------------------------------------------------- # Plotting inside of GUI @@ -876,7 +1109,11 @@ def in_gui_plotting(self, resdict, plot="idr"): plot_refsig, plot_idr in the library. """ try: - if self.resdict["SOURCE"] in ["OTB_REFSIG", "CUSTOMCSV_REFSIG", "DELSYS_REFSIG"]: + if self.resdict["SOURCE"] in [ + "OTB_REFSIG", + "CUSTOMCSV_REFSIG", + "DELSYS_REFSIG", + ]: self.fig = openhdemg.plot_refsig( emgfile=resdict, showimmediately=False, tight_layout=True ) @@ -894,13 +1131,17 @@ def in_gui_plotting(self, resdict, plot="idr"): ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) - self.canvas.get_tk_widget().grid(row=0, column=0, rowspan=6, sticky=(N,S,E,W), padx=5) + self.canvas.get_tk_widget().grid( + row=0, column=0, rowspan=6, sticky=(N, S, E, W), padx=5 + ) toolbar = NavigationToolbar2Tk(self.canvas, self.right, pack_toolbar=False) toolbar.grid(row=5, column=0, sticky=S) plt.close() except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) # ----------------------------------------------------------------------------------------------- # Analysis results display @@ -919,7 +1160,10 @@ def display_results(self, input_df): """ # Create frame for output self.terminal = ttk.LabelFrame( - self.master, text="Result Output", height=100, relief="ridge", + self.master, + text="Result Output", + height=100, + relief="ridge", ) self.terminal.grid( column=0, row=21, columnspan=2, pady=8, padx=10, sticky=(N, S, W, E) @@ -942,6 +1186,7 @@ def display_results(self, input_df): # Show results table.show() + # ----------------------------------------------------------------------------------------------- def run_main(): # Run GUI upon calling From cda63c86f93c062bcd8cfc9308cd13c8edb27bf9 Mon Sep 17 00:00:00 2001 From: Paul Ritsche Date: Sun, 24 Mar 2024 21:22:01 +0100 Subject: [PATCH 42/57] Added: style and fixed icon bugs --- openhdemg/gui/gui_files/openhdemg.json | 359 ++++++++++++++ .../gui/gui_modules/advanced_analyses.py | 381 ++++++++++----- openhdemg/gui/gui_modules/analyse_force.py | 62 +-- openhdemg/gui/gui_modules/edit_mus.py | 98 ++-- openhdemg/gui/gui_modules/edit_refsig.py | 158 ++++-- openhdemg/gui/gui_modules/gui_helpers.py | 53 +- openhdemg/gui/gui_modules/gui_plotting.py | 239 +++++---- openhdemg/gui/gui_modules/mu_properties.py | 135 +++-- openhdemg/gui/openhdemg_gui.py | 462 ++++++++---------- openhdemg/gui/settings.py | 17 +- 10 files changed, 1276 insertions(+), 688 deletions(-) create mode 100644 openhdemg/gui/gui_files/openhdemg.json diff --git a/openhdemg/gui/gui_files/openhdemg.json b/openhdemg/gui/gui_files/openhdemg.json new file mode 100644 index 0000000..18c3da8 --- /dev/null +++ b/openhdemg/gui/gui_files/openhdemg.json @@ -0,0 +1,359 @@ +{ + "CTk": { + "fg_color": [ + "LightBlue4", + "LightBlue4" + ] + }, + "CTkToplevel": { + "fg_color": [ + "LightBlue4", + "LightBlue4" + ] + }, + "CTkFrame": { + "corner_radius": 0, + "border_width": 0, + "fg_color": [ + "LightBlue4", + "LightBlue4" + ], + "top_fg_color": [ + "LightBlue4", + "LightBlue4" + ], + "border_color": [ + "LightBlue4", + "LightBlue4" + ] + }, + "CTkButton": { + "corner_radius": 6, + "border_width": 1, + "fg_color": [ + "#E5E4E2", + "#E5E4E2" + ], + "hover_color": [ + "#325882", + "#325882" + ], + "border_color": [ + "#000000", + "#000000" + ], + "text_color": [ + "#000000", + "#000000" + ], + "text_color_disabled": [ + "gray74", + "gray60" + ] + }, + "CTkLabel": { + "corner_radius": 0, + "fg_color": "transparent", + "text_color": [ + "gray14", + "gray84" + ] + }, + "CTkEntry": { + "corner_radius": 6, + "border_width": 2, + "fg_color": [ + "#F9F9FA", + "#343638" + ], + "border_color": [ + "#979DA2", + "#565B5E" + ], + "text_color": [ + "gray14", + "gray84" + ], + "placeholder_text_color": [ + "gray52", + "gray62" + ] + }, + "CTkCheckBox": { + "corner_radius": 6, + "border_width": 3, + "fg_color": [ + "#3a7ebf", + "#1f538d" + ], + "border_color": [ + "#3E454A", + "#949A9F" + ], + "hover_color": [ + "#325882", + "#14375e" + ], + "checkmark_color": [ + "#DCE4EE", + "gray90" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray60", + "gray45" + ] + }, + "CTkSwitch": { + "corner_radius": 1000, + "border_width": 3, + "button_length": 0, + "fg_color": [ + "#939BA2", + "#4A4D50" + ], + "progress_color": [ + "#3a7ebf", + "#1f538d" + ], + "button_color": [ + "gray36", + "#D5D9DE" + ], + "button_hover_color": [ + "gray20", + "gray100" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray60", + "gray45" + ] + }, + "CTkRadioButton": { + "corner_radius": 1000, + "border_width_checked": 6, + "border_width_unchecked": 3, + "fg_color": [ + "#3a7ebf", + "#1f538d" + ], + "border_color": [ + "#3E454A", + "#949A9F" + ], + "hover_color": [ + "#325882", + "#14375e" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray60", + "gray45" + ] + }, + "CTkProgressBar": { + "corner_radius": 1000, + "border_width": 0, + "fg_color": [ + "#585858", + "#585858" + ], + "progress_color": [ + "#FFBF00", + "#FFBF00" + ], + "border_color": [ + "black", + "black" + ] + }, + "CTkSlider": { + "corner_radius": 1000, + "button_corner_radius": 1000, + "border_width": 6, + "button_length": 0, + "fg_color": [ + "#939BA2", + "#4A4D50" + ], + "progress_color": [ + "gray40", + "#AAB0B5" + ], + "button_color": [ + "#3a7ebf", + "#1f538d" + ], + "button_hover_color": [ + "#325882", + "#14375e" + ] + }, + "CTkOptionMenu": { + "corner_radius": 6, + "fg_color": [ + "#3a7ebf", + "#1f538d" + ], + "button_color": [ + "#325882", + "#14375e" + ], + "button_hover_color": [ + "#234567", + "#1e2c40" + ], + "text_color": [ + "#DCE4EE", + "#DCE4EE" + ], + "text_color_disabled": [ + "gray74", + "gray60" + ] + }, + "CTkComboBox": { + "corner_radius": 6, + "border_width": 2, + "fg_color": [ + "#F9F9FA", + "#343638" + ], + "border_color": [ + "#979DA2", + "#565B5E" + ], + "button_color": [ + "#979DA2", + "#565B5E" + ], + "button_hover_color": [ + "#6E7174", + "#7A848D" + ], + "text_color": [ + "gray14", + "gray84" + ], + "text_color_disabled": [ + "gray50", + "gray45" + ] + }, + "CTkScrollbar": { + "corner_radius": 1000, + "border_spacing": 4, + "fg_color": "transparent", + "button_color": [ + "gray55", + "gray41" + ], + "button_hover_color": [ + "gray40", + "gray53" + ] + }, + "CTkSegmentedButton": { + "corner_radius": 6, + "border_width": 2, + "fg_color": [ + "#979DA2", + "gray29" + ], + "selected_color": [ + "#3a7ebf", + "#1f538d" + ], + "selected_hover_color": [ + "#325882", + "#14375e" + ], + "unselected_color": [ + "#979DA2", + "gray29" + ], + "unselected_hover_color": [ + "gray70", + "gray41" + ], + "text_color": [ + "#DCE4EE", + "#DCE4EE" + ], + "text_color_disabled": [ + "gray74", + "gray60" + ] + }, + "CTkTextbox": { + "corner_radius": 6, + "border_width": 0, + "fg_color": [ + "gray100", + "gray20" + ], + "border_color": [ + "#979DA2", + "#565B5E" + ], + "text_color": [ + "gray14", + "gray84" + ], + "scrollbar_button_color": [ + "gray55", + "gray41" + ], + "scrollbar_button_hover_color": [ + "gray40", + "gray53" + ] + }, + "CTkScrollableFrame": { + "label_fg_color": [ + "gray80", + "gray21" + ] + }, + "DropdownMenu": { + "fg_color": [ + "gray90", + "gray20" + ], + "hover_color": [ + "gray75", + "gray28" + ], + "text_color": [ + "gray14", + "gray84" + ] + }, + "CTkFont": { + "macOS": { + "family": "SF Display", + "size": 18, + "weight": "normal" + }, + "Windows": { + "family": "Roboto", + "size": 18, + "weight": "normal" + }, + "Linux": { + "family": "Roboto", + "size": 18, + "weight": "normal" + } + } +} \ No newline at end of file diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index b42e87b..02c5e80 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -8,19 +8,20 @@ import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog + class AdvancedAnalysis: """ A class to manage advanced analysis tools in an openhdemg GUI application. - This class provides a window for conducting advanced analyses on EMG data. - It allows users to select and utilize different analysis tools and set parameters - for these tools. The class supports functionalities like motor unit tracking, + This class provides a window for conducting advanced analyses on EMG data. + It allows users to select and utilize different analysis tools and set parameters + for these tools. The class supports functionalities like motor unit tracking, duplicate removal, and conduction velocity analysis. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this + The parent widget, typically the main application window, to which this AdvancedAnalysis instance belongs. matrix_rc_adv : StringVar Tkinter StringVar for storing matrix rows and columns information. @@ -53,7 +54,7 @@ class AdvancedAnalysis: Perform the selected advanced analysis based on user-defined parameters. on_matrix_none_adv(self, *args) Callback function for handling changes in matrix code selection. - + Examples -------- >>> main_window = Tk() @@ -62,23 +63,24 @@ class AdvancedAnalysis: Notes ----- - The class is designed to be a part of a larger GUI application and interacts with EMG + The class is designed to be a part of a larger GUI application and interacts with EMG data and analysis tools. It depends on the state and data of the `parent` widget. """ + def __init__(self, parent): """ Initialize a new instance of the AdvancedAnalysis class. - Sets up a window with various controls for performing advanced EMG data analyses. - The method configures and places widgets for tool selection, matrix orientation, - matrix code, and an analysis button in a grid layout. It also initializes + Sets up a window with various controls for performing advanced EMG data analyses. + The method configures and places widgets for tool selection, matrix orientation, + matrix code, and an analysis button in a grid layout. It also initializes several Tkinter StringVars and BooleanVars for user inputs and settings. Parameters ---------- parent : object - The parent widget, usually the main application window, which provides + The parent widget, usually the main application window, which provides necessary context and data for the analysis functions. Raises @@ -100,50 +102,61 @@ def __init__(self, parent): self.emgfile2 = {} self.extension_factor_adv = StringVar() - # Initialize parent and load parent settings + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() # Disable config for DELSYS files try: if self.parent.resdict["SOURCE"] == "DELSYS": - show_error_dialog(parent=self, error=None, solution=str("Advanced Tools for Delsys are only accessible from the library.")) + show_error_dialog( + parent=self, + error=None, + solution=str( + "Advanced Tools for Delsys are only accessible from the library." + ), + ) return except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) return # Open window self.a_window = ctk.CTkToplevel(fg_color="LightBlue4") self.a_window.title("Advanced Tools Window") - + # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" - self.head.iconbitmap(default=iconpath) + self.a_window.iconbitmap(default=iconpath) if platform.startswith("win"): - self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - + self.a_window.after(200, lambda: self.a_window.iconbitmap(iconpath)) + self.a_window.grab_set() # Set resizable window # Configure columns with a loop for col in range(3): - self.head.columnconfigure(col, weight=1) + self.a_window.columnconfigure(col, weight=1) # Configure rows with a loop for row in range(8): - self.head.rowconfigure(row, weight=1) + self.a_window.rowconfigure(row, weight=1) # Add Label ctk.CTkLabel( - self.a_window, text="Select tool and matrix:", font=('Segoe UI',15, 'bold'), - text_color="#000000", anchor="w" + self.a_window, + text="Select tool and matrix:", + font=("Segoe UI", 20, "underline"), + anchor="w", ).grid(row=0, column=0) # Analysis Tool - ctk.CTkLabel(self.a_window, text="Analysis Tool", font=('Segoe UI',15, 'bold')).grid( - row=2, column=0, sticky=(W, E)) + ctk.CTkLabel( + self.a_window, text="Analysis Tool", font=("Segoe UI", 18, "bold") + ).grid(row=2, column=0, sticky=(W, E)) # Add Selection Combobox adv_box_values = ( @@ -153,32 +166,52 @@ def __init__(self, parent): ) self.advanced_method = StringVar() adv_box = ctk.CTkComboBox( - self.a_window, width=170, variable=self.advanced_method, - values=adv_box_values, state="readonly") + self.a_window, + width=200, + variable=self.advanced_method, + values=adv_box_values, + state="readonly", + ) adv_box.grid(row=2, column=1, sticky=(W, E)) self.advanced_method.set("Motor Unit Tracking") # Matrix Orientation - ctk.CTkLabel(self.a_window, text="Matrix Orientation", font=('Segoe UI',15, 'bold')).grid( - row=3, column=0, sticky=(W, E)) + ctk.CTkLabel( + self.a_window, text="Matrix Orientation", font=("Segoe UI", 18, "bold") + ).grid(row=3, column=0, sticky=(W, E)) self.mat_orientation_adv = StringVar() orientation = ctk.CTkComboBox( - self.a_window, width=100, variable=self.mat_orientation_adv, - values=("0", "180"), state="readonly") + self.a_window, + width=100, + variable=self.mat_orientation_adv, + values=("0", "180"), + state="readonly", + ) orientation.grid(row=3, column=1, sticky=(W, E)) self.mat_orientation_adv.set("180") # Matrix code - ctk.CTkLabel(self.a_window, text="Matrix Code", font=('Segoe UI',15, 'bold')).grid( - row=4, column=0, sticky=(W, E)) + ctk.CTkLabel( + self.a_window, text="Matrix Code", font=("Segoe UI", 18, "bold") + ).grid(row=4, column=0, sticky=(W, E)) self.mat_code_adv = StringVar() - matrix_code_vals = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings.delsys_sensor_label, "None") + matrix_code_vals = ( + "GR08MM1305", + "GR04MM1305", + "GR10MM0808", + self.parent.settings.emg_from_delsys__emg_sensor_name, # NOTE: Giacomo check this please + "None", + ) matrix_code = ctk.CTkComboBox( - self.a_window, width=150, variable=self.mat_code_adv, - values=matrix_code_vals, state="readonly") + self.a_window, + width=150, + variable=self.mat_code_adv, + values=matrix_code_vals, + state="readonly", + ) matrix_code.grid(row=4, column=1, sticky=(W, E)) self.mat_code_adv.set("GR08MM1305") - + # Trace variabel for updating window self.mat_code_adv.trace_add("write", self.on_matrix_none_adv) @@ -187,7 +220,12 @@ def __init__(self, parent): self.a_window, text="Advanced Analysis", command=self.advanced_analysis, - fg_color="#000000", text_color="white", border_color="white", border_width=1) + fg_color="#000000", + text_color="white", + border_color="white", + border_width=1, + hover_color="#FFBF00", + ) adv_button.grid(column=0, row=7) # Add padding to widgets @@ -201,34 +239,38 @@ def on_matrix_none_adv(self, *args): """ Handle changes in the matrix code selection in the AdvancedAnalysis GUI. - This callback function is triggered when the `mat_code_adv` variable changes. - It dynamically updates the GUI to add or remove an entry box for specifying - matrix rows and columns. When 'None' is selected for the matrix code, it creates - an entry box for the user to input the rows and columns. Otherwise, it removes + This callback function is triggered when the `mat_code_adv` variable changes. + It dynamically updates the GUI to add or remove an entry box for specifying + matrix rows and columns. When 'None' is selected for the matrix code, it creates + an entry box for the user to input the rows and columns. Otherwise, it removes this entry box. Parameters ---------- *args : tuple - The arguments passed to the callback function. Not used in the function but + The arguments passed to the callback function. Not used in the function but required for compatibility with Tkinter's trace mechanism. Notes ----- - The method is part of the AdvancedAnalysis class and interacts with the GUI elements - specific to advanced analysis options. It ensures that the GUI is responsive to user + The method is part of the AdvancedAnalysis class and interacts with the GUI elements + specific to advanced analysis options. It ensures that the GUI is responsive to user selections and updates the interface accordingly. """ # Necessary to distinguish between None and other if self.mat_code_adv.get() == "None": - + # Set label for matrix rows and columns - mat_label_adv = ctk.CTkLabel(self.a_window, text="Rows, Columns:", font=('Segoe UI',15, 'bold')) - mat_label_adv.grid(row=5, column=1, sticky = W) + mat_label_adv = ctk.CTkLabel( + self.a_window, text="Rows, Columns:", font=("Segoe UI", 18, "bold") + ) + mat_label_adv.grid(row=5, column=1, sticky=W) - row_cols_entry_adv = ctk.CTkEntry(self.a_window, width=8, textvariable= self.matrix_rc_adv) - row_cols_entry_adv.grid(row=6, column=1, sticky = W, padx=5, pady=2) + row_cols_entry_adv = ctk.CTkEntry( + self.a_window, width=30, textvariable=self.matrix_rc_adv + ) + row_cols_entry_adv.grid(row=6, column=1, sticky=W, padx=5, pady=2) self.matrix_rc_adv.set("13,5") else: @@ -243,41 +285,41 @@ def advanced_analysis(self): """ Open a top-level window based on the selected advanced analysis method. - This method is responsible for generating different GUI windows depending on the - advanced analysis option chosen by the user. It dynamically creates GUI elements - like dropdowns, buttons, and checkboxes specific to the selected analysis tool, + This method is responsible for generating different GUI windows depending on the + advanced analysis option chosen by the user. It dynamically creates GUI elements + like dropdowns, buttons, and checkboxes specific to the selected analysis tool, such as 'Motor Unit Tracking', 'Conduction Velocity', or 'Duplicate Removal'. Raises ------ AttributeError - If a required file is not loaded prior to performing the analysis or if invalid + If a required file is not loaded prior to performing the analysis or if invalid rows and columns arguments are entered for the 'Conduction Velocity' analysis. Notes ----- - The method is part of the AdvancedAnalysis class and interacts with other GUI elements - and functionalities of the application. It ensures that the GUI adapts to the user's + The method is part of the AdvancedAnalysis class and interacts with other GUI elements + and functionalities of the application. It ensures that the GUI adapts to the user's choice of analysis, providing relevant options and settings for each analysis type. """ if self.advanced_method.get() == "Motor Unit Tracking": head_title = "MUs Tracking Window" - elif self.advanced_method.get() == "Conduction Velocity": + elif self.advanced_method.get() == "Conduction Velocity": head_title = "Conduction Velocity Window" else: head_title = "Duplicate Removal Window" self.head = ctk.CTkToplevel(fg_color="LightBlue4") self.head.title(head_title) - - # Set window icon + + # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - - self.a_window.grab_set() + + self.head.grab_set() # Set resizable window # Configure columns with a loop @@ -288,28 +330,39 @@ def advanced_analysis(self): for row in range(17): self.head.rowconfigure(row, weight=1) - # Specify Signal signal_value = ("OTB", "DEMUSE", "OPENHDEMG", "CUSTOMCSV") signal_entry = ctk.CTkComboBox( - self.head, width=150, variable=self.filetype_adv, - values=signal_value, state="readonly") + self.head, + width=150, + variable=self.filetype_adv, + values=signal_value, + state="readonly", + ) signal_entry.grid(column=0, row=1, sticky=(W, E)) self.filetype_adv.set("Type of file") self.filetype_adv.trace_add("write", self.on_filetype_change_adv) # Load file - load1 = ctk.CTkButton(self.head, text="Load File 1", command=self.open_emgfile1, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + load1 = ctk.CTkButton( + self.head, + text="Load File 1", + command=self.open_emgfile1, + ) load1.grid(column=0, row=2, sticky=(W, E)) # Load file - load2 = ctk.CTkButton(self.head, text="Load File 2", command=self.open_emgfile2, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + load2 = ctk.CTkButton( + self.head, + text="Load File 2", + command=self.open_emgfile2, + ) load2.grid(column=0, row=3, sticky=(W, E)) # Threshold label - threshold_label = ctk.CTkLabel(self.head, text="Threshold:", font=('Segoe UI',15, 'bold')) + threshold_label = ctk.CTkLabel( + self.head, text="Threshold:", font=("Segoe UI", 15, "bold") + ) threshold_label.grid(column=0, row=9) # Combobox for threshold @@ -324,7 +377,9 @@ def advanced_analysis(self): self.threshold_adv.set("0.8") # Time Label - time_window_label = ctk.CTkLabel(self.head, text="Time window:", font=('Segoe UI',15, 'bold')) + time_window_label = ctk.CTkLabel( + self.head, text="Time window:", font=("Segoe UI", 18, "bold") + ) time_window_label.grid(column=0, row=10) # Time Combobox @@ -339,43 +394,62 @@ def advanced_analysis(self): self.time_window.set("25") # Exclude below threshold - exclude_label = ctk.CTkLabel(self.head, text="Exclude below threshold", font=('Segoe UI',15, 'bold')) + exclude_label = ctk.CTkLabel( + self.head, text="Exclude below threshold", font=("Segoe UI", 18, "bold") + ) exclude_label.grid(column=0, row=11) # Add exclude checkbox exclude_checkbox = ctk.CTkCheckBox( - self.head, variable=self.exclude_thres, bg_color="LightBlue4", - onvalue="True", offvalue="False", text="" + self.head, + variable=self.exclude_thres, + bg_color="LightBlue4", + onvalue="True", + offvalue="False", + text="", ) exclude_checkbox.grid(column=1, row=11) self.exclude_thres.set(True) # Filter - filter_label = ctk.CTkLabel(self.head, text="Filter", font=('Segoe UI',15, 'bold')) + filter_label = ctk.CTkLabel( + self.head, text="Filter", font=("Segoe UI", 18, "bold") + ) filter_label.grid(column=0, row=12) # Add filter checkbox filter_checkbox = ctk.CTkCheckBox( - self.head, variable=self.filter_adv, bg_color="LightBlue4", - onvalue="True", offvalue="False", text="" + self.head, + variable=self.filter_adv, + bg_color="LightBlue4", + onvalue="True", + offvalue="False", + text="", ) filter_checkbox.grid(column=1, row=12) self.filter_adv.set(True) # Exclude below threshold - show_label = ctk.CTkLabel(self.head, text="Show", font=('Segoe UI',15, 'bold')) + show_label = ctk.CTkLabel(self.head, text="Show", font=("Segoe UI", 18, "bold")) show_label.grid(column=0, row=13) # Add exclude checkbox show_checkbox = ctk.CTkCheckBox( - self.head, variable=self.show_adv, bg_color="LightBlue4", - onvalue="True", offvalue="False", text="" + self.head, + variable=self.show_adv, + bg_color="LightBlue4", + onvalue="True", + offvalue="False", + text="", ) show_checkbox.grid(column=1, row=13) # Add button to execute MU tracking - track_button = ctk.CTkButton(self.head, text="Track", command=self.track_mus, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + track_button = ctk.CTkButton( + self.head, + text="Track", + command=self.track_mus, + ) track_button.grid(column=0, row=15, columnspan=2, sticky=(W, E)) # Add padding @@ -387,7 +461,9 @@ def advanced_analysis(self): if self.advanced_method.get() == "Duplicate Removal": # Add Which label - ctk.CTkLabel(self.head, text="Which", font=('Segoe UI',15, 'bold')).grid(column=0, row=14) + ctk.CTkLabel(self.head, text="Which", font=("Segoe UI", 18, "bold")).grid( + column=0, row=14 + ) # Combobox for Which option which_combobox = ctk.CTkComboBox( @@ -423,10 +499,16 @@ def advanced_analysis(self): code=self.mat_code_adv.get(), orientation=int(self.mat_orientation_adv.get()), n_rows=list_rcs[0], - n_cols=list_rcs[1] + n_cols=list_rcs[1], ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Number of specified rows and columns must match number of channels.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Number of specified rows and columns must match number of channels." + ), + ) return # # DELSYS conduction velocity not available # elif self.mat_code_adv.get() == "Trigno Galileo Sensor": @@ -450,16 +532,28 @@ def advanced_analysis(self): ) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Please make sure to load a file prior to Conduction velocity calculation.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Please make sure to load a file prior to Conduction velocity calculation." + ), + ) self.head.destroy() except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Please make sure to enter valid Rows, Columns arguments." - + "Arguments must be non-negative and seperated by `,`.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Please make sure to enter valid Rows, Columns arguments." + + " Arguments must be non-negative and seperated by `,`." + ), + ) self.head.destroy() # Destroy first window to avoid too many pop-ups - self.a_window.destroy() + self.a_window.withdraw() # TODO check for reference as destroy will evoke tcl error upon continuing, whereas withdraw creates issue when closing. ### Define function for advanced analysis tools def open_emgfile1(self): @@ -474,7 +568,7 @@ def open_emgfile1(self): -------- open_emgfile1(), openhdemg.askopenfile() """ - try: + try: # Open OTB file if self.filetype_adv.get() == "OTB": self.emgfile1 = openhdemg.askopenfile( @@ -488,10 +582,18 @@ def open_emgfile1(self): ) # Add filename to GUI - ctk.CTkLabel(self.head, text="File 1 loaded", font=('Segoe UI',15, 'bold')).grid(column=1, row=2) + ctk.CTkLabel( + self.head, text="File 1 loaded", font=("Segoe UI", 15, "bold") + ).grid(column=1, row=2) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to specify a valid filetype or extension factor.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Make sure to specify a valid filetype or extension factor." + ), + ) def open_emgfile2(self): """ @@ -519,30 +621,38 @@ def open_emgfile2(self): ) # Add filename to GUI - ctk.CTkLabel(self.head, text="File 2 loaded", font=('Segoe UI',15, 'bold')).grid(column=1, row=3) + ctk.CTkLabel( + self.head, text="File 2 loaded", font=("Segoe UI", 15, "bold") + ).grid(column=1, row=3) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to specify a valid filetype or extension factor.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Make sure to specify a valid filetype or extension factor." + ), + ) def on_filetype_change_adv(self, *args): """ Handle changes in the file type selection in the AdvancedAnalysis GUI. - This callback function is triggered when the `filetype_adv` variable changes. - Specifically, it updates the GUI to add or remove a combobox for specifying - the OTB (OpenToBe) extension factors when 'OTB' is selected as the file type. + This callback function is triggered when the `filetype_adv` variable changes. + Specifically, it updates the GUI to add or remove a combobox for specifying + the OTB (OpenToBe) extension factors when 'OTB' is selected as the file type. For other file types, this additional combobox is removed. Parameters ---------- *args : tuple - The arguments passed to the callback function. Not used in the function but + The arguments passed to the callback function. Not used in the function but required for compatibility with Tkinter's trace mechanism. Notes ----- - The method is part of the AdvancedAnalysis class and interacts with the GUI elements - specific to file type selection. It ensures that the GUI is responsive to user + The method is part of the AdvancedAnalysis class and interacts with the GUI elements + specific to file type selection. It ensures that the GUI is responsive to user selections and updates the interface accordingly. """ # Add a combobox containing the OTB extension factors @@ -551,7 +661,7 @@ def on_filetype_change_adv(self, *args): self.otb_combobox = ctk.CTkComboBox( self.head, values=["8", "9", "10", "11", "12", "13", "14", "15", "16"], - width=8, + width=30, variable=self.extension_factor_adv, state="readonly", ) @@ -593,7 +703,11 @@ def track_mus(self): n_rows = None n_cols = None except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Verify that Rows and Columns are separated by ','")) + show_error_dialog( + parent=self, + error=e, + solution=str("Verify that Rows and Columns are separated by ','"), + ) try: # Track motor units @@ -619,7 +733,7 @@ def track_mus(self): column=2, row=0, columnspan=2, - rowspan=12, + rowspan=14, pady=8, padx=10, sticky=(N, S, W, E), @@ -630,18 +744,28 @@ def track_mus(self): track_table.show() except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to load all required EMG files prior to tracking.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Make sure to load all required EMG files prior to tracking." + ), + ) except ValueError as e: - show_error_dialog(parent=self, error=e, - solution=str("Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Extension Factor (in case of OTB file)" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Threshold" - + "\n - Rows, Columns")) - + show_error_dialog( + parent=self, + error=e, + solution=str( + "Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Extension Factor (in case of OTB file)" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Threshold" + + "\n - Rows, Columns" + ), + ) def remove_duplicates_between(self): """ @@ -673,7 +797,11 @@ def remove_duplicates_between(self): n_rows = None n_cols = None except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Verify that Rows and Columns are separated by ','")) + show_error_dialog( + parent=self, + error=e, + solution=str("Verify that Rows and Columns are separated by ','"), + ) try: # Remove motor unit duplicates @@ -696,15 +824,26 @@ def remove_duplicates_between(self): openhdemg.asksavefile(emg_file2) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to load all required EMG files prior to tracking.")) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Make sure to load all required EMG files prior to tracking." + ), + ) except ValueError as e: - show_error_dialog(parent=self, error=e, - solution=str("Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Extension Factor (in case of OTB file)" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Threshold" - + "\n - Which" - + "\n - Rows, Columns",)) + show_error_dialog( + parent=self, + error=e, + solution=str( + "Enter valid input parameters." + + "\nPotenital error sources:" + + "\n - Extension Factor (in case of OTB file)" + + "\n - Matrix Code" + + "\n - Matrix Orientation" + + "\n - Threshold" + + "\n - Which" + + "\n - Rows, Columns", + ), + ) diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index 8e2b478..3961092 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -13,14 +13,14 @@ class AnalyseForce: """ A class for conducting force analysis in an openhdemg GUI application. - This class provides a window for analyzing force signals. It includes functionalities - for calculating Maximum Voluntary Contraction (MVC) and Rate of Force Development (RFD). + This class provides a window for analyzing force signals. It includes functionalities + for calculating Maximum Voluntary Contraction (MVC) and Rate of Force Development (RFD). The class is activated through the "Analyse Force" button in the main GUI window. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this AnalyseForce + The parent widget, typically the main application window, to which this AnalyseForce instance belongs. head : CTkToplevel The top-level widget for the Force Analysis window. @@ -35,7 +35,7 @@ class AnalyseForce: Calculate and display the Maximum Voluntary Contraction (MVC). get_rfd(self) Calculate and display the Rate of Force Development (RFD) over specified milliseconds. - + Examples -------- >>> main_window = Tk() @@ -44,7 +44,7 @@ class AnalyseForce: Notes ----- - The class is designed to be a part of a larger GUI application and interacts with force + The class is designed to be a part of a larger GUI application and interacts with force signal data accessible via the `parent` widget. """ @@ -52,42 +52,42 @@ def __init__(self, parent): """ Initialize a new instance of the AnalyseForce class. - This method sets up the GUI components for the Force Analysis Window. It includes buttons - for calculating MVC and RFD, and an entry field for specifying RFD milliseconds. The method - configures and places various widgets such as labels, buttons, and entry fields in a grid + This method sets up the GUI components for the Force Analysis Window. It includes buttons + for calculating MVC and RFD, and an entry field for specifying RFD milliseconds. The method + configures and places various widgets such as labels, buttons, and entry fields in a grid layout for user interaction. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this AnalyseForce + The parent widget, typically the main application window, to which this AnalyseForce instance belongs. The parent is used for accessing shared resources and data. Raises ------ AttributeError - If certain widgets or properties are not properly instantiated due to missing + If certain widgets or properties are not properly instantiated due to missing parent configurations or resources. """ - # Initialize parent and load parent settings - + # Initialize parent and load parent settings + self.parent = parent self.parent.load_settings() - + # Create new window - self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head = ctk.CTkToplevel() self.head.title("Force Analysis Window") - + # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" - self.head.iconbitmap(default=iconpath) + self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - + self.head.grab_set() - + # Set resizable window # Configure columns with a loop for col in range(3): @@ -96,22 +96,20 @@ def __init__(self, parent): # Configure rows with a loop for row in range(10): self.head.rowconfigure(row, weight=1) - + # Get MVC - get_mvf = ctk.CTkButton(self.head, text="Get MVC", command=self.get_mvc, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + get_mvf = ctk.CTkButton(self.head, text="Get MVC", command=self.get_mvc) get_mvf.grid(column=0, row=1, sticky=(W, E), padx=5, pady=5) # Get RFD separator1 = ttk.Separator(self.head, orient="horizontal") separator1.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.head, text="RFD miliseconds", font=('Segoe UI',15, 'bold')).grid( - column=1, row=3, sticky=(W, E), padx=5, pady=5 - ) + ctk.CTkLabel( + self.head, text="RFD miliseconds", font=("Segoe UI", 18, "bold") + ).grid(column=1, row=3, sticky=(W, E), padx=5, pady=5) - get_rfd = ctk.CTkButton(self.head, text="Get RFD", command=self.get_rfd, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + get_rfd = ctk.CTkButton(self.head, text="Get RFD", command=self.get_rfd) get_rfd.grid(column=0, row=4, sticky=(W, E), padx=5, pady=5) self.rfdms = StringVar() @@ -147,7 +145,9 @@ def get_mvc(self): self.parent.display_results(input_df=self.parent.mvc_df) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) def get_rfd(self): """ @@ -173,9 +173,13 @@ def get_rfd(self): # Use comprehension to iterate through ms_list = [int(i) for i in ms_list] # Calculate rfd - self.parent.rfd = openhdemg.compute_rfd(emgfile=self.parent.resdict, ms=ms_list) + self.parent.rfd = openhdemg.compute_rfd( + emgfile=self.parent.resdict, ms=ms_list + ) # Display results self.parent.display_results(input_df=self.parent.rfd) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index 0c5ce46..f5511d8 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -12,15 +12,15 @@ class MURemovalWindow: """ A class for managing the removal of motor units (MUs) in a GUI application. - This class creates a window that offers options to select and remove specific MUs. - It is activated from the main GUI window and is intended to provide functionalities - for manipulating motor unit data. The class raises an AttributeError if it is instantiated + This class creates a window that offers options to select and remove specific MUs. + It is activated from the main GUI window and is intended to provide functionalities + for manipulating motor unit data. The class raises an AttributeError if it is instantiated without a loaded file for analysis. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this MURemovalWindow + The parent widget, typically the main application window, to which this MURemovalWindow instance belongs. resdict : dict A dictionary containing relevant data and settings, including the number of MUs. @@ -37,7 +37,7 @@ class MURemovalWindow: Remove the selected motor unit from the analysis. remove_empty(self) Remove all motor units that are empty or have no data. - + Examples -------- >>> main_window = Tk() @@ -52,48 +52,49 @@ class MURemovalWindow: Notes ----- - The class is designed to interact with the data structure provided by the `resdict` + The class is designed to interact with the data structure provided by the `resdict` attribute, which is expected to contain specific keys and values relevant to the MU analysis. """ + def __init__(self, parent): """ Initialize a new instance of the MURemovalWindow class. - This method sets up the GUI components for the Motor Unit Removal Window. It includes - a dropdown menu to select a motor unit (MU) for removal and buttons to remove either - the selected MU or all empty MUs. The method configures and places various widgets such + This method sets up the GUI components for the Motor Unit Removal Window. It includes + a dropdown menu to select a motor unit (MU) for removal and buttons to remove either + the selected MU or all empty MUs. The method configures and places various widgets such as labels, comboboxes, and buttons in a grid layout for user interaction. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this MURemovalWindow + The parent widget, typically the main application window, to which this MURemovalWindow instance belongs. The parent is used for accessing shared resources and data. Raises ------ AttributeError - If certain widgets or properties are not properly instantiated due to missing + If certain widgets or properties are not properly instantiated due to missing parent configurations or resources. """ try: - # Initialize parent and load parent settings + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() # Create new window - self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head = ctk.CTkToplevel() # Set the background color of the top-level window self.head.title("Motor Unit Removal Window") - + # Set the icon for the window head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - + self.head.grab_set() # Set resizable window @@ -104,33 +105,47 @@ def __init__(self, parent): # Configure rows with a loop for row in range(10): self.head.rowconfigure(row, weight=1) - + # Select Motor Unit - ctk.CTkLabel(self.head, text="Select MU:", font=('Segoe UI',15, 'bold')).grid( - column=1, row=0, padx=5, pady=5, sticky=W - ) + ctk.CTkLabel( + self.head, text="Select MU:", font=("Segoe UI", 15, "bold") + ).grid(column=1, row=0, padx=5, pady=5, sticky=W) self.mu_to_remove = StringVar() removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] removed_mu_value = list(map(str, removed_mu_value)) removed_mu = ctk.CTkComboBox( - self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value, state="readonly" + self.head, + width=10, + variable=self.mu_to_remove, + values=removed_mu_value, + state="readonly", + ) + removed_mu.grid( + column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5 ) - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) # Remove Motor unit - remove = ctk.CTkButton(self.head, text="Remove MU", command=self.remove, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + remove = ctk.CTkButton( + self.head, + text="Remove MU", + command=self.remove, + ) remove.grid(column=1, row=2, sticky=(W, E), padx=5, pady=5) # Remove empty MUs - remove_empty = ctk.CTkButton(self.head, text="Remove empty MUs", command=self.remove_empty, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + remove_empty = ctk.CTkButton( + self.head, + text="Remove empty MUs", + command=self.remove_empty, + ) remove_empty.grid(column=2, row=2, padx=5, pady=5) - + except AttributeError as e: self.head.destroy() - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) def remove(self): """ @@ -149,8 +164,9 @@ def remove(self): emgfile=self.parent.resdict, munumber=int(self.mu_to_remove.get()) ) # Upate MU number - ctk.CTkLabel(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"]), font=('Segoe UI',15, 'bold')).grid( - column=2, row=3, sticky=(W, E) + self.parent.n_of_mus.configure( + text="N of MUs: " + str(self.parent.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), ) # Update selection field @@ -161,15 +177,19 @@ def remove(self): self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value ) removed_mu.configure(state="readonly") - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + removed_mu.grid( + column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5 + ) # Update plot if hasattr(self.parent, "fig"): self.parent.in_gui_plotting(resdict=self.parent.resdict) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) - + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) + def remove_empty(self): """ Instance method that removes all empty MUs. @@ -186,10 +206,10 @@ def remove_empty(self): self.parent.resdict = openhdemg.delete_empty_mus(self.parent.resdict) # Upate MU number - ctk.Label(self.parent.left, text=str(self.parent.resdict["NUMBER_OF_MUS"]), font=('Segoe UI',15, 'bold')).grid( - column=2, row=3, sticky=(W, E) + self.parent.n_of_mus.configure( + text="N of MUs: " + str(self.parent.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), ) - # Update selection field self.mu_to_remove = StringVar() removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] @@ -198,11 +218,15 @@ def remove_empty(self): self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value ) removed_mu.configure(state="readonly") - removed_mu.grid(column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5) + removed_mu.grid( + column=1, row=1, columnspan=2, sticky=(W, E), padx=5, pady=5 + ) # Update plot if hasattr(self.parent, "fig"): self.parent.in_gui_plotting(resdict=self.parent.resdict) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index 22cb520..c1f9b49 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -12,15 +12,15 @@ class EditRefsig: """ A class to manage editing of the reference signal in a GUI application. - This class creates a window that offers various options for editing the reference signal. - It includes functionalities for filtering the signal, removing offset, converting the signal, - and transforming it to a percentage value. The class is instantiated when the "RefSig Editing" + This class creates a window that offers various options for editing the reference signal. + It includes functionalities for filtering the signal, removing offset, converting the signal, + and transforming it to a percentage value. The class is instantiated when the "RefSig Editing" button in the master GUI window is pressed. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this EditRefsig + The parent widget, typically the main application window, to which this EditRefsig instance belongs. head : CTkToplevel The top-level widget for the Reference Signal Editing window. @@ -51,7 +51,7 @@ class EditRefsig: Convert the reference signal using the specified operation (Multiply/Divide) and factor. to_percent(self) Convert the reference signal to a percentage value based on the specified MVC value. - + Examples -------- >>> main_window = Tk() @@ -60,48 +60,49 @@ class EditRefsig: Notes ----- - This class relies on the `ctk` and `ttk` modules from the `tkinter` library. The class is designed - to be instantiated from within a larger GUI application and operates on the reference signal data + This class relies on the `ctk` and `ttk` modules from the `tkinter` library. The class is designed + to be instantiated from within a larger GUI application and operates on the reference signal data that is accessible via the `parent` widget. """ + def __init__(self, parent): """ Initialize a new instance of the EditRefsig class. - This method sets up the GUI components for the Reference Signal Editing Window. It includes - controls for filtering the reference signal, removing its offset, converting it, and - transforming it to a percentage value. The method configures and places various widgets + This method sets up the GUI components for the Reference Signal Editing Window. It includes + controls for filtering the reference signal, removing its offset, converting it, and + transforming it to a percentage value. The method configures and places various widgets such as labels, entries, buttons, and combo boxes in a grid layout for user interaction. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this EditRefsig + The parent widget, typically the main application window, to which this EditRefsig instance belongs. The parent is used for accessing shared resources and data. Raises ------ AttributeError - If certain widgets or properties are not properly instantiated due to missing + If certain widgets or properties are not properly instantiated due to missing parent configurations or resources. """ - # Initialize parent and load parent settings + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() # Create new window - self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head = ctk.CTkToplevel() self.head.title("Reference Signal Editing Window") - + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - + self.head.grab_set() - + # Set resizable window # Configure columns with a loop for col in range(3): @@ -110,14 +111,21 @@ def __init__(self, parent): # Configure rows with a loop for row in range(10): self.head.rowconfigure(row, weight=1) - + # Filter Refsig # Define Labels - ctk.CTkLabel(self.head, text="Filter Order", font=('Segoe UI',15, 'bold')).grid(column=1, row=0, sticky=(W, E)) - ctk.CTkLabel(self.head, text="Cutoff Freq", font=('Segoe UI',15, 'bold')).grid(column=2, row=0, sticky=(W, E)) + ctk.CTkLabel( + self.head, text="Filter Order", font=("Segoe UI", 18, "bold") + ).grid(column=1, row=0, sticky=(W, E)) + ctk.CTkLabel(self.head, text="Cutoff Freq", font=("Segoe UI", 18, "bold")).grid( + column=2, row=0, sticky=(W, E) + ) # Fiter button - basic = ctk.CTkButton(self.head, text="Filter Refsig", command=self.filter_refsig, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + basic = ctk.CTkButton( + self.head, + text="Filter Refsig", + command=self.filter_refsig, + ) basic.grid(column=0, row=1, sticky=W) self.filter_order = StringVar() order = ctk.CTkEntry(self.head, width=100, textvariable=self.filter_order) @@ -133,14 +141,19 @@ def __init__(self, parent): separator2 = ttk.Separator(self.head, orient="horizontal") separator2.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.head, text="Offset Value", font=('Segoe UI',15, 'bold')).grid(column=1, row=3, sticky=(W, E)) - ctk.CTkLabel(self.head, text="Automatic Offset", font=('Segoe UI',15, 'bold')).grid( - column=2, row=3, sticky=(W, E) - ) + ctk.CTkLabel( + self.head, text="Offset Value", font=("Segoe UI", 18, "bold") + ).grid(column=1, row=3, sticky=(W, E)) + ctk.CTkLabel( + self.head, text="Automatic Offset", font=("Segoe UI", 18, "bold") + ).grid(column=2, row=3, sticky=(W, E)) # Offset removal button - basic2 = ctk.CTkButton(self.head, text="Remove Offset", command=self.remove_offset, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + basic2 = ctk.CTkButton( + self.head, + text="Remove Offset", + command=self.remove_offset, + ) basic2.grid(column=0, row=4, sticky=W) self.offsetval = StringVar() @@ -157,13 +170,17 @@ def __init__(self, parent): separator3.grid(column=0, columnspan=3, row=5, sticky=(W, E), padx=5, pady=5) # Convert Reference signal - ctk.CTkLabel(self.head, text="Operator", font=('Segoe UI',15, 'bold')).grid(column=1, row=6, sticky=(W, E)) - ctk.CTkLabel(self.head, text="Factor", font=('Segoe UI',15, 'bold')).grid( + ctk.CTkLabel(self.head, text="Operator", font=("Segoe UI", 18, "bold")).grid( + column=1, row=6, sticky=(W, E) + ) + ctk.CTkLabel(self.head, text="Factor", font=("Segoe UI", 18, "bold")).grid( column=2, row=6, sticky=(W, E) ) self.convert = StringVar() - convert = ctk.CTkComboBox(self.head, width=100, variable=self.convert, values=("Multiply", "Divide")) + convert = ctk.CTkComboBox( + self.head, width=100, variable=self.convert, values=("Multiply", "Divide") + ) convert.configure(state="readonly") convert.grid(column=1, row=7) self.convert.set("Multiply") @@ -173,30 +190,37 @@ def __init__(self, parent): factor.grid(column=2, row=7) self.convert_factor.set(2.5) - convert_button = ctk.CTkButton(self.head, text="Convert", command=self.convert_refsig, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + convert_button = ctk.CTkButton( + self.head, + text="Convert", + command=self.convert_refsig, + ) convert_button.grid(column=0, row=7, sticky=W) separator3 = ttk.Separator(self.head, orient="horizontal") separator3.grid(column=0, columnspan=3, row=8, sticky=(W, E), padx=5, pady=5) # Convert to percentage - ctk.CTkLabel(self.head, text="MVC Value", font=('Segoe UI',15, 'bold')).grid(column=1, row=9, sticky=(W, E)) - - percent_button = ctk.CTkButton(self.head, text="To Percent*", command=self.to_percent, - fg_color="#E5E4E2", text_color="black", border_color="black", border_width=1) + ctk.CTkLabel(self.head, text="MVC Value", font=("Segoe UI", 18, "bold")).grid( + column=1, row=9, sticky=(W, E) + ) + + percent_button = ctk.CTkButton( + self.head, + text="To Percent*", + command=self.to_percent, + ) percent_button.grid(column=0, row=10, sticky=W) self.mvc_value = DoubleVar() mvc = ctk.CTkEntry(self.head, width=100, textvariable=self.mvc_value) mvc.grid(column=1, row=10) - - ctk.CTkLabel(self.head, - text= "*Use this button \nonly if your Refsig \nis in absolute values!", - font=("Arial", 8)).grid( - column=2, row=9, rowspan=2 - ) + ctk.CTkLabel( + self.head, + text="*Use this button \nonly if your Refsig \nis in absolute values!", + font=("Arial", 8), + ).grid(column=2, row=9, rowspan=2) # Add padding to all children widgets of head for child in self.head.winfo_children(): @@ -231,7 +255,9 @@ def filter_refsig(self): self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_fil") except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + ) def remove_offset(self): """ @@ -260,10 +286,16 @@ def remove_offset(self): self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to specify valid filtering or offset values.")) + show_error_dialog( + parent=self, + error=e, + solution=str("Make sure to specify valid filtering or offset values."), + ) def convert_refsig(self): """ @@ -278,22 +310,32 @@ def convert_refsig(self): When no reference signal file is available ValueError When invalid conversion factor is specified - + """ try: if self.convert.get() == "Multiply": - self.parent.resdict["REF_SIGNAL"] = self.parent.resdict["REF_SIGNAL"] * self.convert_factor.get() + self.parent.resdict["REF_SIGNAL"] = ( + self.parent.resdict["REF_SIGNAL"] * self.convert_factor.get() + ) elif self.convert.get() == "Divide": - self.parent.resdict["REF_SIGNAL"] = self.parent.resdict["REF_SIGNAL"] / self.convert_factor.get() + self.parent.resdict["REF_SIGNAL"] = ( + self.parent.resdict["REF_SIGNAL"] / self.convert_factor.get() + ) # Update Plot self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to specify valid conversion factor.")) + show_error_dialog( + parent=self, + error=e, + solution=str("Make sure to specify valid conversion factor."), + ) def to_percent(self): """ @@ -310,12 +352,20 @@ def to_percent(self): When invalid conversion factor is specified """ try: - self.parent.resdict["REF_SIGNAL"] = (self.parent.resdict["REF_SIGNAL"] * 100) / self.mvc_value.get() + self.parent.resdict["REF_SIGNAL"] = ( + self.parent.resdict["REF_SIGNAL"] * 100 + ) / self.mvc_value.get() # Update Plot self.parent.in_gui_plotting(resdict=self.parent.resdict) except AttributeError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure a Refsig file is loaded.")) + show_error_dialog( + parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + ) except ValueError as e: - show_error_dialog(parent=self, error=e, solution=str("Make sure to specify valid conversion factor.")) + show_error_dialog( + parent=self, + error=e, + solution=str("Make sure to specify valid conversion factor."), + ) diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 8c85dce..f21bf93 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -1,7 +1,6 @@ """Module that contains all helper functions for the GUI""" -from tkinter import W, E, filedialog -import customtkinter as ctk +from tkinter import filedialog import pandas as pd import openhdemg.library as openhdemg @@ -106,12 +105,12 @@ def resize_file(self): # Delsys requires different handling for resize if self.parent.resdict["SOURCE"] == "DELSYS": self.parent.resdict, _, _ = openhdemg.resize_emgfile( - emgfile=self.parent.resdict, area=[start, end], - accuracy="maintain" + emgfile=self.parent.resdict, area=[start, end], accuracy="maintain" ) else: self.parent.resdict, _, _ = openhdemg.resize_emgfile( - emgfile=self.parent.resdict, area=[start, end], + emgfile=self.parent.resdict, + area=[start, end], accuracy=self.parent.settings.resize_emgfile__accuracy, ignore_negative_ipts=self.parent.settings.resize_emgfile__ignore_negative_ipts, ) @@ -119,14 +118,15 @@ def resize_file(self): self.parent.in_gui_plotting(resdict=self.parent.resdict) # Update filelength - ctk.CTkLabel( - self.parent.left, text=str(self.parent.resdict["EMG_LENGTH"]), - font=('Segoe UI', 12) - ).grid(column=2, row=4, sticky=(W, E)) + self.parent.file_length.configure( + text="N of MUs: " + str(self.parent.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -150,15 +150,13 @@ def export_to_excel(self): path = filedialog.askdirectory() # Define excel writer - writer = pd.ExcelWriter( - path + "/Results_" + self.parent.filename + ".xlsx" - ) + writer = pd.ExcelWriter(path + "/Results_" + self.parent.filename + ".xlsx") # Check for attributes and write sheets if hasattr(self.parent, "mvc_df"): self.parent.mvc_df.to_excel(writer, sheet_name="MVC") - if hasattr(self.parent, "rfd"): + if hasattr(self.parent, "rfd"): self.parent.rfd.to_excel(writer, sheet_name="RFD") if hasattr(self.parent, "exportable_df"): @@ -167,9 +165,7 @@ def export_to_excel(self): ) if hasattr(self.parent, "mus_dr"): - self.parent.mus_dr.to_excel( - writer, sheet_name="MU Discharge Rate" - ) + self.parent.mus_dr.to_excel(writer, sheet_name="MU Discharge Rate") if hasattr(self.parent, "mu_thresholds"): self.mu_thresholds.to_excel(writer, sheet_name="MU Thresholds") @@ -178,21 +174,22 @@ def export_to_excel(self): except IndexError as e: show_error_dialog( - parent=self, error=e, - solution=str( - "Please conduct at least one analysis before saving." - ), + parent=self, + error=e, + solution=str("Please conduct at least one analysis before saving."), ) except AttributeError as e: show_error_dialog( - parent=self, error=e, - solution=str("Make sure a file is loaded.")) + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) except PermissionError as e: show_error_dialog( - parent=self, error=e, - solution=str("If /Results.xlsx already opened, please close.")) + parent=self, + error=e, + solution=str("If /Results.xlsx already opened, please close."), + ) def sort_mus(self): """ @@ -224,13 +221,15 @@ def sort_mus(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) except KeyError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Sorting not possible when ≤ 1" + "\nMU is present in the File (i.e. Refsigs)" diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 6178d1b..9c288d1 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -129,13 +129,11 @@ def __init__(self, parent): self.parent = parent self.parent.load_settings() - self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head = ctk.CTkToplevel() self.head.title("Plot Window") # Set window icon - head_path = os.path.dirname( - os.path.dirname(os.path.abspath(__file__)) - ) + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): @@ -159,7 +157,7 @@ def __init__(self, parent): ctk.CTkLabel( self.head, text="Reference signal", - font=('Segoe UI', 15, 'bold'), + font=("Segoe UI", 18, "bold"), ).grid(column=0, row=0, sticky=W) self.ref_but = StringVar() ref_button = ctk.CTkCheckBox( @@ -168,7 +166,7 @@ def __init__(self, parent): bg_color="LightBlue4", onvalue="True", offvalue="False", - text="" + text="", ) ref_button.grid(column=1, row=0, sticky=(W)) self.ref_but.set(False) @@ -177,7 +175,7 @@ def __init__(self, parent): ctk.CTkLabel( self.head, text="Time in seconds", - font=('Segoe UI', 15, 'bold'), + font=("Segoe UI", 18, "bold"), ).grid(column=0, row=1, sticky=W) self.time_sec = StringVar() time_button = ctk.CTkCheckBox( @@ -186,7 +184,7 @@ def __init__(self, parent): bg_color="LightBlue4", onvalue="True", offvalue="False", - text="" + text="", ) time_button.grid(column=1, row=1, sticky=W) self.time_sec.set(False) @@ -195,45 +193,45 @@ def __init__(self, parent): ctk.CTkLabel( self.head, text="Figure size in cm (h,w)", - font=('Segoe UI', 15, 'bold'), + font=("Segoe UI", 18, "bold"), ).grid(column=0, row=2) self.size_fig = StringVar() - fig_entry = ctk.CTkEntry( - self.head, width=100, textvariable=self.size_fig - ) + fig_entry = ctk.CTkEntry(self.head, width=100, textvariable=self.size_fig) self.size_fig.set("20,15") fig_entry.grid(column=1, row=2, sticky=W) # Plot emgsig plt_emgsig = ctk.CTkButton( - self.head, text="Plot EMGsig", command=self.plt_emgsignal, - fg_color="#E5E4E2", text_color="black", border_color="black", - border_width=1, + self.head, + text="Plot EMGsig", + command=self.plt_emgsignal, ) plt_emgsig.grid(column=0, row=3, sticky=W) self.channels = StringVar() channel_entry_values = ("0", "0,1,2", "0,1,2,3") channel_entry = ctk.CTkComboBox( - self.head, width=150, variable=self.channels, - values=channel_entry_values + self.head, + width=150, + variable=self.channels, + values=channel_entry_values, ) channel_entry.grid(column=1, row=3, sticky=(W, E)) self.channels.set("Channel Numbers") # Plot refsig plt_refsig = ctk.CTkButton( - self.head, text="Plot RefSig", command=self.plt_refsignal, - fg_color="#E5E4E2", text_color="black", border_color="black", - border_width=1, + self.head, + text="Plot RefSig", + command=self.plt_refsignal, ) plt_refsig.grid(column=0, row=4, sticky=W) # Plot motor unit pulses plt_pulses = ctk.CTkButton( - self.head, text="Plot MUpulses", command=self.plt_mupulses, - fg_color="#E5E4E2", text_color="black", border_color="black", - border_width=1, + self.head, + text="Plot MUpulses", + command=self.plt_mupulses, ) plt_pulses.grid(column=0, row=5, sticky=W) @@ -241,24 +239,28 @@ def __init__(self, parent): self.linewidth = StringVar() linewidth_entry_values = ("0.25", "0.5", "0.75", "1") linewidth_entry = ctk.CTkComboBox( - self.head, width=15, variable=self.linewidth, - values=linewidth_entry_values + self.head, + width=15, + variable=self.linewidth, + values=linewidth_entry_values, ) linewidth_entry.grid(column=1, row=5, sticky=(W, E)) self.linewidth.set("Linewidth") # Plot impulse train plt_ipts_but = ctk.CTkButton( - self.head, text="Plot Source", command=self.plt_ipts, - fg_color="#E5E4E2", text_color="black", border_color="black", - border_width=1, + self.head, + text="Plot Source", + command=self.plt_ipts, ) plt_ipts_but.grid(column=0, row=6, sticky=W) self.mu_numb = StringVar() munumb_entry_values = ("0", "0,1,2", "0,1,2,3", "all") munumb_entry = ctk.CTkComboBox( - self.head, width=15, variable=self.mu_numb, + self.head, + width=15, + variable=self.mu_numb, values=munumb_entry_values, ) munumb_entry.grid(column=1, row=6, sticky=(W, E)) @@ -266,17 +268,19 @@ def __init__(self, parent): # Plot instantaneous discharge rate plt_idr_but = ctk.CTkButton( - self.head, text="Plot IDR", command=self.plt_idr, - fg_color="#E5E4E2", text_color="black", border_color="black", - border_width=1, + self.head, + text="Plot IDR", + command=self.plt_idr, ) plt_idr_but.grid(column=0, row=7, sticky=W) self.mu_numb_idr = StringVar() munumb_entry_idr_values = ("0", "0,1,2", "0,1,2,3", "all") munumb_entry_idr = ctk.CTkComboBox( - self.head, width=15, variable=self.mu_numb_idr, - values=munumb_entry_idr_values + self.head, + width=15, + variable=self.mu_numb_idr, + values=munumb_entry_idr_values, ) munumb_entry_idr.grid(column=1, row=7, sticky=(W, E)) self.mu_numb_idr.set("MU Number") @@ -289,7 +293,7 @@ def __init__(self, parent): # Matrix code ctk.CTkLabel( - self.head, text="Matrix Code", font=('Segoe UI', 15, 'bold') + self.head, text="Matrix Code", font=("Segoe UI", 18, "bold") ).grid(row=0, column=3, sticky=(W)) self.mat_code = StringVar() @@ -297,14 +301,17 @@ def __init__(self, parent): matrix_code_values = ( "GR08MM1305", "GR04MM1305", - "GR10MM0808", + "GR10MM0808", "Trigno Galileo Sensor", "Custom order", "None", ) matrix_code = ctk.CTkComboBox( - self.head, width=100, variable=self.mat_code, - values=matrix_code_values, state="readonly", + self.head, + width=100, + variable=self.mat_code, + values=matrix_code_values, + state="readonly", ) matrix_code.grid(row=0, column=4, sticky=(W, E)) self.mat_code.set("GR08MM1305") @@ -314,13 +321,18 @@ def __init__(self, parent): # Matrix Orientation ctk.CTkLabel( - self.head, text="Orientation", font=('Segoe UI', 15, 'bold'), + self.head, + text="Orientation", + font=("Segoe UI", 18, "bold"), ).grid(row=1, column=3, sticky=(W)) self.mat_orientation = StringVar() orientation_values = ("0", "180") orientation = ctk.CTkComboBox( - self.head, width=15, variable=self.mat_orientation, - values=orientation_values, state="readonly" + self.head, + width=15, + variable=self.mat_orientation, + values=orientation_values, + state="readonly", ) orientation.grid(row=1, column=4, sticky=(W, E)) self.mat_orientation.set("180") @@ -331,20 +343,21 @@ def __init__(self, parent): # Plot derivation # Button deriv_button = ctk.CTkButton( - self.head, text="Plot Derivation", - command=self.plot_derivation, fg_color="#E5E4E2", - text_color="black", border_color="black", border_width=1, + self.head, + text="Plot Derivation", + command=self.plot_derivation, ) deriv_button.grid(row=3, column=3, sticky=W) # Combobox Config self.deriv_config = StringVar() - configuration_values = ( - "Single differential", "Double differential" - ) + configuration_values = ("Single differential", "Double differential") configuration = ctk.CTkComboBox( - self.head, width=15, variable=self.deriv_config, - values=configuration_values, state="readonly" + self.head, + width=15, + variable=self.deriv_config, + values=configuration_values, + state="readonly", ) configuration.grid(row=3, column=4, sticky=(W, E)) self.deriv_config.set("Configuration") @@ -353,8 +366,11 @@ def __init__(self, parent): self.deriv_matrix = StringVar() mat_column_values = ("col0", "col1", "col2", "col3", "col4") mat_column = ctk.CTkComboBox( - self.head, width=100, variable=self.deriv_matrix, - values=mat_column_values, state="readonly" + self.head, + width=100, + variable=self.deriv_matrix, + values=mat_column_values, + state="readonly", ) mat_column.grid(row=3, column=5, sticky=(W, E)) self.deriv_matrix.set("Matrix Column") @@ -362,9 +378,9 @@ def __init__(self, parent): # Motor unit action potential # Button muap_button = ctk.CTkButton( - self.head, text="Plot MUAPs", command=self.plot_muaps, - fg_color="#E5E4E2", text_color="black", border_color="black", - border_width=1, + self.head, + text="Plot MUAPs", + command=self.plot_muaps, ) muap_button.grid(row=4, column=3, sticky=W) @@ -376,8 +392,11 @@ def __init__(self, parent): "Double differential", ) config_muap = ctk.CTkComboBox( - self.head, width=15, variable=self.muap_config, - values=config_muap_values, state="readonly" + self.head, + width=15, + variable=self.muap_config, + values=config_muap_values, + state="readonly", ) config_muap.grid(row=4, column=4, sticky=(W, E)) self.muap_config.set("Configuration") @@ -391,8 +410,11 @@ def __init__(self, parent): str(number) for number in range(0, self.parent.resdict["NUMBER_OF_MUS"]) ) muap_munum = ctk.CTkComboBox( - self.head, width=15, variable=self.muap_munum, - values=mu_numbers, state="readonly", + self.head, + width=15, + variable=self.muap_munum, + values=mu_numbers, + state="readonly", ) muap_munum.grid(row=4, column=5, sticky=(W, E)) self.muap_munum.set("MU Number") @@ -401,7 +423,9 @@ def __init__(self, parent): self.muap_time = StringVar() timewindow_values = ("25", "50", "100", "200") timewindow = ctk.CTkComboBox( - self.head, width=120, variable=self.muap_time, + self.head, + width=120, + variable=self.muap_time, values=timewindow_values, ) timewindow.grid(row=4, column=6, sticky=(W, E)) @@ -412,15 +436,14 @@ def __init__(self, parent): # Matrix Illustration Graphic matrix_canvas = ctk.CTkCanvas( - self.head, height=150, width=600, bg="white", + self.head, + height=150, + width=600, + bg="white", ) matrix_canvas.grid(row=5, column=3, rowspan=5, columnspan=5) - parent_dir = os.path.dirname( - os.path.dirname(os.path.abspath(__file__)) - ) - self.matrix = PhotoImage( - file=parent_dir + "/gui_files/Matrix.png" - ) + parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.matrix = PhotoImage(file=parent_dir + "/gui_files/Matrix.png") matrix_canvas.create_image(0, 0, anchor="nw", image=self.matrix) # Information Button self.info = ctk.CTkImage( @@ -435,6 +458,7 @@ def __init__(self, parent): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + border_width=0, command=lambda: ( ( webbrowser.open( @@ -446,11 +470,15 @@ def __init__(self, parent): info_button.grid(row=0, column=6, sticky=E) for child in self.head.winfo_children(): - child.grid_configure(padx=5, pady=5, ) + child.grid_configure( + padx=5, + pady=5, + ) except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) self.head.destroy() @@ -469,12 +497,16 @@ def on_matrix_none(self, *args): if self.mat_code.get() == "None": # Place label defined in init # Label for matriy rows columns - self.mat_label = ttk.Label(self.head, text="Rows, Columns:") + self.mat_label = ctk.CTkLabel( + self.head, text="Rows, Columns:", font=("Segoe UI", 18, "bold") + ) self.mat_label.grid(row=0, column=5, sticky=E) # Column entry only when specified matrix code - self.row_cols_entry = ttk.Entry( - self.head, width=8, textvariable=self.matrix_rc, + self.row_cols_entry = ctk.CTkEntry( + self.head, + width=30, + textvariable=self.matrix_rc, ) self.row_cols_entry.grid(row=0, column=6, sticky=W, padx=5) self.matrix_rc.set("13,5") @@ -538,21 +570,22 @@ def plt_emgsignal(self): except ValueError as e: show_error_dialog( - parent=self, error=e, - solution=str( - "Enter valid channel number or non-negative figure size." - ), + parent=self, + error=e, + solution=str("Enter valid channel number or non-negative figure size."), ) except KeyError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid channel number."), ) except IndexError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Enter valid figure size. Must be non negative and tuple of (heigth, width)." ), @@ -614,7 +647,8 @@ def plt_mupulses(self): except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid linewidth number."), ) @@ -679,13 +713,15 @@ def plt_ipts(self): except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid motor unit number."), ) except KeyError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid motor unit number."), ) @@ -747,13 +783,15 @@ def plt_idr(self): except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid motor unit number."), ) except KeyError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid motor unit number."), ) @@ -791,12 +829,13 @@ def plot_derivation(self): code=self.mat_code.get(), orientation=int(self.mat_orientation.get()), n_rows=list_rcs[0], - n_cols=list_rcs[1] + n_cols=list_rcs[1], ) except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Number of specified rows and columns must match number of channels." ), @@ -832,7 +871,8 @@ def plot_derivation(self): ) except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Enter valid input parameters." + "\nPotenital error sources:" @@ -845,13 +885,15 @@ def plot_derivation(self): except UnboundLocalError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid Configuration and Matrix Column."), ) except KeyError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid Matrix Column."), ) @@ -889,7 +931,8 @@ def plot_muaps(self): self.parent.resdict, ) openhdemg.plot_muaps( - muaps_dict[int(self.muap_munum.get())], figsize=figsize, + muaps_dict[int(self.muap_munum.get())], + figsize=figsize, ) else: @@ -904,12 +947,13 @@ def plot_muaps(self): code=self.mat_code.get(), orientation=int(self.mat_orientation.get()), n_rows=list_rcs[0], - n_cols=list_rcs[1] + n_cols=list_rcs[1], ) except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Number of specified rows and columns must match" ), @@ -951,12 +995,14 @@ def plot_muaps(self): # Plot MUAPS openhdemg.plot_muaps( - sta_dict[int(self.muap_munum.get())], figsize=figsize, + sta_dict[int(self.muap_munum.get())], + figsize=figsize, ) except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Enter valid input parameters." + "\nPotenital error sources:" @@ -970,7 +1016,8 @@ def plot_muaps(self): ) show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Enter valid input parameters." + "\nPotenital error sources:" @@ -985,12 +1032,14 @@ def plot_muaps(self): except UnboundLocalError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid Configuration."), ) except KeyError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid Matrix Column."), ) diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index a6c8bd6..dad39f2 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -71,6 +71,7 @@ class MuAnalysis: `parent`'s properties. """ + def __init__(self, parent): """ Initialize a new instance of the MuAnalysis class. @@ -106,13 +107,13 @@ def __init__(self, parent): self.parent = parent self.parent.load_settings() # Create new window - self.head = ctk.CTkToplevel(fg_color="LightBlue4") + self.head = ctk.CTkToplevel() self.head.title("Motor Unit Properties Window") # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon2.ico" - self.head.iconbitmap(default=iconpath) + self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) self.head.grab_set() @@ -128,11 +129,15 @@ def __init__(self, parent): # MVC Entry ctk.CTkLabel( - self.head, text="Enter MVC[n]:", font=('Segoe UI', 15, 'bold'), + self.head, + text="Enter MVC[n]:", + font=("Segoe UI", 18, "bold"), ).grid(column=0, row=0, sticky=(W)) self.mvc_value = StringVar() enter_mvc = ctk.CTkEntry( - self.head, width=100, textvariable=self.mvc_value, + self.head, + width=100, + textvariable=self.mvc_value, ) enter_mvc.grid(column=1, row=0, sticky=(W, E)) @@ -141,17 +146,20 @@ def __init__(self, parent): separator.grid(column=0, columnspan=4, row=2, padx=5, pady=5) thresh = ctk.CTkButton( - self.head, text="Compute threshold", - command=self.compute_mu_threshold, fg_color="#E5E4E2", - text_color="black", border_color="black", border_width=1, + self.head, + text="Compute threshold", + command=self.compute_mu_threshold, ) thresh.grid(column=0, row=3, sticky=W) self.ct_event = StringVar() ct_event_values = ("rt", "dert", "rt_dert") ct_events_entry = ctk.CTkComboBox( - self.head, width=100, variable=self.ct_event, - values=ct_event_values, state="readonly", + self.head, + width=100, + variable=self.ct_event, + values=ct_event_values, + state="readonly", ) ct_events_entry.grid(column=1, row=3) self.ct_event.set("Event") @@ -159,8 +167,11 @@ def __init__(self, parent): self.ct_type = StringVar() ct_types_values = ("abs", "rel", "abs_rel") ct_types_entry = ctk.CTkComboBox( - self.head, width=100, variable=self.ct_type, - values=ct_types_values, state="readonly", + self.head, + width=100, + variable=self.ct_type, + values=ct_types_values, + state="readonly", ) ct_types_entry.grid(column=2, row=3) self.ct_type.set("Type") @@ -168,34 +179,46 @@ def __init__(self, parent): # Compute motor unit discharge rate separator1 = ttk.Separator(self.head, orient="horizontal") separator1.grid( - column=0, columnspan=4, row=4, sticky=(W, E), padx=5, pady=5, + column=0, + columnspan=4, + row=4, + sticky=(W, E), + padx=5, + pady=5, ) ctk.CTkLabel( - self.head, text="Firings at Rec", font=('Segoe UI', 15, 'bold'), + self.head, + text="Firings at Rec", + font=("Segoe UI", 18, "bold"), ).grid(column=1, row=5, sticky=(W, E)) ctk.CTkLabel( - self.head, text="Firings Start/End Steady", - font=('Segoe UI', 15, 'bold'), + self.head, + text="Firings Start/End Steady", + font=("Segoe UI", 18, "bold"), ).grid(column=2, row=5, sticky=(W, E)) dr_rate = ctk.CTkButton( - self.head, text="Compute discharge rate", - command=self.compute_mu_dr, fg_color="#E5E4E2", text_color="black", - border_color="black", border_width=1, + self.head, + text="Compute discharge rate", + command=self.compute_mu_dr, ) dr_rate.grid(column=0, row=6, sticky=W) self.firings_rec = StringVar() firings_1 = ctk.CTkEntry( - self.head, width=100, textvariable=self.firings_rec, + self.head, + width=100, + textvariable=self.firings_rec, ) firings_1.grid(column=1, row=6) self.firings_rec.set(4) self.firings_ste = StringVar() firings_2 = ctk.CTkEntry( - self.head, width=100, textvariable=self.firings_ste, + self.head, + width=100, + textvariable=self.firings_ste, ) firings_2.grid(column=2, row=6) self.firings_ste.set(10) @@ -209,8 +232,11 @@ def __init__(self, parent): "rec_derec_steady", ) dr_events_entry = ctk.CTkComboBox( - self.head, width=100, variable=self.dr_event, - values=dr_events_values, state="readonly", + self.head, + width=100, + variable=self.dr_event, + values=dr_events_values, + state="readonly", ) dr_events_entry.grid(column=3, row=6, sticky=E) self.dr_event.set("Event") @@ -218,35 +244,46 @@ def __init__(self, parent): # Compute basic motor unit properties separator2 = ttk.Separator(self.head, orient="horizontal") separator2.grid( - column=0, columnspan=4, row=7, sticky=(W, E), padx=5, pady=5, + column=0, + columnspan=4, + row=7, + sticky=(W, E), + padx=5, + pady=5, ) ctk.CTkLabel( - self.head, text="Firings at Rec", - font=('Segoe UI', 15, 'bold'), + self.head, + text="Firings at Rec", + font=("Segoe UI", 18, "bold"), ).grid(column=1, row=8, sticky=(W, E)) ctk.CTkLabel( - self.head, text="Firings Start/End Steady", - font=('Segoe UI', 15, 'bold'), + self.head, + text="Firings Start/End Steady", + font=("Segoe UI", 18, "bold"), ).grid(column=2, row=8, sticky=(W, E)) basic = ctk.CTkButton( - self.head, text="Basic MU properties", - command=self.basic_mus_properties, fg_color="#E5E4E2", - text_color="black", border_color="black", border_width=1, + self.head, + text="Basic MU properties", + command=self.basic_mus_properties, ) basic.grid(column=0, row=9, sticky=W) self.b_firings_rec = StringVar() b_firings_1 = ctk.CTkEntry( - self.head, width=100, textvariable=self.b_firings_rec, + self.head, + width=100, + textvariable=self.b_firings_rec, ) b_firings_1.grid(column=1, row=9) self.b_firings_rec.set(4) self.b_firings_ste = StringVar() b_firings_2 = ctk.CTkEntry( - self.head, width=100, textvariable=self.b_firings_ste, + self.head, + width=100, + textvariable=self.b_firings_ste, ) b_firings_2.grid(column=2, row=9) self.b_firings_ste.set(10) @@ -293,19 +330,22 @@ def compute_mu_threshold(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Enter valid MVC, Event or Type."), ) except AssertionError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Specify Event and/or Type."), ) @@ -345,12 +385,13 @@ def compute_mu_dr(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, - solution=str("Make sure a file is loaded.")) + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Enter valid Firings value or select a correct number of points." ), @@ -358,7 +399,8 @@ def compute_mu_dr(self): except AssertionError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Specify Event and/or Type."), ) @@ -404,12 +446,13 @@ def basic_mus_properties(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, - solution=str("Make sure a file is loaded.")) + parent=self, error=e, solution=str("Make sure a file is loaded.") + ) except ValueError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str( "Enter valid MVC value or select a correct number of points." ), @@ -417,10 +460,10 @@ def basic_mus_properties(self): except AssertionError as e: show_error_dialog( - parent=self, error=e, - solution=str("Specify Event and/or Type.")) + parent=self, error=e, solution=str("Specify Event and/or Type.") + ) except UnboundLocalError as e: show_error_dialog( - parent=self, error=e, - solution=str("Select start/end area again.")) + parent=self, error=e, solution=str("Select start/end area again.") + ) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index b732914..7429a1a 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -33,7 +33,6 @@ FigureCanvasTkAgg, NavigationToolbar2Tk, ) -from matplotlib.figure import Figure import openhdemg.library as openhdemg import openhdemg.gui.settings as settings @@ -50,9 +49,12 @@ matplotlib.use("TkAgg") +ctk.set_default_color_theme( + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/openhdemg.json" +) -class emgGUI: +class emgGUI(ctk.CTk): """ This class is used to create a graphical user interface for the openhdemg library. @@ -80,17 +82,17 @@ class emgGUI: Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", "CUSTOMCSV", "CUSTOMCSV_REFSIG". self.left : tk.frame - Left frame inside of master that contains all buttons and filespecs. + Left frame inside of self that contains all buttons and filespecs. self.logo : String containing the path to image file containing logo of openhdemg. self.logo_canvas : tk.canvas Canvas to display logo of Open_HG-EMG when openend. - self.master: tk - TK master window containing all widget children for this GUI. + self.self: tk + TK self window containing all widget children for this GUI. self.resdict : dict Dictionary derived from input EMG file for further analysis. self.right : tk.frame - Left frame inside of master that contains plotting canvas. + Left frame inside of self that contains plotting canvas. self.terminal : ttk.Labelframe Tkinter labelframe that is used to display the results table in the GUI. @@ -104,33 +106,29 @@ class emgGUI: Contact Icon displayed in GUI. self.cite : tk.PhotoImage Citation Icon displayed in GUI. - self.otb_combobox : ttk.Combobox - Combobox appearing in main GUI window or advanced analysis window when - OTB files are loaded. Contains the extension factor for OTB files. - Stringvariable containing the self.extension_factor : tk.StringVar() Stringvariable containing the OTB extension factor value. Methods ------- - __init__(master) - Initializes GUI class and main GUI window (master). + __init__(self) + Initializes GUI class and main GUI window (self). get_file_input() Gets emgfile location and respective file is loaded. - Executed when button "Load File" in master GUI window pressed. + Executed when button "Load File" in self GUI window pressed. save_emgfile() Saves the edited emgfile dictionary to a .json file. - Executed when button "Save File" in master GUI window pressed. + Executed when button "Save File" in self GUI window pressed. reset_analysis() Resets the whole analysis, restores the original input file and the graph. - Executed when button "Reset analysis" in master GUI window pressed. + Executed when button "Reset analysis" in self GUI window pressed. in_gui_plotting() Method used for creating plot inside the GUI (on the GUI canvas). - Executed when button "View MUs" in master GUI window pressed. + Executed when button "View MUs" in self GUI window pressed. mu_analysis() Opens seperate window to calculated specific motor unit properties. - Executed when button "MU properties" in master GUI window pressed. + Executed when button "MU properties" in self GUI window pressed. display_results() Method used to display result table containing analysis results. @@ -148,42 +146,43 @@ class emgGUI: documentation in the library. """ - def __init__(self, master): + def __init__(self): """ - Initialization of master GUI window upon calling. + Initialization of GUI window upon calling. Parameters ---------- - master: tk + : tk tk class object """ + super().__init__() # Load settings self.load_settings() # Set up GUI - self.master = master - self.master.title("openhdemg") + self.title("openhdemg") master_path = os.path.dirname(os.path.abspath(__file__)) - iconpath = master_path + "/gui_files/Icon.ico" - self.master.iconbitmap(iconpath) + ctk.set_default_color_theme(master_path + "/gui_files/openhdemg.json") + + iconpath = master_path + "/gui_files/Icon2.ico" + self.iconbitmap(iconpath) # Necessary for resizing - self.master.columnconfigure(0, weight=1) - self.master.columnconfigure(1, weight=1) - self.master.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=5) + self.rowconfigure(0, weight=1) + self.minsize(width=600, height=400) # Create left side framing for functionalities self.left = ctk.CTkFrame( - self.master, - fg_color=self.settings.gui_background_color, - corner_radius=0, + self, ) self.left.grid(column=0, row=0, sticky=(N, S, E, W)) # Configure columns with a loop - for col in range(3): - self.left.columnconfigure(col, weight=1) + self.left.columnconfigure(0, weight=1) + self.left.columnconfigure(1, weight=1) # Configure rows with a loop for row in range(21): @@ -218,60 +217,51 @@ def __init__(self, master): self.left, text="Load File", command=self.get_file_input, - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) load.grid(column=0, row=3, sticky=(N, S, E, W)) # File specifications ctk.CTkLabel( self.left, - text="Filespecs:", - font=("Segoe UI", 15, "bold"), + text="Filespecs", + font=("Segoe UI", 18, "underline"), ).grid(column=1, row=1, sticky=(W)) - ctk.CTkLabel( + self.n_channels = ctk.CTkLabel( self.left, text="N Channels:", font=("Segoe UI", 15, "bold"), - ).grid(column=1, row=2, sticky=(W)) - ctk.CTkLabel( + ) + self.n_channels.grid(column=1, row=2, sticky=(W)) + self.n_of_mus = ctk.CTkLabel( self.left, text="N of MUs:", font=("Segoe UI", 15, "bold"), - ).grid(column=1, row=3, sticky=(W)) - ctk.CTkLabel( + ) + self.n_of_mus.grid(column=1, row=3, sticky=(W)) + self.file_length = ctk.CTkLabel( self.left, - text="File length:", + text="File Length:", font=("Segoe UI", 15, "bold"), - ).grid(column=1, row=4, sticky=(W)) + ) + self.file_length.grid(column=1, row=4, sticky=(W)) separator0 = ttk.Separator(self.left, orient="horizontal") - separator0.grid(column=0, columnspan=3, row=5, sticky=(E, W)) + separator0.grid(column=0, columnspan=2, row=5, sticky=(E, W)) # Save File save = ctk.CTkButton( self.left, text="Save File", command=self.save_emgfile, - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) save.grid(column=0, row=6, sticky=(N, S, E, W)) separator1 = ttk.Separator(self.left, orient="horizontal") - separator1.grid(column=0, columnspan=3, row=7, sticky=(E, W)) + separator1.grid(column=0, columnspan=2, row=7, sticky=(E, W)) # Export to Excel export = ctk.CTkButton( self.left, text="Save Results", command=lambda: (GUIHelpers(parent=self).export_to_excel()), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) export.grid(column=1, row=6, sticky=(N, S, E, W)) @@ -280,10 +270,6 @@ def __init__(self, master): self.left, text="View MUs", command=lambda: (self.in_gui_plotting(resdict=self.resdict)), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) firings.grid(column=0, row=8, sticky=(N, S, E, W)) @@ -292,39 +278,27 @@ def __init__(self, master): self.left, text="Sort MUs", command=lambda: (GUIHelpers(parent=self).sort_mus()), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) sorting.grid(column=1, row=8, sticky=(N, S, E, W)) separator2 = ttk.Separator(self.left, orient="horizontal") - separator2.grid(column=0, columnspan=3, row=9, sticky=(E, W)) + separator2.grid(column=0, columnspan=2, row=9, sticky=(E, W)) # Remove Motor Units remove_mus = ctk.CTkButton( self.left, text="Remove MUs", command=lambda: (MURemovalWindow(parent=self)), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) remove_mus.grid(column=0, row=10, sticky=(N, S, E, W)) separator3 = ttk.Separator(self.left, orient="horizontal") - separator3.grid(column=0, columnspan=3, row=11, sticky=(E, W)) + separator3.grid(column=0, columnspan=2, row=11, sticky=(E, W)) # Filter Reference Signal reference = ctk.CTkButton( self.left, text="RefSig Editing", command=lambda: (EditRefsig(parent=self)), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) reference.grid(column=0, row=12, sticky=(N, S, E, W)) @@ -333,66 +307,46 @@ def __init__(self, master): self.left, text="Resize File", command=lambda: (GUIHelpers(parent=self).resize_file()), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) resize.grid(column=1, row=12, sticky=(N, S, E, W)) separator4 = ttk.Separator(self.left, orient="horizontal") - separator4.grid(column=0, columnspan=3, row=13, sticky=(E, W)) + separator4.grid(column=0, columnspan=2, row=13, sticky=(E, W)) # Force Analysis force = ctk.CTkButton( self.left, text="Analyse Force", command=lambda: (AnalyseForce(parent=self)), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) force.grid(column=0, row=14, sticky=(N, S, E, W)) separator5 = ttk.Separator(self.left, orient="horizontal") - separator5.grid(column=0, columnspan=3, row=15, sticky=(E, W)) + separator5.grid(column=0, columnspan=2, row=15, sticky=(E, W)) # Motor Unit properties mus = ctk.CTkButton( self.left, text="MU Properties", command=lambda: (MuAnalysis(parent=self)), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) mus.grid(column=1, row=14, sticky=(N, S, E, W)) separator6 = ttk.Separator(self.left, orient="horizontal") - separator6.grid(column=0, columnspan=3, row=17, sticky=(E, W)) + separator6.grid(column=0, columnspan=2, row=17, sticky=(E, W)) # Plot EMG plots = ctk.CTkButton( self.left, text="Plot EMG", command=lambda: (PlotEmg(parent=self)), - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) plots.grid(column=0, row=16, sticky=(N, S, E, W)) separator7 = ttk.Separator(self.left, orient="horizontal") - separator7.grid(column=0, columnspan=3, row=19, sticky=(E, W)) + separator7.grid(column=0, columnspan=2, row=19, sticky=(E, W)) # Reset Analysis reset = ctk.CTkButton( self.left, text="Reset Analysis", command=self.reset_analysis, - fg_color="#E5E4E2", - text_color="black", - border_color="black", - border_width=1, ) reset.grid(column=1, row=18, sticky=(N, S, E, W)) @@ -401,20 +355,16 @@ def __init__(self, master): self.left, text="Advanced Tools", command=lambda: (AdvancedAnalysis(self)), - fg_color="#000000", - text_color="white", - border_color="white", - border_width=1, hover_color="#FFBF00", + fg_color="#000000", + text_color="#FFFFFF", + border_color="#FFFFFF", ) advanced.grid(row=20, column=0, columnspan=2, sticky=(N, S, E, W)) # Create right side framing for functionalities self.right = ctk.CTkFrame( - self.master, - fg_color="LightBlue4", - corner_radius=0, - bg_color="LightBlue4", + self, ) self.right.grid(column=1, row=0, sticky=(N, S, E, W)) @@ -425,18 +375,11 @@ def __init__(self, master): for row in range(5): self.right.rowconfigure(row, weight=1) - # Create empty figure - self.first_fig = Figure(figsize=(20 / 2.54, 15 / 2.54), frameon=True) - self.canvas = FigureCanvasTkAgg(self.first_fig, master=self.right) - self.canvas.get_tk_widget().grid( - row=0, column=0, rowspan=6, sticky=(N, S, E, W) - ) - # Create logo figure self.logo_canvas = Canvas( self.right, - height=590, - width=800, + height=800, + width=1000, bg="white", ) self.logo_canvas.grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) @@ -444,10 +387,9 @@ def __init__(self, master): logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) - self.logo_canvas.create_image( - 400, - 300, - anchor="center", + self.logo_canvas.create_image( # TODO make resizable + 1000, + 500, image=self.logo, ) @@ -468,6 +410,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + border_width=0, ) settings_b.grid(column=1, row=0, sticky=W, pady=(0, 20)) @@ -485,6 +428,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + border_width=0, command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/gui_intro/")) ), @@ -505,6 +449,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + border_width=0, command=lambda: ( ( webbrowser.open( @@ -529,6 +474,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + border_width=0, command=lambda: ( ( webbrowser.open( @@ -553,6 +499,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + border_width=0, command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/contacts/")) ), @@ -573,6 +520,7 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + border_width=0, command=lambda: ( (webbrowser.open("https://www.giacomovalli.com/openhdemg/cite-us/")) ), @@ -595,7 +543,6 @@ def load_settings(self): # If not previously imported, just import it global settings self.settings = importlib.reload(settings) - self.update_gui_variables() def open_settings(self): """ @@ -617,14 +564,6 @@ def open_settings(self): else: # Linux or other subprocess.run(["xdg-open", file_path]) - # Unused (yet) - def update_gui_variables(self): - """ - Method to update variables changes in the settings file - """ - - pass - def get_file_input(self): """ Instance Method to load the file for analysis. The user is asked to @@ -664,18 +603,19 @@ def load_file(): ignore_negative_ipts=self.settings.emg_from_otb__ignore_negative_ipts, ) # Add filespecs - ctk.CTkLabel( - self.left, - text=str(len(self.resdict["RAW_SIGNAL"].columns)), - ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, - text=str(self.resdict["NUMBER_OF_MUS"]), - ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, - text=str(self.resdict["EMG_LENGTH"]), - ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) elif self.filetype.get() == "DEMUSE": # Ask user to select the file @@ -690,16 +630,19 @@ def load_file(): ignore_negative_ipts=self.settings.emg_from_demuse__ignore_negative_ipts, ) # Add filespecs - ctk.CTkLabel( - self.left, - text=str(len(self.resdict["RAW_SIGNAL"].columns)), - ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, text=str(self.resdict["NUMBER_OF_MUS"]) - ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, text=str(self.resdict["EMG_LENGTH"]) - ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) elif self.filetype.get() == "DELSYS": # Ask user to select the file @@ -718,15 +661,19 @@ def load_file(): rawemg_filepath=self.file_path, mus_directory=self.mus_path ) # Add filespecs - ctk.CTkLabel( - self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, text=str(self.resdict["NUMBER_OF_MUS"]) - ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, text=str(self.resdict["EMG_LENGTH"]) - ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) elif self.filetype.get() == "OPENHDEMG": # Ask user to select the file @@ -744,27 +691,34 @@ def load_file(): "CUSTOMCSV", "DELSYS", ]: - ctk.CTkLabel( - self.left, - text=str(len(self.resdict["RAW_SIGNAL"].columns)), - ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, text=str(self.resdict["NUMBER_OF_MUS"]) - ).grid(column=2, row=3, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel( - self.left, text=str(self.resdict["EMG_LENGTH"]) - ).grid(column=2, row=4, sticky=(W, E), padx=5, pady=5) + # Add filespecs + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) else: - # Reconfigure labels for refsig - ctk.CTkLabel( - self.left, - text=str(len(self.resdict["REF_SIGNAL"].columns)), - ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text="NA").grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 + # Add filespecs + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), ) - ctk.CTkLabel(self.left, text=" ").grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), ) else: # Ask user to select the file @@ -779,14 +733,18 @@ def load_file(): fsamp=float(self.fsamp.get()), ) # Add filespecs - ctk.CTkLabel(self.left, text="Custom CSV").grid( - column=2, row=2, sticky=(W, E), padx=5, pady=5 + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), ) - ctk.CTkLabel(self.left, text="").grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), ) - ctk.CTkLabel(self.left, text="").grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), ) # Get filename @@ -794,7 +752,7 @@ def load_file(): self.filename = filename # Add filename to label - self.master.title(self.filename) + self.title(self.filename) # End progress progress.grid_remove() @@ -844,7 +802,7 @@ def load_file(): self.filename = filename # Add filename to label - self.master.title(self.filename) + self.title(self.filename) # Reconfigure labels for refsig ctk.CTkLabel( @@ -857,12 +815,9 @@ def load_file(): column=2, row=4, sticky=(W, E), padx=5, pady=5 ) - # for child in self.left.winfo_children(): - # child.grid_configure(padx=5, pady=5) - # End progress - progress.grid_remove() # NOTE does it matter the order of grid_remove and stop? They are used with mixed orders! progress.stop() + progress.grid_remove() return @@ -922,16 +877,14 @@ def load_file(): except: # End progress - progress.grid_remove() progress.stop() + progress.grid_remove() # Indicate Progress progress = ctk.CTkProgressBar( self.left, mode="indeterminate", - fg_color="#585858", width=100, - progress_color="#FFBF00", ) progress.grid(row=4, column=0) progress.start() @@ -947,12 +900,9 @@ def on_filetype_change(self, *args): create a second combobox on the grid at column 0 and row 2 and when the filetype is set to something else it will remove the second combobox from the grid. """ - # TODO here, instead of showing the boxes, we shopuld simply write a text to tell them to check in the gui_settings - # File if the settings are appropriate for their file. e.g., extension factor and fsamp. - # A text "Verify openfiles settings" might be appropriate. To be displayed similary to "Ignored for DEMUSE files". if self.filetype.get() not in ["OTB"]: - if hasattr(self, "otb_combobox"): - self.otb_combobox.grid_forget() + if hasattr(self, "otb_text"): + self.otb_text.grid_forget() if self.filetype.get() not in ["CUSTOMCSV"]: if hasattr(self, "csv_entry"): self.csv_entry.grid_forget() @@ -963,53 +913,22 @@ def on_filetype_change(self, *args): # Add a combobox containing the OTB extension factors # in case an OTB file is loaded if self.filetype.get() == "OTB": - self.extension_factor = StringVar() - self.otb_combobox = ctk.CTkComboBox( + self.extension_factor = int(settings.emg_from_otb__extension_factor) + self.otb_text = ctk.CTkLabel( self.left, - values=[ - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", - "29", - "30", - "31", - "32", - ], - width=8, - variable=self.extension_factor, - state="readonly", + text="Verify openfiles settings!", + font=("Segoe UI", 12, "bold"), + text_color="black", ) - self.otb_combobox.grid(column=0, row=2, sticky=(W, E), padx=5) - self.otb_combobox.set("Extension Factor") + self.otb_text.grid(column=0, row=2, sticky=(W, E), padx=5) elif self.filetype.get() in ["CUSTOMCSV", "CUSTOMCSV_REFSIG"]: - self.fsamp = StringVar(value="Fsamp") - self.csv_entry = ctk.CTkEntry( + self.fsamp = int(settings.emg_from_customcsv__sampling_frequency) + self.csv_entry = ctk.CTkLabel( self.left, - width=8, - textvariable=self.fsamp, + text="Verify openfiles settings!", + font=("Segoe UI", 12, "bold"), + text_color="black", ) self.csv_entry.grid(column=0, row=2, sticky=(W, E), padx=5) @@ -1131,14 +1050,18 @@ def reset_analysis(self): rawemg_filepath=self.file_path, mus_directory=self.mus_path ) # Update Filespecs - ctk.CTkLabel( - self.left, text=str(len(self.resdict["RAW_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel( - self.left, text=str(self.resdict["NUMBER_OF_MUS"]) - ).grid(column=2, row=3, sticky=(W, E)) - ctk.CTkLabel(self.left, text=str(self.resdict["EMG_LENGTH"])).grid( - column=2, row=4, sticky=(W, E) + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), ) else: @@ -1153,14 +1076,18 @@ def reset_analysis(self): ) # Recondifgure labels for refsig - ctk.CTkLabel( - self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E)) - ctk.CTkLabel(self.left, text="NA").grid( - column=2, row=3, sticky=(W, E) + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), ) - ctk.CTkLabel(self.left, text=" ").grid( - column=2, row=4, sticky=(W, E) + self.n_of_mus.configure( + text="N of MUs: ", + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: ", + font=("Segoe UI", 15, "bold"), ) # Update Plot @@ -1170,7 +1097,7 @@ def reset_analysis(self): # Clear frame for output if hasattr(self, "terminal"): self.terminal = ttk.LabelFrame( - self.master, text="Result Output", height=100, relief="ridge" + self, text="Result Output", height=100, relief="ridge" ) self.terminal.grid( column=0, @@ -1218,19 +1145,22 @@ def in_gui_plotting(self, resdict, plot="idr"): "DELSYS_REFSIG", ]: self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=True + emgfile=resdict, showimmediately=False, tight_layout=False ) elif plot == "idr": self.fig = openhdemg.plot_idr( - emgfile=resdict, showimmediately=False, tight_layout=True + emgfile=resdict, + showimmediately=False, + tight_layout=False, + # figsize=[900 * (2.54 / 96), 800 * (2.54 / 96)], ) elif plot == "refsig_fil": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=True + emgfile=resdict, showimmediately=False, tight_layout=False ) elif plot == "refsig_off": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=True + emgfile=resdict, showimmediately=False, tight_layout=False ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) @@ -1263,7 +1193,7 @@ def display_results(self, input_df): """ # Create frame for output self.terminal = ttk.LabelFrame( - self.master, + self, text="Result Output", height=100, relief="ridge", @@ -1294,12 +1224,12 @@ def display_results(self, input_df): def run_main(): # Run GUI upon calling if __name__ == "__main__": - root = Tk() - emgGUI(root) - root.mainloop() + app = emgGUI() + app._state_before_windows_set_titlebar_color = "zoomed" + app.mainloop() if __name__ == "__main__": - root = Tk() - emgGUI(root) - root.mainloop() + app = emgGUI() + app._state_before_windows_set_titlebar_color = "zoomed" + app.mainloop() diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index a0ad3bc..bb7f128 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -16,17 +16,6 @@ import numpy as np -# These graphic parameters are updated only after restarting the GUI -# -# --------------------------------- GUI LOOK ---------------------------------- -gui_background_color = "LightBlue4" -# Options are #TODO Paul please explain what colors can be used, I will add these in the tutorial -#TODO we also must rename this file in something more specific like gui_settings.py -# Please insert gui_background_color were needed in the .py GUI files, I didn't do that - - -# The following parameters are updated without restarting the GUI -# # --------------------------------- openfiles --------------------------------- # in emg_from_demuse() # DONE and it works @@ -36,6 +25,7 @@ emg_from_otb__refsig = [True, "fullsampled"] emg_from_otb__extras = None emg_from_otb__ignore_negative_ipts = False +emg_from_otb__extension_factor = 8 # TODO Giacomo check please # in refsig_from_otb() refsig_from_otb__refsig = "fullsampled" @@ -58,6 +48,7 @@ emg_from_customcsv__accuracy = "ACCURACY" emg_from_customcsv__extras = "EXTRAS" emg_from_customcsv__ied = 8 +emg_from_customcsv__sampling_frequency = 1000 # TODO Giacomo check please # TODO in main window and in advanced tools, when selecting OTB, delsys and custom CSV (both emgifle and refsig) write to check settings file # in refsig_from_customcsv() @@ -68,7 +59,7 @@ save_json_emgfile__compresslevel = 4 # DONE and it works -# ---------------------------------- analysis --------------------------------- # DONE and it works +# ---------------------------------- analysis --------------------------------- # in compute_thresholds() compute_thresholds__n_firings = 1 @@ -80,7 +71,7 @@ basic_mus_properties__constrain_pulses = [True, 3] -# ----------------------------------- tools ----------------------------------- # DONE and it works +# ----------------------------------- tools ----------------------------------- # in resize_emgfile() resize_emgfile__how = "ref_signal" From 676c752b3de616cea3329731d98719eb4be40ecc Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:53:22 +0100 Subject: [PATCH 43/57] Advancing with settings and formatting Remains only advanced_analyses.py to do. Everything else is working fine although I will have to perform a deep testing. Still, the icon does not display on Windows. openhdemg.json has been renamed for clarity. --- .../{openhdemg.json => gui_color_theme.json} | 0 openhdemg/gui/gui_files/test.pdf | Bin 41213 -> 0 bytes openhdemg/gui/gui_modules/analyse_force.py | 67 ++- openhdemg/gui/gui_modules/edit_mus.py | 82 +-- openhdemg/gui/gui_modules/edit_refsig.py | 171 +++--- openhdemg/gui/gui_modules/gui_helpers.py | 7 + openhdemg/gui/gui_modules/gui_plotting.py | 13 +- openhdemg/gui/gui_modules/mu_properties.py | 2 +- openhdemg/gui/openhdemg_gui.py | 532 ++++++++++-------- openhdemg/gui/settings.py | 12 +- 10 files changed, 512 insertions(+), 374 deletions(-) rename openhdemg/gui/gui_files/{openhdemg.json => gui_color_theme.json} (100%) delete mode 100644 openhdemg/gui/gui_files/test.pdf diff --git a/openhdemg/gui/gui_files/openhdemg.json b/openhdemg/gui/gui_files/gui_color_theme.json similarity index 100% rename from openhdemg/gui/gui_files/openhdemg.json rename to openhdemg/gui/gui_files/gui_color_theme.json diff --git a/openhdemg/gui/gui_files/test.pdf b/openhdemg/gui/gui_files/test.pdf deleted file mode 100644 index 882cbe1ed264c8cb7b6ca2bfbaee6db3d9050a9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41213 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 diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index 3961092..dccb53e 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -13,19 +13,21 @@ class AnalyseForce: """ A class for conducting force analysis in an openhdemg GUI application. - This class provides a window for analyzing force signals. It includes functionalities - for calculating Maximum Voluntary Contraction (MVC) and Rate of Force Development (RFD). - The class is activated through the "Analyse Force" button in the main GUI window. + This class provides a window for analyzing force signals. It includes + functionalities for calculating Maximum Voluntary Contraction (MVC) and + Rate of Force Development (RFD). The class is activated through the + "Analyse Force" button in the main GUI window. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this AnalyseForce - instance belongs. + The parent widget, typically the main application window, to which + this AnalyseForce instance belongs. head : CTkToplevel The top-level widget for the Force Analysis window. rfdms : StringVar - Tkinter StringVar to store the milliseconds value for Rate of Force Development (RFD) analysis. + Tkinter StringVar to store the milliseconds value for Rate of Force + Development (RFD) analysis. Methods ------- @@ -34,7 +36,8 @@ class AnalyseForce: get_mvc(self) Calculate and display the Maximum Voluntary Contraction (MVC). get_rfd(self) - Calculate and display the Rate of Force Development (RFD) over specified milliseconds. + Calculate and display the Rate of Force Development (RFD) over + specified milliseconds. Examples -------- @@ -44,34 +47,35 @@ class AnalyseForce: Notes ----- - The class is designed to be a part of a larger GUI application and interacts with force - signal data accessible via the `parent` widget. + The class is designed to be a part of a larger GUI application and + interacts with force signal data accessible via the `parent` widget. """ def __init__(self, parent): """ Initialize a new instance of the AnalyseForce class. - This method sets up the GUI components for the Force Analysis Window. It includes buttons - for calculating MVC and RFD, and an entry field for specifying RFD milliseconds. The method - configures and places various widgets such as labels, buttons, and entry fields in a grid + This method sets up the GUI components for the Force Analysis Window. + It includes buttons for calculating MVC and RFD, and an entry field + for specifying RFD milliseconds. The method configures and places + various widgets such as labels, buttons, and entry fields in a grid layout for user interaction. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this AnalyseForce - instance belongs. The parent is used for accessing shared resources and data. + The parent widget, typically the main application window, to which + this AnalyseForce instance belongs. The parent is used for + accessing shared resources and data. Raises ------ AttributeError - If certain widgets or properties are not properly instantiated due to missing - parent configurations or resources. - + If certain widgets or properties are not properly instantiated due + to missing parent configurations or resources. """ - # Initialize parent and load parent settings + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() @@ -98,22 +102,30 @@ def __init__(self, parent): self.head.rowconfigure(row, weight=1) # Get MVC - get_mvf = ctk.CTkButton(self.head, text="Get MVC", command=self.get_mvc) + get_mvf = ctk.CTkButton( + self.head, text="Get MVC", command=self.get_mvc, + ) get_mvf.grid(column=0, row=1, sticky=(W, E), padx=5, pady=5) # Get RFD separator1 = ttk.Separator(self.head, orient="horizontal") - separator1.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) + separator1.grid( + column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5, + ) ctk.CTkLabel( self.head, text="RFD miliseconds", font=("Segoe UI", 18, "bold") ).grid(column=1, row=3, sticky=(W, E), padx=5, pady=5) - get_rfd = ctk.CTkButton(self.head, text="Get RFD", command=self.get_rfd) + get_rfd = ctk.CTkButton( + self.head, text="Get RFD", command=self.get_rfd, + ) get_rfd.grid(column=0, row=4, sticky=(W, E), padx=5, pady=5) self.rfdms = StringVar() - milisecond = ctk.CTkEntry(self.head, width=100, textvariable=self.rfdms) + milisecond = ctk.CTkEntry( + self.head, width=100, textvariable=self.rfdms, + ) milisecond.grid(column=1, row=4, sticky=(W, E), padx=5, pady=5) self.rfdms.set("50,100,150,200") @@ -135,6 +147,7 @@ def get_mvc(self): -------- get_mvc in the library """ + try: # get MVC mvc = openhdemg.get_mvc(emgfile=self.parent.resdict) @@ -146,12 +159,14 @@ def get_mvc(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("Make sure a file is loaded."), ) def get_rfd(self): """ - Instance method to calculate RFD at specified timepoints based on user selection. + Instance method to calculate RFD at specified timepoints based on user + selection. Executed when button "Get RFD" in Analyze Force window is pressed. The Results of the analysis are displayed in the results terminal. @@ -165,6 +180,7 @@ def get_rfd(self): -------- get_rfd in library """ + try: # Define list for RFD computation ms = str(self.rfdms.get()) @@ -181,5 +197,6 @@ def get_rfd(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("Make sure a file is loaded."), ) diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index f5511d8..e876665 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -12,22 +12,24 @@ class MURemovalWindow: """ A class for managing the removal of motor units (MUs) in a GUI application. - This class creates a window that offers options to select and remove specific MUs. - It is activated from the main GUI window and is intended to provide functionalities - for manipulating motor unit data. The class raises an AttributeError if it is instantiated - without a loaded file for analysis. + This class creates a window that offers options to select and remove + specific MUs. It is activated from the main GUI window and is intended to + provide functionalities for manipulating motor unit data. The class raises + an AttributeError if it is instantiated without a loaded file for analysis. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this MURemovalWindow - instance belongs. + The parent widget, typically the main application window, to which + this MURemovalWindow instance belongs. resdict : dict - A dictionary containing relevant data and settings, including the number of MUs. + A dictionary containing relevant data and settings, including the + number of MUs. head : CTkToplevel The top-level widget for the Motor Unit Removal window. mu_to_remove : StringVar - Tkinter StringVar to store the ID of the motor unit selected for removal. + Tkinter StringVar to store the ID of the motor unit selected for + removal. Methods ------- @@ -52,32 +54,35 @@ class MURemovalWindow: Notes ----- - The class is designed to interact with the data structure provided by the `resdict` - attribute, which is expected to contain specific keys and values relevant to the MU analysis. - + The class is designed to interact with the data structure provided by the + `resdict` attribute, which is expected to contain specific keys and values + relevant to the MU analysis. """ def __init__(self, parent): """ Initialize a new instance of the MURemovalWindow class. - This method sets up the GUI components for the Motor Unit Removal Window. It includes - a dropdown menu to select a motor unit (MU) for removal and buttons to remove either - the selected MU or all empty MUs. The method configures and places various widgets such - as labels, comboboxes, and buttons in a grid layout for user interaction. + This method sets up the GUI components for the Motor Unit Removal + Window. It includes a dropdown menu to select a motor unit (MU) for + removal and buttons to remove either the selected MU or all empty MUs. + The method configures and places various widgets such as labels, + comboboxes, and buttons in a grid layout for user interaction. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this MURemovalWindow - instance belongs. The parent is used for accessing shared resources and data. + The parent widget, typically the main application window, to which + this MURemovalWindow instance belongs. The parent is used for + accessing shared resources and data. Raises ------ AttributeError - If certain widgets or properties are not properly instantiated due to missing - parent configurations or resources. + If certain widgets or properties are not properly instantiated due + to missing parent configurations or resources. """ + try: # Initialize parent and load parent settings self.parent = parent @@ -89,7 +94,9 @@ def __init__(self, parent): self.head.title("Motor Unit Removal Window") # Set the icon for the window - head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + head_path = os.path.dirname( + os.path.dirname(os.path.abspath(__file__)) + ) iconpath = head_path + "/gui_files/Icon2.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): @@ -144,24 +151,28 @@ def __init__(self, parent): except AttributeError as e: self.head.destroy() show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("Make sure a file is loaded."), ) def remove(self): """ - Instance method that actually removes a selected motor unit based on user specification. + Instance method that actually removes a selected motor unit based on + user specification. - Executed when button "Remove MU" in Motor Unit Removal Window is pressed. - The emgfile and the plot are subsequently updated. + Executed when button "Remove MU" in Motor Unit Removal Window is + pressed. The emgfile and the plot are subsequently updated. See Also -------- delete_mus in library. """ + try: # Get resdict with MU removed self.parent.resdict = openhdemg.delete_mus( - emgfile=self.parent.resdict, munumber=int(self.mu_to_remove.get()) + emgfile=self.parent.resdict, + munumber=int(self.mu_to_remove.get()), ) # Upate MU number self.parent.n_of_mus.configure( @@ -174,7 +185,8 @@ def remove(self): removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] removed_mu_value = list(map(str, removed_mu_value)) removed_mu = ctk.CTkComboBox( - self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value + self.head, width=10, variable=self.mu_to_remove, + values=removed_mu_value, ) removed_mu.configure(state="readonly") removed_mu.grid( @@ -187,23 +199,27 @@ def remove(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("Make sure a file is loaded."), ) def remove_empty(self): """ Instance method that removes all empty MUs. - Executed when button "Remove empty MUs" in Motor Unit Removal Window is pressed. - The emgfile and the plot are subsequently updated. + Executed when button "Remove empty MUs" in Motor Unit Removal Window + is pressed. The emgfile and the plot are subsequently updated. See Also -------- delete_empty_mus in library. """ + try: # Get resdict with MU removed - self.parent.resdict = openhdemg.delete_empty_mus(self.parent.resdict) + self.parent.resdict = openhdemg.delete_empty_mus( + self.parent.resdict, + ) # Upate MU number self.parent.n_of_mus.configure( @@ -215,7 +231,8 @@ def remove_empty(self): removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] removed_mu_value = list(map(str, removed_mu_value)) removed_mu = ctk.CTkComboBox( - self.head, width=10, variable=self.mu_to_remove, values=removed_mu_value + self.head, width=10, variable=self.mu_to_remove, + values=removed_mu_value, ) removed_mu.configure(state="readonly") removed_mu.grid( @@ -228,5 +245,6 @@ def remove_empty(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("Make sure a file is loaded."), ) diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_refsig.py index c1f9b49..187baf4 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_refsig.py @@ -12,45 +12,55 @@ class EditRefsig: """ A class to manage editing of the reference signal in a GUI application. - This class creates a window that offers various options for editing the reference signal. - It includes functionalities for filtering the signal, removing offset, converting the signal, - and transforming it to a percentage value. The class is instantiated when the "RefSig Editing" + This class creates a window that offers various options for editing the + reference signal. It includes functionalities for filtering the signal, + removing offset, converting the signal, and transforming it to a + percentage value. The class is instantiated when the "RefSig Editing" button in the master GUI window is pressed. Attributes ---------- parent : object - The parent widget, typically the main application window, to which this EditRefsig - instance belongs. + The parent widget, typically the main application window, to which + this EditRefsig instance belongs. head : CTkToplevel The top-level widget for the Reference Signal Editing window. filter_order : StringVar - Tkinter StringVar to store the filter order for reference signal filtering. + Tkinter StringVar to store the filter order for reference signal + filtering. cutoff_freq : StringVar - Tkinter StringVar to store the cutoff frequency for reference signal filtering. + Tkinter StringVar to store the cutoff frequency for reference signal + filtering. offsetval : StringVar - Tkinter StringVar to store the offset value to be removed from the reference signal. + Tkinter StringVar to store the offset value to be removed from the + reference signal. auto_eval : StringVar Tkinter StringVar to store the value for automatic offset evaluation. convert : StringVar - Tkinter StringVar to store the operation (Multiply/Divide) for reference signal conversion. + Tkinter StringVar to store the operation (Multiply/Divide) for + reference signal conversion. convert_factor : DoubleVar Tkinter DoubleVar to store the factor for reference signal conversion. mvc_value : DoubleVar - Tkinter DoubleVar to store the MVC (Maximum Voluntary Contraction) value for percentage conversion. + Tkinter DoubleVar to store the MVC (Maximum Voluntary Contraction) + value for percentage conversion. Methods ------- __init__(self, parent) Initialize a new instance of the EditRefsig class. filter_refsig(self) - Apply filtering to the reference signal based on the specified order and cutoff frequency. + Apply filtering to the reference signal based on the specified order + and cutoff frequency. remove_offset(self) - Remove or adjust the offset of the reference signal based on the specified value or automatic evaluation. + Remove or adjust the offset of the reference signal based on the + specified value or automatic evaluation. convert_refsig(self) - Convert the reference signal using the specified operation (Multiply/Divide) and factor. + Convert the reference signal using the specified operation + (Multiply/Divide) and factor. to_percent(self) - Convert the reference signal to a percentage value based on the specified MVC value. + Convert the reference signal to a percentage value based on the + specified MVC value. Examples -------- @@ -60,33 +70,37 @@ class EditRefsig: Notes ----- - This class relies on the `ctk` and `ttk` modules from the `tkinter` library. The class is designed - to be instantiated from within a larger GUI application and operates on the reference signal data - that is accessible via the `parent` widget. - + This class relies on the `ctk` and `ttk` modules from the `tkinter` + library. The class is designed to be instantiated from within a larger GUI + application and operates on the reference signal data that is accessible + via the `parent` widget. """ def __init__(self, parent): """ Initialize a new instance of the EditRefsig class. - This method sets up the GUI components for the Reference Signal Editing Window. It includes - controls for filtering the reference signal, removing its offset, converting it, and - transforming it to a percentage value. The method configures and places various widgets - such as labels, entries, buttons, and combo boxes in a grid layout for user interaction. + This method sets up the GUI components for the Reference Signal + Editing Window. It includes controls for filtering the reference + signal, removing its offset, converting it, and transforming it to a + percentage value. The method configures and places various widgets + such as labels, entries, buttons, and combo boxes in a grid layout for + user interaction. Parameters ---------- parent : object - The parent widget, typically the main application window, to which this EditRefsig - instance belongs. The parent is used for accessing shared resources and data. + The parent widget, typically the main application window, to which + this EditRefsig instance belongs. The parent is used for accessing + shared resources and data. Raises ------ AttributeError - If certain widgets or properties are not properly instantiated due to missing - parent configurations or resources. + If certain widgets or properties are not properly instantiated due + to missing parent configurations or resources. """ + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() @@ -115,11 +129,11 @@ def __init__(self, parent): # Filter Refsig # Define Labels ctk.CTkLabel( - self.head, text="Filter Order", font=("Segoe UI", 18, "bold") + self.head, text="Filter Order", font=("Segoe UI", 18, "bold"), ).grid(column=1, row=0, sticky=(W, E)) - ctk.CTkLabel(self.head, text="Cutoff Freq", font=("Segoe UI", 18, "bold")).grid( - column=2, row=0, sticky=(W, E) - ) + ctk.CTkLabel( + self.head, text="Cutoff Freq", font=("Segoe UI", 18, "bold"), + ).grid(column=2, row=0, sticky=(W, E)) # Fiter button basic = ctk.CTkButton( self.head, @@ -128,18 +142,24 @@ def __init__(self, parent): ) basic.grid(column=0, row=1, sticky=W) self.filter_order = StringVar() - order = ctk.CTkEntry(self.head, width=100, textvariable=self.filter_order) + order = ctk.CTkEntry( + self.head, width=100, textvariable=self.filter_order, + ) order.grid(column=1, row=1) self.filter_order.set(4) self.cutoff_freq = StringVar() - cutoff = ctk.CTkEntry(self.head, width=100, textvariable=self.cutoff_freq) + cutoff = ctk.CTkEntry( + self.head, width=100, textvariable=self.cutoff_freq, + ) cutoff.grid(column=2, row=1) self.cutoff_freq.set(15) # Remove offset of reference signal separator2 = ttk.Separator(self.head, orient="horizontal") - separator2.grid(column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5) + separator2.grid( + column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5, + ) ctk.CTkLabel( self.head, text="Offset Value", font=("Segoe UI", 18, "bold") @@ -157,7 +177,9 @@ def __init__(self, parent): basic2.grid(column=0, row=4, sticky=W) self.offsetval = StringVar() - offset = ctk.CTkEntry(self.head, width=100, textvariable=self.offsetval) + offset = ctk.CTkEntry( + self.head, width=100, textvariable=self.offsetval, + ) offset.grid(column=1, row=4) self.offsetval.set(4) @@ -167,26 +189,31 @@ def __init__(self, parent): self.auto_eval.set(0) separator3 = ttk.Separator(self.head, orient="horizontal") - separator3.grid(column=0, columnspan=3, row=5, sticky=(W, E), padx=5, pady=5) + separator3.grid( + column=0, columnspan=3, row=5, sticky=(W, E), padx=5, pady=5, + ) # Convert Reference signal - ctk.CTkLabel(self.head, text="Operator", font=("Segoe UI", 18, "bold")).grid( - column=1, row=6, sticky=(W, E) - ) - ctk.CTkLabel(self.head, text="Factor", font=("Segoe UI", 18, "bold")).grid( - column=2, row=6, sticky=(W, E) - ) + ctk.CTkLabel( + self.head, text="Operator", font=("Segoe UI", 18, "bold"), + ).grid(column=1, row=6, sticky=(W, E)) + ctk.CTkLabel( + self.head, text="Factor", font=("Segoe UI", 18, "bold"), + ).grid(column=2, row=6, sticky=(W, E)) self.convert = StringVar() convert = ctk.CTkComboBox( - self.head, width=100, variable=self.convert, values=("Multiply", "Divide") + self.head, width=100, variable=self.convert, + values=("Multiply", "Divide"), ) convert.configure(state="readonly") convert.grid(column=1, row=7) self.convert.set("Multiply") self.convert_factor = DoubleVar() - factor = ctk.CTkEntry(self.head, width=100, textvariable=self.convert_factor) + factor = ctk.CTkEntry( + self.head, width=100, textvariable=self.convert_factor, + ) factor.grid(column=2, row=7) self.convert_factor.set(2.5) @@ -198,12 +225,14 @@ def __init__(self, parent): convert_button.grid(column=0, row=7, sticky=W) separator3 = ttk.Separator(self.head, orient="horizontal") - separator3.grid(column=0, columnspan=3, row=8, sticky=(W, E), padx=5, pady=5) + separator3.grid( + column=0, columnspan=3, row=8, sticky=(W, E), padx=5, pady=5, + ) # Convert to percentage - ctk.CTkLabel(self.head, text="MVC Value", font=("Segoe UI", 18, "bold")).grid( - column=1, row=9, sticky=(W, E) - ) + ctk.CTkLabel( + self.head, text="MVC Value", font=("Segoe UI", 18, "bold"), + ).grid(column=1, row=9, sticky=(W, E)) percent_button = ctk.CTkButton( self.head, @@ -232,8 +261,8 @@ def filter_refsig(self): """ Instance method that filters the refig based on user selected specs. - Executed when button "Filter Refsig" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. + Executed when button "Filter Refsig" in Reference Signal Editing + Window is pressed. The emgfile and the GUI plot are updated. Raises ------ @@ -244,6 +273,7 @@ def filter_refsig(self): -------- filter_refsig in library. """ + try: # Filter refsig self.parent.resdict = openhdemg.filter_refsig( @@ -252,19 +282,22 @@ def filter_refsig(self): cutoff=int(self.cutoff_freq.get()), ) # Plot filtered Refsig - self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_fil") + self.parent.in_gui_plotting( + resdict=self.parent.resdict, plot="refsig_fil", + ) except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + parent=self, error=e, + solution=str("Make sure a Refsig file is loaded."), ) def remove_offset(self): """ Instance Method that removes user specified/selected Refsig offset. - Executed when button "Remove offset" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. + Executed when button "Remove offset" in Reference Signal Editing + Window is pressed. The emgfile and the GUI plot are updated. Raises ------ @@ -275,6 +308,7 @@ def remove_offset(self): -------- remove_offset in library. """ + try: # Remove offset self.parent.resdict = openhdemg.remove_offset( @@ -283,11 +317,14 @@ def remove_offset(self): auto=int(self.auto_eval.get()), ) # Update Plot - self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") + self.parent.in_gui_plotting( + resdict=self.parent.resdict, plot="refsig_off", + ) except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + parent=self, error=e, + solution=str("Make sure a Refsig file is loaded."), ) except ValueError as e: @@ -301,8 +338,8 @@ def convert_refsig(self): """ Instance Method that converts Refsig by multiplication or division. - Executed when button "Convert" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. + Executed when button "Convert" in Reference Signal Editing Window is + pressed. The emgfile and the GUI plot are updated. Raises ------ @@ -310,8 +347,8 @@ def convert_refsig(self): When no reference signal file is available ValueError When invalid conversion factor is specified - """ + try: if self.convert.get() == "Multiply": self.parent.resdict["REF_SIGNAL"] = ( @@ -323,11 +360,14 @@ def convert_refsig(self): ) # Update Plot - self.parent.in_gui_plotting(resdict=self.parent.resdict, plot="refsig_off") + self.parent.in_gui_plotting( + resdict=self.parent.resdict, plot="refsig_off", + ) except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + parent=self, error=e, + solution=str("Make sure a Refsig file is loaded."), ) except ValueError as e: @@ -339,10 +379,11 @@ def convert_refsig(self): def to_percent(self): """ - Instance Method that converts Refsig to a percentag value. Should only be used when the Refsig is in absolute values. + Instance Method that converts Refsig to a percentag value. Should only + be used when the Refsig is in absolute values. - Executed when button "To Percen" in Reference Signal Editing Window is pressed. - The emgfile and the GUI plot are updated. + Executed when button "To Percen" in Reference Signal Editing Window is + pressed. The emgfile and the GUI plot are updated. Raises ------ @@ -351,6 +392,7 @@ def to_percent(self): ValueError When invalid conversion factor is specified """ + try: self.parent.resdict["REF_SIGNAL"] = ( self.parent.resdict["REF_SIGNAL"] * 100 @@ -360,7 +402,8 @@ def to_percent(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a Refsig file is loaded.") + parent=self, error=e, + solution=str("Make sure a Refsig file is loaded."), ) except ValueError as e: diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index f21bf93..160e2ef 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -130,6 +130,13 @@ def resize_file(self): solution=str("Make sure a file is loaded."), ) + except ValueError as e: + show_error_dialog( + parent=self, + error=e, + solution=str("Verify settings for resize_emgfile()."), + ) + def export_to_excel(self): """ Instnace method to export any prior analysis results. Results are diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 9c288d1..540cfcf 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -194,7 +194,7 @@ def __init__(self, parent): self.head, text="Figure size in cm (h,w)", font=("Segoe UI", 18, "bold"), - ).grid(column=0, row=2) + ).grid(column=0, row=2, sticky=W) self.size_fig = StringVar() fig_entry = ctk.CTkEntry(self.head, width=100, textvariable=self.size_fig) self.size_fig.set("20,15") @@ -297,14 +297,13 @@ def __init__(self, parent): ).grid(row=0, column=3, sticky=(W)) self.mat_code = StringVar() - # NOTE the matrix codes can only be those accepted by sort_rawemg(). matrix_code_values = ("GR08MM1305", "GR04MM1305", "GR10MM0808", self.parent.settings.delsys_sensor_label, "None") matrix_code_values = ( + "Custom order", + "None", "GR08MM1305", "GR04MM1305", "GR10MM0808", "Trigno Galileo Sensor", - "Custom order", - "None", ) matrix_code = ctk.CTkComboBox( self.head, @@ -314,10 +313,10 @@ def __init__(self, parent): state="readonly", ) matrix_code.grid(row=0, column=4, sticky=(W, E)) - self.mat_code.set("GR08MM1305") + self.mat_code.set("Custom order") # Trace matrix code value - self.mat_code.trace_add("write", self.on_matrix_none) + self.mat_code.trace_add("write", self.on_matrix_none) #TODO # Matrix Orientation ctk.CTkLabel( @@ -505,7 +504,7 @@ def on_matrix_none(self, *args): # Column entry only when specified matrix code self.row_cols_entry = ctk.CTkEntry( self.head, - width=30, + width=80, textvariable=self.matrix_rc, ) self.row_cols_entry.grid(row=0, column=6, sticky=W, padx=5) diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index dad39f2..041d85f 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -101,8 +101,8 @@ def __init__(self, parent): and custom widgets (like CTkLabel, CTkEntry, CTkButton, CTkComboBox). Each widget is configured with specific properties like size, color, and variable bindings and placed in a grid layout. - """ + # Initialize parent and load parent settings self.parent = parent self.parent.load_settings() diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 7429a1a..04d79da 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -50,100 +50,99 @@ matplotlib.use("TkAgg") ctk.set_default_color_theme( - os.path.dirname(os.path.abspath(__file__)) + "/gui_files/openhdemg.json" + os.path.dirname(os.path.abspath(__file__)) + + "/gui_files/gui_color_theme.json" ) class emgGUI(ctk.CTk): """ - This class is used to create a graphical user interface for the openhdemg - library. - - Within this class and corresponding childs, most functionalities of the - openhdemg library are packed in a GUI. However, the library is more - comprehensive and much more adaptable to the users needs. - - Attributes - ---------- - self.canvas : matplotlib.backends.backend_tkagg - Canvas for plotting figures inside the GUI. - self.channels : int or list - The channel (int) or channels (list of int) to plot. - The list can be passed as a manually-written with: "0,1,2,3,4,5...,n", - channels is expected to be with base 0. - self.fig : matplotlib.figure - Figure to be plotted on Canvas. - self.filename : str - String and name of the file to be analysed. - self.filepath : str - String containing the path to EMG file selected for analysis. - self.filetype : str - String containing the filetype of import EMG file. - Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", - "CUSTOMCSV", "CUSTOMCSV_REFSIG". - self.left : tk.frame - Left frame inside of self that contains all buttons and filespecs. + This class is used to create a graphical user interface for the openhdemg + library. + + Within this class and corresponding childs, most functionalities of the + openhdemg library are packed in a GUI. However, the library is more + comprehensive and much more adaptable to the users needs. + + Attributes + ---------- + self.canvas : matplotlib.backends.backend_tkagg + Canvas for plotting figures inside the GUI. + self.channels : int or list + The channel (int) or channels (list of int) to plot. + The list can be passed as a manually-written with: "0,1,2,3,4,5...,n", + channels is expected to be with base 0. + self.fig : matplotlib.figure + Figure to be plotted on Canvas. + self.filename : str + String and name of the file to be analysed. + self.filepath : str + String containing the path to EMG file selected for analysis. + self.filetype : str + String containing the filetype of import EMG file. + Filetype can be "OPENHDEMG", "OTB", "DEMUSE", "OTB_REFSIG", + "CUSTOMCSV", "CUSTOMCSV_REFSIG". + self.left : tk.frame + Left frame inside of self that contains all buttons and filespecs. self.logo : - String containing the path to image file containing logo of openhdemg. - self.logo_canvas : tk.canvas - Canvas to display logo of Open_HG-EMG when openend. - self.self: tk - TK self window containing all widget children for this GUI. - self.resdict : dict - Dictionary derived from input EMG file for further analysis. - self.right : tk.frame - Left frame inside of self that contains plotting canvas. - self.terminal : ttk.Labelframe - Tkinter labelframe that is used to display the results table in the - GUI. - self.info : tk.PhotoImage - Information Icon displayed in GUI. - self.online : tk.Photoimage - Online Icon displayed in GUI. - self.redirect : tk.PhotoImage - Redirection Icon displayed in GUI. - self.contact : tk.PhotoImage - Contact Icon displayed in GUI. - self.cite : tk.PhotoImage - Citation Icon displayed in GUI. - self.extension_factor : tk.StringVar() - Stringvariable containing the OTB extension factor value. - - Methods - ------- - __init__(self) - Initializes GUI class and main GUI window (self). - get_file_input() - Gets emgfile location and respective file is loaded. - Executed when button "Load File" in self GUI window pressed. - save_emgfile() - Saves the edited emgfile dictionary to a .json file. - Executed when button "Save File" in self GUI window pressed. - reset_analysis() - Resets the whole analysis, restores the original input file and the - graph. - Executed when button "Reset analysis" in self GUI window pressed. - in_gui_plotting() - Method used for creating plot inside the GUI (on the GUI canvas). - Executed when button "View MUs" in self GUI window pressed. - mu_analysis() - Opens seperate window to calculated specific motor unit properties. - Executed when button "MU properties" in self GUI window pressed. - display_results() - Method used to display result table containing analysis results. - - Notes - ----- - Please note that altough we created a GUI class, the included methods/ - instances are highly specific. We did not conceptualize the - methods/instances to be used seperately. Similar functionalities are - available in the library and were specifically coded to be used - seperately/singularly. - - Most instance methods of this class heavily rely on the functions provided - in the library. In the section "See Also" at each instance method, the - reader is referred to the corresponding function and extensive - documentation in the library. + String containing the path to image file containing logo of openhdemg. + self.logo_canvas : tk.canvas + Canvas to display logo of Open_HG-EMG when openend. + self.self: tk + TK self window containing all widget children for this GUI. + self.resdict : dict + Dictionary derived from input EMG file for further analysis. + self.right : tk.frame + Left frame inside of self that contains plotting canvas. + self.terminal : ttk.Labelframe + Tkinter labelframe that is used to display the results table in the + GUI. + self.info : tk.PhotoImage + Information Icon displayed in GUI. + self.online : tk.Photoimage + Online Icon displayed in GUI. + self.redirect : tk.PhotoImage + Redirection Icon displayed in GUI. + self.contact : tk.PhotoImage + Contact Icon displayed in GUI. + self.cite : tk.PhotoImage + Citation Icon displayed in GUI. + + Methods + ------- + __init__(self) + Initializes GUI class and main GUI window (self). + get_file_input() + Gets emgfile location and respective file is loaded. + Executed when button "Load File" in self GUI window pressed. + save_emgfile() + Saves the edited emgfile dictionary to a .json file. + Executed when button "Save File" in self GUI window pressed. + reset_analysis() + Resets the whole analysis, restores the original input file and the + graph. + Executed when button "Reset analysis" in self GUI window pressed. + in_gui_plotting() + Method used for creating plot inside the GUI (on the GUI canvas). + Executed when button "View MUs" in self GUI window pressed. + mu_analysis() + Opens seperate window to calculated specific motor unit properties. + Executed when button "MU properties" in self GUI window pressed. + display_results() + Method used to display result table containing analysis results. + + Notes + ----- + Please note that altough we created a GUI class, the included methods/ + instances are highly specific. We did not conceptualize the + methods/instances to be used seperately. Similar functionalities are + available in the library and were specifically coded to be used + seperately/singularly. + + Most instance methods of this class heavily rely on the functions provided + in the library. In the section "See Also" at each instance method, the + reader is referred to the corresponding function and extensive + documentation in the library. """ def __init__(self): @@ -163,7 +162,9 @@ def __init__(self): # Set up GUI self.title("openhdemg") master_path = os.path.dirname(os.path.abspath(__file__)) - ctk.set_default_color_theme(master_path + "/gui_files/openhdemg.json") + ctk.set_default_color_theme( + master_path + "/gui_files/gui_color_theme.json" + ) iconpath = master_path + "/gui_files/Icon2.ico" self.iconbitmap(iconpath) @@ -387,7 +388,8 @@ def __init__(self): logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) - self.logo_canvas.create_image( # TODO make resizable + # TODO make resizable and centre the logo + self.logo_canvas.create_image( 1000, 500, image=self.logo, @@ -597,7 +599,7 @@ def load_file(): # Load file self.resdict = openhdemg.emg_from_otb( filepath=self.file_path, - ext_factor=int(self.extension_factor.get()), + ext_factor=self.settings.emg_from_otb__ext_factor, refsig=self.settings.emg_from_otb__refsig, extras=self.settings.emg_from_otb__extras, ignore_negative_ipts=self.settings.emg_from_otb__ignore_negative_ipts, @@ -658,7 +660,11 @@ def load_file(): # load DELSYS self.resdict = openhdemg.emg_from_delsys( - rawemg_filepath=self.file_path, mus_directory=self.mus_path + rawemg_filepath=self.file_path, + mus_directory=self.mus_path, + emg_sensor_name=self.settings.emg_from_delsys__emg_sensor_name, + refsig_sensor_name=self.settings.emg_from_delsys__refsig_sensor_name, + filename_from=self.settings.emg_from_delsys__filename_from, ) # Add filespecs self.n_channels.configure( @@ -709,15 +715,16 @@ def load_file(): # Add filespecs self.n_channels.configure( text="N Channels: " - + str(len(self.resdict["RAW_SIGNAL"].columns)), + + str(len(self.resdict["REF_SIGNAL"].columns)), font=("Segoe UI", 15, ("bold")), ) self.n_of_mus.configure( - text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + text="N of MUs: N/A", font=("Segoe UI", 15, "bold"), ) self.file_length.configure( - text="File Length: " + str(self.resdict["EMG_LENGTH"]), + text="File Length: " + + str(len(self.resdict["REF_SIGNAL"].iloc[:, 0])), font=("Segoe UI", 15, "bold"), ) else: @@ -730,7 +737,15 @@ def load_file(): # load file self.resdict = openhdemg.emg_from_customcsv( filepath=self.file_path, - fsamp=float(self.fsamp.get()), + ref_signal=self.settings.emg_from_customcsv__ref_signal, + raw_signal=self.settings.emg_from_customcsv__raw_signal, + ipts=self.settings.emg_from_customcsv__ipts, + mupulses=self.settings.emg_from_customcsv__mupulses, + binary_mus_firing=self.settings.emg_from_customcsv__binary_mus_firing, + accuracy=self.settings.emg_from_customcsv__accuracy, + extras=self.settings.emg_from_customcsv__extras, + fsamp=self.settings.emg_from_customcsv__fsamp, + ied=self.settings.emg_from_customcsv__ied, ) # Add filespecs self.n_channels.configure( @@ -758,8 +773,8 @@ def load_file(): progress.grid_remove() progress.stop() - # This sections is used for refsig loading as they required not the - # the filespecs to be loaded. + # This sections is used for refsig loading as they do not + # require the filespecs to be loaded. else: if self.filetype.get() == "OTB_REFSIG": file_path = filedialog.askopenfilename( @@ -769,7 +784,9 @@ def load_file(): self.file_path = file_path # load refsig self.resdict = openhdemg.refsig_from_otb( - filepath=self.file_path + filepath=self.file_path, + refsig=self.settings.refsig_from_otb__refsig, + extras=self.settings.refsig_from_otb__extras, ) elif self.filetype.get() == "DELSYS_REFSIG": @@ -782,7 +799,8 @@ def load_file(): self.file_path = file_path # load DELSYS self.resdict = openhdemg.refsig_from_delsys( - filepath=self.file_path + filepath=self.file_path, + refsig_sensor_name=self.settings.refsig_from_delsys__refsig_sensor_name, ) elif self.filetype.get() == "CUSTOMCSV_REFSIG": @@ -794,7 +812,9 @@ def load_file(): # load refsig self.resdict = openhdemg.refsig_from_customcsv( filepath=self.file_path, - fsamp=float(self.fsamp.get()), + ref_signal=self.settings.refsig_from_customcsv__ref_signal, + extras=self.settings.refsig_from_customcsv__extras, + fsamp=self.settings.refsig_from_customcsv__fsamp, ) # Get filename @@ -804,15 +824,20 @@ def load_file(): # Add filename to label self.title(self.filename) - # Reconfigure labels for refsig - ctk.CTkLabel( - self.left, text=str(len(self.resdict["REF_SIGNAL"].columns)) - ).grid(column=2, row=2, sticky=(W, E), padx=5, pady=5) - ctk.CTkLabel(self.left, text="NA").grid( - column=2, row=3, sticky=(W, E), padx=5, pady=5 + # Add filespecs + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["REF_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), ) - ctk.CTkLabel(self.left, text=" ").grid( - column=2, row=4, sticky=(W, E), padx=5, pady=5 + self.n_of_mus.configure( + text="N of MUs: N/A", + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + + str(len(self.resdict["REF_SIGNAL"].iloc[:, 0])), + font=("Segoe UI", 15, "bold"), ) # End progress @@ -880,6 +905,9 @@ def load_file(): progress.stop() progress.grid_remove() + # Re-Load settings + self.load_settings() + # Indicate Progress progress = ctk.CTkProgressBar( self.left, @@ -895,42 +923,27 @@ def load_file(): def on_filetype_change(self, *args): """ - This function is called when the value of the filetype variable is changed. - When the filetype is set to "OTB", "CUSTOMCSV", "CUSTOMCSV_REFSIG" it will - create a second combobox on the grid at column 0 and row 2 and when the filetype - is set to something else it will remove the second combobox from the grid. + This function is called when the value of the filetype variable is + changed. If filetype is set to != "OPENHDEMG", it will create a second + combobox on the grid at column 0 and row 2 and when the filetype is + set to "OPENHDEMG" it will remove the second combobox from the grid. """ - if self.filetype.get() not in ["OTB"]: - if hasattr(self, "otb_text"): - self.otb_text.grid_forget() - if self.filetype.get() not in ["CUSTOMCSV"]: - if hasattr(self, "csv_entry"): - self.csv_entry.grid_forget() - if self.filetype.get() not in ["CUSTOMCSV_REFSIG"]: - if hasattr(self, "csv_entry"): - self.csv_entry.grid_forget() - - # Add a combobox containing the OTB extension factors - # in case an OTB file is loaded - if self.filetype.get() == "OTB": - self.extension_factor = int(settings.emg_from_otb__extension_factor) - self.otb_text = ctk.CTkLabel( - self.left, - text="Verify openfiles settings!", - font=("Segoe UI", 12, "bold"), - text_color="black", - ) - self.otb_text.grid(column=0, row=2, sticky=(W, E), padx=5) - elif self.filetype.get() in ["CUSTOMCSV", "CUSTOMCSV_REFSIG"]: - self.fsamp = int(settings.emg_from_customcsv__sampling_frequency) - self.csv_entry = ctk.CTkLabel( + # Make sure to forget all the previous labels + if hasattr(self, "verify_settings_text"): + self.verify_settings_text.grid_forget() + + if self.filetype.get() != "OPENHDEMG": + # Display the text: Verify openfiles settings! + self.verify_settings_text = ctk.CTkLabel( self.left, text="Verify openfiles settings!", font=("Segoe UI", 12, "bold"), text_color="black", ) - self.csv_entry.grid(column=0, row=2, sticky=(W, E), padx=5) + self.verify_settings_text.grid( + column=0, row=2, sticky=(W, E), padx=5, + ) def save_emgfile(self): """ @@ -987,6 +1000,9 @@ def save_file(): solution=str("Make sure a file is loaded."), ) + # Re-Load settings + self.load_settings() + # Indicate Progress progress = ctk.CTkProgressBar(self.left, mode="indeterminate") progress.grid(row=4, column=0) @@ -998,11 +1014,11 @@ def save_file(): def reset_analysis(self): """ - Instance method to restore the GUI to base data. Any analysis progress will be deleted by - reloading the original file. + Instance method to restore the GUI to base data. Any analysis progress + will be deleted by reloading the original file. - Executed when button "Reset Analysis" in master GUI window is pressed. The emgfile is - updated to its original state. + Executed when button "Reset Analysis" in master GUI window is pressed. + The emgfile is updated to its original state. Raises ------ @@ -1011,123 +1027,155 @@ def reset_analysis(self): FileNotFoundError When no file was loaded in the GUI. """ + # Get user input and check whether analysis wants to be truly resetted - if messagebox.askokcancel( + if not messagebox.askokcancel( title="Attention", message="Do you really want to reset the analysis?", icon="warning", ): - # user decided to rest analysis - try: - # reload original file - if self.filetype.get() in [ - "OTB", - "DEMUSE", - "OPENHDEMG", - "CUSTOMCSV", - "DELSYS", - ]: - if self.filetype.get() == "OTB": - self.resdict = openhdemg.emg_from_otb( - filepath=self.file_path, - ext_factor=int(self.extension_factor.get()), - ) - - elif self.filetype.get() == "DEMUSE": - self.resdict = openhdemg.emg_from_demuse( - filepath=self.file_path - ) + # user decided to not rest analysis + return - elif self.filetype.get() == "OPENHDEMG": - self.resdict = openhdemg.emg_from_json(filepath=self.file_path) + # user decided to rest analysis + try: + # reload original file + if self.filetype.get() in [ + "OTB", + "DEMUSE", + "OPENHDEMG", + "CUSTOMCSV", + "DELSYS", + ]: + if self.filetype.get() == "OTB": + self.resdict = openhdemg.emg_from_otb( + filepath=self.file_path, + ext_factor=self.settings.emg_from_otb__ext_factor, + refsig=self.settings.emg_from_otb__refsig, + extras=self.settings.emg_from_otb__extras, + ignore_negative_ipts=self.settings.emg_from_otb__ignore_negative_ipts, + ) - elif self.filetype.get() == "CUSTOMCSV": - self.resdict = openhdemg.emg_from_customcsv( - filepath=self.file_path - ) - elif self.filetype.get() == "DELSYS": - self.resdict = openhdemg.emg_from_delsys( - rawemg_filepath=self.file_path, mus_directory=self.mus_path - ) - # Update Filespecs - self.n_channels.configure( - text="N Channels: " - + str(len(self.resdict["RAW_SIGNAL"].columns)), - font=("Segoe UI", 15, ("bold")), + elif self.filetype.get() == "DEMUSE": + self.resdict = openhdemg.emg_from_demuse( + filepath=self.file_path, + ignore_negative_ipts=self.settings.emg_from_demuse__ignore_negative_ipts, ) - self.n_of_mus.configure( - text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), - font=("Segoe UI", 15, "bold"), + + elif self.filetype.get() == "OPENHDEMG": + self.resdict = openhdemg.emg_from_json(filepath=self.file_path) + + elif self.filetype.get() == "CUSTOMCSV": + self.resdict = openhdemg.emg_from_customcsv( + filepath=self.file_path, + ref_signal=self.settings.emg_from_customcsv__ref_signal, + raw_signal=self.settings.emg_from_customcsv__raw_signal, + ipts=self.settings.emg_from_customcsv__ipts, + mupulses=self.settings.emg_from_customcsv__mupulses, + binary_mus_firing=self.settings.emg_from_customcsv__binary_mus_firing, + accuracy=self.settings.emg_from_customcsv__accuracy, + extras=self.settings.emg_from_customcsv__extras, + fsamp=self.settings.emg_from_customcsv__fsamp, + ied=self.settings.emg_from_customcsv__ied, ) - self.file_length.configure( - text="File Length: " + str(self.resdict["EMG_LENGTH"]), - font=("Segoe UI", 15, "bold"), + elif self.filetype.get() == "DELSYS": + self.resdict = openhdemg.emg_from_delsys( + rawemg_filepath=self.file_path, + mus_directory=self.mus_path, + emg_sensor_name=self.settings.emg_from_delsys__emg_sensor_name, + refsig_sensor_name=self.settings.emg_from_delsys__refsig_sensor_name, + filename_from=self.settings.emg_from_delsys__filename_from, ) + # Update Filespecs + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["RAW_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: " + str(self.resdict["NUMBER_OF_MUS"]), + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + str(self.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) - else: - # load refsig - if self.filetype.get() == "OTB_REFSIG": - self.resdict = openhdemg.refsig_from_otb( - filepath=self.file_path - ) - else: # CUSTOMCSV_REFSIG - self.resdict = openhdemg.refsig_from_customcsv( - filepath=self.file_path - ) - - # Recondifgure labels for refsig - self.n_channels.configure( - text="N Channels: " - + str(len(self.resdict["RAW_SIGNAL"].columns)), - font=("Segoe UI", 15, ("bold")), + else: + # load refsig + if self.filetype.get() == "OTB_REFSIG": + self.resdict = openhdemg.refsig_from_otb( + filepath=self.file_path ) - self.n_of_mus.configure( - text="N of MUs: ", - font=("Segoe UI", 15, "bold"), + elif self.filetype.get() == "DELSYS_REFSIG": + self.resdict = openhdemg.refsig_from_delsys( + filepath=self.file_path, + refsig_sensor_name=self.settings.refsig_from_delsys__refsig_sensor_name, ) - self.file_length.configure( - text="File Length: ", - font=("Segoe UI", 15, "bold"), + elif self.filetype.get() == "CUSTOMCSV_REFSIG": + self.resdict = openhdemg.refsig_from_customcsv( + filepath=self.file_path, + ref_signal=self.settings.refsig_from_customcsv__ref_signal, + extras=self.settings.refsig_from_customcsv__extras, + fsamp=self.settings.refsig_from_customcsv__fsamp, ) - # Update Plot - if hasattr(self, "fig"): - self.in_gui_plotting(resdict=self.resdict) + # Reconfigure labels for refsig + self.n_channels.configure( + text="N Channels: " + + str(len(self.resdict["REF_SIGNAL"].columns)), + font=("Segoe UI", 15, ("bold")), + ) + self.n_of_mus.configure( + text="N of MUs: N/A", + font=("Segoe UI", 15, "bold"), + ) + self.file_length.configure( + text="File Length: " + + str(len(self.resdict["REF_SIGNAL"].iloc[:, 0])), + font=("Segoe UI", 15, "bold"), + ) - # Clear frame for output - if hasattr(self, "terminal"): - self.terminal = ttk.LabelFrame( - self, text="Result Output", height=100, relief="ridge" - ) - self.terminal.grid( - column=0, - row=21, - columnspan=2, - pady=8, - padx=10, - sticky=(N, S, W, E), - ) + # Update Plot + if hasattr(self, "fig"): + self.in_gui_plotting(resdict=self.resdict) - except AttributeError as e: - show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + # Clear frame for output + if hasattr(self, "terminal"): + self.terminal = ttk.LabelFrame( + self, text="Result Output", height=100, relief="ridge" ) - - except FileNotFoundError as e: - show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + self.terminal.grid( + column=0, + row=21, + columnspan=2, + pady=8, + padx=10, + sticky=(N, S, W, E), ) + except AttributeError as e: + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) + + except FileNotFoundError as e: + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) + # ----------------------------------------------------------------------------------------------- # Plotting inside of GUI def in_gui_plotting(self, resdict, plot="idr"): """ - Instance method to plot any analysis results in the GUI for inspection. Plots are updated - during the analysis process. + Instance method to plot any analysis results in the GUI for inspection. + Plots are updated during the analysis process. - Executed when button "View MUs" in master GUI window is pressed or when the original - input file is changed. + Executed when button "View MUs" in master GUI window is pressed or + when the original input file is changed. Raises ------ @@ -1138,6 +1186,7 @@ def in_gui_plotting(self, resdict, plot="idr"): -------- plot_refsig, plot_idr in the library. """ + try: if self.resdict["SOURCE"] in [ "OTB_REFSIG", @@ -1145,7 +1194,7 @@ def in_gui_plotting(self, resdict, plot="idr"): "DELSYS_REFSIG", ]: self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=False + emgfile=resdict, showimmediately=False, tight_layout=False, ) elif plot == "idr": self.fig = openhdemg.plot_idr( @@ -1156,24 +1205,27 @@ def in_gui_plotting(self, resdict, plot="idr"): ) elif plot == "refsig_fil": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=False + emgfile=resdict, showimmediately=False, tight_layout=False, ) elif plot == "refsig_off": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=False + emgfile=resdict, showimmediately=False, tight_layout=False, ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) self.canvas.get_tk_widget().grid( row=0, column=0, rowspan=6, sticky=(N, S, E, W), padx=5 ) - toolbar = NavigationToolbar2Tk(self.canvas, self.right, pack_toolbar=False) + toolbar = NavigationToolbar2Tk( + self.canvas, self.right, pack_toolbar=False, + ) toolbar.grid(row=5, column=0, sticky=S) plt.close() except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("Make sure a file is loaded."), ) # ----------------------------------------------------------------------------------------------- @@ -1191,6 +1243,7 @@ def display_results(self, input_df): input_df : pd.DataFrame Dataftame containing the analysis results. """ + # Create frame for output self.terminal = ttk.LabelFrame( self, @@ -1199,7 +1252,8 @@ def display_results(self, input_df): relief="ridge", ) self.terminal.grid( - column=0, row=21, columnspan=2, pady=8, padx=10, sticky=(N, S, W, E) + column=0, row=21, columnspan=2, pady=8, padx=10, + sticky=(N, S, W, E), ) # Display results diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index bb7f128..bf180c1 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -18,14 +18,14 @@ # --------------------------------- openfiles --------------------------------- -# in emg_from_demuse() # DONE and it works +# in emg_from_demuse() emg_from_demuse__ignore_negative_ipts = False -# in emg_from_otb() # DONE and it works +# in emg_from_otb() +emg_from_otb__ext_factor = 8 emg_from_otb__refsig = [True, "fullsampled"] emg_from_otb__extras = None emg_from_otb__ignore_negative_ipts = False -emg_from_otb__extension_factor = 8 # TODO Giacomo check please # in refsig_from_otb() refsig_from_otb__refsig = "fullsampled" @@ -47,16 +47,16 @@ emg_from_customcsv__binary_mus_firing = "BINARY_MUS_FIRING" emg_from_customcsv__accuracy = "ACCURACY" emg_from_customcsv__extras = "EXTRAS" +emg_from_customcsv__fsamp = 2048 emg_from_customcsv__ied = 8 -emg_from_customcsv__sampling_frequency = 1000 # TODO Giacomo check please -# TODO in main window and in advanced tools, when selecting OTB, delsys and custom CSV (both emgifle and refsig) write to check settings file # in refsig_from_customcsv() refsig_from_customcsv__ref_signal = "REF_SIGNAL" refsig_from_customcsv__extras = "EXTRAS" +refsig_from_customcsv__fsamp = 2048 # in save_json_emgfile() -save_json_emgfile__compresslevel = 4 # DONE and it works +save_json_emgfile__compresslevel = 4 # ---------------------------------- analysis --------------------------------- From 6f78b721839ee9d33b7086db200b4b0119ade4fe Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:55:02 +0100 Subject: [PATCH 44/57] Update error handling Update error handling in tools.py --- openhdemg/library/tools.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openhdemg/library/tools.py b/openhdemg/library/tools.py index 99d9902..c8c609d 100644 --- a/openhdemg/library/tools.py +++ b/openhdemg/library/tools.py @@ -91,6 +91,11 @@ def showselect(emgfile, how="ref_signal", title="", titlesize=12, nclic=2): elif how == "mean_emg": data_to_plot = emgfile["RAW_SIGNAL"].mean(axis=1) y_label = "Mean EMG signal" + else: + raise ValueError( + "Wrong argument in showselect(). how can only be 'ref_signal' or " + + f"'mean_emg'. {how} was passed instead." + ) # Show the signal for the selection plt.figure(num="Fig_ginput") From 3614f0eca70cd0d790858c56f8c81bda8c9adb01 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:28:11 +0100 Subject: [PATCH 45/57] Completed GUI settings --- .../gui/gui_modules/advanced_analyses.py | 366 +++++++++++------- openhdemg/gui/gui_modules/gui_plotting.py | 17 +- openhdemg/gui/settings.py | 8 +- 3 files changed, 245 insertions(+), 146 deletions(-) diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index 02c5e80..de7f9d1 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -14,9 +14,9 @@ class AdvancedAnalysis: A class to manage advanced analysis tools in an openhdemg GUI application. This class provides a window for conducting advanced analyses on EMG data. - It allows users to select and utilize different analysis tools and set parameters - for these tools. The class supports functionalities like motor unit tracking, - duplicate removal, and conduction velocity analysis. + It allows users to select and utilize different analysis tools and set + parameters for these tools. The class supports functionalities like motor + unit tracking, duplicate removal, and conduction velocity analysis. Attributes ---------- @@ -51,7 +51,8 @@ class AdvancedAnalysis: __init__(self, parent) Initialize a new instance of the AdvancedAnalysis class. advanced_analysis(self) - Perform the selected advanced analysis based on user-defined parameters. + Perform the selected advanced analysis based on user-defined + parameters. on_matrix_none_adv(self, *args) Callback function for handling changes in matrix code selection. @@ -63,32 +64,34 @@ class AdvancedAnalysis: Notes ----- - The class is designed to be a part of a larger GUI application and interacts with EMG - data and analysis tools. It depends on the state and data of the `parent` widget. - + The class is designed to be a part of a larger GUI application and + interacts with EMG data and analysis tools. It depends on the state and + data of the `parent` widget. """ def __init__(self, parent): """ Initialize a new instance of the AdvancedAnalysis class. - Sets up a window with various controls for performing advanced EMG data analyses. - The method configures and places widgets for tool selection, matrix orientation, - matrix code, and an analysis button in a grid layout. It also initializes - several Tkinter StringVars and BooleanVars for user inputs and settings. + Sets up a window with various controls for performing advanced EMG + data analyses. The method configures and places widgets for tool + selection, matrix orientation, matrix code, and an analysis button in + a grid layout. It also initializes several Tkinter StringVars and + BooleanVars for user inputs and settings. Parameters ---------- parent : object - The parent widget, usually the main application window, which provides - necessary context and data for the analysis functions. + The parent widget, usually the main application window, which + provides necessary context and data for the analysis functions. Raises ------ AttributeError - If certain operations are attempted without a loaded file or necessary data. - + If certain operations are attempted without a loaded file or + necessary data. """ + # Define attributes for later usage not defined in init self.matrix_rc_adv = StringVar() self.filetype_adv = StringVar() @@ -113,13 +116,17 @@ def __init__(self, parent): parent=self, error=None, solution=str( - "Advanced Tools for Delsys are only accessible from the library." + "Advanced Tools for Delsys are only accessible from " + + "the library." ), ) return - except AttributeError as e: + except AttributeError: + pass # Allow to open advanced tools even if no file is loaded + except Exception as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("An unexpected error occurred."), ) return @@ -177,7 +184,8 @@ def __init__(self, parent): # Matrix Orientation ctk.CTkLabel( - self.a_window, text="Matrix Orientation", font=("Segoe UI", 18, "bold") + self.a_window, text="Matrix Orientation", + font=("Segoe UI", 18, "bold"), ).grid(row=3, column=0, sticky=(W, E)) self.mat_orientation_adv = StringVar() orientation = ctk.CTkComboBox( @@ -196,11 +204,11 @@ def __init__(self, parent): ).grid(row=4, column=0, sticky=(W, E)) self.mat_code_adv = StringVar() matrix_code_vals = ( + "Custom order", + "None", "GR08MM1305", "GR04MM1305", "GR10MM0808", - self.parent.settings.emg_from_delsys__emg_sensor_name, # NOTE: Giacomo check this please - "None", ) matrix_code = ctk.CTkComboBox( self.a_window, @@ -212,7 +220,7 @@ def __init__(self, parent): matrix_code.grid(row=4, column=1, sticky=(W, E)) self.mat_code_adv.set("GR08MM1305") - # Trace variabel for updating window + # Trace variable for updating window self.mat_code_adv.trace_add("write", self.on_matrix_none_adv) # Analysis Button @@ -237,46 +245,52 @@ def __init__(self, parent): def on_matrix_none_adv(self, *args): """ - Handle changes in the matrix code selection in the AdvancedAnalysis GUI. + Handle changes in the matrix code selection in the AdvancedAnalysis + GUI. - This callback function is triggered when the `mat_code_adv` variable changes. - It dynamically updates the GUI to add or remove an entry box for specifying - matrix rows and columns. When 'None' is selected for the matrix code, it creates - an entry box for the user to input the rows and columns. Otherwise, it removes - this entry box. + This callback function is triggered when the `mat_code_adv` variable + changes. It dynamically updates the GUI to add or remove an entry box + for specifying matrix rows and columns. When 'None' is selected for + the matrix code, it creates an entry box for the user to input the + rows and columns. Otherwise, it removes this entry box. Parameters ---------- *args : tuple - The arguments passed to the callback function. Not used in the function but - required for compatibility with Tkinter's trace mechanism. + The arguments passed to the callback function. Not used in the + function but required for compatibility with Tkinter's trace + mechanism. Notes ----- - The method is part of the AdvancedAnalysis class and interacts with the GUI elements - specific to advanced analysis options. It ensures that the GUI is responsive to user - selections and updates the interface accordingly. - + The method is part of the AdvancedAnalysis class and interacts with + the GUI elements specific to advanced analysis options. It ensures + that the GUI is responsive to user selections and updates the + interface accordingly. """ + # Necessary to distinguish between None and other if self.mat_code_adv.get() == "None": # Set label for matrix rows and columns - mat_label_adv = ctk.CTkLabel( - self.a_window, text="Rows, Columns:", font=("Segoe UI", 18, "bold") + self.mat_label_adv = ctk.CTkLabel( + self.a_window, text="Rows,Columns:", + font=("Segoe UI", 18, "bold"), ) - mat_label_adv.grid(row=5, column=1, sticky=W) + self.mat_label_adv.grid(row=5, column=1, sticky=W) - row_cols_entry_adv = ctk.CTkEntry( - self.a_window, width=30, textvariable=self.matrix_rc_adv + self.row_cols_entry_adv = ctk.CTkEntry( + self.a_window, width=80, textvariable=self.matrix_rc_adv + ) + self.row_cols_entry_adv.grid( + row=6, column=1, sticky=W, padx=5, pady=2, ) - row_cols_entry_adv.grid(row=6, column=1, sticky=W, padx=5, pady=2) self.matrix_rc_adv.set("13,5") - else: + # Remove the elements if hasattr(self, "row_cols_entry_adv"): - row_cols_entry_adv.grid_forget() - mat_label_adv.grid_forget() + self.row_cols_entry_adv.grid_forget() + self.mat_label_adv.grid_forget() # Update main advanced window based on selection self.a_window.update_idletasks() @@ -285,32 +299,30 @@ def advanced_analysis(self): """ Open a top-level window based on the selected advanced analysis method. - This method is responsible for generating different GUI windows depending on the - advanced analysis option chosen by the user. It dynamically creates GUI elements - like dropdowns, buttons, and checkboxes specific to the selected analysis tool, - such as 'Motor Unit Tracking', 'Conduction Velocity', or 'Duplicate Removal'. + This method is responsible for generating different GUI windows + depending on the advanced analysis option chosen by the user. It + dynamically creates GUI elements like dropdowns, buttons, and + checkboxes specific to the selected analysis tool, such as 'Motor Unit + Tracking', 'Conduction Velocity', or 'Duplicate Removal'. Raises ------ AttributeError - If a required file is not loaded prior to performing the analysis or if invalid - rows and columns arguments are entered for the 'Conduction Velocity' analysis. + If a required file is not loaded prior to performing the analysis + or if invalid rows and columns arguments are entered for the + 'Conduction Velocity' analysis. Notes ----- - The method is part of the AdvancedAnalysis class and interacts with other GUI elements - and functionalities of the application. It ensures that the GUI adapts to the user's - choice of analysis, providing relevant options and settings for each analysis type. + The method is part of the AdvancedAnalysis class and interacts with + other GUI elements and functionalities of the application. It ensures + that the GUI adapts to the user's choice of analysis, providing + relevant options and settings for each analysis type. """ - if self.advanced_method.get() == "Motor Unit Tracking": - head_title = "MUs Tracking Window" - elif self.advanced_method.get() == "Conduction Velocity": - head_title = "Conduction Velocity Window" - else: - head_title = "Duplicate Removal Window" + # Set head window self.head = ctk.CTkToplevel(fg_color="LightBlue4") - self.head.title(head_title) + self.head.title(self.advanced_method.get()) # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -361,7 +373,7 @@ def advanced_analysis(self): # Threshold label threshold_label = ctk.CTkLabel( - self.head, text="Threshold:", font=("Segoe UI", 15, "bold") + self.head, text="Threshold:", font=("Segoe UI", 18, "bold") ) threshold_label.grid(column=0, row=9) @@ -378,24 +390,25 @@ def advanced_analysis(self): # Time Label time_window_label = ctk.CTkLabel( - self.head, text="Time window:", font=("Segoe UI", 18, "bold") + self.head, text="Time window (ms):", font=("Segoe UI", 18, "bold") ) time_window_label.grid(column=0, row=10) # Time Combobox time_combobox = ctk.CTkComboBox( self.head, - values=("25", "50"), + values=("25", "30", "40", "50", "75", "100"), variable=self.time_window, state="readonly", width=100, ) time_combobox.grid(column=1, row=10) - self.time_window.set("25") + self.time_window.set("50") # Exclude below threshold exclude_label = ctk.CTkLabel( - self.head, text="Exclude below threshold", font=("Segoe UI", 18, "bold") + self.head, text="Exclude below threshold", + font=("Segoe UI", 18, "bold"), ) exclude_label.grid(column=0, row=11) @@ -404,8 +417,8 @@ def advanced_analysis(self): self.head, variable=self.exclude_thres, bg_color="LightBlue4", - onvalue="True", - offvalue="False", + onvalue=True, + offvalue=False, text="", ) exclude_checkbox.grid(column=1, row=11) @@ -422,24 +435,26 @@ def advanced_analysis(self): self.head, variable=self.filter_adv, bg_color="LightBlue4", - onvalue="True", - offvalue="False", + onvalue=True, + offvalue=False, text="", ) filter_checkbox.grid(column=1, row=12) self.filter_adv.set(True) - # Exclude below threshold - show_label = ctk.CTkLabel(self.head, text="Show", font=("Segoe UI", 18, "bold")) + # Show + show_label = ctk.CTkLabel( + self.head, text="Show", font=("Segoe UI", 18, "bold"), + ) show_label.grid(column=0, row=13) - # Add exclude checkbox + # Add show checkbox show_checkbox = ctk.CTkCheckBox( self.head, variable=self.show_adv, bg_color="LightBlue4", - onvalue="True", - offvalue="False", + onvalue=True, + offvalue=False, text="", ) show_checkbox.grid(column=1, row=13) @@ -461,9 +476,9 @@ def advanced_analysis(self): if self.advanced_method.get() == "Duplicate Removal": # Add Which label - ctk.CTkLabel(self.head, text="Which", font=("Segoe UI", 18, "bold")).grid( - column=0, row=14 - ) + ctk.CTkLabel( + self.head, text="Which", font=("Segoe UI", 18, "bold") + ).grid(column=0, row=14) # Combobox for Which option which_combobox = ctk.CTkComboBox( @@ -478,7 +493,8 @@ def advanced_analysis(self): # Add button to execute MU tracking track_button.configure( - text="Remove Duplicates", command=self.remove_duplicates_between + text="Remove Duplicates", + command=self.remove_duplicates_between, ) if self.advanced_method.get() == "Conduction Velocity": @@ -496,8 +512,7 @@ def advanced_analysis(self): # Sort emg file sorted_rawemg = openhdemg.sort_rawemg( self.parent.resdict, - code=self.mat_code_adv.get(), - orientation=int(self.mat_orientation_adv.get()), + code="None", n_rows=list_rcs[0], n_cols=list_rcs[1], ) @@ -506,17 +521,31 @@ def advanced_analysis(self): parent=self, error=e, solution=str( - "Number of specified rows and columns must match number of channels." + "Number of specified rows and columns must " + + "match the number of channels." + ), + ) + return + + elif self.mat_code_adv.get() == "Custom order": + try: + # Sort emg file + sorted_rawemg = openhdemg.sort_rawemg( + self.parent.resdict, + code="Custom order", + custom_sorting_order=self.parent.settings.custom_sorting_order, + ) + except ValueError as e: + show_error_dialog( + parent=self, + error=e, + solution=str( + "Number of rows * columns must match the " + + "number of channels. Verify " + + "custom_sorting_order in settings." ), ) return - # # DELSYS conduction velocity not available - # elif self.mat_code_adv.get() == "Trigno Galileo Sensor": - # tk.messagebox.showerror( - # "Information", - # "MUs conduction velocity estimation is not available for this matrix." - # ) - # return else: # Sort emg file @@ -529,6 +558,9 @@ def advanced_analysis(self): openhdemg.MUcv_gui( emgfile=self.parent.resdict, sorted_rawemg=sorted_rawemg, + n_firings=self.parent.settings.MUcv_gui__n_firings, + muaps_timewindow=self.parent.settings.MUcv_gui__muaps_timewindow, + figsize=self.parent.settings.MUcv_gui__figsize, ) except AttributeError as e: @@ -536,7 +568,8 @@ def advanced_analysis(self): parent=self, error=e, solution=str( - "Please make sure to load a file prior to Conduction velocity calculation." + "Please make sure to load a file in the main window " + + "for Conduction velocity calculation." ), ) self.head.destroy() @@ -546,7 +579,7 @@ def advanced_analysis(self): parent=self, error=e, solution=str( - "Please make sure to enter valid Rows, Columns arguments." + "Please make sure to enter valid Rows,Columns arguments." + " Arguments must be non-negative and seperated by `,`." ), ) @@ -558,7 +591,7 @@ def advanced_analysis(self): ### Define function for advanced analysis tools def open_emgfile1(self): """ - Open EMG file based on the selected file type and extension factor. + Open EMG file based on the selected file type. This function is used to open and store the first emgfile that is required for the MU tracking. As both files required are loaded by @@ -568,15 +601,35 @@ def open_emgfile1(self): -------- open_emgfile1(), openhdemg.askopenfile() """ + try: - # Open OTB file if self.filetype_adv.get() == "OTB": self.emgfile1 = openhdemg.askopenfile( filesource=self.filetype_adv.get(), - otb_ext_factor=int(self.extension_factor_adv.get()), + ignore_negative_ipts=self.parent.settings.emg_from_otb__ignore_negative_ipts, + otb_ext_factor=self.parent.settings.emg_from_otb__ext_factor, + otb_refsig_type=self.parent.settings.emg_from_otb__refsig, + otb_extras=self.parent.settings.emg_from_otb__extras, ) - # Open all other filetypes - else: + elif self.filetype_adv.get() == "DEMUSE": + self.emgfile1 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + ignore_negative_ipts=self.parent.settings.emg_from_demuse__ignore_negative_ipts, + ) + elif self.filetype_adv.get() == "CUSTOMCSV": + self.emgfile1 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + custom_ref_signal=self.parent.settings.emg_from_customcsv__ref_signal, + custom_raw_signal=self.parent.settings.emg_from_customcsv__raw_signal, + custom_ipts=self.parent.settings.emg_from_customcsv__ipts, + custom_mupulses=self.parent.settings.emg_from_customcsv__mupulses, + custom_binary_mus_firing=self.parent.settings.emg_from_customcsv__binary_mus_firing, + custom_accuracy=self.parent.settings.emg_from_customcsv__accuracy, + custom_extras=self.parent.settings.emg_from_customcsv__extras, + custom_fsamp=self.parent.settings.emg_from_customcsv__fsamp, + custom_ied=self.parent.settings.emg_from_customcsv__ied, + ) + else: # OPENHDEMG self.emgfile1 = openhdemg.askopenfile( filesource=self.filetype_adv.get(), ) @@ -586,18 +639,18 @@ def open_emgfile1(self): self.head, text="File 1 loaded", font=("Segoe UI", 15, "bold") ).grid(column=1, row=2) - except ValueError as e: + except Exception as e: show_error_dialog( parent=self, error=e, solution=str( - "Make sure to specify a valid filetype or extension factor." + "Make sure to specify a valid filetype or correct settings" ), ) def open_emgfile2(self): """ - Open EMG file based on the selected file type and extension factor. + Open EMG file based on the selected file type. This function is used to open and store the first emgfile that is required for the MU tracking. As both files required are loaded by @@ -607,15 +660,35 @@ def open_emgfile2(self): -------- open_emgfile1(), openhdemg.askopenfile() """ + try: - # Open OTB file if self.filetype_adv.get() == "OTB": self.emgfile2 = openhdemg.askopenfile( filesource=self.filetype_adv.get(), - otb_ext_factor=int(self.extension_factor_adv.get()), + ignore_negative_ipts=self.parent.settings.emg_from_otb__ignore_negative_ipts, + otb_ext_factor=self.parent.settings.emg_from_otb__ext_factor, + otb_refsig_type=self.parent.settings.emg_from_otb__refsig, + otb_extras=self.parent.settings.emg_from_otb__extras, ) - # Open all other filetypes - else: + elif self.filetype_adv.get() == "DEMUSE": + self.emgfile2 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + ignore_negative_ipts=self.parent.settings.emg_from_demuse__ignore_negative_ipts, + ) + elif self.filetype_adv.get() == "CUSTOMCSV": + self.emgfile2 = openhdemg.askopenfile( + filesource=self.filetype_adv.get(), + custom_ref_signal=self.parent.settings.emg_from_customcsv__ref_signal, + custom_raw_signal=self.parent.settings.emg_from_customcsv__raw_signal, + custom_ipts=self.parent.settings.emg_from_customcsv__ipts, + custom_mupulses=self.parent.settings.emg_from_customcsv__mupulses, + custom_binary_mus_firing=self.parent.settings.emg_from_customcsv__binary_mus_firing, + custom_accuracy=self.parent.settings.emg_from_customcsv__accuracy, + custom_extras=self.parent.settings.emg_from_customcsv__extras, + custom_fsamp=self.parent.settings.emg_from_customcsv__fsamp, + custom_ied=self.parent.settings.emg_from_customcsv__ied, + ) + else: # OPENHDEMG self.emgfile2 = openhdemg.askopenfile( filesource=self.filetype_adv.get(), ) @@ -625,12 +698,12 @@ def open_emgfile2(self): self.head, text="File 2 loaded", font=("Segoe UI", 15, "bold") ).grid(column=1, row=3) - except ValueError as e: + except Exception as e: show_error_dialog( parent=self, error=e, solution=str( - "Make sure to specify a valid filetype or extension factor." + "Make sure to specify a valid filetype or correct settings" ), ) @@ -638,40 +711,41 @@ def on_filetype_change_adv(self, *args): """ Handle changes in the file type selection in the AdvancedAnalysis GUI. - This callback function is triggered when the `filetype_adv` variable changes. - Specifically, it updates the GUI to add or remove a combobox for specifying - the OTB (OpenToBe) extension factors when 'OTB' is selected as the file type. - For other file types, this additional combobox is removed. + This callback function is triggered when the `filetype_adv` variable + changes. If filetype is set to != "OPENHDEMG", it will create a second + combobox on the grid at column 0 and row 2 and when the filetype is + set to "OPENHDEMG" it will remove the second combobox from the grid. Parameters ---------- *args : tuple - The arguments passed to the callback function. Not used in the function but - required for compatibility with Tkinter's trace mechanism. + The arguments passed to the callback function. Not used in the + function but required for compatibility with Tkinter's trace + mechanism. Notes ----- - The method is part of the AdvancedAnalysis class and interacts with the GUI elements - specific to file type selection. It ensures that the GUI is responsive to user - selections and updates the interface accordingly. + The method is part of the AdvancedAnalysis class and interacts with + the GUI elements specific to file type selection. It ensures that the + GUI is responsive to user selections and updates the interface + accordingly. """ - # Add a combobox containing the OTB extension factors - # in case an OTB file is loaded - if self.filetype_adv.get() == "OTB": - self.otb_combobox = ctk.CTkComboBox( + + # Make sure to forget all the previous labels + if hasattr(self, "adv_verify_settings_text"): + self.adv_verify_settings_text.grid_forget() + + if self.filetype_adv.get() != "OPENHDEMG": + # Display the text: Verify openfiles settings! + self.adv_verify_settings_text = ctk.CTkLabel( self.head, - values=["8", "9", "10", "11", "12", "13", "14", "15", "16"], - width=30, - variable=self.extension_factor_adv, - state="readonly", + text="Verify openfiles settings!", + font=("Segoe UI", 12, "bold"), + text_color="black", + ) + self.adv_verify_settings_text.grid( + column=1, row=1, sticky=(W, E), padx=5, ) - self.otb_combobox.grid(column=1, row=1, sticky=(W, E), padx=5) - self.otb_combobox.set("Extension Factor") - - # Forget the widget in case the filetype is changed - else: - if hasattr(self, "otb_combobox"): - self.otb_combobox.grid_forget() def track_mus(self): """ @@ -693,6 +767,10 @@ def track_mus(self): -------- openhdemg.tracking() """ + + # Reload settings for tracking + self.parent.load_settings() + try: if self.mat_code_adv.get() == "None": # Get rows and columns and turn into list @@ -714,12 +792,15 @@ def track_mus(self): tracking_res = openhdemg.tracking( emgfile1=self.emgfile1, emgfile2=self.emgfile2, + firings=self.parent.settings.tracking__firings, + derivation=self.parent.settings.tracking__derivation, threshold=float(self.threshold_adv.get()), timewindow=int(self.time_window.get()), matrixcode=self.mat_code_adv.get(), orientation=int(self.mat_orientation_adv.get()), n_rows=n_rows, n_cols=n_cols, + custom_sorting_order=self.parent.settings.custom_sorting_order, exclude_belowthreshold=self.exclude_thres.get(), filter=self.filter_adv.get(), show=self.show_adv.get(), @@ -727,7 +808,8 @@ def track_mus(self): # Add result terminal track_terminal = ttk.LabelFrame( - self.head, text="MUs Tracking Result", height=100, relief="ridge" + self.head, text="MUs Tracking Result", height=100, + relief="ridge", ) track_terminal.grid( column=2, @@ -763,7 +845,8 @@ def track_mus(self): + "\n - Matrix Code" + "\n - Matrix Orientation" + "\n - Threshold" - + "\n - Rows, Columns" + + "\n - Rows,Columns" + + "\n - custom_sorting_order in settings" ), ) @@ -773,8 +856,10 @@ def remove_duplicates_between(self): Notes ----- - The function uses the openhdemg.remove_duplicates_between function to remove duplicates between two EMG files. - If the required parameters are not provided, the function will raise an AttributeError or ValueError. + The function uses the openhdemg.remove_duplicates_between function to + remove duplicates between two EMG files. If the required parameters + are not provided, the function will raise an AttributeError or + ValueError. Raises ------ @@ -787,6 +872,7 @@ def remove_duplicates_between(self): -------- openhdemg.remove_duplicates_between(), openhdemg.asksavefile() """ + try: if self.mat_code_adv.get() == "None": # Get rows and columns and turn into list @@ -808,20 +894,29 @@ def remove_duplicates_between(self): emg_file1, emg_file2, _ = openhdemg.remove_duplicates_between( emgfile1=self.emgfile1, emgfile2=self.emgfile2, - threshold=float(self.threshold_adv.get()), + firings=self.parent.settings.remove_duplicates_between__firings, + derivation=self.parent.settings.remove_duplicates_between__derivation, timewindow=int(self.time_window.get()), + threshold=float(self.threshold_adv.get()), matrixcode=self.mat_code_adv.get(), orientation=int(self.mat_orientation_adv.get()), n_rows=n_rows, n_cols=n_cols, + custom_sorting_order=self.parent.settings.custom_sorting_order, filter=self.filter_adv.get(), show=self.show_adv.get(), which=self.which_adv.get(), ) # Save files - openhdemg.asksavefile(emg_file1) - openhdemg.asksavefile(emg_file2) + openhdemg.asksavefile( + emg_file1, + compresslevel=self.parent.settings.save_json_emgfile__compresslevel, + ) + openhdemg.asksavefile( + emg_file2, + compresslevel=self.parent.settings.save_json_emgfile__compresslevel, + ) except AttributeError as e: show_error_dialog( @@ -844,6 +939,7 @@ def remove_duplicates_between(self): + "\n - Matrix Orientation" + "\n - Threshold" + "\n - Which" - + "\n - Rows, Columns", + + "\n - Rows,Columns" + + "\n - custom_sorting_order in settings" ), ) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 540cfcf..a3b6643 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -497,7 +497,7 @@ def on_matrix_none(self, *args): # Place label defined in init # Label for matriy rows columns self.mat_label = ctk.CTkLabel( - self.head, text="Rows, Columns:", font=("Segoe UI", 18, "bold") + self.head, text="Rows,Columns:", font=("Segoe UI", 18, "bold") ) self.mat_label.grid(row=0, column=5, sticky=E) @@ -878,7 +878,7 @@ def plot_derivation(self): + "\n - Matrix Code" + "\n - Matrix Orientation" + "\n - Figure size arguments" - + "\n - Rows, Columns arguments" + + "\n - Rows,Columns arguments" ), ) @@ -954,7 +954,8 @@ def plot_muaps(self): parent=self, error=e, solution=str( - "Number of specified rows and columns must match" + "Number of specified rows and columns must " + + "match the number of channels." ), ) return @@ -965,7 +966,7 @@ def plot_muaps(self): emgfile=self.parent.resdict, code=self.mat_code.get(), orientation=int(self.mat_orientation.get()), - custom_sorting_order=self.parent.settings.sort_rawemg__custom_sorting_order, + custom_sorting_order=self.parent.settings.custom_sorting_order, ) # calcualte derivation @@ -1010,11 +1011,12 @@ def plot_muaps(self): + "\n - Figure size arguments" + "\n - Timewindow" + "\n - MU Number" - + "\n - Rows, Columns arguments" + + "\n - Rows,Columns arguments" + + "\n - custom_sorting_order in settings" ), ) - show_error_dialog( + show_error_dialog( # TODO Paul do we need two of these show_error_dialog? parent=self, error=e, solution=str( @@ -1025,7 +1027,8 @@ def plot_muaps(self): + "\n - Figure size arguments" + "\n - Timewindow" + "\n - MU Number" - + "\n - Rows, Columns arguments" + + "\n - Rows,Columns arguments" + + "\n - custom_sorting_order in settings" ), ) diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index bf180c1..498d047 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -80,8 +80,6 @@ # ------------------------------------ muap ----------------------------------- -# TODO missing custom order (2 variables) in traking and duplicates - # in tracking() tracking__firings = "all" tracking__derivation = "sd" @@ -95,5 +93,7 @@ MUcv_gui__muaps_timewindow = 50 MUcv_gui__figsize = [25, 20] -# --------------------------------- electrodes -------------------------------- # DONE only in plot window, it works -sort_rawemg__custom_sorting_order = None +# --------------------------------- electrodes -------------------------------- +# This custom sorting order is valid for all the GUI windows, although the +# documentation is accessible in the api of the electrodes module. +custom_sorting_order = None From f9d2d10dc8d7c71cd73704463a911a10330308e9 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:09:25 +0100 Subject: [PATCH 46/57] Fixes and Improvements Almost everything seems to be fixed, apart from the lagging resizing and the progress bar, an additional check will be performed over the next days --- openhdemg/gui/gui_files/Icon.ico | Bin 1947 -> 0 bytes openhdemg/gui/gui_files/Icon2.ico | Bin 3126 -> 0 bytes openhdemg/gui/gui_files/Icon_transp.ico | Bin 0 -> 2014 bytes openhdemg/gui/gui_modules/__init__.py | 2 +- .../gui/gui_modules/advanced_analyses.py | 4 +- openhdemg/gui/gui_modules/analyse_force.py | 2 +- openhdemg/gui/gui_modules/edit_mus.py | 2 +- .../{edit_refsig.py => edit_sig.py} | 159 +++++++++++------- openhdemg/gui/gui_modules/error_handler.py | 3 +- openhdemg/gui/gui_modules/gui_plotting.py | 11 +- openhdemg/gui/gui_modules/mu_properties.py | 6 +- openhdemg/gui/openhdemg_gui.py | 34 ++-- 12 files changed, 135 insertions(+), 88 deletions(-) delete mode 100644 openhdemg/gui/gui_files/Icon.ico delete mode 100644 openhdemg/gui/gui_files/Icon2.ico create mode 100644 openhdemg/gui/gui_files/Icon_transp.ico rename openhdemg/gui/gui_modules/{edit_refsig.py => edit_sig.py} (72%) diff --git a/openhdemg/gui/gui_files/Icon.ico b/openhdemg/gui/gui_files/Icon.ico deleted file mode 100644 index 490e0ffc7127faea55a8b4d76d982342e181bf06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1947 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 diff --git a/openhdemg/gui/gui_files/Icon_transp.ico b/openhdemg/gui/gui_files/Icon_transp.ico new file mode 100644 index 0000000000000000000000000000000000000000..a5a8522e9d85ba084d690fd7fa8f6929fdafbc42 GIT binary patch literal 2014 zcmV<42O;zCbOK2u5v^%S*`PxJ!^=E7%GOqK&UY{{n3VYe7&HL2R{Fw9#cD7rcqoD5OdKJT%Z@z!hB zs(5A=vJCijWtzjjlWrXGBxH9R2@_QfiC60J@AtT!t)(c?1G6wUts?W>*`8mv@z_`&{c81KE% zxDBN(!}vNfjM6bQJi%qQnx8v_;SbTbs+#Kn+BV?)s;WhI;bIfI9@U5zDN7CF)c7hP{1o%lWpIADL)p{ROzhNdN!_2uVaiRCt`7muqYs zR~5&9_s-1j?0VO0Z~WT48XUXLqbiP*1}Z~Cnn-QbG~@%}8V<)!bwY}?o&ExW6*86bQ_HO>te3&`+ zp7TGCd(Is}4S3@z)-Mau69~|4`e>Jeh!7~(C7#YPkVw&geVmhDeV&18b&91H9NJBA z`%d;YM|dz2=5viSq^gjIamzC{&aUX#@xA2Ee zx3L$I_jnOlu1>w4B;)!PUgUt^4_&wMO=l14YMAfz_7*jlMc}S&Jab29-uOaZmmupJ zm|6~3Rm@}&E@(R3(ZRPK?cqSFsIc?+Pr0viH9yew>Wm}6QaV0qElmKx0W%1lt2p%5 zGi)g%@Zw_xR=4nM(=xQB!d?p7)$q$44zgI7i*A6nHi9eLcy`}DuP7xz*9!KoiLt2` z;{Z9=AgE=ki@+QlSk=moZ){~7fII}UWf>0z=FWKy$hto2^fZpDG6Hxu8l%Wqgogm+ z#;53P4DtC|je{2fQ|3?z)Y4702nEr4?)meNX_hTvdZP7&YbGGTRvL!1L4i^=5O5(< zM_VMy7FlP~9k`)AR0k(dKnR7SDmJ>xP!J9x>-=nzejjVms6K}!uv{I#%pp-&szNth zgn*`l>8D*vL8Nxm9&)aZspXKu#d5VOW@`xq=#q#;-Hh*Z1Pm_%LSfgEf;lLZbRd4S z6da{t2nQ+L+G@Zvj^z-SnJfdi)E2%10f{CYe8M8T*g%>O0k#cUivgKRb0(FUM?mJ^ z1q<95JP%wY(WQf_1J}SK~5R`Wn?>Vj(iDA_(;w& zeDNdxEfk1jzl{A=wXWME+#G4!WL^CbgURJc;j+5!8W*S6uzPtQ0hui$@b}TXc_8*K zmlL-zk!xTq8|4elXE{A_J4%4#Y6Of7Y0FR6^|3X20fpeB*>maB0t z-chItTWN^=@rf8ohEg#ida^Lkn$0-6aJ(WL_dLRy-%g<~x(3rzaOG1T|H@%5&3!IA zWG|iIg>zSU#j)p0ie7a$T;gKlGx<4C{O1T6sX|IB8P8e0Hm+rVAc%L*FCoxoUZ+34p3zJbVSTntRuP8h`GrBA>v@EO^Tjv#`O$kAbVgvk#t@$H^Rc#;Ztu7&!XyX`T4cIG_W z&tK;4>ys#SPay)B>D#1Hf$NA%~Cpop0?wUr>uMg1D5o1%>PglUC w9YKQv^c(pqJ57+-x`is{GaR2}S07*qoM6N<$f=K(vJ^%m! literal 0 HcmV?d00001 diff --git a/openhdemg/gui/gui_modules/__init__.py b/openhdemg/gui/gui_modules/__init__.py index 0eb910f..847e263 100644 --- a/openhdemg/gui/gui_modules/__init__.py +++ b/openhdemg/gui/gui_modules/__init__.py @@ -3,7 +3,7 @@ "error_handler"] from openhdemg.gui.gui_modules.edit_mus import * -from openhdemg.gui.gui_modules.edit_refsig import * +from openhdemg.gui.gui_modules.edit_sig import * from openhdemg.gui.gui_modules.gui_helpers import * from openhdemg.gui.gui_modules.analyse_force import * from openhdemg.gui.gui_modules.mu_properties import * diff --git a/openhdemg/gui/gui_modules/advanced_analyses.py b/openhdemg/gui/gui_modules/advanced_analyses.py index de7f9d1..a4e95cc 100644 --- a/openhdemg/gui/gui_modules/advanced_analyses.py +++ b/openhdemg/gui/gui_modules/advanced_analyses.py @@ -136,7 +136,7 @@ def __init__(self, parent): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.a_window.iconbitmap(default=iconpath) if platform.startswith("win"): self.a_window.after(200, lambda: self.a_window.iconbitmap(iconpath)) @@ -326,7 +326,7 @@ def advanced_analysis(self): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) diff --git a/openhdemg/gui/gui_modules/analyse_force.py b/openhdemg/gui/gui_modules/analyse_force.py index dccb53e..bd8fde1 100644 --- a/openhdemg/gui/gui_modules/analyse_force.py +++ b/openhdemg/gui/gui_modules/analyse_force.py @@ -85,7 +85,7 @@ def __init__(self, parent): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index e876665..5dbc568 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -97,7 +97,7 @@ def __init__(self, parent): head_path = os.path.dirname( os.path.dirname(os.path.abspath(__file__)) ) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) diff --git a/openhdemg/gui/gui_modules/edit_refsig.py b/openhdemg/gui/gui_modules/edit_sig.py similarity index 72% rename from openhdemg/gui/gui_modules/edit_refsig.py rename to openhdemg/gui/gui_modules/edit_sig.py index 187baf4..d0173aa 100644 --- a/openhdemg/gui/gui_modules/edit_refsig.py +++ b/openhdemg/gui/gui_modules/edit_sig.py @@ -8,13 +8,13 @@ from openhdemg.gui.gui_modules.error_handler import show_error_dialog -class EditRefsig: +class EditSig: """ - A class to manage editing of the reference signal in a GUI application. + A class to manage editing of the signals in a GUI application. This class creates a window that offers various options for editing the - reference signal. It includes functionalities for filtering the signal, - removing offset, converting the signal, and transforming it to a + reference and EMG signal. It includes functionalities for filtering the + signal, removing offset, converting the signal, and transforming it to a percentage value. The class is instantiated when the "RefSig Editing" button in the master GUI window is pressed. @@ -22,7 +22,7 @@ class EditRefsig: ---------- parent : object The parent widget, typically the main application window, to which - this EditRefsig instance belongs. + this EditSig instance belongs. head : CTkToplevel The top-level widget for the Reference Signal Editing window. filter_order : StringVar @@ -48,7 +48,7 @@ class EditRefsig: Methods ------- __init__(self, parent) - Initialize a new instance of the EditRefsig class. + Initialize a new instance of the EditSig class. filter_refsig(self) Apply filtering to the reference signal based on the specified order and cutoff frequency. @@ -65,8 +65,8 @@ class EditRefsig: Examples -------- >>> main_window = Tk() - >>> edit_refsig = EditRefsig(main_window) - >>> edit_refsig.head.mainloop() + >>> edit_sig = EditSig(main_window) + >>> edit_sig.head.mainloop() Notes ----- @@ -78,20 +78,19 @@ class EditRefsig: def __init__(self, parent): """ - Initialize a new instance of the EditRefsig class. + Initialize a new instance of the EditSig class. - This method sets up the GUI components for the Reference Signal - Editing Window. It includes controls for filtering the reference - signal, removing its offset, converting it, and transforming it to a - percentage value. The method configures and places various widgets - such as labels, entries, buttons, and combo boxes in a grid layout for - user interaction. + This method sets up the GUI components for the Signal Editing Window. + It includes controls for filtering the signal, removing its offset, + converting it, and transforming it to a percentage value. The method + configures and places various widgets such as labels, entries, buttons, + and combo boxes in a grid layout for user interaction. Parameters ---------- parent : object The parent widget, typically the main application window, to which - this EditRefsig instance belongs. The parent is used for accessing + this EditSig instance belongs. The parent is used for accessing shared resources and data. Raises @@ -110,7 +109,7 @@ def __init__(self, parent): self.head.title("Reference Signal Editing Window") head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) @@ -123,50 +122,79 @@ def __init__(self, parent): self.head.columnconfigure(col, weight=1) # Configure rows with a loop - for row in range(10): + for row in range(12): self.head.rowconfigure(row, weight=1) + # Filter RAW EMG signal + ctk.CTkLabel( + self.head, text="EMG Signal", font=("Segoe UI", 18, "bold"), + ).grid(column=0, row=0, sticky=W) + ctk.CTkLabel( + self.head, text="Filter Order", font=("Segoe UI", 18, "bold"), + ).grid(column=1, row=1, sticky=(W, E)) + ctk.CTkLabel( + self.head, text="BandPass Freq", font=("Segoe UI", 18, "bold"), + ).grid(column=2, row=1, sticky=(W, E)) + ctk.CTkButton( + self.head, + text="Filter EMG signal", + command=self.filter_emgsig, + ).grid(column=0, row=2, sticky=W) + self.emg_filter_order = StringVar() + ctk.CTkEntry( + self.head, width=100, textvariable=self.emg_filter_order, + ).grid(column=1, row=2) + self.emg_filter_order.set(2) + self.emg_bandpass_freq = StringVar() + ctk.CTkEntry( + self.head, width=100, textvariable=self.emg_bandpass_freq, + ).grid(column=2, row=2) + self.emg_bandpass_freq.set("20-500") + ttk.Separator( + self.head, orient="horizontal" + ).grid( + column=0, columnspan=3, row=3, sticky=(W, E), padx=5, pady=5, + ) + # Filter Refsig # Define Labels + ctk.CTkLabel( + self.head, text="Reference Signal", font=("Segoe UI", 18, "bold"), + ).grid(column=0, row=4, sticky=W) ctk.CTkLabel( self.head, text="Filter Order", font=("Segoe UI", 18, "bold"), - ).grid(column=1, row=0, sticky=(W, E)) + ).grid(column=1, row=5, sticky=(W, E)) ctk.CTkLabel( self.head, text="Cutoff Freq", font=("Segoe UI", 18, "bold"), - ).grid(column=2, row=0, sticky=(W, E)) + ).grid(column=2, row=5, sticky=(W, E)) # Fiter button basic = ctk.CTkButton( self.head, text="Filter Refsig", command=self.filter_refsig, ) - basic.grid(column=0, row=1, sticky=W) - self.filter_order = StringVar() - order = ctk.CTkEntry( - self.head, width=100, textvariable=self.filter_order, + basic.grid(column=0, row=6, sticky=W) + self.refsig_filter_order = StringVar() + ref_order = ctk.CTkEntry( + self.head, width=100, textvariable=self.refsig_filter_order, ) - order.grid(column=1, row=1) - self.filter_order.set(4) + ref_order.grid(column=1, row=6) + self.refsig_filter_order.set(4) self.cutoff_freq = StringVar() cutoff = ctk.CTkEntry( self.head, width=100, textvariable=self.cutoff_freq, ) - cutoff.grid(column=2, row=1) + cutoff.grid(column=2, row=6) self.cutoff_freq.set(15) # Remove offset of reference signal - separator2 = ttk.Separator(self.head, orient="horizontal") - separator2.grid( - column=0, columnspan=3, row=2, sticky=(W, E), padx=5, pady=5, - ) - ctk.CTkLabel( self.head, text="Offset Value", font=("Segoe UI", 18, "bold") - ).grid(column=1, row=3, sticky=(W, E)) + ).grid(column=1, row=7, sticky=(W, E)) ctk.CTkLabel( self.head, text="Automatic Offset", font=("Segoe UI", 18, "bold") - ).grid(column=2, row=3, sticky=(W, E)) + ).grid(column=2, row=7, sticky=(W, E)) # Offset removal button basic2 = ctk.CTkButton( @@ -174,32 +202,27 @@ def __init__(self, parent): text="Remove Offset", command=self.remove_offset, ) - basic2.grid(column=0, row=4, sticky=W) + basic2.grid(column=0, row=8, sticky=W) self.offsetval = StringVar() offset = ctk.CTkEntry( self.head, width=100, textvariable=self.offsetval, ) - offset.grid(column=1, row=4) + offset.grid(column=1, row=8) self.offsetval.set(4) self.auto_eval = StringVar() auto = ctk.CTkEntry(self.head, width=100, textvariable=self.auto_eval) - auto.grid(column=2, row=4) + auto.grid(column=2, row=8) self.auto_eval.set(0) - separator3 = ttk.Separator(self.head, orient="horizontal") - separator3.grid( - column=0, columnspan=3, row=5, sticky=(W, E), padx=5, pady=5, - ) - # Convert Reference signal ctk.CTkLabel( self.head, text="Operator", font=("Segoe UI", 18, "bold"), - ).grid(column=1, row=6, sticky=(W, E)) + ).grid(column=1, row=9, sticky=(W, E)) ctk.CTkLabel( self.head, text="Factor", font=("Segoe UI", 18, "bold"), - ).grid(column=2, row=6, sticky=(W, E)) + ).grid(column=2, row=9, sticky=(W, E)) self.convert = StringVar() convert = ctk.CTkComboBox( @@ -207,14 +230,14 @@ def __init__(self, parent): values=("Multiply", "Divide"), ) convert.configure(state="readonly") - convert.grid(column=1, row=7) + convert.grid(column=1, row=10) self.convert.set("Multiply") self.convert_factor = DoubleVar() factor = ctk.CTkEntry( self.head, width=100, textvariable=self.convert_factor, ) - factor.grid(column=2, row=7) + factor.grid(column=2, row=10) self.convert_factor.set(2.5) convert_button = ctk.CTkButton( @@ -222,40 +245,60 @@ def __init__(self, parent): text="Convert", command=self.convert_refsig, ) - convert_button.grid(column=0, row=7, sticky=W) - - separator3 = ttk.Separator(self.head, orient="horizontal") - separator3.grid( - column=0, columnspan=3, row=8, sticky=(W, E), padx=5, pady=5, - ) + convert_button.grid(column=0, row=10, sticky=W) # Convert to percentage ctk.CTkLabel( self.head, text="MVC Value", font=("Segoe UI", 18, "bold"), - ).grid(column=1, row=9, sticky=(W, E)) + ).grid(column=1, row=11, sticky=(W, E)) percent_button = ctk.CTkButton( self.head, text="To Percent*", command=self.to_percent, ) - percent_button.grid(column=0, row=10, sticky=W) + percent_button.grid(column=0, row=12, sticky=W) self.mvc_value = DoubleVar() mvc = ctk.CTkEntry(self.head, width=100, textvariable=self.mvc_value) - mvc.grid(column=1, row=10) + mvc.grid(column=1, row=12) ctk.CTkLabel( self.head, text="*Use this button \nonly if your Refsig \nis in absolute values!", font=("Arial", 8), - ).grid(column=2, row=9, rowspan=2) + ).grid(column=2, row=11, rowspan=2) # Add padding to all children widgets of head for child in self.head.winfo_children(): child.grid_configure(padx=5, pady=5) - ### Define functions for Refsig editing + ### Define functions for signal editing + + def filter_emgsig(self): + # Get the bandpass frequency string + bandpass_freq = self.emg_bandpass_freq.get() + # Split the string into lowcut and highcut values + lowcut, highcut = map(int, bandpass_freq.split("-")) + + try: + # Filter RAW EMG + self.parent.resdict = openhdemg.filter_rawemg( + emgfile=self.parent.resdict, + order=int(self.emg_filter_order.get()), + lowcut=lowcut, + highcut=highcut, + ) + # Plot filtered Refsig + self.parent.in_gui_plotting( + resdict=self.parent.resdict, + ) # Re-plot main plot to indicate that something happened + + except AttributeError as e: + show_error_dialog( + parent=self, error=e, + solution=str("Make sure a file is loaded."), + ) def filter_refsig(self): """ @@ -278,7 +321,7 @@ def filter_refsig(self): # Filter refsig self.parent.resdict = openhdemg.filter_refsig( emgfile=self.parent.resdict, - order=int(self.filter_order.get()), + order=int(self.refsig_filter_order.get()), cutoff=int(self.cutoff_freq.get()), ) # Plot filtered Refsig diff --git a/openhdemg/gui/gui_modules/error_handler.py b/openhdemg/gui/gui_modules/error_handler.py index c6db5d7..a01933a 100644 --- a/openhdemg/gui/gui_modules/error_handler.py +++ b/openhdemg/gui/gui_modules/error_handler.py @@ -64,6 +64,7 @@ class ErrorDialog: >>> ErrorDialog(root, error, solution) >>> root.mainloop() """ + def __init__(self, parent, error, solution): self.parent = parent self.head = ctk.CTkToplevel(fg_color="#FFBF00") @@ -72,7 +73,7 @@ def __init__(self, parent, error, solution): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index a3b6643..89ab7b0 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -134,7 +134,7 @@ def __init__(self, parent): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.head.iconbitmap(default=iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) @@ -316,7 +316,7 @@ def __init__(self, parent): self.mat_code.set("Custom order") # Trace matrix code value - self.mat_code.trace_add("write", self.on_matrix_none) #TODO + self.mat_code.trace_add("write", self.on_matrix_none) # Matrix Orientation ctk.CTkLabel( @@ -337,7 +337,7 @@ def __init__(self, parent): self.mat_orientation.set("180") # Disable the orientation setting for DELSYS files if self.parent.resdict["SOURCE"] == "DELSYS": - orientation.config(state="disabled") + orientation.configure(state="disabled") # Plot derivation # Button @@ -401,7 +401,8 @@ def __init__(self, parent): self.muap_config.set("Configuration") # Disable config for DELSYS files if self.parent.resdict["SOURCE"] == "DELSYS": - config_muap.config(state="disabled") + config_muap.configure(state="disabled") + # NOTE config does not exist for CTk widgets. Use configure # Combobox MU Number self.muap_munum = StringVar() @@ -431,7 +432,7 @@ def __init__(self, parent): self.muap_time.set("Timewindow (ms)") # Disable Timewindow for DELSYS files if self.parent.resdict["SOURCE"] == "DELSYS": - timewindow.config(state="disabled") + timewindow.configure(state="disabled") # Matrix Illustration Graphic matrix_canvas = ctk.CTkCanvas( diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index 041d85f..9728b8f 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -112,7 +112,7 @@ def __init__(self, parent): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - iconpath = head_path + "/gui_files/Icon2.ico" + iconpath = head_path + "/gui_files/Icon_transp.ico" self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) @@ -143,7 +143,9 @@ def __init__(self, parent): # Compute MU re-/derecruitement threshold separator = ttk.Separator(self.head, orient="horizontal") - separator.grid(column=0, columnspan=4, row=2, padx=5, pady=5) + separator.grid( + column=0, columnspan=2, row=2, sticky=(W, E), padx=5, pady=5, + ) # TODO one row has been skipped between enter_mvc and separator thresh = ctk.CTkButton( self.head, diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 04d79da..2d678bd 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -38,7 +38,7 @@ import openhdemg.gui.settings as settings from openhdemg.gui.gui_modules import ( MURemovalWindow, - EditRefsig, + EditSig, GUIHelpers, AnalyseForce, MuAnalysis, @@ -47,12 +47,13 @@ show_error_dialog, ) - matplotlib.use("TkAgg") ctk.set_default_color_theme( os.path.dirname(os.path.abspath(__file__)) + "/gui_files/gui_color_theme.json" ) +# TODO are you sure this is working? Because you always specify the colors +# manually in the code class emgGUI(ctk.CTk): @@ -166,7 +167,7 @@ def __init__(self): master_path + "/gui_files/gui_color_theme.json" ) - iconpath = master_path + "/gui_files/Icon2.ico" + iconpath = master_path + "/gui_files/Icon_transp.ico" self.iconbitmap(iconpath) # Necessary for resizing @@ -298,8 +299,8 @@ def __init__(self): # Filter Reference Signal reference = ctk.CTkButton( self.left, - text="RefSig Editing", - command=lambda: (EditRefsig(parent=self)), + text="Signal Editing", + command=lambda: (EditSig(parent=self)), ) reference.grid(column=0, row=12, sticky=(N, S, E, W)) @@ -379,8 +380,8 @@ def __init__(self): # Create logo figure self.logo_canvas = Canvas( self.right, - height=800, - width=1000, + width=800, + height=600, bg="white", ) self.logo_canvas.grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) @@ -388,11 +389,13 @@ def __init__(self): logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) - # TODO make resizable and centre the logo + # TODO make resizable and centre the logo - it is now acceptable + # although not resizable nor centered self.logo_canvas.create_image( - 1000, - 500, + 400, + 300, image=self.logo, + anchor="center" ) # Create info buttons @@ -1194,22 +1197,19 @@ def in_gui_plotting(self, resdict, plot="idr"): "DELSYS_REFSIG", ]: self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=False, + emgfile=resdict, showimmediately=False, ) elif plot == "idr": self.fig = openhdemg.plot_idr( - emgfile=resdict, - showimmediately=False, - tight_layout=False, - # figsize=[900 * (2.54 / 96), 800 * (2.54 / 96)], + emgfile=resdict, showimmediately=False, ) elif plot == "refsig_fil": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=False, + emgfile=resdict, showimmediately=False, ) elif plot == "refsig_off": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, tight_layout=False, + emgfile=resdict, showimmediately=False, ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) From 73745830fde94886b72d0b597b477ebbf07ffeda Mon Sep 17 00:00:00 2001 From: Paul Ritsche Date: Sun, 31 Mar 2024 15:27:13 +0200 Subject: [PATCH 47/57] Added: terminal to main GUI --- openhdemg/gui/openhdemg_gui.py | 116 ++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 47 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index b3735b6..5d2bf53 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -11,7 +11,16 @@ import threading import webbrowser from tkinter import ( - messagebox, ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E, + messagebox, + ttk, + filedialog, + Canvas, + StringVar, + Tk, + N, + S, + W, + E, ) import customtkinter as ctk from pandastable import Table, config @@ -21,20 +30,26 @@ import matplotlib.pyplot as plt import matplotlib from matplotlib.backends.backend_tkagg import ( - FigureCanvasTkAgg, NavigationToolbar2Tk, + FigureCanvasTkAgg, + NavigationToolbar2Tk, ) import openhdemg.library as openhdemg import openhdemg.gui.settings as settings from openhdemg.gui.gui_modules import ( - MURemovalWindow, EditSig, GUIHelpers, AnalyseForce, MuAnalysis, PlotEmg, - AdvancedAnalysis, show_error_dialog, + MURemovalWindow, + EditSig, + GUIHelpers, + AnalyseForce, + MuAnalysis, + PlotEmg, + AdvancedAnalysis, + show_error_dialog, ) matplotlib.use("TkAgg") ctk.set_default_color_theme( - os.path.dirname(os.path.abspath(__file__)) - + "/gui_files/gui_color_theme.json" + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/gui_color_theme.json" ) @@ -128,7 +143,7 @@ class emgGUI(ctk.CTk): documentation in the library. """ - def __init__(self): + def __init__(self, *args, **kwargs): """ Initialization of GUI window upon calling. @@ -137,7 +152,7 @@ def __init__(self): : tk tk class object """ - super().__init__() + super().__init__(*args, **kwargs) # Load settings self.load_settings() @@ -145,9 +160,7 @@ def __init__(self): # Set up GUI self.title("openhdemg") master_path = os.path.dirname(os.path.abspath(__file__)) - ctk.set_default_color_theme( - master_path + "/gui_files/gui_color_theme.json" - ) + ctk.set_default_color_theme(master_path + "/gui_files/gui_color_theme.json") iconpath = master_path + "/gui_files/Icon_transp.ico" self.iconbitmap(iconpath) @@ -245,7 +258,8 @@ def __init__(self): export = ctk.CTkButton( self.left, text="Save Results", - command=lambda: (GUIHelpers(parent=self).export_to_excel())) + command=lambda: (GUIHelpers(parent=self).export_to_excel()), + ) export.grid(column=1, row=6, sticky=(N, S, E, W)) # View Motor Unit Firings @@ -370,12 +384,7 @@ def __init__(self): logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) - self.logo_canvas.create_image( - 400, - 300, - image=self.logo, - anchor="center" - ) + self.logo_canvas.create_image(400, 300, image=self.logo, anchor="center") # Create info buttons # Settings button @@ -511,6 +520,24 @@ def __init__(self): ) cite_button.grid(row=5, column=1, sticky=W, pady=(0, 20)) + # Create frame for output + self.terminal = ctk.CTkFrame( + self, + width=1000, + height=100, + fg_color="lightgrey", + border_width=2, + border_color="White", + ) + self.terminal.grid( + column=0, + row=21, + columnspan=2, + pady=8, + padx=10, + sticky=(N, S, W, E), + ) + for child in self.left.winfo_children(): child.grid_configure(padx=5, pady=5) @@ -924,7 +951,10 @@ def on_filetype_change(self, *args): text_color="black", ) self.verify_settings_text.grid( - column=0, row=2, sticky=(W, E), padx=5, + column=0, + row=2, + sticky=(W, E), + padx=5, ) def save_emgfile(self): @@ -1070,8 +1100,7 @@ def reset_analysis(self): ) # Update Filespecs self.n_channels.configure( - text="N Channels: " - + str(len(self.resdict["RAW_SIGNAL"].columns)), + text="N Channels: " + str(len(self.resdict["RAW_SIGNAL"].columns)), font=("Segoe UI", 15, ("bold")), ) self.n_of_mus.configure( @@ -1086,9 +1115,7 @@ def reset_analysis(self): else: # load refsig if self.filetype.get() == "OTB_REFSIG": - self.resdict = openhdemg.refsig_from_otb( - filepath=self.file_path - ) + self.resdict = openhdemg.refsig_from_otb(filepath=self.file_path) elif self.filetype.get() == "DELSYS_REFSIG": self.resdict = openhdemg.refsig_from_delsys( filepath=self.file_path, @@ -1104,8 +1131,7 @@ def reset_analysis(self): # Reconfigure labels for refsig self.n_channels.configure( - text="N Channels: " - + str(len(self.resdict["REF_SIGNAL"].columns)), + text="N Channels: " + str(len(self.resdict["REF_SIGNAL"].columns)), font=("Segoe UI", 15, ("bold")), ) self.n_of_mus.configure( @@ -1138,13 +1164,15 @@ def reset_analysis(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) except FileNotFoundError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -1176,19 +1204,23 @@ def in_gui_plotting(self, resdict, plot="idr"): "DELSYS_REFSIG", ]: self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) elif plot == "idr": self.fig = openhdemg.plot_idr( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) elif plot == "refsig_fil": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) elif plot == "refsig_off": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) @@ -1196,14 +1228,17 @@ def in_gui_plotting(self, resdict, plot="idr"): row=0, column=0, rowspan=6, sticky=(N, S, E, W), padx=5 ) toolbar = NavigationToolbar2Tk( - self.canvas, self.right, pack_toolbar=False, + self.canvas, + self.right, + pack_toolbar=False, ) toolbar.grid(row=5, column=0, sticky=S) plt.close() except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -1222,19 +1257,6 @@ def display_results(self, input_df): input_df : pd.DataFrame Dataftame containing the analysis results. """ - - # Create frame for output - self.terminal = ttk.LabelFrame( - self, - text="Result Output", - height=100, - relief="ridge", - ) - self.terminal.grid( - column=0, row=21, columnspan=2, pady=8, padx=10, - sticky=(N, S, W, E), - ) - # Display results table = Table( self.terminal, From c5c463501a81ad2c39e293b71c7b121c6135f9c6 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 31 Mar 2024 15:32:06 +0200 Subject: [PATCH 48/57] build(pyproject.toml): Added name and dynamic version for cz bump --- .cz.toml | 9 ++ .github/workflows/.pre-commit.yml | 16 +++ .pre-commit-config.yaml | 30 ++++++ CHANGELOG.md | 7 ++ openhdemg/gui/gui_modules/edit_mus.py | 27 ++++-- openhdemg/gui/gui_modules/edit_sig.py | 107 +++++++++++++++------ openhdemg/gui/gui_modules/error_handler.py | 38 +++++--- openhdemg/gui/gui_modules/gui_plotting.py | 7 +- openhdemg/gui/openhdemg_gui.py | 99 ++++++++++--------- pyproject.toml | 5 + reqs_for_devs.txt | Bin 5448 -> 5527 bytes requirements.txt | Bin 392 -> 437 bytes setup.py | 10 +- 13 files changed, 249 insertions(+), 106 deletions(-) create mode 100644 .cz.toml create mode 100644 .github/workflows/.pre-commit.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md diff --git a/.cz.toml b/.cz.toml new file mode 100644 index 0000000..6e00be4 --- /dev/null +++ b/.cz.toml @@ -0,0 +1,9 @@ +[tool] +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "pep440" +version_provider = "pep621" +version = "0.1.0-beta3" +update_changelog_on_bump = true +major_version_zero = true diff --git a/.github/workflows/.pre-commit.yml b/.github/workflows/.pre-commit.yml new file mode 100644 index 0000000..21b7dff --- /dev/null +++ b/.github/workflows/.pre-commit.yml @@ -0,0 +1,16 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.1 + with: + extra_args: black --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7dc3fe6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: mixed-line-ending + - id: name-tests-test + # - repo: local + # hooks: + # - id: pytest-run + # name: pytest-run + # stages: [commit] + # types: [python] + # entry: pytest + # language: system + # pass_filenames: false + # always_run: true + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + - repo: https://github.com/PyCQA/isort + rev: 5.10.1 + hooks: + - id: isort + args: ["--profile", "black"] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5a25a6f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.1.0 (2024-03-31) + +## 0.1.0-beta.3 (2023-12-04) + +## 0.1.0-beta.2 (2023-09-11) + +## 0.1.0-beta.1 (2023-07-04) diff --git a/openhdemg/gui/gui_modules/edit_mus.py b/openhdemg/gui/gui_modules/edit_mus.py index 5dbc568..0a5d8fa 100644 --- a/openhdemg/gui/gui_modules/edit_mus.py +++ b/openhdemg/gui/gui_modules/edit_mus.py @@ -1,9 +1,11 @@ """Module containing the MU Removal GUI class""" -from tkinter import StringVar, W, E import os from sys import platform +from tkinter import E, StringVar, W + import customtkinter as ctk + import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog @@ -94,11 +96,9 @@ def __init__(self, parent): self.head.title("Motor Unit Removal Window") # Set the icon for the window - head_path = os.path.dirname( - os.path.dirname(os.path.abspath(__file__)) - ) + head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon_transp.ico" - self.head.iconbitmap(default=iconpath) + self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) @@ -151,7 +151,8 @@ def __init__(self, parent): except AttributeError as e: self.head.destroy() show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -185,7 +186,9 @@ def remove(self): removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] removed_mu_value = list(map(str, removed_mu_value)) removed_mu = ctk.CTkComboBox( - self.head, width=10, variable=self.mu_to_remove, + self.head, + width=10, + variable=self.mu_to_remove, values=removed_mu_value, ) removed_mu.configure(state="readonly") @@ -199,7 +202,8 @@ def remove(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -231,7 +235,9 @@ def remove_empty(self): removed_mu_value = [*range(0, self.parent.resdict["NUMBER_OF_MUS"])] removed_mu_value = list(map(str, removed_mu_value)) removed_mu = ctk.CTkComboBox( - self.head, width=10, variable=self.mu_to_remove, + self.head, + width=10, + variable=self.mu_to_remove, values=removed_mu_value, ) removed_mu.configure(state="readonly") @@ -245,6 +251,7 @@ def remove_empty(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) diff --git a/openhdemg/gui/gui_modules/edit_sig.py b/openhdemg/gui/gui_modules/edit_sig.py index d0173aa..03853af 100644 --- a/openhdemg/gui/gui_modules/edit_sig.py +++ b/openhdemg/gui/gui_modules/edit_sig.py @@ -1,9 +1,11 @@ """Module containing the Resif editing class""" -from tkinter import ttk, W, E, StringVar, DoubleVar -import customtkinter as ctk import os from sys import platform +from tkinter import DoubleVar, E, StringVar, W, ttk + +import customtkinter as ctk + import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog @@ -110,7 +112,7 @@ def __init__(self, parent): head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon_transp.ico" - self.head.iconbitmap(default=iconpath) + self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) @@ -127,13 +129,19 @@ def __init__(self, parent): # Filter RAW EMG signal ctk.CTkLabel( - self.head, text="EMG Signal", font=("Segoe UI", 18, "bold"), + self.head, + text="EMG Signal", + font=("Segoe UI", 18, "bold"), ).grid(column=0, row=0, sticky=W) ctk.CTkLabel( - self.head, text="Filter Order", font=("Segoe UI", 18, "bold"), + self.head, + text="Filter Order", + font=("Segoe UI", 18, "bold"), ).grid(column=1, row=1, sticky=(W, E)) ctk.CTkLabel( - self.head, text="BandPass Freq", font=("Segoe UI", 18, "bold"), + self.head, + text="BandPass Freq", + font=("Segoe UI", 18, "bold"), ).grid(column=2, row=1, sticky=(W, E)) ctk.CTkButton( self.head, @@ -142,30 +150,43 @@ def __init__(self, parent): ).grid(column=0, row=2, sticky=W) self.emg_filter_order = StringVar() ctk.CTkEntry( - self.head, width=100, textvariable=self.emg_filter_order, + self.head, + width=100, + textvariable=self.emg_filter_order, ).grid(column=1, row=2) self.emg_filter_order.set(2) self.emg_bandpass_freq = StringVar() ctk.CTkEntry( - self.head, width=100, textvariable=self.emg_bandpass_freq, + self.head, + width=100, + textvariable=self.emg_bandpass_freq, ).grid(column=2, row=2) self.emg_bandpass_freq.set("20-500") - ttk.Separator( - self.head, orient="horizontal" - ).grid( - column=0, columnspan=3, row=3, sticky=(W, E), padx=5, pady=5, + ttk.Separator(self.head, orient="horizontal").grid( + column=0, + columnspan=3, + row=3, + sticky=(W, E), + padx=5, + pady=5, ) # Filter Refsig # Define Labels ctk.CTkLabel( - self.head, text="Reference Signal", font=("Segoe UI", 18, "bold"), + self.head, + text="Reference Signal", + font=("Segoe UI", 18, "bold"), ).grid(column=0, row=4, sticky=W) ctk.CTkLabel( - self.head, text="Filter Order", font=("Segoe UI", 18, "bold"), + self.head, + text="Filter Order", + font=("Segoe UI", 18, "bold"), ).grid(column=1, row=5, sticky=(W, E)) ctk.CTkLabel( - self.head, text="Cutoff Freq", font=("Segoe UI", 18, "bold"), + self.head, + text="Cutoff Freq", + font=("Segoe UI", 18, "bold"), ).grid(column=2, row=5, sticky=(W, E)) # Fiter button basic = ctk.CTkButton( @@ -176,14 +197,18 @@ def __init__(self, parent): basic.grid(column=0, row=6, sticky=W) self.refsig_filter_order = StringVar() ref_order = ctk.CTkEntry( - self.head, width=100, textvariable=self.refsig_filter_order, + self.head, + width=100, + textvariable=self.refsig_filter_order, ) ref_order.grid(column=1, row=6) self.refsig_filter_order.set(4) self.cutoff_freq = StringVar() cutoff = ctk.CTkEntry( - self.head, width=100, textvariable=self.cutoff_freq, + self.head, + width=100, + textvariable=self.cutoff_freq, ) cutoff.grid(column=2, row=6) self.cutoff_freq.set(15) @@ -206,7 +231,9 @@ def __init__(self, parent): self.offsetval = StringVar() offset = ctk.CTkEntry( - self.head, width=100, textvariable=self.offsetval, + self.head, + width=100, + textvariable=self.offsetval, ) offset.grid(column=1, row=8) self.offsetval.set(4) @@ -218,15 +245,21 @@ def __init__(self, parent): # Convert Reference signal ctk.CTkLabel( - self.head, text="Operator", font=("Segoe UI", 18, "bold"), + self.head, + text="Operator", + font=("Segoe UI", 18, "bold"), ).grid(column=1, row=9, sticky=(W, E)) ctk.CTkLabel( - self.head, text="Factor", font=("Segoe UI", 18, "bold"), + self.head, + text="Factor", + font=("Segoe UI", 18, "bold"), ).grid(column=2, row=9, sticky=(W, E)) self.convert = StringVar() convert = ctk.CTkComboBox( - self.head, width=100, variable=self.convert, + self.head, + width=100, + variable=self.convert, values=("Multiply", "Divide"), ) convert.configure(state="readonly") @@ -235,7 +268,9 @@ def __init__(self, parent): self.convert_factor = DoubleVar() factor = ctk.CTkEntry( - self.head, width=100, textvariable=self.convert_factor, + self.head, + width=100, + textvariable=self.convert_factor, ) factor.grid(column=2, row=10) self.convert_factor.set(2.5) @@ -249,7 +284,9 @@ def __init__(self, parent): # Convert to percentage ctk.CTkLabel( - self.head, text="MVC Value", font=("Segoe UI", 18, "bold"), + self.head, + text="MVC Value", + font=("Segoe UI", 18, "bold"), ).grid(column=1, row=11, sticky=(W, E)) percent_button = ctk.CTkButton( @@ -296,7 +333,8 @@ def filter_emgsig(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -326,12 +364,14 @@ def filter_refsig(self): ) # Plot filtered Refsig self.parent.in_gui_plotting( - resdict=self.parent.resdict, plot="refsig_fil", + resdict=self.parent.resdict, + plot="refsig_fil", ) except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a Refsig file is loaded."), ) @@ -361,12 +401,14 @@ def remove_offset(self): ) # Update Plot self.parent.in_gui_plotting( - resdict=self.parent.resdict, plot="refsig_off", + resdict=self.parent.resdict, + plot="refsig_off", ) except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a Refsig file is loaded."), ) @@ -404,12 +446,14 @@ def convert_refsig(self): # Update Plot self.parent.in_gui_plotting( - resdict=self.parent.resdict, plot="refsig_off", + resdict=self.parent.resdict, + plot="refsig_off", ) except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a Refsig file is loaded."), ) @@ -445,7 +489,8 @@ def to_percent(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a Refsig file is loaded."), ) diff --git a/openhdemg/gui/gui_modules/error_handler.py b/openhdemg/gui/gui_modules/error_handler.py index a01933a..c2fb3b9 100644 --- a/openhdemg/gui/gui_modules/error_handler.py +++ b/openhdemg/gui/gui_modules/error_handler.py @@ -1,8 +1,9 @@ """Module containing the error message designs""" import os -from sys import platform import traceback +from sys import platform + import customtkinter as ctk from PIL import Image @@ -74,7 +75,7 @@ def __init__(self, parent, error, solution): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon_transp.ico" - self.head.iconbitmap(default=iconpath) + self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) @@ -83,39 +84,52 @@ def __init__(self, parent, error, solution): # Create a frame for the content with blue background, placed in the # middle. self.content_frame = ctk.CTkFrame( - self.head, corner_radius=10, fg_color="LightBlue4", + self.head, + corner_radius=10, + fg_color="LightBlue4", bg_color="#FFBF00", ) self.content_frame.pack(padx=50, expand=True, fill="both") # Load an information icon and display it self.info_photo = ctk.CTkImage( - light_image=Image.open(path + "/Error.png"), size=(50, 50), + light_image=Image.open(path + "/Error.png"), + size=(50, 50), ) self.icon = ctk.CTkLabel( - self.content_frame, text="", image=self.info_photo, + self.content_frame, + text="", + image=self.info_photo, bg_color="LightBlue4", ) self.icon.pack(pady=5) self.icon_info = ctk.CTkLabel( - self.content_frame, text="INFORMATION", font=("Arial", 16, "bold"), + self.content_frame, + text="INFORMATION", + font=("Arial", 16, "bold"), text_color="#123456", ) self.icon_info.pack(pady=5) # Error solution label (larger, bold), placed below the icon self.solution_label = ctk.CTkLabel( - self.content_frame, text=solution, font=("Arial", 14, "bold"), - wraplength=400, fg_color="LightBlue4", + self.content_frame, + text=solution, + font=("Arial", 14, "bold"), + wraplength=400, + fg_color="LightBlue4", ) self.solution_label.pack(pady=(10, 5)) # Error traceback label (smaller) self.error_label = ctk.CTkLabel( - self.content_frame, text=error, font=("Arial", 10), wraplength=400, + self.content_frame, + text=error, + font=("Arial", 10), + wraplength=400, fg_color="LightBlue4", ) - self.error_label.pack(pady=(5, 10), expand=True, fill='both') + self.error_label.pack(pady=(5, 10), expand=True, fill="both") # Make window modal self.head.grab_set() # Redirect all events to this window @@ -158,7 +172,9 @@ def show_error_dialog(parent, error, solution): else: error_message = "".join( traceback.format_exception( - type(error), value=error, tb=error.__traceback__, + type(error), + value=error, + tb=error.__traceback__, ) ) diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 89ab7b0..9b35dbd 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -2,11 +2,12 @@ import os import webbrowser -from tkinter import ttk, W, E, N, S, StringVar, PhotoImage -from PIL import Image from sys import platform +from tkinter import E, N, PhotoImage, S, StringVar, W, ttk import customtkinter as ctk +from PIL import Image + import openhdemg.library as openhdemg from openhdemg.gui.gui_modules.error_handler import show_error_dialog @@ -135,7 +136,7 @@ def __init__(self, parent): # Set window icon head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon_transp.ico" - self.head.iconbitmap(default=iconpath) + self.head.iconbitmap(iconpath) if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index b3735b6..408562b 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -2,43 +2,42 @@ This file contains the gui functionalities of openhdemg. """ +import importlib import os -import sys import subprocess -import importlib - -import tkinter as tk +import sys import threading +import tkinter as tk import webbrowser -from tkinter import ( - messagebox, ttk, filedialog, Canvas, StringVar, Tk, N, S, W, E, -) +from tkinter import Canvas, E, N, S, StringVar, Tk, W, filedialog, messagebox, ttk + import customtkinter as ctk +import matplotlib +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from pandastable import Table, config - from PIL import Image -import matplotlib.pyplot as plt -import matplotlib -from matplotlib.backends.backend_tkagg import ( - FigureCanvasTkAgg, NavigationToolbar2Tk, -) - -import openhdemg.library as openhdemg import openhdemg.gui.settings as settings +import openhdemg.library as openhdemg from openhdemg.gui.gui_modules import ( - MURemovalWindow, EditSig, GUIHelpers, AnalyseForce, MuAnalysis, PlotEmg, - AdvancedAnalysis, show_error_dialog, + AdvancedAnalysis, + AnalyseForce, + EditSig, + GUIHelpers, + MuAnalysis, + MURemovalWindow, + PlotEmg, + show_error_dialog, ) matplotlib.use("TkAgg") ctk.set_default_color_theme( - os.path.dirname(os.path.abspath(__file__)) - + "/gui_files/gui_color_theme.json" + os.path.dirname(os.path.abspath(__file__)) + "/gui_files/gui_color_theme.json" ) -class emgGUI(ctk.CTk): +class emgGUI(tk.Tk): """ This class is used to create a graphical user interface for the openhdemg library. @@ -145,9 +144,7 @@ def __init__(self): # Set up GUI self.title("openhdemg") master_path = os.path.dirname(os.path.abspath(__file__)) - ctk.set_default_color_theme( - master_path + "/gui_files/gui_color_theme.json" - ) + ctk.set_default_color_theme(master_path + "/gui_files/gui_color_theme.json") iconpath = master_path + "/gui_files/Icon_transp.ico" self.iconbitmap(iconpath) @@ -245,7 +242,8 @@ def __init__(self): export = ctk.CTkButton( self.left, text="Save Results", - command=lambda: (GUIHelpers(parent=self).export_to_excel())) + command=lambda: (GUIHelpers(parent=self).export_to_excel()), + ) export.grid(column=1, row=6, sticky=(N, S, E, W)) # View Motor Unit Firings @@ -370,12 +368,7 @@ def __init__(self): logo_path = master_path + "/gui_files/logo.png" # Get logo path self.logo = tk.PhotoImage(file=logo_path) - self.logo_canvas.create_image( - 400, - 300, - image=self.logo, - anchor="center" - ) + self.logo_canvas.create_image(400, 300, image=self.logo, anchor="center") # Create info buttons # Settings button @@ -924,7 +917,10 @@ def on_filetype_change(self, *args): text_color="black", ) self.verify_settings_text.grid( - column=0, row=2, sticky=(W, E), padx=5, + column=0, + row=2, + sticky=(W, E), + padx=5, ) def save_emgfile(self): @@ -1070,8 +1066,7 @@ def reset_analysis(self): ) # Update Filespecs self.n_channels.configure( - text="N Channels: " - + str(len(self.resdict["RAW_SIGNAL"].columns)), + text="N Channels: " + str(len(self.resdict["RAW_SIGNAL"].columns)), font=("Segoe UI", 15, ("bold")), ) self.n_of_mus.configure( @@ -1086,9 +1081,7 @@ def reset_analysis(self): else: # load refsig if self.filetype.get() == "OTB_REFSIG": - self.resdict = openhdemg.refsig_from_otb( - filepath=self.file_path - ) + self.resdict = openhdemg.refsig_from_otb(filepath=self.file_path) elif self.filetype.get() == "DELSYS_REFSIG": self.resdict = openhdemg.refsig_from_delsys( filepath=self.file_path, @@ -1104,8 +1097,7 @@ def reset_analysis(self): # Reconfigure labels for refsig self.n_channels.configure( - text="N Channels: " - + str(len(self.resdict["REF_SIGNAL"].columns)), + text="N Channels: " + str(len(self.resdict["REF_SIGNAL"].columns)), font=("Segoe UI", 15, ("bold")), ) self.n_of_mus.configure( @@ -1138,13 +1130,15 @@ def reset_analysis(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) except FileNotFoundError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -1176,19 +1170,23 @@ def in_gui_plotting(self, resdict, plot="idr"): "DELSYS_REFSIG", ]: self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) elif plot == "idr": self.fig = openhdemg.plot_idr( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) elif plot == "refsig_fil": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) elif plot == "refsig_off": self.fig = openhdemg.plot_refsig( - emgfile=resdict, showimmediately=False, + emgfile=resdict, + showimmediately=False, ) self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) @@ -1196,14 +1194,17 @@ def in_gui_plotting(self, resdict, plot="idr"): row=0, column=0, rowspan=6, sticky=(N, S, E, W), padx=5 ) toolbar = NavigationToolbar2Tk( - self.canvas, self.right, pack_toolbar=False, + self.canvas, + self.right, + pack_toolbar=False, ) toolbar.grid(row=5, column=0, sticky=S) plt.close() except AttributeError as e: show_error_dialog( - parent=self, error=e, + parent=self, + error=e, solution=str("Make sure a file is loaded."), ) @@ -1231,7 +1232,11 @@ def display_results(self, input_df): relief="ridge", ) self.terminal.grid( - column=0, row=21, columnspan=2, pady=8, padx=10, + column=0, + row=21, + columnspan=2, + pady=8, + padx=10, sticky=(N, S, W, E), ) diff --git a/pyproject.toml b/pyproject.toml index f54900d..8b64c1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,7 @@ [build-system] requires = ["setuptools>=68.0"] + +[project] +dynamic = ["version"] +name = "openhdemg" +version = "0.1.0" diff --git a/reqs_for_devs.txt b/reqs_for_devs.txt index df18e5b0eea6601c627290a468c669d58051e796..fee988e1bacdd9a83b1612c8432b983222c4dbe7 100644 GIT binary patch delta 1313 zcmY*ZO=}ZT6un70NoFRQnS9hzV~wSh5=)vk=~pO4Du^GoU{}&bk+w#g*0iM`NJWq$ z1vgU5qm(XOxDXeDATIm?3Q9DWVKWEonBVD(tyeb4NdA*!)b|% zj3EsgvKs1+X)TQNT9$E7ONqZ%Siu6#F;~9MhTij{V2(Y)36vRePOdAmtB!={NNZHR z!4u3l8btlYAnNS|3ydFwe30r#5cQfvSU=}sle!-|#lQDLEiG`O!u_nV8}2l~2Tz0g z9BNCs8D*XU*C*`#+mD6OQ!boiydk(1zQD0hPpO|_N1_47qMr45*6W_4PLUsWTW#oI zc&%p`zv^)7w+`lJ13e50P6|FSu>RD@i1HIW)v&A zsCXi{6~$e6DOioVPF&;3nxGLHqpPCby*q}oEmSC4P<-9O*=<^=^TGn_zJ)r+7CK4W zU>&kEa-&zclXBf#_l$t>;-L+O=Qg}JuyIdnHqdrC|n)zg7737ytkO delta 1262 zcmYLJPe{{Y82&c*fBWtKu(52dA!26pw>is$h>>)tK^;Uqc$j~nWv->JQYi2c<{?Zz zmWNIrLOOKrAR!V#(J6$WgLsmT649YV=y~34g!~>~zW4o}_kExD{kGBd>HLd6i?M#G z9Wb5-ET@x(@~A7Ss3z33x~m58msJ;)*+66cLke++nE}R4Afl)y)l4Ifk(crHsHS&X z{I|v5Cm!+YTb6{y(6g2fV9k;Tevy+O7SLbgs-UOl7I3yX znQh%)QH$a;ah`?h^=^ketb0=Mso<7}^?dYn0*^eb*6U@p884?g>ZP~pm2g$h_WUj5?0PrwK{&BKxthW|PwQ=GLvc25=DOgmy zInTXFrV;TF9hI>a^!B0`?spdZos8z>_h3OS;dT*R_4kuSL{?r=eQT=fj=YkxFmRgu z@N1#hqUjW?cmVb}#8mKqQljc_s2;T!73HH;I~L Date: Sun, 31 Mar 2024 15:41:58 +0200 Subject: [PATCH 49/57] refactor(openhdemg_gui.py): Adapted ctk class from which emgGUI() inherits --- openhdemg/gui/openhdemg_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index b8ec172..5e359e9 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -37,7 +37,7 @@ ) -class emgGUI(tk.Tk): +class emgGUI(ctk.CTk): """ This class is used to create a graphical user interface for the openhdemg library. From d89fb6a0e4ed900876bf74d3abe9a9f6d211330d Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Sun, 31 Mar 2024 18:32:12 +0200 Subject: [PATCH 50/57] Minor fixes --- openhdemg/library/muap.py | 33 ++++++++++++++++++++++++++++----- openhdemg/library/plotemg.py | 3 ++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/openhdemg/library/muap.py b/openhdemg/library/muap.py index 79a18b9..7f085f0 100644 --- a/openhdemg/library/muap.py +++ b/openhdemg/library/muap.py @@ -371,6 +371,7 @@ def sta( # Compute half of the timewindow in samples timewindow_samples = round((timewindow / 1000) * emgfile["FSAMP"]) halftime = round(timewindow_samples / 2) + tottime = halftime * 2 # Container of the STA for every MUs # {0: {}, 1: {}, 2: {}, 3: {}} @@ -378,23 +379,35 @@ def sta( # Calculate STA on sorted_rawemg for every mu and put it into sta_dict[mu] for mu in sta_dict.keys(): + # Check if there are firings in this MU + tot_firings = len(emgfile["MUPULSES"][mu]) + if tot_firings == 0: + raise ValueError( + "Empty MU in sta(). First use delete_empty_mus()." + ) + # Set firings if firings="all" if firings == "all": - firings_ = [0, len(emgfile["MUPULSES"][mu])] + firings_ = [0, tot_firings] else: firings_ = firings + # Get current mupulses + thismups = emgfile["MUPULSES"][mu][firings_[0]: firings_[1]] + # Calculate STA for each column in sorted_rawemg sorted_rawemg_sta = {} for col in sorted_rawemg.keys(): row_dict = {} for row in sorted_rawemg[col].columns: - thismups = emgfile["MUPULSES"][mu][firings_[0]: firings_[1]] - df = sorted_rawemg[col][row].to_numpy() + emg_array = sorted_rawemg[col][row].to_numpy() # Calculate STA using NumPy vectorized operations sta_values = [] for pulse in thismups: - sta_values.append(df[pulse - halftime: pulse + halftime]) + ls = emg_array[pulse - halftime: pulse + halftime] + # Avoid incomplete muaps + if len(ls) == tottime: + sta_values.append(ls) row_dict[row] = np.mean(sta_values, axis=0) sorted_rawemg_sta[col] = pd.DataFrame(row_dict) sta_dict[mu] = sorted_rawemg_sta @@ -470,6 +483,7 @@ def st_muap(emgfile, sorted_rawemg, timewindow=50): # Compute half of the timewindow in samples timewindow_samples = round((timewindow / 1000) * emgfile["FSAMP"]) halftime = round(timewindow_samples / 2) + tottime = halftime * 2 # Container of the ST for every MUs # {0: {}, 1: {}, 2: {}, 3: {} ...} @@ -477,6 +491,13 @@ def st_muap(emgfile, sorted_rawemg, timewindow=50): # Calculate ST on sorted_rawemg for every mu and put it into sta_dict[mu] for mu in sta_dict.keys(): + # Check if there are firings in this MU + tot_firings = len(emgfile["MUPULSES"][mu]) + if tot_firings == 0: + raise ValueError( + "Empty MU in sta(). First use delete_empty_mus()." + ) + # Container for the st of each MUs' matrix column. sta_dict_cols = {} # Get MUPULSES for this MU @@ -492,7 +513,9 @@ def st_muap(emgfile, sorted_rawemg, timewindow=50): # Calculate ST using NumPy vectorized operations for pos, pulse in enumerate(thismups): muap = this_emgsig[pulse - halftime: pulse + halftime] - crow_muaps[pos] = muap + # Avoid incomplete muaps + if len(muap) == tottime: + crow_muaps[pos] = muap sta_dict_crows[row] = pd.DataFrame(crow_muaps) sta_dict_cols[col] = sta_dict_crows sta_dict[mu] = sta_dict_cols diff --git a/openhdemg/library/plotemg.py b/openhdemg/library/plotemg.py index 1c88a8d..7732b29 100644 --- a/openhdemg/library/plotemg.py +++ b/openhdemg/library/plotemg.py @@ -158,7 +158,8 @@ def plot_emgsig( else: raise TypeError( - "While calling the plot_emgsig function, you should pass an integer, a list or 'all' to channels" + "While calling the plot_emgsig function, you should pass an " + + "integer or a list to channels" ) if addrefsig: From 49a269262f4d40d8a8e11ff09d2be1512b64e35c Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:02:18 +0200 Subject: [PATCH 51/57] GUI fixes completed Completed bug fixing of GUI modules. All the functionalities have been verified 1 by 1, with all the test files available to me and by combining various tasks. The GUI is now robust and reliable. --- MANIFEST.in | 6 +- openhdemg/gui/gui_files/Logo_high_res.png | Bin 0 -> 182222 bytes openhdemg/gui/gui_files/logo.png | Bin 123219 -> 0 bytes openhdemg/gui/gui_modules/edit_sig.py | 27 +- openhdemg/gui/gui_modules/gui_helpers.py | 49 ++- openhdemg/gui/gui_modules/gui_plotting.py | 32 +- openhdemg/gui/gui_modules/mu_properties.py | 8 +- openhdemg/gui/openhdemg_gui.py | 352 +++++++++++---------- 8 files changed, 263 insertions(+), 211 deletions(-) create mode 100644 openhdemg/gui/gui_files/Logo_high_res.png delete mode 100644 openhdemg/gui/gui_files/logo.png diff --git a/MANIFEST.in b/MANIFEST.in index 53f47b0..6722f5c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,13 +4,15 @@ include LICENSE include requirements.txt # Add all the non .py files in openhdemg -include openhdemg/gui/gui_files/Icon.ico +include openhdemg/gui/gui_files/Icon_transp.ico include openhdemg/gui/gui_files/Cite.png include openhdemg/gui/gui_files/Contact.png +include openhdemg/gui/gui_files/Gear.png include openhdemg/gui/gui_files/Info.png -include openhdemg/gui/gui_files/logo.png +include openhdemg/gui/gui_files/Logo_high_res.png include openhdemg/gui/gui_files/Matrix.png include openhdemg/gui/gui_files/Online.png include openhdemg/gui/gui_files/Redirect.png +include openhdemg/gui/gui_files/gui_color_theme.json include openhdemg/library/decomposed_test_files/otb_testfile.mat diff --git a/openhdemg/gui/gui_files/Logo_high_res.png b/openhdemg/gui/gui_files/Logo_high_res.png new file mode 100644 index 0000000000000000000000000000000000000000..191d8c687eb0881531ce2d03be0a0ef490ee1c82 GIT binary patch literal 182222 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 literal 0 HcmV?d00001 diff --git a/openhdemg/gui/gui_files/logo.png b/openhdemg/gui/gui_files/logo.png deleted file mode 100644 index b889201bf7cff4d11317d08bc5472c8358b48f41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123219 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� 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%0A0Nfet;4hPq_+x*G@I`H>wTg>krhn>ZsFaFJupj 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

S84>T(1j>i7zVHr~07x|{;5ueUX}PQ= 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

U2o_1bghvIX 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 M+)kGNcJ9yr0@n6$00000 diff --git a/openhdemg/gui/gui_modules/edit_sig.py b/openhdemg/gui/gui_modules/edit_sig.py index 03853af..8397adc 100644 --- a/openhdemg/gui/gui_modules/edit_sig.py +++ b/openhdemg/gui/gui_modules/edit_sig.py @@ -266,7 +266,8 @@ def __init__(self, parent): convert.grid(column=1, row=10) self.convert.set("Multiply") - self.convert_factor = DoubleVar() + # DoubleVar does not support - sign, use StringVar + self.convert_factor = StringVar() factor = ctk.CTkEntry( self.head, width=100, @@ -313,6 +314,20 @@ def __init__(self, parent): ### Define functions for signal editing def filter_emgsig(self): + """ + Instance method that filters the EMG signal based on user selected + specs. + + Raises + ------ + AttributeError + When no emgfile is available. + + See Also + -------- + filter_rawemg in library. + """ + # Get the bandpass frequency string bandpass_freq = self.emg_bandpass_freq.get() # Split the string into lowcut and highcut values @@ -435,13 +450,14 @@ def convert_refsig(self): """ try: + convert_factor = float(self.convert_factor.get()) if self.convert.get() == "Multiply": self.parent.resdict["REF_SIGNAL"] = ( - self.parent.resdict["REF_SIGNAL"] * self.convert_factor.get() + self.parent.resdict["REF_SIGNAL"] * convert_factor ) elif self.convert.get() == "Divide": self.parent.resdict["REF_SIGNAL"] = ( - self.parent.resdict["REF_SIGNAL"] / self.convert_factor.get() + self.parent.resdict["REF_SIGNAL"] / convert_factor ) # Update Plot @@ -485,7 +501,10 @@ def to_percent(self): self.parent.resdict["REF_SIGNAL"] * 100 ) / self.mvc_value.get() # Update Plot - self.parent.in_gui_plotting(resdict=self.parent.resdict) + self.parent.in_gui_plotting( + resdict=self.parent.resdict, + plot="refsig_off", + ) except AttributeError as e: show_error_dialog( diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 160e2ef..68b1d7e 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -93,11 +93,17 @@ def resize_file(self): """ try: + title = ( + "Select the start/end area of the steady-state by hovering the" + + " mouse \nand pressing the 'a'-key. Wrong points can be " + + "removed with right \nclick or canc/delete key. When ready, " + + "press enter." + ) # Open selection window for user points = openhdemg.showselect( emgfile=self.parent.resdict, how=self.parent.settings.resize_emgfile__how, - title="Select the start/end area to resize by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start, end = points[0], points[1] @@ -105,7 +111,8 @@ def resize_file(self): # Delsys requires different handling for resize if self.parent.resdict["SOURCE"] == "DELSYS": self.parent.resdict, _, _ = openhdemg.resize_emgfile( - emgfile=self.parent.resdict, area=[start, end], accuracy="maintain" + emgfile=self.parent.resdict, area=[start, end], + accuracy="maintain", ) else: self.parent.resdict, _, _ = openhdemg.resize_emgfile( @@ -114,14 +121,29 @@ def resize_file(self): accuracy=self.parent.settings.resize_emgfile__accuracy, ignore_negative_ipts=self.parent.settings.resize_emgfile__ignore_negative_ipts, ) - # Update Plot - self.parent.in_gui_plotting(resdict=self.parent.resdict) - # Update filelength - self.parent.file_length.configure( - text="N of MUs: " + str(self.parent.resdict["EMG_LENGTH"]), - font=("Segoe UI", 15, "bold"), - ) + # Update Plot + if self.parent.resdict["SOURCE"] in [ + "DEMUSE", "OTB", "CUSTOMCSV", "DELSYS", + ]: + self.parent.in_gui_plotting(resdict=self.parent.resdict) + # Update filelength + self.parent.file_length.configure( + text="N of MUs: " + str(self.parent.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) + elif self.parent.resdict["SOURCE"] in [ + "OTB_REFSIG", "CUSTOMCSV_REFSIG", "DELSYS_REFSIG", + ]: + self.parent.in_gui_plotting( + resdict=self.parent.resdict, plot="refsig_off", + ) + # Update filelength + length = len(self.parent.resdict["REF_SIGNAL"].index) + self.parent.file_length.configure( + text="N of MUs: " + str(length), + font=("Segoe UI", 15, "bold"), + ) except AttributeError as e: show_error_dialog( @@ -134,7 +156,10 @@ def resize_file(self): show_error_dialog( parent=self, error=e, - solution=str("Verify settings for resize_emgfile()."), + solution=str( + "Wrong number of points in showselect() or \n " + + "Verify settings for resize_emgfile()." + ), ) def export_to_excel(self): @@ -166,7 +191,7 @@ def export_to_excel(self): if hasattr(self.parent, "rfd"): self.parent.rfd.to_excel(writer, sheet_name="RFD") - if hasattr(self.parent, "exportable_df"): + if hasattr(self.parent, "mu_prop_df"): self.parent.mu_prop_df.to_excel( writer, sheet_name="Basic MU Properties" ) @@ -175,7 +200,7 @@ def export_to_excel(self): self.parent.mus_dr.to_excel(writer, sheet_name="MU Discharge Rate") if hasattr(self.parent, "mu_thresholds"): - self.mu_thresholds.to_excel(writer, sheet_name="MU Thresholds") + self.parent.mu_thresholds.to_excel(writer, sheet_name="MU Thresholds") writer.close() diff --git a/openhdemg/gui/gui_modules/gui_plotting.py b/openhdemg/gui/gui_modules/gui_plotting.py index 9b35dbd..bffe838 100644 --- a/openhdemg/gui/gui_modules/gui_plotting.py +++ b/openhdemg/gui/gui_modules/gui_plotting.py @@ -364,7 +364,10 @@ def __init__(self, parent): # Combobox Matrix self.deriv_matrix = StringVar() - mat_column_values = ("col0", "col1", "col2", "col3", "col4") + mat_column_values = ( + "col0", "col1", "col2", "col3", "col4", "col5", "col6", + "col7", "col8", "col9", "col10", "col11", + ) mat_column = ctk.CTkComboBox( self.head, width=100, @@ -407,9 +410,14 @@ def __init__(self, parent): # Combobox MU Number self.muap_munum = StringVar() - mu_numbers = tuple( - str(number) for number in range(0, self.parent.resdict["NUMBER_OF_MUS"]) - ) + if self.parent.resdict["SOURCE"] in [ + "DEMUSE", "OTB", "CUSTOMCSV", "DELSYS", + ]: + mu_numbers = tuple( + str(number) for number in range(0, self.parent.resdict["NUMBER_OF_MUS"]) + ) + else: + mu_numbers = () # Exception of refsig only files muap_munum = ctk.CTkComboBox( self.head, width=15, @@ -1018,22 +1026,6 @@ def plot_muaps(self): ), ) - show_error_dialog( # TODO Paul do we need two of these show_error_dialog? - parent=self, - error=e, - solution=str( - "Enter valid input parameters." - + "\nPotenital error sources:" - + "\n - Matrix Code" - + "\n - Matrix Orientation" - + "\n - Figure size arguments" - + "\n - Timewindow" - + "\n - MU Number" - + "\n - Rows,Columns arguments" - + "\n - custom_sorting_order in settings" - ), - ) - except UnboundLocalError as e: show_error_dialog( parent=self, diff --git a/openhdemg/gui/gui_modules/mu_properties.py b/openhdemg/gui/gui_modules/mu_properties.py index 9728b8f..ea75c24 100644 --- a/openhdemg/gui/gui_modules/mu_properties.py +++ b/openhdemg/gui/gui_modules/mu_properties.py @@ -387,7 +387,8 @@ def compute_mu_dr(self): except AttributeError as e: show_error_dialog( - parent=self, error=e, solution=str("Make sure a file is loaded.") + parent=self, error=e, + solution=str("Make sure a file is loaded."), ) except ValueError as e: @@ -395,7 +396,8 @@ def compute_mu_dr(self): parent=self, error=e, solution=str( - "Enter valid Firings value or select a correct number of points." + "Enter valid Firings value or select a correct number of " + + "points." ), ) @@ -442,6 +444,8 @@ def basic_mus_properties(self): ignore_negative_ipts=self.parent.settings.basic_mus_properties__ignore_negative_ipts, constrain_pulses=self.parent.settings.basic_mus_properties__constrain_pulses, mvc=float(self.mvc_value.get()), + start_steady=-1, + end_steady=-1, ) # Display results self.parent.display_results(self.parent.mu_prop_df) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 5e359e9..7ca8a1e 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -4,9 +4,9 @@ import importlib import os +import copy import subprocess import sys -import threading import tkinter as tk import webbrowser from tkinter import Canvas, E, N, S, StringVar, Tk, W, filedialog, messagebox, ttk @@ -15,7 +15,7 @@ import matplotlib import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk -from pandastable import Table, config +from pandastable import Table from PIL import Image import openhdemg.gui.settings as settings @@ -74,11 +74,15 @@ class emgGUI(ctk.CTk): TK self window containing all widget children for this GUI. self.resdict : dict Dictionary derived from input EMG file for further analysis. + self.resdict_copy_of_original : dict + A deepcopy of self.resdict stored for resetting the analyses. self.right : tk.frame Left frame inside of self that contains plotting canvas. self.terminal : ttk.Labelframe Tkinter labelframe that is used to display the results table in the GUI. + self.processing_indicator : ctk.CTkButton + Button used to indicate that the file is loading/saving. self.info : tk.PhotoImage Information Icon displayed in GUI. self.online : tk.Photoimage @@ -136,6 +140,7 @@ def __init__(self, *args, **kwargs): : tk tk class object """ + super().__init__(*args, **kwargs) # Load settings @@ -153,6 +158,7 @@ def __init__(self, *args, **kwargs): self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=5) self.rowconfigure(0, weight=1) + self.rowconfigure(1, weight=1) # Output self.minsize(width=600, height=400) # Create left side framing for functionalities @@ -201,6 +207,16 @@ def __init__(self, *args, **kwargs): ) load.grid(column=0, row=3, sticky=(N, S, E, W)) + # Button to indicate that the file is loading + self.processing_indicator = ctk.CTkButton( + self.left, + text="Processing!", + fg_color="#FFA500", # Orange + ) + self.processing_indicator.grid(column=0, row=4, sticky=(N, S, E, W)) + # Hide, don't forget to preserve rowconfigure settings + self.processing_indicator.lower() + # File specifications ctk.CTkLabel( self.left, @@ -356,23 +372,36 @@ def __init__(self, *args, **kwargs): for row in range(5): self.right.rowconfigure(row, weight=1) - # Create logo figure + # Create logo canvas figure self.logo_canvas = Canvas( self.right, width=800, height=600, bg="white", ) - self.logo_canvas.grid(row=0, column=0, rowspan=6, sticky=(N, S, E, W)) - - logo_path = master_path + "/gui_files/logo.png" # Get logo path - self.logo = tk.PhotoImage(file=logo_path) + self.logo_canvas.grid( + row=0, column=0, rowspan=6, sticky=(N, S, E, W), pady=(5, 0), + ) - self.logo_canvas.create_image(400, 300, image=self.logo, anchor="center") + # Load the logo as a resizable matplotlib figure + logo_path = master_path + "/gui_files/Logo_high_res.png" + logo = plt.imread(logo_path) + logo_fig, ax = plt.subplots() + ax.imshow(logo) + ax.axis('off') # Turn off axis + logo_fig.tight_layout() # Adjust layout padding + + # Plot the figure in the in_gui_plotting canvas + self.canvas = FigureCanvasTkAgg(logo_fig, master=self.logo_canvas) + self.canvas.get_tk_widget().pack( + expand=True, fill="both", padx=5, pady=5, + ) + plt.close() + # This solution is more flexible and memory efficient than previously. # Create info buttons # Settings button - gear_path = master_path + "/gui_files/gear.png" + gear_path = master_path + "/gui_files/Gear.png" self.gear = ctk.CTkImage( light_image=Image.open(gear_path), size=(30, 30), @@ -505,20 +534,15 @@ def __init__(self, *args, **kwargs): cite_button.grid(row=5, column=1, sticky=W, pady=(0, 20)) # Create frame for output - self.terminal = ctk.CTkFrame( - self, - width=1000, - height=100, - fg_color="lightgrey", - border_width=2, - border_color="White", + self.terminal = ttk.LabelFrame( + self, text="Result Output", height=150, relief="ridge", ) self.terminal.grid( column=0, - row=21, + row=1, columnspan=2, - pady=8, - padx=10, + pady=5, + padx=5, sticky=(N, S, W, E), ) @@ -566,6 +590,9 @@ def get_file_input(self): Executed when the button "Load File" in master GUI window is pressed. + This creates both an object containing the file to use and an object + to reset to the original file. + See Also -------- emg_from_otb, emg_from_demuse, emg_from_delsys, emg_from_customcsv, @@ -762,9 +789,9 @@ def load_file(): # Add filename to label self.title(self.filename) - # End progress - progress.grid_remove() - progress.stop() + # Lower processing_indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() # This sections is used for refsig loading as they do not # require the filespecs to be loaded. @@ -833,11 +860,21 @@ def load_file(): font=("Segoe UI", 15, "bold"), ) - # End progress - progress.stop() - progress.grid_remove() + # Lower processing_indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() + + # If file succesfully loaded, delete previous analyses results + self.delete_previous_analyses_results() - return + # Make a copy of the loaded file + self.resdict_copy_of_original = copy.deepcopy(self.resdict) + + # Display the loaded file + if self.resdict["SOURCE"] in ["DEMUSE", "OTB", "CUSTOMCSV", "DELSYS"]: + self.in_gui_plotting(self.resdict) + else: + self.in_gui_plotting(self.resdict, plot="refsig_off") except ValueError as e: show_error_dialog( @@ -850,9 +887,9 @@ def load_file(): + "specify the correct folder." ), ) - # End progress - progress.stop() - progress.grid_remove() + # Lower processing_indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() except FileNotFoundError as e: show_error_dialog( @@ -863,9 +900,9 @@ def load_file(): + "according to your specification." ), ) - # End progress - progress.stop() - progress.grid_remove() + # Lower processing_indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() except TypeError as e: show_error_dialog( @@ -876,9 +913,9 @@ def load_file(): + "according to your specification." ), ) - # End progress - progress.stop() - progress.grid_remove() + # Lower processing_indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() except KeyError as e: show_error_dialog( @@ -889,30 +926,27 @@ def load_file(): + "according to your specification." ), ) - # End progress - progress.stop() - progress.grid_remove() + # Lower processing_indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() except: - # End progress - progress.stop() - progress.grid_remove() + # Lower processing_indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() # Re-Load settings self.load_settings() - # Indicate Progress - progress = ctk.CTkProgressBar( - self.left, - mode="indeterminate", - width=100, - ) - progress.grid(row=4, column=0) - progress.start() + # Display the processing indicator + self.processing_indicator.lift() + + # Call the function to load the file + load_file() - # Create a thread to run the load_file function - load_thread = threading.Thread(target=load_file) - load_thread.start() + # Remove file loading indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() def on_filetype_change(self, *args): """ @@ -959,54 +993,36 @@ def save_emgfile(self): save_json_emgfile in library. """ - def save_file(): - try: - # Ask user to select the directory and file name - save_filepath = filedialog.asksaveasfilename( - defaultextension=".json", - filetypes=(("JSON files", "*.json"), ("all files", "*.*")), - ) - - if not save_filepath: - # End progress - progress.stop() - progress.grid_remove() - return # User canceled the file dialog - - # Get emgfile - save_emg = self.resdict - - # Save json file - openhdemg.save_json_emgfile( - emgfile=save_emg, - filepath=save_filepath, - compresslevel=self.settings.save_json_emgfile__compresslevel, - ) - - # End progress - progress.stop() - progress.grid_remove() - - return - - except AttributeError as e: - show_error_dialog( - parent=self, - error=e, - solution=str("Make sure a file is loaded."), - ) - # Re-Load settings self.load_settings() - # Indicate Progress - progress = ctk.CTkProgressBar(self.left, mode="indeterminate") - progress.grid(row=4, column=0) - progress.start() + # Display the processing indicator + self.processing_indicator.lift() - # Create a thread to run the save_file function - save_thread = threading.Thread(target=save_file) - save_thread.start() + # Save the file + try: + openhdemg.asksavefile( + emgfile=self.resdict, + compresslevel=self.settings.save_json_emgfile__compresslevel, + ) + except AttributeError as e: + # Remove file saving indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() + # Show error + show_error_dialog( + parent=self, + error=e, + solution=str("Make sure a file is loaded."), + ) + except Exception: + # Remove file saving indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() + + # Remove file saving indicator + if hasattr(self, "processing_indicator"): + self.processing_indicator.lower() def reset_analysis(self): """ @@ -1035,53 +1051,13 @@ def reset_analysis(self): # user decided to rest analysis try: - # reload original file - if self.filetype.get() in [ - "OTB", - "DEMUSE", - "OPENHDEMG", - "CUSTOMCSV", - "DELSYS", + # Revert to original file + if self.resdict["SOURCE"] in [ + "OTB", "DEMUSE", "CUSTOMCSV", "DELSYS", ]: - if self.filetype.get() == "OTB": - self.resdict = openhdemg.emg_from_otb( - filepath=self.file_path, - ext_factor=self.settings.emg_from_otb__ext_factor, - refsig=self.settings.emg_from_otb__refsig, - extras=self.settings.emg_from_otb__extras, - ignore_negative_ipts=self.settings.emg_from_otb__ignore_negative_ipts, - ) - - elif self.filetype.get() == "DEMUSE": - self.resdict = openhdemg.emg_from_demuse( - filepath=self.file_path, - ignore_negative_ipts=self.settings.emg_from_demuse__ignore_negative_ipts, - ) + # Use the resdict copy + self.resdict = self.resdict_copy_of_original - elif self.filetype.get() == "OPENHDEMG": - self.resdict = openhdemg.emg_from_json(filepath=self.file_path) - - elif self.filetype.get() == "CUSTOMCSV": - self.resdict = openhdemg.emg_from_customcsv( - filepath=self.file_path, - ref_signal=self.settings.emg_from_customcsv__ref_signal, - raw_signal=self.settings.emg_from_customcsv__raw_signal, - ipts=self.settings.emg_from_customcsv__ipts, - mupulses=self.settings.emg_from_customcsv__mupulses, - binary_mus_firing=self.settings.emg_from_customcsv__binary_mus_firing, - accuracy=self.settings.emg_from_customcsv__accuracy, - extras=self.settings.emg_from_customcsv__extras, - fsamp=self.settings.emg_from_customcsv__fsamp, - ied=self.settings.emg_from_customcsv__ied, - ) - elif self.filetype.get() == "DELSYS": - self.resdict = openhdemg.emg_from_delsys( - rawemg_filepath=self.file_path, - mus_directory=self.mus_path, - emg_sensor_name=self.settings.emg_from_delsys__emg_sensor_name, - refsig_sensor_name=self.settings.emg_from_delsys__refsig_sensor_name, - filename_from=self.settings.emg_from_delsys__filename_from, - ) # Update Filespecs self.n_channels.configure( text="N Channels: " + str(len(self.resdict["RAW_SIGNAL"].columns)), @@ -1096,22 +1072,14 @@ def reset_analysis(self): font=("Segoe UI", 15, "bold"), ) - else: - # load refsig - if self.filetype.get() == "OTB_REFSIG": - self.resdict = openhdemg.refsig_from_otb(filepath=self.file_path) - elif self.filetype.get() == "DELSYS_REFSIG": - self.resdict = openhdemg.refsig_from_delsys( - filepath=self.file_path, - refsig_sensor_name=self.settings.refsig_from_delsys__refsig_sensor_name, - ) - elif self.filetype.get() == "CUSTOMCSV_REFSIG": - self.resdict = openhdemg.refsig_from_customcsv( - filepath=self.file_path, - ref_signal=self.settings.refsig_from_customcsv__ref_signal, - extras=self.settings.refsig_from_customcsv__extras, - fsamp=self.settings.refsig_from_customcsv__fsamp, - ) + # Update Plot + self.in_gui_plotting(resdict=self.resdict) + + elif self.resdict["SOURCE"] in [ + "OTB_REFSIG", "CUSTOMCSV_REFSIG", "DELSYS_REFSIG" + ]: + # Use the resdict copy + self.resdict = self.resdict_copy_of_original # Reconfigure labels for refsig self.n_channels.configure( @@ -1128,23 +1096,25 @@ def reset_analysis(self): font=("Segoe UI", 15, "bold"), ) - # Update Plot - if hasattr(self, "fig"): - self.in_gui_plotting(resdict=self.resdict) + # Update Plot + self.in_gui_plotting(resdict=self.resdict, plot="refsig_off") # Clear frame for output if hasattr(self, "terminal"): self.terminal = ttk.LabelFrame( - self, text="Result Output", height=100, relief="ridge" + self, text="Result Output", height=150, relief="ridge" ) self.terminal.grid( column=0, - row=21, + row=1, columnspan=2, - pady=8, - padx=10, + pady=5, + padx=5, sticky=(N, S, W, E), - ) + ) # Repeat original settings in init + + # Delete previous analyses results + self.delete_previous_analyses_results() except AttributeError as e: show_error_dialog( @@ -1160,6 +1130,23 @@ def reset_analysis(self): solution=str("Make sure a file is loaded."), ) + def delete_previous_analyses_results(self): + """ + Instance method to delete the objects storing the analyses results. + """ + + # Check for attributes and delete them if present + if hasattr(self, "mvc_df"): + del self.mvc_df + if hasattr(self, "rfd"): + del self.rfd + if hasattr(self, "mu_prop_df"): + del self.mu_prop_df + if hasattr(self, "mus_dr"): + del self.mus_dr + if hasattr(self, "mu_thresholds"): + del self.mu_thresholds + # ---------------------------------------------------------------------------------------------- # Plotting inside of GUI @@ -1207,16 +1194,24 @@ def in_gui_plotting(self, resdict, plot="idr"): showimmediately=False, ) - self.canvas = FigureCanvasTkAgg(self.fig, master=self.right) - self.canvas.get_tk_widget().grid( - row=0, column=0, rowspan=6, sticky=(N, S, E, W), padx=5 + # Remove previous figure + if hasattr(self, 'canvas'): + self.canvas.get_tk_widget().destroy() + + # Pack figure inside logo_canvas. This is more reliable and should + # be more efficient. + self.canvas = FigureCanvasTkAgg(self.fig, master=self.logo_canvas) + self.canvas.get_tk_widget().pack( + expand=True, fill="both", padx=5, pady=5, ) + + # Add toolbar toolbar = NavigationToolbar2Tk( self.canvas, self.right, pack_toolbar=False, ) - toolbar.grid(row=5, column=0, sticky=S) + toolbar.grid(row=5, column=0, sticky=(S, E), padx=5, pady=5) plt.close() except AttributeError as e: @@ -1241,7 +1236,22 @@ def display_results(self, input_df): input_df : pd.DataFrame Dataftame containing the analysis results. """ + # Display results + # Clear/recreate frame for output + if hasattr(self, "terminal"): + self.terminal = ttk.LabelFrame( + self, text="Result Output", height=150, relief="ridge", + ) + self.terminal.grid( + column=0, + row=1, + columnspan=2, + pady=5, + padx=5, + sticky=(N, S, W, E), + ) # Repeat original settings in init + table = Table( self.terminal, dataframe=input_df, @@ -1252,8 +1262,8 @@ def display_results(self, input_df): ) # Resize column width - options = {"cellwidth": 10} - config.apply_options(options, table) + """ options = {"cellwidth": 10} + config.apply_options(options, table) """ # Show results table.show() From 49255a35045757021caa35ce97999bad5c49cc00 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:04:35 +0200 Subject: [PATCH 52/57] Fixes and improvements Only minor changes --- openhdemg/library/analysis.py | 28 ++++++++++++++++++++++++---- openhdemg/library/muap.py | 2 +- openhdemg/library/openfiles.py | 10 +++++++++- openhdemg/library/tools.py | 17 +++++++---------- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/openhdemg/library/analysis.py b/openhdemg/library/analysis.py index f7c1a9b..1f6dbed 100644 --- a/openhdemg/library/analysis.py +++ b/openhdemg/library/analysis.py @@ -317,11 +317,16 @@ def compute_dr( idr = compute_idr(emgfile=emgfile) # Check if we need to manually select the area for the steady-state phase + title = ( + "Select the start/end area of the steady-state by hovering the mouse" + + "\nand pressing the 'a'-key. Wrong points can be removed with right " + + "\nclick or canc/delete key. When ready, press enter." + ) if event_ == "rec_derec_steady" or event_ == "steady": if (start_steady < 0 and end_steady < 0) or (start_steady < 0 or end_steady < 0): points = showselect( emgfile, - title="Select the start/end area of the steady-state by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_steady, end_steady = points[0], points[1] @@ -564,10 +569,15 @@ def basic_mus_properties( # TODO make new examples, also with accuracy # Check if we need to select the steady-state phase + title = ( + "Select the start/end area of the steady-state by hovering the mouse" + + "\nand pressing the 'a'-key. Wrong points can be removed with right " + + "\nclick or canc/delete key. When ready, press enter." + ) if (start_steady < 0 and end_steady < 0) or (start_steady < 0 or end_steady < 0): points = showselect( emgfile, - title="Select the start/end area of the steady-state by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_steady, end_steady = points[0], points[1] @@ -864,10 +874,15 @@ def compute_covisi( # Check if we need to manually select the area for the steady-state # phase. if event_ == "rec_derec_steady" or event_ == "steady": + title = ( + "Select the start/end area of the steady-state by hovering the mouse" + + "\nand pressing the 'a'-key. Wrong points can be removed with right " + + "\nclick or canc/delete key. When ready, press enter." + ) if (start_steady < 0 and end_steady < 0) or (start_steady < 0 or end_steady < 0): points = showselect( emgfile, - title="Select the start/end area of the steady-state by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_steady, end_steady = points[0], points[1] @@ -1058,10 +1073,15 @@ def compute_drvariability( # Check if we need to manually select the area for the steady-state phase if event_ == "rec_derec_steady" or event_ == "steady": + title = ( + "Select the start/end area of the steady-state by hovering the mouse" + + "\nand pressing the 'a'-key. Wrong points can be removed with right " + + "\nclick or canc/delete key. When ready, press enter." + ) if (start_steady < 0 and end_steady < 0) or (start_steady < 0 or end_steady < 0): points = showselect( emgfile, - title="Select the start/end area of the steady-state by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_steady, end_steady = points[0], points[1] diff --git a/openhdemg/library/muap.py b/openhdemg/library/muap.py index 7f085f0..5ec8543 100644 --- a/openhdemg/library/muap.py +++ b/openhdemg/library/muap.py @@ -1627,7 +1627,7 @@ def __init__( "..", "gui", "gui_files", - "Icon.ico" + "Icon_transp.ico" ) self.root.iconbitmap(iconpath) diff --git a/openhdemg/library/openfiles.py b/openhdemg/library/openfiles.py index 4a39999..68e8cd0 100644 --- a/openhdemg/library/openfiles.py +++ b/openhdemg/library/openfiles.py @@ -1844,7 +1844,15 @@ def emg_from_json(filepath): ref_signal.sort_index(inplace=True) # ACCURACY accuracy = pd.read_json(jsonemgfile["ACCURACY"], orient='split') - accuracy.columns = accuracy.columns.astype(int) + try: + accuracy.columns = accuracy.columns.astype(int) + except Exception: + accuracy.columns = [*range(len(accuracy.columns))] + warnings.warn( + "Error while loading accuracy, check or recalculate accuracy" + ) + # TODO error occurring when accuracy was recalculated on empty MUs. + # Check if the error is present also for other params. accuracy.index = accuracy.index.astype(int) accuracy.sort_index(inplace=True) # IPTS diff --git a/openhdemg/library/tools.py b/openhdemg/library/tools.py index c8c609d..af520aa 100644 --- a/openhdemg/library/tools.py +++ b/openhdemg/library/tools.py @@ -315,11 +315,10 @@ def resize_emgfile( # Resize the reference signal and identify the first value of the # index to resize the mupulses. Then, reset the index. rs_emgfile["REF_SIGNAL"] = rs_emgfile["REF_SIGNAL"].loc[start_:end_] - first_idx = rs_emgfile["REF_SIGNAL"].index[0] rs_emgfile["REF_SIGNAL"] = rs_emgfile["REF_SIGNAL"].reset_index(drop=True) - rs_emgfile["RAW_SIGNAL"] = ( - rs_emgfile["RAW_SIGNAL"].loc[start_:end_].reset_index(drop=True) - ) + rs_emgfile["RAW_SIGNAL"] = rs_emgfile["RAW_SIGNAL"].loc[start_:end_] + first_idx = rs_emgfile["RAW_SIGNAL"].index[0] + rs_emgfile["RAW_SIGNAL"] = rs_emgfile["RAW_SIGNAL"].reset_index(drop=True) rs_emgfile["IPTS"] = rs_emgfile["IPTS"].loc[start_:end_].reset_index(drop=True) rs_emgfile["EMG_LENGTH"] = int(len(rs_emgfile["RAW_SIGNAL"].index)) rs_emgfile["BINARY_MUS_FIRING"] = ( @@ -341,20 +340,19 @@ def resize_emgfile( if rs_emgfile["NUMBER_OF_MUS"] > 0: if not rs_emgfile["IPTS"].empty: # Calculate SIL - to_append = [] for mu in range(rs_emgfile["NUMBER_OF_MUS"]): res = compute_sil( ipts=rs_emgfile["IPTS"][mu], mupulses=rs_emgfile["MUPULSES"][mu], ignore_negative_ipts=ignore_negative_ipts, ) - to_append.append(res) - rs_emgfile["ACCURACY"] = pd.DataFrame(to_append) + rs_emgfile["ACCURACY"].iloc[mu] = res else: raise ValueError( - "Impossible to calculate ACCURACY (SIL). IPTS not found." + - " If IPTS is not present or empty, set accuracy='maintain'" + "Impossible to calculate ACCURACY (SIL). IPTS not " + + "found. If IPTS is not present or empty, set " + + "accuracy='maintain'" ) elif accuracy == "maintain": @@ -370,7 +368,6 @@ def resize_emgfile( elif emgfile["SOURCE"] in ["OTB_REFSIG", "CUSTOMCSV_REFSIG", "DELSYS_REFSIG"]: rs_emgfile["REF_SIGNAL"] = rs_emgfile["REF_SIGNAL"].loc[start_:end_] - first_idx = rs_emgfile["REF_SIGNAL"].index[0] rs_emgfile["REF_SIGNAL"] = rs_emgfile["REF_SIGNAL"].reset_index(drop=True) return rs_emgfile, start_, end_ From 4294beaa3582fb9523e59e297b2afa9365b367b7 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:32:20 +0200 Subject: [PATCH 53/57] Final GUI fixes and improvements --- openhdemg/gui/backup_settings.py | 102 ++++++++++++++++++ .../gui/{gui_modules => gui_files}/Error.png | Bin openhdemg/gui/gui_modules/edit_sig.py | 2 +- openhdemg/gui/gui_modules/error_handler.py | 4 +- openhdemg/gui/gui_modules/gui_helpers.py | 46 +++----- openhdemg/gui/settings.py | 5 +- 6 files changed, 122 insertions(+), 37 deletions(-) create mode 100644 openhdemg/gui/backup_settings.py rename openhdemg/gui/{gui_modules => gui_files}/Error.png (100%) diff --git a/openhdemg/gui/backup_settings.py b/openhdemg/gui/backup_settings.py new file mode 100644 index 0000000..fb335ea --- /dev/null +++ b/openhdemg/gui/backup_settings.py @@ -0,0 +1,102 @@ +""" +Module docstring explaining how to change the GUI settings. + +The settings can be related to both the GUI appearence and the analyses +functions. Parameters for the analyses functions are clustered based on the +openhdemg library's modules (here described as #----- MODULE NAME -----) and +can be better known from the API section of the openhdemg website. + +Each parameter for the analyses functions is named FunctionName__Parameter. +An extensive explanation of each "Parameter" can be found in the specific +API module and in the specific "FunctionName". + +A tutorial on how to use this settings file is available at: +https://www.giacomovalli.com/openhdemg/gui_settings/ + +If you mess up with the settings, you can restore the default ones from +backup_settings.py +""" + +import numpy as np + +# --------------------------------- openfiles --------------------------------- + +# in emg_from_demuse() +emg_from_demuse__ignore_negative_ipts = False + +# in emg_from_otb() +emg_from_otb__ext_factor = 8 +emg_from_otb__refsig = [True, "fullsampled"] +emg_from_otb__extras = None +emg_from_otb__ignore_negative_ipts = False + +# in refsig_from_otb() +refsig_from_otb__refsig = "fullsampled" +refsig_from_otb__extras = None + +# in emg_from_delsys() +emg_from_delsys__emg_sensor_name = "Galileo sensor" +emg_from_delsys__refsig_sensor_name = "Trigno Load Cell" +emg_from_delsys__filename_from = "mus_directory" + +# in refsig_from_delsys() +refsig_from_delsys__refsig_sensor_name = "Trigno Load Cell" + +# in emg_from_customcsv() +emg_from_customcsv__ref_signal = "REF_SIGNAL" +emg_from_customcsv__raw_signal = "RAW_SIGNAL" +emg_from_customcsv__ipts = "IPTS" +emg_from_customcsv__mupulses = "MUPULSES" +emg_from_customcsv__binary_mus_firing = "BINARY_MUS_FIRING" +emg_from_customcsv__accuracy = "ACCURACY" +emg_from_customcsv__extras = "EXTRAS" +emg_from_customcsv__fsamp = 2048 +emg_from_customcsv__ied = 8 + +# in refsig_from_customcsv() +refsig_from_customcsv__ref_signal = "REF_SIGNAL" +refsig_from_customcsv__extras = "EXTRAS" +refsig_from_customcsv__fsamp = 2048 + +# in save_json_emgfile() +save_json_emgfile__compresslevel = 4 + + +# ---------------------------------- analysis --------------------------------- + +# in compute_thresholds() +compute_thresholds__n_firings = 1 + +# in basic_mus_properties() +basic_mus_properties__n_firings_rt_dert = 1 +basic_mus_properties__accuracy = "default" +basic_mus_properties__ignore_negative_ipts = False +basic_mus_properties__constrain_pulses = [True, 3] + + +# ----------------------------------- tools ----------------------------------- + +# in resize_emgfile() +resize_emgfile__how = "ref_signal" +resize_emgfile__accuracy = "recalculate" +resize_emgfile__ignore_negative_ipts = False + + +# ------------------------------------ muap ----------------------------------- +# in tracking() +tracking__firings = "all" +tracking__derivation = "sd" + +# in remove_duplicates_between() +remove_duplicates_between__firings = "all" +remove_duplicates_between__derivation = "sd" + +# in MUcv_gui() +MUcv_gui__n_firings = [0, 50] +MUcv_gui__muaps_timewindow = 50 +MUcv_gui__figsize = [25, 20] + +# --------------------------------- electrodes -------------------------------- +# This custom sorting order is valid for all the GUI windows, although the +# documentation is accessible in the api of the electrodes module. +custom_sorting_order = None diff --git a/openhdemg/gui/gui_modules/Error.png b/openhdemg/gui/gui_files/Error.png similarity index 100% rename from openhdemg/gui/gui_modules/Error.png rename to openhdemg/gui/gui_files/Error.png diff --git a/openhdemg/gui/gui_modules/edit_sig.py b/openhdemg/gui/gui_modules/edit_sig.py index 8397adc..d23e562 100644 --- a/openhdemg/gui/gui_modules/edit_sig.py +++ b/openhdemg/gui/gui_modules/edit_sig.py @@ -108,7 +108,7 @@ def __init__(self, parent): # Create new window self.head = ctk.CTkToplevel() - self.head.title("Reference Signal Editing Window") + self.head.title("Signal Editing Window") head_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) iconpath = head_path + "/gui_files/Icon_transp.ico" diff --git a/openhdemg/gui/gui_modules/error_handler.py b/openhdemg/gui/gui_modules/error_handler.py index c2fb3b9..a3a6ca4 100644 --- a/openhdemg/gui/gui_modules/error_handler.py +++ b/openhdemg/gui/gui_modules/error_handler.py @@ -79,8 +79,6 @@ def __init__(self, parent, error, solution): if platform.startswith("win"): self.head.after(200, lambda: self.head.iconbitmap(iconpath)) - path = os.path.dirname(os.path.abspath(__file__)) - # Create a frame for the content with blue background, placed in the # middle. self.content_frame = ctk.CTkFrame( @@ -93,7 +91,7 @@ def __init__(self, parent, error, solution): # Load an information icon and display it self.info_photo = ctk.CTkImage( - light_image=Image.open(path + "/Error.png"), + light_image=Image.open(head_path + "/gui_files/Error.png"), size=(50, 50), ) self.icon = ctk.CTkLabel( diff --git a/openhdemg/gui/gui_modules/gui_helpers.py b/openhdemg/gui/gui_modules/gui_helpers.py index 68b1d7e..415aa26 100644 --- a/openhdemg/gui/gui_modules/gui_helpers.py +++ b/openhdemg/gui/gui_modules/gui_helpers.py @@ -92,18 +92,16 @@ def resize_file(self): showselect, resize_emgfile in library. """ + # TODO this try/except is not catching when the user closes the plot to + # select the resizing area (from the X button without selecting points). + # Instead, the error appears when closing the GUI. Same happens for mu + # properties analysis. try: - title = ( - "Select the start/end area of the steady-state by hovering the" - + " mouse \nand pressing the 'a'-key. Wrong points can be " - + "removed with right \nclick or canc/delete key. When ready, " - + "press enter." - ) # Open selection window for user points = openhdemg.showselect( emgfile=self.parent.resdict, how=self.parent.settings.resize_emgfile__how, - title=title, + title="Select the start/end area to resize by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", titlesize=10, ) start, end = points[0], points[1] @@ -121,29 +119,14 @@ def resize_file(self): accuracy=self.parent.settings.resize_emgfile__accuracy, ignore_negative_ipts=self.parent.settings.resize_emgfile__ignore_negative_ipts, ) - # Update Plot - if self.parent.resdict["SOURCE"] in [ - "DEMUSE", "OTB", "CUSTOMCSV", "DELSYS", - ]: - self.parent.in_gui_plotting(resdict=self.parent.resdict) - # Update filelength - self.parent.file_length.configure( - text="N of MUs: " + str(self.parent.resdict["EMG_LENGTH"]), - font=("Segoe UI", 15, "bold"), - ) - elif self.parent.resdict["SOURCE"] in [ - "OTB_REFSIG", "CUSTOMCSV_REFSIG", "DELSYS_REFSIG", - ]: - self.parent.in_gui_plotting( - resdict=self.parent.resdict, plot="refsig_off", - ) - # Update filelength - length = len(self.parent.resdict["REF_SIGNAL"].index) - self.parent.file_length.configure( - text="N of MUs: " + str(length), - font=("Segoe UI", 15, "bold"), - ) + self.parent.in_gui_plotting(resdict=self.parent.resdict) + + # Update filelength + self.parent.file_length.configure( + text="N of MUs: " + str(self.parent.resdict["EMG_LENGTH"]), + font=("Segoe UI", 15, "bold"), + ) except AttributeError as e: show_error_dialog( @@ -157,9 +140,8 @@ def resize_file(self): parent=self, error=e, solution=str( - "Wrong number of points in showselect() or \n " - + "Verify settings for resize_emgfile()." - ), + "Wrong number of inputs or verify settings for " + + "resize_emgfile()."), ) def export_to_excel(self): diff --git a/openhdemg/gui/settings.py b/openhdemg/gui/settings.py index 498d047..fb335ea 100644 --- a/openhdemg/gui/settings.py +++ b/openhdemg/gui/settings.py @@ -11,7 +11,10 @@ API module and in the specific "FunctionName". A tutorial on how to use this settings file is available at: -# TODO add link to docs tutorial +https://www.giacomovalli.com/openhdemg/gui_settings/ + +If you mess up with the settings, you can restore the default ones from +backup_settings.py """ import numpy as np From dbcddc5592fa5ecc1ca814204e4ea774a5f3b31b Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:33:15 +0200 Subject: [PATCH 54/57] Improved docstrings and descriptions --- openhdemg/library/analysis.py | 85 +++++++++++++++++----------- openhdemg/library/electrodes.py | 20 +++++-- openhdemg/library/mathtools.py | 49 ++++++++++++++++- openhdemg/library/muap.py | 16 ++++++ openhdemg/library/openfiles.py | 7 +++ openhdemg/library/plotemg.py | 9 ++- openhdemg/library/tools.py | 98 +++++++++++++++++++++++++++------ 7 files changed, 228 insertions(+), 56 deletions(-) diff --git a/openhdemg/library/analysis.py b/openhdemg/library/analysis.py index 1f6dbed..d8f297f 100644 --- a/openhdemg/library/analysis.py +++ b/openhdemg/library/analysis.py @@ -32,8 +32,10 @@ def compute_thresholds( ``rt_dert`` Both recruitment and derecruitment tresholds will be calculated. + ``rt`` Only recruitment tresholds will be calculated. + ``dert`` Only derecruitment tresholds will be calculated. type_ : str {"abs_rel", "rel", "abs"}, default "abs_rel" @@ -41,8 +43,10 @@ def compute_thresholds( ``abs_rel`` Both absolute and relative tresholds will be calculated. + ``rel`` Only relative tresholds will be calculated. + ``abs`` Only absolute tresholds will be calculated. n_firings : int, default 1 @@ -214,17 +218,21 @@ def compute_dr( event_ : str {"rec_derec_steady", "rec", "derec", "rec_derec", "steady"}, default "rec_derec_steady" When to calculate the DR. - ``rec_derec_steady`` - DR is calculated at recruitment, derecruitment and during the - steady-state phase. - ``rec`` - DR is calculated at recruitment. - ``derec`` - DR is calculated at derecruitment. - ``rec_derec`` - DR is calculated at recruitment and derecruitment. - ``steady`` - DR is calculated during the steady-state phase. + ``rec_derec_steady`` + DR is calculated at recruitment, derecruitment and during the + steady-state phase. + + ``rec`` + DR is calculated at recruitment. + + ``derec`` + DR is calculated at derecruitment. + + ``rec_derec`` + DR is calculated at recruitment and derecruitment. + + ``steady`` + DR is calculated during the steady-state phase. Returns ------- @@ -494,10 +502,13 @@ def basic_mus_properties( ``default`` The original accuracy measure already contained in the emgfile is returned without any computation. + ``SIL`` The Silhouette score is computed. + ``PNR`` The pulse to noise ratio is computed. + ``SIL_PNR`` Both the Silhouette score and the pulse to noise ratio are computed. @@ -774,17 +785,21 @@ def compute_covisi( event_ : str {"rec_derec_steady", "rec", "derec", "rec_derec", "steady"}, default "rec_derec_steady" When to calculate the COVisi. - ``rec_derec_steady`` - covisi is calculated at recruitment, derecruitment and during - the steady-state phase. - ``rec`` - covisi is calculated at recruitment. - ``derec`` - covisi is calculated at derecruitment. - ``rec_derec`` - covisi is calculated at recruitment and derecruitment. - ``steady`` - covisi is calculated during the steady-state phase. + ``rec_derec_steady`` + covisi is calculated at recruitment, derecruitment and during + the steady-state phase. + + ``rec`` + covisi is calculated at recruitment. + + ``derec`` + covisi is calculated at derecruitment. + + ``rec_derec`` + covisi is calculated at recruitment and derecruitment. + + ``steady`` + covisi is calculated during the steady-state phase. single_mu_number : int, default -1 Number of the specific MU to compute the COVisi. If single_mu_number >= 0, only the COVisi of the entire contraction @@ -988,17 +1003,21 @@ def compute_drvariability( event_ : str {"rec_derec_steady", "rec", "derec", "rec_derec", "steady"}, default "rec_derec_steady" When to calculate the DR variability. - ``rec_derec_steady`` - DR variability is calculated at recruitment, derecruitment and - during the steady-state phase. - ``rec`` - DR variability is calculated at recruitment. - ``derec`` - DR variability is calculated at derecruitment. - ``rec_derec`` - DR variability is calculated at recruitment and derecruitment. - ``steady`` - DR variability is calculated during the steady-state phase. + ``rec_derec_steady`` + DR variability is calculated at recruitment, derecruitment and + during the steady-state phase. + + ``rec`` + DR variability is calculated at recruitment. + + ``derec`` + DR variability is calculated at derecruitment. + + ``rec_derec`` + DR variability is calculated at recruitment and derecruitment. + + ``steady`` + DR variability is calculated during the steady-state phase. Returns ------- diff --git a/openhdemg/library/electrodes.py b/openhdemg/library/electrodes.py index 8e36dbc..f565ca1 100644 --- a/openhdemg/library/electrodes.py +++ b/openhdemg/library/electrodes.py @@ -166,11 +166,17 @@ def sort_rawemg( The code of the matrix used. It can be one of: ``GR08MM1305`` + ``GR04MM1305`` + ``GR10MM0808`` + ``Trigno Galileo Sensor`` + ``Custom order`` + ``None`` + If "None", the electodes are not sorted but n_rows and n_cols must be specified when dividebycolumn == True. If "Custom order", the electrodes are sorted based on @@ -199,8 +205,8 @@ def sort_rawemg( Specifically, the number of columns are defined by len(custom_sorting_order) while the number of rows by len(custom_sorting_order[0]). np.nan can be used to specify empty - channels. Please refer to the Examples section for the structure of - the custom sorting order. + channels. Please refer to the Notes and Examples section for the + structure of the custom sorting order. Returns ------- @@ -216,6 +222,9 @@ def sort_rawemg( ----- The returned file is called ``sorted_rawemg`` for convention. + Additional info on how to create the custom sorting order is available at: + https://www.giacomovalli.com/openhdemg/gui_settings/#electrodes + Examples -------- Sort emgfile RAW_SIGNAL and divide it by columns with built-in sorting @@ -290,6 +299,9 @@ def sort_rawemg( The custom_sorting_order refers to a grid of 13 rows and 5 columns with the empty channel in last position. + Additional info on how to create the custom sorting order is available at: + https://www.giacomovalli.com/openhdemg/gui_settings/#electrodes + >>> import openhdemg.library as emg >>> emgfile = emg.askopenfile(filesource="OTB", otb_ext_factor=8) >>> custom_sorting_order = [ @@ -356,7 +368,7 @@ def sort_rawemg( channel. Channel Order GR08MM1305 - 0 1 2 3 4 + 0 1 2 3 4 0 64 39 38 13 12 1 63 40 37 14 11 2 62 41 36 15 10 @@ -382,7 +394,7 @@ def sort_rawemg( elif orientation == 180: """ Channel Order GR08MM1305 - 0 1 2 3 4 + 0 1 2 3 4 0 NaN 25 26 51 52 1 1 24 27 50 53 2 2 23 28 49 54 diff --git a/openhdemg/library/mathtools.py b/openhdemg/library/mathtools.py index 39eee73..f08b7aa 100644 --- a/openhdemg/library/mathtools.py +++ b/openhdemg/library/mathtools.py @@ -74,6 +74,7 @@ def norm_xcorr(sig1, sig2, out="both"): ``both`` The output is the greatest positive or negative cross-correlation value. + ``max`` The output is the maximum cross-correlation value. @@ -137,10 +138,12 @@ def norm_twod_xcorr(df1, df2, mode="full"): ``full`` The output is the full discrete linear cross-correlation of the inputs. (Default) + ``valid`` The output consists only of those elements that do not rely on the zero-padding. In 'valid' mode, either `sta_mu1` or `sta_mu2` must be at least as large as the other in every dimension. + ``same`` The output is the same size as `in1`, centered with respect to the 'full' output. @@ -262,6 +265,20 @@ def compute_sil(ipts, mupulses, ignore_negative_ipts=False): See also -------- - compute_pnr : to calculate the Pulse to Noise ratio of a single MU. + + Examples + -------- + Calculate the SIL score for the third MU (MU number 2) ignoring the + negative component of the decomposed source. + + >>> import openhdemg.library as emg + >>> emgfile = emg.emg_from_samplefile() + >>> mu_of_interest = 2 + >>> sil_value = emg.compute_sil( + ... ipts=emgfile["IPTS"][mu_of_interest], + ... mupulses=emgfile["MUPULSES"][mu_of_interest], + ... ignore_negative_ipts=True, + ... ) """ # Manage exception of no firings @@ -390,7 +407,7 @@ def compute_pnr( However, this heuristic penalty function penalizes MUs firing during specific types of contractions like explosive contractions (MUs discharge up to 200 pps). - Therefore, in this implementation of the PNR, we did not include the + Therefore, in this implementation of the PNR, we did ``not`` include the penalty based on MUs discharge. Additionally, the user can decide whether to adopt the two coefficients of variations to estimate Pi or not. @@ -398,6 +415,36 @@ def compute_pnr( Pi = CoVIDI + CoVpIDI Otherwise, Pi would be calculated as: Pi = CoV_all_IDI + + Examples + -------- + Calculate the PNR value for the third MU (MU number 2) forcing the + selction of the times of firing. + + >>> import openhdemg.library as emg + >>> emgfile = emg.emg_from_samplefile() + >>> mu_of_interest = 2 + >>> pnr_value = emg.compute_pnr( + ... ipts=emgfile["IPTS"][mu_of_interest], + ... mupulses=emgfile["MUPULSES"][mu_of_interest], + ... fsamp=emgfile["FSAMP"], + ... constrain_pulses=[True, 3], + ... ) + + Calculate the PNR value for the third MU (MU number 2) selecting the times + of firing based on the euristic penalty funtion described in Holobar 2012 + and considering, separately, the paired and the non-paired firings. + + >>> import openhdemg.library as emg + >>> emgfile = emg.emg_from_samplefile() + >>> mu_of_interest = 2 + >>> pnr_value = emg.compute_pnr( + ... ipts=emgfile["IPTS"][mu_of_interest], + ... mupulses=emgfile["MUPULSES"][mu_of_interest], + ... fsamp=emgfile["FSAMP"], + ... constrain_pulses=[False], + ... separate_paired_firings=True, + ... ) """ # Manage exception of no firings diff --git a/openhdemg/library/muap.py b/openhdemg/library/muap.py index 5ec8543..b0a2067 100644 --- a/openhdemg/library/muap.py +++ b/openhdemg/library/muap.py @@ -771,8 +771,10 @@ def tracking( The range of firings to be used for the STA. If a MU has less firings than the range, the upper limit is adjusted accordingly. + ``all`` The STA is calculated over all the firings. + A list can be passed as [start, stop] e.g., [0, 25] to compute the STA on the first 25 firings. derivation : str {mono, sd, dd}, default sd @@ -787,10 +789,15 @@ def tracking( The code of the matrix used. It can be one of: ``GR08MM1305`` + ``GR04MM1305`` + ``GR10MM0808`` + ``Custom order`` + ``None`` + This is necessary to sort the channels in the correct order. If matrixcode="None", the electrodes are not sorted. In this case, n_rows and n_cols must be specified. @@ -1158,8 +1165,10 @@ def remove_duplicates_between( The range of firings to be used for the STA. If a MU has less firings than the range, the upper limit is adjusted accordingly. + ``all`` The STA is calculated over all the firings. + A list can be passed as [start, stop] e.g., [0, 25] to compute the STA on the first 25 firings. derivation : str {mono, sd, dd}, default sd @@ -1174,10 +1183,15 @@ def remove_duplicates_between( The code of the matrix used. It can be one of: ``GR08MM1305`` + ``GR04MM1305`` + ``GR10MM0808`` + ``Custom order`` + ``None`` + This is necessary to sort the channels in the correct order. If matrixcode="None", the electrodes are not sorted. In this case, n_rows and n_cols must be specified. @@ -1226,6 +1240,7 @@ def remove_duplicates_between( ``munumber`` Duplicated MUs are removed from the file with more MUs. + ``accuracy`` The MU with the lowest accuracy is removed. @@ -1567,6 +1582,7 @@ class MUcv_gui(): The range of firings to be used for the STA. If a MU has less firings than the range, the upper limit is adjusted accordingly. + ``all`` The STA is calculated over all the firings. muaps_timewindow : int, default 50 diff --git a/openhdemg/library/openfiles.py b/openhdemg/library/openfiles.py index 68e8cd0..639d3cf 100644 --- a/openhdemg/library/openfiles.py +++ b/openhdemg/library/openfiles.py @@ -1946,21 +1946,28 @@ def askopenfile(initialdir="/", filesource="OPENHDEMG", **kwargs): ``OPENHDEMG`` File saved from openhdemg (.json). + ``DEMUSE`` File saved from DEMUSE (.mat). + ``OTB`` File exported from OTB with decomposition and EMG signal. (.mat). + ``DELSYS`` Files exported from Delsys Neuromap and Neuromap explorer with decomposition and EMG signal (.mat + .txt). + ``CUSTOMCSV`` Custom file format (.csv) with decomposition and EMG signal. + ``OTB_REFSIG`` File exported from OTB with only the reference signal (.mat). + ``DELSYS_REFSIG`` File exported from DELSYS Neuromap with the reference signal (.mat). + ``CUSTOMCSV_REFSIG`` Custom file format (.csv) containing only the reference signal. ignore_negative_ipts : bool, default False diff --git a/openhdemg/library/plotemg.py b/openhdemg/library/plotemg.py index 7732b29..b8d1219 100644 --- a/openhdemg/library/plotemg.py +++ b/openhdemg/library/plotemg.py @@ -23,8 +23,11 @@ def showgoodlayout(tight_layout=True, despined=False): tight_layout : bool, default True If true (default), plt.tight_layout() is applied to the figure. despined : bool or str {"2yaxes"}, default False + False: left and bottom is not despined (standard plotting). + True: all the sides are despined. + ``2yaxes`` Only the top is despined. This is used to show y axes both on the right and left side at the @@ -428,6 +431,7 @@ def plot_mupulses( ``all`` IPTS of all the MUs is plotted. + Otherwise, a single MU (int) or multiple MUs (list of int) can be specified. The list can be passed as a manually-written list or with: @@ -592,8 +596,10 @@ def plot_ipts( emgfile : dict The dictionary containing the emgfile. munumber : str, int or list, default "all" + ``all`` IPTS of all the MUs is plotted. + Otherwise, a single MU (int) or multiple MUs (list of int) can be specified. The list can be passed as a manually-written list or with: @@ -751,7 +757,8 @@ def plot_idr( emgfile : dict The dictionary containing the emgfile. munumber : str, int or list, default "all" - ``all" + + ``all`` IDR of all the MUs is plotted. Otherwise, a single MU (int) or multiple MUs (list of int) can be diff --git a/openhdemg/library/tools.py b/openhdemg/library/tools.py index af520aa..541dc43 100644 --- a/openhdemg/library/tools.py +++ b/openhdemg/library/tools.py @@ -276,10 +276,15 @@ def resize_emgfile( else: # Visualise and select the area to resize + title = ( + "Select the start/end area to resize by hovering the mouse" + + "\nand pressing the 'a'-key. Wrong points can be removed with " + + "right \nclick or canc/delete key. When ready, press enter." + ) points = showselect( emgfile, how=how, - title="Select the start/end area to resize by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_, end_ = points[0], points[1] @@ -395,10 +400,10 @@ def compute_idr(emgfile): A dict containing a pd.DataFrame for each MU (keys are integers). Accessing the key, we have a pd.DataFrame containing: - - mupulses: firing sample. - - diff_mupulses: delta between consecutive firing samples. - - timesec: delta between consecutive firing samples in seconds. - - idr: instantaneous discharge rate. + - mupulses: firing sample. + - diff_mupulses: delta between consecutive firing samples. + - timesec: delta between consecutive firing samples in seconds. + - idr: instantaneous discharge rate. Examples -------- @@ -464,7 +469,9 @@ def compute_idr(emgfile): ) -def delete_mus(emgfile, munumber, if_single_mu="ignore"): +def delete_mus( + emgfile, munumber, if_single_mu="ignore", delete_delsys_muaps=True, +): """ Delete unwanted MUs. @@ -482,12 +489,16 @@ def delete_mus(emgfile, munumber, if_single_mu="ignore"): if_single_mu : str {"ignore", "remove"}, default "ignore" A string indicating how to behave in case of a file with a single MU. - ``ignore`` - Ignore the process and return the original emgfile. (Default) - ``remove`` - Remove the MU and return the emgfile without the MU. (Default) - This should allow full compatibility with the use of this file - in following processing (i.e., save/load and analyse). + ``ignore`` + Ignore the process and return the original emgfile. (Default) + + ``remove`` + Remove the MU and return the emgfile without the MU. (Default) + This should allow full compatibility with the use of this file + in following processing (i.e., save/load and analyse). + delete_delsys_muaps : Bool, default True + If true, deletes also the associated MUAPs computed by the Delsys + software stored in emgfile["EXTRAS"]. Returns ------- @@ -540,6 +551,7 @@ def delete_mus(emgfile, munumber, if_single_mu="ignore"): "EMG_LENGTH" : EMG_LENGTH, ==> "NUMBER_OF_MUS" : NUMBER_OF_MUS, ==> "BINARY_MUS_FIRING" : BINARY_MUS_FIRING, + ==> "EXTRAS" : EXTRAS but only for DELSYS file } """ @@ -594,6 +606,37 @@ def delete_mus(emgfile, munumber, if_single_mu="ignore"): # list of ndarray del_emgfile["MUPULSES"] = [np.array([])] + if emgfile["SOURCE"] == "DELSYS" and delete_delsys_muaps: + # Remove also DELSYS MUAPs + if isinstance(munumber, int): + munumber = [munumber] + + data = del_emgfile["EXTRAS"] + + for mu in munumber: + # Get MU ID + mu_id = f"MU_{mu}_" + # Remove all columns with MU ID + data = data[[col for col in data.columns if not col.startswith(mu_id)]] + + # Rescale the numbers in the remaining column names + col_list = list(data.columns) + if len(col_list) % 4 != 0: + raise ValueError("Unexpected number of channels in Delsys MUAPS") + new_col_list = [] + for mu in range(del_emgfile["NUMBER_OF_MUS"]): + new_col_list.extend( + [ + f"MU_{mu}_CH_0", + f"MU_{mu}_CH_1", + f"MU_{mu}_CH_2", + f"MU_{mu}_CH_3", + ] + ) + data.columns = new_col_list + + del_emgfile["EXTRAS"] = data + return del_emgfile @@ -749,9 +792,14 @@ def compute_covsteady(emgfile, start_steady=-1, end_steady=-1): """ if (start_steady < 0 and end_steady < 0) or (start_steady < 0 or end_steady < 0): + title = ( + "Select the start/end area of the steady-state by hovering the " + + "mouse \nand pressing the 'a'-key. Wrong points can be removed " + + "with right \nclick or canc/delete key. When ready, press enter." + ) points = showselect( emgfile=emgfile, - title="Select the start/end area of the steady-state by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_steady, end_steady = points[0], points[1] @@ -912,9 +960,15 @@ def remove_offset(emgfile, offsetval=0, auto=0): else: # Select the area to calculate the offset # (average value of the selected area) + title = ( + "Select the start/end area to calculate the offset by " + + "hovering the mouse \nand pressing the 'a'-key. Wrong " + + " points can be removed with right \nclick or canc/delete " + + "key. When ready, press enter." + ) points = showselect( emgfile=offs_emgfile, - title="Select the start/end of area to calculate the offset by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_, end_ = points[0], points[1] @@ -924,7 +978,6 @@ def remove_offset(emgfile, offsetval=0, auto=0): offs_emgfile["REF_SIGNAL"][0] = ( offs_emgfile["REF_SIGNAL"][0] - float(offsetval[0]) ) - print(offsetval) else: # Compute and subtract the offset value. @@ -950,6 +1003,7 @@ def get_mvc(emgfile, how="showselect", conversion_val=0): ``showselect`` Ask the user to select the area where to calculate the MVC with a GUI. + ``all`` Calculate the MVC on the entire file. conversion_val : float or int, default 0 @@ -996,9 +1050,14 @@ def get_mvc(emgfile, how="showselect", conversion_val=0): elif how == "showselect": # Select the area to measure the MVC (maximum value) + title = ( + "Select the start/end area to compute MVC by hovering the " + + "mouse \nand pressing the 'a'-key. Wrong points can be removed " + + "with right \nclick or canc/delete key. When ready, press enter." + ) points = showselect( emgfile=emgfile, - title="Select the start/end area to compute MVC by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, ) start_, end_ = points[0], points[1] @@ -1103,9 +1162,14 @@ def compute_rfd( start_ = startpoint else: # Otherwise select the starting point for the RFD + title = ( + "Select the start/end area to compute the RFD by hovering the " + + "mouse \nand pressing the 'a'-key. Wrong points can be removed " + + "with right \nclick or canc/delete key. When ready, press enter." + ) points = showselect( emgfile, - title="Select the start area to compute the RFD by hovering the mouse\nand pressing the 'a'-key. Wrong points can be removed with right click.\nWhen ready, press enter.", + title=title, titlesize=10, nclic=1, ) From 31c69b0e8d9f37f18b0b2de411859d08eec31f54 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:34:19 +0200 Subject: [PATCH 55/57] Configuration for 0.1.0.b4 release --- .pre-commit-config.yaml | 1 + CHANGELOG.md | 7 ------- MANIFEST.in | 1 + README.md | 2 +- openhdemg/__init__.py | 2 +- pyproject.toml | 6 ------ reqs_for_devs.txt | Bin 5527 -> 0 bytes requirements.txt | Bin 437 -> 366 bytes setup.py | 7 ++++++- 9 files changed, 10 insertions(+), 16 deletions(-) delete mode 100644 reqs_for_devs.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7dc3fe6..5fc3a9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +# TODO section to check and complete # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a25a6f..e69de29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +0,0 @@ -## 0.1.0 (2024-03-31) - -## 0.1.0-beta.3 (2023-12-04) - -## 0.1.0-beta.2 (2023-09-11) - -## 0.1.0-beta.1 (2023-07-04) diff --git a/MANIFEST.in b/MANIFEST.in index 6722f5c..d5e3106 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ include openhdemg/gui/gui_files/Logo_high_res.png include openhdemg/gui/gui_files/Matrix.png include openhdemg/gui/gui_files/Online.png include openhdemg/gui/gui_files/Redirect.png +include openhdemg/gui/gui_files/Error.png include openhdemg/gui/gui_files/gui_color_theme.json include openhdemg/library/decomposed_test_files/otb_testfile.mat diff --git a/README.md b/README.md index 587b082..9b50bff 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ python -m openhdemg.gui.openhdemg_gui Once opened, it will look like this. It is cool, isn't it? -![gui_preview](https://www.giacomovalli.com/openhdemg/md_graphics/index/gui_preview.png) +![gui_preview](https://www.giacomovalli.com/openhdemg/md_graphics/index/gui_preview_v2.png) ## Why openhdemg The *openhdemg* project was born in 2022 with the aim to provide the HD-EMG community with a free and open-source framework to analyse motor units' properties. diff --git a/openhdemg/__init__.py b/openhdemg/__init__.py index 3f54c02..578cf8c 100644 --- a/openhdemg/__init__.py +++ b/openhdemg/__init__.py @@ -1,3 +1,3 @@ __all__ = ["__version__"] -__version__ = "0.1.0-beta.3" +__version__ = "0.1.0-beta.4" diff --git a/pyproject.toml b/pyproject.toml index 8b64c1b..8b13789 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1 @@ -[build-system] -requires = ["setuptools>=68.0"] -[project] -dynamic = ["version"] -name = "openhdemg" -version = "0.1.0" diff --git a/reqs_for_devs.txt b/reqs_for_devs.txt deleted file mode 100644 index fee988e1bacdd9a83b1612c8432b983222c4dbe7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5527 zcmai&Pg7e*5X9fv-$gDE7>7d+PHw5V%2m!KCrU^H1k#gudVs`-Ptw2b9qsCm3H6PRGviYE|kt|2D zA8EChUAMFOEW7jOB3rTAHB0%PB#U8M_4nGQm(5d(=V(|E@l*2iTZ%PROpw$*zygY~ zc8~RQo^9A`AGVlk?L}B-&7FSEg?g!SOoJfz)M zX{D^bD9SwNd!E;vD@=D8#VqG?CY&Xs&nW_)N3vYYzE?e3q3u);@|!j9JH9XE-4V!g zoK|51)Zk?;A}_Uymr?Q)6(#el9CN9ah5W-3G1}yZ7{dckr~>}Fq!fG+sYddWbb-fS zcxFjQp^{I@BlbqUO~_-F%AHrfesBJghp)neMn}@+R25$~n}iPLtdU7#>`A=C*;GFJ zWPv|xl4z{n3i1{$7_{S#H04E`<-G?EpkBtjzrZj67I0V2laFu z*8nf>hkNO9B^}O$2-IkpSk`SQ|6YHmzwRn8!z%L!Rf@^NPB^0~?kJ{1tNO)?Ssb z`g@!F4bwV1;m=illz%22tg)LF`!)*Z<5mok>mcEn>37tGHPy4f)7IqYin&@~bM~;e z)hgJm*z~;Vgm|NfsFm447rLME;Lo8K z?kQGenZ`c%qJkyg+Sa$Fy4T4JwAR}q=WP{{)BY@uyuzAu!8)^_>zy{d^5fisQC9%9 zaQlM&wLG>`2~0#KnL0i>TU7@wY!un&9oj{e;feJ?1$)CZORZV&X4R)(?cX^qwU8)730RvSrNB4tUS#^gPw=f5Iuzcn6aMaSd-&0ch^d>;ueE9;yD9z;F|(U zd=<_zTc}Fl;#Rj5a;tDFEA!;JNHnX%%JE4(NZdaNS1YA}^g+AOCMaA5u!bhKVhE;~ zP-Z7GG0)$mxWjjh?fdU_LRvTFr4e~^!nR^FZU<+1>Vv(kp{_n> zV^r2kR1lW`ZocS_{%49*?pgTRiYM-|H$vJoIQh`E?taW&#p^a|cwB+wU2Dbk*;{A&^PSH^TO);IIP&@Me;o0eirXb`OHc90 zK2YG#=Q&FA9VOie-p_!2){v@Bq(Vlzr^JwY{gpHMhiAFifEqo?n?U3S=kq-nAu^1pT1SIhwQ9v zPMrGfJ=EVlfnArEy{QAP(&v7w_)U7T(|z1GT?@YksV?t5YGrmJ{+qJz)0~AHpEO2S zEXL`4o5;@p!;3mP!kauZ<+(F+hjIR;^TBKG-V)c`tBPjIOyjoOX+LZGU4-K3a#pO1 I!oi3C014DJ#Q*>R diff --git a/requirements.txt b/requirements.txt index 47b38cabc203b586be7271baad83b863c0c993bb..9a01b8d606ec3a603c5068c29a0756efd0b62f51 100644 GIT binary patch literal 366 zcmZ9H%?^Sv5QOKehvw`>=yr39STyTjl9bx+|Jh$SzZo7=YcdIK;*Ib;9; literal 437 zcmYL`SrURU5JdZ%yLbw}t@6VQxFQ0x{lZV+k3(&u(r%m=f2!^IblHuF!y7|>?Le> zaRguI-dyXESv}~2T>ouZJ7j%Nrhj&0-i`O?Nx!_LGul87SN8Vi`RDiuGorG_(?mG# Nrz4mDiEDVP#RpyXR5$ Date: Fri, 5 Apr 2024 16:34:37 +0200 Subject: [PATCH 56/57] Update website --- docs/gui_advanced.md | 39 ++-- docs/gui_basics.md | 104 +++++++--- docs/gui_intro.md | 15 +- docs/gui_settings.md | 194 ++++++++++++++++++ docs/index.md | 2 +- .../gui/advanced_analysis_window.png | Bin 14875 -> 0 bytes .../gui/advanced_tools_window_v2.png | Bin 0 -> 14907 bytes .../gui/duplicate_removal_window.png | Bin 12814 -> 0 bytes .../gui/duplicate_removal_window_v2.png | Bin 0 -> 18237 bytes .../md_graphics/gui/force_analysis_window.png | Bin 7219 -> 0 bytes .../gui/force_analysis_window_v2.png | Bin 0 -> 8256 bytes docs/md_graphics/gui/mu_properties_window.png | Bin 16325 -> 0 bytes .../gui/mu_properties_window_v2.png | Bin 0 -> 19018 bytes docs/md_graphics/gui/mu_tracking_window.png | Bin 11132 -> 0 bytes .../md_graphics/gui/mu_tracking_window_v2.png | Bin 0 -> 15282 bytes docs/md_graphics/gui/plot_window.png | Bin 54301 -> 0 bytes docs/md_graphics/gui/plot_window_v2.png | Bin 0 -> 60791 bytes docs/md_graphics/gui/refsig_filter_window.png | Bin 17829 -> 0 bytes docs/md_graphics/gui/remove_mu_window.png | Bin 6503 -> 0 bytes docs/md_graphics/gui/remove_mu_window_v2.png | Bin 0 -> 7113 bytes .../md_graphics/gui/signal_editing_window.png | Bin 0 -> 26050 bytes docs/md_graphics/index/gui_preview.png | Bin 77400 -> 0 bytes docs/md_graphics/index/gui_preview_v2.png | Bin 0 -> 69208 bytes docs/overrides/main.html | 9 +- docs/tutorials/setup_working_env.md | 29 ++- .../setup_working_env/screen_visualC14_1.png | Bin 0 -> 16582 bytes .../setup_working_env/screen_visualC14_2.png | Bin 0 -> 105837 bytes docs/what's-new.md | 64 +++++- mkdocs.yml | 1 + 29 files changed, 393 insertions(+), 64 deletions(-) create mode 100644 docs/gui_settings.md delete mode 100644 docs/md_graphics/gui/advanced_analysis_window.png create mode 100644 docs/md_graphics/gui/advanced_tools_window_v2.png delete mode 100644 docs/md_graphics/gui/duplicate_removal_window.png create mode 100644 docs/md_graphics/gui/duplicate_removal_window_v2.png delete mode 100644 docs/md_graphics/gui/force_analysis_window.png create mode 100644 docs/md_graphics/gui/force_analysis_window_v2.png delete mode 100644 docs/md_graphics/gui/mu_properties_window.png create mode 100644 docs/md_graphics/gui/mu_properties_window_v2.png delete mode 100644 docs/md_graphics/gui/mu_tracking_window.png create mode 100644 docs/md_graphics/gui/mu_tracking_window_v2.png delete mode 100644 docs/md_graphics/gui/plot_window.png create mode 100644 docs/md_graphics/gui/plot_window_v2.png delete mode 100644 docs/md_graphics/gui/refsig_filter_window.png delete mode 100644 docs/md_graphics/gui/remove_mu_window.png create mode 100644 docs/md_graphics/gui/remove_mu_window_v2.png create mode 100644 docs/md_graphics/gui/signal_editing_window.png delete mode 100644 docs/md_graphics/index/gui_preview.png create mode 100644 docs/md_graphics/index/gui_preview_v2.png create mode 100644 docs/tutorials/setup_working_env/screen_visualC14_1.png create mode 100644 docs/tutorials/setup_working_env/screen_visualC14_2.png diff --git a/docs/gui_advanced.md b/docs/gui_advanced.md index 4c84d3d..c2999b7 100644 --- a/docs/gui_advanced.md +++ b/docs/gui_advanced.md @@ -1,10 +1,12 @@ # Graphical Interface -This is the toturial for the `Advanced Tools` in the *openhdemg* GUI. Great that you made it this far! In the next few sections we will take a look at the more advanced functions implemented in the GUI. But first of all, you need to click the `Advanced Tools`button in the main window of the GUI to get to the respective adavanced analysis. The `Advanced Tools Window` will open. +This is the toturial for the `Advanced Tools` in the *openhdemg* GUI. Great that you made it this far! In the next few sections we will take a look at the more advanced functions implemented in the GUI. But first of all, you need to click the `Advanced Tools` button in the main window of the GUI to get to the respective adavanced analysis. The `Advanced Tools Window` will open. + +![advanced_analysis](md_graphics/gui/advanced_tools_window_v2.png) Please note, the `Advanced Tools` might not be available for all the files, as some of them might not have a sufficient number of electrodes to directly perform the advanced analyses. If you want to use the advanced tools anyway, you can still do so from the library. -![advanced_analysis](md_graphics/gui/advanced_analysis_window.png) +## Start a Specific Tool So far, we have included three advanced analyses in the *openhdemg* GUI. @@ -12,37 +14,43 @@ So far, we have included three advanced analyses in the *openhdemg* GUI. - `Duplicate Removal` - `Conduction Velocity Estimation` -For all of those, the specification of a `Matrix Orientation` and a `Matrix Code` is required. The `Matrix Orientaion` must match the one of your matrix during acquisition. You can find a reference image for the `Orientation` at the bottom in the right side of the `Plot Window` when using the `Plot EMG`function. The `Matrix Orientation` can be either **0** or **180** and must be chosen from the dropdown list. +For all of those, the specification of a `Matrix Code` and a `Matrix Orientation` is required. + +The `Matrix Code` must be specified according to the one you used during acquisition. So far, the implemented codes are: -The `Matrix Code` must be specified according to the one you used during acquisition. So far, the codes + - `Custom order` + - `None` + - `GR08MM1305` + - `GR04MM1305` + - `GR10MM0808` -- `GR08MM1305` -- `GR04MM1305` -- `GR10MM0808` -- `None` +In case you selected `Custom order`, you must also specify the custom order in the GUI settings. Please refer to [this tutorial](gui_settings.md/#electrodes) for further instructions on how to do so. -are implemented. You must choose one from the respective dropdown list. In case you selected `None`, the entrybox `Rows, Columns` will appear. Please specify the number of rows and columns of your used matrix since you now bypass included matrix codes. `Orientation` is ignored when `Matrix Code` is `None`. In example, specifying +In case you selected `None`, the entrybox `Rows, Columns` will appear. Please specify the number of rows and columns of your used matrix since you now bypass included matrix codes. In example, specifying ```Python Rows, Columns: 13, 5 ``` -means that your File has 65 channels. +means that your File has 65 channels organised over 13 rows and 5 columns. + +If you selected one of the built-in sorting orders (e.g., `GR08MM1305`, `GR04MM1305`, `GR10MM0808`), you need to specify also the `Orientation` in row two and column four in the left side of the `Plot Window`. The `Orientaion` must match the one of your matrix during acquisition. You can find a reference image for the `Orientation` at the bottom in the right side of the `Plot Window`. `Orientation` is ignored when `Matrix Code` is `None` or `Custom order`. Once you specified these parameter, you can click the `Advaned Analysis` button to start your analysis. ----------------------------------------- ## Motor Unit Tracking -When you want to track MUs across two different files, you need to select the `Motor Unit Tracking` options and specify the `Matrix Code` and `Matrix Orentation` in the `Advanced Tools Window`. Once you clicked the `Advanced Analysis` button, the `MUs Tracking Window` will pop-up. -![mus_tracking](md_graphics/gui/mu_tracking_window.png) +When you want to track MUs across two different files, you need to select the `Motor Unit Tracking` option and specify the `Matrix Code` and `Matrix Orentation` in the `Advanced Tools Window`. Once you clicked the `Advanced Analysis` button, the `MUs Tracking Window` will pop-up. + +![mus_tracking](md_graphics/gui/mu_tracking_window_v2.png) 1. You need to specify the `Type of file` you want to track MUs across in the respective dropdown. The available filetypes are: - - `OTB` (.mat file exportable by OTBiolab+) - - `DEMUSE` (.mat file used in DEMUSE) - `OPENHDEMG` (emgfile or reference signal stored in .json format) - `CUSTOMCSV` (custom data from a .csv file) + - `OTB` (.mat file exportable by OTBiolab+) + - `DEMUSE` (.mat file used in DEMUSE) Each filetype corresponds to a distinct datatype that should match the file you want to analyse. So, select the **Type of file** corresponding to the type of your file. In case you selected `OTB`, specify the `extension factor` in the dropdown. @@ -61,9 +69,10 @@ When you want to track MUs across two different files, you need to select the `M 8. By clicking the `Track` button, you can start the analysis. The tracking results will be displayed in the `MUs Tracking Resul` output in the right side of the `MUs Tracking Window`. ## Duplicate Removal + When you want to remove MUs duplicates across different files, you need to select the `Duplicate Removal` options and specify the `Matrix Code` and `Matrix Orentation` in the `Advanced Tools Window`. Once you clicked the `Advanced Analysis` button, the `Duplicate Removal Window` will pop-up. `Duplicate Removal` requires similar input as `Motor Unit Tracking`, so please take a look at the [`Motor Unit Tracking`](#motor-unit-tracking) section. However, you need to do two more things. -![duplicate_removal](md_graphics/gui/duplicate_removal_window.png) +![duplicate_removal](md_graphics/gui/duplicate_removal_window_v2.png) 1. You should specify How to remove the duplicated MUs in the `Which` dropdown. You can choose between diff --git a/docs/gui_basics.md b/docs/gui_basics.md index ad6ed17..fbc17ff 100644 --- a/docs/gui_basics.md +++ b/docs/gui_basics.md @@ -5,42 +5,57 @@ This is the basic introduction to the *openhdemg* GUI. In the next few sections, -------------------------------------------- ## Motor Unit Sorting + To sort the MUs included in your analysis file in order of their recruitement, we implemented a sorting algorithm. The MUs are sorted based on their recruitement order in an ascending manner. 1. Load a file. Take a look at the [intro](gui_intro.md#specifying-an-analysis-file) section on how to do so. -2. Pay attention to view the MUs first, using the `View MUs` button (we explained this button in the [intro](gui_intro.md) chapter). The MUs will be sorted anyways, but without viewing them you won't see what is happening. - -3. On the left hand side in the main window of the GUI, you can find the `Sort MUs` button. It is located in row three, column two. Once you press the button, the MUs will be sorted. +2. On the left hand side in the main window of the GUI, you can find the `Sort MUs` button. It is located in row three, column two. Once you press the button, the MUs will be sorted based on recruitment order. ## Remove Motor Units -To remove MUs included in your analysis file, you can click the `Remove MUs` button. The button is located on the left hand side in the main window of the GUI in column one of row four. -![remove_mus](md_graphics/gui/remove_mu_window.png) +To remove MUs included in your analysis file, you can click the `Remove MUs` button. The button is located on the left hand side in the main window of the GUI in column one of row four. -1. View the MUs using the `View MUs` button prior to MU removal, you can directly see what is happening. +![remove_mus](md_graphics/gui/remove_mu_window_v2.png) -2. Click the `Remove MUs` button, and a file is loaded, a pop-up window will open. +1. Click the `Remove MUs` button, and a file is loaded, a pop-up window will open. -3. Select the MU you want to delete from the analysis file from the `Select MU:`dropdown. +2. Select the MU you want to delete from the analysis file from the `Select MU:`dropdown. ```Python Select MU: 1 ``` will result in the second MU to be deleted (Python is base 0). -4. Click the `Remove MU` button to remove the MU. +3. Click the `Remove MU` button to remove the MU. + +Alternatively, you can click the `Remove empty MUs` button to delete all the MUs without discharge times. These can be present in the *emgfile* as the result of decomposed duplicates that have not been fully removed. + +## Signal Editing + +It is often necessary to edit (e.g., filter or convert) the signals. In the *openhdemg* GUI this can be done from the `Signal Editing Window`. In order to open this window, click the `Signal Editing` button. + +![signal_editing](md_graphics/gui/signal_editing_window.png) -Alternatively, you can click the `Remove empty MUs` button to delete all the MUs without discharge times. These can be present in the emgfile as the result of decomposed duplicates that have not been fully removed. +### EMG Signal Filtering -## Reference Signal Editing -The *openhdemg* GUI also allows you to edit and filter reference signals corresponding to your analysis file (this can be either a file containing both the MUs and the reference signal or a file containing only the reference signal). +It is common practice to filter the EMG signal before decomposition. However, if your *emgfile* contains the raw (unfiltered) signal, this can be easily filtered from the `Signal Editing Window`. -![reference_sig](md_graphics/gui/refsig_filter_window.png) +1. Click the `Signal Editing` button located in row five and column one, a new pop-up window opens. In the `Signal Editing Window`, you can band-pass filter the EMG signal and process the reference signal (see next paragraph). -1. View the MUs using the `View MUs` button prior to reference signal editing, so you can see what is happening. +3. When you click the `Filter EMG signal` button, the EMG signal is band-pass filtered (Zero-lag, Butterworth) according to values specified in the `Filter Order` and `BandPass Freq` textboxes. In example, specifiying -2. Click the `RefSig Editing` button located in row five and column one, a new pop-up window opens. In the `Reference Signal Editing Window`, you can low-pass filter the reference signal as well as remove any signal offset. Additionally, you can also convert your reference signal by a specific factor (amplification factor) or convert it from absolute to percentage (relative or normalised) values. + ```Python + Filter Order: 2 + BandPass Freq: 20-500 + ``` + will allow only frequencies between 20 and 500 Hz to pass trough. + +### Reference Signal Editing + +The *openhdemg* GUI also allows you to edit and filter reference signals contained in your analysis file (this can be either a file containing both the MUs and the reference signal or a file containing only the reference signal). + +1. Click the `Signal Editing` button located in row five and column one, a new pop-up window opens. In the `Signal Editing Window`, you can low-pass filter the reference signal as well as remove any signal offset. Additionally, you can also convert your reference signal by a specific factor (amplification factor) or convert it from absolute to percentage (relative or normalised) values. 3. When you click the `Filter RefSig` button, the reference signal is low-pass filtered (Zero-lag, Butterworth) according to values specified in the `Filter Order` and `Cutoff Freq` textboxes. In example, specifiying @@ -48,7 +63,7 @@ The *openhdemg* GUI also allows you to edit and filter reference signals corresp Filter Order: 4 Cutoff Freq: 15 ``` - will allow only frequencies below 15 Hz to pass trough. The filter order of 4 indicates a fourth degree polynomial transfer function. + will allow only frequencies below 15 Hz to pass trough. 4. When you click the `Remove Offset` button, the reference signal's offset will be removed according to the values specified in the `Offset Value` and `Automatic Offset` textboxes. In example, specifying @@ -78,6 +93,14 @@ The *openhdemg* GUI also allows you to edit and filter reference signals corresp ``` will amplify the reference signal 2.5 times. + While: + + ```Python + Operator : "Multiply" + Factor: -1 + ``` + will make your negative reference signal become positive. + 6. When you click the `To Percent` button, the reference signal in absolute values is converted to percentage (relative or normalised) values based on the provided `MVC Value`. **This step should be performed before any analysis, because *openhdemg* is designed to work with a normalised reference signal.** In example, a file with a reference signal in absolute values ranging from 0 to 100 will be normalised from 0 to 20 if ```Python @@ -93,12 +116,12 @@ Sometimes, resizing of your analysis file is unevitable. Luckily, *openhdemg* pr 2. Clicking the `Resize File` button will open a new pop-up plot of your analysis file. -3. Follow the instructions in the plot to resize the file. Simply click in the signal twice (once for start-point, once for end-point) to specify the resizing region and press enter to confirm your coice. +3. Follow the instructions in the plot to resize the file. Simply click in the signal twice (once for start-point, once for end-point) to specify the resizing region and press enter to confirm your choice. ## Analyse Force Signal In order to analyse the force signal in your analysis file, you can press the `Analyse Force` button located in row six and column one in the left side of the GUI. A new pop-up window will open where you can analyse the maximum voluntary contraction (MVC) value as well as the rate of force development (RFD). -![force_analysis](md_graphics/gui/force_analysis_window.png) +![force_analysis](md_graphics/gui/force_analysis_window_v2.png) ### Maximum voluntary contraction 1. In order to get the MVC value, simply press the `Get MVC` button. A pop-up plot opens and you can select the area where you suspect the MVC to be. @@ -114,14 +137,14 @@ In order to analyse the force signal in your analysis file, you can press the `A ```Python RFD miliseconds: 50,100,150,200 ``` - will result in RFD value calculation between the intervals 0-50ms, 50-100ms, 100-150ms and 150-200ms. You can also specify less or more values in the `RFD miliseconds` textbox. + will result in RFD value calculation between the intervals 0-50ms, 0-100ms, 0-150ms and 0-200ms. You can also specify less or more values in the `RFD miliseconds` textbox. 4. You can edit or copy any value in the `Result Output`, however, you need to close the top-level `Force Analysis Window` first. ## Motor Unit Properties When you press the `MU Properties` button in row six and column two, the `Motor Unit Properties` Window will pop up. In this window, you have the option to analyse several MUs propierties such as the MUs recruitement threshold or the MUs discharge rate. -![mus_properties](md_graphics/gui/mu_properties_window.png) +![mus_properties](md_graphics/gui/mu_properties_window_v2.png) 1. Specify your priorly calculated MVC in the `Enter MVC [N]:` textbox, like @@ -156,7 +179,7 @@ Subsequently to specifying the MVC, you can compute the MUs recruitement thresho Subsequently to specifying the MVC, you can compute the MUs discharge rate by entering the respective firing rates and event. 1. Specify the number of firings at recruitment and derecruitment to consider for the calculation in the `Firings at Rec` textbox. -2. Enter the number of firings over which to calculate the DR at the start and end of the steady-state phase in the `Firings Start/End Steady` textbox. +2. Enter the number of firings over which to calculate the discharge rate at the start and end of the steady-state phase in the `Firings Start/End Steady` textbox. For example: ```Python @@ -189,8 +212,8 @@ Subsequently to specifying the MVC, you can calculate a number of basic MUs prop and are all displayed in the `Result Output` once the analysis in completed. -1. Specify the number of firings at recruitment and derecruitment to consider for the calculation in the `Firings at Rec` textbox. -2. Enter the start and end point (in samples) of the steady-state phase in the `Firings Start/End Steady` textbox. For example, +1. Specify the number of firings at recruitment and derecruitment to consider for the calculation of discharge rate in the `Firings at Rec` textbox. +2. Enter the number of firings over which to calculate the discharge rate at the start and end of the steady-state phase in the `Firings Start/End Steady` textbox. For example, ```Python Firings at Rec: 4 @@ -202,9 +225,9 @@ and are all displayed in the `Result Output` once the analysis in completed. ## Plot Motor Units In *openhdemg* we have implemented options to plot your analysis file ... a lot of options! -Upon clicking the `Plot MUs` button, the `Plot Window` will pop up. In the top right corner of the window, you can find an information button forwarding you directly to some tutorials. +Upon clicking the `Plot MUs` button, the `Plot Window` will pop up. In the top right corner of the window, you can find an information button forwarding you directly to this tutorial. -![plot_mus](md_graphics/gui/plot_window.png) +![plot_mus](md_graphics/gui/plot_window_v2.png) You can choose between the follwing plotting options: @@ -214,7 +237,7 @@ You can choose between the follwing plotting options: - Plot the decomposed source. (Plot Source) - Plot the instantaneous discharge rate (IDR). (Plot IDR) - Plot the differential derivation of the raw emg signal by matrix column. (Plot Derivation) -- Plot motor unit action potentials (MUAPs) obtained from spike-triggered average from one or multiple MUs. (Plot MUAPs) +- Plot motor unit action potentials (MUAPs) obtained from spike-triggered average (STA) from one or multiple MUs. Please note that for Delsys files, the displayed MUAPs are those computed in the Delsys software and not those obtained via STA. (Plot MUAPs) Prior to plotting you can **optionally** select a few options on the left side of the `Plot Window`. @@ -229,44 +252,56 @@ Prior to plotting you can **optionally** select a few options on the left side o These three setting options are universally used in all plots. There are two more specification options on the right side of the `Plot Window` only relevant when using the `Plot Derivation` or `Plot MUAP` buttons. 1. The `Matrix Code` must be specified in row one and column four in the right side of the `Plot Window` according to the one you used during acquisition. So far, implemented codes are: + + - `Custom order` + - `None` - `GR08MM1305` - `GR04MM1305` - `GR10MM0808` - - `None` + - `Trigno Galileo Sensor` + + In case you selected `Custom order`, you must also specify the custom order in the GUI settings. Please refer to [this tutorial](gui_settings.md/#electrodes) for further instructions on how to do so. In case you selected `None`, the entrybox `Rows, Columns` will appear. Please specify the number of rows and columns of your used matrix since you now bypass included matrix codes. In example, specifying + ```Python Rows, Columns: 13, 5 ``` - means that your File has 65 channels. + means that your File has 65 channels organised over 13 rows and 5 columns. -2. You need to specify the `Orientation` in row two and column four in the left side of the `Plot Window`. The `Orientaion` must match the one of your matrix during acquisition. You can find a reference image for the `Orientation` at the bottom in the right side of the `Plot Window`. `Orientation` is ignored when `Matrix Code` is `None`. +2. If you selected one of the built-in sorting orders (e.g., `GR08MM1305`, `GR04MM1305`, `GR10MM0808`), you need to specify also the `Orientation` in row two and column four in the left side of the `Plot Window`. The `Orientaion` must match the one of your matrix during acquisition. You can find a reference image for the `Orientation` at the bottom in the right side of the `Plot Window`. `Orientation` is ignored when `Matrix Code` is `None`, `Custom order` or `Trigno Galileo Sensor`. ### Plot Raw EMG Signal + 1. Click the `Plot EMGsig` button in row four and column one in the left side of the `Plot Window`, to plot the raw emg signal of your analysis file. 2. Enter or select a `Channel Number` in / from the dropdown list. For example, if you want to plot `Channel Number` one enter *0* in the dropdown. If you want to plot `Channel Numbers` one, two and three enter *0,1,2* in the dropdown. 3. Once you have clicked the `Plot EMGsig` button, a pop-up plot will appear. ### Plot Reference Signal + 1. Click the `Plot RefSig` button in row five and column one in the left side of the `Plot Window`, to plot the reference signal. 2. Once you have clicked the `Plot RefSig` button, a pop-up plot will appear. ### Plot Motor Unit Pulses + 1. Click the `Plot MUpulses` button in row six and column one in the left side of the `Plot Window`, to plot the single pulses of the MUs in your analysis file. 2. Enter/select a pulse `Linewidth` in/from the dropdown list. For example, if you want to use a `Linewidth` of one, enter *1* in the dropdown. 3. Once you have clicked the `Plot MUpulses` button, a pop-up plot will appear. ### Plot the Decomposed Source + 1. Click the `Plot Source` button in row seven and column one in the left side of the `Plot Window`, to plot the Source of the decomposed MUs in your analysis file. 2. Enter/select a `MU Number` in/from the dropdown list. For example, if you want to plot the source for `MU Number` one enter *0* in the dropdown. If you want to plot the sources for `MU Number` one, two and three enter *0,1,2,* in the dropdown. You can also set `MU Number` to "all" to plot the sources for all included MUs in the analysis file. 3. Once you have clicked the `Plot Source` button, a pop-up plot will appear. ### Plot Instanteous Discharge rate + 1. Click the `Plot IDR` button in row eight and column one in the left side of the `Plot Window`, to plot the IDR of the MUs in your analysis file. 2. Enter/select a `MU Number` in/from the dropdown list. For example, if you want to plot the IDR of `MU Number` one enter *0* in the dropdown. If you want to plot the IDR of `MU Number` one, two and three enter *0,1,2* in the dropdown. You can also set `MU Number` to "all" to plot the IDR of all included MUs in the analysis file. 3. Once you have clicked the `Plot IDR` button, a pop-up plot will appear. ### Plot Differential Derivation + 1. Click the `Plot Derivation` button in row four and column three in the right side of the `Plot Window`, to plot the differential derivation of the MUs in your analysis file. 2. Specify the `Configuration` for the calculation first. You can choose from: - `Single differential` (Calculate single differential of raw signal on matrix rows) @@ -282,9 +317,11 @@ These three setting options are universally used in all plots. There are two mor - `Double differential`(Calculate double differential of raw signal on matrix rows) 3. Specify the respective `MU Number` you want to plot. You can choose one from the `MU Number` dropdown list. 4. Specify the `Timewindow` of the plots. You can choose from the `Timewindow` dropdown list or enter any integer. -5. Once you have clicked the `Plot MUAPs` button, a new pop-up plot appears. +5. Once you have clicked the `Plot MUAPs` button, a new pop-up plot appears. -## Saving Your Analysis File +Please note, if you are using the Delsys decomposition outcome, the plotted MUAPs will be those calculated bu the Delsys software. + +## Saving Your Analysis File Subsequently to analysing your emg-file in the *openhdemg* GUI, it is beneficial to save it. Otherwise, all changes will be lost when the GUI is closed. 1. Click the `Save File` button in row two and column two in the left side of the main window. @@ -296,8 +333,9 @@ Some analyses included in the *openhdemg* GUI return values that are displayed i 1. click the `Save Results` button in row two and column two in the left side of the main window. 2. Specify a location where to save the file and confirm. You can find the file there with the name of your analysis file. -## Resetting Your Analysis -We all make mistakes! But, most likely, we are also able to correct them. In case you have made a mistake in the analysis of you emg-file in the *openhdemg* GUI, we have implemented a reset button for you. Click the `Reset Analysis` button in row eight and column two in the lef side of the main window to reset any analysis you previously performed since opening the GUI and inputting an analysis file. Your analysis file is reset to the original file and all the changes are discarded. So, no need to be perfect! +## Resetting Your Analysis + +We all make mistakes! But, most likely, we are also able to correct them. In case you have made a mistake in the analysis of your emgfile in the *openhdemg* GUI, we have implemented a reset button for you. Click the `Reset Analysis` button in row eight and column two in the lef side of the main window to reset any analysis you previously performed since opening the GUI and inputting an analysis file. Your analysis file is reset to the original file and all the changes are discarded. So, no need to be perfect! -------------------------------------------- diff --git a/docs/gui_intro.md b/docs/gui_intro.md index 83467cd..04ee4a2 100644 --- a/docs/gui_intro.md +++ b/docs/gui_intro.md @@ -14,9 +14,9 @@ python -m openhdemg.gui.openhdemg_gui Let us shortly walk you through the main window of the GUI. An image of the starting page of the GUI is displayed below. -![gui_preview](md_graphics/index/gui_preview.png) +![gui_preview](md_graphics/index/gui_preview_v2.png) -This is your starting point for every analysis. On the left hand side you can find all the entryboxes and buttons relevant for the analyses you want to perform. In the middle you can see the plotting canvas where plots of the HD-EMG data analysis are displayed. On the right hand side you can find information buttons leading you directly to more information, tutorials, and more. And, with a little swoosh of magic, the results window appears at the bottom of the GUI once an analysis is finished. +This is your starting point for every analysis. On the left hand side you can find all the entryboxes and buttons relevant for the analyses you want to perform. In the middle you can see the plotting canvas where plots of the HD-EMG data analysis are displayed. On the right hand side you can find information buttons leading you directly to more information, tutorials, and more. Very important, at the top of this section, there is the settings icon (Gear icon). By clicking this icon, you will be able to [change some of the GUI settings](gui_settings.md). Finally, at the bottom of the GUI there is a window used to display the results of the analyses. ------------------------------------------------- @@ -33,17 +33,20 @@ This is your starting point for every analysis. On the left hand side you can fi - `CUSTOMCSV` (custom data from a .csv file) - `CUSTOMCSV_REFSIG` (Reference signal in a custom .csv file) - Each filetype corresponds to a distinct datatype that should match the file you want to analyse. So, select the `Type of file` corresponding to the type of your file. In case you selected `OTB`, specify the `extension factor` in the dropdown. + Each filetype corresponds to a distinct datatype that should match the file you want to analyse. So, select the `Type of file` corresponding to the type of your file. -2. To actually load the file, click the **Load File** button and select the file you want to analyse. In case of occurence, follow the error messages and repeat this and the previos step. +2. Before loading a file, verify that the [GUI settings](gui_settings.md) are correct for your needs. -3. Once the file is successfully loaded, the specifications of the file you want to analyse will be displayed next to the **Load File** button. +3. To actually load the file, click the **Load File** button and select the file you want to analyse. In case of occurence, follow the error messages and repeat this and the previos step. + +4. Once the file is successfully loaded, the specifications of the file you want to analyse will be displayed next to the **Load File** button. ## Viewing an analysis file It doesn't get any simpler than this! -Once a file is successfully loaded as described above, you can click the `View MUs` button to plot/view your file. In the middle section of the GUI, a plot containing your data should appear. +Once a file is successfully loaded as described above, a plot containing your data should appear in the middle section of the GUI, allowing you to +view the content of your file. This can also be force by clicking the `View MUs` button. ---------------------------------------- diff --git a/docs/gui_settings.md b/docs/gui_settings.md new file mode 100644 index 0000000..ec04841 --- /dev/null +++ b/docs/gui_settings.md @@ -0,0 +1,194 @@ +Since the release of version 0.1.0-beta.4, we have introduced GUI settings. These allow you to customise functions behaviour when using the GUI, making it more flexible and adaptable to specific users needs. + +## Access GUI settings + +Accessing these GUI settings is simple: just click on the Gear icon located at the top-right corner of the GUI window. + +![gui_preview](md_graphics/index/gui_preview_v2.png) + +Upon clicking the settings icon, a Python file will open in your default text editor. Here, you'll be able to modify the settings according to your requirements. + +## Modify GUI settings + +The GUI settings file (named *settings.py*) is organised into distinct topic sections. + +All the variables that can be modified are labelled as: + +- Name of the calling function + _ _ + Name of the variable + +In this way, the values that each variable can assume can be discovered from the API section of this website by navigating into the topic, then into the Name of the function and then looking at the specific variable. + +The variables that can be modified from the GUI settings, as well as their values, can differ between different *openhdemg* releases. Therefore, the user is always encouraged to check the specifics APIs, and not to rely on this guide, which only serves didactical purposes. + +### openfiles + +The first section is named *openfiles* and controls how files are loaded into *openhdemg* or saved in .json format. + +The API section for this topic is [here](api_openfiles.md). + +For example, the GUI behaviour when loading Custom .csv files can be adjusted in the following code snippet: + +```Python +# in emg_from_customcsv() +emg_from_customcsv__ref_signal = "REF_SIGNAL" +emg_from_customcsv__raw_signal = "RAW_SIGNAL" +emg_from_customcsv__ipts = "IPTS" +emg_from_customcsv__mupulses = "MUPULSES" +emg_from_customcsv__binary_mus_firing = "BINARY_MUS_FIRING" +emg_from_customcsv__accuracy = "ACCURACY" +emg_from_customcsv__extras = "EXTRAS" +emg_from_customcsv__fsamp = 2048 +emg_from_customcsv__ied = 8 +``` + +In the first line of code, *emg_from_customcsv__ref_signal = "REF_SIGNAL"* indicates that the variable *ref_signal* belonging to the function *emg_from_customcsv* is set to "REF_SIGNAL". + +The complete documentation of the function *emg_from_customcsv()* can be accessed [here](api_openfiles.md#openhdemg.library.openfiles.emg_from_customcsv). Reading the specific APIs, you will always know what parameters can be passed to each variable! + +Similarly, the GUI behaviour when saving .json files can be adjusted in the following code snippet: + +```Python +# in save_json_emgfile() +save_json_emgfile__compresslevel = 4 +``` + +And the corresponding APIs can be accessed [here](api_openfiles.md#openhdemg.library.openfiles.save_json_emgfile). + +### analysis + +The section named *analysis* controls how basic MUs properties (i.e., MUs recruitment/derecruitment thresholds, discharge rate and accuracy measures) are calculated. + +The API section for this topic is [here](api_analysis.md). + +For example, in the following code snippet we can adjust how many firings are used to calculate the recruitment/derecruitment thresholds or how the accuracy measure is computed. + +```Python +# in basic_mus_properties() +basic_mus_properties__n_firings_rt_dert = 1 +basic_mus_properties__accuracy = "default" +basic_mus_properties__ignore_negative_ipts = False +basic_mus_properties__constrain_pulses = [True, 3] +``` + +The corresponding APIs for the function *basic_mus_properties* can be accessed [here](api_analysis.md/#openhdemg.library.analysis.basic_mus_properties). + +### tools + +The section named *tools* controls additional functionalities that are necessary for the usability of the library. The functions contained in this section can be considered as "tools" or shortcuts necessary to operate with the HD-EMG recordings. + +The API section for this topic is [here](api_tools.md). + +For example, in the following code snippet we can adjust how to resize the emgfile and how to deal with the accuracy measure in the new resized file. + +```Python +# in resize_emgfile() +resize_emgfile__how = "ref_signal" +resize_emgfile__accuracy = "recalculate" +resize_emgfile__ignore_negative_ipts = False +``` + +The corresponding APIs for the function *resize_emgfile* can be accessed [here](api_tools.md/#openhdemg.library.tools.resize_emgfile). + +### muap + +The section named *muap* controls all the functionalities that require MU action potentials. Currently, it controls the behaviour of functionalities such as MU tracking, duplicate removal and conduction velocity estimation, among others. + +The API section for this topic is [here](api_muap.md). + +For example, in the following code snippet we can adjust how to perform MU tracking. As you might have noticed using the GUI, it is already possibile to specify many parameters for MU tracking directly in the GUI, and these settings here offer an additional level of customisability. + +```Python +# in tracking() +tracking__firings = "all" +tracking__derivation = "sd" +``` + +The corresponding APIs for the function *tracking* can be accessed [here](api_muap.md/#openhdemg.library.muap.tracking). + +### electrodes + +The section named *electrodes* allows to specify a custom order for electrodes sorting. The sorting order is used any time the grid channels need to be oriented in a specific manner, wich usually reflects how the operator positioned the grid on the skin. A number of common sorting orders are already present the library. However, if the needed sorting order is not available, the user can specify any custom order in this section. This allows for maximum flexibility of the sorting functionalities. + +The API section for this topic is [here](api_electrodes.md). + +In the following example, we show how the sorting order of a grid can be obtained and specified. + +Here you can see the channels order as displayed on the grid. This example refers to the commercially available grid GR08MM1305. + +``` +Channel Order GR08MM1305 + 0 1 2 3 4 +0 64 39 38 13 12 +1 63 40 37 14 11 +2 62 41 36 15 10 +3 61 42 35 16 9 +4 60 43 34 17 8 +5 59 44 33 18 7 +6 58 45 32 19 6 +7 57 46 31 20 5 +8 56 47 30 21 4 +9 55 48 29 22 3 +10 54 49 28 23 2 +11 53 50 27 24 1 +12 52 51 26 25 NaN +``` + +As you can see, this grid of electrodes is composed of 5 columns (from 0 to 4) and 13 rows (from 0 to 12) for a total of 64 channels. Given that the channels are represented in a quadrilateral structure, one channels will result empty. + +Given that Python is in base 0, we first need to convert the channels in base 0, with the final matrix looking like this: + +``` +Channel Order in base 0 + 0 1 2 3 4 +0 63 38 37 12 11 +1 62 39 36 13 10 +2 61 40 35 14 9 +3 60 41 34 15 8 +4 59 42 33 16 7 +5 58 43 32 17 6 +6 57 44 31 18 5 +7 56 45 30 19 4 +8 55 46 29 20 3 +9 54 47 28 21 2 +10 53 48 27 22 1 +11 52 49 26 23 0 +12 51 50 25 24 NaN +``` + +After the conversion from base 1 to base 0, we can proceed creating a list of lists, where each internal list represents a column of the grid, as follows: + +```Python +custom_sorting_order = [ + [63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51], + [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50], + [37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25], + [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, np.nan], +] +``` + +The empty channel can be indicated with *np.nan*, which stands for "not a number". Similarly, we can mark more missing channels with the same approach. + +```Python +custom_sorting_order = [ + [63, 62, 61, 60, 59, 58, np.nan, 56, 55, 54, 53, 52, 51], + [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50], + [37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25], + [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], + [11, 10, 9, np.nan, 7, 6, 5, 4, 3, 2, 1, 0, np.nan], +] +``` + +## What variables can be modified? + +Please note that **only** the variables present in the *settings.py* file can be modified. If you would like to modify additional variables, please [get in touch](contacts.md) with us. + +Additionally, you cannot remove the unused variables from the settings file! You can only modify their value. + +## Restore GUI settings + +If you accidentally modify some variables and the GUI stops working properly, you can restore the original settings by copying and pasting the content of the *backup_settings.py* file. This will be visible in the file explorer of your editor next to the *settings.py* file. + +## More questions? + +We hope that this tutorial was useful. If you need any additional information, do not hesitate to read the answers or ask a question in the [*openhdemg* discussion section](https://github.com/GiacomoValliPhD/openhdemg/discussions){:target="_blank"}. If you are not familiar with GitHub discussions, please read this [post](https://github.com/GiacomoValliPhD/openhdemg/discussions/42){:target="_blank"}. This will allow the *openhdemg* community to answer your questions. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 445390a..4df027e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,7 +52,7 @@ python -m openhdemg.gui.openhdemg_gui Once opened, it will look like this. It is cool, isn't it? -![gui_preview](md_graphics/index/gui_preview.png) +![gui_preview](md_graphics/index/gui_preview_v2.png) ## Why openhdemg The *openhdemg* project was born in 2022 with the aim to provide the HD-EMG community with a free and open-source framework to analyse motor units' properties. diff --git a/docs/md_graphics/gui/advanced_analysis_window.png b/docs/md_graphics/gui/advanced_analysis_window.png deleted file mode 100644 index f8db494180cd78ab0fe07f4a8eed03c93c923dbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14875 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 literal 0 HcmV?d00001 diff --git a/docs/md_graphics/gui/duplicate_removal_window.png b/docs/md_graphics/gui/duplicate_removal_window.png deleted file mode 100644 index dc2307e43284a68f395e2d6b20f9ac632a978664..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12814 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 diff --git a/docs/md_graphics/gui/duplicate_removal_window_v2.png b/docs/md_graphics/gui/duplicate_removal_window_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..e8fb7bcbd881547eefeae1dd28b17bbe0dcf0b71 GIT binary patch literal 18237 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 literal 0 HcmV?d00001 diff --git a/docs/md_graphics/gui/force_analysis_window.png b/docs/md_graphics/gui/force_analysis_window.png deleted file mode 100644 index af434d2323f6659a220560a779a12e9b46511a11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7219 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 F{{x1<7Ipvt diff --git a/docs/md_graphics/gui/force_analysis_window_v2.png b/docs/md_graphics/gui/force_analysis_window_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..33300edd76eef6badfee94765cdc67844e9b9ecc GIT binary patch literal 8256 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 literal 0 HcmV?d00001 diff --git a/docs/md_graphics/gui/mu_properties_window.png b/docs/md_graphics/gui/mu_properties_window.png deleted file mode 100644 index 7dee1659e58af7209a5754ab955f0e60806b871d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16325 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 diff --git a/docs/md_graphics/gui/mu_properties_window_v2.png b/docs/md_graphics/gui/mu_properties_window_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..4d02998ce89da807ae99494066b64f0b1d070443 GIT binary patch literal 19018 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�AqnRy1UUkG&!oV@w!oDyed&LQ09 zT6Q7J0l;b4aH_t?XmVJl$o3VQqD#Vd`f=0uG@xq*xSkh#BNZUd%qgu&v!;ybwHhS_ zm~k<`=|bNMF@&s4&X&7J(`(3-_2yur7X*5cc=vw+u2|?bJUY&2FZq|O83l(2iC!?; z!01{IW{-z$%~`ZaN9i=?XZB0EBLa`^gx|G~ZX_S}b^apQOA*?`L6xcdl!er9!gZuU zQP_^eUR$A^bOTutsduLouv7%Q8CP`mWOOK!>Jr>iZO88#m8>E%#uD%1nN2Z(r7_o1e9>z3^zioC;kbN0wbvK4GB&j2$rL=pPW^d7k1`Z-;K7Di7#(30 zr(rKk!b%3qM*G`X*Baimy3zVu3nhIR+?ryj8Jawvvap$-1x=I=Hp zp2lUB=<{M&ona^0q?EgVojQE1JG_R7rEGi&dhzaD)Rvc>vxdVN8*@X2?feEEPA^pO z@3`8bFL0c|pTbR#GE+x0)`+3+D$mD@bxOfi^NbTUFu!G!7+`7Itd7X^KnCRWDZsE~ z&D^hpIKFzp7 zF+l|CZ8Wg5iva%}p%Diz1imkG5Kxgqv>w^BW!!272qGcC`59uj%jB1qtB2?rz~P|h zqZtCNW-!qQM4rS~D*?pIyko);z3@fgXsNzG0HwamU>r#lJ5gmy!ETNbLAMvH$HOcv zECyJwI{L!!IaP~2b4xe$h?00Yr^?0q`^AABeqVJl%;_6GdstUm0dVPUcJ)4X?u4ZK zj)%1ndugmN0s;Z;@}neRl6gpyKF*Dawp5qn4W+hEGXRw(g;W^jg)kE z{TA=N&%Mv{+~>akynoDRcjlZqbLPyPoNu72vJ4&$84d;p2A-TONF4*?206M}-N8hU zJj*7p$H2H%ZUxqH)KO9tFoi+cjLl#s5H?q+9U6{-AtdT*XKZQ>ailhZSXkK#1Geg# z0Mu4y!T@bvB@QJ!NrSk@qZw3$*!4Yy5KofvM9F3`6p*FT~0aszb zFS-Kg|JTFp0P0^Tj@H5e9VJz2NtgqKnwO22jRPQpLoMWBW-g!(lKy=%x+e^6 zU}tx6aba`eW`j9cuygYB^Rshsv2$^;qA^(E2wO*ES5{m2vuhH+(EvfT87ErZvh1lqTte_BEIGXC8%l!$}{R{LD zE`LEKVKy)a4fNH92m^jw?O$gh@C&8{Ged)ZWB*T(oTQ|x1I*mY1`UI&%ScemNlEf? z^7HYra=($$r2#~QO8jFjAi<^}L$jZqF=He3I;uqlJVCLWw;NbYR`(M~- zeK0e2H2#0#U*BqKA@mF-B>`D0xFgH~@oVU}tF8gD|E>31*T(AC9jB)LbE`O^E%!m>2qm@E0{j&--l*t^Q~`!u~Hqg6{mw6hUm!_QwHj zXaJF7uhGWrjTQP>{_8j}Fc>18BAXN>)JQr+Um3?3M@ahk`0`rmhz}3GkHH#we_NOJ zbXR6Jyu)2u*7_?G35fNAY?VEa*Ao9N@(!|M2^kkPh9G?$x$LlcGP%{TY`0bOGITMf zewyU9M@E)Zi+I0*$F!+;i!wHwo*g}jVQN@dho$9KmChH}4WKoz$WciJYqld63u{Ye zP(iFOy>G6TLP3{)3ma{+4v`4qp_PO99Jz+d=<)Xj#ZXbDQIp8~I)c8!iAE{sZSq<@ zxIH~POK|4ye$OxQ&(+_3&ug`{2MQ3j+}h#jdi>Pyrv+%~>G{2{^vq-n*o}J@4D!W* zGI`Qa2$vb-9I=yBK`u>>luB2p8sFJfRf(IM-FvMmLf<8s&#oIc&gj6*emnLrACE|W z5dMV7NHd!Bp92msJN4FK_c=Xp$g}Q(KA(79?9Q7(+en*S&70~MZ)zDXx%{A9^An%zIh{?PAUrrgc6WTz9RWh8Bp~65X%hvDfB#aNno;zN4g12>|+e z-Ao=^Cdl^fYjfuthEfh19(j&? zGP`rSc$s|jHq43uh{9|&f4tBWB@qHrQdV>o zMLiHX?Ny2MdUpRlp^_p_e= zmsIehb74F9fP~)Rm`3>3{^gScg3=J`0-B?R!;Oi#3D``daB><*Vc5m{tS9Q~h#mKl zpN)sWN6W6a79>1$ta`G+LQS5B8}lD-*Am>h_4ek}P^ExM?44V*xha$QlLT)rd|cRYXdtg=RXGERrBq^OCq(^W9(*;H zD=k}%=?0Jc`#>>c@AdU($c|0D3KlQB{JM1-9MYI}de>T(LYmQHl*fOzj<3BH^8CG7 z_U=VScfQ1mi)1-{YjgCpg3i|O$V+P9 z5H{Vvws5^oq-pA26~Bp{OOO|y7Q*6Rur!W9)-qYP>`eMBMD9}9?Mdjt1ULf!>p!?p? zsIo!c(srls&*db|;3h@`zVm|4!{BAZqbnwQWMCJTsNVj@*vwrcLy`TK@J6Ql0=V7+ zEN62-(KxV_t=wg$EwrIl#HO8IySPEuXoEbYR(}n4yElFTcEqoI`{C8c=YF;hl0JK% z*)~;?=LZF$8>~avwV|4+o^(Mlj2QH7$+hg$U=jyIbwX6LqnrFZqVKfjx4+@3G7 zj7rnrRutR6IEu#g^77b$iv}JFJ;0*}?tW=zns!Oaa>D`|)_D&C$@i$tR9k^^`xX5a z8p7qHz|F3iO>Wdd1%-nd@`tC4W;`Ga_^RzJXWY3$jQT!UcUFHn5bG)LkHRUP(##F6y|eOKxk!=wiQw{t-J3fb4V)mpB@C z^m0G=6McRErSd#rYWJX5x3SVGjY;j^%8%qXE%gu1^mj1dh@E#|dF>h<%+CbU(f@2Z z|0H<0k6=?KCf=u-L7htvMqb$eFwgS9eu9w`cmc(KBSx(UpG_=U6`g!UUq5y7R@D3A zqNm*P0CQ(eG|txOd~#3jCnV^(N-V=Uz3Xrio&Dv_)`we1nGe|wE7#lf-0WW{9WTO; zlp0f4mBq}PU0iSINFXu(w&{UDSChMSV3(WJG8~0=qnobtO7jEuf8sNuLA>e$mjr{Q6)obndWv<&b`lA57TP%X#R^l6di&hhC5F zhu1Oc@<`2D@I9N7yvyLDQ8f&fr@uET1u)FC(SUTSir<8ig|A_smyjdbhAve zdy2vqP`c_NijkFRIhM!(tK?4(>@3h&U0i~nWx>br3T?02QgflK5JI5y_oFXULpMJ} ziIg=uIi$7j+TM~i+IfK+7!V*tk%VKB>P_>Jr)xs%=rM$f;b3>DiDyI9**|aF&PBVz zH=xtAQr>ADpKt)*ry*acIx)Y_a@k5>AyG4T_ysjUvypMzE~#>$ms?pHfs>$c+U~a< zI=(cza72UGYZ(%?UN~XjY_n*)pTxddH1UiUP&+m#jdD&N@(2j4U(?^n6H!lwD%bTQ zlEx*pjI>f$6L!mct*ZEY{nyW2T&flH*H2T+l`Sey=0Tu(a>nvhS(O`bW+9;fjZgP1 zJR$ndWbST(0Y&C#8C4!VX->6Ahq8gPwYkdh{uMfhx^>4AsaSUHrJf_B`uTeTQM1n9 zt^9-BU%tG$1wwP{-b^Tw^0wD(=UHAy4EIa$Hy+?@G}a*ui+ z0ss!_Q0~)?LeisWyBcDp)XH<_0@8$XN5hPJ*0NhEWErE3saxH#M*-LJKfOCf$b*eGacl5>J-ZHp`$I9W)r!tsz8&=GvEdTpSw_V#>+0)Kp3)N{3eg@-K2p{Z zLZm{{QuCFoJeM~>L&IzMHHDe>Ko@OI)6V+7sqf&$nM=sqesb#&hx7v{#UbBjdS(a& zStJlUT0DI84XW&3C64$g&CAQb-Y_6pkZI7X&P8c`2mHR22g?6LHEq**9EvJnS8V5B z{l-~WJ7HCIkVnB<09MpB4ucpYCsR*x%QK5>!15p(M%d`qy@DyN7gOaWiW03Oj>&`8 zjk07=g+$+gEQpd*691YGS_iX6##8ec;zLW>)za2-+_SzDfq$aF*%^X4(|OS>h2Lf0 z?H((H9V=kK%UY}7a-vMtPuwIGbg=X! zA=j{nLX2ka;0qn^&X*$EY!$09dhSu_R3l1dhKIeUG|3}to!^Mde^w=hg7etoi6XO? zF;PPeRkqR-V*}ql~oCEh`USfB_@`7Ys1t5 zh3)3kI_srbo%Hh4JX_EArVEukn!I}7Me(x0IrOV0O4~KKjeCCb`d>~{oi`pr8Vz*S zOlOQafG1(tvPb2V)8aBr3*A#x5ufq(#(W%h2dm8!^rM<<{93ea56M)sz|f+ zkNPsFtlb_pTusgOwYH+>ufI#9lY^^`-E=INo6|^CH?Yty^{(uvXnK{+=}WWeQ7Zk@ z1MNf+=^I&(XmDQB{;zh)4Vj|!=)s^S6bfXT*dG^S*TF1Pt6Zs2S+>7mNGprLpyqsz zd-pZx|03|mI2zRpr8dt(Bdha0XUq#2-Y@avmo~)0v}l`{Bd{hZkA$w^Pj;Akt7{*N zVSWh1e0ewUf3+WD6OZ$xd2z4Zfk)~)xo~Q3GduM4s$1;NAt9M7=jrn4`NX)l_~zS( zpood_f$9k(^I}G~M0Z){Jya<#GvTW?g(F9_b7(x69&*cF%`4NDSXhA;<*7wBZ?X^F zSz-0*$g`Z(N~KG`f_Z#Rzba^tK%~}{<##t-^xR4Zjy_%#SzAlU$$z%nsWI(QoUJGU z0!8g!_~Wb~Ni{m|MaBk}u7dUI)Mreq_&26*FSe153@>~*k|xLx6WHL(J-JUB&g z0RTBfnHRmG%^rPjeXSMM(XtVf#%y8pD>SBbBwI#nIiglD_?1Fqf3ac2CaZ{ zwi3oY%87mS-S63vbDBtZnYm9n&p&EaMK zZnGFgmEpFeS@`AHnS6EvEN*&dF@J zn;ZqAL5aNjsv68>RO~UvBKj)9$J7%P<=Jjau~Nmbx7dAtdvCS&qMlo92UD^z-a#`Cvw~$yvsVUAN%3>Gw?nDbAsS zY-{8bw=*xCC@XpR3~CmYK3GS*N)%k%!>^>;BA;t0)R_uFRz#2`CNtQJL#=NdWzLsg z-pfy}!ZLK75ll#W(R@!gY2l2WNNsLzv)C$@7&#f+Ug|W8P1;;Ap>n&f*J_M8yq;W- z72VzXnuyqvw@wt4ZnOCEs>Ts&>B~@6ZFW|PNl1vNPlji@nl$3$*GPy&l5LP%=Quyp zou01clA7I_ZZ2$ZyzwZK1y+)y_%v+fd6nlkhc1rRURikX$V8PKh(vdmF~9+WNH)T$ z8M4>)HZNazX((R)kSi{G`K`GEE}`0rzW)IpQ0NTPG;#uBbk$Rp1p*fgTK3u^(VIyc z2_)k%KzyjMbGJ_H6#V8@zcnZ?2hEQN2#{9t8<<4ZB`LfE@t8L_{7ACg64ZO=I%$7$ zo>&RFbosEny3>fQKho<&wCh%Uh|fk4q?Fa(QKYSG@XV>Vc$`;a*UgqxiGCa)({fE1 zrrBG(gdUWMWT3ZeGD)sPYU-50K zX8(+T53{*5lLxg8-HljJJ9zMDG)^7_qIp;hAVWN-tfqHaWAm;R8nw=>(Kpfx2$aoZ zVDEDB+@ar8O?ElIF;MV;2mtuF5DLx6pM|AZL-+i6B{tQ_d1z~jMVPPNH1FuQH%6Kq zkWco3ki3uzUc~wfZ@TDp_pNw3%|?U#>>;Imfv+T#X+Q0eEGl3%udD+bM8J?l$pN+Gi4?}_bHwMa*Y7w~k{DPF}m zI#gDI^o{ANk4DbkX$v8SoD{LMO?N!w5pXb|@(?Zp%v`-AUv(Z>T+8tq3iRq`)f%%6 zKG-}Gp?u18XF_P#E=Bfn<=*j4q-LOt@DNu2{#%RJpoPXA!u)UtgL&qsIJ^B7)#z-?}7`oDV$Ul+5Fkyl{4eWYjoD0lz`gt@Jg=Z zVo?^OF*LMXUxBi*(yw95$U@`p>pICdOnTVPppOUIu(>L%j(oA#Hv1tPl!6vW`azo% zi-A<8Px@Y8cg~@j4KoW;Ho7Vgg%I{()2n#Pu4Xb<@PV&`GJ9pk7A{_!yT&uZP}4S8 z0WlE2ls>NKjr)&=U)?StJFqYS#b|)b znA>B*0|G$VvDn%=Y@~f~+_FiNgXYe3nLv4F0`-^6DM-}232OuTdm5j?&fb)YCZw&==6VCGR_eY&?QbK^sN-^_D#_IknadRP*v zjwyvK)gtRVMLzNiLg2~22Frg2NwMFH<%`FbkG3_do^C%Ud#0uu$>6fQ$ttr+c%2ps zL$rldbjawK;&>RfBk86(EjP=I3T*n!(FtnIma~mOm5MyuazkqF&G@YzA<~4rug%Ty zie?aRoz{49@~MnmT<&O0!!f`1C}VD34%ab`J7F+LX~zJ{A)LT8!Rb}FQQx&#$SFm_ zpq%Mxs$>Z~pHQIM(T)jvK@$XsOOY=ZC6%b{@TSns_wanVCV=pZ3HlNHCJ+bsvO2BK z2@YR79^s4bdd??XSXK3^%WcuOIcjWGL3sh7*6hm+;o)!^x!UnBu1EgJzz%N@WR{)x z6p}zBKQqAQHmNDlkwOCEQ$g=G||3WYnGE74K8WyG|Ct+M}Pyy_@KNw%%$?jx-^inSJB2Quqc#af- zLisD;`@`Ws9-w5w3_GO;WNtrciNf7w;5gmyKs^pCxFzufZvc4@Q1 zst%i{k&RMp{ITbkG?9xldNF$Ru#`!QiIBD7v*@WzSyj5N zn`M_BLMgO+zEU6bwn4jo1Sj*h6^cSxa7dI!(OI-e4io4*+Hj%@ z^(4UKKbjIVsN{O1b*ZG-h#n}{v&8$|K7|i&9Z#3^XfJ)O$hgrxK`wKVXUF-AhX+R9 zTswPtHom875jJR@!PrR>EfbMtzgC~yX!YUnJ5|HD|Buw&}DhR@sDa%Ms@gLKz%ti=Vf<t(VD52p<$CUtn?FyhvnPdjY$cs`4%sw#ec zw#J~Rp<0MQ_-TIrQDpM4tR>0F-WatNUaYF7rvs84L(OyB(Ca!49rx4{5-uv^`9#+B zJ{Z$|Iwx?_UilTg4WwMmeiwYA5>3=hC~ur%`A~+g6{~_boE&di<^F>aOP^`Bc>+UY z$UrN%AtGGVQEecux-bEYsi;l!l-5!c-yxl64Llvm>bZIqbL%qoL%ZL)O zz0*)-`x$wFK^^PQ@0pItg<+=EyYH**+-pE2rxM5H;irQwhhYaQ(Oi=}?DunH6i+Oj z$JX@sO7-~6kWmo|yY3xSUn*7XwK#)ITJ%EbgX&Vt3z8qACR*^E>VEDzX*8{XL>C_x zTANZ9gkBEjY&}^^r;Ammq)*aBwik^l6{i@i)|tgBQbf<@5G$TOlsbPf3fLYK3~o}_ z7Z_0r9)QmIL1W3gJ182)`uCHImsA7+EfabR@o}Vs#*RN|JoYCAU022})sz;Uiw_5e z$0eh*6*Yo`j`8YlV~WzNw%ObkCMa-u=q+fa31m&P4Sp8{eJ5qy)LyNJi?Es#l)1>2 zBW07#4<)8e$@{b$1IkVwDODTV`6Q039uvPX_%)F)_;Z!k;pD5bZ7juVb?KXY1ccXj ztO84=%0!zpxT%GVP^~F{YIUXzSUs(KzVStBif0b}>?*YX<^1At&BS!G^A*g?<{Pir z4j3A|Gl<;yvZoR)!5B{5#<{izGW8JX#oyW7@Zl5Bu*U#q@bYv+?=w(UBT*uyo$ zdCy*aL$F1^!KLrX7%?GQo%CJ959|qecn{%JukWBPB;TBwXmKWszvD$tqE>qeN`QPJ zL~6G7KketLCydpBtmnY(uispd8SKYeqVoV`3Np!%Gsk16zI;11 z^J<7DwzB3RUEeDnNx%Fi$R?us(*1k8tFyv5!c(E?5~qTn+J|2>sq+W#0!5f-FTkExcYg!l3ZyY-{}?(ghj5uhLg; zYD#kZqJwx+-g<7yC2hP^0zkN$@rX#39E19bTH~wq2oh3FUPmw7!b8%`eM{L271TO2)bKrq1IwJN?$u8rAZmR1o`PFZ8Cuh!WlU834u zjh8r@k~b2>M^rK5RC!6Ld;TcM|Nr@&tM}Q!b2G3bAmS=1dB@|mhSsqw2ExVeT|Vn> zlIkxGe&QovdAP2lo}eaA>KP z71n4@oQKuE9Ck!P?Ls=6=g6+VHXFJA7QpBQwQ7dxc?NSyUIN;k!*)a{GwOv}8GCvs zK!l8y8{_&Z+z!2t$X*BQ=S5md*Fh5qWWbp$2Cl};NCw(HOV%kB*N9qMEyE{{+-{3^ z$eUT5r4;!ya$^68KXLy-4@nmS8o{;qTn!}AR@v>tODn}=#+3rA^7AcVNH5u*E623H(WsMf#@XgP`D2hQVp!49^8UW~56+fkv&fcV8- z=pmkniQJoz+cuQDZnq5qA6*N&556rMg>-5)1@zaIQW5b!o5PZ5NE3gA(rd;Ok+8=f z_<5Xgv=ad9qA_61#10qoKDFUFqP-i|*MD=5PwjKuVUG_!yNl`MzyuH-g({f#b<EN2AitnC#2I2ZDZXmxs=?8~ z+b`fcw|Adm3|rO`^$MVJBt=4-A`)yc*GFy z_LCmU?_V)saOaWXd?G6F!!+ano5BZz>HrV) znF^8xC{pZE#>8ld*$K7{Pj;CNJf@ib*irw8{q}3if3A9VQ*QQ6p+8TA71VL|&oT9O zEv$S6mrpL%SzKgL&Qn^Ip50u3s@6sG-J*PB-V0p>JrZ<8zlq(?Tz{);boya6=0r1f z6kn;)Akk=KWn~du#3bQJ_vhI8uMU0e9&>QS6J&Cj*sVGbpda z$3gc@f3QsIuh*-P4zipd#v}RVk7C9t(bbx*Vt6BYTFSKa#=8T5y~2}j_-BKbBKF=} zta;sq$=x-f>WNXEn={!}YVL2GA15hfUzbXk;H42e!RU&ub%@|SXO=JO7=iNa?^btp zIJZ5l=m%{W3C3ygyh6@wK1aF>s-!cgUy8}zreO-L`BzO};!11TkO?f)otn7;e6&?0 zI;bv=556>k>Y&Om%o926R4eclyQwgZZ=51a|wr9mD6QMb5Ib) z7PAOHvP|7<5H>Co`>hBGI;YV_r`!AZT(9ikv(rZ8Ino*$%3dcDpoQxiY`(pm9%@SN zku^`_{EDqYw5^^iIv>X`se@-pSjai#-UyV}9~4IUyL^qrsc|^M&f?h@MG``vllO2*wAb)#$dJ2obVsRZREQ|v;>b_f3!~pFq zbVb)yiJqX}SZ718y_xAhJ{VyroG+)<2_I)s{9d#tLkH`v;Om$j3QWE> diff --git a/docs/md_graphics/gui/mu_tracking_window_v2.png b/docs/md_graphics/gui/mu_tracking_window_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..569f6d6adfaeb6ff7d16b532c9bee3dc904c5fc9 GIT binary patch literal 15282 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 U4<&q8U$s?wq^?*j|MKns0k?#kB>(^b literal 0 HcmV?d00001 diff --git a/docs/md_graphics/gui/plot_window.png b/docs/md_graphics/gui/plot_window.png deleted file mode 100644 index 8753830559d19eb7e6803a716ca2c97e3fea3b07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54301 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 fe?KDFagCuQdIRIN=kFWOE5X4%TZDsqlJw#! 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

t!-_cFN8=KsdIzeA`~J} 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 G&i?_VJmk~> literal 0 HcmV?d00001 diff --git a/docs/md_graphics/gui/refsig_filter_window.png b/docs/md_graphics/gui/refsig_filter_window.png deleted file mode 100644 index 2fc33e023e37ca92207c8acb11134bbc55675ef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17829 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 QNEQ&{7w0SC(fs;<0N}`hEC2ui diff --git a/docs/md_graphics/gui/remove_mu_window.png b/docs/md_graphics/gui/remove_mu_window.png deleted file mode 100644 index a1b0f2ab3eab1703636530116ad0ecfb14d68082..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6503 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 literal 0 HcmV?d00001 diff --git a/docs/md_graphics/index/gui_preview.png b/docs/md_graphics/index/gui_preview.png deleted file mode 100644 index 4dc0c77c31d2285ab6661359d7bf13429dd2b388..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77400 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 diff --git a/docs/md_graphics/index/gui_preview_v2.png b/docs/md_graphics/index/gui_preview_v2.png new file mode 100644 index 0000000000000000000000000000000000000000..ebc26383d6e1f3afee97d84bc68a98299eea450f GIT binary patch literal 69208 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

s<&$vS(7 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 Qwaa4}2}N+pTf

-

🎉🎉 Exciting News!

-

We are thrilled to announce the publication of our Tutorial article!

-
Read it Now +

2 X 🎉 == Double News!

+

The 4th openhdemg beta release is now available! =>

+ See what's new +
+

We are thrilled to announce the publication of our Tutorial article! =>

+ Read it Now
{% endblock %} diff --git a/docs/tutorials/setup_working_env.md b/docs/tutorials/setup_working_env.md index d344c31..44fde9c 100644 --- a/docs/tutorials/setup_working_env.md +++ b/docs/tutorials/setup_working_env.md @@ -137,7 +137,7 @@ python -m openhdemg.gui.openhdemg_gui And the GUI will start: -![openhdemg GUI](../md_graphics/index/gui_preview.png) +![openhdemg GUI](../md_graphics/index/gui_preview_v2.png) If you instead want to write your own script using the functions contained in *openhdemg*, follow these steps: @@ -189,7 +189,7 @@ This should solve the issue and you should now be able to activate your Virtual ### *openhdemg* Installation Issues -Windows: +Windows (1): If trying to install *openhdemg* via pip you see this (or a similar) message in the terminal: @@ -199,6 +199,31 @@ ImportError: DLL load failed while importing _cext: The specified module could n the problem might be that you do not have the necessary Visual C++ Redistributable. This can be simply solved by visitng the [Microsoft website](https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170){:target="_blank"} and dowloading and installing the latest Visual Studio 2015, 2017, 2019 and 2022 redistributable. Please note, this is a single redistributable, you don't need to perform multiple dowloads. This should solve the issue and you should now be able to pip install *openhdemg*. If that's not the case, continue reading. +Windows (2): + +If trying to install *openhdemg* via pip you see this (or a similar) message in the terminal: + +``` +Microsoft Visual C++ 14.0 or greater is required. +``` + +The problem might be that you are missing the C++ build tools necessary to compile some code present in libraries such as pandas. To solve this, follow the steps below: + +1. visit [https://visualstudio.microsoft.com/visual-cpp-build-tools/](https://visualstudio.microsoft.com/visual-cpp-build-tools/){:target="_blank"} +2. click the button "download build tools" +3. install the tools +4. once the installation is completed, you should see a window like this: + +![Microsoft_Visual_C_step_1](setup_working_env/screen_visualC14_1.png) + +If the window is different, there should be a button like "install". Clicking "install" or "modify", if present. This should bring you to the next window (it is also possible that you directly visualise the next window if no previous installation of these components is present in your computer): + +![Microsoft_Visual_C_step_2](setup_working_env/screen_visualC14_1.png) + +Here, make sure that you select C++ build tool (as in the figure) and install them. If you have windows 10, you may want to select "Windows 10 SDK 10.0.20348" on the right panel. + +This should solve the issue and you should now be able to pip install *openhdemg*. If that's not the case, continue reading. + ## More questions? We hope that this tutorial was useful. If you need any additional information, do not hesitate to read the answers or ask a question in the [*openhdemg* discussion section](https://github.com/GiacomoValliPhD/openhdemg/discussions){:target="_blank"}. If you are not familiar with GitHub discussions, please read this [post](https://github.com/GiacomoValliPhD/openhdemg/discussions/42){:target="_blank"}. This will allow the *openhdemg* community to answer your questions. diff --git a/docs/tutorials/setup_working_env/screen_visualC14_1.png b/docs/tutorials/setup_working_env/screen_visualC14_1.png new file mode 100644 index 0000000000000000000000000000000000000000..09b0d85276196bd6fd3b82f167b23aee47a85d9d GIT binary patch literal 16582 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 qk_pGo*^@m2@|Ti}m&u!(+g?r_B<)t)(fF181- + ## :octicons-tag-24: 0.1.0-beta.3 :octicons-clock-24: November 2023 This release is focused on expanding the range of supported input files (decomposition outcomes) and to increase the speed and efficiency of the code. Furthermore, the introduction of a new backward compatibility module brings *openhdemg* a step closer to exiting the beta phase. +### Backward Compatibility + +By default, the .json files saved from *openhdemg* version 0.1.0-beta.2 (released in September 2023) cannot be opened in *openhdemg* version 0.1.0-beta.3. However, these files can be easily converted to the newer file format thanks to the new backward compatibility module. We also created a [tutorials section](tutorials/convert_old_json_files.md) where the users are guided to the migration towards newer versions of the library. This will ensure easy migration to the latest *openhdemg* release. + ### Major Achievements - **Extended input file compatibility**: now supporting a wider range of input files, making it easily accessible to anybody. @@ -46,10 +106,6 @@ Added new tutorials explaining: - How to use the backward compatibility module to easily migrate to the newer *openhdemg* releases. - In the tutorial “Setup working environment,” we specified that *openhdemg* is currently working with Python up to the 3.11.x version. We are working to make it compatible with Python 3.12. -### Backward Compatibility - -By default, the .json files saved from *openhdemg* version 0.1.0-beta.2 (released in September 2023) cannot be opened in *openhdemg* version 0.1.0-beta.3. However, these files can be easily converted to the newer file format thanks to the new backward compatibility module. We also created a [tutorials section](tutorials/convert_old_json_files.md) where the users are guided to the migration towards newer versions of the library. This will ensure easy migration to the latest *openhdemg* release. -
## :octicons-tag-24: 0.1.0-beta.2 diff --git a/mkdocs.yml b/mkdocs.yml index eb7a54b..b537729 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,6 +73,7 @@ nav: - Intro: gui_intro.md - Basics: gui_basics.md - Advanced: gui_advanced.md + - Settings: gui_settings.md - What's New: what's-new.md - Contacts: contacts.md - Cite us: cite-us.md From 02bfc1e7c686a64eb7838bdda704bb5cd833aa18 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:11:15 +0200 Subject: [PATCH 57/57] Remove pre-commit --- .cz.toml | 9 --------- .github/workflows/.pre-commit.yml | 16 ---------------- .pre-commit-config.yaml | 31 ------------------------------- CHANGELOG.md | 0 4 files changed, 56 deletions(-) delete mode 100644 .cz.toml delete mode 100644 .github/workflows/.pre-commit.yml delete mode 100644 .pre-commit-config.yaml delete mode 100644 CHANGELOG.md diff --git a/.cz.toml b/.cz.toml deleted file mode 100644 index 6e00be4..0000000 --- a/.cz.toml +++ /dev/null @@ -1,9 +0,0 @@ -[tool] -[tool.commitizen] -name = "cz_conventional_commits" -tag_format = "$version" -version_scheme = "pep440" -version_provider = "pep621" -version = "0.1.0-beta3" -update_changelog_on_bump = true -major_version_zero = true diff --git a/.github/workflows/.pre-commit.yml b/.github/workflows/.pre-commit.yml deleted file mode 100644 index 21b7dff..0000000 --- a/.github/workflows/.pre-commit.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: [main] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 - - uses: pre-commit/action@v3.0.1 - with: - extra_args: black --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 5fc3a9f..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# TODO section to check and complete -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 - hooks: - - id: check-toml - - id: check-yaml - - id: end-of-file-fixer - - id: mixed-line-ending - - id: name-tests-test - # - repo: local - # hooks: - # - id: pytest-run - # name: pytest-run - # stages: [commit] - # types: [python] - # entry: pytest - # language: system - # pass_filenames: false - # always_run: true - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/isort - rev: 5.10.1 - hooks: - - id: isort - args: ["--profile", "black"] diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29..0000000