pyNIBS 0.2024.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. pyNIBS-0.2024.8.dist-info/LICENSE +623 -0
  2. pyNIBS-0.2024.8.dist-info/METADATA +723 -0
  3. pyNIBS-0.2024.8.dist-info/RECORD +107 -0
  4. pyNIBS-0.2024.8.dist-info/WHEEL +5 -0
  5. pyNIBS-0.2024.8.dist-info/top_level.txt +1 -0
  6. pynibs/__init__.py +34 -0
  7. pynibs/coil.py +1367 -0
  8. pynibs/congruence/__init__.py +15 -0
  9. pynibs/congruence/congruence.py +1108 -0
  10. pynibs/congruence/ext_metrics.py +257 -0
  11. pynibs/congruence/stimulation_threshold.py +318 -0
  12. pynibs/data/configuration_exp0.yaml +59 -0
  13. pynibs/data/configuration_linear_MEP.yaml +61 -0
  14. pynibs/data/configuration_linear_RT.yaml +61 -0
  15. pynibs/data/configuration_sigmoid4.yaml +68 -0
  16. pynibs/data/network mapping configuration/configuration guide.md +238 -0
  17. pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +42 -0
  18. pynibs/data/network mapping configuration/configuration_for_testing.yaml +43 -0
  19. pynibs/data/network mapping configuration/configuration_modelTMS.yaml +43 -0
  20. pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +43 -0
  21. pynibs/data/network mapping configuration/output_documentation.md +185 -0
  22. pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +77 -0
  23. pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +1281 -0
  24. pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +1281 -0
  25. pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +1281 -0
  26. pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +1281 -0
  27. pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +1281 -0
  28. pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +1281 -0
  29. pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +1281 -0
  30. pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +1281 -0
  31. pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +1281 -0
  32. pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +1281 -0
  33. pynibs/expio/Mep.py +1518 -0
  34. pynibs/expio/__init__.py +8 -0
  35. pynibs/expio/brainsight.py +979 -0
  36. pynibs/expio/brainvis.py +71 -0
  37. pynibs/expio/cobot.py +239 -0
  38. pynibs/expio/exp.py +1876 -0
  39. pynibs/expio/fit_funs.py +287 -0
  40. pynibs/expio/localite.py +1987 -0
  41. pynibs/expio/signal_ced.py +51 -0
  42. pynibs/expio/visor.py +624 -0
  43. pynibs/freesurfer.py +502 -0
  44. pynibs/hdf5_io/__init__.py +10 -0
  45. pynibs/hdf5_io/hdf5_io.py +1857 -0
  46. pynibs/hdf5_io/xdmf.py +1542 -0
  47. pynibs/mesh/__init__.py +3 -0
  48. pynibs/mesh/mesh_struct.py +1394 -0
  49. pynibs/mesh/transformations.py +866 -0
  50. pynibs/mesh/utils.py +1103 -0
  51. pynibs/models/_TMS.py +211 -0
  52. pynibs/models/__init__.py +0 -0
  53. pynibs/muap.py +392 -0
  54. pynibs/neuron/__init__.py +2 -0
  55. pynibs/neuron/neuron_regression.py +284 -0
  56. pynibs/neuron/util.py +58 -0
  57. pynibs/optimization/__init__.py +5 -0
  58. pynibs/optimization/multichannel.py +278 -0
  59. pynibs/optimization/opt_mep.py +152 -0
  60. pynibs/optimization/optimization.py +1445 -0
  61. pynibs/optimization/workhorses.py +698 -0
  62. pynibs/pckg/__init__.py +0 -0
  63. pynibs/pckg/biosig/biosig4c++-1.9.5.src_fixed.tar.gz +0 -0
  64. pynibs/pckg/libeep/__init__.py +0 -0
  65. pynibs/pckg/libeep/pyeep.so +0 -0
  66. pynibs/regression/__init__.py +11 -0
  67. pynibs/regression/dual_node_detection.py +2375 -0
  68. pynibs/regression/regression.py +2984 -0
  69. pynibs/regression/score_types.py +0 -0
  70. pynibs/roi/__init__.py +2 -0
  71. pynibs/roi/roi.py +895 -0
  72. pynibs/roi/roi_structs.py +1233 -0
  73. pynibs/subject.py +1009 -0
  74. pynibs/tensor_scaling.py +144 -0
  75. pynibs/tests/data/InstrumentMarker20200225163611937.xml +19 -0
  76. pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +14 -0
  77. pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +6373 -0
  78. pynibs/tests/data/Xdmf.dtd +89 -0
  79. pynibs/tests/data/brainsight_niiImage_nifticoord.txt +145 -0
  80. pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +1434 -0
  81. pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +47 -0
  82. pynibs/tests/data/create_subject_testsub.py +332 -0
  83. pynibs/tests/data/data.hdf5 +0 -0
  84. pynibs/tests/data/geo.hdf5 +0 -0
  85. pynibs/tests/test_coil.py +474 -0
  86. pynibs/tests/test_elements2nodes.py +100 -0
  87. pynibs/tests/test_hdf5_io/test_xdmf.py +61 -0
  88. pynibs/tests/test_mesh_transformations.py +123 -0
  89. pynibs/tests/test_mesh_utils.py +143 -0
  90. pynibs/tests/test_nnav_imports.py +101 -0
  91. pynibs/tests/test_quality_measures.py +117 -0
  92. pynibs/tests/test_regressdata.py +289 -0
  93. pynibs/tests/test_roi.py +17 -0
  94. pynibs/tests/test_rotations.py +86 -0
  95. pynibs/tests/test_subject.py +71 -0
  96. pynibs/tests/test_util.py +24 -0
  97. pynibs/tms_pulse.py +34 -0
  98. pynibs/util/__init__.py +4 -0
  99. pynibs/util/dosing.py +233 -0
  100. pynibs/util/quality_measures.py +562 -0
  101. pynibs/util/rotations.py +340 -0
  102. pynibs/util/simnibs.py +763 -0
  103. pynibs/util/util.py +727 -0
  104. pynibs/visualization/__init__.py +2 -0
  105. pynibs/visualization/para.py +4372 -0
  106. pynibs/visualization/plot_2D.py +137 -0
  107. pynibs/visualization/render_3D.py +347 -0
