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
@@ -0,0 +1,1394 @@
1
+ """The `mesh_struct.py` module provides classes and methods for handling and manipulating mesh structures in the
2
+ context of neuroimaging and brain stimulation studies. It includes classes for handling tetrahedral meshes and
3
+ regions of interest (ROIs).
4
+
5
+ The module includes the following classes:
6
+
7
+ - `TetrahedraLinear`: This class represents a mesh consisting of linear tetrahedra. It provides methods for
8
+ calculating various quantities of interest (QOIs) in the mesh, interpolating data between nodes and elements,
9
+ calculating the electric field and current density, and more.
10
+
11
+ - `Mesh`: This class is a general mesh class to initialize default attributes and provides methods to fill default
12
+ values based on the approach, write the mesh data to an HDF5 file, and print the mesh information.
13
+
14
+ - `ROI`: This class represents a region of interest (ROI) in the mesh. It provides methods to write the ROI data to
15
+ an HDF5 file and print the ROI information.
16
+
17
+ Each class and method in this module is documented with docstrings providing more detailed information about its
18
+ purpose, parameters, and return values.
19
+
20
+ This module is primarily used for handling and visualizing data related to neuroimaging and brain stimulation studies.
21
+ """
22
+ import os
23
+ import json
24
+ import time
25
+ import nibabel
26
+ import numpy as np
27
+ from numpy import cross as cycross
28
+ import pynibs
29
+
30
+ __package__ = "pynibs"
31
+
32
+
33
+ def __path__():
34
+ return os.path.dirname(__file__)
35
+
36
+
37
+ class TetrahedraLinear:
38
+ """
39
+ Mesh, consisting of linear tetrahedra.
40
+
41
+ Parameters
42
+ ----------
43
+ points : array of float [N_points x 3]
44
+ Vertices of FE mesh
45
+ triangles : np.ndarray of int [N_tri x 3]
46
+ Connectivity of points forming triangles
47
+ triangles_regions : np.ndarray of int [N_tri x 1]
48
+ Region identifiers of triangles
49
+ tetrahedra : np.ndarray of int [N_tet x 4]
50
+ Connectivity of points forming tetrahedra
51
+ tetrahedra_regions : np.ndarray of int [N_tet x 1]
52
+ Region identifiers of tetrahedra
53
+
54
+ Attributes
55
+ ----------
56
+ N_points : int
57
+ Number of vertices
58
+ N_tet : int
59
+ Number of tetrahedra
60
+ N_tri : int
61
+ Number of triangles
62
+ N_region : int
63
+ Number of regions
64
+ region : np.ndarray of int
65
+ Region labels
66
+ tetrahedra_volume : np.ndarray of float [N_tet x 1]
67
+ Volumes of tetrahedra
68
+ tetrahedra_center : np.ndarray of float [N_tet x 1]
69
+ Center of tetrahedra
70
+ triangles_center : np.ndarray of float [N_tri x 1]
71
+ Center of triangles
72
+ triangles_normal : np.ndarray of float [N_tri x 3]
73
+ Normal components of triangles pointing outwards
74
+ """
75
+
76
+ def __init__(self, points, triangles, triangles_regions, tetrahedra, tetrahedra_regions):
77
+ """ Initialize TetrahedraLinear class """
78
+ self.points = points
79
+ self.triangles = triangles
80
+ self.triangles_regions = triangles_regions
81
+ self.tetrahedra = tetrahedra
82
+ self.tetrahedra_regions = tetrahedra_regions
83
+ # index of points in "tetrahedra" start with 0 or 1
84
+
85
+ self.tetrahedra_triangle_surface_idx = - np.ones((self.triangles.shape[0], 2))
86
+
87
+ # shift index to start always from 0 (python)
88
+ if self.tetrahedra.size != 0:
89
+ self.idx_start = np.min(self.tetrahedra)
90
+ self.tetrahedra = self.tetrahedra - self.idx_start
91
+ self.N_tet = self.tetrahedra.shape[0]
92
+ p1_tet = self.points[self.tetrahedra[:, 0], :] # [P1x P1y P1z]
93
+ p2_tet = self.points[self.tetrahedra[:, 1], :]
94
+ p3_tet = self.points[self.tetrahedra[:, 2], :]
95
+ p4_tet = self.points[self.tetrahedra[:, 3], :]
96
+ self.tetrahedra_volume = pynibs.calc_tetrahedra_volume_cross(p1_tet, p2_tet, p3_tet, p4_tet)
97
+ self.tetrahedra_center = 1.0 / 4 * (p1_tet + p2_tet + p3_tet + p4_tet)
98
+
99
+ else:
100
+ self.N_tet = 0
101
+ self.idx_start = 0
102
+ self.triangles = self.triangles - self.idx_start
103
+
104
+ self.region = np.unique(self.tetrahedra_regions)
105
+
106
+ # number of elements and points etc
107
+ self.N_points = self.points.shape[0]
108
+ self.N_tri = self.triangles.shape[0]
109
+ self.N_region = len(self.region)
110
+
111
+ # count index lists of elements [0,1,2,....]
112
+ self.tetrahedra_index = np.arange(self.N_tet)
113
+ self.triangles_index = np.arange(self.N_tri)
114
+
115
+ if self.N_tri > 0:
116
+ p1_tri = self.points[self.triangles[:, 0], :]
117
+ p2_tri = self.points[self.triangles[:, 1], :]
118
+ p3_tri = self.points[self.triangles[:, 2], :]
119
+
120
+ self.triangles_center = 1.0 / 3 * (p1_tri + p2_tri + p3_tri)
121
+ self.triangles_normal = cycross(p2_tri - p1_tri, p3_tri - p1_tri)
122
+ normal_norm = np.linalg.norm(self.triangles_normal, axis=1)
123
+ normal_norm = normal_norm[:, np.newaxis]
124
+ self.triangles_normal = self.triangles_normal / np.tile(normal_norm, (1, 3))
125
+ self.triangles_area = 0.5 * np.linalg.norm(np.cross(p2_tri - p1_tri, p3_tri - p1_tri), axis=1)
126
+
127
+ def calc_E_on_GM_WM_surface_simnibs(self, phi, dAdt, roi, subject, verbose=False, mesh_idx=0):
128
+ """
129
+ Determines the normal and tangential component of the induced electric field on a GM-WM surface by recalculating
130
+ phi and dA/dt in an epsilon environment around the GM/WM surface (upper and lower GM-WM surface) or by using
131
+ the Simnibs interpolation function.
132
+
133
+ Parameters
134
+ ----------
135
+ phi : np.ndarray of float
136
+ (N_nodes, 1) Scalar electric potential given in the nodes of the mesh.
137
+ dAdt : np.ndarray of float
138
+ (N_nodes, 3) Magnetic vector potential given in the nodes of the mesh.
139
+ roi : pynibs.mesh.mesh_struct.ROI
140
+ RegionOfInterestSurface object class instance.
141
+ subject : pynibs.subject.Subject
142
+ Subject object loaded from .hdf5 file.
143
+ verbose : bool
144
+ Print information to stdout.
145
+ mesh_idx : int
146
+ Mesh index.
147
+
148
+ Returns
149
+ -------
150
+ E_normal : np.ndarray of float
151
+ (N_points, 3) Normal vector of electric field on GM-WM surface.
152
+ E_tangential : np.ndarray of float
153
+ (N_points, 3) Tangential vector of electric field on GM-WM surface.
154
+ """
155
+ import tempfile
156
+ import simnibs.msh.mesh_io as mesh_io
157
+ import simnibs.simulation.fem as fem
158
+ import simnibs.msh.transformations as transformations
159
+
160
+ mesh_folder = subject.mesh[mesh_idx]["mesh_folder"]
161
+
162
+ # load mesh
163
+ mesh = mesh_io.read_msh(subject.mesh[mesh_idx]["fn_mesh_msh"])
164
+
165
+ # write phi and dAdt in msh
166
+ dAdt_SimNIBS = mesh_io.NodeData(dAdt, name='D', mesh=mesh)
167
+ phi_SimNIBS = mesh_io.NodeData(phi.flatten(), name='v', mesh=mesh)
168
+
169
+ if verbose:
170
+ print("Calculating e-field")
171
+ out = fem.calc_fields(phi_SimNIBS, "vDEe", cond=None, dadt=dAdt_SimNIBS)
172
+
173
+ with tempfile.TemporaryDirectory() as f:
174
+ fn_res_tmp = os.path.join(f, "res.msh")
175
+ # mesh_io.write_msh(out, fn_res_tmp)
176
+
177
+ if verbose:
178
+ print("Interpolating values to midlayer of GM")
179
+ # determine e in midlayer
180
+ transformations.middle_gm_interpolation(mesh_fn=out,
181
+ m2m_folder=os.path.join(mesh_folder, "m2m_" + subject.id),
182
+ out_folder=f,
183
+ out_fsaverage=None,
184
+ depth=0.5,
185
+ quantities=['norm', 'normal', 'tangent', 'angle'],
186
+ fields=None,
187
+ open_in_gmsh=False,
188
+ write_msh=False) #
189
+
190
+ # load freesurfer surface
191
+ if type(roi.gm_surf_fname) is not list:
192
+ roi.gm_surf_fname = [roi.gm_surf_fname]
193
+
194
+ points_gm = [None for _ in range(len(roi.gm_surf_fname))]
195
+ con_gm = [None for _ in range(len(roi.gm_surf_fname))]
196
+
197
+ max_idx_gm = 0
198
+
199
+ if (roi.gm_surf_fname is list and len(roi.gm_surf_fname) > 0) or (roi.gm_surf_fname is str):
200
+ fn_surface = list(roi.gm_surf_fname)
201
+ elif (roi.midlayer_surf_fname is list and len(roi.gm_surf_fname) > 0) or (roi.midlayer_surf_fname is str):
202
+ fn_surface = list(roi.midlayer_surf_fname)
203
+
204
+ for i in range(len(fn_surface)):
205
+ points_gm[i], con_gm[i] = nibabel.freesurfer.read_geometry(os.path.join(mesh_folder, fn_surface[i]))
206
+
207
+ con_gm[i] = con_gm[i] + max_idx_gm
208
+
209
+ max_idx_gm = max_idx_gm + points_gm[i].shape[0] # np.max(con_gm[i]) + 2
210
+
211
+ points_gm = np.vstack(points_gm)
212
+ con_gm = np.vstack(con_gm)
213
+
214
+ if verbose:
215
+ print("Processing data to ROI")
216
+ if roi.fn_mask is None or roi.fn_mask == []:
217
+
218
+ if roi.X_ROI is None or roi.X_ROI == []:
219
+ roi.X_ROI = [-np.inf, np.inf]
220
+ if roi.Y_ROI is None or roi.Y_ROI == []:
221
+ roi.Y_ROI = [-np.inf, np.inf]
222
+ if roi.Z_ROI is None or roi.Z_ROI == []:
223
+ roi.Z_ROI = [-np.inf, np.inf]
224
+
225
+ roi_mask_bool = (roi.node_coord_mid[:, 0] > min(roi.X_ROI)) & (
226
+ roi.node_coord_mid[:, 0] < max(roi.X_ROI)) & \
227
+ (roi.node_coord_mid[:, 1] > min(roi.Y_ROI)) & (
228
+ roi.node_coord_mid[:, 1] < max(roi.Y_ROI)) & \
229
+ (roi.node_coord_mid[:, 2] > min(roi.Z_ROI)) & (
230
+ roi.node_coord_mid[:, 2] < max(roi.Z_ROI))
231
+ roi_mask_idx = np.where(roi_mask_bool)
232
+
233
+ else:
234
+ if type(roi.fn_mask) is np.ndarray:
235
+ if roi.fn_mask.ndim == 0:
236
+ roi.fn_mask = roi.fn_mask.astype(str).tolist()
237
+
238
+ # read mask from freesurfer mask file
239
+ mask = nibabel.freesurfer.mghformat.MGHImage.from_filename(
240
+ os.path.join(mesh_folder, roi.fn_mask)).dataobj[:]
241
+ roi_mask_idx = np.where(mask > 0.5)
242
+
243
+ # read results data
244
+ if verbose:
245
+ print("Reading SimNIBS midlayer data")
246
+ e_normal = []
247
+ e_tan = []
248
+
249
+ for fn_surf in fn_surface:
250
+ if "lh" in os.path.split(fn_surf)[1]:
251
+ e_normal.append(nibabel.freesurfer.read_morph_data(
252
+ os.path.join(f, "lh.res.central.E." + "normal")).flatten()[:, np.newaxis])
253
+ e_tan.append(nibabel.freesurfer.read_morph_data(
254
+ os.path.join(f, "lh.res.central.E." + "tangent")).flatten()[:, np.newaxis])
255
+
256
+ if "rh" in os.path.split(fn_surf)[1]:
257
+ e_normal.append(nibabel.freesurfer.read_morph_data(
258
+ os.path.join(f, "rh.res.central.E." + "normal")).flatten()[:, np.newaxis])
259
+ e_tan.append(nibabel.freesurfer.read_morph_data(
260
+ os.path.join(f, "rh.res.central.E." + "tangent")).flatten()[:, np.newaxis])
261
+
262
+ e_normal = np.vstack(e_normal)
263
+ e_tan = np.vstack(e_tan)
264
+
265
+ # transform point data to element data
266
+ if verbose:
267
+ print("Transforming point data to element data")
268
+ e_normal = pynibs.data_nodes2elements(data=e_normal, con=con_gm)
269
+ e_tan = pynibs.data_nodes2elements(data=e_tan, con=con_gm)
270
+
271
+ # crop results data to ROI
272
+ # if not roi_mask_bool.all():
273
+ if roi_mask_idx:
274
+ if verbose:
275
+ print("Cropping results data to ROI")
276
+
277
+ # get row index where all points are lying inside ROI
278
+ con_row_idx = [i for i in range(con_gm.shape[0]) if len(np.intersect1d(con_gm[i,], roi_mask_idx)) == 3]
279
+
280
+ e_normal = e_normal[con_row_idx, :]
281
+ e_tan = e_tan[con_row_idx, :]
282
+
283
+ return e_normal, e_tan
284
+
285
+ def calc_E_on_GM_WM_surface_simnibs_KW(self, phi, dAdt, roi, subject, verbose=False, mesh_idx=0):
286
+ """
287
+ Determines the normal and tangential component of the induced electric field on a GM-WM surface by recalculating
288
+ phi and dA/dt in an epsilon environment around the GM/WM surface (upper and lower GM-WM surface) or by using
289
+ the Simnibs interpolation function.
290
+
291
+ Parameters
292
+ ----------
293
+ phi : np.ndarray of float
294
+ (N_nodes, 1) Scalar electric potential given in the nodes of the mesh.
295
+ dAdt : np.ndarray of float
296
+ (N_nodes, 1) Magnetic vector potential given in the nodes of the mesh.
297
+ roi : pynibs.mesh.mesh_struct.ROI
298
+ RegionOfInterestSurface object class instance.
299
+ subject : pynibs.subject.Subject
300
+ Subject object loaded from .hdf5 file.
301
+ verbose : bool
302
+ Print information to stdout.
303
+ mesh_idx : int
304
+ Mesh index.
305
+
306
+ Returns
307
+ -------
308
+ E_normal : np.ndarray of float
309
+ (N_points, 3) Normal vector of electric field on GM-WM surface.
310
+ E_tangential : np.ndarray of float
311
+ (N_points, 3) Tangential vector of electric field on GM-WM surface.
312
+ """
313
+ import tempfile
314
+ import simnibs.msh.mesh_io as mesh_io
315
+ import simnibs.simulation.fem as fem
316
+ import simnibs.msh.transformations as transformations
317
+
318
+ mesh_folder = subject.mesh[mesh_idx]["mesh_folder"]
319
+
320
+ # load mesh
321
+ mesh = mesh_io.read_msh(subject.mesh[mesh_idx]["fn_mesh_msh"])
322
+
323
+ # write phi and dAdt in msh
324
+ dadt_simnibs = mesh_io.NodeData(dAdt, name='D', mesh=mesh)
325
+ phi_simnibs = mesh_io.NodeData(phi.flatten(), name='v', mesh=mesh)
326
+
327
+ if verbose:
328
+ print("Calculating e-field")
329
+ out = fem.calc_fields(phi_simnibs, "vDEe", cond=None, dadt=dadt_simnibs)
330
+
331
+ with tempfile.TemporaryDirectory() as f:
332
+ fn_res_tmp = os.path.join(f, "res.msh")
333
+ mesh_io.write_msh(out, fn_res_tmp)
334
+
335
+ if verbose:
336
+ print("Interpolating values to midlayer of GM")
337
+ # determine e in midlayer
338
+ transformations.middle_gm_interpolation(mesh_fn=fn_res_tmp,
339
+ m2m_folder=os.path.join(mesh_folder, "m2m_" + subject.id),
340
+ out_folder=f,
341
+ out_fsaverage=None,
342
+ depth=0.5,
343
+ quantities=['norm', 'normal', 'tangent', 'angle'],
344
+ fields=None,
345
+ open_in_gmsh=False) # write_msh=False
346
+
347
+ # load freesurfer surface
348
+ if type(roi.gm_surf_fname) is not list:
349
+ roi.gm_surf_fname = [roi.gm_surf_fname]
350
+
351
+ points_gm = [None for _ in range(len(roi.gm_surf_fname))]
352
+ con_gm = [None for _ in range(len(roi.gm_surf_fname))]
353
+
354
+ max_idx_gm = 0
355
+
356
+ if (type(roi.gm_surf_fname) is list and roi.gm_surf_fname[0] is not None) or \
357
+ (type(roi.gm_surf_fname) is str):
358
+ if type(roi.gm_surf_fname) is str:
359
+ fn_surface = [roi.gm_surf_fname]
360
+ else:
361
+ fn_surface = roi.gm_surf_fname
362
+
363
+ elif (type(roi.midlayer_surf_fname) is list and roi.gm_surf_fname is not None) or \
364
+ (type(roi.midlayer_surf_fname) is str):
365
+ if type(roi.midlayer_surf_fname) is str:
366
+ fn_surface = [roi.midlayer_surf_fname]
367
+ else:
368
+ fn_surface = roi.midlayer_surf_fname
369
+
370
+ for i in range(len(fn_surface)):
371
+ points_gm[i], con_gm[i] = nibabel.freesurfer.read_geometry(os.path.join(mesh_folder, fn_surface[i]))
372
+
373
+ con_gm[i] = con_gm[i] + max_idx_gm
374
+
375
+ max_idx_gm = max_idx_gm + points_gm[i].shape[0] # np.max(con_gm[i]) + 2
376
+
377
+ points_gm = np.vstack(points_gm)
378
+ con_gm = np.vstack(con_gm)
379
+
380
+ if verbose:
381
+ print("Processing data to ROI")
382
+ if roi.fn_mask is None or roi.fn_mask == []:
383
+
384
+ if roi.X_ROI is None or roi.X_ROI == []:
385
+ roi.X_ROI = [-np.inf, np.inf]
386
+ if roi.Y_ROI is None or roi.Y_ROI == []:
387
+ roi.Y_ROI = [-np.inf, np.inf]
388
+ if roi.Z_ROI is None or roi.Z_ROI == []:
389
+ roi.Z_ROI = [-np.inf, np.inf]
390
+
391
+ roi_mask_bool = (roi.node_coord_mid[:, 0] > min(roi.X_ROI)) & (
392
+ roi.node_coord_mid[:, 0] < max(roi.X_ROI)) & \
393
+ (roi.node_coord_mid[:, 1] > min(roi.Y_ROI)) & (
394
+ roi.node_coord_mid[:, 1] < max(roi.Y_ROI)) & \
395
+ (roi.node_coord_mid[:, 2] > min(roi.Z_ROI)) & (
396
+ roi.node_coord_mid[:, 2] < max(roi.Z_ROI))
397
+ roi_mask_idx = np.where(roi_mask_bool)
398
+
399
+ else:
400
+ if type(roi.fn_mask) is np.ndarray:
401
+ if roi.fn_mask.ndim == 0:
402
+ roi.fn_mask = roi.fn_mask.astype(str).tolist()
403
+
404
+ # read mask from freesurfer mask file
405
+ mask = nibabel.freesurfer.mghformat.MGHImage.from_filename(
406
+ os.path.join(mesh_folder, roi.fn_mask)).dataobj[:]
407
+ roi_mask_idx = np.where(mask > 0.5)
408
+
409
+ # read results data
410
+ if verbose:
411
+ print("Reading SimNIBS midlayer data")
412
+ e_normal = []
413
+ e_tan = []
414
+
415
+ for fn_surf in fn_surface:
416
+ if "lh" in os.path.split(fn_surf)[1]:
417
+ e_normal.append(nibabel.freesurfer.read_morph_data(
418
+ os.path.join(f, "lh.res.central.E." + "normal")).flatten()[:, np.newaxis])
419
+ e_tan.append(nibabel.freesurfer.read_morph_data(
420
+ os.path.join(f, "lh.res.central.E." + "tangent")).flatten()[:, np.newaxis])
421
+
422
+ if "rh" in os.path.split(fn_surf)[1]:
423
+ e_normal.append(nibabel.freesurfer.read_morph_data(
424
+ os.path.join(f, "rh.res.central.E." + "normal")).flatten()[:, np.newaxis])
425
+ e_tan.append(nibabel.freesurfer.read_morph_data(
426
+ os.path.join(f, "rh.res.central.E." + "tangent")).flatten()[:, np.newaxis])
427
+
428
+ e_normal = np.vstack(e_normal)
429
+ e_tan = np.vstack(e_tan)
430
+
431
+ # transform point data to element data
432
+ if verbose:
433
+ print("Transforming point data to element data")
434
+ e_normal = pynibs.data_nodes2elements(data=e_normal, con=con_gm)
435
+ e_tan = pynibs.data_nodes2elements(data=e_tan, con=con_gm)
436
+
437
+ # crop results data to ROI
438
+ # if not roi_mask_bool.all():
439
+ if roi_mask_idx:
440
+ if verbose:
441
+ print("Cropping results data to ROI")
442
+
443
+ # get row index where all points are lying inside ROI
444
+ con_row_idx = [i for i in range(con_gm.shape[0]) if len(np.intersect1d(con_gm[i,], roi_mask_idx)) == 3]
445
+
446
+ e_normal = e_normal[con_row_idx, :]
447
+ e_tan = e_tan[con_row_idx, :]
448
+
449
+ return e_normal, e_tan
450
+
451
+ def calc_E_on_GM_WM_surface3(self, phi, dAdt, roi, verbose=True, mode="components"):
452
+ """
453
+ Determines the normal and tangential component of the induced electric field on a GM-WM surface by recalculating
454
+ phi and dA/dt in an epsilon environment around the GM/WM surface (upper and lower GM-WM surface).
455
+
456
+ Parameters
457
+ ----------
458
+ phi : np.ndarray of float
459
+ (N_nodes, 1) Scalar electric potential given in the nodes of the mesh.
460
+ dAdt : np.ndarray of float
461
+ (N_nodes, 3) Magnetic vector potential given in the nodes of the mesh.
462
+ roi : pynibs.mesh.mesh_struct.ORI
463
+ RegionOfInterestSurface object class instance.
464
+ verbose : bool
465
+ Print information to stdout.
466
+ mode : str
467
+ Select mode of output:
468
+ - "components" : return x, y, and z component of tangential and normal components
469
+ - "magnitude" : return magnitude of tangential and normal component (normal with sign for direction)
470
+
471
+ Returns
472
+ -------
473
+ E_normal : np.ndarray of float
474
+ (N_nodes, 3) Normal vector of electric field on GM-WM surface.
475
+ E_tangential : np.ndarray of float
476
+ (N_nodes, 3) Tangential vector of electric field on GM-WM surface.
477
+ """
478
+ # check if dimension are fitting
479
+ assert phi.shape[0] == dAdt.shape[0]
480
+ assert dAdt.shape[1] == 3
481
+
482
+ # interpolate electric scalar potential to central points of upper and lower surface triangles
483
+ if verbose:
484
+ print("Interpolating electric scalar potential to central points of upper and lower surface triangles")
485
+ phi_gm_wm_surface_up = self.calc_QOI_in_points_tet_idx(qoi=phi,
486
+ points_out=roi.tri_center_coord_up,
487
+ tet_idx=roi.tet_idx_tri_center_up.flatten())
488
+
489
+ phi_gm_wm_surface_low = self.calc_QOI_in_points_tet_idx(qoi=phi,
490
+ points_out=roi.tri_center_coord_low,
491
+ tet_idx=roi.tet_idx_tri_center_low.flatten())
492
+
493
+ # determine distance between upper and lower surface (in m!)
494
+ d = np.linalg.norm(roi.tri_center_coord_up - roi.tri_center_coord_low, axis=1)[:, np.newaxis] * 1E-3
495
+ d[np.argwhere(d == 0)[:, 0]] = 1e-6 # delete zero distances
496
+
497
+ # determine surface normal vector (normalized)
498
+ # n = ((points_up - points_low) / np.tile(d, (1, 3)))*1E-3
499
+ # n = (points_up - points_low) * 1E-3
500
+
501
+ p1_tri = roi.node_coord_mid[roi.node_number_list[:, 0], :]
502
+ p2_tri = roi.node_coord_mid[roi.node_number_list[:, 1], :]
503
+ p3_tri = roi.node_coord_mid[roi.node_number_list[:, 2], :]
504
+
505
+ n = cycross(p2_tri - p1_tri, p3_tri - p1_tri)
506
+ normal_norm = np.linalg.norm(n, axis=1)
507
+ normal_norm = normal_norm[:, np.newaxis]
508
+ n = n / np.tile(normal_norm, (1, 3))
509
+
510
+ # interpolate magnetic vector potential to central surface points (primary electric field)
511
+ # E_pri = griddata(self.points, dAdt, surf_mid, method='linear', fill_value=np.NaN, rescale=False)
512
+ if verbose:
513
+ print("Interpolating magnetic vector potential to central surface points (primary electric field)")
514
+ e_pri = self.calc_QOI_in_points_tet_idx(qoi=dAdt,
515
+ points_out=roi.tri_center_coord_mid,
516
+ tet_idx=roi.tet_idx_tri_center_mid.flatten())
517
+
518
+ # determine its normal component
519
+ e_pri_normal = np.multiply(np.sum(np.multiply(e_pri, n), axis=1)[:, np.newaxis], n)
520
+
521
+ # determine gradient of phi and multiply with surface normal (secondary electric field)
522
+ e_sec_normal = np.multiply((phi_gm_wm_surface_up - phi_gm_wm_surface_low) * 1E-3 / d, n)
523
+
524
+ # combine (normal) primary and secondary electric field
525
+ e_normal = self.calc_E(e_sec_normal, e_pri_normal)
526
+
527
+ # compute tangential component of secondary electric field on surface
528
+ if verbose:
529
+ print("Interpolating scalar electric potential to nodes of midlayer (primary electric field)")
530
+ phi_surf_mid_nodes = self.calc_QOI_in_points_tet_idx(qoi=phi,
531
+ points_out=roi.node_coord_mid,
532
+ tet_idx=roi.tet_idx_node_coord_mid.flatten())
533
+
534
+ if verbose:
535
+ print("Determine gradient of scalar electric potential on midlayer surface (E_sec_tangential)")
536
+ e_sec_tan = pynibs.calc_gradient_surface(phi=phi_surf_mid_nodes,
537
+ points=roi.node_coord_mid,
538
+ triangles=roi.node_number_list)
539
+
540
+ # compute tangential component of primary electric field on surface
541
+ e_pri_tan = e_pri - e_pri_normal
542
+
543
+ # compute tangential component of total electric field
544
+ e_tan = self.calc_E(e_sec_tan, e_pri_tan)
545
+
546
+ # determine total E on surface (sanity check)
547
+ # E = self.calc_QOI_in_points(E, surf_mid)
548
+
549
+ if mode == "magnitude":
550
+ # get sign info of normal component
551
+ e_normal_dir = (np.sum(e_normal * n, axis=1) > 0)[:, np.newaxis].astype(int)
552
+
553
+ e_normal_dir[e_normal_dir == 1] = 1
554
+ e_normal_dir[e_normal_dir == 0] = -1
555
+
556
+ # determine magnitude of vectors and assign sign info
557
+ e_tan = np.linalg.norm(e_tan, axis=1)[:, np.newaxis]
558
+ e_normal = np.linalg.norm(e_normal, axis=1)[:, np.newaxis] * e_normal_dir
559
+
560
+ return e_normal, e_tan
561
+
562
+ def calc_E_on_GM_WM_surface(self, E, roi):
563
+ """
564
+ Determines the normal and tangential component of the induced electric field on a GM-WM surface using
565
+ nearest neighbour principle.
566
+
567
+ Parameters
568
+ ----------
569
+ E : np.ndarray of float [N_tri x 3]
570
+ Induced electric field given in the tetrahedra centre of the mesh instance
571
+ roi : pynibs.roi.RegionOfInterestSurface
572
+ RegionOfInterestSurface object class instance
573
+
574
+ Returns
575
+ -------
576
+ E_normal : np.ndarray of float [N_points x 3]
577
+ Normal vector of electric field on GM-WM surface
578
+ E_tangential : np.ndarray of float [N_points x 3]
579
+ Tangential vector of electric field on GM-WM surface
580
+ """
581
+
582
+ e_gm_wm_surface = E[roi.tet_idx_nodes_mid, :]
583
+
584
+ # determine surface normal vector (normalized)
585
+ n = cycross(roi.node_coord_mid[roi.node_number_list[:, 1]] - roi.node_coord_mid[roi.node_number_list[:, 0]],
586
+ roi.node_coord_mid[roi.node_number_list[:, 2]] - roi.node_coord_mid[roi.node_number_list[:, 0]])
587
+ n = n / np.linalg.norm(n, axis=1)[:, np.newaxis]
588
+
589
+ # determine its normal component
590
+ e_normal = np.multiply(np.sum(np.multiply(e_gm_wm_surface, n), axis=1)[:, np.newaxis], n)
591
+
592
+ # compute tangential component of total electric field
593
+ e_tan = e_gm_wm_surface - e_normal
594
+
595
+ # determine total E on surface (sanity check)
596
+ # E = self.calc_QOI_in_points(E, surf_mid)
597
+
598
+ return e_normal, e_tan
599
+
600
+ def calc_QOI_in_points(self, qoi, points_out):
601
+ """
602
+ Calculate QOI_out in points_out using the mesh instance and the quantity of interest (QOI).
603
+
604
+ Parameters
605
+ ----------
606
+ qoi : np.ndarray of float
607
+ Quantity of interest in nodes of tetrahedra mesh instance
608
+ points_out : np.ndarray of float
609
+ Point coordinates (x, y, z) where the qoi is going to be interpolated by linear basis functions
610
+
611
+ Returns
612
+ -------
613
+ qoi_out : np.ndarray of float
614
+ Quantity of interest in points_out
615
+
616
+ """
617
+
618
+ n_phi_points_out = points_out.shape[0]
619
+ qoi_out = np.zeros(
620
+ [n_phi_points_out, qoi.shape[1] if qoi.ndim > 1 else 1])
621
+
622
+ p1_all = self.points[self.tetrahedra[:, 0], :]
623
+ p2_all = self.points[self.tetrahedra[:, 1], :]
624
+ p3_all = self.points[self.tetrahedra[:, 2], :]
625
+ p4_all = self.points[self.tetrahedra[:, 3], :]
626
+
627
+ # identify in which tetrahedron the point lies
628
+ # (all other volumes have at least one negative sub-volume)
629
+
630
+ # determine all volumes (replacing points with points_out)
631
+ # find the element where all volumes are > 0 (not inverted element)
632
+ # get index of this tetrahedron
633
+ # do it successively to decrease amount of volume calculations for all
634
+ # 4 points in tetrahedra
635
+ for i in range(n_phi_points_out):
636
+ start = time.time()
637
+ vtest1 = pynibs.calc_tetrahedra_volume_cross(np.tile(points_out[i, :], (p1_all.shape[0], 1)),
638
+ p2_all,
639
+ p3_all,
640
+ p4_all)
641
+ tet_idx_bool_1 = (vtest1 >= 0)
642
+ tet_idx_1 = np.nonzero(tet_idx_bool_1)[0]
643
+
644
+ vtest2 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx_1, :],
645
+ np.tile(
646
+ points_out[i, :], (tet_idx_1.shape[0], 1)),
647
+ p3_all[tet_idx_1, :],
648
+ p4_all[tet_idx_1, :])
649
+ tet_idx_bool_2 = (vtest2 >= 0)
650
+ tet_idx_2 = tet_idx_1[np.nonzero(tet_idx_bool_2)[0]]
651
+
652
+ vtest3 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx_2, :],
653
+ p2_all[tet_idx_2, :],
654
+ np.tile(
655
+ points_out[i, :], (tet_idx_2.shape[0], 1)),
656
+ p4_all[tet_idx_2, :])
657
+ tet_idx_bool_3 = (vtest3 >= 0)
658
+ tet_idx_3 = tet_idx_2[np.nonzero(tet_idx_bool_3)[0]]
659
+
660
+ vtest4 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx_3, :],
661
+ p2_all[tet_idx_3, :],
662
+ p3_all[tet_idx_3, :],
663
+ np.tile(points_out[i, :], (tet_idx_3.shape[0], 1)))
664
+ tet_idx_bool_4 = (vtest4 >= 0)
665
+ tet_idx = tet_idx_3[np.nonzero(tet_idx_bool_4)[0]]
666
+
667
+ # calculate subvolumes of final tetrahedron and its total volume
668
+ vsub1 = pynibs.calc_tetrahedra_volume_cross(points_out[i, :][np.newaxis],
669
+ p2_all[tet_idx, :],
670
+ p3_all[tet_idx, :],
671
+ p4_all[tet_idx, :])
672
+ vsub2 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx, :],
673
+ points_out[i, :][np.newaxis],
674
+ p3_all[tet_idx, :],
675
+ p4_all[tet_idx, :])
676
+ vsub3 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx, :],
677
+ p2_all[tet_idx, :],
678
+ points_out[i, :][np.newaxis],
679
+ p4_all[tet_idx, :])
680
+ vsub4 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx, :],
681
+ p2_all[tet_idx, :],
682
+ p3_all[tet_idx, :],
683
+ points_out[i, :][np.newaxis], )
684
+
685
+ vsub = np.array([vsub1, vsub2, vsub3, vsub4])
686
+ vtot = np.sum(vsub)
687
+
688
+ # calculate phi_out
689
+ qoi_out[i,] = 1.0 * np.dot(vsub.T, qoi[self.tetrahedra[tet_idx[0], :],]) / vtot
690
+
691
+ stop = time.time()
692
+ print(('Total: Point: {:d}/{:d} [{} sec]\n'.format(i + 1, n_phi_points_out, stop - start)))
693
+
694
+ return qoi_out
695
+
696
+ def calc_QOI_in_points_tet_idx(self, qoi, points_out, tet_idx):
697
+ """
698
+ Calculate QOI_out in points_out sitting in tet_idx using the mesh instance and the quantity of interest (QOI).
699
+
700
+ Parameters
701
+ ----------
702
+ qoi : np.ndarray of float
703
+ Quantity of interest in nodes of tetrahedra mesh instance
704
+ points_out : np.ndarray of float
705
+ Point coordinates (x, y, z) where the qoi is going to be interpolated by linear basis functions
706
+ tet_idx : np.ndarray of int
707
+ Element indices where the points_out are sitting
708
+
709
+ Returns
710
+ -------
711
+ qoi_out : np.ndarray of float
712
+ Quantity of interest in points_out
713
+
714
+ """
715
+
716
+ n_phi_points_out = points_out.shape[0]
717
+ qoi_out = np.zeros([n_phi_points_out, qoi.shape[1] if qoi.ndim > 1 else 1])
718
+
719
+ p1_all = self.points[self.tetrahedra[:, 0], :]
720
+ p2_all = self.points[self.tetrahedra[:, 1], :]
721
+ p3_all = self.points[self.tetrahedra[:, 2], :]
722
+ p4_all = self.points[self.tetrahedra[:, 3], :]
723
+
724
+ # determine sub-volumes
725
+ vsub1 = pynibs.calc_tetrahedra_volume_cross(points_out,
726
+ p2_all[tet_idx, :],
727
+ p3_all[tet_idx, :],
728
+ p4_all[tet_idx, :])
729
+ vsub2 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx, :],
730
+ points_out,
731
+ p3_all[tet_idx, :],
732
+ p4_all[tet_idx, :])
733
+ vsub3 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx, :],
734
+ p2_all[tet_idx, :],
735
+ points_out,
736
+ p4_all[tet_idx, :])
737
+ vsub4 = pynibs.calc_tetrahedra_volume_cross(p1_all[tet_idx, :],
738
+ p2_all[tet_idx, :],
739
+ p3_all[tet_idx, :],
740
+ points_out)
741
+ vsub = np.hstack([vsub1, vsub2, vsub3, vsub4])
742
+ vtot = np.sum(vsub, axis=1)
743
+
744
+ # calculate the QOIs in the tetrahedron of interest
745
+ for i in range(qoi.shape[1]):
746
+ qoi_out[:, i] = 1.0 * np.sum(np.multiply(vsub, qoi[self.tetrahedra[tet_idx, :], i]), axis=1) / vtot
747
+
748
+ # for i in range(N_phi_points_out):
749
+ # # calculate subvolumes of final tetrahedron and its total volume
750
+ # Vsub1 = pynibs.calc_tetrahedra_volume_cross(points_out[i, :][np.newaxis],
751
+ # p2_all[tet_idx[i], :][np.newaxis],
752
+ # p3_all[tet_idx[i], :][np.newaxis],
753
+ # P4_all[tet_idx[i], :][np.newaxis])
754
+ # vsub2 = pynibs.calc_tetrahedra_volume_cross(P1_all[tet_idx[i], :][np.newaxis],
755
+ # points_out[i, :][np.newaxis],
756
+ # p3_all[tet_idx[i], :][np.newaxis],
757
+ # P4_all[tet_idx[i], :][np.newaxis])
758
+ # Vsub3 = pynibs.calc_tetrahedra_volume_cross(P1_all[tet_idx[i], :][np.newaxis],
759
+ # p2_all[tet_idx[i], :][np.newaxis],
760
+ # points_out[i, :][np.newaxis],
761
+ # P4_all[tet_idx[i], :][np.newaxis])
762
+ # Vsub4 = pynibs.calc_tetrahedra_volume_cross(P1_all[tet_idx[i], :][np.newaxis],
763
+ # p2_all[tet_idx[i], :][np.newaxis],
764
+ # p3_all[tet_idx[i], :][np.newaxis],
765
+ # points_out[i, :][np.newaxis])
766
+ #
767
+ # vtot = np.sum([Vsub1, vsub2, Vsub3, Vsub4])
768
+ #
769
+ # # calculate the QOIs in the tetrahedron of interest
770
+ # qoi_out[i,] = 1.0 * np.dot(vsub.T, qoi[self.tetrahedra[tet_idx[i], :],]) / vtot
771
+
772
+ return qoi_out
773
+
774
+ def data_nodes2elements(self, data):
775
+ """
776
+ Interpolate data given in the nodes to the tetrahedra center.
777
+
778
+ Parameters
779
+ ----------
780
+ data : np.ndarray [N_nodes x N_data]
781
+ Data in nodes
782
+
783
+ Returns
784
+ -------
785
+ data_elements : np.ndarray [N_elements x N_data]
786
+ Data in elements
787
+ """
788
+ data_elements = np.sum(data[self.tetrahedra[:, i]] for i in range(4)) / 4.0
789
+
790
+ return data_elements
791
+
792
+ def data_elements2nodes(self, data):
793
+ """
794
+ Transforms an data in tetrahedra into the nodes after Zienkiewicz et al. (1992) [1]_.
795
+ Can only transform volume data, i.e. needs the data in the surrounding tetrahedra to average it to the nodes.
796
+ Will not work well for discontinuous fields (like E, if several tissues are used).
797
+
798
+ Parameters
799
+ ----------
800
+ data : np.ndarray [N_elements x N_data]
801
+ Data in tetrahedra
802
+
803
+ Returns
804
+ -------
805
+ data_nodes : np.ndarray [N_nodes x N_data]
806
+ Data in nodes
807
+
808
+ Notes
809
+ -----
810
+ .. [1] Zienkiewicz, Olgierd Cecil, and Jian Zhong Zhu. "The superconvergent patch recovery and a
811
+ posteriori error estimates. Part 1: The recovery technique." International Journal for
812
+ Numerical Methods in Engineering 33.7 (1992): 1331-1364.
813
+ """
814
+
815
+ # check dimension of input data
816
+ if data.ndim == 1:
817
+ data = data[:, np.newaxis]
818
+
819
+ n_data = data.shape[1]
820
+ data_nodes = np.zeros((self.N_points, n_data))
821
+
822
+ if self.N_tet != data.shape[0]:
823
+ raise ValueError("The number of data points in the data has to be equal to the number"
824
+ "of elements in the mesh")
825
+
826
+ value = np.atleast_2d(data)
827
+ if value.shape[0] < value.shape[1]:
828
+ value = value.T
829
+
830
+ # nd = np.zeros((self.N_points, N_data))
831
+
832
+ # get all nodes used in tetrahedra, creates the NodeData structure
833
+ # uq = np.unique(msh.elm[msh.elm.tetrahedra])
834
+ # nd = NodeData(np.zeros((len(uq), self.nr_comp)), self.field_name, mesh=msh)
835
+ # nd.node_number = uq
836
+
837
+ # Get the point in the outside surface
838
+ points_outside = np.unique(self.get_outside_faces())
839
+ outside_points_mask = np.in1d(self.tetrahedra, points_outside).reshape(-1, 4)
840
+ masked_th_nodes = np.copy(self.tetrahedra)
841
+ masked_th_nodes[outside_points_mask] = -1
842
+
843
+ # Calculates the quantities needed for the superconvergent patch recovery
844
+ uq_in, th_nodes = np.unique(masked_th_nodes, return_inverse=True)
845
+
846
+ baricenters = self.tetrahedra_center
847
+ volumes = self.tetrahedra_volume
848
+ baricenters = np.hstack([np.ones((baricenters.shape[0], 1)), baricenters])
849
+
850
+ A = np.empty((len(uq_in), 4, 4))
851
+ b = np.empty((len(uq_in), 4, n_data), 'float64')
852
+ for i in range(4):
853
+ for j in range(i, 4):
854
+ A[:, i, j] = np.bincount(th_nodes.reshape(-1),
855
+ np.repeat(baricenters[:, i], 4) *
856
+ np.repeat(baricenters[:, j], 4))
857
+ A[:, 1, 0] = A[:, 0, 1]
858
+ A[:, 2, 0] = A[:, 0, 2]
859
+ A[:, 3, 0] = A[:, 0, 3]
860
+ A[:, 2, 1] = A[:, 1, 2]
861
+ A[:, 3, 1] = A[:, 1, 3]
862
+ A[:, 3, 2] = A[:, 2, 3]
863
+
864
+ for j in range(n_data):
865
+ for i in range(4):
866
+ b[:, i, j] = np.bincount(th_nodes.reshape(-1),
867
+ np.repeat(baricenters[:, i], 4) *
868
+ np.repeat(value[:, j], 4))
869
+
870
+ a = np.linalg.solve(A[1:], b[1:])
871
+ p = np.hstack([np.ones((len(uq_in) - 1, 1)), self.points[uq_in[1:]]])
872
+ f = np.einsum('ij, ijk -> ik', p, a)
873
+ data_nodes[uq_in[1:]] = f
874
+
875
+ # Assigns the average value to the points in the outside surface
876
+ masked_th_nodes = np.copy(self.tetrahedra)
877
+ masked_th_nodes[~outside_points_mask] = -1
878
+ uq_out, th_nodes_out = np.unique(masked_th_nodes, return_inverse=True)
879
+
880
+ sum_vals = np.empty((len(uq_out), n_data), 'float64')
881
+
882
+ for j in range(n_data):
883
+ sum_vals[:, j] = np.bincount(th_nodes_out.reshape(-1),
884
+ np.repeat(value[:, j], 4) *
885
+ np.repeat(volumes, 4))
886
+
887
+ sum_vols = np.bincount(th_nodes_out.reshape(-1), np.repeat(volumes, 4))
888
+
889
+ data_nodes[uq_out[1:]] = (sum_vals / sum_vols[:, None])[1:]
890
+
891
+ return data_nodes
892
+
893
+ def get_outside_faces(self, tetrahedra_indices=None):
894
+ """
895
+ Creates a list of nodes in each face that are in the outer volume.
896
+
897
+ Parameters
898
+ ----------
899
+ tetrahedra_indices : np.ndarray
900
+ Indices of the tetrehedra where the outer volume is to be determined (default: all tetrahedra)
901
+
902
+ Returns
903
+ -------
904
+ faces : np.ndarray
905
+ List of nodes in faces in arbitrary order
906
+ """
907
+
908
+ if tetrahedra_indices is None:
909
+ tetrahedra_indices = self.tetrahedra_index
910
+
911
+ th = self.tetrahedra[tetrahedra_indices]
912
+ faces = th[:, [[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]]]
913
+ faces = faces.reshape(-1, 3)
914
+ hash_array = np.array([hash(f.tobytes()) for f in np.sort(faces, axis=1)])
915
+ unique, idx, inv, count = np.unique(hash_array, return_index=True,
916
+ return_inverse=True, return_counts=True)
917
+
918
+ # if np.any(count > 2):
919
+ # raise ValueError('Invalid Mesh: Found a face with more than 2 adjacent'
920
+ # ' tetrahedra!')
921
+
922
+ outside_faces = faces[idx[count == 1]]
923
+
924
+ return outside_faces
925
+
926
+ def calc_gradient(self, phi):
927
+ """
928
+ Calculate gradient of scalar DOF in tetrahedra center.
929
+
930
+ Parameters
931
+ ----------
932
+ phi : np.ndarray of float [N_nodes]
933
+ Scalar DOF the gradient is calculated for
934
+
935
+ Returns
936
+ -------
937
+ grad_phi : np.ndarray of float [N_tet x 3]
938
+ Gradient of Scalar DOF in tetrahedra center
939
+ """
940
+
941
+ a1 = np.vstack((self.points[self.tetrahedra[:, 3], :] - self.points[self.tetrahedra[:, 1], :],
942
+ self.points[self.tetrahedra[:, 2], :] -
943
+ self.points[self.tetrahedra[:, 0], :],
944
+ self.points[self.tetrahedra[:, 3], :] -
945
+ self.points[self.tetrahedra[:, 0], :],
946
+ self.points[self.tetrahedra[:, 1], :] - self.points[self.tetrahedra[:, 0], :]))
947
+
948
+ a2 = np.vstack((self.points[self.tetrahedra[:, 2], :] - self.points[self.tetrahedra[:, 1], :],
949
+ self.points[self.tetrahedra[:, 3], :] -
950
+ self.points[self.tetrahedra[:, 0], :],
951
+ self.points[self.tetrahedra[:, 1], :] -
952
+ self.points[self.tetrahedra[:, 0], :],
953
+ self.points[self.tetrahedra[:, 2], :] - self.points[self.tetrahedra[:, 0], :]))
954
+
955
+ # a3 = np.vstack((self.points[self.tetrahedra[:, 0], :] - self.points[self.tetrahedra[:, 1], :],
956
+ # self.points[self.tetrahedra[:, 1], :] -
957
+ # self.points[self.tetrahedra[:, 0], :],
958
+ # self.points[self.tetrahedra[:, 2], :] -
959
+ # self.points[self.tetrahedra[:, 0], :],
960
+ # self.points[self.tetrahedra[:, 3], :] - self.points[self.tetrahedra[:, 0], :]))
961
+
962
+ volumes = np.sum(np.multiply(cycross(a1, a2), a3), 1)
963
+ volumes = volumes[:, np.newaxis]
964
+ dlambda = np.transpose(np.reshape(cycross(
965
+ a1, a2) / np.tile(volumes, (1, 3)), (self.N_tet, 4, 3), order='F'), (0, 2, 1))
966
+
967
+ grad_phi = np.zeros((self.N_tet, 3))
968
+ # calculate gradient at barycenters of tetrahedra
969
+ for j in range(4):
970
+ grad_phi = grad_phi + dlambda[:, :, j] * np.tile(phi[self.tetrahedra[:, j]], (1, 3))
971
+
972
+ return grad_phi
973
+
974
+ def calc_E(self, grad_phi, omegaA):
975
+ """
976
+ Calculate electric field with gradient of electric potential and omega-scaled magnetic vector potential A.
977
+
978
+ .. math:: \mathbf{E}=-\\nabla\\varphi-\omega\mathbf{A}
979
+
980
+ Parameters
981
+ ----------
982
+ grad_phi : np.ndarray of float
983
+ (N_tet, 3) Gradient of Scalar DOF in tetrahedra center.
984
+ omegaA : np.ndarray of float
985
+ (N_tet, 3) Magnetic vector potential in tetrahedra center (scaled with angular frequency omega).
986
+
987
+ Returns
988
+ -------
989
+ E : np.ndarray of float
990
+ (N_tet, 3) Electric field in tetrahedra center.
991
+ """
992
+ e = -grad_phi - omegaA
993
+ return e
994
+
995
+ def calc_J(self, E, sigma):
996
+ """
997
+ Calculate current density J. The conductivity sigma is a list of np.arrays containing conductivities of
998
+ regions (scalar and/or tensor).
999
+
1000
+ .. math::
1001
+ \mathbf{J} = [\sigma]\mathbf{E}
1002
+
1003
+ Parameters
1004
+ ----------
1005
+ E : np.ndarray of float
1006
+ (N_tet, 3) Electric field in tetrahedra center.
1007
+ sigma : list of np.ndarray of float
1008
+ [N_regions](3, 3) Conductivities of regions (scalar and/or tensor).
1009
+
1010
+ Returns
1011
+ -------
1012
+ E : np.ndarray of float
1013
+ (N_tet, 3) Electric field in tetrahedra center.
1014
+ """
1015
+ j = np.zeros((E.shape[0], 3))
1016
+
1017
+ for i in range(self.N_region):
1018
+ tet_bool_idx = self.tetrahedra_regions == self.region[i]
1019
+ j[tet_bool_idx[:, 0], :] = np.dot(
1020
+ sigma[i], E[tet_bool_idx[:, 0], :].T).T
1021
+ return j
1022
+
1023
+ def calc_surface_adjacent_tetrahedra_idx_list(self, fname):
1024
+ """
1025
+ Determine the indices of the tetrahedra touching the surfaces and save the indices into a .txt file specified
1026
+ with fname.
1027
+
1028
+ Parameters
1029
+ ----------
1030
+ fname : str
1031
+ Filename of output .txt file.
1032
+
1033
+ Returns
1034
+ -------
1035
+ <File> : .txt file
1036
+ Element indices of the tetrahedra touching the surfaces (outer-most elements)
1037
+ """
1038
+ # determine indices of the 2 adjacent tetrahedra with common face on
1039
+ # surface
1040
+ # P1_idx = np.zeros((self.N_tet, 1), dtype=bool)
1041
+ # p2_idx = np.zeros((self.N_tet, 1), dtype=bool)
1042
+ # p3_idx = np.zeros((self.N_tet, 1), dtype=bool)
1043
+ tet_idx_pos = np.zeros((self.N_tri, 1)).astype(int)
1044
+ tet_idx_neg = np.zeros((self.N_tri, 1)).astype(int)
1045
+
1046
+ start = time.time()
1047
+
1048
+ tetrahedra0 = self.tetrahedra[:, 0]
1049
+ tetrahedra1 = self.tetrahedra[:, 1]
1050
+ tetrahedra2 = self.tetrahedra[:, 2]
1051
+ tetrahedra3 = self.tetrahedra[:, 3]
1052
+
1053
+ for i in range(self.N_tri):
1054
+
1055
+ if not (i % 100) and i > 0:
1056
+ stop = time.time()
1057
+ print(('Tri: {:d}/{:d} [{} sec]\n'.format(i, self.N_tri, stop - start)))
1058
+ start = time.time()
1059
+
1060
+ triangle = set(self.triangles[i, :])
1061
+
1062
+ triangle0 = self.triangles[i, 0]
1063
+ triangle1 = self.triangles[i, 1]
1064
+ triangle2 = self.triangles[i, 2]
1065
+
1066
+ p1_idx = (tetrahedra0 == triangle0) | (tetrahedra1 == triangle0) | (
1067
+ tetrahedra2 == triangle0) | (tetrahedra3 == triangle0)
1068
+ p2_idx = (tetrahedra0 == triangle1) | (tetrahedra1 == triangle1) | (
1069
+ tetrahedra2 == triangle1) | (tetrahedra3 == triangle1)
1070
+ p3_idx = (tetrahedra0 == triangle2) | (tetrahedra1 == triangle2) | (
1071
+ tetrahedra2 == triangle2) | (tetrahedra3 == triangle2)
1072
+
1073
+ tet_bool_idx = p1_idx & p2_idx & p3_idx
1074
+ tet_idx = np.where(tet_bool_idx)[0][:]
1075
+
1076
+ # get 4th (test) point of e.g. first tetrahedron which is not in
1077
+ # plane
1078
+ p4_idx = list(set(self.tetrahedra[tet_idx[0], :]) - triangle)
1079
+
1080
+ # calculate projection of the line between:
1081
+ # center of triangle -> 4th point
1082
+ # and
1083
+ # normal of the triangle
1084
+ c = np.dot(
1085
+ self.points[p4_idx, :] - self.triangles_center[i, :], self.triangles_normal[i, :])
1086
+
1087
+ # positive projection: normal points to the 4th (test) point of first tetrahedron
1088
+ # and first tetrahedron is on "positive" side
1089
+
1090
+ # outermost surface (has only one adjacent tetrahedron)
1091
+ if len(tet_idx) == 1:
1092
+ if c > 0:
1093
+ tet_idx_pos[i] = tet_idx[0]
1094
+ tet_idx_neg[i] = -1
1095
+
1096
+ else:
1097
+ tet_idx_pos[i] = -1
1098
+ tet_idx_neg[i] = tet_idx[0]
1099
+
1100
+ # inner surfaces have 2 adjacent tetrahedra
1101
+ else:
1102
+ if c > 0:
1103
+ tet_idx_pos[i] = tet_idx[0]
1104
+ tet_idx_neg[i] = tet_idx[1]
1105
+ else:
1106
+ tet_idx_pos[i] = tet_idx[1]
1107
+ tet_idx_neg[i] = tet_idx[0]
1108
+
1109
+ # save the indices of the tetrahedra sharing the surfaces (negative,
1110
+ # i.e. bottom side first)
1111
+ self.tetrahedra_triangle_surface_idx = np.hstack(
1112
+ [tet_idx_neg, tet_idx_pos])
1113
+ f = open(fname, 'w')
1114
+ np.savetxt(f, self.tetrahedra_triangle_surface_idx, '%d')
1115
+ f.close()
1116
+
1117
+ def calc_E_normal_tangential_surface(self, E, fname):
1118
+ """
1119
+ Calculate normal and tangential component of electric field on given surfaces of mesh instance.
1120
+
1121
+ Parameters
1122
+ ----------
1123
+ E : np.ndarray of float [N_tri x 3]
1124
+ Electric field data on surfaces
1125
+ fname : str
1126
+ Filename of the .txt file containing the tetrahedra indices, which are adjacent to the surface triangles
1127
+ generated by the method "calc_surface_adjacent_tetrahedra_idx_list(self, fname)"
1128
+
1129
+ Returns
1130
+ -------
1131
+ En_pos : np.ndarray of float [N_tri x 3]
1132
+ Normal component of electric field of top side (outside) of surface
1133
+ En_neg : np.ndarray of float [N_tri x 3]
1134
+ Normal component of electric field of bottom side (inside) of surface
1135
+ n : np.ndarray of float [N_tri x 3]
1136
+ Normal vector
1137
+ Et : np.ndarray of float [N_tri x 3]
1138
+ Tangential component of electric field lying in surface
1139
+ t : np.ndarray of float [N_tri x 3]
1140
+ Tangential vector
1141
+ """
1142
+
1143
+ n = self.triangles_normal
1144
+ en_pos = np.zeros((self.N_tri, 1))
1145
+ en_neg = np.zeros((self.N_tri, 1))
1146
+ et = np.zeros((self.N_tri, 1))
1147
+ t = np.zeros((self.N_tri, 3))
1148
+ self.tetrahedra_triangle_surface_idx = np.loadtxt(fname).astype(int)
1149
+
1150
+ for i in range(self.N_tri):
1151
+ en_neg[i, 0] = np.dot(
1152
+ E[self.tetrahedra_triangle_surface_idx[i, 0], :], n[i, :])
1153
+
1154
+ if self.tetrahedra_triangle_surface_idx[i, 1] > -1:
1155
+ en_pos[i, 0] = np.dot(
1156
+ E[self.tetrahedra_triangle_surface_idx[i, 1], :], n[i, :])
1157
+ else:
1158
+ en_pos[i, 0] = np.nan
1159
+
1160
+ t[i, :] = E[self.tetrahedra_triangle_surface_idx[i, 0], :] - \
1161
+ 1.0 * en_neg[i, 0] * n[i, :]
1162
+ et[i, 0] = np.linalg.norm(t[i, :])
1163
+ t[i, :] = t[i, :] / et[i, 0] if et[i, 0] > 0 else np.zeros(3)
1164
+
1165
+ return en_pos, en_neg, n, et, t
1166
+
1167
+ def get_faces(self, tetrahedra_indexes=None):
1168
+ """
1169
+ Creates a list of nodes in each face and a list of faces in each tetrahedra.
1170
+
1171
+ Parameters
1172
+ ----------
1173
+ tetrahedra_indexes : np.ndarray
1174
+ Indices of the tetrehedra where the faces are to be determined (default: all tetrahedra)
1175
+
1176
+ Returns
1177
+ -------
1178
+ faces : np.ndarray
1179
+ List of nodes in faces, in arbitrary order
1180
+ th_faces : np.ndarray
1181
+ List of faces in each tetrahedra, starts at 0, order=((0, 2, 1), (0, 1, 3), (0, 3, 2), (1, 2, 3))
1182
+ face_adjacency_list : np.ndarray
1183
+ List of tetrahedron adjacent to each face, filled with -1 if a face is in a
1184
+ single tetrahedron. Not in the normal element ordering, but only in the order
1185
+ the tetrahedra are presented
1186
+ """
1187
+
1188
+ if tetrahedra_indexes is None:
1189
+ tetrahedra_indexes = np.arange(self.tetrahedra.shape[0])
1190
+ # th = self[tetrahedra_indexes]
1191
+ th = self.tetrahedra[tetrahedra_indexes, :]
1192
+ faces = th[:, [[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]]]
1193
+ faces = faces.reshape(-1, 3)
1194
+ hash_array = np.array([hash(f.tobytes()) for f in np.sort(faces, axis=1)])
1195
+ unique, idx, inv, count = np.unique(hash_array, return_index=True,
1196
+ return_inverse=True, return_counts=True)
1197
+ faces = faces[idx]
1198
+ face_adjacency_list = -np.ones((len(unique), 2), dtype=int)
1199
+ face_adjacency_list[:, 0] = idx // 4
1200
+
1201
+ # if np.any(count > 2):
1202
+ # raise ValueError('Invalid Mesh: Found a face with more than 2 adjacent'
1203
+ # ' tetrahedra!')
1204
+
1205
+ # Remove the faces already seen from consideration
1206
+ # Second round in order to make adjacency list
1207
+ # create a new array with a mask in the elements already seen
1208
+ mask = unique[-1] + 1
1209
+ hash_array_masked = np.copy(hash_array)
1210
+ hash_array_masked[idx] = mask
1211
+ # make another array, where we delete the elements we have already seen
1212
+ hash_array_reduced = np.delete(hash_array, idx)
1213
+ # Finds where each element of the second array is in the first array
1214
+ # (https://stackoverflow.com/a/8251668)
1215
+ hash_array_masked_sort = hash_array_masked.argsort()
1216
+ hash_array_repeated_pos = hash_array_masked_sort[
1217
+ np.searchsorted(hash_array_masked[hash_array_masked_sort], hash_array_reduced)]
1218
+ # Now find the index of the face corresponding to each element in the
1219
+ # hash_array_reduced
1220
+ faces_repeated = np.searchsorted(unique, hash_array_reduced)
1221
+ # Finally, fill out the second column in the adjacency list
1222
+ face_adjacency_list[faces_repeated, 1] = hash_array_repeated_pos // 4
1223
+
1224
+ return faces, inv.reshape(-1, 4), face_adjacency_list
1225
+
1226
+
1227
+ class Mesh:
1228
+ """"
1229
+ Mesh class to initialize default attributes.
1230
+ """
1231
+
1232
+ def __init__(self, mesh_name, subject_id, subject_folder):
1233
+ self.subject_id = subject_id
1234
+ self.subject_folder = subject_folder
1235
+ self.name = mesh_name
1236
+ self.info = None
1237
+ self.approach = None # 'mri2mesh', 'headreco', 'charm'
1238
+ self.mri_idx = None
1239
+
1240
+ # default parameters
1241
+ self.mesh_folder = os.path.join(subject_folder, 'mesh', mesh_name)
1242
+ self.fn_mesh_msh = os.path.join(self.mesh_folder, f"{subject_id}.msh")
1243
+ self.fn_mesh_hdf5 = os.path.join(self.mesh_folder, f"{subject_id}.hdf5")
1244
+ self.fn_tensor_vn = f"d2c_{subject_id}{os.sep}dti_results_T1space{os.sep}DTI_conf_tensor.nii.gz"
1245
+ self.fn_mri_conform = f"T1.nii.gz"
1246
+ self.fn_lh_midlayer = f"fs_{subject_id}{os.sep}surf{os.sep}lh.central"
1247
+ self.fn_rh_midlayer = f"fs_{subject_id}{os.sep}surf{os.sep}rh.central"
1248
+ self.vertex_density = 1.0 # headreco
1249
+ self.numvertices = 100000 # mri2mesh
1250
+
1251
+ # refinement parameters
1252
+ self.center = None
1253
+ self.radius = None
1254
+ self.element_size = None
1255
+ self.refine_domains = None
1256
+ self.smooth_domains = None
1257
+
1258
+ self.fn_lh_wm = None
1259
+ self.fn_rh_wm = None
1260
+ self.fn_lh_gm = None
1261
+ self.fn_rh_gm = None
1262
+ self.fn_lh_gm_curv = None
1263
+ self.fn_rh_gm_curv = None
1264
+
1265
+ # charm meshes
1266
+ self.smooth_skin = None
1267
+ self.refinement_roi = None
1268
+ self.refinemement_element_size = None
1269
+
1270
+ def fill_defaults(self, approach):
1271
+ """
1272
+ Initializes attributes for a headreco mesh.
1273
+
1274
+ Parameters
1275
+ ----------
1276
+ approach: str
1277
+ 'headreco'
1278
+ 'mri2mesh'
1279
+ 'charm'
1280
+ """
1281
+ self.approach = approach
1282
+ if approach == 'headreco':
1283
+ self.fn_mesh_msh = os.path.join(self.mesh_folder, f"{self.subject_id}.msh")
1284
+ self.fn_mesh_hdf5 = os.path.join(self.mesh_folder, f"{self.subject_id}.hdf5")
1285
+ self.fn_tensor_vn = f"d2c_{self.subject_id}{os.sep}dti_results_T1space{os.sep}DTI_conf_tensor.nii.gz"
1286
+ self.fn_mri_conform = f"{self.subject_id}_T1fs_conform.nii.gz"
1287
+ self.fn_lh_midlayer = f"fs_{self.subject_id}{os.sep}surf{os.sep}lh.central"
1288
+ self.fn_rh_midlayer = f"fs_{self.subject_id}{os.sep}surf{os.sep}rh.central"
1289
+
1290
+ elif approach == 'mri2mesh':
1291
+ self.fn_mesh_msh = os.path.join(self.mesh_folder, f"{self.subject_id}.msh")
1292
+ self.fn_mesh_hdf5 = os.path.join(self.mesh_folder, f"{self.subject_id}.hdf5")
1293
+ self.fn_tensor_vn = f"d2c_{self.subject_id}{os.sep}dti_results_T1space{os.sep}DTI_conf_tensor.nii.gz"
1294
+ self.fn_mri_conform = f"{self.subject_id}_T1fs_conform.nii.gz"
1295
+ self.fn_lh_gm_curv = f"fs_{self.subject_id}{os.sep}surf{os.sep}lh.curv.pial"
1296
+ self.fn_rh_gm_curv = f"fs_{self.subject_id}{os.sep}surf{os.sep}rh.curv.pial"
1297
+
1298
+ elif approach == 'charm':
1299
+ self.fn_mesh_msh = os.path.join(self.mesh_folder, f"{self.subject_id}.msh")
1300
+ self.fn_mesh_hdf5 = os.path.join(self.mesh_folder, f"{self.subject_id}.hdf5")
1301
+ self.fn_tensor_vn = f"d2c_{self.subject_id}{os.sep}dti_results_T1space{os.sep}DTI_conf_tensor.nii.gz"
1302
+ self.fn_mri_conform = f"{self.subject_id}_T1.nii.gz"
1303
+ self.fn_lh_midlayer = f"m2m_{self.subject_id}{os.sep}surfaces{os.sep}lh.central.gii"
1304
+ self.fn_rh_midlayer = f"m2m_{self.subject_id}{os.sep}surfaces{os.sep}rh.central.gii"
1305
+ self.use_fs = False
1306
+
1307
+ else:
1308
+ raise NotImplementedError(f"Approach {approach} not implemented.")
1309
+
1310
+ def write_to_hdf5(self, fn_hdf5, check_file_exist=False, verbose=False):
1311
+ """
1312
+ Write this mesh' attributes to .hdf5 file.
1313
+
1314
+ Parameters
1315
+ ----------
1316
+ fn_hdf5 : str
1317
+ check_file_exist : bool
1318
+ Check if provided filenames exist, warn if not.
1319
+ verbose : bool
1320
+ Print self information
1321
+ """
1322
+
1323
+ pynibs.write_dict_to_hdf5(fn_hdf5=fn_hdf5, data=self.__dict__, folder=f"mesh/{self.name}",
1324
+ check_file_exist=check_file_exist)
1325
+ if verbose:
1326
+ self.print()
1327
+
1328
+ def print(self):
1329
+ """
1330
+ Print self information.
1331
+ """
1332
+ n_left, n_right = int(32 - np.floor((len(self.name) + 10) / 2)), int(32 - np.ceil((len(self.name) + 10) / 2))
1333
+ n_left, n_right = np.max(n_left, 0), np.max(n_right, 0)
1334
+ print(" " + "=" * n_left + f" Mesh {self.name}: " + "=" * n_right)
1335
+ print("\t" + json.dumps(self.__dict__, sort_keys=False, indent="\t", ))
1336
+ print(" " + "=" * 64 + "\n")
1337
+
1338
+
1339
+ class ROI:
1340
+ """
1341
+ Region of interest class to initialize default attributes.
1342
+ """
1343
+
1344
+ def __init__(self, subject_id, roi_name, mesh_name):
1345
+ self.subject_id = subject_id
1346
+ self.name = roi_name
1347
+ self.mesh_name = mesh_name
1348
+ self.type = None # 'surface' or 'volume'
1349
+ self.info = None
1350
+ self.template = None # None, 'MNI', 'fsaverage', 'subject'
1351
+ self.gm_surf_fname = None
1352
+ self.wm_surf_fname = None
1353
+ self.midlayer_surf_fname = None
1354
+ self.delta = 0.5
1355
+ self.refine = False
1356
+ self.X_ROI = None
1357
+ self.Y_ROI = None
1358
+ self.Z_ROI = None
1359
+ self.center = None
1360
+ self.radius = None
1361
+ self.layer = 3
1362
+ self.fn_mask = None
1363
+ self.fn_mask_avg = None
1364
+ self.hemisphere = None
1365
+ self.midlayer_surf_fname = None
1366
+ self.tri_center_coord_mid = None
1367
+
1368
+ def write_to_hdf5(self, fn_hdf5, check_file_exist=False, verbose=False):
1369
+ """
1370
+ Write this mesh' attributes to .hdf5 file.
1371
+
1372
+ Parameters
1373
+ ----------
1374
+ fn_hdf5 : str
1375
+ check_file_exist : bool
1376
+ Check if provided filenames exist, warn if not.
1377
+ verbose : bool
1378
+ Print self information
1379
+ """
1380
+
1381
+ pynibs.write_dict_to_hdf5(fn_hdf5=fn_hdf5, data=self.__dict__, folder=f"roi/{self.mesh_name}/{self.name}",
1382
+ check_file_exist=check_file_exist)
1383
+ if verbose:
1384
+ self.print()
1385
+
1386
+ def print(self):
1387
+ """
1388
+ Print self information.
1389
+ """
1390
+ n_left, n_right = int(32 - np.floor((len(self.name) + 10) / 2)), int(32 - np.ceil((len(self.name) + 10) / 2))
1391
+ n_left, n_right = np.max(n_left, 0), np.max(n_right, 0)
1392
+ print(" " + "=" * n_left + f" ROI {self.name}: " + "=" * n_right)
1393
+ print("\t" + json.dumps(self.__dict__, sort_keys=False, indent="\t", ))
1394
+ print(" " + "=" * 64 + "\n")