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.
- pyNIBS-0.2024.8.dist-info/LICENSE +623 -0
- pyNIBS-0.2024.8.dist-info/METADATA +723 -0
- pyNIBS-0.2024.8.dist-info/RECORD +107 -0
- pyNIBS-0.2024.8.dist-info/WHEEL +5 -0
- pyNIBS-0.2024.8.dist-info/top_level.txt +1 -0
- pynibs/__init__.py +34 -0
- pynibs/coil.py +1367 -0
- pynibs/congruence/__init__.py +15 -0
- pynibs/congruence/congruence.py +1108 -0
- pynibs/congruence/ext_metrics.py +257 -0
- pynibs/congruence/stimulation_threshold.py +318 -0
- pynibs/data/configuration_exp0.yaml +59 -0
- pynibs/data/configuration_linear_MEP.yaml +61 -0
- pynibs/data/configuration_linear_RT.yaml +61 -0
- pynibs/data/configuration_sigmoid4.yaml +68 -0
- pynibs/data/network mapping configuration/configuration guide.md +238 -0
- pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +42 -0
- pynibs/data/network mapping configuration/configuration_for_testing.yaml +43 -0
- pynibs/data/network mapping configuration/configuration_modelTMS.yaml +43 -0
- pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +43 -0
- pynibs/data/network mapping configuration/output_documentation.md +185 -0
- pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +77 -0
- pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +1281 -0
- pynibs/expio/Mep.py +1518 -0
- pynibs/expio/__init__.py +8 -0
- pynibs/expio/brainsight.py +979 -0
- pynibs/expio/brainvis.py +71 -0
- pynibs/expio/cobot.py +239 -0
- pynibs/expio/exp.py +1876 -0
- pynibs/expio/fit_funs.py +287 -0
- pynibs/expio/localite.py +1987 -0
- pynibs/expio/signal_ced.py +51 -0
- pynibs/expio/visor.py +624 -0
- pynibs/freesurfer.py +502 -0
- pynibs/hdf5_io/__init__.py +10 -0
- pynibs/hdf5_io/hdf5_io.py +1857 -0
- pynibs/hdf5_io/xdmf.py +1542 -0
- pynibs/mesh/__init__.py +3 -0
- pynibs/mesh/mesh_struct.py +1394 -0
- pynibs/mesh/transformations.py +866 -0
- pynibs/mesh/utils.py +1103 -0
- pynibs/models/_TMS.py +211 -0
- pynibs/models/__init__.py +0 -0
- pynibs/muap.py +392 -0
- pynibs/neuron/__init__.py +2 -0
- pynibs/neuron/neuron_regression.py +284 -0
- pynibs/neuron/util.py +58 -0
- pynibs/optimization/__init__.py +5 -0
- pynibs/optimization/multichannel.py +278 -0
- pynibs/optimization/opt_mep.py +152 -0
- pynibs/optimization/optimization.py +1445 -0
- pynibs/optimization/workhorses.py +698 -0
- pynibs/pckg/__init__.py +0 -0
- pynibs/pckg/biosig/biosig4c++-1.9.5.src_fixed.tar.gz +0 -0
- pynibs/pckg/libeep/__init__.py +0 -0
- pynibs/pckg/libeep/pyeep.so +0 -0
- pynibs/regression/__init__.py +11 -0
- pynibs/regression/dual_node_detection.py +2375 -0
- pynibs/regression/regression.py +2984 -0
- pynibs/regression/score_types.py +0 -0
- pynibs/roi/__init__.py +2 -0
- pynibs/roi/roi.py +895 -0
- pynibs/roi/roi_structs.py +1233 -0
- pynibs/subject.py +1009 -0
- pynibs/tensor_scaling.py +144 -0
- pynibs/tests/data/InstrumentMarker20200225163611937.xml +19 -0
- pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +14 -0
- pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +6373 -0
- pynibs/tests/data/Xdmf.dtd +89 -0
- pynibs/tests/data/brainsight_niiImage_nifticoord.txt +145 -0
- pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +1434 -0
- pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +47 -0
- pynibs/tests/data/create_subject_testsub.py +332 -0
- pynibs/tests/data/data.hdf5 +0 -0
- pynibs/tests/data/geo.hdf5 +0 -0
- pynibs/tests/test_coil.py +474 -0
- pynibs/tests/test_elements2nodes.py +100 -0
- pynibs/tests/test_hdf5_io/test_xdmf.py +61 -0
- pynibs/tests/test_mesh_transformations.py +123 -0
- pynibs/tests/test_mesh_utils.py +143 -0
- pynibs/tests/test_nnav_imports.py +101 -0
- pynibs/tests/test_quality_measures.py +117 -0
- pynibs/tests/test_regressdata.py +289 -0
- pynibs/tests/test_roi.py +17 -0
- pynibs/tests/test_rotations.py +86 -0
- pynibs/tests/test_subject.py +71 -0
- pynibs/tests/test_util.py +24 -0
- pynibs/tms_pulse.py +34 -0
- pynibs/util/__init__.py +4 -0
- pynibs/util/dosing.py +233 -0
- pynibs/util/quality_measures.py +562 -0
- pynibs/util/rotations.py +340 -0
- pynibs/util/simnibs.py +763 -0
- pynibs/util/util.py +727 -0
- pynibs/visualization/__init__.py +2 -0
- pynibs/visualization/para.py +4372 -0
- pynibs/visualization/plot_2D.py +137 -0
- 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
|