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_advanced.md b/docs/GUI_advanced.md index 422fd97..96fb3e5 100644 --- a/docs/GUI_advanced.md +++ b/docs/GUI_advanced.md @@ -4,18 +4,28 @@ 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. -Once you specified these parameter, you can click the "Advaned Analysis" button to start your analysis. +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..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) @@ -215,6 +229,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`. @@ -232,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 @@ -277,7 +298,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/docs/md_graphics/GUI/Refsig_Filter_window.png b/docs/md_graphics/GUI/Refsig_Filter_window.png index 9f638ee..370fdef 100644 Binary files a/docs/md_graphics/GUI/Refsig_Filter_window.png and b/docs/md_graphics/GUI/Refsig_Filter_window.png differ 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/mkdocs.yml b/mkdocs.yml index 181226e..c74ed95 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,9 +43,9 @@ nav: - For new users: - Setup working environment: tutorials/Setup_working_env.md - Graphical-Interface: - - Intro: GUI_intro.md - - Basics: GUI_basics.md - - Advanced: GUI_advanced.md + - intro: GUI_intro.md + - basics: GUI_basics.md + - advanced: GUI_advanced.md - What's-New.md - Contacts.md - Cite-Us.md @@ -108,9 +108,9 @@ extra: generator: false # Remove 'Made with Material for MkDocs' in the footer. social: - icon: fontawesome/brands/github - link: https://github.com/GiacomoValliPhD/openhdemg + link: https://github.com/GiacomoValliPhD - icon: fontawesome/brands/twitter - link: https://twitter.com/openhdemg + link: https:// version: provider: mike # TODO versioning diff --git a/openhdemg/gui/openhdemg_gui.py b/openhdemg/gui/openhdemg_gui.py index 6397b99..8da01f4 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, 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. @@ -178,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. @@ -290,6 +302,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 +503,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 +514,8 @@ 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 +523,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 +533,18 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + command=lambda: ( + ( + 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, @@ -536,12 +553,18 @@ 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) # 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 +573,18 @@ def __init__(self, master): height=30, bg_color="LightBlue4", fg_color="LightBlue4", + command=lambda: ( + ( + 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 +593,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 +963,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 +976,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 +987,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 +998,36 @@ 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) + 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 @@ -1229,6 +1284,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 +1299,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 +1358,7 @@ def filter_refsig(self): Raises ------ - TypeError + AttributeError When no reference signal file is available. See Also @@ -1275,19 +1375,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 +1404,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 +1963,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 +1971,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 +1985,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 +2059,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 +2072,8 @@ 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 +2081,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) + 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. @@ -2196,12 +2366,35 @@ 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.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": @@ -2229,7 +2422,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( @@ -2249,12 +2443,36 @@ def plot_muaps(self): processing (i.e., differential) and computed on the same timewindow. """ 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.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) @@ -2287,9 +2505,10 @@ def plot_muaps(self): + "\nPotenital error sources:" + "\n - Matrix Code" + "\n - Matrix Orientation" - + "\n - Figure size" + + "\n - Figure size arguments" + "\n - Timewindow" - + "\n - MU Number", + + "\n - MU Number" + + "\n - Rows, Columns arguments", ) except UnboundLocalError: @@ -2446,12 +2665,35 @@ 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.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 + 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, @@ -2465,12 +2707,20 @@ 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() ### Define function for advanced analysis tools - def open_emgfile1(self): """ Open EMG file based on the selected file type and extension factor. @@ -2483,7 +2733,7 @@ def open_emgfile1(self): -------- open_emgfile1(), openhdemg.askopenfile() """ - try: + try: # Open OTB file if self.filetype_adv.get() == "OTB": self.emgfile1 = openhdemg.askopenfile( @@ -2577,6 +2827,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( @@ -2586,6 +2851,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(), @@ -2623,7 +2890,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): @@ -2646,15 +2914,32 @@ 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( + 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(), @@ -2679,7 +2964,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/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..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 @@ -1098,6 +1100,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 +1119,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 +1168,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 +1178,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 +1204,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 +1230,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..424f559 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 = [ @@ -45,24 +45,24 @@ 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( - name="testgiacomovalli", + name="openhdemg", 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={ "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", }, - version=emg.__version__, # "0.1.0-beta.30", + version=emg.__version__, install_requires=INSTALL_REQUIRES, include_package_data=True, packages=PACKAGES,