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
|
@@ -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")
|