├── .gitignore ├── Example Figures ├── Final_emg_data.png ├── recruitment_model_100MUs.png ├── sEMG_electrode_array_100MUs_3X3.png └── sEMG_with_and_without_noise_100MUs.png ├── Example_Code_Figures ├── Membrane_Tripole │ ├── Membrane_Tripole.png │ ├── run.py │ └── MU.py ├── Recruitment_Model │ ├── Recruitment_Model.png │ └── run.py ├── sEMG_Electrode_Array │ ├── sEMG_Electrode_Array.png │ ├── run.py │ └── MU.py └── sEMG_Signal_Noise │ ├── sEMG_Signal_Colored_Noise.png │ ├── run.py │ └── MU.py ├── setup.py ├── LICENSE ├── README.md └── semg_sim ├── main.py ├── SD.py ├── MU.py └── sEMG.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | .venv -------------------------------------------------------------------------------- /Example Figures/Final_emg_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example Figures/Final_emg_data.png -------------------------------------------------------------------------------- /Example Figures/recruitment_model_100MUs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example Figures/recruitment_model_100MUs.png -------------------------------------------------------------------------------- /Example Figures/sEMG_electrode_array_100MUs_3X3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example Figures/sEMG_electrode_array_100MUs_3X3.png -------------------------------------------------------------------------------- /Example Figures/sEMG_with_and_without_noise_100MUs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example Figures/sEMG_with_and_without_noise_100MUs.png -------------------------------------------------------------------------------- /Example_Code_Figures/Membrane_Tripole/Membrane_Tripole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example_Code_Figures/Membrane_Tripole/Membrane_Tripole.png -------------------------------------------------------------------------------- /Example_Code_Figures/Recruitment_Model/Recruitment_Model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example_Code_Figures/Recruitment_Model/Recruitment_Model.png -------------------------------------------------------------------------------- /Example_Code_Figures/sEMG_Electrode_Array/sEMG_Electrode_Array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example_Code_Figures/sEMG_Electrode_Array/sEMG_Electrode_Array.png -------------------------------------------------------------------------------- /Example_Code_Figures/sEMG_Signal_Noise/sEMG_Signal_Colored_Noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Josef-Djarf/sEMG-Sim/HEAD/Example_Code_Figures/sEMG_Signal_Noise/sEMG_Signal_Colored_Noise.png -------------------------------------------------------------------------------- /Example_Code_Figures/Membrane_Tripole/run.py: -------------------------------------------------------------------------------- 1 | 2 | from MU import MotorUnit 3 | from sEMG import SurfaceEMG 4 | 5 | motor_unit = MotorUnit() 6 | 7 | motor_unit.plot_current_distribution_action_potential() 8 | 9 | -------------------------------------------------------------------------------- /Example_Code_Figures/sEMG_Signal_Noise/run.py: -------------------------------------------------------------------------------- 1 | from MU import MotorUnit 2 | from sEMG import SurfaceEMG 3 | 4 | 5 | surface_emg = SurfaceEMG() 6 | 7 | # Simulate the surface EMG signal. 8 | surface_emg.simulate_surface_emg() -------------------------------------------------------------------------------- /Example_Code_Figures/Recruitment_Model/run.py: -------------------------------------------------------------------------------- 1 | from MU import MotorUnit 2 | from sEMG import SurfaceEMG 3 | 4 | 5 | surface_emg = SurfaceEMG() 6 | 7 | # Plot the motor unit recruitment firing pattern. 8 | surface_emg.plot_recruitment_model() -------------------------------------------------------------------------------- /Example_Code_Figures/sEMG_Electrode_Array/run.py: -------------------------------------------------------------------------------- 1 | from MU import MotorUnit 2 | from sEMG import SurfaceEMG 3 | 4 | 5 | surface_emg = SurfaceEMG() 6 | 7 | # Plot the surface EMG signal for electrode's array. 8 | surface_emg.plot_suface_emg_array() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | requirements = [ 4 | "numpy", 5 | "matplotlib" 6 | ] 7 | 8 | setup( 9 | author="Josef Djärf", 10 | author_email="josef.djarfs@gmail.com", 11 | name="semg_sim", 12 | version="0.0.1", 13 | packages=["semg_sim"], 14 | license="MIT", 15 | long_description=open("README.md").read(), 16 | install_requires=requirements 17 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Josef Djärf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files sEMG-Sim, to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sEMG-Sim version: 0.1 2 | Surface Electromyography Simulator - Multiple parameter modelling of synthetic electromyography (EMG) data. 3 | 4 | # install 5 | 6 | To install, clone the repository and run: 7 | 8 | ``` 9 | pip install -e . 10 | ``` 11 | in the root folder. 12 | 13 | # General Info: 14 | This code is the result of our Master's thesis project. What follows below is some general information about the project and a discription of the project. 15 | Our objective was to implement an sEMG simulation model in Python. The model is based on mathematical models obtained 16 | from scientific literature. The model contains several adjustable parameters including motor unit properties (size, firing rate, etc), 17 | volume conductor, and setup for the recording electrodes. The work has been completed in association with the Neuroengineering group, Departement of Biomedical Engineering, Faculty of Engineering, Lund Universty, Sweden. 18 | 19 | # Authors: Ahmad Alosta & Josef Djärf 20 | 21 | # Abstract: 22 | Surface electromyography (sEMG) measures skeletal muscle function by recording muscle activity from the surface of the skin. The technique can be used to diagnose neuromuscular diseases, and as an aid in rehabilitation, biomedical research, and human-computer interaction. A simulation model for sEMG data can assess decomposition algorithms and help develop new diagnostic tools. Such simulation models have previously not been available. We have written open-source code in Python to generate synthetic sEMG data. The code is publicly accessible via GitHub, an online platform for software development. The implemented model has multiple parameters that influence the artificially generated signal. The model was implemented with a bottom-up design, starting at a single muscle fibre and ending with the sEMG signal generated from up to hundreds of active motor units. The simulated signal can be recorded in potentially dozens of selectively positioned surface electrodes. The model’s foundation is mathematical equations found throughout the scientific literature surrounding motor control and biological signalling, e.g., action potential propagation, membrane current distribution, and motor unit recruitment. We assert that the model incorporates the most significant features for generating sEMG data. The synthetically generated data was decomposed to study the simulated motor unit action potentials. The presented model can be used as ground truth to assess the performance of decomposition algorithms for sEMG. The analysis of sEMG signals can provide valuable insights into muscle activity, contributing to our understanding of motor control and aiding the development of prosthetics and assistive technologies. 23 | -------------------------------------------------------------------------------- /semg_sim/main.py: -------------------------------------------------------------------------------- 1 | # ######################### ######################### ########################## ####################### 2 | # Read Me # 3 | # ######################### ######################### ########################## ####################### 4 | 5 | """"User Manual for the Motor Unit (in MU-file), Surface Electromyography (in sEMG-file), and SaveData (in SD-file) Classes: 6 | These three clases make the program for synthetic surface electromyography data. 7 | 8 | Motor Unit class: 9 | 10 | The Motor Unit (MU) class represents a single motor unit. It has the following methods: 11 | Methods: 12 | get_tripole_amplitude(): Returns the amplitude of the tripole signal generated by the calculated current distribution. 13 | plot_current_distribution_action_potential(): Plots the current distribution and action potential, computed in get_tripole_amplitude(). 14 | get_tripole_distance(): Returns the distance between the tripole of the current distribution. 15 | simulate_fibre_action_potential(): Simulates the action potential of a single muscle fibre in the motor unit. 16 | plot_fibre_action_potential(): Plots the single fibre action potential for all electrodes, computed in simulate_fibre_action_potential(). 17 | simulate_motor_unit(): Simulates the action potential of the entire motor unit. 18 | plot_motor_unit(): Plots the action potential of the motor unit for all electrodes, computed in simulate_motor_unit(). 19 | 20 | The MU class also has a number of attributes that can be used to configure the simulation, such as: 21 | Attributes: 22 | For get_tripole_amplitude: 23 | A: Constant to fit the amplitude of the tripole (V). 24 | B: Resting membrane potential amplitude (V). 25 | C: Muscle fibre proportionality constant. 26 | plot_length: Length for plotting the action potential and current distribution expressed in mm (20 mm). 27 | scaling_factor: A scaling factor expressed in mm^-1, lambda (λ). 28 | 29 | For simulate_fibre_action_potential: 30 | fibre_length: Length of fibre (mm). 31 | neuromuscular_junction: Position of NMJ along the length of the fibre (mm). 32 | conduction_velocity: Conduction velocity along the muscle fibre (m/s). 33 | electrode_shift: Position of center of electrode array along the length of the fibre (mm). [electrode_shift = neuromuscular_junction means the array sits centered above NMJ.] 34 | number_of_electrodes_z: Number of elecrodes in the array, in the direction of the fibre. 35 | number_of_electrodes_x: Number of electrodes in the array across the fibre. 36 | inter_electrode_spacing: Distance between electrodes in the array (mm). 37 | radial_conductivity: Conductivity in radial direction, across the fibre (m/s). 38 | ratio_axial_radial_conductivity: Ratio between axial and radial conductivity, (fibre direction conductivity / radial conductivity) 39 | fibre_depth: Initial depth from the surface EMG down to the fibre (mm). 40 | 41 | For simulate_motor_unit: 42 | motor_unit_radius: Radius of the motor unit (mm). 43 | number_of_fibres: The number of fibres in a given motor unit. 44 | 45 | For plotting methods: 46 | y_limit_minimum: Minimum value of plot y-axis. 47 | y_limit_maximum: Maximum value of plot y-axis. 48 | ... 49 | 50 | Surface Electromyography Class: 51 | The Surface Electromyography (sEMG) class represents a surface EMG signal. It has the following methods: 52 | Methods: 53 | simulate_recruitment_model(): Simulates the motor unit recruitment with firing patterns on increasing the number of muscle fibres per motor unit. 54 | plot_recruitment_model(): Plots the firing patterns of the motor unit recruitment. 55 | simulate_surface_emg(): Simulates the surface EMG signal generated by the motor unit recruitment. 56 | plot_surface_emg_array(): Plots the surface EMG signal array generated by the summation over the motor unit recruitment. 57 | plot_one_electrod_surface_emg(): Plots the surface EMG signal for only one electrode generated by the summation over the motor unit recruitment. 58 | 59 | The sEMG class also has a number of attributes that can be used to configure the simulation, such as: 60 | Attributes: 61 | For simulate_recruitment_model: 62 | simulation_time: Total simulation time in seconds. 63 | sampling_rate: Sample rate in Hz. 64 | ramp: Up-stable-down in seconds. 65 | maximum_excitation_level: Percentage % of max exc. 66 | number_of_motor_units: Number of motoneurons in the pool. 67 | recruitment_range: Range of recruitment threshold values. 68 | excitatory_gain: Gain of the excitatory drive-firing rate relationship. 69 | minimum_firing_rate: Minimum firing rate (Hz). 70 | peak_firing_rate_first_unit: Peak firing rate of the first motoneuron (Hz). 71 | peak_firing_rate_difference: Desired difference in peak firing rates between the first and last units (Hz): 72 | inter_spike_interval_coefficient_variation: The inter spike interval variance coefficient. 73 | 74 | For simulate_surface_emg: 75 | twitch_force_range: The range of twitch forces RP (force units). 76 | motor_unit_density: The motor-unit fibre density (20 unit fibres/mm^2 area of muscle). 77 | smallest_motor_unit_number_of_fibres: The smallest motor unit innervated 28 fibres. 78 | largest_motor_unit_number_of_fibres: The largest motor unit innervated 2728 fibres. 79 | muscle_fibre_diameter: The muscle-fibre diameter (46 µm). 80 | muscle_cross_sectional_diameter: The muscle cross-sectional diameter (1.5 cm). 81 | electrodes_in_z: Number of elecrodes in the array, in the direction of the fibre. 82 | electrodes_in_x: Number of electrodes in the array across the fibre. 83 | 84 | For plotting methods: 85 | y_limit_minimum: Minimum value of plot y-axis. 86 | y_limit_maximum: Maximum value of plot y-axis. 87 | ... 88 | 89 | Save Data class: 90 | 91 | The SaveData (SD) class provides methods for saving and loading data from files. It has the following methods: 92 | Methods: 93 | save_output_data(): Saves the data of the any signal to a file. 94 | open_and_load_saved_data(): Opens the already saved data from a file .npy for usage in Python. 95 | save_single_fibre_action_potential(): Saves the action potential of a single muscle fibre in the motor unit to a file. 96 | plot_saved_motor_unit(): Plots the saved file for action potential of the motor unit with 10 electrodes in the z-direction and 5 electrodes in the x-direction using the default number of electrodes as in the saved simulated file. 97 | plot_saved_surface_emg_array():Plots the saved file for surface EMG array by changing the electrodes in the z-direction to 1 and the electrodes in the x-direction to 1 using the default number of electrodes as in the saved simulated file. 98 | plot_saved_one_electrod_surface_emg(): Plots the saved file for surface EMG singal for one electrode postioned in the electrode array. 99 | 100 | The SD class also has a number of attributes that can be used to configure the saving and loading of data, such as: 101 | Attributes: 102 | For save_output_data: 103 | filename: The filename to save the output data to. 104 | output_data: The output data to be saved. 105 | 106 | For open_and_load_saved_data: 107 | filename: The name of the file containing the saved .npy data. 108 | 109 | For plotting mothods: 110 | y_limit_minimum: Minimum value of plot y-axis. 111 | y_limit_minimum: Maximum value of plot y-axis. 112 | number_of_electrodes_z: Number of elecrodes in the array, in the direction of the fibre. 113 | number_of_electrodes_x: Number of electrodes in the array across the fibre. 114 | time_length: 115 | delta_time: 116 | simulation_time: Total simulation time in seconds. 117 | sampling_rate: Sample rate (10 kHz). 118 | ....................................................................................................................................... 119 | Example usage: 120 | 121 | from MU import MotorUnit 122 | from sEMG import SurfaceEMG 123 | from SD import SaveData 124 | 125 | ### Create a MotorUnit object. 126 | motor_unit = MotorUnit() 127 | 128 | # Changing values. 129 | #motor_unit.A = 10 130 | #motor_unit.B = -10 131 | #motor_unit.C = 2 132 | #motor_unit.plot_length = 3 133 | #motor_unit.scaling_factor = 3 134 | 135 | # Get the amplitude of the tripole current distribution. 136 | amplitudes = motor_unit.get_tripole_amplitude() 137 | 138 | # Changing values. 139 | #motor_unit.y_limit_minimum = -2 140 | #motor_unit.y_limit_maximum = 2 141 | 142 | # Plot the current distribution and the action potential. 143 | motor_unit.plot_current_distribution_action_potential() 144 | 145 | # Get the distance between the tripole current distribution. 146 | distances = motor_unit.get_tripole_distance() 147 | 148 | # Change values. 149 | #motor_unit.fibre_length = 1 150 | #motor_unit.conduction_velocity = 1 151 | #motor_unit.ratio_axial_radial_conductivity = 1 152 | #motor_unit.radial_conductivity = 1 153 | #motor_unit.inter_electrode_spacing = 1 154 | #motor_unit.number_of_electrodes_z = 10 155 | #motor_unit.electrode_shift = 1 156 | #motor_unit.initial_neuromuscular_junction = 1 157 | #motor_unit.fibre_depth = 1 158 | #motor_unit.extermination_zone_width = 1 159 | #motor_unit.innervation_zone_width = 1 160 | #motor_unit.time_length = 10 161 | 162 | # Simulate the single fibre's action potential. 163 | motor_unit.simulate_fibre_action_potential() 164 | 165 | # Plot the the single fibre's action potential. 166 | motor_unit.plot_fibre_action_potential 167 | 168 | # Change values. 169 | #motor_unit.motor_unit_radius = 1 170 | #motor_unit.number_of_fibres = 1 171 | 172 | # Simulate the motor unit's action potential. 173 | motor_unit.simulate_motor_unit() 174 | 175 | # Plot the motor unit's action potential. 176 | motor_unit.plot_motor_unit() 177 | 178 | ### Create a SurfaceEMG object. 179 | surface_emg = SurfaceEMG() 180 | 181 | # Simulate the motor unit recruitment firing pattern. 182 | surface_emg.simulate_recruitment_model() 183 | 184 | # Plot the motor unit recruitment firing pattern. 185 | surface_emg.plot_recruitment_model() 186 | 187 | # Simulate the surface EMG signal. 188 | surface_emg.simulate_surface_emg() 189 | 190 | # Plot the surface EMG signal only for first electrode in the array. 191 | surface_emg.plot_first_electrode_suface_EMG() 192 | 193 | # Plot the surface EMG signal for electrode's array. 194 | surface_emg.plot_suface_emg_array() 195 | 196 | ### Create a SaveData object 197 | save_data = SaveData() 198 | 199 | # Save the saved motor unit data to a file. 200 | motot_unit_data = MotorUnit().simulate_motor_unit() 201 | save_data.save_output_data(motot_unit_data, 'simulate_motot_unit') 202 | 203 | # Load the saved motor unit data from a file. 204 | motor_unit_data = save_data.open_and_load_saved_data('simulate_motor_unit.npy') 205 | 206 | # Plot the saved motor unit data. 207 | save_data.plot_saved_motor_unit(motor_unit_data) 208 | 209 | # Save the surface EMG signal to a file. 210 | surface_emg_data = SurfaceEMG().simulate_surface_emg() 211 | save_data.save_output_data(surface_emg_data, 'simulate_surface_emg') 212 | 213 | # Load the saved surface EMG signal data to a file. 214 | semg_data = save_data.open_and_load_saved_data('simulate_surface_emg.npy') 215 | 216 | # Plot the saved surface EMG signal data. 217 | save_data.plot_saved_surface_emg_array(semg_data) 218 | """ 219 | # ######################### ######################### ########################## ####################### 220 | # THE END # 221 | # ######################### ######################### ########################## ####################### 222 | -------------------------------------------------------------------------------- /semg_sim/SD.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.ticker import NullFormatter 4 | import scipy.io 5 | plt.rcParams['font.family'] = 'Times New Roman' 6 | 7 | class SaveData: 8 | """A class that saves and plots data. 9 | 10 | Methods: 11 | save_output_data(): Saves the data of the any signal to a file. 12 | open_and_load_saved_data(): Opens the already saved data from a file .npy for usage in Python. 13 | save_single_fibre_action_potential(): Saves the action potential of a single muscle fibre in the motor unit to a file. 14 | plot_saved_motor_unit(): Plots the saved file for action potential of the motor unit with 10 electrodes in the z-direction and 5 electrodes in the x-direction using the default number of electrodes as in the saved simulated file. 15 | plot_saved_surface_emg_array():Plots the saved file for surface EMG array by changing the electrodes in the z-direction to 1 and the electrodes in the x-direction to 1 using the default number of electrodes as in the saved simulated file. 16 | 17 | Attributes: 18 | For save_output_data: 19 | filename: The filename to save the output data to. 20 | output_data: The output data to be saved. 21 | 22 | For open_and_load_saved_data: 23 | filename: The name of the file containing the saved .npy data. 24 | 25 | For plotting mothods: 26 | y_limit_minimum: Minimum value of plot y-axis. 27 | y_limit_minimum: Maximum value of plot y-axis. 28 | number_of_electrodes_z: Number of elecrodes in the array, in the direction of the fibre. 29 | number_of_electrodes_x: Number of electrodes in the array across the fibre. 30 | time_length: 31 | delta_time: 32 | simulation_time: Total simulation time in seconds. 33 | sampling_rate: Sample rate (10 kHz). 34 | 35 | """ 36 | ... 37 | 38 | def __init__(self): 39 | """Initializes a new Motor Unit object. 40 | 41 | """ 42 | ... 43 | 44 | self.y_limit_minimum:float = -1 45 | self.y_limit_maximum:float = 1 46 | self.number_of_electrodes_z:int = 1 47 | self.number_of_electrodes_x:int = 1 48 | 49 | self.time_length:int = 35 50 | self.delta_time:float = 0.1 # (10 kHz) 51 | self.simulation_time:int = 30 52 | self.sampling_rate:int = 10000 53 | 54 | ######################### 1 ######################### Save Output Data ########################## ####################### 55 | def save_output_data(self, output_data, filename): 56 | """Saves the output data to a file. 57 | 58 | Args: 59 | output_data: The output data to be saved. 60 | filename: The filename to save the output data to. 61 | """ 62 | ... 63 | 64 | np.save(filename, output_data) # , allow_pickle = True 65 | 66 | # Example usage: 67 | 68 | #output_data = SaveData().save_output_data(output_data, 'filename') 69 | 70 | # Use the code to save data in your folder here. 71 | 72 | ######################### 2 ######################### Open and Load Saved Data ########################## ####################### 73 | def open_and_load_saved_data(self, filename): 74 | """Opens and loads a saved .npy data from a file. 75 | 76 | Args: 77 | filename: The name of the file containing the saved .npy data. 78 | 79 | Returns: 80 | A Python object containing the loaded data. 81 | """ 82 | ... 83 | 84 | data = np.load(filename) # , allow_pickle = True 85 | 86 | return data 87 | 88 | # Example usage: 89 | 90 | #data = SaveData().open_and_load_saved_data('saved_data.npy') 91 | 92 | # Use the loaded data in your code here. 93 | 94 | ######################### 3 ######################### Plot Saved Motor Unit ########################## ####################### 95 | def plot_saved_motor_unit(self, motor_unit_array): 96 | """Plots the motor unit action potential from the summed number of single fibres computed in simulate_motor_unit(). 97 | 98 | Args: 99 | 100 | 101 | Returns: 102 | 103 | """ 104 | ... 105 | ### Default arguments: 106 | time_length = self.time_length 107 | delta_time = self.delta_time 108 | y_limit_minimum = self.y_limit_minimum 109 | y_limit_maximum = self.y_limit_maximum 110 | number_of_electrodes_z = self.number_of_electrodes_z 111 | number_of_electrodes_x = self.number_of_electrodes_x 112 | 113 | time_array = np.arange(0, time_length + delta_time, delta_time) 114 | 115 | ### Plot the normalized motor unit action potential 116 | normalized_motor_unit = motor_unit_array 117 | normalized_motor_unit = (normalized_motor_unit - normalized_motor_unit.mean()) / (normalized_motor_unit.max() - normalized_motor_unit.min()) 118 | 119 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 120 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 121 | zeros_array = np.zeros(len(array_size)) 122 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 123 | array_size_x = np.append(array_size_x,zeros_array) 124 | 125 | fig1 = plt.figure(1) 126 | for i in range(len(array_size)): 127 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 128 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 129 | ax.grid(which = 'both', ls = 'dashed') 130 | plt.plot(time_array, normalized_motor_unit[i, :]) 131 | plt.xlim(time_array[0], time_array[-1] - 1) 132 | if i < len(array_size) - number_of_electrodes_x: 133 | ax.xaxis.set_major_formatter(NullFormatter()) 134 | plt.ylim(y_limit_minimum,y_limit_maximum) 135 | ax.yaxis.set_major_formatter(NullFormatter()) 136 | if i == 0: 137 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 138 | for j in range(i): 139 | if i == array_size_x[j]: 140 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 141 | elif number_of_electrodes_x == 1: 142 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 143 | plt.suptitle('Motor Unit Action Potential', fontsize = 20, fontweight = 'bold') 144 | if number_of_electrodes_x > 1: 145 | fig1.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre') 146 | else: 147 | fig1.supxlabel('Time (ms)') 148 | if number_of_electrodes_z > 1: 149 | fig1.supylabel('Motor Unit Action Potential\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center') 150 | else: 151 | fig1.supylabel('Motor Unit Action Potential', ha = 'center', va = 'center') 152 | 153 | return plt.show() 154 | 155 | # Example usage: 156 | 157 | #plot_mu = SaveData().plot_saved_motor_unit(data) 158 | 159 | # Use the code to save data in your folder here. 160 | 161 | ######################### 4 ######################### Plot Saved Surface Electromyography Array wtithout Noise ########################## ####################### 162 | def plot_saved_surface_emg_array_no_noise(self, surface_emg_array): 163 | """Generates the recruitment and rate coding organization of motor units. 164 | 165 | Arguments: 166 | firing_times_motor_unit 167 | time_array 168 | 169 | Returns: 170 | A plot of the recruitment model for each motor unit. 171 | """ 172 | ... 173 | 174 | ### Default arguments: 175 | simulation_time = self.simulation_time 176 | sampling_rate = self.sampling_rate 177 | y_limit_minimum = self.y_limit_minimum 178 | y_limit_maximum = self.y_limit_maximum 179 | number_of_electrodes_z = self.number_of_electrodes_z 180 | number_of_electrodes_x = self.number_of_electrodes_x 181 | 182 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 183 | 184 | electrode_sum = np.zeros((number_of_electrodes_z * number_of_electrodes_x, len(time_array))) 185 | 186 | for ne in range(len(electrode_sum)): 187 | motor_unit_sum = np.zeros(len(time_array)) 188 | for m, simulation in enumerate(surface_emg_array): 189 | motor_unit_sum += simulation[ne,:] 190 | electrode_sum[ne,:] = motor_unit_sum 191 | 192 | ### Plot the normalized motor unit action potential 193 | normalized_simulation = electrode_sum 194 | #normalized_simulation = (normalized_simulation - normalized_simulation.mean()) / (normalized_simulation.max() - normalized_simulation.min()) 195 | normalized_simulation = y_limit_minimum + ((normalized_simulation - normalized_simulation.min())*(y_limit_maximum-y_limit_minimum)) / (normalized_simulation.max() - normalized_simulation.min()) 196 | 197 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 198 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 199 | zeros_array = np.zeros(len(array_size)) 200 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 201 | array_size_x = np.append(array_size_x, zeros_array) 202 | 203 | ### Plot the simulations for each motor unit as an array 204 | fig2 = plt.figure(2) 205 | for i in range(len(array_size)): 206 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 207 | plt.subplots_adjust(wspace = 0.0, hspace = 0.0) 208 | ax.grid(which = 'both', ls = 'dashed') 209 | plt.plot(time_array, normalized_simulation[i, :]) 210 | plt.xlim(time_array[0], time_array[-1] - 1) 211 | if i < len(array_size) - number_of_electrodes_x: 212 | ax.xaxis.set_major_formatter(NullFormatter()) 213 | ax.yaxis.set_major_formatter(NullFormatter()) 214 | if i == 0: 215 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 216 | for j in range(i): 217 | if i == array_size_x[j]: 218 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 219 | elif number_of_electrodes_x == 1: 220 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 221 | plt.ylim(y_limit_minimum,y_limit_maximum) 222 | plt.suptitle('The Surface Electromyography Signal', fontsize = 20) 223 | 224 | if number_of_electrodes_x > 1: 225 | fig2.supxlabel('Time (s)\n Electrodes in the x direction, i.e. vertically across the fibre') 226 | else: 227 | fig2.supxlabel('Time (s)') 228 | if number_of_electrodes_z > 1: 229 | fig2.supylabel('Normalized sEMG Signal\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center') 230 | else: 231 | fig2.supylabel('Normalized sEMG Signal', ha = 'center', va = 'center') 232 | 233 | return plt.show() 234 | 235 | # Example usage: 236 | 237 | #s_emg = SaveData() 238 | #data = s_emg.open_and_load_saved_data('saved_data.npy') 239 | #semg.number_of_electrodes_z = 1 or 10 or 13 240 | #semg.number_of_electrodes_x = 1 or 5 241 | #plot_semg = s_emg.plot_saved_suface_emg_array(data) 242 | 243 | ######################### 5 ######################### Plot Saved Surface Electromyography Array with Noise ########################## ####################### 244 | def plot_saved_surface_emg_array(self, surface_emg_array): 245 | """Generates the recruitment and rate coding organization of motor units. 246 | 247 | Arguments: 248 | firing_times_motor_unit 249 | time_array 250 | 251 | Returns: 252 | A plot of the recruitment model for each motor unit. 253 | """ 254 | ... 255 | 256 | ### Default arguments: 257 | simulation_time = self.simulation_time 258 | sampling_rate = self.sampling_rate 259 | y_limit_minimum = self.y_limit_minimum 260 | y_limit_maximum = self.y_limit_maximum 261 | number_of_electrodes_z = self.number_of_electrodes_z 262 | number_of_electrodes_x = self.number_of_electrodes_x 263 | 264 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 265 | 266 | ### Plot the normalized motor unit action potential 267 | normalized_simulation = surface_emg_array 268 | #normalized_simulation = (normalized_simulation - normalized_simulation.mean()) / (normalized_simulation.max() - normalized_simulation.min()) 269 | normalized_simulation = y_limit_minimum + ((normalized_simulation - normalized_simulation.min())*(y_limit_maximum-y_limit_minimum)) / (normalized_simulation.max() - normalized_simulation.min()) 270 | 271 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 272 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 273 | zeros_array = np.zeros(len(array_size)) 274 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 275 | array_size_x = np.append(array_size_x, zeros_array) 276 | 277 | ### Plot the simulations for each motor unit as an array 278 | fig3 = plt.figure(3) 279 | for i in range(len(array_size)): 280 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 281 | plt.subplots_adjust(wspace = 0.0, hspace = 0.0) 282 | ax.grid(which = 'both', ls = 'dashed') 283 | plt.plot(time_array, normalized_simulation[i, :]) 284 | plt.xlim(time_array[0], time_array[-1] - 1) 285 | if i < len(array_size) - number_of_electrodes_x: 286 | ax.xaxis.set_major_formatter(NullFormatter()) 287 | ax.yaxis.set_major_formatter(NullFormatter()) 288 | if i == 0: 289 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 290 | for j in range(i): 291 | if i == array_size_x[j]: 292 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 293 | elif number_of_electrodes_x == 1: 294 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 295 | plt.ylim(y_limit_minimum,y_limit_maximum) 296 | plt.suptitle('The Surface Electromyography Signal', fontsize = 20, fontweight = 'bold') 297 | 298 | if number_of_electrodes_x > 1: 299 | fig3.supxlabel('Time (s)\n Electrodes in the x direction, i.e. vertically across the fibre') 300 | else: 301 | fig3.supxlabel('Time (s)') 302 | if number_of_electrodes_z > 1: 303 | fig3.supylabel('Normalized sEMG Signal\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center') 304 | else: 305 | fig3.supylabel('Normalized sEMG Signal', ha = 'center', va = 'center') 306 | 307 | return plt.show() 308 | 309 | # Example usage: 310 | 311 | #s_emg = SaveData() 312 | #data = s_emg.open_and_load_saved_data('saved_data.npy') 313 | #semg.number_of_electrodes_z = 1 or 10 or 13 314 | #semg.number_of_electrodes_x = 1 or 5 315 | #plot_semg = s_emg.plot_saved_suface_emg_array(data) 316 | 317 | ######################### 6 ######################### Plot Saved Surface Electromyography Array without Noise ########################## ####################### 318 | def plot_saved_one_electrode_surface_emg_no_noise(self, surface_emg, number_of_electrodes_z, number_of_electrodes_x): 319 | ### Default arguments: 320 | simulation_time = self.simulation_time 321 | sampling_rate = self.sampling_rate 322 | 323 | electrod_postion = number_of_electrodes_z * number_of_electrodes_x 324 | 325 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 326 | 327 | fig4 = plt.figure(4) 328 | ### Plot the simulations for each motor unit after sum 329 | electrod_one_sum = np.zeros(len(time_array)) 330 | 331 | for m, simulation in enumerate(surface_emg): 332 | electrod_one_sum += simulation[electrod_postion-1,:] 333 | 334 | plt.plot(time_array, electrod_one_sum*10**6) # Plot the electrode row postion with m numbers of motor units. 335 | 336 | plt.xlabel('Time (s)') 337 | plt.ylabel('Amplitdue (µV)') 338 | fig4.suptitle('sEMG Signal for One Electrode', fontweight = 'bold') 339 | 340 | return plt.show() 341 | 342 | ######################### 7 ######################### Plot Saved Surface Electromyography Array with Noise ########################## ####################### 343 | def plot_saved_one_electrode_surface_emg(self, surface_emg, number_of_electrodes_z, number_of_electrodes_x): 344 | ### Default arguments: 345 | simulation_time = self.simulation_time 346 | sampling_rate = self.sampling_rate 347 | 348 | electrod_postion = number_of_electrodes_z * number_of_electrodes_x 349 | 350 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 351 | 352 | fig5 = plt.figure(5) 353 | ### Plot the simulations for each motor unit after sum 354 | plt.plot(time_array, surface_emg[electrod_postion-1,:]*10**6) # Plot the electrode row postion with m numbers of motor units. 355 | 356 | plt.xlabel('Time (s)') 357 | plt.ylabel('Amplitude (µV)') 358 | fig5.suptitle('sEMG Signal for One Electrode', fontweight = 'bold') 359 | 360 | return plt.show() 361 | 362 | ######################### 8 ######################### Save Output Data ########################## ####################### 363 | def save_output_data_csv(self, output_data, filename): 364 | """Saves the output data to a file in csv format. 365 | 366 | Args: 367 | output_data: The output data to be saved in csv. 368 | filename: The filename to save the output data to. 369 | """ 370 | ... 371 | 372 | np.savetxt(filename + '.csv', output_data, delimiter=',') 373 | 374 | # Example usage: 375 | 376 | #output_data = SaveData().save_output_data(output_data, 'filename') 377 | 378 | # Use the code to save data in your folder here. 379 | 380 | ######################### 9 ######################### Save Output Data ########################## ####################### 381 | def save_output_data_mat(self, output_data, filename): 382 | """Saves the output data to a file in .mat format. 383 | 384 | Args: 385 | output_data: The output data to be saved in mat format. 386 | filename: The filename to save the output data to. 387 | """ 388 | ... 389 | 390 | sampling_rate = self.sampling_rate 391 | number_of_electrodes_z = self.number_of_electrodes_z 392 | number_of_electrodes_x = self.number_of_electrodes_x 393 | 394 | string = str(sampling_rate) 395 | scipy.io.savemat(filename + '_' + number_of_electrodes_z + 'X' + number_of_electrodes_x + '_sf_' + string + '.mat', {'output': output_data}) 396 | 397 | # Example usage: 398 | 399 | #output_data = SaveData().save_output_data(output_data, 'filename') 400 | 401 | # Use the code to save data in your folder here. 402 | 403 | # ######################### ######################### ########################## ####################### 404 | # THE END # 405 | # ######################### ######################### ########################## ####################### 406 | -------------------------------------------------------------------------------- /semg_sim/MU.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.ticker import NullFormatter 4 | import scipy.io 5 | plt.rcParams['font.family'] = 'Times New Roman' 6 | 7 | class MotorUnit: 8 | """A class that represents a motor unit (MU). 9 | 10 | Methods: 11 | get_tripole_amplitude: Calculates the action potential and the current distribution. Returns the tripole amplitudes of the current distribution. 12 | plot_current_distribution_action_potential: Plots the normalized action potential and current distribution based on the given parameter values. 13 | get_tripole_distance: Calculates the distance between the poles of the current distribution based on returned value from get_tripole_amplitude. 14 | simulate_fibre_action_potential: Simulates the action potential recorded by a given number of electrodes, positioned along one fibre, for the given current distriubtion. 15 | plot_fibre_action_potential: Plots the normalized action potential for one fibre over the electrodes positioned along the fibre. 16 | simulate_motor_unit: Simulates the action potential for a given number of fibres, for electrodes positioned along the motor unit. 17 | plot_motor_unit: Plots the normalized action potential simulated for one motor unit, for a given number of fibres, and electrodes positioned along the motor unit. 18 | 19 | Attributes: 20 | For get_tripole_amplitude: 21 | A: Constant to fit the amplitude of the tripole (V). 22 | B: Resting membrane potential amplitude (V). 23 | C: Muscle fibre proportionality constant. 24 | plot_length: Length for plotting the action potential and current distribution expressed in mm (20 mm). 25 | scaling_factor: A scaling factor expressed in mm^-1, lambda (λ). 26 | 27 | For simulate_fibre_action_potential: 28 | fibre_length: Length of fibre (mm). 29 | neuromuscular_junction: Position of NMJ along the length of the fibre (mm). 30 | conduction_velocity: Conduction velocity along the muscle fibre (m/s). 31 | electrode_shift: Position of center of electrode array along the length of the fibre (mm). [electrode_shift = neuromuscular_junction means the array sits centered above NMJ.] 32 | number_of_electrodes_z: Number of elecrodes in the array, in the direction of the fibre. 33 | number_of_electrodes_x: Number of electrodes in the array across the fibre. 34 | inter_electrode_spacing: Distance between electrodes in the array (mm). 35 | radial_conductivity: Conductivity in radial direction, across the fibre (m/s). 36 | ratio_axial_radial_conductivity: Ratio between axial and radial conductivity, (fibre direction conductivity / radial conductivity) 37 | fibre_depth: Initial depth from the surface EMG down to the fibre (mm). 38 | 39 | For simulate_motor_unit: 40 | motor_unit_radius: Radius of the motor unit (mm). 41 | number_of_fibres: The number of fibres in a given motor unit. 42 | 43 | For plotting methods: 44 | y_limit_minimum: Minimum value of plot y-axis. 45 | y_limit_maximum: Maximum value of plot y-axis. 46 | """ 47 | ... 48 | 49 | def __init__(self): 50 | """Initializes a new Motor Unit object. 51 | 52 | """ 53 | ... 54 | 55 | ### Default values for generating a tripole: 56 | self.A:float = 96e-3 57 | self.B:float = -90e-3 58 | self.C:int = 1 59 | self.plot_length:int = 20 60 | self.scaling_factor:float = 1 61 | 62 | ### Default values for a single fibre: 63 | self.fibre_length:int = 210 64 | self.conduction_velocity:int = 4 65 | self.ratio_axial_radial_conductivity:int = 6 # 0.001 66 | self.radial_conductivity:float = 0.303 67 | self.inter_electrode_spacing:float = 10 # 20 mm 68 | self.number_of_electrodes_z:int = 1 69 | self.number_of_electrodes_x:int = 1 70 | self.electrode_shift:int = 165 # 75 z-axis 71 | self.initial_neuromuscular_junction:int = 90 72 | self.fibre_depth:float = 10 73 | self.x_position_single_fibre:float = 10 74 | self.extermination_zone_width:int = 10 75 | self.innervation_zone_width:int = 5 76 | self.time_length:int = 35 77 | 78 | ### Default values for a motor unit: 79 | self.motor_unit_radius:float = 1 80 | self.number_of_fibres:int = 100 81 | self.motor_unit_depth:float = 10 82 | self.motor_unit_x_position:float = 0 83 | 84 | ### Default values for plots: 85 | self.y_limit_minimum:float = -1 86 | self.y_limit_maximum:float = 1 87 | 88 | ######################### 1 ######################### Get Tripole Amplitude ########################## ####################### 89 | def get_tripole_amplitude(self): 90 | """Generates the tripole amplitude from the membrane current distribution source. 91 | 92 | Args: 93 | According to Modeling of Surface Myoelectric Signals, part I. Merletti, 1999. 94 | A: A suitable constant to fit the amplitude of the action potential expressed in V (96 mV = 96e-3). 95 | B: The resting membrane poteintial expressed in V (-90 mV = -90e-3). 96 | C: Muscle fibre (proportionality) constant (1). 97 | plot_length: Length of plotting the action potential and current distribution expressed in mm (20 mm). 98 | scaling_factor: A scale factor expressed in mm^-1, which is lambda (λ), (1 mm^-1). 99 | 100 | Returns: 101 | A numpy array containing the tripole amplitude of the current distribution. 102 | """ 103 | ... 104 | 105 | ### Default arguments: 106 | A = self.A 107 | B = self.B 108 | C = self.C 109 | plot_length = self.plot_length 110 | scaling_factor = self.scaling_factor 111 | 112 | ######################### Action Potential, Current Distribution ####################### 113 | ### Create the z-axis vector, which is the sampled vector of plotting the action potential and current distribution: 114 | delta_length = 0.1 # (10 kHz) total distance between samples 115 | z = np.arange(0, plot_length + delta_length, delta_length) 116 | self.z = z 117 | 118 | ### A mathematical description of the action potential 119 | action_potential = A * (scaling_factor * z)**3 * np.exp(-scaling_factor * z) - B # eq.1.1 Merletti part 1 120 | self.action_potential = action_potential 121 | 122 | ### Calculate the mebrane current distribution which is proportional to the second derivative of the action potential 123 | current_distribution = C * A * scaling_factor**2 * (scaling_factor * z) * (6 - 6 * scaling_factor * z + scaling_factor**2 * z**2) * np.exp(-scaling_factor * z) # eq. 1.2 Merletti part 1 124 | self.current_distribution = current_distribution 125 | 126 | ######################### Pole Amplitude ####################### 127 | ### Calculate the pole amplitude 128 | ## To calculate pole_one, pole_two, and pole_three 129 | # Discretize the current distribution to 1 and -1 130 | current_distribution_discrete = np.where(current_distribution > 0, 1, -1) 131 | 132 | # Calculate the absolute difference between each sample of discretized current distribution 133 | current_distribution_difference = np.abs(np.diff(current_distribution_discrete)) 134 | 135 | # Locate the absolute differences that are greater than zero 136 | pole_location_index = np.where(current_distribution_difference > 0)[0] 137 | self.pole_location_index = pole_location_index 138 | 139 | # Calculate the poles 140 | dz = z[1] - z[0] 141 | self.dz = dz 142 | 143 | ### Sum pole magnitude for each part of the tripole. 144 | pole_one = np.sum(current_distribution[:pole_location_index[1]]) * dz 145 | pole_two = np.sum(current_distribution[pole_location_index[1] + 1:pole_location_index[2]]) * dz 146 | pole_three = np.sum(current_distribution[pole_location_index[2] + 1:]) * dz 147 | 148 | ### Use rounding adjustment to set sum of all poles equal to zero. 149 | pole_rounding_adjustment = np.abs(pole_one + pole_two + pole_three) 150 | pole_one = pole_one + pole_rounding_adjustment 151 | pole_two = pole_two - pole_rounding_adjustment 152 | pole_three = pole_three + pole_rounding_adjustment 153 | 154 | ### Poles amplitude array. 155 | poles_amplitude = np.array([pole_one, pole_two, pole_three]) 156 | #print('Poles Amplitude =', poles_amplitude) 157 | 158 | return poles_amplitude 159 | 160 | ######################### 2 ######################### Get Pole Distance ########################## ####################### 161 | def get_tripole_distance(self): 162 | """Calculates the distance between the poles of the current distribution. 163 | 164 | Args: 165 | pole_location_index: A list of integers containing the indices of the poles in the current distribution. 166 | dz: The sampling interval of the current distribution. 167 | current_distribution 168 | 169 | Returns: 170 | A list of two floats containing the distances between the poles. 171 | """ 172 | ... 173 | 174 | ### Default arguments 175 | self.get_tripole_amplitude() 176 | pole_location_index = self.pole_location_index 177 | dz = self.dz 178 | current_distribution = self.current_distribution 179 | 180 | ### Calculate the distance between the poles 181 | """a, b represent tripole asymmetry 182 | a is the distance between pole 1 and pole 2. 183 | b is the distance between pole 1 and pole 3. 184 | The following rules of the current sources must hold: 185 | pole_one + pole_two + pole_three = 0 186 | pole_two*a + pole_three*b = 0 187 | """ 188 | 189 | ## Calculate the cumulative sum of each phase. 190 | pole_one_sum = np.cumsum(current_distribution[:pole_location_index[1]]) 191 | pole_two_sum = np.cumsum(current_distribution[pole_location_index[1]:pole_location_index[2]]) 192 | pole_three_sum = np.cumsum(current_distribution[pole_location_index[2]:]) 193 | 194 | ## Locate the location index (z-coordinate) at half the cumulative sum of each phase. 195 | pole_one_location = np.where(pole_one_sum > 0.5 * np.sum(current_distribution[:pole_location_index[1]]))[0] 196 | pole_two_location = pole_location_index[1] + np.where(pole_two_sum < 0.5 * np.sum(current_distribution[pole_location_index[1]:pole_location_index[2]]))[0] 197 | pole_three_location = pole_location_index[2] + np.where(pole_three_sum > 0.5 * np.sum(current_distribution[pole_location_index[2]:]))[0] 198 | 199 | ## Calculate the pole positions 200 | pole_one_position = pole_one_location[0] * dz 201 | pole_two_position = pole_two_location[0] * dz 202 | pole_three_position = pole_three_location[0] * dz 203 | 204 | ## Calculate the distance between the poles 205 | a = pole_two_position - pole_one_position 206 | b = pole_three_position - pole_one_position 207 | 208 | pole_distances = np.array([a, b]) 209 | #print('Pole Distances =', pole_distances) 210 | 211 | return pole_distances 212 | 213 | ######################### 3 ######################### Plot Current Distribution & Action Potential ########################## ####################### 214 | def plot_current_distribution_action_potential(self): 215 | """Plots the membrane current distribution and action potential. 216 | 217 | Args: 218 | current_distribution 219 | action_potential 220 | z: Sampled vector of plotting 221 | 222 | Returns: 223 | A plot of the current distribution and action potential. 224 | """ 225 | ... 226 | 227 | ### default arguments 228 | poles_amplitude = self.get_tripole_amplitude() 229 | pole_distances = self.get_tripole_distance() 230 | current_distribution = self.current_distribution 231 | action_potential = self.action_potential 232 | z = self.z 233 | pole_location_index = self.pole_location_index 234 | 235 | ######################### Normalization and Plot ####################### 236 | ### Plot the normalized current distribution and action potential 237 | # Normalize the signals using min-max feature scaling between points y_limit_minimum and y_limit_maximum. 238 | y_limit_minimum = self.y_limit_minimum 239 | y_limit_maximum = self.y_limit_maximum 240 | 241 | #normalized_current_distribution = y_limit_minimum + ((current_distribution - current_distribution.min())*(y_limit_maximum-y_limit_minimum)) / (current_distribution.max() - current_distribution.min()) 242 | #normalized_action_potential = y_limit_minimum + ((action_potential - action_potential.min())*(y_limit_maximum-y_limit_minimum)) / (action_potential.max() - action_potential.min()) 243 | 244 | fig1 = plt.figure(1) 245 | 246 | ax = plt.subplot(2,1,1) 247 | plt.plot(z, current_distribution*1000) 248 | plt.ylabel('Im, Current Distribution (mA)') 249 | #plt.axvline(z[np.argmax(current_distribution)], color = 'r', linestyle = '--', label = 'P1') 250 | #plt.axvline(z[np.argmin(current_distribution)], color = 'g', linestyle = '--', label = 'P2') 251 | #plt.legend() 252 | y_p1 = poles_amplitude[0]*1000 253 | y_p2 = poles_amplitude[1]*1000 254 | y_p3 = poles_amplitude[2]*1000 255 | 256 | p1_p2 = pole_distances[0] 257 | p1_P3 = pole_distances[1] 258 | 259 | x_p1 = z[pole_location_index[0]] 260 | x_p2 = z[pole_location_index[1]] 261 | x_p3 = z[pole_location_index[2]] 262 | 263 | p1_location = (x_p2 - x_p1) / 2 264 | p2_location = p1_location + p1_p2 265 | p3_location = p1_location + p1_P3 266 | 267 | plt.plot(p1_location, y_p1, 'o') 268 | plt.text(p1_location + 0.2, y_p1, 'P1') 269 | plt.vlines(x = p1_location, ymin = 0, ymax = y_p1, color = 'k') 270 | 271 | plt.plot(p2_location, y_p2, 'o') 272 | plt.text(p2_location + 0.2, y_p2, 'P2') 273 | plt.vlines(x = p2_location, ymin = 0, ymax = y_p2, color = 'k') 274 | 275 | plt.plot(p3_location, y_p3, 'o') 276 | plt.text(p3_location + 0.2, y_p3, 'P3') 277 | plt.vlines(x = p3_location, ymin = 0, ymax = y_p3, color = 'k') 278 | 279 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 280 | ax.xaxis.set_major_formatter(NullFormatter()) 281 | plt.title('Membrane Current Distribution', fontweight = 'bold') 282 | 283 | ax = plt.subplot(2,1,2) 284 | plt.plot(z, action_potential*1000-180) 285 | plt.ylabel('Vm, Action Potential (mV)') 286 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 287 | plt.title('Membrane Action Potential', fontweight = 'bold') 288 | 289 | fig1.supxlabel('z, Distance (mm)') 290 | 291 | A = self.A 292 | 293 | # Set the desired resolution 294 | plt.savefig('current_and_action_potential_{}mA_amplitude.png'.format(A*1000), dpi = 600) 295 | scipy.io.savemat('current' + '.mat', {'Im': current_distribution}) 296 | scipy.io.savemat('action_potential' + '.mat', {'Vm': action_potential}) 297 | 298 | return plt.show() 299 | 300 | ######################### 4 ######################### Simulate Fibre Action Potential ########################## ####################### 301 | def simulate_fibre_action_potential(self): 302 | """Simulates the action potentials recorded by all electrodes for one fibre. 303 | 304 | Args: 305 | fibre_length 306 | neuromuscular_junction 307 | conduction_velocity 308 | electrode_shift 309 | number_of_electrodes_z 310 | inter_electrode_spacing 311 | radial_conductivity 312 | ratio_axial_radial_conductivity 313 | fibre_depth 314 | 315 | Returns: 316 | A numpy array containing the simulated action potentials for electrodes positioned along the fibre. 317 | """ 318 | ... 319 | 320 | ### Choose default values for attributes 321 | fibre_length = self.fibre_length 322 | neuromuscular_junction = self.initial_neuromuscular_junction 323 | conduction_velocity = self.conduction_velocity 324 | electrode_shift = self.electrode_shift 325 | number_of_electrodes_z = self.number_of_electrodes_z 326 | number_of_electrodes_x = self.number_of_electrodes_x 327 | inter_electrode_spacing = self.inter_electrode_spacing 328 | radial_conductivity = self.radial_conductivity 329 | ratio_axial_radial_conductivity = self.ratio_axial_radial_conductivity 330 | extermination_zone_width = self.extermination_zone_width 331 | innervation_zone_width = self.innervation_zone_width 332 | fibre_depth = self.fibre_depth 333 | time_length = self.time_length 334 | x_position_single_fibre = self.x_position_single_fibre 335 | 336 | ### Simulation of poles moving along the fibre in time. Create time array. 337 | delta_time = 0.1 # (10 kHz) 338 | time_array = np.arange(0, time_length + delta_time, delta_time) 339 | self.time_array = time_array 340 | 341 | ### Create the current tripole with initial pole amplitude and positions 342 | tripole_amplitude = self.get_tripole_amplitude() 343 | tripole_distance = self.get_tripole_distance() 344 | 345 | a = tripole_distance[0] 346 | b = tripole_distance[1] 347 | pole_one = tripole_amplitude[0] 348 | pole_two = tripole_amplitude[1] 349 | pole_three = tripole_amplitude[2] 350 | 351 | # Array of mirrored tripole amplitudes 352 | P = np.array([pole_one, pole_two, pole_three, pole_three, pole_two, pole_one]).reshape(-1, 1) 353 | Pi = np.tile(P,(1,len(time_array))) 354 | 355 | ### Create uniformly distributed tendon ends at the extermination zones of each fibre and uniformly distributed neuromuscular junctions at the innervation zones. 356 | delta_length = 0.1 # (10 kHz) 357 | fibre_length_array = np.arange(0, fibre_length + delta_length, delta_length) 358 | 359 | ### Create random variation in the rightmost tendon ends. 360 | right_tendon_end_variation = fibre_length_array[-1] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 361 | 362 | ### Create variation in the leftmost tendon ends. 363 | left_tendon_end_variation = fibre_length_array[0] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 364 | 365 | ### Create small variation in the NMJ location. 366 | neuromuscular_junction = neuromuscular_junction + (innervation_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 367 | 368 | ### Force fibrelengths longer than the ordinary defined fibre length to be within the defined max fibre length. 369 | fibre_length = right_tendon_end_variation - left_tendon_end_variation 370 | 371 | ### Move poles with an initial offset, length of a tripole (b) 372 | initial_offset = b 373 | ## Initialise the locations of the poles at action potential initialisation 374 | location_pole_one = neuromuscular_junction - initial_offset + b # initial location of Pole 1 375 | location_pole_two = neuromuscular_junction - initial_offset - a + b # initial location of Pole 2 376 | location_pole_three = neuromuscular_junction - initial_offset # initial location of Pole 3 377 | location_pole_four = neuromuscular_junction + initial_offset # initial location of Pole 4 378 | location_pole_five = neuromuscular_junction + initial_offset + a - b # initial location of Pole 5 379 | location_pole_six = neuromuscular_junction + initial_offset - b # initial location of Pole 6 380 | 381 | ## Array of initial pole locations 382 | initial_pole_locations = (np.array(np.array([location_pole_one, location_pole_two, location_pole_three, location_pole_four, location_pole_five, location_pole_six]))[np.newaxis]).T 383 | 384 | ### Simulation of poles moving along the fibre in time. 385 | ## Move poles 1-3 in positive direction, with regards to conduction velocity. 386 | location_poles_right = np.array(initial_pole_locations[0:3] + conduction_velocity * time_array) 387 | # Set poles out of bounds to neuromuscular_junction 388 | location_poles_right[location_poles_right < neuromuscular_junction] = neuromuscular_junction 389 | 390 | ## Move poles 4-6 in negative direction, with regards to conduction velocity. 391 | location_poles_left = np.array(initial_pole_locations[3:6] - conduction_velocity * time_array) 392 | # Set poles out of bounds to neuromuscular_junction 393 | location_poles_left[location_poles_left > neuromuscular_junction] = neuromuscular_junction 394 | 395 | # Combine poles of both directions in one matrix 396 | location_poles_all = np.vstack((location_poles_right, location_poles_left)) 397 | 398 | ### Find and replace out of bounds locations at muscle fibre ends, both postive and negative end. Repalce out of bounds locations with fibre bound. 399 | # To get Merletti simulation 400 | #location_poles_all[location_poles_all < left_tendon_end_variation] = left_tendon_end_variation 401 | #location_poles_all[location_poles_all > fibre_length] = fibre_length 402 | 403 | ### Defining the detection system 404 | # Create a vector for the locations of number of electrodes along the fibre. 405 | electrode_locations_z = np.zeros(number_of_electrodes_z) 406 | # Create a spacing vector for the electrode locations, with number of electrodes. 407 | for i in range(number_of_electrodes_z): 408 | electrode_locations_z[i] = inter_electrode_spacing * i - ((inter_electrode_spacing * (number_of_electrodes_z - 1)) / 2) 409 | electrode_locations_z = electrode_locations_z + electrode_shift # Add interelectrode shift. 410 | 411 | electrode_locations_x = np.zeros(number_of_electrodes_x) 412 | # Create a spacing vector for the electrode locations, with number of electrodes. 413 | for j in range(number_of_electrodes_x): 414 | electrode_locations_x[j] = inter_electrode_spacing * j - ((inter_electrode_spacing * (number_of_electrodes_x - 1)) / 2) 415 | 416 | ### Create the single fibre action potential 417 | single_fibre_action_potential = np.zeros((number_of_electrodes_z, number_of_electrodes_x, len(time_array))) 418 | # Finding the potentials observed at each electrode. 419 | for z in range(number_of_electrodes_z): 420 | for x in range(number_of_electrodes_x): 421 | single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + np.exp(0.09*np.abs((electrode_locations_z[z]-neuromuscular_junction)))*(electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 422 | # To get Merletti simulation 423 | #single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + (electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 424 | single_fibre_action_potential = np.reshape(single_fibre_action_potential, (len(electrode_locations_z)*len(electrode_locations_x), len(time_array))) 425 | 426 | #print('Single Fibre Action Potential', single_fibre_action_potential) 427 | 428 | return -single_fibre_action_potential 429 | 430 | ######################### 5 ######################### Plot Fibre Action Potential ########################## ####################### 431 | def plot_fibre_action_potential(self): 432 | """Plots the single fibre action potentials computed in simulate_fibre_action_potential(). 433 | 434 | Args: 435 | 436 | single_fibre_action_potential 437 | time_array 438 | y_limit_minimum 439 | y_limit_maximum 440 | number_of_electrodes_z 441 | 442 | Returns: 443 | 444 | A plot of the single fibre action potential for all electrodes. 445 | 446 | """ 447 | ... 448 | ### Default arguments: 449 | single_fibre_action_potential = self.simulate_fibre_action_potential() 450 | time_array = self.time_array 451 | y_limit_minimum = self.y_limit_minimum 452 | y_limit_maximum = self.y_limit_maximum 453 | number_of_electrodes_z = self.number_of_electrodes_z 454 | number_of_electrodes_x = self.number_of_electrodes_x 455 | 456 | ### Plot the normalized single fibre action potential 457 | normalized_single_fibre_action_potential = single_fibre_action_potential 458 | normalized_single_fibre_action_potential = (normalized_single_fibre_action_potential - normalized_single_fibre_action_potential.mean()) / (normalized_single_fibre_action_potential.max() - normalized_single_fibre_action_potential.min()) 459 | 460 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 461 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 462 | zeros_array = np.zeros(len(array_size)) 463 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 464 | array_size_x = np.append(array_size_x,zeros_array) 465 | 466 | fig2 = plt.figure(2) 467 | for i in range(len(array_size)): 468 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 469 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 470 | ax.grid(which = 'both', ls = 'dashed') 471 | plt.plot(time_array, normalized_single_fibre_action_potential[i, :]) 472 | plt.xlim(time_array[0], time_array[-1] - 1) 473 | if i < len(array_size) - number_of_electrodes_x: 474 | ax.xaxis.set_major_formatter(NullFormatter()) 475 | plt.ylim(y_limit_minimum,y_limit_maximum) 476 | ax.yaxis.set_major_formatter(NullFormatter()) 477 | if i == 0: 478 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 479 | for j in range(i): 480 | if i == array_size_x[j]: 481 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 482 | elif number_of_electrodes_x == 1: 483 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 484 | 485 | plt.suptitle('Single Fibre Action Potential', fontsize = 20, fontweight = 'bold') 486 | fig2.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 487 | fig2.supylabel('Electrodes in the z direction, i.e. along the fibre', fontsize = 15) 488 | 489 | # Set the desired resolution 490 | plt.savefig('single_fibre_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 491 | scipy.io.savemat('single_fibre_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'SFAP': single_fibre_action_potential}) 492 | 493 | return plt.show() 494 | 495 | ######################### 6 ######################### Simulate Motor Unit Action Potential ########################## ####################### 496 | def simulate_motor_unit(self): 497 | """Simulates the summed action potentials from a number of fibres in a motor unit, recorded by all electrodes. 498 | 499 | Args: 500 | single_fibre_action_potential 501 | time_array 502 | number_of_electrodes_z 503 | number_of_fibres 504 | 505 | Returns: 506 | A numpy array containing the simulated motor unit action potentials for all electrodes positioned. 507 | """ 508 | ... 509 | 510 | ### Default arguments: 511 | self.simulate_fibre_action_potential() 512 | time_array = self.time_array 513 | motor_unit_radius = self.motor_unit_radius 514 | number_of_fibres = self.number_of_fibres 515 | motor_unit_depth = self.motor_unit_depth 516 | motor_unit_x_position = self.motor_unit_x_position 517 | 518 | ### Calcualte the fibre depth variation. 519 | ### Calculate the motor unit depth variation that use to calcualte the fibre depth variation 520 | theta_angle = 2 * np.pi * np.random.random(number_of_fibres) 521 | 522 | x_position_single_fibre = np.zeros(number_of_fibres) 523 | y_position_single_fibre = np.zeros(number_of_fibres) 524 | 525 | # calculating coordinates 526 | for i in range(number_of_fibres): 527 | radial_position_single_fibre = motor_unit_radius * np.random.random() 528 | x_position_single_fibre[i] = radial_position_single_fibre * np.cos(theta_angle[i]) 529 | y_position_single_fibre[i] = radial_position_single_fibre * np.sin(theta_angle[i]) 530 | 531 | ### Simulate the total action potential of a motor unit for the defined number of single fibres. 532 | motor_unit_matrix = None 533 | for i in range(number_of_fibres): 534 | self.fibre_depth = motor_unit_depth - y_position_single_fibre[i] 535 | self.fibre_x_position = motor_unit_x_position + x_position_single_fibre[i] 536 | 537 | single_fibre = self.simulate_fibre_action_potential() 538 | 539 | # Generate matrix for motor unit and add each fibre. 540 | if i == 0: 541 | motor_unit_matrix = single_fibre 542 | else: 543 | motor_unit_matrix = motor_unit_matrix + single_fibre 544 | 545 | motor_unit_matrix_with_time = np.vstack((time_array, motor_unit_matrix)) 546 | motor_unit_matrix = motor_unit_matrix_with_time[1:] 547 | 548 | #print('Motor Unit Action Potential', motor_unit_matrix) 549 | return motor_unit_matrix 550 | 551 | ######################### 7 ######################### Plot Motor Unit Action Potential ########################## ####################### 552 | def plot_motor_unit(self): 553 | """Plots the motor unit action potential from the summed number of single fibres computed in simulate_motor_unit(). 554 | 555 | Args: 556 | motor_unit_matrix 557 | time_array 558 | y_limit_minimum 559 | y_limit_maximum 560 | number_of_electrodes_z 561 | 562 | Returns: 563 | A plot of the motor unit action potential for all electrodes. 564 | """ 565 | ... 566 | ### Default arguments: 567 | motor_unit = self.simulate_motor_unit() 568 | time_array = self.time_array 569 | y_limit_minimum = self.y_limit_minimum 570 | y_limit_maximum = self.y_limit_maximum 571 | number_of_electrodes_z = self.number_of_electrodes_z 572 | number_of_electrodes_x = self.number_of_electrodes_x 573 | 574 | ### Plot the normalized motor unit action potential 575 | normalized_motor_unit = motor_unit 576 | normalized_motor_unit = (normalized_motor_unit - normalized_motor_unit.mean()) / (normalized_motor_unit.max() - normalized_motor_unit.min()) 577 | 578 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 579 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 580 | zeros_array = np.zeros(len(array_size)) 581 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 582 | array_size_x = np.append(array_size_x,zeros_array) 583 | 584 | fig3 = plt.figure(3) 585 | for i in range(len(array_size)): 586 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 587 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 588 | ax.grid(which = 'both', ls = 'dashed') 589 | plt.plot(time_array, normalized_motor_unit[i, :]) 590 | plt.xlim(time_array[0], time_array[-1] - 1) 591 | if i < len(array_size) - number_of_electrodes_x: 592 | ax.xaxis.set_major_formatter(NullFormatter()) 593 | ax.yaxis.set_major_formatter(NullFormatter()) 594 | if i == 0: 595 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 596 | for j in range(i): 597 | if i == array_size_x[j]: 598 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 599 | elif number_of_electrodes_x == 1: 600 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 601 | plt.ylim(y_limit_minimum,y_limit_maximum) 602 | plt.suptitle('Motor Unit Action Potential', fontsize = 20, fontweight = 'bold') 603 | if number_of_electrodes_x > 1: 604 | fig3.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 605 | else: 606 | fig3.supxlabel('Time (ms)', fontsize = 15) 607 | if number_of_electrodes_z > 1: 608 | fig3.supylabel('Motor Unit Action Potential\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center', fontsize = 15) 609 | else: 610 | fig3.supylabel('Motor Unit Action Potential', ha = 'center', va = 'center', fontsize = 15) 611 | 612 | # Set the desired resolution 613 | plt.savefig('one_motor_unit_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 614 | scipy.io.savemat('one_motor_unit_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'MUAP': motor_unit}) 615 | 616 | return plt.show() 617 | 618 | # ######################### ######################### ########################## ####################### 619 | # THE END # 620 | # ######################### ######################### ########################## ####################### 621 | -------------------------------------------------------------------------------- /Example_Code_Figures/Membrane_Tripole/MU.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.ticker import NullFormatter 4 | import scipy.io 5 | plt.rcParams['font.family'] = 'Times New Roman' 6 | 7 | 8 | 9 | class MotorUnit: 10 | """A class that represents a motor unit (MU). 11 | 12 | Methods: 13 | get_tripole_amplitude: Calculates the action potential and the current distribution. Returns the tripole amplitudes of the current distribution. 14 | plot_current_distribution_action_potential: Plots the normalized action potential and current distribution based on the given parameter values. 15 | get_tripole_distance: Calculates the distance between the poles of the current distribution based on returned value from get_tripole_amplitude. 16 | simulate_fibre_action_potential: Simulates the action potential recorded by a given number of electrodes, positioned along one fibre, for the given current distriubtion. 17 | plot_fibre_action_potential: Plots the normalized action potential for one fibre over the electrodes positioned along the fibre. 18 | simulate_motor_unit: Simulates the action potential for a given number of fibres, for electrodes positioned along the motor unit. 19 | plot_motor_unit: Plots the normalized action potential simulated for one motor unit, for a given number of fibres, and electrodes positioned along the motor unit. 20 | 21 | Attributes: 22 | For get_tripole_amplitude: 23 | A: Constant to fit the amplitude of the tripole (V). 24 | B: Resting membrane potential amplitude (V). 25 | C: Muscle fibre proportionality constant. 26 | plot_length: Length for plotting the action potential and current distribution expressed in mm (20 mm). 27 | scaling_factor: A scaling factor expressed in mm^-1, lambda (λ). 28 | 29 | For simulate_fibre_action_potential: 30 | fibre_length: Length of fibre (mm). 31 | neuromuscular_junction: Position of NMJ along the length of the fibre (mm). 32 | conduction_velocity: Conduction velocity along the muscle fibre (m/s). 33 | electrode_shift: Position of center of electrode array along the length of the fibre (mm). [electrode_shift = neuromuscular_junction means the array sits centered above NMJ.] 34 | number_of_electrodes_z: Number of elecrodes in the array, in the direction of the fibre. 35 | number_of_electrodes_x: Number of electrodes in the array across the fibre. 36 | inter_electrode_spacing: Distance between electrodes in the array (mm). 37 | radial_conductivity: Conductivity in radial direction, across the fibre (m/s). 38 | ratio_axial_radial_conductivity: Ratio between axial and radial conductivity, (fibre direction conductivity / radial conductivity) 39 | fibre_depth: Initial depth from the surface EMG down to the fibre (mm). 40 | 41 | For simulate_motor_unit: 42 | motor_unit_radius: Radius of the motor unit (mm). 43 | number_of_fibres: The number of fibres in a given motor unit. 44 | 45 | For plotting methods: 46 | y_limit_minimum: Minimum value of plot y-axis. 47 | y_limit_maximum: Maximum value of plot y-axis. 48 | """ 49 | ... 50 | 51 | def __init__(self): 52 | """Initializes a new Motor Unit object. 53 | 54 | """ 55 | ... 56 | 57 | ### Default values for generating a tripole: 58 | self.A:float = 96e-3 59 | self.B:float = -90e-3 60 | self.C:int = 1 61 | self.plot_length:int = 20 62 | self.scaling_factor:float = 1 63 | 64 | ### Default values for a single fibre: 65 | self.fibre_length:int = 210 66 | self.conduction_velocity:int = 4 67 | self.ratio_axial_radial_conductivity:int = 6 # 0.001 68 | self.radial_conductivity:float = 0.303 69 | self.inter_electrode_spacing:float = 10 # 20 mm 70 | self.number_of_electrodes_z:int = 1 71 | self.number_of_electrodes_x:int = 1 72 | self.electrode_shift:int = 165 # 75 z-axis 73 | self.initial_neuromuscular_junction:int = 90 74 | self.fibre_depth:float = 10 75 | self.x_position_single_fibre:float = 10 76 | self.extermination_zone_width:int = 10 77 | self.innervation_zone_width:int = 5 78 | self.time_length:int = 35 79 | 80 | ### Default values for a motor unit: 81 | self.motor_unit_radius:float = 1 82 | self.number_of_fibres:int = 100 83 | self.motor_unit_depth:float = 10 84 | self.motor_unit_x_position:float = 0 85 | 86 | ### Default values for plots: 87 | self.y_limit_minimum:float = -1 88 | self.y_limit_maximum:float = 1 89 | 90 | ######################### 1 ######################### Get Tripole Amplitude ########################## ####################### 91 | def get_tripole_amplitude(self): 92 | """Generates the tripole amplitude from the membrane current distribution source. 93 | 94 | Args: 95 | According to Modeling of Surface Myoelectric Signals, part I. Merletti, 1999. 96 | A: A suitable constant to fit the amplitude of the action potential expressed in V (96 mV = 96e-3). 97 | B: The resting membrane poteintial expressed in V (-90 mV = -90e-3). 98 | C: Muscle fibre (proportionality) constant (1). 99 | plot_length: Length of plotting the action potential and current distribution expressed in mm (20 mm). 100 | scaling_factor: A scale factor expressed in mm^-1, which is lambda (λ), (1 mm^-1). 101 | 102 | Returns: 103 | A numpy array containing the tripole amplitude of the current distribution. 104 | """ 105 | ... 106 | 107 | ### Default arguments: 108 | A = self.A 109 | B = self.B 110 | C = self.C 111 | plot_length = self.plot_length 112 | scaling_factor = self.scaling_factor 113 | 114 | ######################### Action Potential, Current Distribution ####################### 115 | ### Create the z-axis vector, which is the sampled vector of plotting the action potential and current distribution: 116 | delta_length = 0.1 # (10 kHz) total distance between samples 117 | z = np.arange(0, plot_length + delta_length, delta_length) 118 | self.z = z 119 | 120 | ### A mathematical description of the action potential 121 | action_potential = A * (scaling_factor * z)**3 * np.exp(-scaling_factor * z) - B # eq.1.1 Merletti part 1 122 | self.action_potential = action_potential 123 | 124 | ### Calculate the mebrane current distribution which is proportional to the second derivative of the action potential 125 | current_distribution = C * A * scaling_factor**2 * (scaling_factor * z) * (6 - 6 * scaling_factor * z + scaling_factor**2 * z**2) * np.exp(-scaling_factor * z) # eq. 1.2 Merletti part 1 126 | self.current_distribution = current_distribution 127 | 128 | ######################### Pole Amplitude ####################### 129 | ### Calculate the pole amplitude 130 | ## To calculate pole_one, pole_two, and pole_three 131 | # Discretize the current distribution to 1 and -1 132 | current_distribution_discrete = np.where(current_distribution > 0, 1, -1) 133 | 134 | # Calculate the absolute difference between each sample of discretized current distribution 135 | current_distribution_difference = np.abs(np.diff(current_distribution_discrete)) 136 | 137 | # Locate the absolute differences that are greater than zero 138 | pole_location_index = np.where(current_distribution_difference > 0)[0] 139 | self.pole_location_index = pole_location_index 140 | 141 | # Calculate the poles 142 | dz = z[1] - z[0] 143 | self.dz = dz 144 | 145 | ### Sum pole magnitude for each part of the tripole. 146 | pole_one = np.sum(current_distribution[:pole_location_index[1]]) * dz 147 | pole_two = np.sum(current_distribution[pole_location_index[1] + 1:pole_location_index[2]]) * dz 148 | pole_three = np.sum(current_distribution[pole_location_index[2] + 1:]) * dz 149 | 150 | ### Use rounding adjustment to set sum of all poles equal to zero. 151 | pole_rounding_adjustment = np.abs(pole_one + pole_two + pole_three) 152 | pole_one = pole_one + pole_rounding_adjustment 153 | pole_two = pole_two - pole_rounding_adjustment 154 | pole_three = pole_three + pole_rounding_adjustment 155 | 156 | ### Poles amplitude array. 157 | poles_amplitude = np.array([pole_one, pole_two, pole_three]) 158 | #print('Poles Amplitude =', poles_amplitude) 159 | 160 | return poles_amplitude 161 | 162 | ######################### 2 ######################### Get Pole Distance ########################## ####################### 163 | def get_tripole_distance(self): 164 | """Calculates the distance between the poles of the current distribution. 165 | 166 | Args: 167 | pole_location_index: A list of integers containing the indices of the poles in the current distribution. 168 | dz: The sampling interval of the current distribution. 169 | current_distribution 170 | 171 | Returns: 172 | A list of two floats containing the distances between the poles. 173 | """ 174 | ... 175 | 176 | ### Default arguments 177 | self.get_tripole_amplitude() 178 | pole_location_index = self.pole_location_index 179 | dz = self.dz 180 | current_distribution = self.current_distribution 181 | 182 | ### Calculate the distance between the poles 183 | """a, b represent tripole asymmetry 184 | a is the distance between pole 1 and pole 2. 185 | b is the distance between pole 1 and pole 3. 186 | The following rules of the current sources must hold: 187 | pole_one + pole_two + pole_three = 0 188 | pole_two*a + pole_three*b = 0 189 | """ 190 | 191 | ## Calculate the cumulative sum of each phase. 192 | pole_one_sum = np.cumsum(current_distribution[:pole_location_index[1]]) 193 | pole_two_sum = np.cumsum(current_distribution[pole_location_index[1]:pole_location_index[2]]) 194 | pole_three_sum = np.cumsum(current_distribution[pole_location_index[2]:]) 195 | 196 | ## Locate the location index (z-coordinate) at half the cumulative sum of each phase. 197 | pole_one_location = np.where(pole_one_sum > 0.5 * np.sum(current_distribution[:pole_location_index[1]]))[0] 198 | pole_two_location = pole_location_index[1] + np.where(pole_two_sum < 0.5 * np.sum(current_distribution[pole_location_index[1]:pole_location_index[2]]))[0] 199 | pole_three_location = pole_location_index[2] + np.where(pole_three_sum > 0.5 * np.sum(current_distribution[pole_location_index[2]:]))[0] 200 | 201 | ## Calculate the pole positions 202 | pole_one_position = pole_one_location[0] * dz 203 | pole_two_position = pole_two_location[0] * dz 204 | pole_three_position = pole_three_location[0] * dz 205 | 206 | ## Calculate the distance between the poles 207 | a = pole_two_position - pole_one_position 208 | b = pole_three_position - pole_one_position 209 | 210 | pole_distances = np.array([a, b]) 211 | #print('Pole Distances =', pole_distances) 212 | 213 | return pole_distances 214 | 215 | ######################### 3 ######################### Plot Current Distribution & Action Potential ########################## ####################### 216 | def plot_current_distribution_action_potential(self): 217 | """Plots the membrane current distribution and action potential. 218 | 219 | Args: 220 | current_distribution 221 | action_potential 222 | z: Sampled vector of plotting 223 | 224 | Returns: 225 | A plot of the current distribution and action potential. 226 | """ 227 | ... 228 | 229 | ### default arguments 230 | poles_amplitude = self.get_tripole_amplitude() 231 | pole_distances = self.get_tripole_distance() 232 | current_distribution = self.current_distribution 233 | action_potential = self.action_potential 234 | z = self.z 235 | pole_location_index = self.pole_location_index 236 | 237 | ######################### Normalization and Plot ####################### 238 | ### Plot the normalized current distribution and action potential 239 | # Normalize the signals using min-max feature scaling between points y_limit_minimum and y_limit_maximum. 240 | y_limit_minimum = self.y_limit_minimum 241 | y_limit_maximum = self.y_limit_maximum 242 | 243 | #normalized_current_distribution = y_limit_minimum + ((current_distribution - current_distribution.min())*(y_limit_maximum-y_limit_minimum)) / (current_distribution.max() - current_distribution.min()) 244 | #normalized_action_potential = y_limit_minimum + ((action_potential - action_potential.min())*(y_limit_maximum-y_limit_minimum)) / (action_potential.max() - action_potential.min()) 245 | 246 | fig1 = plt.figure(1) 247 | 248 | ax = plt.subplot(2,1,1) 249 | plt.plot(z, current_distribution*1000) 250 | plt.ylabel('Im, Current Distribution (mA)') 251 | #plt.axvline(z[np.argmax(current_distribution)], color = 'r', linestyle = '--', label = 'P1') 252 | #plt.axvline(z[np.argmin(current_distribution)], color = 'g', linestyle = '--', label = 'P2') 253 | #plt.legend() 254 | y_p1 = poles_amplitude[0]*1000 255 | y_p2 = poles_amplitude[1]*1000 256 | y_p3 = poles_amplitude[2]*1000 257 | 258 | p1_p2 = pole_distances[0] 259 | p1_P3 = pole_distances[1] 260 | 261 | x_p1 = z[pole_location_index[0]] 262 | x_p2 = z[pole_location_index[1]] 263 | x_p3 = z[pole_location_index[2]] 264 | 265 | p1_location = (x_p2 - x_p1) / 2 266 | p2_location = p1_location + p1_p2 267 | p3_location = p1_location + p1_P3 268 | 269 | plt.plot(p1_location, y_p1, 'o') 270 | plt.text(p1_location + 0.2, y_p1, 'P1') 271 | plt.vlines(x = p1_location, ymin = 0, ymax = y_p1, color = 'k') 272 | 273 | plt.plot(p2_location, y_p2, 'o') 274 | plt.text(p2_location + 0.2, y_p2, 'P2') 275 | plt.vlines(x = p2_location, ymin = 0, ymax = y_p2, color = 'k') 276 | 277 | plt.plot(p3_location, y_p3, 'o') 278 | plt.text(p3_location + 0.2, y_p3, 'P3') 279 | plt.vlines(x = p3_location, ymin = 0, ymax = y_p3, color = 'k') 280 | 281 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 282 | ax.xaxis.set_major_formatter(NullFormatter()) 283 | plt.title('Membrane Current Distribution', fontweight = 'bold') 284 | 285 | ax = plt.subplot(2,1,2) 286 | plt.plot(z, action_potential*1000-180) 287 | plt.ylabel('Vm, Action Potential (mV)') 288 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 289 | plt.title('Membrane Action Potential', fontweight = 'bold') 290 | 291 | fig1.supxlabel('z, Distance (mm)') 292 | 293 | A = self.A 294 | 295 | # Set the desired resolution 296 | plt.savefig('current_and_action_potential_{}mA_amplitude.png'.format(A*1000), dpi = 600) 297 | scipy.io.savemat('current' + '.mat', {'Im': current_distribution}) 298 | scipy.io.savemat('action_potential' + '.mat', {'Vm': action_potential}) 299 | 300 | return plt.show() 301 | 302 | ######################### 4 ######################### Simulate Fibre Action Potential ########################## ####################### 303 | def simulate_fibre_action_potential(self): 304 | """Simulates the action potentials recorded by all electrodes for one fibre. 305 | 306 | Args: 307 | fibre_length 308 | neuromuscular_junction 309 | conduction_velocity 310 | electrode_shift 311 | number_of_electrodes_z 312 | inter_electrode_spacing 313 | radial_conductivity 314 | ratio_axial_radial_conductivity 315 | fibre_depth 316 | 317 | Returns: 318 | A numpy array containing the simulated action potentials for electrodes positioned along the fibre. 319 | """ 320 | ... 321 | 322 | ### Choose default values for attributes 323 | fibre_length = self.fibre_length 324 | neuromuscular_junction = self.initial_neuromuscular_junction 325 | conduction_velocity = self.conduction_velocity 326 | electrode_shift = self.electrode_shift 327 | number_of_electrodes_z = self.number_of_electrodes_z 328 | number_of_electrodes_x = self.number_of_electrodes_x 329 | inter_electrode_spacing = self.inter_electrode_spacing 330 | radial_conductivity = self.radial_conductivity 331 | ratio_axial_radial_conductivity = self.ratio_axial_radial_conductivity 332 | extermination_zone_width = self.extermination_zone_width 333 | innervation_zone_width = self.innervation_zone_width 334 | fibre_depth = self.fibre_depth 335 | time_length = self.time_length 336 | x_position_single_fibre = self.x_position_single_fibre 337 | 338 | ### Simulation of poles moving along the fibre in time. Create time array. 339 | delta_time = 0.1 # (10 kHz) 340 | time_array = np.arange(0, time_length + delta_time, delta_time) 341 | self.time_array = time_array 342 | 343 | ### Create the current tripole with initial pole amplitude and positions 344 | tripole_amplitude = self.get_tripole_amplitude() 345 | tripole_distance = self.get_tripole_distance() 346 | 347 | a = tripole_distance[0] 348 | b = tripole_distance[1] 349 | pole_one = tripole_amplitude[0] 350 | pole_two = tripole_amplitude[1] 351 | pole_three = tripole_amplitude[2] 352 | 353 | # Array of mirrored tripole amplitudes 354 | P = np.array([pole_one, pole_two, pole_three, pole_three, pole_two, pole_one]).reshape(-1, 1) 355 | Pi = np.tile(P,(1,len(time_array))) 356 | 357 | ### Create uniformly distributed tendon ends at the extermination zones of each fibre and uniformly distributed neuromuscular junctions at the innervation zones. 358 | delta_length = 0.1 # (10 kHz) 359 | fibre_length_array = np.arange(0, fibre_length + delta_length, delta_length) 360 | 361 | ### Create random variation in the rightmost tendon ends. 362 | right_tendon_end_variation = fibre_length_array[-1] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 363 | 364 | ### Create variation in the leftmost tendon ends. 365 | left_tendon_end_variation = fibre_length_array[0] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 366 | 367 | ### Create small variation in the NMJ location. 368 | neuromuscular_junction = neuromuscular_junction + (innervation_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 369 | 370 | ### Force fibrelengths longer than the ordinary defined fibre length to be within the defined max fibre length. 371 | fibre_length = right_tendon_end_variation - left_tendon_end_variation 372 | 373 | ### Move poles with an initial offset, length of a tripole (b) 374 | initial_offset = b 375 | ## Initialise the locations of the poles at action potential initialisation 376 | location_pole_one = neuromuscular_junction - initial_offset + b # initial location of Pole 1 377 | location_pole_two = neuromuscular_junction - initial_offset - a + b # initial location of Pole 2 378 | location_pole_three = neuromuscular_junction - initial_offset # initial location of Pole 3 379 | location_pole_four = neuromuscular_junction + initial_offset # initial location of Pole 4 380 | location_pole_five = neuromuscular_junction + initial_offset + a - b # initial location of Pole 5 381 | location_pole_six = neuromuscular_junction + initial_offset - b # initial location of Pole 6 382 | 383 | ## Array of initial pole locations 384 | initial_pole_locations = (np.array(np.array([location_pole_one, location_pole_two, location_pole_three, location_pole_four, location_pole_five, location_pole_six]))[np.newaxis]).T 385 | 386 | ### Simulation of poles moving along the fibre in time. 387 | ## Move poles 1-3 in positive direction, with regards to conduction velocity. 388 | location_poles_right = np.array(initial_pole_locations[0:3] + conduction_velocity * time_array) 389 | # Set poles out of bounds to neuromuscular_junction 390 | location_poles_right[location_poles_right < neuromuscular_junction] = neuromuscular_junction 391 | 392 | ## Move poles 4-6 in negative direction, with regards to conduction velocity. 393 | location_poles_left = np.array(initial_pole_locations[3:6] - conduction_velocity * time_array) 394 | # Set poles out of bounds to neuromuscular_junction 395 | location_poles_left[location_poles_left > neuromuscular_junction] = neuromuscular_junction 396 | 397 | # Combine poles of both directions in one matrix 398 | location_poles_all = np.vstack((location_poles_right, location_poles_left)) 399 | 400 | ### Find and replace out of bounds locations at muscle fibre ends, both postive and negative end. Repalce out of bounds locations with fibre bound. 401 | # To get Merletti simulation 402 | #location_poles_all[location_poles_all < left_tendon_end_variation] = left_tendon_end_variation 403 | #location_poles_all[location_poles_all > fibre_length] = fibre_length 404 | 405 | ### Defining the detection system 406 | # Create a vector for the locations of number of electrodes along the fibre. 407 | electrode_locations_z = np.zeros(number_of_electrodes_z) 408 | # Create a spacing vector for the electrode locations, with number of electrodes. 409 | for i in range(number_of_electrodes_z): 410 | electrode_locations_z[i] = inter_electrode_spacing * i - ((inter_electrode_spacing * (number_of_electrodes_z - 1)) / 2) 411 | electrode_locations_z = electrode_locations_z + electrode_shift # Add interelectrode shift. 412 | 413 | electrode_locations_x = np.zeros(number_of_electrodes_x) 414 | # Create a spacing vector for the electrode locations, with number of electrodes. 415 | for j in range(number_of_electrodes_x): 416 | electrode_locations_x[j] = inter_electrode_spacing * j - ((inter_electrode_spacing * (number_of_electrodes_x - 1)) / 2) 417 | 418 | ### Create the single fibre action potential 419 | single_fibre_action_potential = np.zeros((number_of_electrodes_z, number_of_electrodes_x, len(time_array))) 420 | # Finding the potentials observed at each electrode. 421 | for z in range(number_of_electrodes_z): 422 | for x in range(number_of_electrodes_x): 423 | single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + np.exp(0.09*np.abs((electrode_locations_z[z]-neuromuscular_junction)))*(electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 424 | # To get Merletti simulation 425 | #single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + (electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 426 | single_fibre_action_potential = np.reshape(single_fibre_action_potential, (len(electrode_locations_z)*len(electrode_locations_x), len(time_array))) 427 | 428 | #print('Single Fibre Action Potential', single_fibre_action_potential) 429 | 430 | return -single_fibre_action_potential 431 | 432 | ######################### 5 ######################### Plot Fibre Action Potential ########################## ####################### 433 | def plot_fibre_action_potential(self): 434 | """Plots the single fibre action potentials computed in simulate_fibre_action_potential(). 435 | 436 | Args: 437 | 438 | single_fibre_action_potential 439 | time_array 440 | y_limit_minimum 441 | y_limit_maximum 442 | number_of_electrodes_z 443 | 444 | Returns: 445 | 446 | A plot of the single fibre action potential for all electrodes. 447 | 448 | """ 449 | ... 450 | ### Default arguments: 451 | single_fibre_action_potential = self.simulate_fibre_action_potential() 452 | time_array = self.time_array 453 | y_limit_minimum = self.y_limit_minimum 454 | y_limit_maximum = self.y_limit_maximum 455 | number_of_electrodes_z = self.number_of_electrodes_z 456 | number_of_electrodes_x = self.number_of_electrodes_x 457 | 458 | ### Plot the normalized single fibre action potential 459 | normalized_single_fibre_action_potential = single_fibre_action_potential 460 | normalized_single_fibre_action_potential = (normalized_single_fibre_action_potential - normalized_single_fibre_action_potential.mean()) / (normalized_single_fibre_action_potential.max() - normalized_single_fibre_action_potential.min()) 461 | 462 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 463 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 464 | zeros_array = np.zeros(len(array_size)) 465 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 466 | array_size_x = np.append(array_size_x,zeros_array) 467 | 468 | fig2 = plt.figure(2) 469 | for i in range(len(array_size)): 470 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 471 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 472 | ax.grid(which = 'both', ls = 'dashed') 473 | plt.plot(time_array, normalized_single_fibre_action_potential[i, :]) 474 | plt.xlim(time_array[0], time_array[-1] - 1) 475 | if i < len(array_size) - number_of_electrodes_x: 476 | ax.xaxis.set_major_formatter(NullFormatter()) 477 | plt.ylim(y_limit_minimum,y_limit_maximum) 478 | ax.yaxis.set_major_formatter(NullFormatter()) 479 | if i == 0: 480 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 481 | for j in range(i): 482 | if i == array_size_x[j]: 483 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 484 | elif number_of_electrodes_x == 1: 485 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 486 | 487 | plt.suptitle('Single Fibre Action Potential', fontsize = 20, fontweight = 'bold') 488 | fig2.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 489 | fig2.supylabel('Electrodes in the z direction, i.e. along the fibre', fontsize = 15) 490 | 491 | # Set the desired resolution 492 | plt.savefig('single_fibre_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 493 | scipy.io.savemat('single_fibre_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'SFAP': single_fibre_action_potential}) 494 | 495 | return plt.show() 496 | 497 | ######################### 6 ######################### Simulate Motor Unit Action Potential ########################## ####################### 498 | def simulate_motor_unit(self): 499 | """Simulates the summed action potentials from a number of fibres in a motor unit, recorded by all electrodes. 500 | 501 | Args: 502 | single_fibre_action_potential 503 | time_array 504 | number_of_electrodes_z 505 | number_of_fibres 506 | 507 | Returns: 508 | A numpy array containing the simulated motor unit action potentials for all electrodes positioned. 509 | """ 510 | ... 511 | 512 | ### Default arguments: 513 | self.simulate_fibre_action_potential() 514 | time_array = self.time_array 515 | motor_unit_radius = self.motor_unit_radius 516 | number_of_fibres = self.number_of_fibres 517 | motor_unit_depth = self.motor_unit_depth 518 | motor_unit_x_position = self.motor_unit_x_position 519 | 520 | ### Calcualte the fibre depth variation. 521 | ### Calculate the motor unit depth variation that use to calcualte the fibre depth variation 522 | theta_angle = 2 * np.pi * np.random.random(number_of_fibres) 523 | 524 | x_position_single_fibre = np.zeros(number_of_fibres) 525 | y_position_single_fibre = np.zeros(number_of_fibres) 526 | 527 | # calculating coordinates 528 | for i in range(number_of_fibres): 529 | radial_position_single_fibre = motor_unit_radius * np.random.random() 530 | x_position_single_fibre[i] = radial_position_single_fibre * np.cos(theta_angle[i]) 531 | y_position_single_fibre[i] = radial_position_single_fibre * np.sin(theta_angle[i]) 532 | 533 | ### Simulate the total action potential of a motor unit for the defined number of single fibres. 534 | motor_unit_matrix = None 535 | for i in range(number_of_fibres): 536 | self.fibre_depth = motor_unit_depth - y_position_single_fibre[i] 537 | self.fibre_x_position = motor_unit_x_position + x_position_single_fibre[i] 538 | 539 | single_fibre = self.simulate_fibre_action_potential() 540 | 541 | # Generate matrix for motor unit and add each fibre. 542 | if i == 0: 543 | motor_unit_matrix = single_fibre 544 | else: 545 | motor_unit_matrix = motor_unit_matrix + single_fibre 546 | 547 | motor_unit_matrix_with_time = np.vstack((time_array, motor_unit_matrix)) 548 | motor_unit_matrix = motor_unit_matrix_with_time[1:] 549 | 550 | #print('Motor Unit Action Potential', motor_unit_matrix) 551 | return motor_unit_matrix 552 | 553 | ######################### 7 ######################### Plot Motor Unit Action Potential ########################## ####################### 554 | def plot_motor_unit(self): 555 | """Plots the motor unit action potential from the summed number of single fibres computed in simulate_motor_unit(). 556 | 557 | Args: 558 | motor_unit_matrix 559 | time_array 560 | y_limit_minimum 561 | y_limit_maximum 562 | number_of_electrodes_z 563 | 564 | Returns: 565 | A plot of the motor unit action potential for all electrodes. 566 | """ 567 | ... 568 | ### Default arguments: 569 | motor_unit = self.simulate_motor_unit() 570 | time_array = self.time_array 571 | y_limit_minimum = self.y_limit_minimum 572 | y_limit_maximum = self.y_limit_maximum 573 | number_of_electrodes_z = self.number_of_electrodes_z 574 | number_of_electrodes_x = self.number_of_electrodes_x 575 | 576 | ### Plot the normalized motor unit action potential 577 | normalized_motor_unit = motor_unit 578 | normalized_motor_unit = (normalized_motor_unit - normalized_motor_unit.mean()) / (normalized_motor_unit.max() - normalized_motor_unit.min()) 579 | 580 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 581 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 582 | zeros_array = np.zeros(len(array_size)) 583 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 584 | array_size_x = np.append(array_size_x,zeros_array) 585 | 586 | fig3 = plt.figure(3) 587 | for i in range(len(array_size)): 588 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 589 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 590 | ax.grid(which = 'both', ls = 'dashed') 591 | plt.plot(time_array, normalized_motor_unit[i, :]) 592 | plt.xlim(time_array[0], time_array[-1] - 1) 593 | if i < len(array_size) - number_of_electrodes_x: 594 | ax.xaxis.set_major_formatter(NullFormatter()) 595 | ax.yaxis.set_major_formatter(NullFormatter()) 596 | if i == 0: 597 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 598 | for j in range(i): 599 | if i == array_size_x[j]: 600 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 601 | elif number_of_electrodes_x == 1: 602 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 603 | plt.ylim(y_limit_minimum,y_limit_maximum) 604 | plt.suptitle('Motor Unit Action Potential', fontsize = 20, fontweight = 'bold') 605 | if number_of_electrodes_x > 1: 606 | fig3.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 607 | else: 608 | fig3.supxlabel('Time (ms)', fontsize = 15) 609 | if number_of_electrodes_z > 1: 610 | fig3.supylabel('Motor Unit Action Potential\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center', fontsize = 15) 611 | else: 612 | fig3.supylabel('Motor Unit Action Potential', ha = 'center', va = 'center', fontsize = 15) 613 | 614 | # Set the desired resolution 615 | plt.savefig('one_motor_unit_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 616 | scipy.io.savemat('one_motor_unit_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'MUAP': motor_unit}) 617 | 618 | return plt.show() 619 | 620 | # ######################### ######################### ########################## ####################### 621 | # THE END # 622 | # ######################### ######################### ########################## ####################### -------------------------------------------------------------------------------- /Example_Code_Figures/sEMG_Electrode_Array/MU.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.ticker import NullFormatter 4 | import scipy.io 5 | plt.rcParams['font.family'] = 'Times New Roman' 6 | 7 | 8 | 9 | class MotorUnit: 10 | """A class that represents a motor unit (MU). 11 | 12 | Methods: 13 | get_tripole_amplitude: Calculates the action potential and the current distribution. Returns the tripole amplitudes of the current distribution. 14 | plot_current_distribution_action_potential: Plots the normalized action potential and current distribution based on the given parameter values. 15 | get_tripole_distance: Calculates the distance between the poles of the current distribution based on returned value from get_tripole_amplitude. 16 | simulate_fibre_action_potential: Simulates the action potential recorded by a given number of electrodes, positioned along one fibre, for the given current distriubtion. 17 | plot_fibre_action_potential: Plots the normalized action potential for one fibre over the electrodes positioned along the fibre. 18 | simulate_motor_unit: Simulates the action potential for a given number of fibres, for electrodes positioned along the motor unit. 19 | plot_motor_unit: Plots the normalized action potential simulated for one motor unit, for a given number of fibres, and electrodes positioned along the motor unit. 20 | 21 | Attributes: 22 | For get_tripole_amplitude: 23 | A: Constant to fit the amplitude of the tripole (V). 24 | B: Resting membrane potential amplitude (V). 25 | C: Muscle fibre proportionality constant. 26 | plot_length: Length for plotting the action potential and current distribution expressed in mm (20 mm). 27 | scaling_factor: A scaling factor expressed in mm^-1, lambda (λ). 28 | 29 | For simulate_fibre_action_potential: 30 | fibre_length: Length of fibre (mm). 31 | neuromuscular_junction: Position of NMJ along the length of the fibre (mm). 32 | conduction_velocity: Conduction velocity along the muscle fibre (m/s). 33 | electrode_shift: Position of center of electrode array along the length of the fibre (mm). [electrode_shift = neuromuscular_junction means the array sits centered above NMJ.] 34 | number_of_electrodes_z: Number of elecrodes in the array, in the direction of the fibre. 35 | number_of_electrodes_x: Number of electrodes in the array across the fibre. 36 | inter_electrode_spacing: Distance between electrodes in the array (mm). 37 | radial_conductivity: Conductivity in radial direction, across the fibre (m/s). 38 | ratio_axial_radial_conductivity: Ratio between axial and radial conductivity, (fibre direction conductivity / radial conductivity) 39 | fibre_depth: Initial depth from the surface EMG down to the fibre (mm). 40 | 41 | For simulate_motor_unit: 42 | motor_unit_radius: Radius of the motor unit (mm). 43 | number_of_fibres: The number of fibres in a given motor unit. 44 | 45 | For plotting methods: 46 | y_limit_minimum: Minimum value of plot y-axis. 47 | y_limit_maximum: Maximum value of plot y-axis. 48 | """ 49 | ... 50 | 51 | def __init__(self): 52 | """Initializes a new Motor Unit object. 53 | 54 | """ 55 | ... 56 | 57 | ### Default values for generating a tripole: 58 | self.A:float = 96e-3 59 | self.B:float = -90e-3 60 | self.C:int = 1 61 | self.plot_length:int = 20 62 | self.scaling_factor:float = 1 63 | 64 | ### Default values for a single fibre: 65 | self.fibre_length:int = 210 66 | self.conduction_velocity:int = 4 67 | self.ratio_axial_radial_conductivity:int = 6 # 0.001 68 | self.radial_conductivity:float = 0.303 69 | self.inter_electrode_spacing:float = 5 # 20 mm 70 | self.number_of_electrodes_z:int = 1 71 | self.number_of_electrodes_x:int = 1 72 | self.electrode_shift:int = 165 # 75 z-axis 73 | self.initial_neuromuscular_junction:int = 90 74 | self.fibre_depth:float = 10 75 | self.x_position_single_fibre:float = 10 76 | self.extermination_zone_width:int = 10 77 | self.innervation_zone_width:int = 5 78 | self.time_length:int = 35 79 | 80 | ### Default values for a motor unit: 81 | self.motor_unit_radius:float = 1 82 | self.number_of_fibres:int = 100 83 | self.motor_unit_depth:float = 10 84 | self.motor_unit_x_position:float = 0 85 | 86 | ### Default values for plots: 87 | self.y_limit_minimum:float = -1 88 | self.y_limit_maximum:float = 1 89 | 90 | ######################### 1 ######################### Get Tripole Amplitude ########################## ####################### 91 | def get_tripole_amplitude(self): 92 | """Generates the tripole amplitude from the membrane current distribution source. 93 | 94 | Args: 95 | According to Modeling of Surface Myoelectric Signals, part I. Merletti, 1999. 96 | A: A suitable constant to fit the amplitude of the action potential expressed in V (96 mV = 96e-3). 97 | B: The resting membrane poteintial expressed in V (-90 mV = -90e-3). 98 | C: Muscle fibre (proportionality) constant (1). 99 | plot_length: Length of plotting the action potential and current distribution expressed in mm (20 mm). 100 | scaling_factor: A scale factor expressed in mm^-1, which is lambda (λ), (1 mm^-1). 101 | 102 | Returns: 103 | A numpy array containing the tripole amplitude of the current distribution. 104 | """ 105 | ... 106 | 107 | ### Default arguments: 108 | A = self.A 109 | B = self.B 110 | C = self.C 111 | plot_length = self.plot_length 112 | scaling_factor = self.scaling_factor 113 | 114 | ######################### Action Potential, Current Distribution ####################### 115 | ### Create the z-axis vector, which is the sampled vector of plotting the action potential and current distribution: 116 | delta_length = 0.1 # (10 kHz) total distance between samples 117 | z = np.arange(0, plot_length + delta_length, delta_length) 118 | self.z = z 119 | 120 | ### A mathematical description of the action potential 121 | action_potential = A * (scaling_factor * z)**3 * np.exp(-scaling_factor * z) - B # eq.1.1 Merletti part 1 122 | self.action_potential = action_potential 123 | 124 | ### Calculate the mebrane current distribution which is proportional to the second derivative of the action potential 125 | current_distribution = C * A * scaling_factor**2 * (scaling_factor * z) * (6 - 6 * scaling_factor * z + scaling_factor**2 * z**2) * np.exp(-scaling_factor * z) # eq. 1.2 Merletti part 1 126 | self.current_distribution = current_distribution 127 | 128 | ######################### Pole Amplitude ####################### 129 | ### Calculate the pole amplitude 130 | ## To calculate pole_one, pole_two, and pole_three 131 | # Discretize the current distribution to 1 and -1 132 | current_distribution_discrete = np.where(current_distribution > 0, 1, -1) 133 | 134 | # Calculate the absolute difference between each sample of discretized current distribution 135 | current_distribution_difference = np.abs(np.diff(current_distribution_discrete)) 136 | 137 | # Locate the absolute differences that are greater than zero 138 | pole_location_index = np.where(current_distribution_difference > 0)[0] 139 | self.pole_location_index = pole_location_index 140 | 141 | # Calculate the poles 142 | dz = z[1] - z[0] 143 | self.dz = dz 144 | 145 | ### Sum pole magnitude for each part of the tripole. 146 | pole_one = np.sum(current_distribution[:pole_location_index[1]]) * dz 147 | pole_two = np.sum(current_distribution[pole_location_index[1] + 1:pole_location_index[2]]) * dz 148 | pole_three = np.sum(current_distribution[pole_location_index[2] + 1:]) * dz 149 | 150 | ### Use rounding adjustment to set sum of all poles equal to zero. 151 | pole_rounding_adjustment = np.abs(pole_one + pole_two + pole_three) 152 | pole_one = pole_one + pole_rounding_adjustment 153 | pole_two = pole_two - pole_rounding_adjustment 154 | pole_three = pole_three + pole_rounding_adjustment 155 | 156 | ### Poles amplitude array. 157 | poles_amplitude = np.array([pole_one, pole_two, pole_three]) 158 | #print('Poles Amplitude =', poles_amplitude) 159 | 160 | return poles_amplitude 161 | 162 | ######################### 2 ######################### Get Pole Distance ########################## ####################### 163 | def get_tripole_distance(self): 164 | """Calculates the distance between the poles of the current distribution. 165 | 166 | Args: 167 | pole_location_index: A list of integers containing the indices of the poles in the current distribution. 168 | dz: The sampling interval of the current distribution. 169 | current_distribution 170 | 171 | Returns: 172 | A list of two floats containing the distances between the poles. 173 | """ 174 | ... 175 | 176 | ### Default arguments 177 | self.get_tripole_amplitude() 178 | pole_location_index = self.pole_location_index 179 | dz = self.dz 180 | current_distribution = self.current_distribution 181 | 182 | ### Calculate the distance between the poles 183 | """a, b represent tripole asymmetry 184 | a is the distance between pole 1 and pole 2. 185 | b is the distance between pole 1 and pole 3. 186 | The following rules of the current sources must hold: 187 | pole_one + pole_two + pole_three = 0 188 | pole_two*a + pole_three*b = 0 189 | """ 190 | 191 | ## Calculate the cumulative sum of each phase. 192 | pole_one_sum = np.cumsum(current_distribution[:pole_location_index[1]]) 193 | pole_two_sum = np.cumsum(current_distribution[pole_location_index[1]:pole_location_index[2]]) 194 | pole_three_sum = np.cumsum(current_distribution[pole_location_index[2]:]) 195 | 196 | ## Locate the location index (z-coordinate) at half the cumulative sum of each phase. 197 | pole_one_location = np.where(pole_one_sum > 0.5 * np.sum(current_distribution[:pole_location_index[1]]))[0] 198 | pole_two_location = pole_location_index[1] + np.where(pole_two_sum < 0.5 * np.sum(current_distribution[pole_location_index[1]:pole_location_index[2]]))[0] 199 | pole_three_location = pole_location_index[2] + np.where(pole_three_sum > 0.5 * np.sum(current_distribution[pole_location_index[2]:]))[0] 200 | 201 | ## Calculate the pole positions 202 | pole_one_position = pole_one_location[0] * dz 203 | pole_two_position = pole_two_location[0] * dz 204 | pole_three_position = pole_three_location[0] * dz 205 | 206 | ## Calculate the distance between the poles 207 | a = pole_two_position - pole_one_position 208 | b = pole_three_position - pole_one_position 209 | 210 | pole_distances = np.array([a, b]) 211 | #print('Pole Distances =', pole_distances) 212 | 213 | return pole_distances 214 | 215 | ######################### 3 ######################### Plot Current Distribution & Action Potential ########################## ####################### 216 | def plot_current_distribution_action_potential(self): 217 | """Plots the membrane current distribution and action potential. 218 | 219 | Args: 220 | current_distribution 221 | action_potential 222 | z: Sampled vector of plotting 223 | 224 | Returns: 225 | A plot of the current distribution and action potential. 226 | """ 227 | ... 228 | 229 | ### default arguments 230 | poles_amplitude = self.get_tripole_amplitude() 231 | pole_distances = self.get_tripole_distance() 232 | current_distribution = self.current_distribution 233 | action_potential = self.action_potential 234 | z = self.z 235 | pole_location_index = self.pole_location_index 236 | 237 | ######################### Normalization and Plot ####################### 238 | ### Plot the normalized current distribution and action potential 239 | # Normalize the signals using min-max feature scaling between points y_limit_minimum and y_limit_maximum. 240 | y_limit_minimum = self.y_limit_minimum 241 | y_limit_maximum = self.y_limit_maximum 242 | 243 | #normalized_current_distribution = y_limit_minimum + ((current_distribution - current_distribution.min())*(y_limit_maximum-y_limit_minimum)) / (current_distribution.max() - current_distribution.min()) 244 | #normalized_action_potential = y_limit_minimum + ((action_potential - action_potential.min())*(y_limit_maximum-y_limit_minimum)) / (action_potential.max() - action_potential.min()) 245 | 246 | fig1 = plt.figure(1) 247 | 248 | ax = plt.subplot(2,1,1) 249 | plt.plot(z, current_distribution*1000) 250 | plt.ylabel('Im, Current Distribution (mA)') 251 | #plt.axvline(z[np.argmax(current_distribution)], color = 'r', linestyle = '--', label = 'P1') 252 | #plt.axvline(z[np.argmin(current_distribution)], color = 'g', linestyle = '--', label = 'P2') 253 | #plt.legend() 254 | y_p1 = poles_amplitude[0]*1000 255 | y_p2 = poles_amplitude[1]*1000 256 | y_p3 = poles_amplitude[2]*1000 257 | 258 | p1_p2 = pole_distances[0] 259 | p1_P3 = pole_distances[1] 260 | 261 | x_p1 = z[pole_location_index[0]] 262 | x_p2 = z[pole_location_index[1]] 263 | x_p3 = z[pole_location_index[2]] 264 | 265 | p1_location = (x_p2 - x_p1) / 2 266 | p2_location = p1_location + p1_p2 267 | p3_location = p1_location + p1_P3 268 | 269 | plt.plot(p1_location, y_p1, 'o') 270 | plt.text(p1_location + 0.2, y_p1, 'P1') 271 | plt.vlines(x = p1_location, ymin = 0, ymax = y_p1, color = 'k') 272 | 273 | plt.plot(p2_location, y_p2, 'o') 274 | plt.text(p2_location + 0.2, y_p2, 'P2') 275 | plt.vlines(x = p2_location, ymin = 0, ymax = y_p2, color = 'k') 276 | 277 | plt.plot(p3_location, y_p3, 'o') 278 | plt.text(p3_location + 0.2, y_p3, 'P3') 279 | plt.vlines(x = p3_location, ymin = 0, ymax = y_p3, color = 'k') 280 | 281 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 282 | ax.xaxis.set_major_formatter(NullFormatter()) 283 | plt.title('Membrane Current Distribution', fontweight = 'bold') 284 | 285 | ax = plt.subplot(2,1,2) 286 | plt.plot(z, action_potential*1000-180) 287 | plt.ylabel('Vm, Action Potential (mV)') 288 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 289 | plt.title('Membrane Action Potential', fontweight = 'bold') 290 | 291 | fig1.supxlabel('z, Distance (mm)') 292 | 293 | A = self.A 294 | 295 | # Set the desired resolution 296 | plt.savefig('current_and_action_potential_{}mA_amplitude.png'.format(A*1000), dpi = 600) 297 | scipy.io.savemat('current' + '.mat', {'Im': current_distribution}) 298 | scipy.io.savemat('action_potential' + '.mat', {'Vm': action_potential}) 299 | 300 | return plt.show() 301 | 302 | ######################### 4 ######################### Simulate Fibre Action Potential ########################## ####################### 303 | def simulate_fibre_action_potential(self): 304 | """Simulates the action potentials recorded by all electrodes for one fibre. 305 | 306 | Args: 307 | fibre_length 308 | neuromuscular_junction 309 | conduction_velocity 310 | electrode_shift 311 | number_of_electrodes_z 312 | inter_electrode_spacing 313 | radial_conductivity 314 | ratio_axial_radial_conductivity 315 | fibre_depth 316 | 317 | Returns: 318 | A numpy array containing the simulated action potentials for electrodes positioned along the fibre. 319 | """ 320 | ... 321 | 322 | ### Choose default values for attributes 323 | fibre_length = self.fibre_length 324 | neuromuscular_junction = self.initial_neuromuscular_junction 325 | conduction_velocity = self.conduction_velocity 326 | electrode_shift = self.electrode_shift 327 | number_of_electrodes_z = self.number_of_electrodes_z 328 | number_of_electrodes_x = self.number_of_electrodes_x 329 | inter_electrode_spacing = self.inter_electrode_spacing 330 | radial_conductivity = self.radial_conductivity 331 | ratio_axial_radial_conductivity = self.ratio_axial_radial_conductivity 332 | extermination_zone_width = self.extermination_zone_width 333 | innervation_zone_width = self.innervation_zone_width 334 | fibre_depth = self.fibre_depth 335 | time_length = self.time_length 336 | x_position_single_fibre = self.x_position_single_fibre 337 | 338 | ### Simulation of poles moving along the fibre in time. Create time array. 339 | delta_time = 0.1 # (10 kHz) 340 | time_array = np.arange(0, time_length + delta_time, delta_time) 341 | self.time_array = time_array 342 | 343 | ### Create the current tripole with initial pole amplitude and positions 344 | tripole_amplitude = self.get_tripole_amplitude() 345 | tripole_distance = self.get_tripole_distance() 346 | 347 | a = tripole_distance[0] 348 | b = tripole_distance[1] 349 | pole_one = tripole_amplitude[0] 350 | pole_two = tripole_amplitude[1] 351 | pole_three = tripole_amplitude[2] 352 | 353 | # Array of mirrored tripole amplitudes 354 | P = np.array([pole_one, pole_two, pole_three, pole_three, pole_two, pole_one]).reshape(-1, 1) 355 | Pi = np.tile(P,(1,len(time_array))) 356 | 357 | ### Create uniformly distributed tendon ends at the extermination zones of each fibre and uniformly distributed neuromuscular junctions at the innervation zones. 358 | delta_length = 0.1 # (10 kHz) 359 | fibre_length_array = np.arange(0, fibre_length + delta_length, delta_length) 360 | 361 | ### Create random variation in the rightmost tendon ends. 362 | right_tendon_end_variation = fibre_length_array[-1] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 363 | 364 | ### Create variation in the leftmost tendon ends. 365 | left_tendon_end_variation = fibre_length_array[0] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 366 | 367 | ### Create small variation in the NMJ location. 368 | neuromuscular_junction = neuromuscular_junction + (innervation_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 369 | 370 | ### Force fibrelengths longer than the ordinary defined fibre length to be within the defined max fibre length. 371 | fibre_length = right_tendon_end_variation - left_tendon_end_variation 372 | 373 | ### Move poles with an initial offset, length of a tripole (b) 374 | initial_offset = b 375 | ## Initialise the locations of the poles at action potential initialisation 376 | location_pole_one = neuromuscular_junction - initial_offset + b # initial location of Pole 1 377 | location_pole_two = neuromuscular_junction - initial_offset - a + b # initial location of Pole 2 378 | location_pole_three = neuromuscular_junction - initial_offset # initial location of Pole 3 379 | location_pole_four = neuromuscular_junction + initial_offset # initial location of Pole 4 380 | location_pole_five = neuromuscular_junction + initial_offset + a - b # initial location of Pole 5 381 | location_pole_six = neuromuscular_junction + initial_offset - b # initial location of Pole 6 382 | 383 | ## Array of initial pole locations 384 | initial_pole_locations = (np.array(np.array([location_pole_one, location_pole_two, location_pole_three, location_pole_four, location_pole_five, location_pole_six]))[np.newaxis]).T 385 | 386 | ### Simulation of poles moving along the fibre in time. 387 | ## Move poles 1-3 in positive direction, with regards to conduction velocity. 388 | location_poles_right = np.array(initial_pole_locations[0:3] + conduction_velocity * time_array) 389 | # Set poles out of bounds to neuromuscular_junction 390 | location_poles_right[location_poles_right < neuromuscular_junction] = neuromuscular_junction 391 | 392 | ## Move poles 4-6 in negative direction, with regards to conduction velocity. 393 | location_poles_left = np.array(initial_pole_locations[3:6] - conduction_velocity * time_array) 394 | # Set poles out of bounds to neuromuscular_junction 395 | location_poles_left[location_poles_left > neuromuscular_junction] = neuromuscular_junction 396 | 397 | # Combine poles of both directions in one matrix 398 | location_poles_all = np.vstack((location_poles_right, location_poles_left)) 399 | 400 | ### Find and replace out of bounds locations at muscle fibre ends, both postive and negative end. Repalce out of bounds locations with fibre bound. 401 | # To get Merletti simulation 402 | #location_poles_all[location_poles_all < left_tendon_end_variation] = left_tendon_end_variation 403 | #location_poles_all[location_poles_all > fibre_length] = fibre_length 404 | 405 | ### Defining the detection system 406 | # Create a vector for the locations of number of electrodes along the fibre. 407 | electrode_locations_z = np.zeros(number_of_electrodes_z) 408 | # Create a spacing vector for the electrode locations, with number of electrodes. 409 | for i in range(number_of_electrodes_z): 410 | electrode_locations_z[i] = inter_electrode_spacing * i - ((inter_electrode_spacing * (number_of_electrodes_z - 1)) / 2) 411 | electrode_locations_z = electrode_locations_z + electrode_shift # Add interelectrode shift. 412 | 413 | electrode_locations_x = np.zeros(number_of_electrodes_x) 414 | # Create a spacing vector for the electrode locations, with number of electrodes. 415 | for j in range(number_of_electrodes_x): 416 | electrode_locations_x[j] = inter_electrode_spacing * j - ((inter_electrode_spacing * (number_of_electrodes_x - 1)) / 2) 417 | 418 | ### Create the single fibre action potential 419 | single_fibre_action_potential = np.zeros((number_of_electrodes_z, number_of_electrodes_x, len(time_array))) 420 | # Finding the potentials observed at each electrode. 421 | for z in range(number_of_electrodes_z): 422 | for x in range(number_of_electrodes_x): 423 | single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + np.exp(0.09*np.abs((electrode_locations_z[z]-neuromuscular_junction)))*(electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 424 | # To get Merletti simulation 425 | #single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + (electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 426 | single_fibre_action_potential = np.reshape(single_fibre_action_potential, (len(electrode_locations_z)*len(electrode_locations_x), len(time_array))) 427 | 428 | #print('Single Fibre Action Potential', single_fibre_action_potential) 429 | 430 | return -single_fibre_action_potential 431 | 432 | ######################### 5 ######################### Plot Fibre Action Potential ########################## ####################### 433 | def plot_fibre_action_potential(self): 434 | """Plots the single fibre action potentials computed in simulate_fibre_action_potential(). 435 | 436 | Args: 437 | 438 | single_fibre_action_potential 439 | time_array 440 | y_limit_minimum 441 | y_limit_maximum 442 | number_of_electrodes_z 443 | 444 | Returns: 445 | 446 | A plot of the single fibre action potential for all electrodes. 447 | 448 | """ 449 | ... 450 | ### Default arguments: 451 | single_fibre_action_potential = self.simulate_fibre_action_potential() 452 | time_array = self.time_array 453 | y_limit_minimum = self.y_limit_minimum 454 | y_limit_maximum = self.y_limit_maximum 455 | number_of_electrodes_z = self.number_of_electrodes_z 456 | number_of_electrodes_x = self.number_of_electrodes_x 457 | 458 | ### Plot the normalized single fibre action potential 459 | normalized_single_fibre_action_potential = single_fibre_action_potential 460 | normalized_single_fibre_action_potential = (normalized_single_fibre_action_potential - normalized_single_fibre_action_potential.mean()) / (normalized_single_fibre_action_potential.max() - normalized_single_fibre_action_potential.min()) 461 | 462 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 463 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 464 | zeros_array = np.zeros(len(array_size)) 465 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 466 | array_size_x = np.append(array_size_x,zeros_array) 467 | 468 | fig2 = plt.figure(2) 469 | for i in range(len(array_size)): 470 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 471 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 472 | ax.grid(which = 'both', ls = 'dashed') 473 | plt.plot(time_array, normalized_single_fibre_action_potential[i, :]) 474 | plt.xlim(time_array[0], time_array[-1] - 1) 475 | if i < len(array_size) - number_of_electrodes_x: 476 | ax.xaxis.set_major_formatter(NullFormatter()) 477 | plt.ylim(y_limit_minimum,y_limit_maximum) 478 | ax.yaxis.set_major_formatter(NullFormatter()) 479 | if i == 0: 480 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 481 | for j in range(i): 482 | if i == array_size_x[j]: 483 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 484 | elif number_of_electrodes_x == 1: 485 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 486 | 487 | plt.suptitle('Single Fibre Action Potential', fontsize = 20, fontweight = 'bold') 488 | fig2.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 489 | fig2.supylabel('Electrodes in the z direction, i.e. along the fibre', fontsize = 15) 490 | 491 | # Set the desired resolution 492 | plt.savefig('single_fibre_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 493 | scipy.io.savemat('single_fibre_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'SFAP': single_fibre_action_potential}) 494 | 495 | return plt.show() 496 | 497 | ######################### 6 ######################### Simulate Motor Unit Action Potential ########################## ####################### 498 | def simulate_motor_unit(self): 499 | """Simulates the summed action potentials from a number of fibres in a motor unit, recorded by all electrodes. 500 | 501 | Args: 502 | single_fibre_action_potential 503 | time_array 504 | number_of_electrodes_z 505 | number_of_fibres 506 | 507 | Returns: 508 | A numpy array containing the simulated motor unit action potentials for all electrodes positioned. 509 | """ 510 | ... 511 | 512 | ### Default arguments: 513 | self.simulate_fibre_action_potential() 514 | time_array = self.time_array 515 | motor_unit_radius = self.motor_unit_radius 516 | number_of_fibres = self.number_of_fibres 517 | motor_unit_depth = self.motor_unit_depth 518 | motor_unit_x_position = self.motor_unit_x_position 519 | 520 | ### Calcualte the fibre depth variation. 521 | ### Calculate the motor unit depth variation that use to calcualte the fibre depth variation 522 | theta_angle = 2 * np.pi * np.random.random(number_of_fibres) 523 | 524 | x_position_single_fibre = np.zeros(number_of_fibres) 525 | y_position_single_fibre = np.zeros(number_of_fibres) 526 | 527 | # calculating coordinates 528 | for i in range(number_of_fibres): 529 | radial_position_single_fibre = motor_unit_radius * np.random.random() 530 | x_position_single_fibre[i] = radial_position_single_fibre * np.cos(theta_angle[i]) 531 | y_position_single_fibre[i] = radial_position_single_fibre * np.sin(theta_angle[i]) 532 | 533 | ### Simulate the total action potential of a motor unit for the defined number of single fibres. 534 | motor_unit_matrix = None 535 | for i in range(number_of_fibres): 536 | self.fibre_depth = motor_unit_depth - y_position_single_fibre[i] 537 | self.fibre_x_position = motor_unit_x_position + x_position_single_fibre[i] 538 | 539 | single_fibre = self.simulate_fibre_action_potential() 540 | 541 | # Generate matrix for motor unit and add each fibre. 542 | if i == 0: 543 | motor_unit_matrix = single_fibre 544 | else: 545 | motor_unit_matrix = motor_unit_matrix + single_fibre 546 | 547 | motor_unit_matrix_with_time = np.vstack((time_array, motor_unit_matrix)) 548 | motor_unit_matrix = motor_unit_matrix_with_time[1:] 549 | 550 | #print('Motor Unit Action Potential', motor_unit_matrix) 551 | return motor_unit_matrix 552 | 553 | ######################### 7 ######################### Plot Motor Unit Action Potential ########################## ####################### 554 | def plot_motor_unit(self): 555 | """Plots the motor unit action potential from the summed number of single fibres computed in simulate_motor_unit(). 556 | 557 | Args: 558 | motor_unit_matrix 559 | time_array 560 | y_limit_minimum 561 | y_limit_maximum 562 | number_of_electrodes_z 563 | 564 | Returns: 565 | A plot of the motor unit action potential for all electrodes. 566 | """ 567 | ... 568 | ### Default arguments: 569 | motor_unit = self.simulate_motor_unit() 570 | time_array = self.time_array 571 | y_limit_minimum = self.y_limit_minimum 572 | y_limit_maximum = self.y_limit_maximum 573 | number_of_electrodes_z = self.number_of_electrodes_z 574 | number_of_electrodes_x = self.number_of_electrodes_x 575 | 576 | ### Plot the normalized motor unit action potential 577 | normalized_motor_unit = motor_unit 578 | normalized_motor_unit = (normalized_motor_unit - normalized_motor_unit.mean()) / (normalized_motor_unit.max() - normalized_motor_unit.min()) 579 | 580 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 581 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 582 | zeros_array = np.zeros(len(array_size)) 583 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 584 | array_size_x = np.append(array_size_x,zeros_array) 585 | 586 | fig3 = plt.figure(3) 587 | for i in range(len(array_size)): 588 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 589 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 590 | ax.grid(which = 'both', ls = 'dashed') 591 | plt.plot(time_array, normalized_motor_unit[i, :]) 592 | plt.xlim(time_array[0], time_array[-1] - 1) 593 | if i < len(array_size) - number_of_electrodes_x: 594 | ax.xaxis.set_major_formatter(NullFormatter()) 595 | ax.yaxis.set_major_formatter(NullFormatter()) 596 | if i == 0: 597 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 598 | for j in range(i): 599 | if i == array_size_x[j]: 600 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 601 | elif number_of_electrodes_x == 1: 602 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 603 | plt.ylim(y_limit_minimum,y_limit_maximum) 604 | plt.suptitle('Motor Unit Action Potential', fontsize = 20, fontweight = 'bold') 605 | if number_of_electrodes_x > 1: 606 | fig3.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 607 | else: 608 | fig3.supxlabel('Time (ms)', fontsize = 15) 609 | if number_of_electrodes_z > 1: 610 | fig3.supylabel('Motor Unit Action Potential\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center', fontsize = 15) 611 | else: 612 | fig3.supylabel('Motor Unit Action Potential', ha = 'center', va = 'center', fontsize = 15) 613 | 614 | # Set the desired resolution 615 | plt.savefig('one_motor_unit_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 616 | scipy.io.savemat('one_motor_unit_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'MUAP': motor_unit}) 617 | 618 | return plt.show() 619 | 620 | # ######################### ######################### ########################## ####################### 621 | # THE END # 622 | # ######################### ######################### ########################## ####################### -------------------------------------------------------------------------------- /Example_Code_Figures/sEMG_Signal_Noise/MU.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib.ticker import NullFormatter 4 | import scipy.io 5 | plt.rcParams['font.family'] = 'Times New Roman' 6 | 7 | 8 | 9 | class MotorUnit: 10 | """A class that represents a motor unit (MU). 11 | 12 | Methods: 13 | get_tripole_amplitude: Calculates the action potential and the current distribution. Returns the tripole amplitudes of the current distribution. 14 | plot_current_distribution_action_potential: Plots the normalized action potential and current distribution based on the given parameter values. 15 | get_tripole_distance: Calculates the distance between the poles of the current distribution based on returned value from get_tripole_amplitude. 16 | simulate_fibre_action_potential: Simulates the action potential recorded by a given number of electrodes, positioned along one fibre, for the given current distriubtion. 17 | plot_fibre_action_potential: Plots the normalized action potential for one fibre over the electrodes positioned along the fibre. 18 | simulate_motor_unit: Simulates the action potential for a given number of fibres, for electrodes positioned along the motor unit. 19 | plot_motor_unit: Plots the normalized action potential simulated for one motor unit, for a given number of fibres, and electrodes positioned along the motor unit. 20 | 21 | Attributes: 22 | For get_tripole_amplitude: 23 | A: Constant to fit the amplitude of the tripole (V). 24 | B: Resting membrane potential amplitude (V). 25 | C: Muscle fibre proportionality constant. 26 | plot_length: Length for plotting the action potential and current distribution expressed in mm (20 mm). 27 | scaling_factor: A scaling factor expressed in mm^-1, lambda (λ). 28 | 29 | For simulate_fibre_action_potential: 30 | fibre_length: Length of fibre (mm). 31 | neuromuscular_junction: Position of NMJ along the length of the fibre (mm). 32 | conduction_velocity: Conduction velocity along the muscle fibre (m/s). 33 | electrode_shift: Position of center of electrode array along the length of the fibre (mm). [electrode_shift = neuromuscular_junction means the array sits centered above NMJ.] 34 | number_of_electrodes_z: Number of elecrodes in the array, in the direction of the fibre. 35 | number_of_electrodes_x: Number of electrodes in the array across the fibre. 36 | inter_electrode_spacing: Distance between electrodes in the array (mm). 37 | radial_conductivity: Conductivity in radial direction, across the fibre (m/s). 38 | ratio_axial_radial_conductivity: Ratio between axial and radial conductivity, (fibre direction conductivity / radial conductivity) 39 | fibre_depth: Initial depth from the surface EMG down to the fibre (mm). 40 | 41 | For simulate_motor_unit: 42 | motor_unit_radius: Radius of the motor unit (mm). 43 | number_of_fibres: The number of fibres in a given motor unit. 44 | 45 | For plotting methods: 46 | y_limit_minimum: Minimum value of plot y-axis. 47 | y_limit_maximum: Maximum value of plot y-axis. 48 | """ 49 | ... 50 | 51 | def __init__(self): 52 | """Initializes a new Motor Unit object. 53 | 54 | """ 55 | ... 56 | 57 | ### Default values for generating a tripole: 58 | self.A:float = 96e-3 59 | self.B:float = -90e-3 60 | self.C:int = 1 61 | self.plot_length:int = 20 62 | self.scaling_factor:float = 1 63 | 64 | ### Default values for a single fibre: 65 | self.fibre_length:int = 210 66 | self.conduction_velocity:int = 4 67 | self.ratio_axial_radial_conductivity:int = 6 # 0.001 68 | self.radial_conductivity:float = 0.303 69 | self.inter_electrode_spacing:float = 10 # 20 mm 70 | self.number_of_electrodes_z:int = 1 71 | self.number_of_electrodes_x:int = 1 72 | self.electrode_shift:int = 165 # 75 z-axis 73 | self.initial_neuromuscular_junction:int = 90 74 | self.fibre_depth:float = 10 75 | self.x_position_single_fibre:float = 10 76 | self.extermination_zone_width:int = 10 77 | self.innervation_zone_width:int = 5 78 | self.time_length:int = 35 79 | 80 | ### Default values for a motor unit: 81 | self.motor_unit_radius:float = 1 82 | self.number_of_fibres:int = 100 83 | self.motor_unit_depth:float = 10 84 | self.motor_unit_x_position:float = 0 85 | 86 | ### Default values for plots: 87 | self.y_limit_minimum:float = -1 88 | self.y_limit_maximum:float = 1 89 | 90 | ######################### 1 ######################### Get Tripole Amplitude ########################## ####################### 91 | def get_tripole_amplitude(self): 92 | """Generates the tripole amplitude from the membrane current distribution source. 93 | 94 | Args: 95 | According to Modeling of Surface Myoelectric Signals, part I. Merletti, 1999. 96 | A: A suitable constant to fit the amplitude of the action potential expressed in V (96 mV = 96e-3). 97 | B: The resting membrane poteintial expressed in V (-90 mV = -90e-3). 98 | C: Muscle fibre (proportionality) constant (1). 99 | plot_length: Length of plotting the action potential and current distribution expressed in mm (20 mm). 100 | scaling_factor: A scale factor expressed in mm^-1, which is lambda (λ), (1 mm^-1). 101 | 102 | Returns: 103 | A numpy array containing the tripole amplitude of the current distribution. 104 | """ 105 | ... 106 | 107 | ### Default arguments: 108 | A = self.A 109 | B = self.B 110 | C = self.C 111 | plot_length = self.plot_length 112 | scaling_factor = self.scaling_factor 113 | 114 | ######################### Action Potential, Current Distribution ####################### 115 | ### Create the z-axis vector, which is the sampled vector of plotting the action potential and current distribution: 116 | delta_length = 0.1 # (10 kHz) total distance between samples 117 | z = np.arange(0, plot_length + delta_length, delta_length) 118 | self.z = z 119 | 120 | ### A mathematical description of the action potential 121 | action_potential = A * (scaling_factor * z)**3 * np.exp(-scaling_factor * z) - B # eq.1.1 Merletti part 1 122 | self.action_potential = action_potential 123 | 124 | ### Calculate the mebrane current distribution which is proportional to the second derivative of the action potential 125 | current_distribution = C * A * scaling_factor**2 * (scaling_factor * z) * (6 - 6 * scaling_factor * z + scaling_factor**2 * z**2) * np.exp(-scaling_factor * z) # eq. 1.2 Merletti part 1 126 | self.current_distribution = current_distribution 127 | 128 | ######################### Pole Amplitude ####################### 129 | ### Calculate the pole amplitude 130 | ## To calculate pole_one, pole_two, and pole_three 131 | # Discretize the current distribution to 1 and -1 132 | current_distribution_discrete = np.where(current_distribution > 0, 1, -1) 133 | 134 | # Calculate the absolute difference between each sample of discretized current distribution 135 | current_distribution_difference = np.abs(np.diff(current_distribution_discrete)) 136 | 137 | # Locate the absolute differences that are greater than zero 138 | pole_location_index = np.where(current_distribution_difference > 0)[0] 139 | self.pole_location_index = pole_location_index 140 | 141 | # Calculate the poles 142 | dz = z[1] - z[0] 143 | self.dz = dz 144 | 145 | ### Sum pole magnitude for each part of the tripole. 146 | pole_one = np.sum(current_distribution[:pole_location_index[1]]) * dz 147 | pole_two = np.sum(current_distribution[pole_location_index[1] + 1:pole_location_index[2]]) * dz 148 | pole_three = np.sum(current_distribution[pole_location_index[2] + 1:]) * dz 149 | 150 | ### Use rounding adjustment to set sum of all poles equal to zero. 151 | pole_rounding_adjustment = np.abs(pole_one + pole_two + pole_three) 152 | pole_one = pole_one + pole_rounding_adjustment 153 | pole_two = pole_two - pole_rounding_adjustment 154 | pole_three = pole_three + pole_rounding_adjustment 155 | 156 | ### Poles amplitude array. 157 | poles_amplitude = np.array([pole_one, pole_two, pole_three]) 158 | #print('Poles Amplitude =', poles_amplitude) 159 | 160 | return poles_amplitude 161 | 162 | ######################### 2 ######################### Get Pole Distance ########################## ####################### 163 | def get_tripole_distance(self): 164 | """Calculates the distance between the poles of the current distribution. 165 | 166 | Args: 167 | pole_location_index: A list of integers containing the indices of the poles in the current distribution. 168 | dz: The sampling interval of the current distribution. 169 | current_distribution 170 | 171 | Returns: 172 | A list of two floats containing the distances between the poles. 173 | """ 174 | ... 175 | 176 | ### Default arguments 177 | self.get_tripole_amplitude() 178 | pole_location_index = self.pole_location_index 179 | dz = self.dz 180 | current_distribution = self.current_distribution 181 | 182 | ### Calculate the distance between the poles 183 | """a, b represent tripole asymmetry 184 | a is the distance between pole 1 and pole 2. 185 | b is the distance between pole 1 and pole 3. 186 | The following rules of the current sources must hold: 187 | pole_one + pole_two + pole_three = 0 188 | pole_two*a + pole_three*b = 0 189 | """ 190 | 191 | ## Calculate the cumulative sum of each phase. 192 | pole_one_sum = np.cumsum(current_distribution[:pole_location_index[1]]) 193 | pole_two_sum = np.cumsum(current_distribution[pole_location_index[1]:pole_location_index[2]]) 194 | pole_three_sum = np.cumsum(current_distribution[pole_location_index[2]:]) 195 | 196 | ## Locate the location index (z-coordinate) at half the cumulative sum of each phase. 197 | pole_one_location = np.where(pole_one_sum > 0.5 * np.sum(current_distribution[:pole_location_index[1]]))[0] 198 | pole_two_location = pole_location_index[1] + np.where(pole_two_sum < 0.5 * np.sum(current_distribution[pole_location_index[1]:pole_location_index[2]]))[0] 199 | pole_three_location = pole_location_index[2] + np.where(pole_three_sum > 0.5 * np.sum(current_distribution[pole_location_index[2]:]))[0] 200 | 201 | ## Calculate the pole positions 202 | pole_one_position = pole_one_location[0] * dz 203 | pole_two_position = pole_two_location[0] * dz 204 | pole_three_position = pole_three_location[0] * dz 205 | 206 | ## Calculate the distance between the poles 207 | a = pole_two_position - pole_one_position 208 | b = pole_three_position - pole_one_position 209 | 210 | pole_distances = np.array([a, b]) 211 | #print('Pole Distances =', pole_distances) 212 | 213 | return pole_distances 214 | 215 | ######################### 3 ######################### Plot Current Distribution & Action Potential ########################## ####################### 216 | def plot_current_distribution_action_potential(self): 217 | """Plots the membrane current distribution and action potential. 218 | 219 | Args: 220 | current_distribution 221 | action_potential 222 | z: Sampled vector of plotting 223 | 224 | Returns: 225 | A plot of the current distribution and action potential. 226 | """ 227 | ... 228 | 229 | ### default arguments 230 | poles_amplitude = self.get_tripole_amplitude() 231 | pole_distances = self.get_tripole_distance() 232 | current_distribution = self.current_distribution 233 | action_potential = self.action_potential 234 | z = self.z 235 | pole_location_index = self.pole_location_index 236 | 237 | ######################### Normalization and Plot ####################### 238 | ### Plot the normalized current distribution and action potential 239 | # Normalize the signals using min-max feature scaling between points y_limit_minimum and y_limit_maximum. 240 | y_limit_minimum = self.y_limit_minimum 241 | y_limit_maximum = self.y_limit_maximum 242 | 243 | #normalized_current_distribution = y_limit_minimum + ((current_distribution - current_distribution.min())*(y_limit_maximum-y_limit_minimum)) / (current_distribution.max() - current_distribution.min()) 244 | #normalized_action_potential = y_limit_minimum + ((action_potential - action_potential.min())*(y_limit_maximum-y_limit_minimum)) / (action_potential.max() - action_potential.min()) 245 | 246 | fig1 = plt.figure(1) 247 | 248 | ax = plt.subplot(2,1,1) 249 | plt.plot(z, current_distribution*1000) 250 | plt.ylabel('Im, Current Distribution (mA)') 251 | #plt.axvline(z[np.argmax(current_distribution)], color = 'r', linestyle = '--', label = 'P1') 252 | #plt.axvline(z[np.argmin(current_distribution)], color = 'g', linestyle = '--', label = 'P2') 253 | #plt.legend() 254 | y_p1 = poles_amplitude[0]*1000 255 | y_p2 = poles_amplitude[1]*1000 256 | y_p3 = poles_amplitude[2]*1000 257 | 258 | p1_p2 = pole_distances[0] 259 | p1_P3 = pole_distances[1] 260 | 261 | x_p1 = z[pole_location_index[0]] 262 | x_p2 = z[pole_location_index[1]] 263 | x_p3 = z[pole_location_index[2]] 264 | 265 | p1_location = (x_p2 - x_p1) / 2 266 | p2_location = p1_location + p1_p2 267 | p3_location = p1_location + p1_P3 268 | 269 | plt.plot(p1_location, y_p1, 'o') 270 | plt.text(p1_location + 0.2, y_p1, 'P1') 271 | plt.vlines(x = p1_location, ymin = 0, ymax = y_p1, color = 'k') 272 | 273 | plt.plot(p2_location, y_p2, 'o') 274 | plt.text(p2_location + 0.2, y_p2, 'P2') 275 | plt.vlines(x = p2_location, ymin = 0, ymax = y_p2, color = 'k') 276 | 277 | plt.plot(p3_location, y_p3, 'o') 278 | plt.text(p3_location + 0.2, y_p3, 'P3') 279 | plt.vlines(x = p3_location, ymin = 0, ymax = y_p3, color = 'k') 280 | 281 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 282 | ax.xaxis.set_major_formatter(NullFormatter()) 283 | plt.title('Membrane Current Distribution', fontweight = 'bold') 284 | 285 | ax = plt.subplot(2,1,2) 286 | plt.plot(z, action_potential*1000-180) 287 | plt.ylabel('Vm, Action Potential (mV)') 288 | #plt.yticks([y_limit_minimum, 0 , y_limit_maximum]) 289 | plt.title('Membrane Action Potential', fontweight = 'bold') 290 | 291 | fig1.supxlabel('z, Distance (mm)') 292 | 293 | A = self.A 294 | 295 | # Set the desired resolution 296 | plt.savefig('current_and_action_potential_{}mA_amplitude.png'.format(A*1000), dpi = 600) 297 | scipy.io.savemat('current' + '.mat', {'Im': current_distribution}) 298 | scipy.io.savemat('action_potential' + '.mat', {'Vm': action_potential}) 299 | 300 | return plt.show() 301 | 302 | ######################### 4 ######################### Simulate Fibre Action Potential ########################## ####################### 303 | def simulate_fibre_action_potential(self): 304 | """Simulates the action potentials recorded by all electrodes for one fibre. 305 | 306 | Args: 307 | fibre_length 308 | neuromuscular_junction 309 | conduction_velocity 310 | electrode_shift 311 | number_of_electrodes_z 312 | inter_electrode_spacing 313 | radial_conductivity 314 | ratio_axial_radial_conductivity 315 | fibre_depth 316 | 317 | Returns: 318 | A numpy array containing the simulated action potentials for electrodes positioned along the fibre. 319 | """ 320 | ... 321 | 322 | ### Choose default values for attributes 323 | fibre_length = self.fibre_length 324 | neuromuscular_junction = self.initial_neuromuscular_junction 325 | conduction_velocity = self.conduction_velocity 326 | electrode_shift = self.electrode_shift 327 | number_of_electrodes_z = self.number_of_electrodes_z 328 | number_of_electrodes_x = self.number_of_electrodes_x 329 | inter_electrode_spacing = self.inter_electrode_spacing 330 | radial_conductivity = self.radial_conductivity 331 | ratio_axial_radial_conductivity = self.ratio_axial_radial_conductivity 332 | extermination_zone_width = self.extermination_zone_width 333 | innervation_zone_width = self.innervation_zone_width 334 | fibre_depth = self.fibre_depth 335 | time_length = self.time_length 336 | x_position_single_fibre = self.x_position_single_fibre 337 | 338 | ### Simulation of poles moving along the fibre in time. Create time array. 339 | delta_time = 0.1 # (10 kHz) 340 | time_array = np.arange(0, time_length + delta_time, delta_time) 341 | self.time_array = time_array 342 | 343 | ### Create the current tripole with initial pole amplitude and positions 344 | tripole_amplitude = self.get_tripole_amplitude() 345 | tripole_distance = self.get_tripole_distance() 346 | 347 | a = tripole_distance[0] 348 | b = tripole_distance[1] 349 | pole_one = tripole_amplitude[0] 350 | pole_two = tripole_amplitude[1] 351 | pole_three = tripole_amplitude[2] 352 | 353 | # Array of mirrored tripole amplitudes 354 | P = np.array([pole_one, pole_two, pole_three, pole_three, pole_two, pole_one]).reshape(-1, 1) 355 | Pi = np.tile(P,(1,len(time_array))) 356 | 357 | ### Create uniformly distributed tendon ends at the extermination zones of each fibre and uniformly distributed neuromuscular junctions at the innervation zones. 358 | delta_length = 0.1 # (10 kHz) 359 | fibre_length_array = np.arange(0, fibre_length + delta_length, delta_length) 360 | 361 | ### Create random variation in the rightmost tendon ends. 362 | right_tendon_end_variation = fibre_length_array[-1] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 363 | 364 | ### Create variation in the leftmost tendon ends. 365 | left_tendon_end_variation = fibre_length_array[0] + (extermination_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 366 | 367 | ### Create small variation in the NMJ location. 368 | neuromuscular_junction = neuromuscular_junction + (innervation_zone_width/2 * np.random.rand()) * ((np.random.randint(0,2)*2) - 1) 369 | 370 | ### Force fibrelengths longer than the ordinary defined fibre length to be within the defined max fibre length. 371 | fibre_length = right_tendon_end_variation - left_tendon_end_variation 372 | 373 | ### Move poles with an initial offset, length of a tripole (b) 374 | initial_offset = b 375 | ## Initialise the locations of the poles at action potential initialisation 376 | location_pole_one = neuromuscular_junction - initial_offset + b # initial location of Pole 1 377 | location_pole_two = neuromuscular_junction - initial_offset - a + b # initial location of Pole 2 378 | location_pole_three = neuromuscular_junction - initial_offset # initial location of Pole 3 379 | location_pole_four = neuromuscular_junction + initial_offset # initial location of Pole 4 380 | location_pole_five = neuromuscular_junction + initial_offset + a - b # initial location of Pole 5 381 | location_pole_six = neuromuscular_junction + initial_offset - b # initial location of Pole 6 382 | 383 | ## Array of initial pole locations 384 | initial_pole_locations = (np.array(np.array([location_pole_one, location_pole_two, location_pole_three, location_pole_four, location_pole_five, location_pole_six]))[np.newaxis]).T 385 | 386 | ### Simulation of poles moving along the fibre in time. 387 | ## Move poles 1-3 in positive direction, with regards to conduction velocity. 388 | location_poles_right = np.array(initial_pole_locations[0:3] + conduction_velocity * time_array) 389 | # Set poles out of bounds to neuromuscular_junction 390 | location_poles_right[location_poles_right < neuromuscular_junction] = neuromuscular_junction 391 | 392 | ## Move poles 4-6 in negative direction, with regards to conduction velocity. 393 | location_poles_left = np.array(initial_pole_locations[3:6] - conduction_velocity * time_array) 394 | # Set poles out of bounds to neuromuscular_junction 395 | location_poles_left[location_poles_left > neuromuscular_junction] = neuromuscular_junction 396 | 397 | # Combine poles of both directions in one matrix 398 | location_poles_all = np.vstack((location_poles_right, location_poles_left)) 399 | 400 | ### Find and replace out of bounds locations at muscle fibre ends, both postive and negative end. Repalce out of bounds locations with fibre bound. 401 | # To get Merletti simulation 402 | #location_poles_all[location_poles_all < left_tendon_end_variation] = left_tendon_end_variation 403 | #location_poles_all[location_poles_all > fibre_length] = fibre_length 404 | 405 | ### Defining the detection system 406 | # Create a vector for the locations of number of electrodes along the fibre. 407 | electrode_locations_z = np.zeros(number_of_electrodes_z) 408 | # Create a spacing vector for the electrode locations, with number of electrodes. 409 | for i in range(number_of_electrodes_z): 410 | electrode_locations_z[i] = inter_electrode_spacing * i - ((inter_electrode_spacing * (number_of_electrodes_z - 1)) / 2) 411 | electrode_locations_z = electrode_locations_z + electrode_shift # Add interelectrode shift. 412 | 413 | electrode_locations_x = np.zeros(number_of_electrodes_x) 414 | # Create a spacing vector for the electrode locations, with number of electrodes. 415 | for j in range(number_of_electrodes_x): 416 | electrode_locations_x[j] = inter_electrode_spacing * j - ((inter_electrode_spacing * (number_of_electrodes_x - 1)) / 2) 417 | 418 | ### Create the single fibre action potential 419 | single_fibre_action_potential = np.zeros((number_of_electrodes_z, number_of_electrodes_x, len(time_array))) 420 | # Finding the potentials observed at each electrode. 421 | for z in range(number_of_electrodes_z): 422 | for x in range(number_of_electrodes_x): 423 | single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + np.exp(0.09*np.abs((electrode_locations_z[z]-neuromuscular_junction)))*(electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 424 | # To get Merletti simulation 425 | #single_fibre_action_potential[z, x, :] = (1/(2*np.pi * radial_conductivity) * np.sum((Pi / (np.sqrt(((electrode_locations_x[x] - x_position_single_fibre)**2 + fibre_depth**2) * ratio_axial_radial_conductivity + (electrode_locations_z[z] - location_poles_all)**2))), axis=0)) 426 | single_fibre_action_potential = np.reshape(single_fibre_action_potential, (len(electrode_locations_z)*len(electrode_locations_x), len(time_array))) 427 | 428 | #print('Single Fibre Action Potential', single_fibre_action_potential) 429 | 430 | return -single_fibre_action_potential 431 | 432 | ######################### 5 ######################### Plot Fibre Action Potential ########################## ####################### 433 | def plot_fibre_action_potential(self): 434 | """Plots the single fibre action potentials computed in simulate_fibre_action_potential(). 435 | 436 | Args: 437 | 438 | single_fibre_action_potential 439 | time_array 440 | y_limit_minimum 441 | y_limit_maximum 442 | number_of_electrodes_z 443 | 444 | Returns: 445 | 446 | A plot of the single fibre action potential for all electrodes. 447 | 448 | """ 449 | ... 450 | ### Default arguments: 451 | single_fibre_action_potential = self.simulate_fibre_action_potential() 452 | time_array = self.time_array 453 | y_limit_minimum = self.y_limit_minimum 454 | y_limit_maximum = self.y_limit_maximum 455 | number_of_electrodes_z = self.number_of_electrodes_z 456 | number_of_electrodes_x = self.number_of_electrodes_x 457 | 458 | ### Plot the normalized single fibre action potential 459 | normalized_single_fibre_action_potential = single_fibre_action_potential 460 | normalized_single_fibre_action_potential = (normalized_single_fibre_action_potential - normalized_single_fibre_action_potential.mean()) / (normalized_single_fibre_action_potential.max() - normalized_single_fibre_action_potential.min()) 461 | 462 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 463 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 464 | zeros_array = np.zeros(len(array_size)) 465 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 466 | array_size_x = np.append(array_size_x,zeros_array) 467 | 468 | fig2 = plt.figure(2) 469 | for i in range(len(array_size)): 470 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 471 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 472 | ax.grid(which = 'both', ls = 'dashed') 473 | plt.plot(time_array, normalized_single_fibre_action_potential[i, :]) 474 | plt.xlim(time_array[0], time_array[-1] - 1) 475 | if i < len(array_size) - number_of_electrodes_x: 476 | ax.xaxis.set_major_formatter(NullFormatter()) 477 | plt.ylim(y_limit_minimum,y_limit_maximum) 478 | ax.yaxis.set_major_formatter(NullFormatter()) 479 | if i == 0: 480 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 481 | for j in range(i): 482 | if i == array_size_x[j]: 483 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 484 | elif number_of_electrodes_x == 1: 485 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 486 | 487 | plt.suptitle('Single Fibre Action Potential', fontsize = 20, fontweight = 'bold') 488 | fig2.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 489 | fig2.supylabel('Electrodes in the z direction, i.e. along the fibre', fontsize = 15) 490 | 491 | # Set the desired resolution 492 | plt.savefig('single_fibre_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 493 | scipy.io.savemat('single_fibre_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'SFAP': single_fibre_action_potential}) 494 | 495 | return plt.show() 496 | 497 | ######################### 6 ######################### Simulate Motor Unit Action Potential ########################## ####################### 498 | def simulate_motor_unit(self): 499 | """Simulates the summed action potentials from a number of fibres in a motor unit, recorded by all electrodes. 500 | 501 | Args: 502 | single_fibre_action_potential 503 | time_array 504 | number_of_electrodes_z 505 | number_of_fibres 506 | 507 | Returns: 508 | A numpy array containing the simulated motor unit action potentials for all electrodes positioned. 509 | """ 510 | ... 511 | 512 | ### Default arguments: 513 | self.simulate_fibre_action_potential() 514 | time_array = self.time_array 515 | motor_unit_radius = self.motor_unit_radius 516 | number_of_fibres = self.number_of_fibres 517 | motor_unit_depth = self.motor_unit_depth 518 | motor_unit_x_position = self.motor_unit_x_position 519 | 520 | ### Calcualte the fibre depth variation. 521 | ### Calculate the motor unit depth variation that use to calcualte the fibre depth variation 522 | theta_angle = 2 * np.pi * np.random.random(number_of_fibres) 523 | 524 | x_position_single_fibre = np.zeros(number_of_fibres) 525 | y_position_single_fibre = np.zeros(number_of_fibres) 526 | 527 | # calculating coordinates 528 | for i in range(number_of_fibres): 529 | radial_position_single_fibre = motor_unit_radius * np.random.random() 530 | x_position_single_fibre[i] = radial_position_single_fibre * np.cos(theta_angle[i]) 531 | y_position_single_fibre[i] = radial_position_single_fibre * np.sin(theta_angle[i]) 532 | 533 | ### Simulate the total action potential of a motor unit for the defined number of single fibres. 534 | motor_unit_matrix = None 535 | for i in range(number_of_fibres): 536 | self.fibre_depth = motor_unit_depth - y_position_single_fibre[i] 537 | self.fibre_x_position = motor_unit_x_position + x_position_single_fibre[i] 538 | 539 | single_fibre = self.simulate_fibre_action_potential() 540 | 541 | # Generate matrix for motor unit and add each fibre. 542 | if i == 0: 543 | motor_unit_matrix = single_fibre 544 | else: 545 | motor_unit_matrix = motor_unit_matrix + single_fibre 546 | 547 | motor_unit_matrix_with_time = np.vstack((time_array, motor_unit_matrix)) 548 | motor_unit_matrix = motor_unit_matrix_with_time[1:] 549 | 550 | #print('Motor Unit Action Potential', motor_unit_matrix) 551 | return motor_unit_matrix 552 | 553 | ######################### 7 ######################### Plot Motor Unit Action Potential ########################## ####################### 554 | def plot_motor_unit(self): 555 | """Plots the motor unit action potential from the summed number of single fibres computed in simulate_motor_unit(). 556 | 557 | Args: 558 | motor_unit_matrix 559 | time_array 560 | y_limit_minimum 561 | y_limit_maximum 562 | number_of_electrodes_z 563 | 564 | Returns: 565 | A plot of the motor unit action potential for all electrodes. 566 | """ 567 | ... 568 | ### Default arguments: 569 | motor_unit = self.simulate_motor_unit() 570 | time_array = self.time_array 571 | y_limit_minimum = self.y_limit_minimum 572 | y_limit_maximum = self.y_limit_maximum 573 | number_of_electrodes_z = self.number_of_electrodes_z 574 | number_of_electrodes_x = self.number_of_electrodes_x 575 | 576 | ### Plot the normalized motor unit action potential 577 | normalized_motor_unit = motor_unit 578 | normalized_motor_unit = (normalized_motor_unit - normalized_motor_unit.mean()) / (normalized_motor_unit.max() - normalized_motor_unit.min()) 579 | 580 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 581 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 582 | zeros_array = np.zeros(len(array_size)) 583 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 584 | array_size_x = np.append(array_size_x,zeros_array) 585 | 586 | fig3 = plt.figure(3) 587 | for i in range(len(array_size)): 588 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 589 | plt.subplots_adjust(wspace=0.0, hspace=0.0) 590 | ax.grid(which = 'both', ls = 'dashed') 591 | plt.plot(time_array, normalized_motor_unit[i, :]) 592 | plt.xlim(time_array[0], time_array[-1] - 1) 593 | if i < len(array_size) - number_of_electrodes_x: 594 | ax.xaxis.set_major_formatter(NullFormatter()) 595 | ax.yaxis.set_major_formatter(NullFormatter()) 596 | if i == 0: 597 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 598 | for j in range(i): 599 | if i == array_size_x[j]: 600 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 601 | elif number_of_electrodes_x == 1: 602 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 603 | plt.ylim(y_limit_minimum,y_limit_maximum) 604 | plt.suptitle('Motor Unit Action Potential', fontsize = 20, fontweight = 'bold') 605 | if number_of_electrodes_x > 1: 606 | fig3.supxlabel('Time (ms)\n Electrodes in the x direction, i.e. vertically across the fibre', fontsize = 15) 607 | else: 608 | fig3.supxlabel('Time (ms)', fontsize = 15) 609 | if number_of_electrodes_z > 1: 610 | fig3.supylabel('Motor Unit Action Potential\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center', fontsize = 15) 611 | else: 612 | fig3.supylabel('Motor Unit Action Potential', ha = 'center', va = 'center', fontsize = 15) 613 | 614 | # Set the desired resolution 615 | plt.savefig('one_motor_unit_action_potential_{}X{}.png'.format(number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 616 | scipy.io.savemat('one_motor_unit_action_potential_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'MUAP': motor_unit}) 617 | 618 | return plt.show() 619 | 620 | # ######################### ######################### ########################## ####################### 621 | # THE END # 622 | # ######################### ######################### ########################## ####################### -------------------------------------------------------------------------------- /semg_sim/sEMG.py: -------------------------------------------------------------------------------- 1 | from MU import MotorUnit 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import random 5 | import scipy.io 6 | from matplotlib.ticker import NullFormatter 7 | plt.rcParams['font.family'] = 'Times New Roman' 8 | 9 | class SurfaceEMG: 10 | """A class that erepresnts a surface EMG (sEMG). 11 | 12 | Methods: 13 | simulate_recruitment_model(): Simulates the motor unit recruitment with firing patterns on increasing the number of muscle fibres per motor unit. 14 | plot_recruitment_model(): Plots the firing patterns of the motor unit recruitment. 15 | simulate_surface_emg(): Simulates the surface EMG signal generated by the motor unit recruitment. 16 | plot_surface_emg_array(): Plots the surface EMG signal generated by the summation over the motor unit recruitment. 17 | 18 | Attributes: 19 | For simulate_recruitment_model: 20 | simulation_time: Total simulation time in seconds. 21 | sampling_rate: Sample rate in Hz. 22 | ramp: Up-stable-down in seconds. 23 | maximum_excitation_level: Percentage % of max exc. 24 | number_of_motor_units: Number of motoneurons in the pool. 25 | recruitment_range: Range of recruitment threshold values. 26 | excitatory_gain: Gain of the excitatory drive-firing rate relationship. 27 | minimum_firing_rate: Minimum firing rate (Hz). 28 | peak_firing_rate_first_unit: Peak firing rate of the first motoneuron (Hz). 29 | peak_firing_rate_difference: Desired difference in peak firing rates between the first and last units (Hz): 30 | inter_spike_interval_coefficient_variation: The inter spike interval variance coefficient. 31 | 32 | For simulate_surface_emg: 33 | twitch_force_range: The range of twitch forces RP (force units). 34 | motor_unit_density: The motor-unit fibre density (20 unit fibres/mm^2 area of muscle). 35 | smallest_motor_unit_number_of_fibres: The smallest motor unit innervated 28 fibres. 36 | largest_motor_unit_number_of_fibres: The largest motor unit innervated 2728 fibres. 37 | muscle_fibre_diameter: The muscle-fibre diameter (46 µm). 38 | muscle_cross_sectional_diameter: The muscle cross-sectional diameter (1.5 cm). 39 | electrodes_in_z: Number of elecrodes in the array, in the direction of the fibre. 40 | electrodes_in_x: Number of electrodes in the array across the fibre. 41 | 42 | For plotting methods: 43 | y_limit_minimum: Minimum value of plot y-axis. 44 | y_limit_maximum: Maximum value of plot y-axis. 45 | """ 46 | ... 47 | 48 | def __init__(self): 49 | """Initializes a new sEMG object. 50 | 51 | """ 52 | ... 53 | 54 | ### Sampling parameters 55 | self.simulation_time:int = 30 # Total simulation time in seconds. 56 | self.sampling_rate:int = 10000 # Sample rate (10 kHz). 57 | self.ramp:np.array = np.array([5, 20, 5]) # Up, stable, and down times of the ramp in seconds. 58 | self.maximum_excitation_level:int = 20 # Maximum excitation level as a percentage of maximum. 59 | self.signal_to_noise_ratio_dB:float = 3 # Signal to noise ratio for Gaussian noise. SNR = 0 returns EMG without noise. 60 | self.signal_amplitude_offset:float = 0 # (mV) Add a value for signal amplitude offset from 0. 61 | 62 | ### Motorneuron parameters 63 | self.number_of_motor_units:int = 200 # Number of motoneurons (units) in the pool. 64 | self.recruitment_range:int = 30 # Range of recruitment threshold values. 65 | self.excitatory_gain:int = 1 # Gain of the excitatory drive-firing rate relationship. 66 | self.minimum_firing_rate:int = 8 # Minumum firing rate. 67 | self.peak_firing_rate_first_unit:int = 35 # Peak firing rate of the first motoneuron. 68 | self.peak_firing_rate_difference:int = 10 # Desired difference in peak firing rates between the first and last units. 69 | self.inter_spike_interval_coefficient_variation:int = 0.15 # The inter spike interval variance coefficient. 70 | self.conduction_velocity_min_value = 4 71 | self.conduction_velocity_max_value = 6 72 | 73 | ### Number of fibres parameters 74 | self.twitch_force_range:int = 100 # The range of twitch forces RP (force units). 75 | self.motor_unit_density:int = 20 # The motor-unit fibre density (20 unit fibres/mm^2 area of muscle). 76 | self.smallest_motor_unit_number_of_fibres:int = 25 # The smallest motor unit innervated 25 fibres. 77 | self.largest_motor_unit_number_of_fibres:int = 2725 # The largest motor unit innervated 2725 fibres. 78 | self.muscle_fibre_diameter:float = 46e-3 # (mm) The muscle-fibre diameter (46 µm). 79 | self.muscle_cross_sectional_diameter:int = 15 # (mm) The muscle cross-sectional diameter (1.5 cm). 80 | self.maximum_number_of_motor_units:int = 200 # The maximum number of motor units to calculate the motor unit radius and number of fibre for each motor unit. 81 | self.motor_unit_depth:int = 10 # (mm) 82 | 83 | ### Plot parameters 84 | self.electrodes_in_z:int = 1 # Number of electrodes in the z-direction. 85 | self.electrodes_in_x:int = 1 # Number of electrodes in the x-direction. 86 | self.y_limit_minimum:float = -1 # Minimum value of plot y-axis. 87 | self.y_limit_maximum:float = 1 # Maximum value of plot y-axis 88 | self.simulations:list = [] # Empty list for simulation. 89 | self.time_start:float = 7.40 # (s) 90 | self.time_end:float = 7.50 # (s) 91 | self.amplitude_start:float = -20 # (mV) 92 | self.amplitude_end:float = 60 # (mV) 93 | 94 | ######################### 1 ######################### Simulate Recruitment Model ########################## ####################### 95 | def simulate_recruitment_model(self): 96 | """Generates the recruitment and rate coding organization of motor units. 97 | 98 | Arguments: 99 | According to Models of Recruitment and Rate Coding Organization in Motor-Unit Pools. Fuglevand, et al 1993. 100 | simulation_time: Entire duration of the simulation (s). 101 | sampling_rate: Sampling frequency of the simulation (Hz). 102 | ramp: Excitatory drive function in a trapeziod shape (ramp-up, stable, ramp-down). 103 | maximum_excitation_level: Maximum excitation level of motor unit in percent (%). 104 | number_of_motor_units: Total number of motor units in the simulation. 105 | recruitment_range: The desired maximum for the range of recruitment threshold values. 106 | excitatory_gain: The gain of the excitatory drive-firing rate relationship. 107 | minimum_firing_rate: Minimum firing rate (Hz). 108 | peak_firing_rate_first_unit: Peak firing rate of the first motoneuron (Hz). 109 | peak_firing_rate_difference: The desired difference in peak firing rates between the first and last units (Hz). 110 | inter_spike_interval_coefficient_variation: The variance of inter spike interval coefficient. 111 | 112 | Returns: 113 | A list containing firing time arrays for each motor unit. 114 | """ 115 | ... 116 | 117 | ### Default arguments: 118 | simulation_time = self.simulation_time 119 | sampling_rate = self.sampling_rate 120 | number_of_motor_units = self.number_of_motor_units 121 | recruitment_range = self.recruitment_range 122 | peak_firing_rate_first_unit = self.peak_firing_rate_first_unit 123 | peak_firing_rate_difference = self.peak_firing_rate_difference 124 | minimum_firing_rate = self.minimum_firing_rate 125 | excitatory_gain = self.excitatory_gain 126 | maximum_excitation_level = self.maximum_excitation_level 127 | ramp = self.ramp 128 | inter_spike_interval_coefficient_variation = self.inter_spike_interval_coefficient_variation 129 | 130 | ### Time vector 131 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 132 | self.time_array = time_array 133 | 134 | ### Calculate the recruitment threshold excitation. Equation (1) in Fuglevand 1993. 135 | a = (np.log(recruitment_range) / number_of_motor_units) # Constant related to eq. (1). 136 | recruitmenexcitatory_drive_thresholdold_excitation = np.exp(a*(np.arange(1, number_of_motor_units + 1, 1))) 137 | 138 | ### Calculate the peak firing rate for each motoneuron. Equation (5) in Fuglevand 1993. 139 | peak_firing_rate_i = peak_firing_rate_first_unit - peak_firing_rate_difference * (recruitmenexcitatory_drive_thresholdold_excitation / recruitmenexcitatory_drive_thresholdold_excitation[-1]) 140 | 141 | ### Calculate the maximum excitation. Equation (8) in Fuglevand 1993. 142 | maximum_excitation = recruitmenexcitatory_drive_thresholdold_excitation[-1] + (peak_firing_rate_i[-1] - minimum_firing_rate) / excitatory_gain 143 | 144 | ### Define the excitatory drive function. 145 | excitatory_drive_function = np.concatenate((np.linspace(0, maximum_excitation * (maximum_excitation_level/100), ramp[0] * sampling_rate), np.ones(ramp[1] * sampling_rate) * maximum_excitation * (maximum_excitation_level/100), (np.flip(np.linspace(0, maximum_excitation * (maximum_excitation_level/100),ramp[2] * sampling_rate))))) 146 | 147 | ### Initialize the firing times for each motoneuron. 148 | firing_times_motor_unit = [[] for i in range(number_of_motor_units)] 149 | 150 | # iteration_variableate over each motoneuron. 151 | for i in range(number_of_motor_units): 152 | # Calculate the thresholded excitatory drive. 153 | excitatory_drive_threshold = excitatory_drive_function - recruitmenexcitatory_drive_thresholdold_excitation[i] 154 | 155 | # Find the samples that are associated with firing. Above this thresh => fire 156 | find_excitatory_drive_threshold = np.where(excitatory_drive_threshold >= 0)[0] 157 | 158 | # If there are no samples associated with firing, continue. 159 | if len(find_excitatory_drive_threshold) == 0: 160 | 161 | continue 162 | 163 | # Calculate the time of the first impulse. 164 | firing_times_motor_unit[i].append(time_array[find_excitatory_drive_threshold[0]]) 165 | #firing_times_motor_unit[i] = np.append(firing_times_motor_unit[i], time_array[find_excitatory_drive_threshold[0]]) 166 | 167 | # Calculate points of exceeded threshold 168 | excitation_difference = excitatory_drive_threshold[find_excitatory_drive_threshold[0]] 169 | 170 | # Time point for the first impulse 171 | time_instance = firing_times_motor_unit[i][0] 172 | 173 | # Initialize the firing counter. 174 | iteration_variable = 0 175 | 176 | ## Iterate until the current time is greater than the last sample point associated with firing. 177 | while time_instance <= time_array[find_excitatory_drive_threshold[-1]]: 178 | # Calculate the interspike interval. 179 | inter_spike_interval = max(1 / (excitatory_gain * excitation_difference + minimum_firing_rate), 1 / peak_firing_rate_i[i]) 180 | 181 | firing_times_motor_unit[i].append(firing_times_motor_unit[i][iteration_variable] + (inter_spike_interval_coefficient_variation * inter_spike_interval) * np.random.randn() + inter_spike_interval) 182 | 183 | # Update the firing counter. 184 | iteration_variable += 1 185 | 186 | # Find the minimum index of the sample point that is closest to the current firing time. 187 | minimum_time_index = np.argmin(np.abs(firing_times_motor_unit[i][iteration_variable] - time_array[find_excitatory_drive_threshold])) 188 | 189 | # Update the thresholded excitatory drive. 190 | excitation_difference = excitatory_drive_threshold[find_excitatory_drive_threshold[minimum_time_index]] 191 | 192 | # Update the current time. 193 | time_instance = firing_times_motor_unit[i][iteration_variable] 194 | 195 | #print('Firing Times for each Motor Unit', firing_times_motor_unit) 196 | scipy.io.savemat('recrtuitment_model' + '_' + str(number_of_motor_units) + 'MUs_' + str(sampling_rate) + 'sf' + '.mat', {'RM': firing_times_motor_unit}) 197 | scipy.io.savemat('time' + '.mat', {'output': time_array}) 198 | 199 | return firing_times_motor_unit 200 | 201 | ######################### 2 ######################### Plot Recruitment Model ########################## ####################### 202 | def plot_recruitment_model(self): 203 | """Plots the firing patterns of the motor unit recruitment. 204 | 205 | Arguments: 206 | firing_times_motor_unit 207 | time_array 208 | 209 | Returns: 210 | A plot of the recruitment model for each motor unit. 211 | """ 212 | ... 213 | 214 | ### Default arguments: 215 | firing_times_motor_unit = self.simulate_recruitment_model() 216 | time_array = self.time_array 217 | 218 | ### En lista med olika färger för varje motor enhet 219 | #colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k'] 220 | def generate_unique_colors(num_colors): 221 | unique_colors = set() 222 | colors = [] 223 | 224 | while len(unique_colors) < num_colors: 225 | color = "#{:02X}{:02X}{:02X}".format(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) 226 | if color not in unique_colors: 227 | unique_colors.add(color) 228 | colors.append(color) 229 | 230 | return colors 231 | 232 | num_colors = self.number_of_motor_units # Ange det önskade antalet unika färger här 233 | colors = generate_unique_colors(num_colors) 234 | 235 | ### Plot of the firing times motor unit 236 | fig4 = plt.figure(4) 237 | for i, time_array in enumerate(firing_times_motor_unit): 238 | color = colors[i % len(colors)] # Välj en färg från listan baserat på i 239 | plt.plot(time_array, [i] * len(time_array), '|', color = color, label = f'Motor Unit {i + 1}') 240 | 241 | plt.xlabel('Time (s)', fontsize = 15) 242 | plt.ylabel('Motor Unit Number', fontsize = 15) 243 | plt.title('Firing Times of Motor Units', fontsize = 20, fontweight = 'bold') 244 | plt.grid(axis = 'x') 245 | 246 | number_of_motor_units = self.number_of_motor_units 247 | # Set the desired resolution 248 | plt.savefig('recruitment_model_{}MUs.png'.format(number_of_motor_units), dpi = 600) 249 | 250 | return plt.show() 251 | 252 | ######################### 3 ######################### Simulate Surface Electromyography ########################## ####################### 253 | def caculate_surface_emg(self): 254 | """Simulates the surface electromyography based on the recruitment model. 255 | 256 | Arguments: 257 | firing_times_motor_unit 258 | time_array 259 | motor_unit_i 260 | twitch_force_range 261 | number_of_motor_units 262 | motor_unit_density 263 | smallest_motor_unit_number_of_fibres 264 | largest_motor_unit_number_of_fibres 265 | muscle_fibre_diameter 266 | muscle_cross_sectional_diameter 267 | electrodes_in_z 268 | electrodes_in_x 269 | 270 | Returns: 271 | A list containing an array with the simulated surface electromyography based on the recruitment model of each motor unit for all electrodes. 272 | """ 273 | ... 274 | 275 | firing_times_motor_unit = self.simulate_recruitment_model() 276 | time_array = self.time_array 277 | motor_unit_i = MotorUnit() 278 | twitch_force_range = self.twitch_force_range 279 | number_of_motor_units = self.number_of_motor_units 280 | motor_unit_density = self.motor_unit_density 281 | smallest_motor_unit_number_of_fibres = self.smallest_motor_unit_number_of_fibres 282 | largest_motor_unit_number_of_fibres = self.largest_motor_unit_number_of_fibres 283 | muscle_fibre_diameter = self.muscle_fibre_diameter 284 | muscle_cross_sectional_diameter = self.muscle_cross_sectional_diameter 285 | electrodes_in_z = self.electrodes_in_z 286 | electrodes_in_x = self.electrodes_in_x 287 | max_number_of_motor_units = self.maximum_number_of_motor_units 288 | motor_unit_depth = self.motor_unit_depth 289 | conduction_velocity_min_value = self.conduction_velocity_min_value 290 | conduction_velocity_max_value = self.conduction_velocity_max_value 291 | 292 | ### Calculate the number of fibres innervated by each motor unit according to equation (21) Fuglevand et al 1993. 293 | # Calculate the peak twitch force for each unit accroding to equation (13) in Fuglevand 1993. 294 | b = (np.log(twitch_force_range) / max_number_of_motor_units) # Constant related to eq. (13). 295 | peak_twitch_force = np.exp(b*(np.arange(1, max_number_of_motor_units + 1, 1))) # Pi, where i = np.arange(1, number_of_motor_units + 1, 1) 296 | 297 | # The numbmer of muscle fibres required to exert one unit of force (1 unit force ≈ twitch force of smallest motor unit) 298 | total_peak_twitch_forces = np.sum(peak_twitch_force) # P_tot 299 | 300 | ## Calculate the total number of fibres (nf_tot) in a muscle, with a cross-sectional area (Am) and average area of a muscle fibre (Af). 301 | # The muscle cross-sectional area (mm^2) 302 | Am = np.pi * (muscle_cross_sectional_diameter/2)**2 # Am 303 | 304 | # The muscle fibre average area (mm^2) 305 | Af = np.pi * (muscle_fibre_diameter/2)**2 # Af 306 | 307 | nf_tot = Am/Af # nf_tot 308 | 309 | ## The number of fibres (nf_i) innervated by each motor unit according to equation (21) Fuglevand et al 1993. 310 | max_number_of_fibres = (nf_tot/total_peak_twitch_forces) * peak_twitch_force # nf_i 311 | if max_number_of_fibres[0] < smallest_motor_unit_number_of_fibres or max_number_of_fibres[-1] > largest_motor_unit_number_of_fibres: 312 | print('Default values have been changed') 313 | number_of_fibres_variation = max_number_of_fibres[0:number_of_motor_units] 314 | 315 | ### The area encompassed by each motor-unit territory (Ai), was then calculated from the unit fibre density according to equation (22) Fuglevand et al 1993 316 | motor_unit_area = number_of_fibres_variation/motor_unit_density # Ai 317 | 318 | # The motor unit radius variation calculated from its area (mm) 319 | max_motor_unit_radius = (motor_unit_area/np.pi) 320 | motor_unit_radius_variation = max_motor_unit_radius[0:number_of_motor_units] 321 | 322 | ### Calculate the motor unit depth variation that use to calcualte the fibre depth variation 323 | eta_angle = 2 * np.pi * np.random.random(number_of_motor_units) 324 | 325 | x_position_motor_unit = np.zeros(number_of_motor_units) 326 | y_position_motor_unit = np.zeros(number_of_motor_units) 327 | # calculating coordinates 328 | for i in range(number_of_motor_units): 329 | radial_position_motor_unit = muscle_cross_sectional_diameter/2 * np.random.random() 330 | x_position_motor_unit[i] = radial_position_motor_unit * np.cos(eta_angle[i]) 331 | y_position_motor_unit[i] = radial_position_motor_unit * np.sin(eta_angle[i]) 332 | 333 | 334 | num_elements = number_of_motor_units 335 | conduction_velocity_equal_steps = np.linspace(conduction_velocity_min_value, conduction_velocity_max_value, num_elements) 336 | conduction_velocity_random = np.random.uniform(conduction_velocity_min_value, conduction_velocity_max_value, num_elements) 337 | 338 | ### Calculate simuations of the surface EMG signal 339 | simulations = [] 340 | 341 | for m, element in enumerate(firing_times_motor_unit): 342 | motor_unit_i.number_of_fibres = int(number_of_fibres_variation[m]) 343 | motor_unit_i.motor_unit_radius = motor_unit_radius_variation[m] 344 | motor_unit_i.motor_unit_depth = motor_unit_depth - y_position_motor_unit[m] 345 | motor_unit_i.motor_unit_x_position = x_position_motor_unit[m] 346 | motor_unit_i.conduction_velocity = conduction_velocity_random[m] 347 | motor_unit_i.number_of_electrodes_z = electrodes_in_z 348 | motor_unit_i.number_of_electrodes_x = electrodes_in_x 349 | 350 | current_motor_unit = motor_unit_i.simulate_motor_unit() 351 | # Add list of every motor unit in a long list. 352 | simulation = np.full((len(current_motor_unit), len(time_array)), current_motor_unit[0,0]) 353 | 354 | for e in range(len(element)): 355 | # Find the time index where a firing occurs 356 | time_index = np.argmin(np.abs(time_array - element[e])) 357 | 358 | # Add the current motor unit to the simulation at the appropriate time index 359 | if simulation[:, time_index:time_index + current_motor_unit.shape[1]].shape >= current_motor_unit.shape: 360 | simulation[:, time_index:time_index + current_motor_unit.shape[1]] += current_motor_unit 361 | 362 | simulations.append(simulation) 363 | #self.simulations = simulations 364 | return simulations 365 | 366 | ######################### 4 ######################### Adds Noise To The Simulation ########################## ####################### 367 | def simulate_surface_emg(self): 368 | """Adds noise to the entire simulation. 369 | 370 | Arguments: 371 | simulations: simulate_surface_emg 372 | 373 | Returns: 374 | A list containing an array with the simulated surface electromyography with added noise. 375 | """ 376 | ... 377 | 378 | signal_to_noise_ratio_dB = self.signal_to_noise_ratio_dB 379 | simulations = self.caculate_surface_emg() 380 | time_array = self.time_array 381 | signal_amplitude_offset = self.signal_amplitude_offset 382 | number_of_electrodes_z = self.electrodes_in_z 383 | number_of_electrodes_x = self.electrodes_in_x 384 | time_start = self.time_start 385 | time_end = self.time_end 386 | amplitude_start = self.amplitude_start 387 | amplitude_end = self.amplitude_end 388 | number_of_motor_units = self.number_of_motor_units 389 | 390 | 391 | if signal_to_noise_ratio_dB == 0: 392 | return simulations 393 | 394 | else: 395 | electrod_postion = number_of_electrodes_x * number_of_electrodes_z 396 | 397 | electrode_sum = np.zeros((number_of_electrodes_z * number_of_electrodes_x, len(time_array))) 398 | electrode_sum_with_noise = np.zeros((number_of_electrodes_z * number_of_electrodes_x, len(time_array))) 399 | 400 | for ne in range(len(electrode_sum)): 401 | motor_unit_sum = np.zeros(len(time_array)) 402 | for m, simulation in enumerate(simulations): 403 | motor_unit_sum += simulation[ne,:] 404 | electrode_sum[ne,:] = motor_unit_sum 405 | 406 | noise_level = np.mean(motor_unit_sum) / (10**(signal_to_noise_ratio_dB/20))*1000 407 | noise = signal_amplitude_offset + noise_level * np.random.normal(size = len(time_array)) 408 | 409 | signal_with_noise = motor_unit_sum + noise 410 | 411 | electrode_sum_with_noise[ne,:] = signal_with_noise 412 | 413 | #plt.figure(figsize = (10, 4)) 414 | #plt.plot(time_array, -electrode_sum_with_noise[electrod_postion-1, :] * 10**3, label='sEMG with noise') 415 | #plt.plot(time_array, -electrode_sum[electrod_postion-1, :] * 10**3, label='Original sEMG') 416 | #plt.title('sEMG signal with and without noise', fontweight = 'bold') 417 | #plt.xlabel('Time (s)') 418 | #plt.ylabel('Amplitude (mV)') 419 | #plt.legend() 420 | #plt.show() 421 | 422 | fig = plt.figure() 423 | ax = plt.subplot(2,1,1) 424 | plt.plot(time_array, electrode_sum_with_noise[electrod_postion-1, :] * 10**3, label = 'sEMG with noise') 425 | plt.plot(time_array, electrode_sum[electrod_postion-1, :] * 10**3, label = 'Original sEMG') 426 | plt.legend() 427 | 428 | ax = plt.subplot(2,1,2) 429 | plt.plot(time_array, electrode_sum_with_noise[electrod_postion-1, :] * 10**3) 430 | plt.plot(time_array, electrode_sum[electrod_postion-1, :] * 10**3) 431 | 432 | plt.xlim(time_start, time_end) 433 | plt.ylim(amplitude_start, amplitude_end) 434 | fig.supxlabel('Time (s)', fontsize = 15) 435 | fig.supylabel('Amplitude (mV)', fontsize = 15) # µ 436 | fig.suptitle('sEMG Signal with and without Noise', fontsize = 20, fontweight = 'bold') 437 | 438 | # Set the desired resolution 439 | plt.savefig('sEMG_with_and_without_noise_{}MUs_{}X{}.png'.format(number_of_motor_units,number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 440 | plt.show() 441 | 442 | scipy.io.savemat('sEMG_without_Noise' + '_' + str(number_of_motor_units) + 'MUs_' + str(number_of_electrodes_z) + 'X' + str(number_of_electrodes_x) + '.mat', {'output': -electrode_sum}) 443 | 444 | 445 | return electrode_sum_with_noise 446 | 447 | ######################### 5 ######################### Plot Suface Electromyography Array without Noise ########################## ####################### 448 | def plot_suface_emg_array_no_noise(self): 449 | """Plots the sEMG array without the added noise. 450 | 451 | Arguments: 452 | simulations 453 | time_array 454 | y_limit_minimum 455 | y_limit_maximum 456 | number_of_electrodes_z 457 | number_of_electrodes_x 458 | 459 | Returns: 460 | Plot of surface EMG without noise 461 | """ 462 | ... 463 | 464 | ### Default arguments: 465 | simulations = self.caculate_surface_emg() 466 | #simulations = self.simulations 467 | time_array = self.time_array 468 | y_limit_minimum = self.y_limit_minimum 469 | y_limit_maximum = self.y_limit_maximum 470 | number_of_electrodes_z = self.electrodes_in_z 471 | number_of_electrodes_x = self.electrodes_in_x 472 | 473 | electrode_sum = np.zeros((number_of_electrodes_z * number_of_electrodes_x, len(time_array))) 474 | 475 | for ne in range(len(electrode_sum)): 476 | motor_unit_sum = np.zeros(len(time_array)) 477 | for m, simulation in enumerate(simulations): 478 | motor_unit_sum += simulation[ne,:] 479 | electrode_sum[ne,:] = motor_unit_sum 480 | 481 | ### Plot the normalized motor unit action potential 482 | normalized_simulation = electrode_sum 483 | normalized_simulation = (normalized_simulation - normalized_simulation.mean()) / (normalized_simulation.max() - normalized_simulation.min()) 484 | 485 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 486 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 487 | zeros_array = np.zeros(len(array_size)) 488 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 489 | array_size_x = np.append(array_size_x, zeros_array) 490 | 491 | ### Plot the simulations for each motor unit as an array 492 | fig5 = plt.figure(figsize = (10, 6)) 493 | for i in range(len(array_size)): 494 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 495 | plt.subplots_adjust(wspace = 0.0, hspace = 0.0) 496 | ax.grid(which = 'both', ls = 'dashed') 497 | plt.plot(time_array, normalized_simulation[i, :]) 498 | plt.xlim(time_array[0], time_array[-1] - 1) 499 | if i < len(array_size) - number_of_electrodes_x: 500 | ax.xaxis.set_major_formatter(NullFormatter()) 501 | plt.ylim(y_limit_minimum,y_limit_maximum) 502 | ax.yaxis.set_major_formatter(NullFormatter()) 503 | if i == 0: 504 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 505 | for j in range(i): 506 | if i == array_size_x[j]: 507 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 508 | elif number_of_electrodes_x == 1: 509 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 510 | plt.suptitle('The Surface Electromyography Signal Array', fontsize = 20, fontweight = 'bold') 511 | 512 | if number_of_electrodes_x > 1: 513 | fig5.supxlabel('Time (s)\n Electrodes in the x direction, i.e. vertically across the fibre') 514 | else: 515 | fig5.supxlabel('Time (s)') 516 | if number_of_electrodes_z > 1: 517 | fig5.supylabel('Normalized sEMG Signal\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center') 518 | else: 519 | fig5.supylabel('Normalized sEMG Signal', ha = 'center', va = 'center') 520 | 521 | number_of_motor_units = self.number_of_motor_units 522 | # Set the desired resolution 523 | plt.savefig('sEMG_electrode_array_without_noise_{}MUs_{}X{}.png'.format(number_of_motor_units,number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 524 | 525 | return plt.show() 526 | 527 | ######################### 6 ######################### Plot Suface Electromyography Array with Noise ########################## ####################### 528 | def plot_suface_emg_array(self): 529 | """Plots the sEMG array with added noise. 530 | 531 | Arguments: 532 | simulations 533 | time_array 534 | y_limit_minimum 535 | y_limit_maximum 536 | number_of_electrodes_z 537 | number_of_electrodes_x 538 | 539 | Returns: 540 | Plot of surface array EMG with added noise. 541 | """ 542 | ... 543 | 544 | ### Default arguments: 545 | simulations = self.simulate_surface_emg() 546 | #simulations = self.simulations 547 | time_array = self.time_array 548 | y_limit_minimum = self.y_limit_minimum 549 | y_limit_maximum = self.y_limit_maximum 550 | number_of_electrodes_z = self.electrodes_in_z 551 | number_of_electrodes_x = self.electrodes_in_x 552 | 553 | ### Plot the normalized motor unit action potential 554 | normalized_simulation = simulations 555 | normalized_simulation = (normalized_simulation - normalized_simulation.mean()) / (normalized_simulation.max() - normalized_simulation.min()) 556 | 557 | # The single fibre action potentials recorded by the electrodes positioned along the length of the fibre. 558 | array_size = np.arange(1, (number_of_electrodes_z*number_of_electrodes_x)+1, 1) 559 | zeros_array = np.zeros(len(array_size)) 560 | array_size_x = np.arange(0, number_of_electrodes_z*number_of_electrodes_x, number_of_electrodes_x) 561 | array_size_x = np.append(array_size_x, zeros_array) 562 | 563 | ### Plot the simulations for each motor unit as an array 564 | fig6 = plt.figure(figsize = (10, 6)) 565 | for i in range(len(array_size)): 566 | ax = plt.subplot(number_of_electrodes_z , number_of_electrodes_x, array_size[i]) 567 | plt.subplots_adjust(wspace = 0.0, hspace = 0.0) 568 | ax.grid(which = 'both', ls = 'dashed') 569 | plt.plot(time_array, normalized_simulation[i, :]) 570 | plt.xlim(time_array[0], time_array[-1] - 1) 571 | if i < len(array_size) - number_of_electrodes_x: 572 | ax.xaxis.set_major_formatter(NullFormatter()) 573 | plt.ylim(y_limit_minimum,y_limit_maximum) 574 | ax.yaxis.set_major_formatter(NullFormatter()) 575 | if i == 0: 576 | ax.set_ylabel(1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 577 | for j in range(i): 578 | if i == array_size_x[j]: 579 | ax.set_ylabel(array_size[j], rotation = 0, ha = 'center', va = 'center', fontsize = 15) 580 | elif number_of_electrodes_x == 1: 581 | ax.set_ylabel(array_size[j]+1, rotation = 0, ha = 'center', va = 'center', fontsize = 15) 582 | plt.suptitle('The Surface Electromyography Signal Array', fontsize = 20, fontweight = 'bold') 583 | 584 | if number_of_electrodes_x > 1: 585 | fig6.supxlabel('Time (s)\n Electrodes in the x direction, i.e. vertically across the fibre') 586 | else: 587 | fig6.supxlabel('Time (s)') 588 | if number_of_electrodes_z > 1: 589 | fig6.supylabel('Normalized sEMG Signal\n Electrodes in the z direction, i.e. along the fibre', ha = 'center', va = 'center') 590 | else: 591 | fig6.supylabel('Normalized sEMG Signal', ha = 'center', va = 'center') 592 | 593 | number_of_motor_units = self.number_of_motor_units 594 | # Set the desired resolution 595 | plt.savefig('sEMG_electrode_array_{}MUs_{}X{}.png'.format(number_of_motor_units,number_of_electrodes_z,number_of_electrodes_x), dpi = 600) 596 | 597 | return plt.show() 598 | 599 | ######################### 7 ######################### Plot Saved Surface Electromyography Array without Noise ########################## ####################### 600 | def plot_one_electrode_surface_emg_no_noise(self): 601 | """Plots the sEMG from selected electrode without added noise. 602 | 603 | Arguments: 604 | simulation_time 605 | sampling_rate 606 | surface_emg 607 | electrodes_in_z 608 | electrodes_in_x 609 | 610 | Returns: 611 | Plot of surface EMG from selected elctrode without added noise. 612 | """ 613 | ... 614 | ### Default arguments: 615 | simulation_time = self.simulation_time 616 | sampling_rate = self.sampling_rate 617 | surface_emg = self.caculate_surface_emg() 618 | electrodes_in_z = self.electrodes_in_z 619 | electrodes_in_x = self.electrodes_in_x 620 | number_of_motor_units = self.number_of_motor_units 621 | 622 | electrod_postion = electrodes_in_z * electrodes_in_x 623 | 624 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 625 | 626 | fig7 = plt.figure(7) 627 | ### Plot the simulations for each motor unit after sum 628 | electrod_one_sum = np.zeros(len(time_array)) 629 | 630 | for m, simulation in enumerate(surface_emg): 631 | electrod_one_sum += simulation[electrod_postion-1,:] 632 | 633 | plt.plot(time_array, electrod_one_sum*10**3) # Plot the electrode row postion with m numbers of motor units. 634 | 635 | plt.xlabel('Time (s)') 636 | plt.ylabel('Amplitude (mV)') 637 | fig7.suptitle('sEMG Signal for One Electrode', fontweight = 'bold') 638 | 639 | # Set the desired resolution 640 | plt.savefig('sEMG_one_electrode_wihout_noise_{}MUs_{}X{}.png'.format(number_of_motor_units,electrodes_in_z,electrodes_in_x), dpi = 600) 641 | 642 | return plt.show() 643 | 644 | ######################### 8 ######################### Plot Saved Surface Electromyography Array with Noise ########################## ####################### 645 | def plot_one_electrode_surface_emg(self): 646 | """Plots the sEMG from selected electrode with added noise. 647 | 648 | Arguments: 649 | simulation_time 650 | sampling_rate 651 | surface_emg 652 | electrodes_in_z 653 | electrodes_in_x 654 | 655 | Returns: 656 | Plot of surface EMG from selected elctrode with added noise. 657 | """ 658 | ... 659 | ### Default arguments: 660 | simulation_time = self.simulation_time 661 | sampling_rate = self.sampling_rate 662 | surface_emg = self.simulate_surface_emg() 663 | electrodes_in_z = self.electrodes_in_z 664 | electrodes_in_x = self.electrodes_in_x 665 | number_of_motor_units = self.number_of_motor_units 666 | 667 | electrod_postion = electrodes_in_z * electrodes_in_x 668 | 669 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 670 | 671 | fig8 = plt.figure(8) 672 | 673 | plt.plot(time_array, surface_emg[electrod_postion-1, :]*10**3) # Plot the electrode row postion with m numbers of motor units. 674 | 675 | plt.xlabel('Time (s)') 676 | plt.ylabel('Amplitude (mV)') 677 | fig8.suptitle('sEMG Signal for One Electrode', fontweight = 'bold') 678 | 679 | # Set the desired resolution 680 | plt.savefig('sEMG_one_electrode_{}MUs_{}X{}.png'.format(number_of_motor_units,electrodes_in_z,electrodes_in_x), dpi = 600) 681 | 682 | return plt.show() 683 | 684 | ######################### 9 ######################### Subplot Saved Surface Electromyography Array with Noise ########################## ####################### 685 | def subplot_one_electrode_surface_emg(self): 686 | """Plots the sEMG from selected electrode with added noise. 687 | 688 | Arguments: 689 | simulation_time 690 | sampling_rate 691 | surface_emg 692 | electrodes_in_z 693 | electrodes_in_x 694 | 695 | Returns: 696 | Plot of surface EMG from selected elctrode with added noise. 697 | """ 698 | ... 699 | ### Default arguments: 700 | simulation_time = self.simulation_time 701 | sampling_rate = self.sampling_rate 702 | surface_emg = self.simulate_surface_emg() 703 | electrodes_in_z = self.electrodes_in_z 704 | electrodes_in_x = self.electrodes_in_x 705 | time_start = self.time_start 706 | time_end = self.time_end 707 | amplitude_start = self.amplitude_start 708 | amplitude_end = self.amplitude_end 709 | number_of_motor_units = self.number_of_motor_units 710 | 711 | electrod_postion = electrodes_in_z * electrodes_in_x 712 | 713 | time_array = np.linspace(0, simulation_time, simulation_time*sampling_rate) 714 | 715 | fig9 = plt.figure(9) 716 | ax = plt.subplot(2,1,1) 717 | plt.plot(time_array, surface_emg[electrod_postion-1, :]*10**3) # Plot the electrode row postion with m numbers of motor units. 718 | 719 | ax = plt.subplot(2,1,2) 720 | plt.plot(time_array, surface_emg[electrod_postion-1, :]*10**3) 721 | plt.xlim(time_start, time_end) 722 | plt.ylim(amplitude_start, amplitude_end) 723 | fig9.supxlabel('Time (s)', fontsize = 15) 724 | fig9.supylabel('Amplitude (mV)', fontsize = 15) # µ 725 | fig9.suptitle('sEMG Signal for One Electrode', fontsize = 20, fontweight = 'bold') 726 | 727 | # Set the desired resolution 728 | plt.savefig('sEMG_subplot_one_electrode_{}MUs_{}X{}.png'.format(number_of_motor_units,electrodes_in_z,electrodes_in_x), dpi = 600) 729 | 730 | return plt.show() 731 | 732 | # ######################### ######################### ########################## ####################### 733 | # THE END # 734 | # ######################### ######################### ########################## ####################### 735 | --------------------------------------------------------------------------------