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/util/simnibs.py
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
import h5py
|
|
7
|
+
import tqdm
|
|
8
|
+
import trimesh
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import simnibs
|
|
12
|
+
except (ModuleNotFoundError, ImportError):
|
|
13
|
+
print("SimNIBS not found. Some functions might not work.")
|
|
14
|
+
import warnings
|
|
15
|
+
import collections
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
import pynibs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def e_field_gradient_between_wm_gm(
|
|
22
|
+
roi_surf,
|
|
23
|
+
mesh,
|
|
24
|
+
gm_nodes,
|
|
25
|
+
wm_nodes,
|
|
26
|
+
gm_center_distance,
|
|
27
|
+
wm_center_distance
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Compute local the E-field gradient at the ROI nodes between the gray and white matter boundary sufaces.
|
|
31
|
+
Adapted from neuronibs/cortical_layer.py/add_e_field_gradient_between_wm_gm_field
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
roi_surf : simnibs.Msh
|
|
36
|
+
The surface object representing the ROI.
|
|
37
|
+
mesh : simnibs.Msh
|
|
38
|
+
SimNIBS headmesh.
|
|
39
|
+
gm_nodes : np.ndarray, (3, len(roi_surf.nodes))
|
|
40
|
+
For each node of 'roi_surf' (representing, e.g., a cortical layer) the corresponding point on the gray matter surface.
|
|
41
|
+
-> as returned by 'precompute_geo_info_for_layer_field_interpolation'
|
|
42
|
+
wm_nodes : np.ndarray, (3, len(roi_surf.nodes))
|
|
43
|
+
For each node of 'roi_surf' (representing, e.g., a cortical layer) the corresponding point on the white matter surface.
|
|
44
|
+
-> as returned by 'precompute_geo_info_for_layer_field_interpolation'
|
|
45
|
+
gm_center_distance : np.ndarray, (3, len(roi_surf.nodes))
|
|
46
|
+
The distance between the layer nodes and their corresponding point on the gray matter surface.
|
|
47
|
+
-> as returned by 'precompute_geo_info_for_layer_field_interpolation'
|
|
48
|
+
wm_center_distance : np.ndarray, (3, len(roi_surf.nodes))
|
|
49
|
+
The distance between the layer nodes and their corresponding point on the white matter surface.
|
|
50
|
+
-> as returned by 'precompute_geo_info_for_layer_field_interpolation'
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
e_field_gradient_per_mm : simnibs.mesh.mesh_io.ElementData
|
|
55
|
+
The E-field gradient from the GM to the WM surface normalized to 1 mm with respect to the gray matter thickness.
|
|
56
|
+
per ROI node
|
|
57
|
+
"""
|
|
58
|
+
roi_centers = roi_surf.elements_baricenters().value
|
|
59
|
+
|
|
60
|
+
# concatenate the point lists to avoid re-building the (simnibs-internal) data structures
|
|
61
|
+
# for interpolation upon consecutive calls
|
|
62
|
+
to_be_interpolated = np.concatenate((gm_nodes, roi_centers, wm_nodes))
|
|
63
|
+
try:
|
|
64
|
+
interpolated = mesh.field['magnE'].interpolate_scattered(to_be_interpolated)
|
|
65
|
+
except KeyError:
|
|
66
|
+
e_mag = mesh.field['E'].norm()
|
|
67
|
+
interpolated = e_mag.interpolate_scattered(to_be_interpolated)
|
|
68
|
+
|
|
69
|
+
num_nodes = gm_nodes.shape[0]
|
|
70
|
+
num_nodes_x2 = num_nodes * 2
|
|
71
|
+
num_nodes_x3 = num_nodes * 3
|
|
72
|
+
e_field_gm = interpolated[:num_nodes]
|
|
73
|
+
e_field_center = interpolated[num_nodes:num_nodes_x2]
|
|
74
|
+
e_field_wm = interpolated[num_nodes_x2:num_nodes_x3]
|
|
75
|
+
|
|
76
|
+
e_field_gradient = e_field_gm - e_field_wm
|
|
77
|
+
gray_matter_thickness = gm_center_distance + wm_center_distance
|
|
78
|
+
e_field_gradient_per_mm = e_field_gradient / gray_matter_thickness
|
|
79
|
+
relative_e_field_gradient_per_mm = e_field_gradient_per_mm / e_field_center * 100
|
|
80
|
+
|
|
81
|
+
return simnibs.ElementData(relative_e_field_gradient_per_mm, name='rel_gradient_per_mm', mesh=roi_surf)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def e_field_angle_theta(surface, mesh):
|
|
85
|
+
"""
|
|
86
|
+
Compute angle between local the E-field vector and surface vector at the ROI nodes.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
surface : simnibs.Msh
|
|
91
|
+
The surface object representing the ROI.
|
|
92
|
+
mesh: simnibs.Msh
|
|
93
|
+
The (volumetric) head mesh.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
theta : simnibs.mesh.mesh_io.ElementData
|
|
98
|
+
The angle between local the E-field vector and surface vector for each surface element of the ROI.
|
|
99
|
+
"""
|
|
100
|
+
layer_centers = surface.elements_baricenters().value
|
|
101
|
+
interpolated_cell_centers = mesh.field['E'].interpolate_scattered(layer_centers, out_fill='nearest')
|
|
102
|
+
tri_normal = surface.triangle_normals(smooth=30).value
|
|
103
|
+
e_at_roi_cell_centers_normalized = np.divide(
|
|
104
|
+
interpolated_cell_centers,
|
|
105
|
+
np.linalg.norm(interpolated_cell_centers, axis=1)[:, np.newaxis]
|
|
106
|
+
)
|
|
107
|
+
angle = np.array([np.arccos(np.dot(a, b)) / np.pi * 180 for a, b in
|
|
108
|
+
zip(tri_normal, e_at_roi_cell_centers_normalized)])
|
|
109
|
+
|
|
110
|
+
return simnibs.ElementData(angle, name="theta", mesh=surface)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def precompute_geo_info_for_layer_field_interpolation(simnibs_mesh, roi):
|
|
114
|
+
"""
|
|
115
|
+
Precomputes geometric properties of the corresponding GM and WM nodes
|
|
116
|
+
in the 'simnibs_mesh' of each node of each layer in ``'roi'``.
|
|
117
|
+
|
|
118
|
+
* The corresponding point on the GM and WM surface to each vertex of te ROI surface
|
|
119
|
+
are determined (by raycasting and nearest neighbour search as fallback)
|
|
120
|
+
(interpolation will take place between these nodes)
|
|
121
|
+
* The nodes are moved inside the gray matter by 20 % of their total distance from the GM/WM
|
|
122
|
+
boundary to the midlayer.
|
|
123
|
+
* The distance of the relocated GM and WM nodes to the ROI nodes is determined (required to
|
|
124
|
+
computed the gradient per mm).
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
simnibs_mesh : simnibs.Msh
|
|
129
|
+
The head model volume mesh.
|
|
130
|
+
roi: pynibs.roi.RegionOfInterestSurface
|
|
131
|
+
The ROI object containing the layers.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
layer_gm_wm_info : dict[str, dict[str,np.ndarray]]
|
|
136
|
+
For each layer defined in the mesh (outer dict key: layer_id),
|
|
137
|
+
provide pre-computed geometrical information as a dictionary (outer dict value):
|
|
138
|
+
|
|
139
|
+
* key: gm_nodes
|
|
140
|
+
For each layer node, the corresponding point on the GM surface.
|
|
141
|
+
* key: wm_nodes
|
|
142
|
+
For each layer node, the corresponding point on the WM surface.
|
|
143
|
+
* key: gm_center_distance
|
|
144
|
+
The distance between the layer nodes and the corresponding points on the GM.
|
|
145
|
+
* key: wm_center_distance
|
|
146
|
+
The distance between the layer nodes and the corresponding points on the WM.
|
|
147
|
+
"""
|
|
148
|
+
WHITE_MATTER_SURFACE_LABEL = 1001
|
|
149
|
+
GRAY_MATTER_SURFACE_LABEL = 1002
|
|
150
|
+
|
|
151
|
+
import vtk
|
|
152
|
+
|
|
153
|
+
def make_vtkpolydata(pts, tris):
|
|
154
|
+
# prepare vertices
|
|
155
|
+
pts_vtk = vtk.vtkPoints()
|
|
156
|
+
pts_vtk.SetNumberOfPoints(pts.shape[0])
|
|
157
|
+
for i in range(pts.shape[0]):
|
|
158
|
+
pts_vtk.SetPoint(i, pts[i][0], pts[i][1], pts[i][2])
|
|
159
|
+
|
|
160
|
+
# prepare triangles
|
|
161
|
+
tris_vtk = vtk.vtkCellArray()
|
|
162
|
+
for tri in tris:
|
|
163
|
+
tris_vtk.InsertNextCell(3)
|
|
164
|
+
for v in tri:
|
|
165
|
+
tris_vtk.InsertCellPoint(v)
|
|
166
|
+
|
|
167
|
+
# prepare GM polygonal surface
|
|
168
|
+
surf_vtk = vtk.vtkPolyData()
|
|
169
|
+
surf_vtk.SetPoints(pts_vtk)
|
|
170
|
+
surf_vtk.SetPolys(tris_vtk)
|
|
171
|
+
|
|
172
|
+
return surf_vtk
|
|
173
|
+
|
|
174
|
+
gray_matter_surface = simnibs_mesh.crop_mesh(GRAY_MATTER_SURFACE_LABEL)
|
|
175
|
+
white_matter_surface = simnibs_mesh.crop_mesh(WHITE_MATTER_SURFACE_LABEL)
|
|
176
|
+
|
|
177
|
+
gm_nodes = gray_matter_surface.nodes.node_coord
|
|
178
|
+
gm_tris = gray_matter_surface.elm.node_number_list
|
|
179
|
+
wm_nodes = white_matter_surface.nodes.node_coord
|
|
180
|
+
wm_tris = white_matter_surface.elm.node_number_list
|
|
181
|
+
|
|
182
|
+
# for each point on the layer find corresponding points on the GM/WM surfaces
|
|
183
|
+
# using the VTK implementation of Raycasting
|
|
184
|
+
# make zero indexed and shape (n,3) instead of (n,4), with -1 in the 4th column
|
|
185
|
+
gm_surf_vtk = make_vtkpolydata(gm_nodes, gm_tris[:, :3] - 1)
|
|
186
|
+
wm_surf_vtk = make_vtkpolydata(wm_nodes, wm_tris[:, :3] - 1)
|
|
187
|
+
|
|
188
|
+
gm_intersector = vtk.vtkOBBTree()
|
|
189
|
+
gm_intersector.SetDataSet(gm_surf_vtk)
|
|
190
|
+
gm_intersector.BuildLocator()
|
|
191
|
+
|
|
192
|
+
wm_intersector = vtk.vtkOBBTree()
|
|
193
|
+
wm_intersector.SetDataSet(wm_surf_vtk)
|
|
194
|
+
wm_intersector.BuildLocator()
|
|
195
|
+
|
|
196
|
+
intersectors = [gm_intersector, wm_intersector]
|
|
197
|
+
normal_sign = [1, -1]
|
|
198
|
+
|
|
199
|
+
layer_gm_wm_info = dict()
|
|
200
|
+
for layer_idx in range(len(roi.layers)):
|
|
201
|
+
intersec_pts = [[], []]
|
|
202
|
+
layer_id = roi.layers[layer_idx].id
|
|
203
|
+
layer_surf = roi.layers[layer_idx].surface
|
|
204
|
+
layer_centers = layer_surf.elements_baricenters().value
|
|
205
|
+
layer_normals = roi.layers[layer_idx].get_smoothed_normals()
|
|
206
|
+
|
|
207
|
+
for pt, normal in zip(layer_centers, layer_normals):
|
|
208
|
+
for idx in range(len(intersectors)):
|
|
209
|
+
intersection_pts = vtk.vtkPoints()
|
|
210
|
+
intersected_tris = vtk.vtkIdList()
|
|
211
|
+
|
|
212
|
+
intersector = intersectors[idx]
|
|
213
|
+
|
|
214
|
+
intersector.IntersectWithLine(pt, normal_sign[idx] * 100 * normal + pt, intersection_pts,
|
|
215
|
+
intersected_tris)
|
|
216
|
+
if intersection_pts.GetNumberOfPoints() > 0:
|
|
217
|
+
intersec_pts[idx].append(intersection_pts.GetPoint(0))
|
|
218
|
+
else:
|
|
219
|
+
intersec_pts[idx].append([np.iinfo(np.uint16).max] * 3)
|
|
220
|
+
|
|
221
|
+
gm_intersec_pts = np.array(intersec_pts[0])
|
|
222
|
+
wm_intersec_pts = np.array(intersec_pts[1])
|
|
223
|
+
|
|
224
|
+
# Check the distances of the found points on the GM/WM surface to the layer nodes.
|
|
225
|
+
# If the distance is too high, raycasting failed (e.g. no intersection found or too far away).
|
|
226
|
+
# In this case, we determine the nearest neighbor for these nodes.
|
|
227
|
+
layer_gm_distance = np.linalg.norm(layer_centers - gm_intersec_pts, ord=2, axis=1)
|
|
228
|
+
layer_wm_distance = np.linalg.norm(layer_centers - wm_intersec_pts, ord=2, axis=1)
|
|
229
|
+
|
|
230
|
+
# TODO: consider using a relative distance depending on the local GM thickness
|
|
231
|
+
raycast_error_nodes_gm = np.argwhere(layer_gm_distance > 2) # mm
|
|
232
|
+
raycast_error_nodes_wm = np.argwhere(layer_wm_distance > 2) # mm
|
|
233
|
+
|
|
234
|
+
closest_gm_nodes, _ = gray_matter_surface.nodes.find_closest_node(layer_centers[raycast_error_nodes_gm],
|
|
235
|
+
return_index=True)
|
|
236
|
+
closest_wm_nodes, _ = white_matter_surface.nodes.find_closest_node(layer_centers[raycast_error_nodes_wm],
|
|
237
|
+
return_index=True)
|
|
238
|
+
|
|
239
|
+
gm_intersec_pts[raycast_error_nodes_gm] = closest_gm_nodes
|
|
240
|
+
wm_intersec_pts[raycast_error_nodes_wm] = closest_wm_nodes
|
|
241
|
+
|
|
242
|
+
layer_gm_distance = np.linalg.norm(layer_centers - gm_intersec_pts, ord=2, axis=1)
|
|
243
|
+
layer_wm_distance = np.linalg.norm(layer_centers - wm_intersec_pts, ord=2, axis=1)
|
|
244
|
+
|
|
245
|
+
center_to_gm_vecs = gm_intersec_pts - layer_centers
|
|
246
|
+
center_to_wm_vecs = wm_intersec_pts - layer_centers
|
|
247
|
+
|
|
248
|
+
associated_gray_matter_points = gm_intersec_pts - \
|
|
249
|
+
np.multiply(
|
|
250
|
+
# multiply unit-normals by individual gm thickness, then scale to 20%
|
|
251
|
+
center_to_gm_vecs,
|
|
252
|
+
layer_gm_distance[:, np.newaxis]
|
|
253
|
+
) * 0.1
|
|
254
|
+
associated_white_matter_points = wm_intersec_pts - \
|
|
255
|
+
np.multiply(
|
|
256
|
+
# multiply unit-normals by individual wm thickness, then scale to 20%
|
|
257
|
+
center_to_wm_vecs,
|
|
258
|
+
layer_wm_distance[:, np.newaxis]
|
|
259
|
+
) * 0.1
|
|
260
|
+
|
|
261
|
+
layer_gm_wm_info[layer_id] = {
|
|
262
|
+
"assoc_gm_points": associated_gray_matter_points,
|
|
263
|
+
"assoc_wm_points": associated_white_matter_points,
|
|
264
|
+
"layer_gm_dist": layer_gm_distance,
|
|
265
|
+
"layer_wm_dist": layer_wm_distance
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return layer_gm_wm_info
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def calc_e_in_midlayer_roi(
|
|
272
|
+
e,
|
|
273
|
+
roi,
|
|
274
|
+
mesh=None,
|
|
275
|
+
qoi=None,
|
|
276
|
+
layer_gm_wm_info=None
|
|
277
|
+
):
|
|
278
|
+
"""
|
|
279
|
+
This is to be called by Simnibs as postprocessing function per FEM solve.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
e : np.ndarray or tuple
|
|
284
|
+
E to interpolate. Used to be (v, dAdt).
|
|
285
|
+
roi : pynibs.roi.RegionOfInterestSurface
|
|
286
|
+
mesh : simnibs.msh.Mesh
|
|
287
|
+
qoi : list of str
|
|
288
|
+
List of identifiers of the to-be calculated quantities of interest.
|
|
289
|
+
layer_gm_wm_info : dict[str, dict[str, np.ndarray]], optional
|
|
290
|
+
For each layer defined in the mesh (outer dict key: layer_id),
|
|
291
|
+
provide pre-computed geometrical information as a dictionary (outer dict value):
|
|
292
|
+
|
|
293
|
+
* key: gm_nodes, For each layer node, the corresponding point on the GM surface.
|
|
294
|
+
* key: wm_nodes, For each layer node, the corresponding point on the WM surface.
|
|
295
|
+
* key: gm_center_distance, The distance between the layer nodes and the corresponding points on the GM.
|
|
296
|
+
* key: wm_center_distance, The distance between the layer nodes and the corresponding points on the WM.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
(roi.n_tris, 4) : np.vstack((e_mag, e_norm, e_tan, e_angle)).transpose() for the midlayer
|
|
301
|
+
(len(roi.layers)x(roi.layers[idx].surface.n_tris,4)) : np.vstack((e_mag, e_norm, e_tan, e_angle)).transpose()
|
|
302
|
+
"""
|
|
303
|
+
# set default return quantities
|
|
304
|
+
if qoi is None:
|
|
305
|
+
qoi = ['E', 'mag', 'norm', 'tan', 'angle']
|
|
306
|
+
qois_midlayer = copy.copy(qoi)
|
|
307
|
+
ret_arr_num_components = len(qoi)
|
|
308
|
+
|
|
309
|
+
# special care for E
|
|
310
|
+
if 'E' in qoi:
|
|
311
|
+
ret_arr_num_components += 2 # E is a 3D quantity whose components are stored individually
|
|
312
|
+
qois_midlayer.remove('E') # we don't need this to be computed, it already is
|
|
313
|
+
|
|
314
|
+
qois_layers = [] # the QOIs computed for the cortical layers (not midlayer)
|
|
315
|
+
max_num_elmts = roi.n_tris # number of midlayer elements to define the shape of the output data structure
|
|
316
|
+
interpolation_surfaces = { # initialize the ROI surfaces the QOIs will be interpolated onto
|
|
317
|
+
"midlayer": simnibs.Msh(
|
|
318
|
+
nodes=simnibs.Nodes(node_coord=roi.node_coord_mid),
|
|
319
|
+
elements=simnibs.Elements(triangles=roi.node_number_list + 1)
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# check if interpolation must be done for the cortical layers and initialize accordingly
|
|
324
|
+
if len(roi.layers) > 0 and layer_gm_wm_info is not None:
|
|
325
|
+
if len(layer_gm_wm_info.keys()) == len(roi.layers):
|
|
326
|
+
qois_layers = ['mag', 'theta', 'gradient'] # = parameters of the neuronal meanfield model
|
|
327
|
+
|
|
328
|
+
for layer in roi.layers:
|
|
329
|
+
# Do the cortical layers have more elements than the midlayer?
|
|
330
|
+
max_num_elmts = np.max((max_num_elmts, layer.surface.elm.node_number_list.shape[0]))
|
|
331
|
+
# Expand shape of output data structure by the number of QOIs computed for this layer
|
|
332
|
+
ret_arr_num_components += len(qois_layers)
|
|
333
|
+
# Add this layer to the interpolation surfaces.
|
|
334
|
+
interpolation_surfaces[layer.id] = layer.surface
|
|
335
|
+
else:
|
|
336
|
+
print("[calc_e_in_midlayer] Computation of parameters for neuronal meanfield model was requested,"
|
|
337
|
+
"but number of layer information in 'layer_gm_wm_info' does not match the number of layers"
|
|
338
|
+
"found in the mesh. Skipping computation of neuronal model parameters...")
|
|
339
|
+
|
|
340
|
+
# We used to use (v, dadt) but nowadays E is enough
|
|
341
|
+
if isinstance(e, tuple):
|
|
342
|
+
e = e[0]
|
|
343
|
+
# simnibs calls this with empty data so find out about the results dimensions
|
|
344
|
+
if (e == 0).all():
|
|
345
|
+
return np.zeros((max_num_elmts, ret_arr_num_components))
|
|
346
|
+
|
|
347
|
+
# calc QOIs (all calculations are performed in the element centers)
|
|
348
|
+
data = simnibs.ElementData(e, name='E', mesh=mesh)
|
|
349
|
+
|
|
350
|
+
interp_res_per_surface = dict()
|
|
351
|
+
mesh.elmdata.append(simnibs.ElementData(e, name='E', mesh=mesh))
|
|
352
|
+
|
|
353
|
+
for surface_id, surface in interpolation_surfaces.items():
|
|
354
|
+
print(f"Computing {surface_id}")
|
|
355
|
+
barycenter_surface = simnibs.Msh(
|
|
356
|
+
nodes=simnibs.Nodes(node_coord=surface.elements_baricenters().value),
|
|
357
|
+
elements=None
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# determine surface normals in elements
|
|
361
|
+
p1_tri = surface.nodes.node_coord[surface.elm.node_number_list[:, 0] - 1, :]
|
|
362
|
+
p2_tri = surface.nodes.node_coord[surface.elm.node_number_list[:, 1] - 1, :]
|
|
363
|
+
p3_tri = surface.nodes.node_coord[surface.elm.node_number_list[:, 2] - 1, :]
|
|
364
|
+
|
|
365
|
+
triangles_normals = np.cross(p2_tri - p1_tri, p3_tri - p1_tri).T
|
|
366
|
+
triangles_normals /= np.linalg.norm(triangles_normals, axis=0)
|
|
367
|
+
triangles_normals = triangles_normals.T
|
|
368
|
+
|
|
369
|
+
interpolated = data.interpolate_to_surface(barycenter_surface)
|
|
370
|
+
del barycenter_surface
|
|
371
|
+
qois_calculated = collections.OrderedDict() # maintain order of calculation with OrderedDict
|
|
372
|
+
|
|
373
|
+
if surface_id == "midlayer":
|
|
374
|
+
qois_to_calc = qois_midlayer
|
|
375
|
+
# E is the result of the FEM; all other quantities are secondary QOIs computed from E
|
|
376
|
+
if 'E' in qoi:
|
|
377
|
+
qois_calculated['E'] = interpolated.value
|
|
378
|
+
# qois_calculated['E'] = pynibs.data_nodes2elements(data=qois_calculated['E'],
|
|
379
|
+
# con=(surface.elm.node_number_list - 1))
|
|
380
|
+
elif surface_id != "midlayer" and len(qois_layers) > 0:
|
|
381
|
+
qois_to_calc = qois_layers
|
|
382
|
+
else:
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
for quant in qois_to_calc:
|
|
386
|
+
if quant == 'mag':
|
|
387
|
+
qois_calculated[quant] = interpolated.norm().value
|
|
388
|
+
elif quant == 'norm':
|
|
389
|
+
qois_calculated[quant] = np.sum(qois_calculated["E"] * triangles_normals, axis=1)
|
|
390
|
+
qois_calculated[quant] *= -1
|
|
391
|
+
elif quant == 'tan':
|
|
392
|
+
e_n = np.sum(qois_calculated["E"] * triangles_normals, axis=1)
|
|
393
|
+
e_mag = np.linalg.norm(qois_calculated["E"], axis=1)
|
|
394
|
+
qois_calculated[quant] = np.sqrt(e_mag ** 2 - e_n ** 2)
|
|
395
|
+
elif quant == 'angle':
|
|
396
|
+
qois_calculated[quant] = interpolated.angle().value
|
|
397
|
+
elif quant == 'theta': # basically 'angle' but we want it in degrees for the meanfield model
|
|
398
|
+
qois_calculated[quant] = e_field_angle_theta(surface, mesh).value
|
|
399
|
+
elif quant == 'gradient':
|
|
400
|
+
qois_calculated[quant] = e_field_gradient_between_wm_gm(
|
|
401
|
+
surface,
|
|
402
|
+
mesh,
|
|
403
|
+
layer_gm_wm_info[surface_id]["assoc_gm_points"],
|
|
404
|
+
layer_gm_wm_info[surface_id]["assoc_wm_points"],
|
|
405
|
+
layer_gm_wm_info[surface_id]["layer_gm_dist"],
|
|
406
|
+
layer_gm_wm_info[surface_id]["layer_wm_dist"],
|
|
407
|
+
).value
|
|
408
|
+
else:
|
|
409
|
+
raise ValueError('Invalid quantity: {0}'.format(quant))
|
|
410
|
+
|
|
411
|
+
# - wrap 1D data in array
|
|
412
|
+
try:
|
|
413
|
+
if qois_calculated[quant].ndim == 1:
|
|
414
|
+
qois_calculated[quant] = qois_calculated[quant][:, np.newaxis]
|
|
415
|
+
except KeyError:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# OrderedDict has maintained the order of the QOI-list.
|
|
419
|
+
interp_res_per_surface[surface_id] = [*qois_calculated.values()]
|
|
420
|
+
|
|
421
|
+
mesh.elmdata.pop()
|
|
422
|
+
del qois_calculated, surface, interpolated, mesh, data, e, roi, triangles_normals
|
|
423
|
+
gc.collect()
|
|
424
|
+
|
|
425
|
+
# 0-pad midlayer & layer data to have the same number of rows (hstack not possible otherwise),
|
|
426
|
+
# according to the layer with the highest number of elements.
|
|
427
|
+
for surface_id in interp_res_per_surface.keys():
|
|
428
|
+
num_values_to_be_padded = max_num_elmts - interp_res_per_surface[surface_id][0].shape[0]
|
|
429
|
+
for q in range(len(interp_res_per_surface[surface_id])):
|
|
430
|
+
interp_res_per_surface[surface_id][q] = np.pad(interp_res_per_surface[surface_id][q],
|
|
431
|
+
((0, num_values_to_be_padded), (0, 0)))
|
|
432
|
+
|
|
433
|
+
# Stack all result fields column-wise; each column = 1-result array (or in case of E, a component of E)
|
|
434
|
+
# For midlayer + 5 layers, there will be 21 columns and max_num_elements rows
|
|
435
|
+
# 21 = midlayer(3x for E, 1x each for magE, tanE, normE) + 3*layer(1x each for magE, theta, gradient)
|
|
436
|
+
return np.hstack([np.hstack(surf_data) for surf_data in interp_res_per_surface.values()])
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def read_coil_geo(fn_coil_geo):
|
|
440
|
+
"""
|
|
441
|
+
Reads a coil .geo file.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
fn_coil_geo : str
|
|
446
|
+
Filename of .geo file created from SimNIBS containing the dipole information
|
|
447
|
+
|
|
448
|
+
This reads data from lines like this:
|
|
449
|
+
SP(-5.906416407434245, -56.83325018618547, 104.15283927746198){0.0};
|
|
450
|
+
or
|
|
451
|
+
VP(-70.46122751969759, -68.28080005454271, 29.538763084748382){-0.10087956492712635, 0.0, -0.9948986447775034};
|
|
452
|
+
|
|
453
|
+
Returns
|
|
454
|
+
-------
|
|
455
|
+
dipole_pos : np.ndarray of float
|
|
456
|
+
(n_dip, 3) Dipole positions ``(x, y, z)``.
|
|
457
|
+
dipole_mag : np.ndarray of float
|
|
458
|
+
(n_dip, 1) Dipole magnitude.
|
|
459
|
+
"""
|
|
460
|
+
regex = r"(S|V)P\((.*?)\)\{(.*?)\}"
|
|
461
|
+
with open(fn_coil_geo, 'r') as f:
|
|
462
|
+
dipole_pos = []
|
|
463
|
+
dipole_mag = []
|
|
464
|
+
while f:
|
|
465
|
+
te = f.readline()
|
|
466
|
+
|
|
467
|
+
if te == "":
|
|
468
|
+
break
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
_, pos, mag_or_vec = re.findall(regex, te)[0]
|
|
472
|
+
pos = [float(x) for x in pos.split(',')]
|
|
473
|
+
mag_or_vec = [float(f) for f in mag_or_vec.split(', ')]
|
|
474
|
+
if len(mag_or_vec) > 1:
|
|
475
|
+
mag_or_vec = np.linalg.norm(mag_or_vec)
|
|
476
|
+
# else:
|
|
477
|
+
# mag_or_vec = mag_or_vec[0]
|
|
478
|
+
dipole_pos.append(pos)
|
|
479
|
+
dipole_mag.append(mag_or_vec)
|
|
480
|
+
except IndexError:
|
|
481
|
+
pass
|
|
482
|
+
|
|
483
|
+
dipole_pos = np.vstack(dipole_pos)
|
|
484
|
+
dipole_mag = np.atleast_2d(dipole_mag)
|
|
485
|
+
|
|
486
|
+
return dipole_pos, dipole_mag
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def check_mesh(mesh, verbose=False):
|
|
490
|
+
"""
|
|
491
|
+
Check a simmibs.Mesh for degenerated elements:
|
|
492
|
+
|
|
493
|
+
* zero surface triangles
|
|
494
|
+
* zerso volume tetrahedra
|
|
495
|
+
* negative volume tetrahedra
|
|
496
|
+
|
|
497
|
+
Parameters
|
|
498
|
+
----------
|
|
499
|
+
mesh : str or simnibs.Mesh
|
|
500
|
+
|
|
501
|
+
Other parameters
|
|
502
|
+
----------------
|
|
503
|
+
verbose : book, default: False
|
|
504
|
+
Print some verbosity messages.
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
zero_tris : np.ndarray
|
|
509
|
+
Element indices for zero surface tris (0-indexed)
|
|
510
|
+
zero_tets : np.ndarray
|
|
511
|
+
Element indices for zero volume tets (0-indexed)
|
|
512
|
+
neg_tets : np.ndarray
|
|
513
|
+
Element indicies for negative volume tets (0-indexed)
|
|
514
|
+
"""
|
|
515
|
+
if isinstance(mesh, str):
|
|
516
|
+
mesh = simnibs.read_msh(mesh)
|
|
517
|
+
tris = mesh.elm.node_number_list[mesh.elm.triangles - 1][:, :3]
|
|
518
|
+
points_tri = mesh.nodes[tris]
|
|
519
|
+
tri_area = pynibs.calc_tri_surface(points_tri)
|
|
520
|
+
zero_tris = np.argwhere(np.isclose(tri_area, 0, atol=1e-13))
|
|
521
|
+
if verbose:
|
|
522
|
+
print(f"{len(zero_tris)} zero surface triangles found.")
|
|
523
|
+
|
|
524
|
+
tets = mesh.elm.node_number_list[mesh.elm.tetrahedra - 1]
|
|
525
|
+
points_tets = mesh.nodes[tets]
|
|
526
|
+
tets_volume = pynibs.calc_tet_volume(points_tets)
|
|
527
|
+
zero_tets = np.argwhere(np.isclose(tets_volume, 0, atol=1e-13))
|
|
528
|
+
if verbose:
|
|
529
|
+
print(f"{len(zero_tets)} zero volume tetrahedra found.")
|
|
530
|
+
|
|
531
|
+
tet_idx = mesh.elm.node_number_list[mesh.elm.tetrahedra - 1]
|
|
532
|
+
vol = pynibs.calc_tet_volume(mesh.nodes.node_coord[tet_idx - 1], abs=False)
|
|
533
|
+
neg_idx = np.argwhere(vol > 0)
|
|
534
|
+
if verbose:
|
|
535
|
+
print(f"{len(neg_idx)} negative tets found.")
|
|
536
|
+
neg_idx_in_full_arr = mesh.elm.tetrahedra[neg_idx] - 1
|
|
537
|
+
|
|
538
|
+
return zero_tris, zero_tets + len(mesh.elm.triangles), neg_idx_in_full_arr
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def fix_mesh(mesh, verbose=False):
|
|
542
|
+
"""
|
|
543
|
+
Fixes simnibs.Mesh by removing any zero surface tris and zero volume tets and by fixing negative volume tets.
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
mesh : str or simnibs.Mesh
|
|
548
|
+
Filename of mesh or mesh object.
|
|
549
|
+
|
|
550
|
+
Other parameters
|
|
551
|
+
----------------
|
|
552
|
+
verbose : bool, default: False
|
|
553
|
+
Print some verbosity messages.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
fixed_mesh : simnibs.Mesh
|
|
558
|
+
"""
|
|
559
|
+
if isinstance(mesh, str):
|
|
560
|
+
mesh = simnibs.read_msh(mesh)
|
|
561
|
+
|
|
562
|
+
zero_tris, zero_tets, neg_tets = check_mesh(mesh, verbose=verbose)
|
|
563
|
+
|
|
564
|
+
if neg_tets.size:
|
|
565
|
+
mesh.elm.node_number_list[neg_tets, [0, 1, 2, 3]] = mesh.elm.node_number_list[neg_tets, [0, 1, 3, 2]]
|
|
566
|
+
|
|
567
|
+
if zero_tris.size:
|
|
568
|
+
mesh = mesh.remove_from_mesh(elements=zero_tris + 1)
|
|
569
|
+
|
|
570
|
+
if zero_tets.size:
|
|
571
|
+
mesh = mesh.remove_from_mesh(elements=zero_tets + 1 - zero_tris.size)
|
|
572
|
+
|
|
573
|
+
# check again
|
|
574
|
+
zero_tris, zero_tets, neg_tets = check_mesh(mesh)
|
|
575
|
+
|
|
576
|
+
if zero_tris.size or zero_tets.size or neg_tets.size:
|
|
577
|
+
warnings.warn(f"Couldn't fix mesh: zero_tris: "
|
|
578
|
+
f"{zero_tris.size} zero tris, {zero_tets.size} zero_tets, {neg_tets.size} neg_tets left over.")
|
|
579
|
+
|
|
580
|
+
return mesh
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def smooth_mesh(mesh, output_fn, smooth=.8, approach='taubin', skin_only_output=False, smooth_tissue='skin'):
|
|
584
|
+
"""
|
|
585
|
+
Smoothes the skin compartment of a simnibs mesh. Uses one of three trimesh.smoothing approaches.
|
|
586
|
+
Because tetrahedra and triangle share the same nodes, this also smoothes the volume domain.
|
|
587
|
+
|
|
588
|
+
Parameters
|
|
589
|
+
----------
|
|
590
|
+
mesh : str or simnibs.Mesh
|
|
591
|
+
output_fn : str
|
|
592
|
+
smooth : float, default: 0.8
|
|
593
|
+
Smoothing aggressiveness. ``[0, ..., 1]``.
|
|
594
|
+
approach: str, default: 'taubin'
|
|
595
|
+
Which smoothing approach to use. One of (``'taubin'``, ``'laplacian'``, ``'humphrey'``.)
|
|
596
|
+
smooth_tissue : str or list of int, default: 'skin'
|
|
597
|
+
Which tissue type to smooth. E.g. ``'gm'`` or ``[2, 1002]``.
|
|
598
|
+
|
|
599
|
+
Other parameters
|
|
600
|
+
----------------
|
|
601
|
+
skin_only_output : bool, default: True
|
|
602
|
+
If true, a skin only mesh is written out instead of the full mesh.
|
|
603
|
+
|
|
604
|
+
Returns
|
|
605
|
+
-------
|
|
606
|
+
<file> : The smoothed mesh.
|
|
607
|
+
|
|
608
|
+
.. figure:: ../../doc/images/smooth_mesh.png
|
|
609
|
+
:scale: 50 %
|
|
610
|
+
:alt: Original and smoothed surfaces and volumes.
|
|
611
|
+
|
|
612
|
+
Left: original, spiky mesh. Right: smoothed mesh.
|
|
613
|
+
"""
|
|
614
|
+
if isinstance(mesh, str):
|
|
615
|
+
mesh = simnibs.mesh_io.read_msh(mesh)
|
|
616
|
+
|
|
617
|
+
# only triangles
|
|
618
|
+
mesh_cropped = mesh.crop_mesh(elm_type=2)
|
|
619
|
+
if smooth_tissue == 'skin':
|
|
620
|
+
smooth_tissue = [5, 1005]
|
|
621
|
+
elif smooth_tissue == 'gm':
|
|
622
|
+
smooth_tissue = [2, 1002]
|
|
623
|
+
elif smooth_tissue == 'wm':
|
|
624
|
+
smooth_tissue = [1, 1001]
|
|
625
|
+
elif type(smooth_tissue) == str:
|
|
626
|
+
raise ValueError(f"Don't know smooth_tissue='{smooth_tissue}'.")
|
|
627
|
+
|
|
628
|
+
# only triangles + only specific tissue type
|
|
629
|
+
mesh_cropped = mesh_cropped.crop_mesh(smooth_tissue)
|
|
630
|
+
|
|
631
|
+
# simnibs node indexing is 1-based
|
|
632
|
+
tri_node_nr = mesh_cropped.elm.node_number_list[mesh_cropped.elm.triangles - 1] - 1
|
|
633
|
+
tri_node_nr = tri_node_nr[:, :3] # triangles only have 3 dimensions
|
|
634
|
+
|
|
635
|
+
assert output_fn.endswith('.msh'), f"Wrong file suffix: {output_fn}. Use .msh"
|
|
636
|
+
assert 0 <= smooth <= 1, f"'smooth={smooth}' parameter must be within [0,1]. "
|
|
637
|
+
|
|
638
|
+
# create a Trimesh object based on the skin tris
|
|
639
|
+
mesh_trimesh = trimesh.Trimesh(vertices=mesh_cropped.nodes.node_coord,
|
|
640
|
+
faces=tri_node_nr,
|
|
641
|
+
process=False) # process=False keeps original ordering
|
|
642
|
+
if approach == 'taubin':
|
|
643
|
+
smoothed_mesh = trimesh.smoothing.filter_taubin(mesh_trimesh.copy(), lamb=smooth) # do smoothing
|
|
644
|
+
elif approach == 'laplacian':
|
|
645
|
+
smoothed_mesh = trimesh.smoothing.filter_laplacian(mesh_trimesh.copy(), lamb=smooth) # do smoothing
|
|
646
|
+
elif approach == 'humphrey':
|
|
647
|
+
smoothed_mesh = trimesh.smoothing.filter_humphrey(mesh_trimesh.copy(), alpha=smooth) # do smoothing
|
|
648
|
+
else:
|
|
649
|
+
raise NotImplementedError(f"Approach {approach} not implemented. Use 'taubin', laplacian', or 'humphrey'.")
|
|
650
|
+
|
|
651
|
+
# find indices of nodes in original mesh
|
|
652
|
+
ind_nodes = np.in1d(mesh.nodes.node_coord[:, 0], mesh_cropped.nodes.node_coord[:, 0]) + \
|
|
653
|
+
np.in1d(mesh.nodes.node_coord[:, 1], mesh_cropped.nodes.node_coord[:, 1]) + \
|
|
654
|
+
np.in1d(mesh.nodes.node_coord[:, 2], mesh_cropped.nodes.node_coord[:, 2])
|
|
655
|
+
ind_nodes = np.where(ind_nodes)[0]
|
|
656
|
+
|
|
657
|
+
# This doesn't work if several nodes are at the same location
|
|
658
|
+
if ind_nodes.shape[0] != mesh_cropped.nodes.node_coord.shape[0]:
|
|
659
|
+
# probably some duplicate nodes
|
|
660
|
+
if mesh_cropped.nodes.node_coord.shape[0] != np.unique(mesh_cropped.nodes.node_coord, axis=0).shape[0]:
|
|
661
|
+
# Some other problem
|
|
662
|
+
raise ValueError("Duplicate nodes found in cropped mesh.")
|
|
663
|
+
|
|
664
|
+
# Find the duplicate notes in mesh and remove from ind_nodes list
|
|
665
|
+
unique_elms, dup_elms = np.unique(mesh.nodes.node_coord, axis=0, return_counts=True)
|
|
666
|
+
assert unique_elms.shape[0] == dup_elms.shape[0]
|
|
667
|
+
|
|
668
|
+
# go over nodes that have been found twice
|
|
669
|
+
for dup_idx in np.where(dup_elms != 1)[0]:
|
|
670
|
+
found_dup_idx = np.argwhere(np.sum(mesh.nodes.node_coord == unique_elms[dup_idx], axis=1) == 3)
|
|
671
|
+
for i in range(1, len(found_dup_idx)):
|
|
672
|
+
try:
|
|
673
|
+
dup_2_rem = np.argwhere(ind_nodes == found_dup_idx[i])[0]
|
|
674
|
+
ind_nodes = np.delete(ind_nodes, dup_2_rem)
|
|
675
|
+
except IndexError:
|
|
676
|
+
pass
|
|
677
|
+
|
|
678
|
+
if skin_only_output:
|
|
679
|
+
# replace smoothed nodes in skin only mesh and write to disk
|
|
680
|
+
mesh_cropped.nodes.node_coord = smoothed_mesh.vertices # overwrite simnibs Mesh object's nodes
|
|
681
|
+
mesh_cropped.write(output_fn)
|
|
682
|
+
else:
|
|
683
|
+
# replace surface node_number list in full mesh and write to disk
|
|
684
|
+
mesh.nodes.node_coord[ind_nodes, :] = smoothed_mesh.vertices
|
|
685
|
+
mesh.write(output_fn)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def get_opt_mat(folder, roi=0):
|
|
689
|
+
"""
|
|
690
|
+
Load optimal coil position/orientation matsimnibs from SimNIBS online FEM.
|
|
691
|
+
|
|
692
|
+
Parameter:
|
|
693
|
+
----------
|
|
694
|
+
folder : str
|
|
695
|
+
Folder with optimization results.
|
|
696
|
+
roi : str or int, default: 0
|
|
697
|
+
Region of interest to read data for.
|
|
698
|
+
|
|
699
|
+
Returns
|
|
700
|
+
-------
|
|
701
|
+
opt_matsimnibs : np.ndarray
|
|
702
|
+
Optimal coil position/orientation.
|
|
703
|
+
|
|
704
|
+
"""
|
|
705
|
+
e_fn = os.path.join(folder, 'e.hdf5')
|
|
706
|
+
coil_pos_fn = os.path.join(folder, 'search_positions.hdf5')
|
|
707
|
+
assert os.path.exists(e_fn)
|
|
708
|
+
assert os.path.exists(coil_pos_fn)
|
|
709
|
+
|
|
710
|
+
with h5py.File(e_fn, 'r') as f_e:
|
|
711
|
+
keys = list(f_e[f'e/roi_{roi}'].keys())
|
|
712
|
+
keys = [int(k) for k in keys]
|
|
713
|
+
keys.sort()
|
|
714
|
+
max_val = 0
|
|
715
|
+
best_idx = None
|
|
716
|
+
for k in tqdm.tqdm(keys, total=len(keys), desc="Getting max E"):
|
|
717
|
+
mean_e = f_e[f'e/roi_{roi}/{k:0>4}'][:].mean()
|
|
718
|
+
if mean_e > max_val:
|
|
719
|
+
max_val = mean_e
|
|
720
|
+
best_idx = k
|
|
721
|
+
print(f"Max E {max_val.round(2)} found for idx {best_idx}.")
|
|
722
|
+
|
|
723
|
+
with h5py.File(coil_pos_fn, 'r') as coil_f:
|
|
724
|
+
if coil_f['matsimnibs'][:].shape[2] != len(keys):
|
|
725
|
+
warnings.warn(f"{coil_pos_fn} and {e_fn} have different sizes: {len(keys)} vs "
|
|
726
|
+
f"{coil_f['matsimnibs'][:].shape[2]} "
|
|
727
|
+
f"coil positions.")
|
|
728
|
+
return coil_f['matsimnibs'][:, :, best_idx]
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def get_skin_cortex_distance(mesh, coords, radius=5):
|
|
732
|
+
"""
|
|
733
|
+
Computes the skin-cortex distance (SCD).
|
|
734
|
+
|
|
735
|
+
Parameters
|
|
736
|
+
----------
|
|
737
|
+
mesh : str or simnibs.Mesh
|
|
738
|
+
Mesh object or .msh filename.
|
|
739
|
+
coords : np.ndarray
|
|
740
|
+
[3,] x,y,z coordinates to compute skin-cortex-distacnce for.
|
|
741
|
+
radius : float, default: 5
|
|
742
|
+
Spherical radius around ``coords`` to include points in for SCD computation.
|
|
743
|
+
|
|
744
|
+
Returns
|
|
745
|
+
-------
|
|
746
|
+
SCD : np.ndarray of float
|
|
747
|
+
Skin-cortex distance.
|
|
748
|
+
elm_in_roi : np.ndarray of int
|
|
749
|
+
Elelement numbers from mesh.elm.elm_number that where used to calculate PCD for.
|
|
750
|
+
"""
|
|
751
|
+
from simnibs import read_msh
|
|
752
|
+
if isinstance(mesh, str):
|
|
753
|
+
mesh = read_msh(mesh)
|
|
754
|
+
|
|
755
|
+
centers = mesh.elements_baricenters()[:]
|
|
756
|
+
dist = np.linalg.norm(centers - coords, axis=1)
|
|
757
|
+
elm = mesh.elm.elm_number[
|
|
758
|
+
(dist < radius) *
|
|
759
|
+
np.isin(mesh.elm.tag1, [2]) * # only grey matter
|
|
760
|
+
np.isin(mesh.elm.elm_type, [4]) # only tetrahedra
|
|
761
|
+
]
|
|
762
|
+
skin = centers[np.isin(mesh.elm.tag1, [1005])] # skin triangles
|
|
763
|
+
return np.linalg.norm(skin - coords, axis=1).min(), elm
|