From 88a6a812abe7461304b3b8c1d15f9b690674aba5 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 1 Jul 2023 13:33:34 +0200 Subject: [PATCH 1/7] Added: None Type Code --- docs/tutorials/Setup_working_env.md | 6 + openhdemg/gui/openhdemg_gui.py | 403 +++++++++++++++++++++------- 2 files changed, 309 insertions(+), 100 deletions(-) diff --git a/docs/tutorials/Setup_working_env.md b/docs/tutorials/Setup_working_env.md index 7116034..68e2ece 100644 --- a/docs/tutorials/Setup_working_env.md +++ b/docs/tutorials/Setup_working_env.md @@ -87,10 +87,16 @@ The Virtual environments provide an isolated and controlled environment for your In order to create a virtual environment type in your terminal: +For Windows users: ```shell python -m venv myvenv ``` +For Mac users: +```shell +python3 -m venv myvenv +``` + ![venv_command](Setup_working_env/venv_command.png) This command will create a Virtual environment named `myvenv`. diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 6397b99..8dc2852 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -7,10 +7,10 @@ import customtkinter import webbrowser from tkinter import ttk, filedialog, Canvas -from tkinter import StringVar, Tk, N, S, W, E +from tkinter import StringVar, Tk, N, S, W, E, IntVar, DoubleVar from pandastable import Table, config -from pathlib import Path -from sys import platform + +from PIL import Image import matplotlib.pyplot as plt import matplotlib @@ -47,6 +47,10 @@ class emgGUI: 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" @@ -74,7 +78,7 @@ 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 "OTB", "DEMUSE", or "Refsig". # TODO Paul verify this + Filetype can be "OENHDEMG", "OTB", "DEMUSE", or "OTB_REFSIG", "CUSTOM". self.filter_order : int, default 4 The filter order. self.firings_rec : int, default 4 @@ -127,6 +131,8 @@ class emgGUI: 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. @@ -290,6 +296,10 @@ class emgGUI: 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 ----- @@ -487,7 +497,8 @@ def __init__(self, master): # Create info button # Information Button info_path = master_path + "/gui_files/Info.png" # Get infor button path - self.info = tk.PhotoImage(file=info_path) + self.info = customtkinter.CTkImage(light_image=Image.open(info_path), + size=(30,30)) info_button = customtkinter.CTkButton( self.right, image=self.info, @@ -497,15 +508,9 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - # Get file path - path := Path("./openhdemg/gui/gui_files/test.pdf").resolve(), # Check user OS for pdf opening ( - webbrowser.open_new(str(path)) - if platform in ("win32", "linux") - else os.system(f"open {str(path)}") - if platform == "darwin" - else None + webbrowser.open("https://www.giacomovalli.com/openhdemg/GUI_intro/") ), ), ) @@ -513,7 +518,8 @@ def __init__(self, master): # Button for online tutorials online_path = master_path + "/gui_files/Online.png" - self.online = tk.PhotoImage(file=online_path) + self.online = customtkinter.CTkImage(light_image=Image.open(online_path), + size=(30,30)) online_button = customtkinter.CTkButton( self.right, image=self.online, @@ -522,12 +528,19 @@ 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/tutorials/Setup_working_env/") + ), + ), ) online_button.grid(row=1, column=1, sticky=E) # Button for dev information redirect_path = master_path + "/gui_files/Redirect.png" - self.redirect = tk.PhotoImage(file=redirect_path) + self.redirect = customtkinter.CTkImage(light_image=Image.open(redirect_path), + size=(30,30)) redirect_button = customtkinter.CTkButton( self.right, image=self.redirect, @@ -541,7 +554,8 @@ def __init__(self, master): # Button for contact information contact_path = master_path + "/gui_files/Contact.png" - self.contact = tk.PhotoImage(file=contact_path) + self.contact = customtkinter.CTkImage(light_image=Image.open(contact_path), + size=(30,30)) contact_button = customtkinter.CTkButton( self.right, image=self.contact, @@ -550,12 +564,19 @@ 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/Contacts/") + ), + ), ) contact_button.grid(row=3, column=1, sticky=E) # Button for citatoin information cite_path = master_path + "/gui_files/Cite.png" - self.cite = tk.PhotoImage(file=cite_path) + self.cite = customtkinter.CTkImage(light_image=Image.open(cite_path), + size=(30,30)) cite_button = customtkinter.CTkButton( self.right, image=self.cite, @@ -564,6 +585,12 @@ 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/") + ), + ), ) cite_button.grid(row=4, column=1, sticky=E) @@ -928,7 +955,7 @@ def open_advanced_tools(self): adv_box.set("Motor Unit Tracking") # Matrix Orientation - ttk.Label(self.a_window, text="Matrix Orientation*").grid( + ttk.Label(self.a_window, text="Matrix Orientation").grid( row=3, column=0, sticky=(W, E) ) self.mat_orientation_adv = StringVar() @@ -941,7 +968,7 @@ def open_advanced_tools(self): self.mat_orientation_adv.set("180") # Matrix code - ttk.Label(self.a_window, text="Matrix Code*").grid( + ttk.Label(self.a_window, text="Matrix Code").grid( row=4, column=0, sticky=(W, E) ) self.mat_code_adv = StringVar() @@ -952,13 +979,9 @@ def open_advanced_tools(self): matrix_code["state"] = "readonly" matrix_code.grid(row=4, column=1, sticky=(W, E)) self.mat_code_adv.set("GR08MM1305") - - # Instruction - ttk.Label( - self.a_window, - text="*Ignored for DEMUSE files, \ninsert random values", - font=("Arial", 8), - ).grid(row=5, column=1, sticky=W) + + # Trace variabel for updating window + self.mat_code_adv.trace("w", self.on_matrix_none_adv) # Analysis Button adv_button = ttk.Button( @@ -967,12 +990,35 @@ def open_advanced_tools(self): command=self.advanced_analysis, style="B.TButton", ) - adv_button.grid(column=0, row=6) + 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) + + 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 @@ -1229,6 +1275,7 @@ def edit_refsig(self): 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) @@ -1243,6 +1290,50 @@ def edit_refsig(self): 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) @@ -1258,7 +1349,7 @@ def filter_refsig(self): Raises ------ - TypeError + AttributeError When no reference signal file is available. See Also @@ -1275,19 +1366,19 @@ def filter_refsig(self): # Plot filtered Refsig self.in_gui_plotting(plot="refsig_fil") - except TypeError: + except AttributeError: tk.messagebox.showerror("Information", "Make sure a Refsig file is loaded.") def remove_offset(self): """ - Instance Method that remves user specified/selected Refsig offset. + 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 ------ - TypeError + AttributeError When no reference signal file is available See Also @@ -1304,13 +1395,67 @@ def remove_offset(self): # Update Plot self.in_gui_plotting(plot="refsig_off") - except TypeError: + 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(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 the start & end-point on the plot") + 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() + 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 @@ -1809,7 +1954,7 @@ def plot_emg(self): ) # Matrix code - ttk.Label(self.head, text="Matrix Code*").grid(row=0, column=3, sticky=(W)) + 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", "None") @@ -1817,8 +1962,11 @@ def plot_emg(self): 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)) + 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 @@ -1828,13 +1976,6 @@ def plot_emg(self): orientation.grid(row=1, column=4, sticky=(W, E)) self.mat_orientation.set("180") - # Instruction - ttk.Label( - self.head, - text="*Ignored for DEMUSE files, insert random values", - font=("Arial", 8), - ).grid(row=2, column=3, sticky=W) - # Plot derivation # Button deriv_button = ttk.Button( @@ -1909,8 +2050,9 @@ def plot_emg(self): matrix_canvas.create_image(0, 0, anchor="nw", image=self.matrix) # Information Button - self.info = tk.PhotoImage( - file=os.path.dirname(os.path.abspath(__file__)) + "/gui_files/Info.png" + 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, @@ -1921,15 +2063,9 @@ def plot_emg(self): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - # Get file path - path := Path("./openhdemg/gui/gui_files/test.pdf").resolve(), # Check user OS for pdf opening ( - webbrowser.open_new(str(path)) - if platform in ("win32", "linux") - else os.system(f"open {str(path)}") - if platform == "darwin" - else None + webbrowser.open("https://www.giacomovalli.com/openhdemg/GUI_basics/#plot-motor-units") ), ), ) @@ -1937,13 +2073,39 @@ def plot_emg(self): 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) + + 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. @@ -2196,12 +2358,26 @@ def plot_derivation(self): This function is used to plot also the sorted RAW_SIGNAL. """ try: - # Sort emg file - sorted_file = openhdemg.sort_rawemg( - emgfile=self.resdict, - code=self.mat_code.get(), - orientation=int(self.mat_orientation.get()), - ) + if self.mat_code.get() == "None": + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.row_cols_entry.get().split(",")] + + # 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] + ) + + 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": @@ -2248,55 +2424,68 @@ def plot_muaps(self): ``Remember: the different STAs should be matched`` with same number of electrode, processing (i.e., differential) and computed on the same timewindow. """ - try: + #try: + if self.mat_code.get() == "None": + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.row_cols_entry.get().split(",")] + # 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] ) - # 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( + else: + # Sort emg file + sorted_file = openhdemg.sort_rawemg( emgfile=self.resdict, - sorted_rawemg=diff_file, - firings="all", - timewindow=int(self.muap_time.get()), + 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(",")] + # 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) + # 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" - + "\n - Timewindow" - + "\n - MU Number", - ) + # except ValueError: + # tk.messagebox.showerror( + # "Information", + # "Enter valid input parameters." + # + "\nPotenital error sources:" + # + "\n - Matrix Code" + # + "\n - Matrix Orientation" + # + "\n - Figure size" + # + "\n - Timewindow" + # + "\n - MU Number", + # ) - except UnboundLocalError: - tk.messagebox.showerror("Information", "Enter valid Configuration.") + # except UnboundLocalError: + # tk.messagebox.showerror("Information", "Enter valid Configuration.") - except KeyError: - tk.messagebox.showerror("Information", "Enter valid Matrx Column.") + # except KeyError: + # tk.messagebox.showerror("Information", "Enter valid Matrx Column.") # ----------------------------------------------------------------------------------------------- # Advanced Analysis @@ -2446,12 +2635,27 @@ def advanced_analysis(self): self.head.destroy() self.a_window.destroy() - # Sort emg file - sorted_rawemg = openhdemg.sort_rawemg( - self.resdict, - code=self.mat_code_adv.get(), - orientation=int(self.mat_orientation_adv.get()), - ) + if self.mat_code_adv.get() == "None": + + # Get rows and columns and turn into list + list_rcs = [int(i) for i in self.row_cols_entry.get().split(",")] + + # 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] + ) + + 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, @@ -2470,7 +2674,6 @@ def advanced_analysis(self): 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. From aaa9fa55678bdee075d888e6870fac3792cedc43 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 3 Jul 2023 00:04:54 +0200 Subject: [PATCH 2/7] Fixed: None Matrix Code --- docs/GUI_advanced.md | 24 +++- docs/GUI_basics.md | 9 +- mkdocs.yml | 6 +- openhdemg/gui/openhdemg_gui.py | 204 ++++++++++++++++++++------------- 4 files changed, 154 insertions(+), 89 deletions(-) diff --git a/docs/GUI_advanced.md b/docs/GUI_advanced.md index 422fd97..053aea9 100644 --- a/docs/GUI_advanced.md +++ b/docs/GUI_advanced.md @@ -4,17 +4,27 @@ This is the toturial for the `Advanced Tools` in the *openhdemg* GUI. Great that ![advanced_analysis](md_graphics/GUI/Advanced_analysis_window.png) -So far, we have included three advanced analyses in the *openhdemg* GUI. +So far, we have included three advanced analyses in the *openhdemg* GUI. + - `Motor Unit Tracking` - `Duplicate Removal` - `Conduction Velocity Calculation` 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. -The `Matrix Code` must be specified according to the one you used during acquisition. So far, the codes +The `Matrix Code` must be specified according to the one you used during acquisition. So far, the codes + - `GR08MM1305` - `GR04MM1305` - `GR10MM0808` +- `None` + 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. In example, specifying + +```Python +Rows, Columns: 13, 5 +``` +means that your File has 65 channels. Once you specified these parameter, you can click the "Advaned Analysis" button to start your analysis. ----------------------------------------- @@ -25,6 +35,7 @@ When you want to track MUs across two different files, you need to select the `M ![mus_tracking](md_graphics/GUI/MU_Tracking_window.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) @@ -51,10 +62,11 @@ When you want to remove MUs duplicates across different files, you need to selec ![duplicate_removal](md_graphics/GUI/Duplicate_Removal_window.png) -1. You should specify How to remove the duplicated MUs in the `Which` dropdown. You can choose between -- munumber: Duplicated MUs are removed from the file with more MUs. -- PNR: The MU with the lowest PNR is removed. -- SIL: The MU with the lowest SIL is removed. +1. You should specify How to remove the duplicated MUs in the `Which` dropdown. You can choose between + + - munumber: Duplicated MUs are removed from the file with more MUs. + - PNR: The MU with the lowest PNR is removed. + - SIL: The MU with the lowest SIL is removed. 2. By clicking the `Remove Duplicates` button, you start the removal process. diff --git a/docs/GUI_basics.md b/docs/GUI_basics.md index ed59949..82a0f23 100644 --- a/docs/GUI_basics.md +++ b/docs/GUI_basics.md @@ -215,6 +215,13 @@ These three setting options are universally used in all plots. There are two mor - `GR08MM1305` - `GR04MM1305` - `GR10MM0808` + - `None` + + 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. 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`. @@ -277,7 +284,7 @@ We all make mistakes! But, most likely, we are also able to correct them. In cas -------------------------------------------- -We hope you had fun! We are now at the end of describing the basic functions included in the *openhdemg* GUI. In case you need further clarification, don't hesitate to post a question in the Github discussion forum (LINK). Moreover, if you noticed an error that was not properly catched by the GUI, please file a bug report according to our guidelines (LINK). +We hope you had fun! We are now at the end of describing the basic functions included in the *openhdemg* GUI. In case you need further clarification, don't hesitate to post a question in the [*openhdemg* discussion section](https://github.com/GiacomoValliPhD/openhdemg/discussions){:target="_blank"}. Moreover, if you noticed an error that was not properly catched by the GUI, please file a bug report according to our guidelines (LINK). If you want to proceed to the advanced stuff now, take a look at the [advanced](GUI_advanced.md) tab on the left side of the webpage. ## More questions? diff --git a/mkdocs.yml b/mkdocs.yml index c74ed95..5c0c409 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,9 +77,9 @@ markdown_extensions: smart_enable: all - pymdownx.caret - pymdownx.details - - pymdownx.emoji: - emoji_generator: !!python/name:materialx.emoji.to_svg - emoji_index: !!python/name:materialx.emoji.twemoji + # - pymdownx.emoji: + # emoji_generator: !!python/name:materialx.emoji.to_svg + # emoji_index: !!python/name:materialx.emoji.twemoji - pymdownx.highlight: anchor_linenums: true line_spans: __span diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 8dc2852..4925b4f 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -7,7 +7,7 @@ import customtkinter import webbrowser from tkinter import ttk, filedialog, Canvas -from tkinter import StringVar, Tk, N, S, W, E, IntVar, DoubleVar +from tkinter import StringVar, Tk, N, S, W, E, DoubleVar from pandastable import Table, config from PIL import Image @@ -184,11 +184,17 @@ class emgGUI: analysis window when OTB files are loaded. Contains the extension factor for OTB files. Stringvariable containing the - self.extension_factor : tk.Stringvar() + 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. @@ -1011,6 +1017,7 @@ def on_matrix_none_adv(self, *args): 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"): @@ -2087,7 +2094,6 @@ def on_matrix_none(self, *args): 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:") @@ -2096,6 +2102,7 @@ def on_matrix_none(self, *args): 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"): @@ -2360,17 +2367,26 @@ def plot_derivation(self): try: if self.mat_code.get() == "None": # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.row_cols_entry.get().split(",")] + 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 - # 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] - ) - else: # Sort emg file sorted_file = openhdemg.sort_rawemg( @@ -2405,7 +2421,8 @@ def plot_derivation(self): + "\nPotenital error sources:" + "\n - Matrix Code" + "\n - Matrix Orientation" - + "\n - Figure size", + + "\n - Figure size arguments" + + "\n - Rows, Columns arguments", ) except UnboundLocalError: tk.messagebox.showerror( @@ -2424,68 +2441,80 @@ def plot_muaps(self): ``Remember: the different STAs should be matched`` with same number of electrode, processing (i.e., differential) and computed on the same timewindow. """ - #try: - if self.mat_code.get() == "None": - # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.row_cols_entry.get().split(",")] + 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(",")] - # 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] - ) - else: - # Sort emg file - sorted_file = openhdemg.sort_rawemg( + 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, - code=self.mat_code.get(), - orientation=int(self.mat_orientation.get()), + sorted_rawemg=diff_file, + firings="all", + timewindow=int(self.muap_time.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(",")] + # 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) + # 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" - # + "\n - Timewindow" - # + "\n - MU Number", - # ) + 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 UnboundLocalError: + tk.messagebox.showerror("Information", "Enter valid Configuration.") - # except KeyError: - # tk.messagebox.showerror("Information", "Enter valid Matrx Column.") + except KeyError: + tk.messagebox.showerror("Information", "Enter valid Matrx Column.") # ----------------------------------------------------------------------------------------------- # Advanced Analysis @@ -2638,16 +2667,24 @@ def advanced_analysis(self): if self.mat_code_adv.get() == "None": # Get rows and columns and turn into list - list_rcs = [int(i) for i in self.row_cols_entry.get().split(",")] - - # 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] - ) + 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 else: # Sort emg file @@ -2669,6 +2706,15 @@ def advanced_analysis(self): + "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() @@ -2686,7 +2732,7 @@ def open_emgfile1(self): -------- open_emgfile1(), openhdemg.askopenfile() """ - try: + try: # Open OTB file if self.filetype_adv.get() == "OTB": self.emgfile1 = openhdemg.askopenfile( From 21fb1d9e9f3fdc466022daf303b2960044d354a0 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 3 Jul 2023 00:06:30 +0200 Subject: [PATCH 3/7] Fixed: mkdocs yml --- mkdocs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 5c0c409..c74ed95 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,9 +77,9 @@ markdown_extensions: smart_enable: all - pymdownx.caret - pymdownx.details - # - pymdownx.emoji: - # emoji_generator: !!python/name:materialx.emoji.to_svg - # emoji_index: !!python/name:materialx.emoji.twemoji + - pymdownx.emoji: + emoji_generator: !!python/name:materialx.emoji.to_svg + emoji_index: !!python/name:materialx.emoji.twemoji - pymdownx.highlight: anchor_linenums: true line_spans: __span From ecbdd97bba1506c8a3f64ff65747e3cccebaf814 Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 3 Jul 2023 00:12:13 +0200 Subject: [PATCH 4/7] Fixed: GUI advanced docs --- docs/GUI_advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/GUI_advanced.md b/docs/GUI_advanced.md index 053aea9..96fb3e5 100644 --- a/docs/GUI_advanced.md +++ b/docs/GUI_advanced.md @@ -25,7 +25,7 @@ In case you selected `None`, the entrybox `Rows, Columns` will appear. Please sp Rows, Columns: 13, 5 ``` means that your File has 65 channels. -Once you specified these parameter, you can click the "Advaned Analysis" button to start your analysis. +Once you specified these parameter, you can click the `Advaned Analysis` button to start your analysis. ----------------------------------------- From 73759f77dd3d85a592d7fb460236851f81392187 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 3 Jul 2023 13:25:42 +0200 Subject: [PATCH 5/7] Docs, Formatting and Fixes Changes to uniform function's parameters, documentation and formatting. Also minor bug fixes and implementation in light of release 0.1.0-beta --- README.md | 2 +- docs/API_openfiles.md | 4 +-- docs/GUI_basics.md | 26 +++++++++++---- docs/md_graphics/GUI/Refsig_Filter_window.png | Bin 9839 -> 14395 bytes openhdemg/gui/openhdemg_gui.py | 2 +- openhdemg/library/electrodes.py | 2 +- openhdemg/library/info.py | 4 +-- openhdemg/library/mathtools.py | 30 +++++++++++------- openhdemg/library/muap.py | 13 +++++--- openhdemg/library/openfiles.py | 4 +-- openhdemg/library/plotemg.py | 2 +- setup.py | 6 ++-- 12 files changed, 59 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index cd001bc..dd18d4b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ or conda: conda install -c conda-forge openhdemg ``` -If you want an overview of what you can do with the *openhdemg* library, have a look at the [**Quick Start** section](https://www.giacomovalli.com/openhdemg/Quick-Start/). +If you want an overview of what you can do with the *openhdemg* library, have a look at the [Quick Start section](https://www.giacomovalli.com/openhdemg/Quick-Start/). ## Good to know In addition to the rich set of modules and functions presented in the **API documentation**, *openhdemg* offers also a practical graphical user interface (GUI) from which many tasks can be performed without writing a single line of code! diff --git a/docs/API_openfiles.md b/docs/API_openfiles.md index 705a504..9611ec4 100644 --- a/docs/API_openfiles.md +++ b/docs/API_openfiles.md @@ -35,12 +35,12 @@ Notes ----- Once opened, the file is returned as a dictionary with keys:
-"SOURCE" : source of the file (i.e., "DEMUSE", "OTB", "custom")
+"SOURCE" : source of the file (e.g., "DEMUSE", "OTB", "custom")
"RAW_SIGNAL" : the raw EMG signal
"REF_SIGNAL" : the reference signal
"PNR" : pulse to noise ratio
"SIL" : silouette score
-"IPTS" : pulse train
+"IPTS" : pulse train (decomposed source)
"MUPULSES" : instants of firing
"FSAMP" : sampling frequency
"IED" : interelectrode distance
diff --git a/docs/GUI_basics.md b/docs/GUI_basics.md index 82a0f23..c27d9e6 100644 --- a/docs/GUI_basics.md +++ b/docs/GUI_basics.md @@ -38,7 +38,7 @@ The *openhdemg* GUI also allows you to edit and filter reference signals corresp 1. View the MUs using the `View MUs` button prior to reference signal editing, so you can see what is happening. -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. +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. 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 @@ -66,7 +66,21 @@ The *openhdemg* GUI also allows you to edit and filter reference signals corresp Offset Value : 0 Automatic: 0 ``` - will allow you to manually correct the offset in a new pop-up plot. You just need to follow the instructions on the plot. + will allow you to manually correct the offset in a new pop-up plot. You just need to follow the instructions on the plot. + +5. When you click the `Convert` button, the reference signal will be multiplied or divided (depending on `Operator`) by the `Factor`. In example, specifying + + ```Python + Operator : "Multiply" + Factor: 2.5 + ``` + will amplify the reference signal 2.5 times. + +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 + MVC Value : 500 + ``` ## Resize EMG File Sometimes, resizing of your analysis file is unevitable. Luckily, *openhdemg* provides an easy solution. In row five and column two in the left side of the GUI, you can find the `Resize File` button. @@ -194,7 +208,7 @@ You can choose between the follwing plotting options: - Plot the raw emg signal. Single or multiple channels. (Plot EMGSig) - Plot the reference signal. (Plot RefSig) - Plot all the MUs pulses (binary representation of the firings time). (Plot MUPulses) -- Plot the source of decomposition. (Plot Source) +- 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) @@ -239,9 +253,9 @@ These three setting options are universally used in all plots. There are two mor 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 Source of decomposition -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 MUs decomposition in your analysis file. -2. Enter/select a `MU Number` in/from the dropdown list. For example, if you want to plot the source of `MU Number` one enter *0* in the dropdown. If you want to plot the sources 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 sources of all included MUs in the analysis file. +### 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 diff --git a/docs/md_graphics/GUI/Refsig_Filter_window.png b/docs/md_graphics/GUI/Refsig_Filter_window.png index 9f638ee8d2b0716360471564ee424b66f2c83134..370fdefef2f809c56185e82c86f33ac7051dda99 100644 GIT binary patch literal 14395 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 literal 9839 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 diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 4925b4f..21ac480 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -2897,7 +2897,7 @@ def remove_duplicates_between(self): """ try: # Remove motor unit duplicates - emg_file1, emg_file2 = openhdemg.remove_duplicates_between( + emg_file1, emg_file2, _ = openhdemg.remove_duplicates_between( emgfile1=self.emgfile1, emgfile2=self.emgfile2, threshold=float(self.threshold_adv.get()), diff --git a/openhdemg/library/electrodes.py b/openhdemg/library/electrodes.py index 129b56c..fec4bcb 100644 --- a/openhdemg/library/electrodes.py +++ b/openhdemg/library/electrodes.py @@ -374,7 +374,7 @@ def sort_rawemg( # columns. But first check for missing empty channel. if n_rows * n_cols != sorted_rawemg.shape[1]: raise ValueError( - "Wrong number of channels in sorted_rawemg. Check also n_rows and n_cols" + "Number of specified rows and columns must match the number of channels." ) empty_dict = {f"col{n}": None for n in range(n_cols)} diff --git a/openhdemg/library/info.py b/openhdemg/library/info.py index c95cd24..3f51a66 100644 --- a/openhdemg/library/info.py +++ b/openhdemg/library/info.py @@ -122,7 +122,7 @@ def abbreviations(self): "FSAMP": "Sampling frequency", "IDR": "Instantaneous discharge rate", "IED": "Inter electrode distance", - "IPTS": "Impulse train (source of decomposition)", + "IPTS": "Impulse train (decomposed source)", "MU": "Motor units", "MUAP": "MUs action potential", "PNR": "Pulse to noise ratio", @@ -141,7 +141,7 @@ def abbreviations(self): "FSAMP": "Sampling frequency", "IDR": "Instantaneous discharge rate", "IED": "Inter electrode distance", - "IPTS": "Impulse train (source of decomposition)", + "IPTS": "Impulse train (decomposed source)", "MU": "Motor units", "MUAP": "MUs action potential", "PNR": "Pulse to noise ratio", diff --git a/openhdemg/library/mathtools.py b/openhdemg/library/mathtools.py index c3fdec1..39114de 100644 --- a/openhdemg/library/mathtools.py +++ b/openhdemg/library/mathtools.py @@ -246,7 +246,7 @@ def compute_sil(ipts, mupulses): Parameters ---------- ipts : pd.Series - The source of decomposition (or pulse train, IPTS[mu]) of the MU of + The decomposed source (or pulse train, IPTS[mu]) of the MU of interest. mupulses : ndarray The time of firing (MUPULSES[mu]) of the MU of interest. @@ -261,6 +261,10 @@ 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) + if len(mupulses) == 0: + return np.nan + # Extract source and peaks and align source and peaks based on IPTS source = ipts.to_numpy() peaks_idxs = mupulses - ipts.index[0] @@ -296,22 +300,23 @@ def compute_sil(ipts, mupulses): return sil -def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=False): +def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=True): """ Calculate the pulse to noise ratio for a single MU. Parameters ---------- ipts : pd.Series - The source of decomposition (or pulse train, IPTS[mu]) of the MU of + The decomposed source (or pulse train, IPTS[mu]) of the MU of interest. mupulses : ndarray The time of firing (MUPULSES[mu]) of the MU of interest. 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. This can be set to - True when working with pathological tremor. + 2012, this is common in pathological tremor. The user is encouraged to + use the default value (True) to increase the robustness of the + estimation. Returns ------- @@ -325,8 +330,8 @@ def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=False): # According to Holobar 2014, the PNR is calculated as: # 10 * log10((mean of firings) / (mean of noise)) - # Where instants in the source of decomposition are classified as firings - # or noise based on a threshold value named "Pi" or "r". + # 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: @@ -357,7 +362,6 @@ def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=False): # of variations to estimate Pi or not. # If both are used, Pi would be calculated as: # Pi = CoVIDI + CoVpIDI - # which remains valid also in tremor. # Otherwise, Pi would be calculated as: # Pi = CoV_all_IDI @@ -392,12 +396,14 @@ def compute_pnr(ipts, mupulses, fsamp, separate_paired_firings=False): idinonp = idi[idi >= (fsamp * 0.05)] idip = idi[idi < (fsamp * 0.05)] - CoVIDI = np.std(idinonp) / np.mean(idinonp) - if math.isnan(CoVIDI): + if len(idinonp) > 1: + CoVIDI = np.std(idinonp) / np.mean(idinonp) + else: CoVIDI = 0 - CoVpIDI = np.std(idip) / np.mean(idip) - if math.isnan(CoVpIDI): + if len(idip) > 1: + CoVpIDI = np.std(idip) / np.mean(idip) + else: CoVpIDI = 0 # Calculate Pi diff --git a/openhdemg/library/muap.py b/openhdemg/library/muap.py index af2a64a..50eecd4 100644 --- a/openhdemg/library/muap.py +++ b/openhdemg/library/muap.py @@ -1098,6 +1098,9 @@ def remove_duplicates_between( ------- emgfile1, emgfile2 : dict The original emgfiles without the duplicated MUs. + tracking_res : pd.DataFrame + The results of the tracking including the MU from file 1, + MU from file 2 and the normalised cross-correlation value (XCC). See also -------- @@ -1114,7 +1117,7 @@ def remove_duplicates_between( >>> emgfile1 = emg.askopenfile(filesource="OTB", otb_ext_factor=8) >>> emgfile2 = emg.askopenfile(filesource="OTB", otb_ext_factor=8) - >>> emgfile1, emgfile2 = emg.remove_duplicates_between( + >>> emgfile1, emgfile2, tracking_res = emg.remove_duplicates_between( ... emgfile1, ... emgfile2, ... firings="all", @@ -1163,7 +1166,7 @@ def remove_duplicates_between( emgfile=emgfile1, munumber=mus_to_remove, if_single_mu="remove" ) - return emgfile1, emgfile2 + return emgfile1, emgfile2, tracking_res else: # Remove MUs from emgfile2 @@ -1173,7 +1176,7 @@ def remove_duplicates_between( emgfile=emgfile2, munumber=mus_to_remove, if_single_mu="remove" ) - return emgfile1, emgfile2 + return emgfile1, emgfile2, tracking_res elif which == "PNR": # Create a list containing which MU to remove in which file based @@ -1199,7 +1202,7 @@ def remove_duplicates_between( emgfile=emgfile2, munumber=to_remove2, if_single_mu="remove" ) - return emgfile1, emgfile2 + return emgfile1, emgfile2, tracking_res elif which == "SIL": # Create a list containing which MU to remove in which file based @@ -1225,7 +1228,7 @@ def remove_duplicates_between( emgfile=emgfile2, munumber=to_remove2, if_single_mu="remove" ) - return emgfile1, emgfile2 + return emgfile1, emgfile2, tracking_res else: raise ValueError( diff --git a/openhdemg/library/openfiles.py b/openhdemg/library/openfiles.py index f9c7919..0524985 100644 --- a/openhdemg/library/openfiles.py +++ b/openhdemg/library/openfiles.py @@ -40,7 +40,7 @@ "REF_SIGNAL" : the reference signal "PNR" : pulse to noise ratio "SIL" : silouette score - "IPTS" : pulse train + "IPTS" : pulse train (decomposed source) "MUPULSES" : instants of firing "FSAMP" : sampling frequency "IED" : interelectrode distance @@ -433,7 +433,7 @@ def get_otb_mupulses(binarymusfiring): for i in binarymusfiring: # Loop all the MUs my_ndarray = [] - for idx, x in binarymusfiring[i].items(): # Loop the MU firing times #This was iteritems() in older versions + for idx, x in binarymusfiring[i].items(): # Loop the MU firing times if x > 0: my_ndarray.append(idx) # Take the firing time and add it to the ndarray diff --git a/openhdemg/library/plotemg.py b/openhdemg/library/plotemg.py index 90320ea..eb63e3a 100644 --- a/openhdemg/library/plotemg.py +++ b/openhdemg/library/plotemg.py @@ -582,7 +582,7 @@ def plot_ipts( tight_layout=True, ): """ - Plot the IPTS (source of decomposition). + Plot the IPTS (decomposed source). IPTS is the non-binary representation of the MUs firing times. diff --git a/setup.py b/setup.py index 35607b4..2e3eecc 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ from distutils.core import setup this_directory = Path(__file__).parent -LONG_DESCRIPTION = (this_directory / "README.md").read_text() +long_descr = (this_directory / "README.md").read_text() if __name__ == "__main__": setup( @@ -53,7 +53,7 @@ maintainer="Giacomo Valli", maintainer_email="giacomo.valli@phd.unipd.it", description="Open-source analysis of High-Density EMG data", - long_description=LONG_DESCRIPTION, + long_description=long_descr, long_description_content_type='text/markdown', license="GPL-3.0", project_urls={ @@ -62,7 +62,7 @@ "Source Code": "https://github.com/GiacomoValliPhD/openhdemg", "Bug Tracker": "https://github.com/GiacomoValliPhD/openhdemg/issues", }, - version=emg.__version__, # "0.1.0-beta.30", + version="0.1.0-beta.31", # emg.__version__, install_requires=INSTALL_REQUIRES, include_package_data=True, packages=PACKAGES, From 6e405799b6f013307da43a9b29c08a0c3449688e Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Mon, 3 Jul 2023 23:32:17 +0200 Subject: [PATCH 6/7] Minor changes for first beta release --- openhdemg/gui/openhdemg_gui.py | 40 ++++++++++++++++++++++++++++++++-- openhdemg/library/muap.py | 16 ++++++++------ setup.py | 6 ++--- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 21ac480..a907c38 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -2826,6 +2826,21 @@ def track_mus(self): -------- 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( @@ -2835,6 +2850,8 @@ def track_mus(self): 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(), @@ -2872,7 +2889,8 @@ def track_mus(self): + "\n - Extension Factor (in case of OTB file)" + "\n - Matrix Code" + "\n - Matrix Orientation" - + "\n - Threshold", + + "\n - Threshold" + + "\n - Rows, Columns", ) def remove_duplicates_between(self): @@ -2895,6 +2913,21 @@ 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 + 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( @@ -2904,6 +2937,8 @@ def remove_duplicates_between(self): 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(), @@ -2928,7 +2963,8 @@ def remove_duplicates_between(self): + "\n - Matrix Code" + "\n - Matrix Orientation" + "\n - Threshold" - + "\n - Which", + + "\n - Which" + + "\n - Rows, Columns", ) def calculate_conduct_vel(self): diff --git a/openhdemg/library/muap.py b/openhdemg/library/muap.py index 50eecd4..ef9a2a5 100644 --- a/openhdemg/library/muap.py +++ b/openhdemg/library/muap.py @@ -743,7 +743,7 @@ def tracking( emgfile1, emgfile2, firings="all", - derivation="mono", + derivation="sd", timewindow=50, threshold=0.8, matrixcode="GR08MM1305", @@ -771,7 +771,7 @@ def tracking( 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 mono + derivation : str {mono, sd, dd}, default sd Whether to compute the sta on the monopolar signal, or on the single or double differential derivation. timewindow : int, default 50 @@ -898,6 +898,8 @@ def tracking( emgfile2, emgfile2_sorted, firings=firings, timewindow=timewindow * 2, ) + print("\nTracking started") + # Tracking function to run in parallel def parallel(mu_file1): # Loop all the MUs of file 1 # Dict to fill with the 2d cross-correlation results @@ -911,7 +913,7 @@ def parallel(mu_file1): # Loop all the MUs of file 1 sta_emgfile2[mu_file2], finalduration=0.5 ) - aligned_sta1, aligned_sta2 = sta_emgfile1[mu_file1], sta_emgfile2[mu_file2] + #aligned_sta1, aligned_sta2 = sta_emgfile1[mu_file1], sta_emgfile2[mu_file2] # Second, compute 2d cross-correlation df1, _ = unpack_sta(aligned_sta1) @@ -936,10 +938,10 @@ def parallel(mu_file1): # Loop all the MUs of file 1 return res # Start parallel execution - # Meausere running time + # Measure running time t0 = time.time() - res = Parallel(n_jobs=-1)( + res = Parallel(n_jobs=8)( delayed(parallel)(mu_file1) for mu_file1 in range(emgfile1["NUMBER_OF_MUS"]) ) @@ -1032,7 +1034,7 @@ def remove_duplicates_between( emgfile1, emgfile2, firings="all", - derivation="mono", + derivation="sd", timewindow=50, threshold=0.9, matrixcode="GR08MM1305", @@ -1060,7 +1062,7 @@ def remove_duplicates_between( 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 mono + derivation : str {mono, sd, dd}, default sd Whether to compute the sta on the monopolar signal, or on the single or double differential derivation. timewindow : int, default 50 diff --git a/setup.py b/setup.py index 2e3eecc..c29af46 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ "pyperclip>=1.8.2", "scipy>=1.10.1", "seaborn>=0.12.2", + "joblib>=1.3.1", ] PACKAGES = [ @@ -23,7 +24,6 @@ "openhdemg.gui.gui_files", "openhdemg.library", "openhdemg.library.decomposed_test_files", - "docs.md_graphics.Index", ] CLASSIFIERS = [ @@ -49,7 +49,7 @@ if __name__ == "__main__": setup( - name="testgiacomovalli", + name="openhdemg", maintainer="Giacomo Valli", maintainer_email="giacomo.valli@phd.unipd.it", description="Open-source analysis of High-Density EMG data", @@ -62,7 +62,7 @@ "Source Code": "https://github.com/GiacomoValliPhD/openhdemg", "Bug Tracker": "https://github.com/GiacomoValliPhD/openhdemg/issues", }, - version="0.1.0-beta.31", # emg.__version__, + version=emg.__version__, install_requires=INSTALL_REQUIRES, include_package_data=True, packages=PACKAGES, From 3eb16ca753debe93f4d3640566b5527d54f14ab2 Mon Sep 17 00:00:00 2001 From: Giacomo Valli_PhD <81100252+GiacomoValliPhD@users.noreply.github.com> Date: Tue, 4 Jul 2023 08:56:25 +0200 Subject: [PATCH 7/7] Minor fixes --- openhdemg/gui/openhdemg_gui.py | 9 +++++---- setup.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index a907c38..8da01f4 100644 --- a/openhdemg/gui/openhdemg_gui.py +++ b/openhdemg/gui/openhdemg_gui.py @@ -514,7 +514,6 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - # Check user OS for pdf opening ( webbrowser.open("https://www.giacomovalli.com/openhdemg/GUI_intro/") ), @@ -535,7 +534,6 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - # Check user OS for pdf opening ( webbrowser.open("https://www.giacomovalli.com/openhdemg/tutorials/Setup_working_env/") ), @@ -555,6 +553,11 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + command=lambda: ( + ( + webbrowser.open("https://www.giacomovalli.com/openhdemg/Contacts/#meet-the-developers") + ), + ), ) redirect_button.grid(row=2, column=1, sticky=E) @@ -571,7 +574,6 @@ def __init__(self, master): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - # Check user OS for pdf opening ( webbrowser.open("https://www.giacomovalli.com/openhdemg/Contacts/") ), @@ -2070,7 +2072,6 @@ def plot_emg(self): bg_color="LightBlue4", fg_color="LightBlue4", command=lambda: ( - # Check user OS for pdf opening ( webbrowser.open("https://www.giacomovalli.com/openhdemg/GUI_basics/#plot-motor-units") ), diff --git a/setup.py b/setup.py index c29af46..424f559 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ license="GPL-3.0", project_urls={ "Documentation": "https://giacomovalli.com/openhdemg", - "release notes": "https://giacomovalli.com/openhdemg/What%27s-New", + "Release Notes": "https://giacomovalli.com/openhdemg/What%27s-New", "Source Code": "https://github.com/GiacomoValliPhD/openhdemg", "Bug Tracker": "https://github.com/GiacomoValliPhD/openhdemg/issues", },