pynibs/models/_TMS.py ADDED
@@ -0,0 +1,211 @@
1
+ import os
2
+ import glob
3
+ import copy
4
+ import shutil
5
+ import pynibs
6
+ import simnibs
7
+ import datetime
8
+ import numpy as np
9
+ try:
10
+ from pygpc.AbstractModel import AbstractModel
11
+ except ImportError:
12
+ pass
13
+
14
+
15
+ class _TMS(AbstractModel):
16
+ """
17
+ TMS field calculation using Simnibs
18
+ """
19
+
20
+ def __init__(self):
21
+ """
22
+ Parameters
23
+ ----------
24
+ p : dict
25
+ parameters dictionary
26
+ p["fn_subject"] : str
27
+ Filename of subject object
28
+ (e.g. /data/pt_01756/probands/15484.08/15484.08.pkl)
29
+ p["mesh_idx"] : int
30
+ MESH index the simulations are conducted with
31
+ p["fn_coil"] : str
32
+ Filename of coil .ccd file
33
+ p["coil_position_mean"]: ndarray of float [3 x 4]
34
+ Mean coil position and orientation [x_ori, y_ori, z_ori, loc]
35
+ p["anisotropy_type"]: str or None
36
+ Type of anisotropy ("vn" (volume normalized) or None)
37
+ p["sigma_WM"] : float
38
+ Electrical conductivity of white matter (default: 0.126 S/m)
39
+ p["sigma_GM"] : float
40
+ Electrical conductivity of gray matter (default: 0.275 S/m)
41
+ p["sigma_CSF"] : float
42
+ Electrical conductivity of CSF (default: 1.654 S/m)
43
+ p["sigma_Skull"] : float
44
+ Electrical conductivity of skull (default: 0.010 S/m)
45
+ p["sigma_Scalp"] : float
46
+ Electrical conductivity of scalp (default: 0.465 S/m)
47
+ p["aniso"] : float
48
+ Anisotropy scaling factor (default: 0.5)
49
+ p["x"] : float
50
+ Displacement of TMS coil along first principal axis
51
+ p["y"] : float
52
+ Displacement of TMS coil along second principal axis
53
+ p["z"] : float
54
+ Displacement of TMS coil along third principal axis
55
+ p["psi"] : float
56
+ Rotation of TMS coil around first principal axis in deg
57
+ p["theta"] : float
58
+ Rotation of TMS coil around second principal axis in deg
59
+ p["phi"] : float
60
+ Rotation of TMS coil around third principal axis in deg
61
+ p["fn_results"] : str
62
+ Results folder
63
+ (e.g. /home/raid1/kweise/data/probands/15484.08/results/gpc/20_cond_coil_corrected/electric_field/I_0)
64
+ """
65
+ pass
66
+
67
+ def validate(self):
68
+ pass
69
+
70
+ def simulate(self, process_id=None, matlab_engine=None):
71
+ """
72
+ Calculates the scalar electric potential
73
+
74
+ Returns
75
+ -------
76
+ potential : ndarray of float [n_points]
77
+ Scalar electric potential in nodes of FEM mesh
78
+ additional_data : dict of ndarray
79
+ Additional output data
80
+ - dA/dt [3*n_points] ... magnetic vector potential in nodes of mesh
81
+ - coil_dipoles [3*n_dipoles] ... coil dipole positions
82
+ - coil_mag [n_dipoles] ... coil dipole magnitude
83
+ """
84
+
85
+ # load subject
86
+ subject = pynibs.load_subject(self.p["fn_subject"])
87
+
88
+ # Setting up SimNIBS SESSION
89
+ #######################################################################
90
+ S = simnibs.sim_struct.SESSION()
91
+ S.fnamehead = subject.mesh[self.p["mesh_idx"]]["fn_mesh_msh"]
92
+ S.pathfem = os.path.join(os.path.split(self.p["fn_results"])[0], "tmp", str(process_id))
93
+ S.fields = "veED"
94
+ S.open_in_gmsh = False
95
+
96
+ if os.path.isdir(S.pathfem):
97
+ shutil.rmtree(S.pathfem)
98
+ os.makedirs(S.pathfem)
99
+
100
+ # Setting up coil position
101
+ #######################################################################
102
+ matsimnibs = pynibs.calc_coil_transformation_matrix(
103
+ LOC_mean=self.p["coil_position_mean"][0:3, 3],
104
+ ORI_mean=self.p["coil_position_mean"][0:3, 0:3],
105
+ LOC_var=np.array([self.p["x"], self.p["y"], self.p["z"]]),
106
+ ORI_var=np.array([self.p["psi"], self.p["theta"], self.p["phi"]]),
107
+ V=self.p["coil_position_mean"][0:3, 0:3])
108
+
109
+ # Define the TMS simulation and setting up conductivities
110
+ #######################################################################
111
+ tms = S.add_tmslist()
112
+ tms.fnamecoil = self.p["fn_coil"]
113
+ tms.cond[0].value = self.p["sigma_WM"] # WM
114
+ tms.cond[1].value = self.p["sigma_GM"] # GM
115
+ tms.cond[2].value = self.p["sigma_CSF"] # CSF
116
+ tms.cond[3].value = self.p["sigma_Skull"] # Skull
117
+ tms.cond[4].value = self.p["sigma_Scalp"] # Scalp
118
+
119
+ if not (self.p['anisotropy_type'] in ['iso', 'vn']):
120
+ raise NotImplementedError
121
+
122
+ if self.p['anisotropy_type'] == 'vn':
123
+ S.fname_tensor = subject.mesh[self.p['mesh_idx']]['fn_tensor_vn']
124
+ tms.fn_tensor_nifti = subject.mesh[self.p['mesh_idx']]['fn_tensor_vn']
125
+ tms.anisotropy_type = self.p['anisotropy_type']
126
+
127
+ tms.excentricity_scale = self.p["aniso"]
128
+
129
+ # Define the coil positions
130
+ #######################################################################
131
+ pos = tms.add_position()
132
+ pos.matsimnibs = copy.deepcopy(matsimnibs)
133
+ pos.distance = 0.1 # distance from coil surface to head surface
134
+ pos.didt = 1e6 # in A/s (1e6 A/s = 1 A/us)
135
+ pos.postprocess = S.fields
136
+
137
+ # Running simulation
138
+ #######################################################################
139
+ print("Running computation of TMS induced electric fields.")
140
+ filenames = simnibs.run_simnibs(S, cpus=1)
141
+
142
+ # Reading results from .msh file
143
+ #######################################################################
144
+ print("Reading results from: {}".format(filenames[0]))
145
+
146
+ msh = simnibs.msh.mesh_io.read_msh(filenames[0])
147
+ msh_pyfempp = pynibs.load_mesh_msh(filenames[0])
148
+
149
+ # potential
150
+ for i in range(len(msh.nodedata)):
151
+ if msh.nodedata[i].field_name == "v":
152
+ print("Reading potential")
153
+ v = msh.nodedata[i].value.flatten()
154
+
155
+ # fields
156
+ for i in range(len(msh.elmdata)):
157
+ # save dadt also in nodes
158
+ if msh.elmdata[i].field_name == "D":
159
+ print("Reading dAdt and transforming from elements2nodes")
160
+ D = pynibs.mesh.data_elements2nodes(msh.elmdata[i].value[msh.elm.elm_type == 4,])
161
+ D = D.flatten()[:, np.newaxis]
162
+
163
+ # if msh.elmdata[i].field_name == "E":
164
+ # E_tets = msh.elmdata[i].value[msh.elm.elm_type == 4, ]
165
+ # E_tets = E_tets.flatten()[:, np.newaxis]
166
+ #
167
+ # if msh.elmdata[i].field_name == "normE":
168
+ # normE_tets = msh.elmdata[i].value[msh.elm.elm_type == 4, ]
169
+ # normE_tets = normE_tets.flatten()[:, np.newaxis]
170
+ #
171
+ # if msh.elmdata[i].field_name == "E":
172
+ # E_tris = msh.elmdata[i].value[msh.elm.elm_type == 2, ]
173
+ # E_tris = E_tris.flatten()[:, np.newaxis]
174
+ #
175
+ # if msh.elmdata[i].field_name == "normE":
176
+ # normE_tris = msh.elmdata[i].value[msh.elm.elm_type == 2, ]
177
+ # normE_tris = normE_tris.flatten()[:, np.newaxis]
178
+
179
+ # dipole position and magnitude
180
+ fn_coil_geo = glob.glob(os.path.join(S.pathfem, "*.geo"))[0]
181
+ print("Reading coil dipole information from {}".format(fn_coil_geo))
182
+ dipole_position, dipole_moment_mag = pynibs.simnibs.read_coil_geo(fn_coil_geo)
183
+
184
+ # Additional information
185
+ #######################################################################
186
+ print("Collecting simulation information")
187
+ additional_data = dict()
188
+ additional_data['info/date'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
189
+ additional_data['info/sigma_WM'] = S.poslists[0].cond[0].value # WM
190
+ additional_data['info/sigma_GM'] = S.poslists[0].cond[1].value # GM
191
+ additional_data['info/sigma_CSF'] = S.poslists[0].cond[2].value # CSF
192
+ additional_data['info/sigma_Skull'] = S.poslists[0].cond[3].value # Skull
193
+ additional_data['info/sigma_Scalp'] = S.poslists[0].cond[4].value # Scalp
194
+ additional_data['info/fn_coil'] = S.poslists[0].fnamecoil # TMS coil
195
+ additional_data['info/matsimnibs'] = matsimnibs.flatten() # coil location and orientation
196
+ additional_data['info/dIdt'] = S.poslists[0].pos[0].didt # rate of change of coil current
197
+ additional_data['info/anisotropy_type'] = S.poslists[0].anisotropy_type # type of anisotropy model
198
+ additional_data['info/fn_mesh_msh'] = S.fnamehead # mesh
199
+ additional_data["coil/dipole_position"] = dipole_position.flatten()
200
+ additional_data["coil/dipole_moment_mag"] = dipole_moment_mag.flatten()
201
+ additional_data["data/nodes/D"] = D.flatten()
202
+ # additional_data["data/tets/E"] = E_tets.flatten()
203
+ # additional_data["data/tets/normE"] = normE_tets.flatten()
204
+ # additional_data["data/tris/E"] = E_tris.flatten()
205
+ # additional_data["data/tris/normE"] = normE_tris.flatten()
206
+
207
+ # Deleting temporary files
208
+ print("Removing temporary files: {}".format(S.pathfem))
209
+ shutil.rmtree(S.pathfem)
210
+
211
+ return v, additional_data
File without changes
pynibs/muap.py ADDED
@@ -0,0 +1,392 @@
1
+ import numpy as np
2
+ from scipy import interpolate
3
+
4
+
5
+ def sfap_dip(z):
6
+ dist = 1
7
+
8
+ i = np.zeros(len(z))
9
+ i[np.isclose(z, 0, atol=0.01)] = 1
10
+ i[np.isclose(z, dist, atol=0.01)] = -1
11
+ return i
12
+
13
+
14
+ def sfap(z, sigma_i=1.01, d=55*1e-6, alpha=0.5):
15
+ """
16
+ Single fibre propagating transmembrane current (second spatial derivative of transmembrane potential).
17
+
18
+ S. D. Nandedkar and E. V. Stalberg,“Simulation of single musclefiber action potentials”
19
+ Med. Biol. Eng. Comput., vol. 21, pp. 158–165, Mar.1983.
20
+
21
+ J. Duchene and J.-Y. Hogrel,“A model of EMG generation,”
22
+ IEEETrans. Biomed. Eng., vol. 47, no. 2, pp. 192–200, Feb. 2000
23
+
24
+ Hamilton-Wright, A., & Stashuk, D. W. (2005).
25
+ Physiologically based simulation of clinical EMG signals.
26
+ IEEE Transactions on biomedical engineering, 52(2), 171-183.
27
+
28
+ Parameters
29
+ ----------
30
+ t : ndarray of float [n_t]
31
+ Time in (ms)
32
+ sigma_i : float, optional, default: 1.01
33
+ Intracellular conductivity in (S/m)
34
+ d : float, optional, default: 55*1e-6
35
+ Diameter of muscle fibre in (m)
36
+ v : float, optional, default: 1
37
+ Conduction velocity in (m/s)
38
+ alpha : float, optional, default: 0.5
39
+ Scaling factor to adjust length of AP
40
+
41
+ Returns
42
+ -------
43
+ i : ndarray of float [n_t]
44
+ Transmembrane current of muscle fibre
45
+ """
46
+ # z = v * t
47
+ z[z<0] = 0
48
+ i = (sigma_i * np.pi * d**2) * 96 * alpha**3 * z * np.exp(-alpha*z) * (alpha**2*z**2 - 6*alpha*z + 6)
49
+ # i = (sigma_i * np.pi * d**2) * 96 * (z) * (6-6*(z) + (z)**2)*np.exp(-z)
50
+ # i = (sigma_i * np.pi * d**2)/4 * 3072 * z * (z**2 - 3*z + 1.5) * np.exp(-2*z)
51
+
52
+ return i
53
+
54
+
55
+ def dipole_potential(z, loc, response):
56
+ """
57
+ Returns dipole potential at given coordinates z (interpolates given dipole potential)
58
+ """
59
+ res = np.zeros(len(z))
60
+ mask = np.logical_and(z > loc[0], z < loc[-1])
61
+ f = interpolate.interp1d(loc, response)
62
+ res[mask] = f(z[mask])
63
+
64
+ return res
65
+
66
+
67
+ def create_electrode(l_x, l_z, n_x, n_z):
68
+ """
69
+ Creates electrode coordinates
70
+
71
+ Parameters
72
+ ----------
73
+ l_x : float
74
+ X-extension of electrode in mm
75
+ l_z : float
76
+ Z-extension of electrode in mm
77
+ n_x : int
78
+ Number of point electrode in x-direction
79
+ n_z : int
80
+ Number of point electrodes in z-direction
81
+
82
+ Returns
83
+ -------
84
+ electrode_coords : ndarray of float [n_ele x 3]
85
+ Coordinates of point electrodes (x, y, z)
86
+ """
87
+
88
+ electrode_coords = np.zeros((n_x*n_z, 3))
89
+
90
+ i = 0
91
+ dx = l_x/(n_x-1)
92
+ dz = l_z/(n_z-1)
93
+
94
+ for i_x in range(n_x):
95
+ for i_z in range(n_z):
96
+ electrode_coords[i, :] = np.array([-l_x/2 + i_x*dx, 0, -l_z/2 + i_z*dz])
97
+ i += 1
98
+
99
+ return electrode_coords
100
+
101
+
102
+ def create_muscle_coords(l_x, l_y, n_x, n_y, h):
103
+ """
104
+ Create x and y coordinates of muscle fibres in muscle
105
+
106
+ Parameters
107
+ ----------
108
+ l_x : float
109
+ X-extension of muscle in mm
110
+ l_y : float
111
+ Y-extension of muscle in mm
112
+ n_x : int
113
+ Number of muscle fibres in x-direction
114
+ n_y : int
115
+ Number of muscle fibres in y-direction
116
+ h : float
117
+ Offset of muscle from electrode plane in mm
118
+
119
+ Returns
120
+ -------
121
+ muscle_coords : ndarray of float [n_muscle x 3]
122
+ Coordinates of muscle fibres in x-y plane (x, y, z)
123
+ """
124
+
125
+ muscle_coords = np.zeros((n_x*n_y, 3))
126
+
127
+ i = 0
128
+
129
+ if n_x == 1:
130
+ dx = 0
131
+ else:
132
+ dx = l_x/(n_x-1)
133
+
134
+ if n_y == 1:
135
+ dy = 0
136
+ else:
137
+ dy = l_y/(n_y-1)
138
+
139
+ for i_x in range(n_x):
140
+ for i_y in range(n_y):
141
+ muscle_coords[i, :] = np.array([-l_x/2 + i_x*dx, h + i_y*dy, 0])
142
+ i += 1
143
+
144
+ return muscle_coords
145
+
146
+
147
+ def create_muscle_fibre(x0, y0, L, n_fibre):
148
+ """
149
+ Creates muscle fibre coordinates (in z-direction)
150
+
151
+ Parameters
152
+ ----------
153
+ x0 : float
154
+ X-location of muscle fibre
155
+ y0 : float
156
+ Y-location of muscle fibre
157
+ L : float
158
+ Length of muscle fibre
159
+ n_fibre : float
160
+ Number of discrete fibre elements
161
+
162
+ Returns
163
+ -------
164
+ fibre_coords : ndarray of float [n_fibre x 3]
165
+ Coordinates of muscle fibre in z-direction (x, y, z)
166
+ """
167
+ dz_fibre = L/(n_fibre-1)
168
+ fibre_coords = np.hstack((np.ones((n_fibre, 1))*x0,
169
+ np.ones((n_fibre, 1))*y0,
170
+ (-L/2 + np.arange(n_fibre) * dz_fibre)[:, np.newaxis]))
171
+
172
+ return fibre_coords
173
+
174
+
175
+ def create_signal_matrix(T, dt, fibre_coords, z_e, v):
176
+ """
177
+ Create signal matrix containing the travelling action potential on the fibre
178
+
179
+ Parameters
180
+ ----------
181
+ T : float
182
+ Total time
183
+ dt : float
184
+ Time step
185
+ fibre_coords : ndarray of float [n_fibre x 3]
186
+ Coordinates of muscle fibre in z-direction (x, y, z)
187
+ z_e : float
188
+ Location of action potential generation
189
+ v : float
190
+ Velocity of action potential
191
+
192
+ Returns
193
+ -------
194
+ signal_matrix : ndarray of float [n_time x n_fibre]
195
+ Signal matrix containing the action potential values for each time step in the rows
196
+ """
197
+ N_t = int(T/dt + 1) # number of time-steps
198
+ t = np.linspace(0, T, N_t)
199
+ n_fibre = fibre_coords.shape[0]
200
+ dz_fibre = np.abs(fibre_coords[0, 2] - fibre_coords[1, 2])
201
+ signal_matrix = np.zeros((N_t, n_fibre))
202
+ z_e_idx = np.argmin(np.abs(fibre_coords[:, 2] - z_e))
203
+ z_r = fibre_coords[z_e_idx:, 2] - fibre_coords[-1, 2]
204
+ z_l = fibre_coords[:z_e_idx, 2] - fibre_coords[z_e_idx, 2] # np.arange(-(L/2 + z_e), 0, dz_fibre)
205
+
206
+ for i in range(N_t):
207
+ z_l += v*dt
208
+ z_r += v*dt
209
+
210
+ # ap_r = sfap_dip(z=z_r)
211
+ # ap_l = sfap_dip(z=z_l)
212
+
213
+ ap_r = sfap(t=z_r, v=1)
214
+ ap_l = sfap(t=z_l, v=1)
215
+
216
+ signal_matrix[i, :z_e_idx] = ap_l
217
+ signal_matrix[i, z_e_idx:] = np.flip(ap_r)
218
+
219
+ return signal_matrix, t, fibre_coords[:, 2]
220
+
221
+
222
+ def hermite_rodriguez_1st(t, tau0=0, tau=0, lam=0.002):
223
+ """
224
+ First order Hermite Rodriguez function to model surface MUAPs
225
+
226
+ Parameters
227
+ ----------
228
+ t : ndarray of float [n_t]
229
+ Time axis in s
230
+ tau0 : float, optional, default: 0
231
+ initial shift to ensure causality in s
232
+ tau : float, optional, default: 0
233
+ shift (firing time) in s
234
+ lam : float, optional, default: 2
235
+ Timescale in s
236
+
237
+ Returns
238
+ -------
239
+ y : ndarray of float [n_t]
240
+ Surface MUAP
241
+ """
242
+
243
+ return -(t-tau0-tau) * np.exp(-((t-tau0-tau)/lam)**2)
244
+
245
+
246
+ def weight_signal_matrix(signal_matrix, fn_imp, t, z):
247
+ """
248
+ Weight signal matrix with impulse response from single dipole at every location
249
+ """
250
+ imp = np.loadtxt(fn_imp)
251
+ imp[:, 0:3] = imp[:, 0:3]*1000
252
+ imp[:, 3] = imp[:, 3]/np.max(imp[:, 3])
253
+ signal_matrix_weighted = np.zeros((signal_matrix.shape[0], signal_matrix.shape[1], signal_matrix.shape[1]))
254
+
255
+ for i_t in range(signal_matrix.shape[0]):
256
+ print(f"{i_t}/{signal_matrix.shape[0]}")
257
+ for i_z, z_w in enumerate(z):
258
+ ir_interp = dipole_potential(z=z-z_w, loc=imp[:, 2], response=imp[:, 3])
259
+ signal_matrix_weighted[i_t, i_z, :] = signal_matrix[i_t, i_z] * ir_interp
260
+
261
+ signal_matrix_weighted = np.sum(signal_matrix_weighted, axis=1)
262
+
263
+ return signal_matrix_weighted
264
+
265
+
266
+ def create_sensor_matrix(electrode_coords, fibre_coords, sigma_r=1, sigma_z=1):
267
+ """
268
+ Create sensor matrix containing the inverse distances from the point electrodes to the fibre elements
269
+ weighted by the anisotropy factor of the muscle tissue.
270
+
271
+ Parameters
272
+ ----------
273
+ electrode_coords : ndarray of float [n_ele x 3]
274
+ Coordinates of point electrodes (x, y, z)
275
+ fibre_coords : ndarray of float [n_fibre x 3]
276
+ Coordinates of muscle fibre in z-direction (x, y, z)
277
+ sigma_r : float, optional, default: 1
278
+ Radial conductivity of muscle
279
+ sigma_z : float, optional, default: 1
280
+ Axial conductivity of muscle along fibre
281
+
282
+ Returns
283
+ -------
284
+ sensor_matrix : ndarray of float [n_fibre x n_ele]
285
+ Sensor matrix containing the inverse distances weighted with the anisotropy of muscle tissue
286
+ """
287
+ sigma_factor = sigma_z/sigma_r
288
+
289
+ sensor_matrix = np.zeros((fibre_coords.shape[0], electrode_coords.shape[0]))
290
+
291
+ for i in range(electrode_coords.shape[0]):
292
+ r_f = np.linalg.norm(electrode_coords[i, :2] - fibre_coords[:, :2], axis=1)
293
+
294
+ # sensor_matrix[:, i] = 1/np.linalg.norm(fibre_coords-electrode_coords[i, :], axis=1)
295
+ sensor_matrix[:, i] = 1/np.sqrt(sigma_factor*r_f**2 + (fibre_coords[:, 2]-electrode_coords[i, 2])**2)
296
+
297
+ return sensor_matrix
298
+
299
+
300
+ def compute_signal(signal_matrix, sensor_matrix):
301
+ """
302
+ Determine average signal from one single muscle fibre on all point electrodes
303
+
304
+ Parameters
305
+ ----------
306
+ signal_matrix : ndarray of float [n_time x n_fibre]
307
+ Signal matrix containing the action potential values for each time step in the rows
308
+ sensor_matrix : ndarray of float [n_fibre x n_ele]
309
+ Sensor matrix containing the inverse distances weighted with the anisotropy of muscle tissue
310
+
311
+ Returns
312
+ -------
313
+ signal : ndarray of float [n_time]
314
+ Average signal detected all point electrodes
315
+ """
316
+ signal = np.mean(np.matmul(signal_matrix, sensor_matrix), axis=1)
317
+
318
+ return signal
319
+
320
+
321
+ def calc_mep_wilson(firing_rate_in, t, Qvmax=900, Qmmax=300, q=8, Tmin=14, N=100, M0=42, lam=0.002, tau0=0.006):
322
+ """
323
+ Determine motor evoked potential from incoming firing rate
324
+
325
+ Parameters
326
+ ----------
327
+ firing_rate_in : ndarray of float [n_t]
328
+ Input firing rate from alpha motor neurons
329
+ t : ndarray of float [n_t]
330
+ Time axis in s
331
+ Qvmax : float, optional, default: 900
332
+ Max of incoming firing rate [1/s]
333
+ Qmmax : float, optional, default: 300
334
+ Max of MU firing rate [1/s]
335
+ q : float, optional, default: 8
336
+ Min firing rate of MU [1/s]
337
+ Tmin : float, optional, default: 14
338
+ Min MU threshold [1/s]
339
+ N : float, optional, default: 100
340
+ Number of MU
341
+ M0 : float, optional, default: 42
342
+ Scaling constant of MU amplitude [mV/s]
343
+ lam : float, optional, default: 0.002
344
+ MUAP timescale of first order Hermite Rodriguez function [s]
345
+ tau0 : float, optional, default: 0.006
346
+ Standard shift of MUAP to ensure causality [s]
347
+
348
+ Returns
349
+ -------
350
+ mep : ndarray of float [n_t]
351
+ Motor evoked potential at surface electrode
352
+ """
353
+ dt = t[1] - t[0] # time step
354
+ k = np.arange(N)+1 # MU index
355
+ firing_rate_in_mat = np.repeat(firing_rate_in[np.newaxis, :], N, axis=0)
356
+
357
+ # determine MU thresholds
358
+ alpha = 1/N*np.log(Qvmax/Tmin)
359
+ Tk = Tmin * np.exp(alpha*k)
360
+
361
+ # determine MU amplitude
362
+ Mk = M0 * np.exp(alpha*k)
363
+
364
+ # determine spike rate of MUs from incoming spike rate
365
+ kappak = (Qmmax - q) / (Qvmax - Tk)
366
+ Qk = q + kappak[:, np.newaxis] * (firing_rate_in_mat - Tk[:, np.newaxis])
367
+ Qk[firing_rate_in_mat < Tk[:, np.newaxis]] = 0
368
+
369
+ # determine spike times of MUs by integrating the spike rates
370
+ Qk_int = np.cumsum(Qk, axis=1) * dt
371
+
372
+ spike_times = []
373
+ for k in range(N):
374
+ Qk_int_max = int(np.floor(Qk_int[k, -1]))
375
+
376
+ if Qk_int_max > 0:
377
+ spike_times.append(np.zeros(Qk_int_max))
378
+
379
+ for j in range(Qk_int_max):
380
+ spike_times[k][j] = t[np.where(Qk_int[k, :] > (j+1))[0][0]]
381
+ else:
382
+ spike_times.append([])
383
+
384
+ # determine EMG signal by summing up all MUAPS at determined firing times
385
+ mep = np.zeros(len(t))
386
+
387
+ for k in range(N):
388
+ if len(spike_times[k]) > 0:
389
+ for tau in spike_times[k]:
390
+ mep += Mk[k] * hermite_rodriguez_1st(t=t, tau0=tau0, tau=tau, lam=lam)
391
+
392
+ return mep
@@ -0,0 +1,2 @@
1
+ from .neuron_regression import *
2
+ from .util import *